From 562b9c52960d262c3b200d42a0daf753003364fc Mon Sep 17 00:00:00 2001 From: longvantruong Date: Mon, 28 Apr 2025 14:32:17 +0700 Subject: [PATCH] chore: merge main in to mobile-sdk-custom (#5523) Co-authored-by: Matti Nannt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Co-authored-by: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Co-authored-by: pandeymangg Co-authored-by: Piyush Gupta Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Co-authored-by: Victor Santos Co-authored-by: Dhruwang Co-authored-by: Vijay Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes Co-authored-by: Peter Pesti-Varga Co-authored-by: Piyush Jain <122745947+d3vb0ox@users.noreply.github.com> Co-authored-by: Jakob Schott <154420406+jakobsitory@users.noreply.github.com> Co-authored-by: Gulshan Kumar Co-authored-by: Harsh Bhat <90265455+harshsbhat@users.noreply.github.com> --- .env.example | 5 + .github/copilot-instructions.md | 26 + .../release-docker-github-experimental.yml | 2 - .github/workflows/release-docker-github.yml | 2 - .vscode/settings.json | 4 +- apps/demo/package.json | 4 +- apps/web/.eslintrc.js | 17 + apps/web/.gitignore | 2 +- apps/web/Dockerfile | 53 +- .../components/ConnectWithFormbricks.tsx | 2 +- .../[environmentId]/connect/page.tsx | 8 +- .../environments/[environmentId]/layout.tsx | 2 +- .../[environmentId]/xm-templates/lib/utils.ts | 2 +- .../xm-templates/lib/xm-templates.ts | 236 +- .../[environmentId]/xm-templates/page.tsx | 8 +- .../app/(app)/(onboarding)/lib/onboarding.ts | 4 +- .../landing/components/landing-sidebar.tsx | 6 +- .../[organizationId]/landing/layout.tsx | 6 +- .../[organizationId]/landing/page.tsx | 4 +- .../[organizationId]/layout.test.tsx | 26 +- .../organizations/[organizationId]/layout.tsx | 8 +- .../projects/new/channel/page.tsx | 4 +- .../[organizationId]/projects/new/layout.tsx | 8 +- .../projects/new/mode/page.tsx | 4 +- .../settings/components/ProjectSettings.tsx | 4 +- .../projects/new/settings/page.tsx | 6 +- .../[environmentId]/layout.test.tsx | 14 +- .../environments/[environmentId]/layout.tsx | 2 +- apps/web/app/(app)/components/LoadingCard.tsx | 2 +- .../environments/[environmentId]/actions.ts | 6 +- .../[environmentId]/actions/actions.ts | 6 +- .../actions/components/ActionActivityTab.tsx | 4 +- .../actions/components/ActionRowData.tsx | 4 +- .../[environmentId]/actions/page.tsx | 6 +- .../components/EnvironmentLayout.tsx | 24 +- .../components/EnvironmentStorageHandler.tsx | 2 +- .../components/EnvironmentSwitch.tsx | 2 +- .../components/MainNavigation.tsx | 6 +- .../components/NavigationLink.tsx | 2 +- .../components/PosthogIdentify.test.tsx | 10 +- .../components/TopControlButtons.tsx | 2 +- .../components/WidgetStatusIndicator.tsx | 4 +- .../[environmentId]/integrations/actions.ts | 2 +- .../components/AddIntegrationModal.tsx | 4 +- .../components/ManageIntegration.test.tsx | 151 + .../airtable/components/ManageIntegration.tsx | 14 +- .../integrations/airtable/page.tsx | 8 +- .../integrations/google-sheets/actions.ts | 2 +- .../components/AddIntegrationModal.tsx | 6 +- .../components/ManageIntegration.test.tsx | 162 + .../components/ManageIntegration.tsx | 19 +- .../integrations/google-sheets/page.tsx | 16 +- .../integrations/lib/surveys.ts | 10 +- .../integrations/lib/webhook.ts | 4 +- .../notion/components/AddIntegrationModal.tsx | 6 +- .../components/ManageIntegration.test.tsx | 91 + .../notion/components/ManageIntegration.tsx | 20 +- .../integrations/notion/constants.ts | 2 + .../integrations/notion/page.tsx | 20 +- .../[environmentId]/integrations/page.tsx | 2 +- .../integrations/slack/actions.ts | 2 +- .../components/AddChannelMappingModal.tsx | 4 +- .../components/ManageIntegration.test.tsx | 158 + .../slack/components/ManageIntegration.tsx | 22 +- .../integrations/slack/page.tsx | 6 +- .../[environmentId]/layout.test.tsx | 20 +- .../environments/[environmentId]/layout.tsx | 4 +- .../environments/[environmentId]/page.tsx | 4 +- .../settings/(account)/layout.tsx | 4 +- .../(account)/notifications/actions.ts | 2 +- .../settings/(account)/notifications/page.tsx | 2 +- .../settings/(account)/profile/actions.ts | 6 +- .../components/EditProfileDetailsForm.tsx | 2 +- .../settings/(account)/profile/page.tsx | 6 +- .../(organization)/api-keys/loading.tsx | 2 +- .../(organization)/billing/loading.tsx | 2 +- .../components/OrganizationSettingsNavbar.tsx | 5 +- .../(organization)/enterprise/loading.tsx | 2 +- .../(organization)/enterprise/page.tsx | 4 +- .../(organization)/general/actions.ts | 2 +- .../general/components/DeleteOrganization.tsx | 2 +- .../components/EditOrganizationNameForm.tsx | 2 +- .../(organization)/general/loading.tsx | 2 +- .../(organization)/general/page.test.tsx | 14 +- .../settings/(organization)/general/page.tsx | 4 +- .../settings/(organization)/layout.tsx | 4 +- .../settings/components/SettingsCard.tsx | 4 +- .../surveys/[surveyId]/(analysis)/actions.ts | 2 +- .../components/SurveyAnalysisNavigation.tsx | 2 +- .../surveys/[surveyId]/(analysis)/layout.tsx | 4 +- .../responses/components/ResponsePage.tsx | 2 +- .../components/ResponseTableCell.test.tsx | 165 + .../components/ResponseTableCell.tsx | 10 +- .../components/ResponseTableColumns.tsx | 16 +- .../[surveyId]/(analysis)/responses/page.tsx | 18 +- .../[surveyId]/(analysis)/summary/actions.ts | 2 +- .../summary/components/AddressSummary.tsx | 4 +- .../components/ConsentSummary.test.tsx | 80 + .../summary/components/ConsentSummary.tsx | 8 +- .../summary/components/ContactInfoSummary.tsx | 4 +- .../components/DateQuestionSummary.tsx | 6 +- .../summary/components/FileUploadSummary.tsx | 8 +- .../components/HiddenFieldsSummary.tsx | 8 +- .../components/MatrixQuestionSummary.test.tsx | 47 + .../components/MatrixQuestionSummary.tsx | 8 +- .../components/MultipleChoiceSummary.test.tsx | 275 + .../components/MultipleChoiceSummary.tsx | 87 +- .../summary/components/NPSSummary.test.tsx | 60 + .../summary/components/NPSSummary.tsx | 13 +- .../summary/components/OpenTextSummary.tsx | 4 +- .../components/PictureChoiceSummary.test.tsx | 91 + .../components/PictureChoiceSummary.tsx | 8 +- .../components/QuestionSummaryHeader.tsx | 29 +- .../summary/components/RatingSummary.test.tsx | 87 + .../summary/components/RatingSummary.tsx | 8 +- .../summary/components/SummaryDropOffs.tsx | 30 +- .../summary/components/SummaryList.tsx | 2 +- .../components/SummaryMetadata.test.tsx | 135 + .../summary/components/SummaryMetadata.tsx | 17 +- .../summary/components/SummaryPage.tsx | 4 +- .../components/shareEmbedModal/EmbedView.tsx | 2 +- .../tests/SurveyAnalysisCTA.test.tsx | 8 +- .../(analysis)/summary/lib/emailTemplate.tsx | 8 +- .../(analysis)/summary/lib/insights.ts | 6 +- .../(analysis)/summary/lib/surveySummary.ts | 24 +- .../[surveyId]/(analysis)/summary/page.tsx | 20 +- .../surveys/[surveyId]/actions.ts | 8 +- .../[surveyId]/components/CustomFilter.tsx | 8 +- .../QuestionFilterComboBox.test.tsx | 88 + .../components/QuestionFilterComboBox.tsx | 105 +- .../components/QuestionsComboBox.test.tsx | 55 + .../components/QuestionsComboBox.tsx | 22 +- apps/web/app/(app)/layout.test.tsx | 12 +- apps/web/app/(app)/layout.tsx | 18 +- apps/web/app/(auth)/layout.test.tsx | 6 +- .../organizations/[organizationId]/route.ts | 10 +- .../(redirects)/projects/[projectId]/route.ts | 6 +- apps/web/app/ClientEnvironmentRedirect.tsx | 2 +- apps/web/app/[shortUrlId]/page.tsx | 2 +- .../api/(internal)/insights/lib/document.ts | 4 +- .../api/(internal)/insights/lib/insights.ts | 8 +- .../api/(internal)/insights/lib/utils.test.ts | 20 +- .../app/api/(internal)/insights/lib/utils.ts | 10 +- apps/web/app/api/(internal)/insights/route.ts | 2 +- .../api/(internal)/pipeline/lib/documents.ts | 4 +- .../pipeline/lib/handleIntegrations.ts | 33 +- .../pipeline/lib/survey-follow-up.ts | 50 +- .../tests/__mocks__/survey-follow-up.mock.ts | 267 + .../lib/tests/survey-follow-up.test.ts | 235 + apps/web/app/api/(internal)/pipeline/route.ts | 21 +- apps/web/app/api/cron/ping/route.ts | 4 +- apps/web/app/api/cron/survey-status/route.ts | 4 +- .../lib/notificationResponse.ts | 6 +- apps/web/app/api/cron/weekly-summary/route.ts | 4 +- .../app/api/google-sheet/callback/route.ts | 6 +- apps/web/app/api/google-sheet/route.ts | 12 +- apps/web/app/api/v1/auth.test.ts | 20 +- .../app/sync/[userId]/route.ts | 18 +- .../[environmentId]/app/sync/lib/contact.ts | 2 +- .../[environmentId]/app/sync/lib/survey.ts | 18 +- .../[environmentId]/app/sync/lib/utils.ts | 2 +- .../[environmentId]/displays/lib/contact.ts | 2 +- .../[environmentId]/displays/lib/display.ts | 4 +- .../client/[environmentId]/displays/route.ts | 2 +- .../environment/lib/actionClass.ts | 6 +- .../environment/lib/environmentState.ts | 22 +- .../environment/lib/project.ts | 6 +- .../[environmentId]/environment/lib/survey.ts | 6 +- .../[environmentId]/environment/route.ts | 2 +- .../responses/[responseId]/route.ts | 4 +- .../[environmentId]/responses/lib/contact.ts | 2 +- .../[environmentId]/responses/lib/response.ts | 20 +- .../client/[environmentId]/responses/route.ts | 4 +- .../storage/lib/uploadPrivateFile.ts | 2 +- .../[environmentId]/storage/local/route.ts | 10 +- .../client/[environmentId]/storage/route.ts | 4 +- .../integrations/airtable/callback/route.ts | 8 +- .../app/api/v1/integrations/airtable/route.ts | 4 +- .../v1/integrations/airtable/tables/route.ts | 6 +- .../v1/integrations/notion/callback/route.ts | 8 +- .../app/api/v1/integrations/notion/route.ts | 10 +- .../v1/integrations/slack/callback/route.ts | 4 +- .../app/api/v1/integrations/slack/route.ts | 4 +- .../action-classes/[actionClassId]/route.ts | 2 +- .../action-classes/lib/action-classes.test.ts | 8 +- .../action-classes/lib/action-classes.ts | 6 +- .../api/v1/management/action-classes/route.ts | 2 +- .../responses/[responseId]/route.ts | 4 +- .../v1/management/responses/lib/contact.ts | 2 +- .../v1/management/responses/lib/response.ts | 26 +- .../app/api/v1/management/responses/route.ts | 4 +- .../v1/management/storage/lib/getSignedUrl.ts | 2 +- .../api/v1/management/storage/local/route.ts | 8 +- .../app/api/v1/management/storage/route.ts | 2 +- .../surveys/[surveyId]/lib/surveys.ts | 8 +- .../v1/management/surveys/[surveyId]/route.ts | 4 +- .../surveys/[surveyId]/singleUseIds/route.ts | 10 +- .../api/v1/management/surveys/lib/surveys.ts | 10 +- .../app/api/v1/management/surveys/route.ts | 4 +- .../v1/webhooks/[webhookId]/lib/webhook.ts | 4 +- apps/web/app/api/v1/webhooks/lib/webhook.ts | 6 +- .../[environmentId]/displays/lib/contact.ts | 2 +- .../[environmentId]/displays/lib/display.ts | 4 +- .../client/[environmentId]/displays/route.ts | 2 +- .../[environmentId]/responses/lib/contact.ts | 2 +- .../[environmentId]/responses/lib/response.ts | 20 +- .../client/[environmentId]/responses/route.ts | 4 +- .../[contactAttributeKeyId]/route.ts | 7 + .../contact-attribute-keys/route.ts | 3 + apps/web/app/error.test.tsx | 72 + apps/web/app/error.tsx | 7 +- apps/web/app/global-error.test.tsx | 41 + apps/web/app/global-error.tsx | 22 + apps/web/app/intercom/IntercomClient.test.tsx | 18 +- .../intercom/IntercomClientWrapper.test.tsx | 8 +- .../app/intercom/IntercomClientWrapper.tsx | 2 +- apps/web/app/layout.test.tsx | 6 +- apps/web/app/layout.tsx | 4 +- apps/web/app/lib/formbricks.ts | 2 +- apps/web/app/lib/pipelines.test.ts | 6 +- apps/web/app/lib/pipelines.ts | 2 +- apps/web/app/lib/singleUseSurveys.test.ts | 43 +- apps/web/app/lib/singleUseSurveys.ts | 30 +- apps/web/app/lib/survey-builder.test.ts | 612 ++ apps/web/app/lib/survey-builder.ts | 414 + apps/web/app/lib/templates.ts | 7236 +++++------------ apps/web/app/middleware/bucket.ts | 2 +- apps/web/app/middleware/rate-limit.ts | 2 +- apps/web/app/page.tsx | 12 +- apps/web/app/sentry/SentryProvider.test.tsx | 31 +- apps/web/app/sentry/SentryProvider.tsx | 5 +- .../app/setup/organization/create/actions.ts | 6 +- .../(analysis)/responses/page.tsx | 14 +- .../[sharingKey]/(analysis)/summary/page.tsx | 10 +- apps/web/app/share/[sharingKey]/actions.ts | 10 +- .../[fileName]/lib/delete-file.ts | 4 +- .../[accessType]/[fileName]/lib/get-file.ts | 4 +- .../[accessType]/[fileName]/route.ts | 2 +- apps/web/instrumentation-node.ts | 2 +- apps/web/instrumentation.ts | 9 +- .../web}/lib/__mocks__/database.ts | 0 {packages => apps/web}/lib/account/service.ts | 0 {packages => apps/web}/lib/account/utils.ts | 0 .../web}/lib/actionClass/auth.ts | 2 +- .../web}/lib/actionClass/cache.ts | 0 .../web}/lib/actionClass/service.ts | 2 +- {packages => apps/web}/lib/aiModels.ts | 0 .../web}/lib/airtable/service.ts | 0 {packages => apps/web}/lib/auth.ts | 0 {packages => apps/web}/lib/cache.ts | 0 {packages => apps/web}/lib/cache/segment.ts | 0 {packages => apps/web}/lib/cn.ts | 0 {packages => apps/web}/lib/constants.ts | 1 - apps/web/lib/crypto.test.ts | 59 + apps/web/lib/crypto.ts | 130 + {packages => apps/web}/lib/display/cache.ts | 0 {packages => apps/web}/lib/display/service.ts | 0 .../lib/display/tests/__mocks__/data.mock.ts | 0 .../web}/lib/display/tests/display.test.ts | 20 +- {packages => apps/web}/lib/env.d.ts | 0 {packages => apps/web}/lib/env.ts | 2 - .../web}/lib/environment/auth.ts | 0 .../web}/lib/environment/cache.ts | 0 .../web}/lib/environment/service.ts | 0 {packages => apps/web}/lib/fetcher.ts | 0 {packages => apps/web}/lib/getSurveyUrl.ts | 0 .../web}/lib/googleSheet/service.ts | 14 +- {packages => apps/web}/lib/hashString.ts | 0 {packages => apps/web}/lib/i18n/i18n.mock.ts | 7 +- {packages => apps/web}/lib/i18n/i18n.test.ts | 10 +- .../web}/lib/i18n/reverseTranslation.ts | 2 +- apps/web/lib/i18n/utils.ts | 195 + .../web}/lib/instance/service.ts | 6 +- .../web}/lib/integration/auth.ts | 0 .../web}/lib/integration/cache.ts | 0 .../web}/lib/integration/service.ts | 0 {packages => apps/web}/lib/jwt.ts | 4 +- .../web}/lib/language/service.ts | 0 .../lib/language/tests/__mocks__/data.mock.ts | 0 apps/web/lib/language/tests/language.test.ts | 143 + {packages => apps/web}/lib/localStorage.ts | 0 {packages => apps/web}/lib/markdownIt.ts | 0 .../web}/lib/membership/cache.ts | 0 .../web}/lib/membership/hooks/actions.ts | 0 .../membership/hooks/useMembershipRole.tsx | 0 .../web}/lib/membership/service.ts | 0 .../web}/lib/membership/utils.ts | 0 apps/web/lib/messages/de-DE.json | 2845 +++++++ apps/web/lib/messages/en-US.json | 2845 +++++++ apps/web/lib/messages/fr-FR.json | 2845 +++++++ apps/web/lib/messages/pt-BR.json | 2845 +++++++ apps/web/lib/messages/pt-PT.json | 2845 +++++++ apps/web/lib/messages/zh-Hant-TW.json | 2845 +++++++ {packages => apps/web}/lib/notion/service.ts | 4 +- .../web}/lib/organization/auth.ts | 4 +- .../web}/lib/organization/cache.ts | 0 .../web}/lib/organization/service.ts | 24 +- .../web}/lib/pollyfills/structuredClone.ts | 0 {packages => apps/web}/lib/posthogServer.ts | 2 +- {packages => apps/web}/lib/project/cache.ts | 0 {packages => apps/web}/lib/project/service.ts | 2 +- {packages => apps/web}/lib/response/auth.ts | 2 +- {packages => apps/web}/lib/response/cache.ts | 0 .../web}/lib/response/service.ts | 4 +- .../lib/response/tests/__mocks__/data.mock.ts | 2 +- .../web}/lib/response/tests/constants.ts | 0 .../web}/lib/response/tests/response.test.ts | 66 +- {packages => apps/web}/lib/response/utils.ts | 36 +- .../web}/lib/responseNote/auth.ts | 2 +- .../web}/lib/responseNote/cache.ts | 0 .../web}/lib/responseNote/service.ts | 2 +- {packages => apps/web}/lib/responses.ts | 7 +- {packages => apps/web}/lib/shortUrl/cache.ts | 0 .../web}/lib/shortUrl/service.ts | 2 +- {packages => apps/web}/lib/slack/service.ts | 0 {packages => apps/web}/lib/storage/cache.ts | 0 {packages => apps/web}/lib/storage/service.ts | 1 + {packages => apps/web}/lib/storage/utils.ts | 0 .../web}/lib/styling/constants.ts | 0 {packages => apps/web}/lib/survey/auth.ts | 2 +- {packages => apps/web}/lib/survey/cache.ts | 0 {packages => apps/web}/lib/survey/service.ts | 12 +- .../lib/survey/tests/__mock__/survey.mock.ts | 0 .../web}/lib/survey/tests/survey.test.ts | 81 +- {packages => apps/web}/lib/survey/utils.ts | 0 .../web}/lib/surveyLogic/utils.ts | 2 +- {packages => apps/web}/lib/tag/auth.ts | 0 {packages => apps/web}/lib/tag/cache.ts | 0 {packages => apps/web}/lib/tag/service.ts | 2 +- .../web}/lib/tagOnResponse/auth.ts | 2 +- .../web}/lib/tagOnResponse/cache.ts | 0 .../web}/lib/tagOnResponse/service.ts | 2 +- {packages => apps/web}/lib/telemetry.ts | 2 +- {packages => apps/web}/lib/time.ts | 0 .../web}/lib/useDocumentVisibility.ts | 0 {packages => apps/web}/lib/user/cache.ts | 0 {packages => apps/web}/lib/user/service.ts | 4 +- .../web/lib/utils/action-client-middleware.ts | 2 +- apps/web/lib/utils/action-client.ts | 2 +- {packages => apps/web}/lib/utils/ai.ts | 2 +- apps/web/lib/utils/billing.test.ts | 176 + apps/web/lib/utils/billing.ts | 54 + {packages => apps/web}/lib/utils/colors.ts | 0 {packages => apps/web}/lib/utils/contact.ts | 0 {packages => apps/web}/lib/utils/datetime.ts | 0 {packages => apps/web}/lib/utils/email.ts | 0 .../web}/lib/utils/fileConversion.ts | 0 {packages => apps/web}/lib/utils/headers.ts | 0 .../web}/lib/utils/hooks/useClickOutside.ts | 0 .../lib/utils/hooks/useIntervalWhenFocused.ts | 0 .../web}/lib/utils/hooks/useSyncScroll.ts | 0 {packages => apps/web}/lib/utils/locale.ts | 2 +- {packages => apps/web}/lib/utils/promises.ts | 0 {packages => apps/web}/lib/utils/recall.ts | 4 +- apps/web/lib/utils/services.ts | 22 +- .../web}/lib/utils/singleUseSurveys.ts | 14 +- {packages => apps/web}/lib/utils/strings.ts | 0 {packages => apps/web}/lib/utils/styling.ts | 0 {packages => apps/web}/lib/utils/templates.ts | 4 +- {packages => apps/web}/lib/utils/url.ts | 0 {packages => apps/web}/lib/utils/validate.ts | 0 .../web}/lib/utils/videoUpload.ts | 0 apps/web/middleware.ts | 18 +- .../DeleteAccountModal/actions.test.ts | 74 + .../components/DeleteAccountModal/actions.ts | 4 +- .../DeleteAccountModal/index.test.tsx | 161 + .../components/DeleteAccountModal/index.tsx | 5 +- .../components/RatingSmiley/index.test.tsx | 117 + .../components/RatingSmiley/index.tsx | 32 +- .../components/LanguageDropdown.test.tsx | 94 + .../components/LanguageDropdown.tsx | 10 +- .../components/SurveyLinkDisplay.test.tsx | 22 + .../components/SurveyLinkDisplay.tsx | 7 +- .../components/ShareSurveyLink/index.test.tsx | 247 + .../SingleResponseCard/actions.test.ts | 202 + .../components/SingleResponseCard/actions.ts | 12 +- .../components/HiddenFields.test.tsx | 70 + .../components/HiddenFields.tsx | 2 +- .../components/QuestionSkip.test.tsx | 98 + .../components/QuestionSkip.tsx | 41 +- .../components/RenderResponse.test.tsx | 277 + .../components/RenderResponse.tsx | 10 +- .../components/ResponseNote.test.tsx | 192 + .../components/ResponseNote.tsx | 23 +- .../components/ResponseTagsWrapper.test.tsx | 245 + .../components/ResponseTagsWrapper.tsx | 2 +- .../components/ResponseVariables.test.tsx | 80 + .../SingleResponseCardBody.test.tsx | 125 + .../components/SingleResponseCardBody.tsx | 6 +- .../SingleResponseCardHeader.test.tsx | 159 + .../components/SingleResponseCardHeader.tsx | 8 +- .../components/Smileys.test.tsx | 60 + .../components/VerifiedEmail.test.tsx | 31 + .../SingleResponseCard/index.test.tsx | 190 + .../components/SingleResponseCard/index.tsx | 35 +- .../SingleResponseCard/util.test.ts | 51 + apps/web/modules/analysis/utils.test.tsx | 67 + apps/web/modules/api/v2/auth/api-wrapper.ts | 18 +- .../api/v2/auth/tests/api-wrapper.test.ts | 22 +- .../auth/tests/authenticate-request.test.ts | 8 +- .../tests/authenticated-api-client.test.ts | 4 +- apps/web/modules/api/v2/lib/rate-limit.ts | 2 +- apps/web/modules/api/v2/lib/response.ts | 29 + .../api/v2/lib/tests/rate-limit.test.ts | 12 +- .../modules/api/v2/lib/tests/response.test.ts | 36 +- .../modules/api/v2/lib/tests/utils.test.ts | 51 + apps/web/modules/api/v2/lib/utils.ts | 22 +- .../lib/contact-attribute-key.ts | 183 + .../[contactAttributeKeyId]/lib/openapi.ts | 60 +- .../lib/tests/contact-attribute-key.test.ts | 222 + .../[contactAttributeKeyId]/route.ts | 131 + .../types/contact-attribute-keys.ts | 28 + .../lib/contact-attribute-key.ts | 105 + .../contact-attribute-keys/lib/openapi.ts | 13 +- .../lib/tests/contact-attribute-key.test.ts | 166 + .../lib/tests/utils.test.ts | 106 + .../contact-attribute-keys/lib/utils.ts | 26 + .../contact-attribute-keys/route.ts | 73 + .../types/contact-attribute-keys.ts | 19 +- .../modules/api/v2/management/lib/services.ts | 8 +- .../v2/management/lib/tests/helper.test.ts | 14 +- .../modules/api/v2/management/lib/utils.ts | 3 +- .../responses/[responseId]/lib/display.ts | 2 +- .../responses/[responseId]/lib/response.ts | 6 +- .../responses/[responseId]/lib/survey.ts | 4 +- .../[responseId]/lib/tests/utils.test.ts | 4 +- .../responses/[responseId]/lib/utils.ts | 2 +- .../management/responses/lib/organization.ts | 22 +- .../v2/management/responses/lib/response.ts | 12 +- .../responses/lib/tests/response.test.ts | 6 +- .../responses/lib/tests/utils.test.ts | 8 +- .../api/v2/management/responses/route.ts | 2 +- .../contacts/[contactId]/lib/contacts.ts | 2 +- .../contacts/[contactId]/lib/response.ts | 4 +- .../contacts/[contactId]/lib/surveys.ts | 4 +- .../[segmentId]/lib/contact-attribute-key.ts | 2 +- .../segments/[segmentId]/lib/contact.ts | 6 +- .../segments/[segmentId]/lib/segment.ts | 4 +- .../segments/[segmentId]/lib/surveys.ts | 4 +- .../[segmentId]/lib/tests/segment.test.ts | 8 +- .../[segmentId]/lib/tests/surveys.test.ts | 8 +- .../webhooks/[webhookId]/lib/webhook.ts | 2 +- .../webhooks/lib/tests/utils.test.ts | 8 +- .../webhooks/lib/tests/webhook.test.ts | 14 +- .../api/v2/management/webhooks/lib/webhook.ts | 2 +- .../api/v2/management/webhooks/route.ts | 2 +- apps/web/modules/api/v2/openapi-document.ts | 4 +- .../[organizationId]/lib/utils.test.ts | 10 +- .../project-teams/lib/project-teams.ts | 4 +- .../lib/tests/project-teams.test.ts | 18 +- .../project-teams/lib/utils.ts | 6 +- .../teams/[teamId]/lib/teams.ts | 2 +- .../teams/[teamId]/lib/tests/teams.test.ts | 20 +- .../[organizationId]/teams/lib/teams.ts | 4 +- .../teams/lib/tests/teams.test.ts | 12 +- .../teams/lib/tests/utils.test.ts | 8 +- .../[organizationId]/teams/route.ts | 2 +- .../users/lib/tests/users.test.ts | 24 +- .../users/lib/tests/utils.test.ts | 10 +- .../[organizationId]/users/lib/users.ts | 6 +- .../[organizationId]/users/route.ts | 4 +- apps/web/modules/api/v2/roles/lib/utils.ts | 2 +- apps/web/modules/auth/actions.ts | 4 +- .../modules/auth/components/form-wrapper.tsx | 5 +- .../auth/forgot-password/reset/actions.ts | 4 +- apps/web/modules/auth/invite/lib/invite.ts | 2 +- apps/web/modules/auth/invite/lib/team.ts | 4 +- apps/web/modules/auth/invite/page.tsx | 8 +- apps/web/modules/auth/layout.tsx | 2 +- apps/web/modules/auth/lib/authOptions.test.ts | 38 +- apps/web/modules/auth/lib/authOptions.ts | 10 +- apps/web/modules/auth/lib/brevo.test.ts | 16 +- apps/web/modules/auth/lib/brevo.ts | 4 +- apps/web/modules/auth/lib/totp.test.ts | 12 +- apps/web/modules/auth/lib/user.test.ts | 28 +- apps/web/modules/auth/lib/user.ts | 6 +- apps/web/modules/auth/lib/utils.test.ts | 10 +- .../auth/login/components/login-form.tsx | 6 +- apps/web/modules/auth/login/page.tsx | 18 +- apps/web/modules/auth/signup/actions.ts | 10 +- .../signup/components/signup-form.test.tsx | 16 +- .../modules/auth/signup/lib/invite.test.ts | 30 +- apps/web/modules/auth/signup/lib/invite.ts | 2 +- apps/web/modules/auth/signup/lib/team.ts | 4 +- .../web/modules/auth/signup/lib/utils.test.ts | 12 +- apps/web/modules/auth/signup/page.test.tsx | 22 +- apps/web/modules/auth/signup/page.tsx | 24 +- .../auth/verification-requested/page.tsx | 4 +- apps/web/modules/ee/auth/saml/lib/jackson.ts | 2 +- .../ee/auth/saml/lib/preload-connection.ts | 2 +- .../ee/auth/saml/lib/tests/jackson.test.ts | 4 +- .../saml/lib/tests/preload-connection.test.ts | 4 +- apps/web/modules/ee/billing/actions.ts | 6 +- .../api/lib/checkout-session-completed.ts | 6 +- .../api/lib/create-customer-portal-session.ts | 4 +- .../ee/billing/api/lib/create-subscription.ts | 8 +- .../ee/billing/api/lib/invoice-finalized.ts | 2 +- .../api/lib/is-subscription-cancelled.ts | 6 +- .../ee/billing/api/lib/stripe-webhook.ts | 4 +- .../lib/subscription-created-or-updated.ts | 6 +- .../billing/api/lib/subscription-deleted.ts | 4 +- .../ee/billing/components/billing-slider.tsx | 4 +- .../ee/billing/components/pricing-card.tsx | 4 +- .../ee/billing/components/pricing-table.tsx | 14 +- apps/web/modules/ee/billing/page.tsx | 14 +- .../components/attributes-section.tsx | 4 +- .../[contactId]/components/response-feed.tsx | 6 +- .../components/response-section.tsx | 10 +- .../modules/ee/contacts/[contactId]/page.tsx | 4 +- .../[userId]/attributes/lib/contact.ts | 2 +- .../contacts/[userId]/lib/attributes.ts | 4 +- .../identify/contacts/[userId]/lib/contact.ts | 2 +- .../contacts/[userId]/lib/personState.ts | 18 +- .../contacts/[userId]/lib/segments.ts | 6 +- .../[environmentId]/user/lib/contact.ts | 2 +- .../[environmentId]/user/lib/segments.ts | 6 +- .../[environmentId]/user/lib/update-user.ts | 2 +- .../[environmentId]/user/lib/user-state.ts | 12 +- .../lib/contact-attribute-key.ts | 6 +- .../lib/contact-attribute-keys.ts | 6 +- .../lib/contact-attributes.ts | 2 +- .../contacts/[contactId]/lib/contact.ts | 4 +- .../v1/management/contacts/lib/contacts.ts | 4 +- .../contacts-secondary-navigation.tsx | 2 +- .../ee/contacts/components/contacts-table.tsx | 2 +- .../components/upload-contacts-button.tsx | 6 +- apps/web/modules/ee/contacts/layout.tsx | 10 +- .../web/modules/ee/contacts/lib/attributes.ts | 4 +- .../ee/contacts/lib/contact-attribute-keys.ts | 2 +- .../ee/contacts/lib/contact-attributes.ts | 4 +- .../contacts/lib/contact-survey-link.test.ts | 28 +- .../ee/contacts/lib/contact-survey-link.ts | 6 +- apps/web/modules/ee/contacts/lib/contacts.ts | 6 +- apps/web/modules/ee/contacts/page.tsx | 2 +- .../modules/ee/contacts/segments/actions.ts | 4 +- .../segments/components/add-filter-modal.tsx | 2 +- .../components/create-segment-modal.tsx | 2 +- .../components/segment-activity-tab.tsx | 8 +- .../segments/components/segment-editor.tsx | 4 +- .../segments/components/segment-filter.tsx | 12 +- .../segments/components/segment-settings.tsx | 4 +- .../segment-table-data-row-container.tsx | 2 +- .../segments/components/targeting-card.tsx | 6 +- .../segments/lib/filter/prisma-query.ts | 4 +- .../lib/filter/tests/prisma-query.test.ts | 4 +- .../ee/contacts/segments/lib/segments.ts | 10 +- .../web/modules/ee/contacts/segments/page.tsx | 2 +- apps/web/modules/ee/insights/actions.ts | 2 +- .../components/insight-sheet/index.tsx | 2 +- .../components/insight-sheet/lib/documents.ts | 6 +- .../components/insights-view.test.tsx | 2 +- .../ee/insights/components/insights-view.tsx | 2 +- .../ee/insights/experience/lib/insights.ts | 8 +- .../ee/insights/experience/lib/stats.ts | 6 +- .../modules/ee/insights/experience/page.tsx | 16 +- apps/web/modules/ee/languages/page.tsx | 4 +- .../web/modules/ee/license-check/lib/utils.ts | 20 +- .../components/default-language-select.tsx | 2 +- .../components/edit-language.tsx | 4 +- .../components/language-indicator.tsx | 6 +- .../components/language-select.tsx | 7 +- .../components/language-toggle.tsx | 2 +- .../components/localized-editor.tsx | 6 +- .../components/multi-language-card.tsx | 10 +- .../ee/multi-language-surveys/lib/actions.ts | 14 +- .../web/modules/ee/role-management/actions.ts | 6 +- .../components/add-member-role.tsx | 2 +- .../components/add-member.test.tsx | 10 +- .../components/edit-membership-role.test.tsx | 12 +- .../components/edit-membership-role.tsx | 4 +- .../ee/role-management/lib/membership.ts | 6 +- .../ee/role-management/tests/actions.test.ts | 16 +- apps/web/modules/ee/sso/actions.ts | 2 +- .../ee/sso/components/azure-button.test.tsx | 16 +- .../ee/sso/components/azure-button.tsx | 2 +- .../ee/sso/components/github-button.test.tsx | 14 +- .../ee/sso/components/github-button.tsx | 2 +- .../ee/sso/components/google-button.test.tsx | 14 +- .../ee/sso/components/google-button.tsx | 2 +- .../ee/sso/components/open-id-button.test.tsx | 18 +- .../ee/sso/components/open-id-button.tsx | 2 +- .../ee/sso/components/saml-button.test.tsx | 16 +- .../modules/ee/sso/components/saml-button.tsx | 2 +- .../ee/sso/components/sso-options.test.tsx | 14 +- .../modules/ee/sso/components/sso-options.tsx | 2 +- apps/web/modules/ee/sso/lib/providers.ts | 10 +- apps/web/modules/ee/sso/lib/sso-handlers.ts | 12 +- .../ee/sso/lib/tests/sso-handlers.test.ts | 62 +- .../modules/ee/sso/lib/tests/utils.test.ts | 12 +- apps/web/modules/ee/teams/lib/roles.ts | 6 +- .../ee/teams/project-teams/lib/team.ts | 6 +- .../team-settings/team-settings-modal.tsx | 6 +- .../team-list/components/teams-table.tsx | 2 +- .../teams/team-list/components/teams-view.tsx | 2 +- .../modules/ee/teams/team-list/lib/project.ts | 6 +- .../modules/ee/teams/team-list/lib/team.ts | 8 +- .../ee/two-factor-auth/lib/two-factor-auth.ts | 6 +- .../whitelabel/email-customization/actions.ts | 2 +- .../email-customization-settings.test.tsx | 14 +- .../email-customization-settings.tsx | 8 +- .../email-customization/lib/organization.ts | 8 +- .../ee/whitelabel/remove-branding/actions.ts | 2 +- .../components/branding-settings-card.tsx | 2 +- .../whitelabel/remove-branding/lib/project.ts | 4 +- .../components/email-question-header.tsx | 6 +- .../email/components/email-template.test.tsx | 14 +- .../email/components/email-template.tsx | 12 +- .../components/preview-email-template.tsx | 32 +- .../email/emails/lib/tests/utils.test.tsx | 259 + apps/web/modules/email/emails/lib/utils.tsx | 71 + .../email/emails/survey/follow-up.test.tsx | 61 +- .../modules/email/emails/survey/follow-up.tsx | 69 +- .../emails/survey/response-finished-email.tsx | 95 +- .../create-reminder-notification-body.tsx | 2 +- .../live-survey-notification.tsx | 8 +- .../weekly-summary/notification-footer.tsx | 2 +- apps/web/modules/email/index.tsx | 47 +- apps/web/modules/email/lib/utils.ts | 4 +- .../modules/environments/lib/utils.test.ts | 28 +- apps/web/modules/environments/lib/utils.ts | 14 +- .../components/webhook-overview-tab.tsx | 4 +- .../webhooks/components/webhook-row-data.tsx | 6 +- .../integrations/webhooks/lib/webhook.ts | 4 +- .../modules/integrations/webhooks/page.tsx | 4 +- apps/web/modules/organization/actions.ts | 6 +- apps/web/modules/organization/lib/utils.ts | 6 +- .../organization/settings/api-keys/actions.ts | 34 +- .../components/add-api-key-modal.test.tsx | 39 +- .../api-keys/components/add-api-key-modal.tsx | 57 +- .../api-keys/components/api-key-list.test.tsx | 16 +- .../components/edit-api-keys.test.tsx | 67 +- .../api-keys/components/edit-api-keys.tsx | 42 +- .../components/view-permission-modal.test.tsx | 16 +- .../components/view-permission-modal.tsx | 257 +- .../settings/api-keys/lib/api-key.ts | 31 +- .../settings/api-keys/lib/api-keys.test.ts | 43 +- .../settings/api-keys/lib/projects.test.ts | 14 +- .../settings/api-keys/lib/projects.ts | 4 +- .../settings/api-keys/loading.tsx | 12 +- .../organization/settings/api-keys/page.tsx | 35 +- .../settings/api-keys/types/api-keys.ts | 8 + .../organization/settings/teams/actions.ts | 8 +- .../edit-memberships/edit-memberships.tsx | 2 +- .../edit-memberships/members-info.tsx | 4 +- .../organization-actions.test.tsx | 2 +- .../edit-memberships/organization-actions.tsx | 4 +- .../invite-member/invite-member-modal.tsx | 4 +- .../teams/components/members-view.tsx | 2 +- .../organization/settings/teams/lib/invite.ts | 8 +- .../settings/teams/lib/membership.ts | 6 +- .../organization/settings/teams/page.tsx | 2 +- .../settings/teams/tests/actions.test.ts | 18 +- .../components/project-switcher/index.tsx | 4 +- .../settings/(setup)/app-connection/page.tsx | 2 +- apps/web/modules/projects/settings/actions.ts | 2 +- .../projects/settings/general/actions.ts | 2 +- .../components/delete-project-render.tsx | 4 +- .../general/components/delete-project.tsx | 4 +- .../projects/settings/general/page.tsx | 4 +- .../modules/projects/settings/lib/project.ts | 15 +- apps/web/modules/projects/settings/lib/tag.ts | 4 +- .../look/components/edit-placement-form.tsx | 2 +- .../look/components/theme-styling.tsx | 2 +- .../projects/settings/look/lib/project.ts | 6 +- .../projects/settings/look/loading.tsx | 6 +- .../modules/projects/settings/look/page.tsx | 4 +- .../settings/tags/components/single-tag.tsx | 8 +- .../modules/projects/settings/tags/page.tsx | 4 +- .../modules/setup/(fresh-instance)/layout.tsx | 2 +- .../(fresh-instance)/signup/page.test.tsx | 10 +- .../setup/(fresh-instance)/signup/page.tsx | 12 +- .../[organizationId]/invite/actions.ts | 2 +- .../[organizationId]/invite/lib/invite.ts | 2 +- .../[organizationId]/invite/page.tsx | 4 +- .../setup/organization/create/page.tsx | 8 +- .../components/multi-lang-wrapper.tsx | 4 +- .../components/recall-item-select.tsx | 6 +- .../components/recall-wrapper.tsx | 22 +- .../question-form-input/index.test.tsx | 633 ++ .../components/question-form-input/index.tsx | 27 +- .../components/question-form-input/utils.ts | 4 +- .../start-from-scratch-template.tsx | 6 +- .../components/template-filters.tsx | 6 +- .../components/template-tags.test.tsx | 103 + .../components/template-tags.tsx | 10 +- .../template-list/components/template.tsx | 6 +- .../survey/components/template-list/index.tsx | 2 +- .../components/template-list/lib/survey.ts | 6 +- .../components/template-list/lib/user.ts | 2 +- .../components/template-list/lib/utils.ts | 4 +- apps/web/modules/survey/editor/actions.ts | 2 +- .../editor/components/add-question-button.tsx | 6 +- .../components/address-question-form.tsx | 2 +- .../editor/components/cal-question-form.tsx | 2 +- .../editor/components/conditional-logic.tsx | 4 +- .../components/contact-info-question-form.tsx | 2 +- .../editor/components/date-question-form.tsx | 2 +- .../editor/components/edit-ending-card.tsx | 8 +- .../editor/components/edit-welcome-card.tsx | 4 +- .../components/end-screen-form.test.tsx | 281 + .../editor/components/end-screen-form.tsx | 59 +- .../components/file-upload-question-form.tsx | 8 +- .../components/form-styling-settings.tsx | 8 +- .../editor/components/hidden-fields-card.tsx | 8 +- .../editor/components/how-to-send-card.tsx | 4 +- .../components/logic-editor-actions.tsx | 2 +- .../components/logic-editor-conditions.tsx | 20 +- .../survey/editor/components/logic-editor.tsx | 2 +- .../components/matrix-question-form.tsx | 2 +- .../multiple-choice-question-form.tsx | 2 +- .../editor/components/nps-question-form.tsx | 2 +- .../editor/components/open-question-form.tsx | 2 +- .../components/picture-selection-form.tsx | 4 +- .../survey/editor/components/placement.tsx | 2 +- .../editor/components/question-card.tsx | 6 +- .../components/question-option-choice.tsx | 4 +- .../editor/components/questions-view.tsx | 10 +- .../components/ranking-question-form.tsx | 2 +- .../components/rating-question-form.tsx | 2 +- .../editor/components/redirect-url-form.tsx | 4 +- .../components/response-options-card.tsx | 12 +- .../survey/editor/components/styling-view.tsx | 2 +- .../editor/components/survey-editor-tabs.tsx | 2 +- .../editor/components/survey-editor.tsx | 10 +- .../editor/components/survey-menu-bar.tsx | 6 +- .../components/survey-variables-card-item.tsx | 2 +- .../components/survey-variables-card.tsx | 6 +- .../editor/components/when-to-send-card.tsx | 10 +- .../modules/survey/editor/lib/action-class.ts | 2 +- apps/web/modules/survey/editor/lib/project.ts | 4 +- apps/web/modules/survey/editor/lib/survey.ts | 4 +- apps/web/modules/survey/editor/lib/team.ts | 53 + apps/web/modules/survey/editor/lib/user.ts | 4 +- .../modules/survey/editor/lib/utils.test.tsx | 152 + apps/web/modules/survey/editor/lib/utils.tsx | 64 +- .../modules/survey/editor/lib/validation.ts | 4 +- apps/web/modules/survey/editor/page.tsx | 58 +- .../survey/editor/types/survey-follow-up.ts | 6 + .../components/follow-up-item.test.tsx | 982 +++ .../follow-ups/components/follow-up-item.tsx | 69 +- .../follow-ups/components/follow-up-modal.tsx | 179 +- .../follow-ups/components/follow-ups-view.tsx | 5 + .../modules/survey/follow-ups/lib/utils.ts | 2 +- .../survey/hooks/useSingleUseId.test.tsx | 69 +- apps/web/modules/survey/lib/action-class.ts | 6 +- apps/web/modules/survey/lib/environment.ts | 6 +- apps/web/modules/survey/lib/membership.ts | 6 +- apps/web/modules/survey/lib/organization.ts | 4 +- apps/web/modules/survey/lib/project.ts | 34 +- apps/web/modules/survey/lib/questions.tsx | 2 +- apps/web/modules/survey/lib/response.ts | 4 +- apps/web/modules/survey/lib/survey.ts | 6 +- .../survey/lib/tests/client-utils.test.ts | 6 +- apps/web/modules/survey/lib/utils.ts | 2 +- .../link/components/link-survey-wrapper.tsx | 4 +- .../survey/link/components/pin-screen.tsx | 2 +- .../components/survey-loading-animation.tsx | 2 +- .../link/components/survey-renderer.tsx | 6 +- .../survey/link/components/verify-email.tsx | 6 +- apps/web/modules/survey/link/lib/helper.ts | 2 +- .../survey/link/lib/metadata-utils.test.ts | 32 +- .../modules/survey/link/lib/metadata-utils.ts | 6 +- apps/web/modules/survey/link/lib/project.ts | 6 +- apps/web/modules/survey/link/lib/response.ts | 4 +- apps/web/modules/survey/link/lib/survey.ts | 4 +- apps/web/modules/survey/link/metadata.ts | 2 +- apps/web/modules/survey/list/actions.ts | 2 +- .../survey/list/components/survey-card.tsx | 16 +- .../list/components/survey-dropdown-menu.tsx | 2 +- .../survey/list/components/survey-list.tsx | 2 +- .../components/tests/survey-card.test.tsx | 10 +- .../tests/survey-dropdown-menu.test.tsx | 8 +- .../modules/survey/list/lib/environment.ts | 6 +- .../modules/survey/list/lib/organization.ts | 4 +- apps/web/modules/survey/list/lib/project.ts | 4 +- apps/web/modules/survey/list/lib/survey.ts | 14 +- apps/web/modules/survey/list/page.tsx | 10 +- apps/web/modules/survey/templates/actions.ts | 2 +- .../survey/templates/lib/minimal-survey.ts | 2 +- .../modules/survey/templates/lib/survey.ts | 6 +- apps/web/modules/survey/templates/page.tsx | 4 +- .../advanced-option-toggle/index.tsx | 2 +- .../web/modules/ui/components/alert/index.tsx | 12 +- .../modules/ui/components/alert/stories.tsx | 2 +- .../background-styling-card/index.tsx | 4 +- .../ui/components/badge-select/index.tsx | 6 +- .../web/modules/ui/components/badge/index.tsx | 2 +- .../modules/ui/components/calendar/index.tsx | 2 +- .../card-styling-settings/index.tsx | 6 +- .../modules/ui/components/card/index.test.tsx | 154 + apps/web/modules/ui/components/card/index.tsx | 21 +- .../modules/ui/components/checkbox/index.tsx | 4 +- .../ui/components/client-logo/index.tsx | 8 +- .../ui/components/code-block/index.tsx | 4 +- .../components/popover-picker.tsx | 2 +- .../ui/components/color-picker/index.tsx | 4 +- .../modules/ui/components/command/index.tsx | 8 +- .../components/data-table-header.tsx | 4 +- .../data-table-settings-modal-item.tsx | 2 +- .../components/data-table-toolbar.tsx | 2 +- .../components/selected-row-settings.tsx | 4 +- .../ui/components/date-picker/index.tsx | 2 +- .../ui/components/date-picker/styles.css | 2 +- .../modules/ui/components/dialog/index.tsx | 6 +- .../ui/components/dropdown-menu/index.tsx | 10 +- .../components/editor/components/editor.tsx | 2 +- .../components/environment-notice/index.tsx | 4 +- .../environmentId-base-layout/index.test.tsx | 6 +- .../environmentId-base-layout/index.tsx | 6 +- .../file-input/components/uploader.tsx | 4 +- .../file-input/components/video-settings.tsx | 2 +- .../ui/components/file-input/index.tsx | 10 +- .../components/file-upload-response/index.tsx | 4 +- apps/web/modules/ui/components/form/index.tsx | 2 +- .../ui/components/input-combo-box/index.tsx | 2 +- .../web/modules/ui/components/input/index.tsx | 4 +- .../web/modules/ui/components/label/index.tsx | 4 +- .../components/load-segment-modal/index.tsx | 4 +- .../ui/components/loading-spinner/index.tsx | 2 +- .../web/modules/ui/components/modal/index.tsx | 10 +- .../ui/components/multi-select/badge.tsx | 2 +- .../components/page-url-selector.tsx | 6 +- .../modules/ui/components/otp-input/index.tsx | 2 +- .../components/page-content-wrapper/index.tsx | 2 +- .../ui/components/page-header/index.tsx | 4 +- .../ui/components/password-input/index.tsx | 4 +- .../picture-selection-response/index.tsx | 2 +- .../modules/ui/components/popover/index.tsx | 2 +- .../post-hog-client/indext.test.tsx | 14 +- .../preview-survey/components/modal.tsx | 2 +- .../ui/components/progress-bar/index.test.tsx | 105 + .../ui/components/progress-bar/index.tsx | 19 +- .../ui/components/radio-group/index.tsx | 2 +- .../ui/components/ranking-response/index.tsx | 2 +- .../ui/components/response-badges/index.tsx | 2 +- .../ui/components/search-bar/index.tsx | 2 +- .../components/secondary-navigation/index.tsx | 2 +- .../modules/ui/components/select/index.tsx | 8 +- .../web/modules/ui/components/sheet/index.tsx | 4 +- .../modules/ui/components/skeleton/index.tsx | 2 +- .../modules/ui/components/slider/index.tsx | 6 +- .../ui/components/styling-tabs/index.tsx | 4 +- .../modules/ui/components/switch/index.tsx | 4 +- .../modules/ui/components/tab-bar/index.tsx | 2 +- .../ui/components/tab-toggle/index.tsx | 4 +- .../web/modules/ui/components/table/index.tsx | 2 +- apps/web/modules/ui/components/tabs/index.tsx | 6 +- apps/web/modules/ui/components/tag/index.tsx | 2 +- .../ui/components/toggle-group/index.tsx | 2 +- .../ui/components/toggle-group/toggle.tsx | 2 +- .../modules/ui/components/tooltip/index.tsx | 2 +- apps/web/modules/utils/hooks/actions.ts | 2 +- apps/web/next.config.mjs | 49 +- apps/web/package.json | 33 +- apps/web/playwright/js.spec.ts | 2 +- apps/web/playwright/lib/utils.ts | 1 + apps/web/sentry.edge.config.ts | 7 +- apps/web/sentry.server.config.ts | 7 +- apps/web/tolgee/language.ts | 6 +- apps/web/tolgee/shared.ts | 12 +- apps/web/tsconfig.json | 1 + apps/web/vite.config.mts | 34 +- {packages/lib => apps/web}/vitestSetup.ts | 20 +- docker/docker-compose.yml | 10 +- docs/api-v2-reference/openapi.yml | 435 + .../standards/qa/testing-methodology.mdx | 5 +- .../quickstart/page-view-action.webp | Bin 0 -> 38370 bytes .../quickstart/recontact-options.webp | Bin 0 -> 28052 bytes .../quickstart/survey-type.webp | Bin 0 -> 27498 bytes .../configuration/environment-variables.mdx | 4 +- .../surveys/link-surveys/data-prefilling.mdx | 25 +- .../surveys/link-surveys/quickstart.mdx | 56 +- .../website-app-surveys/framework-guides.mdx | 93 +- .../website-app-surveys/quickstart.mdx | 70 +- .../xm/best-practices/cancel-subscription.mdx | 2 +- .../xm/best-practices/docs-feedback.mdx | 2 +- .../xm/best-practices/feature-chaser.mdx | 2 +- .../xm/best-practices/feedback-box.mdx | 2 +- .../best-practices/improve-email-content.mdx | 2 +- .../xm/best-practices/improve-trial-cr.mdx | 2 +- .../xm/best-practices/interview-prompt.mdx | 2 +- infra/terraform/cloudwatch.tf | 14 +- infra/terraform/elasticache.tf | 71 +- infra/terraform/main.tf | 112 +- infra/terraform/rds.tf | 29 +- infra/terraform/secrets.tf | 29 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../java/com/formbricks/demo/MainActivity.kt | 35 +- .../app/src/main/res/layout/activity_main.xml | 78 +- .../main/res/xml/data_extraction_rules.xml | 5 +- .../main/res/xml/network_security_config.xml | 4 +- .../android/formbricksSDK/build.gradle.kts | 2 +- .../src/main/AndroidManifest.xml | 3 +- .../formbricks/formbrickssdk/Formbricks.kt | 13 +- .../formbrickssdk/api/FormbricksApi.kt | 22 +- .../formbrickssdk/manager/SurveyManager.kt | 67 +- .../formbrickssdk/manager/UserManager.kt | 22 + .../formbrickssdk/model/environment/Survey.kt | 16 + .../formbrickssdk/model/error/SDKError.kt | 4 +- .../model/javascript/EventType.kt | 1 - .../formbrickssdk/model/user/UserStateData.kt | 3 +- .../network/FormbricksService.kt | 2 - .../network/queue/UpdateQueue.kt | 26 +- .../webview/FormbricksFragment.kt | 23 - .../webview/FormbricksViewModel.kt | 21 +- .../formbrickssdk/webview/WebAppInterface.kt | 2 - packages/config-eslint/legacy-library.js | 4 + packages/config-eslint/legacy-next.js | 2 + packages/config-eslint/legacy-react.js | 4 + packages/config-eslint/library.js | 4 + packages/config-eslint/next.js | 2 + packages/config-eslint/package.json | 3 +- packages/config-eslint/react.js | 3 +- packages/config-typescript/package.json | 8 +- packages/database/package.json | 10 +- packages/database/types/survey-follow-up.ts | 1 + packages/i18n-utils/.eslintrc.cjs | 7 + packages/i18n-utils/.gitignore | 4 + packages/i18n-utils/package.json | 41 + packages/i18n-utils/src/index.ts | 1 + .../{lib/i18n => i18n-utils/src}/utils.ts | 194 - packages/i18n-utils/tsconfig.json | 17 + packages/i18n-utils/vite.config.ts | 21 + packages/ios/.gitignore | 2 + packages/ios/Demo/Demo/AppDelegate.swift | 93 +- packages/ios/Demo/Demo/ContentView.swift | 24 - packages/ios/Demo/Demo/DemoApp.swift | 2 +- packages/ios/Demo/Demo/SetupView.swift | 76 + .../FormbricksSDK.xcodeproj/project.pbxproj | 12 +- .../xcschemes/xcschememanagement.plist | 27 - .../FormbricksSDK/FormbricksSDK/Config.swift | 2 - .../FormbricksSDK/Formbricks.swift | 183 +- .../Helpers/AnyCodable/AnyCodable.swift | 2 +- .../Helpers/AnyCodable/AnyDecodable.swift | 2 +- .../Helpers/AnyCodable/AnyEncodable.swift | 2 +- .../Helpers/FormbricksEnvironment.swift | 32 + .../FormbricksSDK/Logger/Logger.swift | 4 +- .../Manager/PresentSurveyManager.swift | 61 +- .../FormbricksSDK/Manager/SurveyManager.swift | 174 +- .../FormbricksSDK/Manager/UserManager.swift | 96 +- .../Model/Environment/Survey.swift | 21 +- .../Model/Javascript/EventType.swift | 1 - .../Model/User/UserStateDetails.swift | 1 + .../Networking/Base/APIClient.swift | 298 +- .../Environment/GetEnvironmentRequest.swift | 2 +- .../Endpoints/User/PostUserRequest.swift | 2 +- .../Networking/Queue/UpdateQueue.swift | 136 +- .../Service/FormbricksService.swift | 2 +- .../WebView/FormbricksViewModel.swift | 37 +- .../FormbricksSDK/WebView/SurveyWebView.swift | 102 +- .../FormbricksSDKTests.swift | 256 +- packages/ios/README.md | 16 +- packages/js-core/.eslintrc.cjs | 1 + packages/js-core/package.json | 2 +- packages/js-core/src/lib/common/api.ts | 3 +- .../js-core/src/lib/common/tests/api.test.ts | 2 +- packages/js-core/vite.config.ts | 81 +- packages/lib/.eslintrc.cjs | 20 - packages/lib/crypto.ts | 102 - packages/lib/language/tests/language.unit.ts | 129 - packages/lib/messages/de-DE.json | 23 +- packages/lib/messages/en-US.json | 23 +- packages/lib/messages/fr-FR.json | 23 +- packages/lib/messages/pt-BR.json | 23 +- packages/lib/messages/pt-PT.json | 23 +- packages/lib/messages/zh-Hant-TW.json | 23 +- packages/lib/package.json | 52 - packages/lib/tsconfig.json | 24 - packages/lib/vite.config.ts | 9 - packages/react-native/package.json | 2 +- .../src/components/survey-web-view.tsx | 14 +- packages/react-native/src/lib/common/api.ts | 2 +- .../src/lib/common/tests/api.test.ts | 2 +- packages/surveys/package.json | 2 +- .../components/buttons/back-button.test.tsx | 14 +- .../components/buttons/submit-button.test.tsx | 26 +- .../src/components/buttons/submit-button.tsx | 2 +- .../components/general/language-switch.tsx | 2 +- .../general/survey-close-button.tsx | 4 +- .../surveys/src/components/general/survey.tsx | 3 +- packages/surveys/src/lib/api-client.test.ts | 28 +- packages/surveys/src/lib/utils.ts | 2 +- packages/types/formbricks-surveys.ts | 1 - packages/types/package.json | 2 +- packages/types/responses.ts | 2 +- packages/types/surveys/types.ts | 30 +- pnpm-lock.yaml | 3118 ++++--- sonar-project.properties | 12 +- turbo.json | 19 +- 989 files changed, 37300 insertions(+), 11896 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx create mode 100644 apps/web/app/api/(internal)/pipeline/lib/tests/__mocks__/survey-follow-up.mock.ts create mode 100644 apps/web/app/api/(internal)/pipeline/lib/tests/survey-follow-up.test.ts create mode 100644 apps/web/app/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts create mode 100644 apps/web/app/api/v2/management/contact-attribute-keys/route.ts create mode 100644 apps/web/app/error.test.tsx create mode 100644 apps/web/app/global-error.test.tsx create mode 100644 apps/web/app/global-error.tsx create mode 100644 apps/web/app/lib/survey-builder.test.ts create mode 100644 apps/web/app/lib/survey-builder.ts rename {packages => apps/web}/lib/__mocks__/database.ts (100%) rename {packages => apps/web}/lib/account/service.ts (100%) rename {packages => apps/web}/lib/account/utils.ts (100%) rename {packages => apps/web}/lib/actionClass/auth.ts (96%) rename {packages => apps/web}/lib/actionClass/cache.ts (100%) rename {packages => apps/web}/lib/actionClass/service.ts (99%) rename {packages => apps/web}/lib/aiModels.ts (100%) rename {packages => apps/web}/lib/airtable/service.ts (100%) rename {packages => apps/web}/lib/auth.ts (100%) rename {packages => apps/web}/lib/cache.ts (100%) rename {packages => apps/web}/lib/cache/segment.ts (100%) rename {packages => apps/web}/lib/cn.ts (100%) rename {packages => apps/web}/lib/constants.ts (99%) create mode 100644 apps/web/lib/crypto.test.ts create mode 100644 apps/web/lib/crypto.ts rename {packages => apps/web}/lib/display/cache.ts (100%) rename {packages => apps/web}/lib/display/service.ts (100%) rename {packages => apps/web}/lib/display/tests/__mocks__/data.mock.ts (100%) rename {packages => apps/web}/lib/display/tests/display.test.ts (79%) rename {packages => apps/web}/lib/env.d.ts (100%) rename {packages => apps/web}/lib/env.ts (98%) rename {packages => apps/web}/lib/environment/auth.ts (100%) rename {packages => apps/web}/lib/environment/cache.ts (100%) rename {packages => apps/web}/lib/environment/service.ts (100%) rename {packages => apps/web}/lib/fetcher.ts (100%) rename {packages => apps/web}/lib/getSurveyUrl.ts (100%) rename {packages => apps/web}/lib/googleSheet/service.ts (96%) rename {packages => apps/web}/lib/hashString.ts (100%) rename {packages => apps/web}/lib/i18n/i18n.mock.ts (98%) rename {packages => apps/web}/lib/i18n/i18n.test.ts (68%) rename {packages => apps/web}/lib/i18n/reverseTranslation.ts (94%) create mode 100644 apps/web/lib/i18n/utils.ts rename {packages => apps/web}/lib/instance/service.ts (90%) rename {packages => apps/web}/lib/integration/auth.ts (100%) rename {packages => apps/web}/lib/integration/cache.ts (100%) rename {packages => apps/web}/lib/integration/service.ts (100%) rename {packages => apps/web}/lib/jwt.ts (97%) rename {packages => apps/web}/lib/language/service.ts (100%) rename {packages => apps/web}/lib/language/tests/__mocks__/data.mock.ts (100%) create mode 100644 apps/web/lib/language/tests/language.test.ts rename {packages => apps/web}/lib/localStorage.ts (100%) rename {packages => apps/web}/lib/markdownIt.ts (100%) rename {packages => apps/web}/lib/membership/cache.ts (100%) rename {packages => apps/web}/lib/membership/hooks/actions.ts (100%) rename {packages => apps/web}/lib/membership/hooks/useMembershipRole.tsx (100%) rename {packages => apps/web}/lib/membership/service.ts (100%) rename {packages => apps/web}/lib/membership/utils.ts (100%) create mode 100644 apps/web/lib/messages/de-DE.json create mode 100644 apps/web/lib/messages/en-US.json create mode 100644 apps/web/lib/messages/fr-FR.json create mode 100644 apps/web/lib/messages/pt-BR.json create mode 100644 apps/web/lib/messages/pt-PT.json create mode 100644 apps/web/lib/messages/zh-Hant-TW.json rename {packages => apps/web}/lib/notion/service.ts (94%) rename {packages => apps/web}/lib/organization/auth.ts (95%) rename {packages => apps/web}/lib/organization/cache.ts (100%) rename {packages => apps/web}/lib/organization/service.ts (94%) rename {packages => apps/web}/lib/pollyfills/structuredClone.ts (100%) rename {packages => apps/web}/lib/posthogServer.ts (97%) rename {packages => apps/web}/lib/project/cache.ts (100%) rename {packages => apps/web}/lib/project/service.ts (99%) rename {packages => apps/web}/lib/response/auth.ts (96%) rename {packages => apps/web}/lib/response/cache.ts (100%) rename {packages => apps/web}/lib/response/service.ts (99%) rename {packages => apps/web}/lib/response/tests/__mocks__/data.mock.ts (99%) rename {packages => apps/web}/lib/response/tests/constants.ts (100%) rename {packages => apps/web}/lib/response/tests/response.test.ts (83%) rename {packages => apps/web}/lib/response/utils.ts (94%) rename {packages => apps/web}/lib/responseNote/auth.ts (98%) rename {packages => apps/web}/lib/responseNote/cache.ts (100%) rename {packages => apps/web}/lib/responseNote/service.ts (99%) rename {packages => apps/web}/lib/responses.ts (92%) rename {packages => apps/web}/lib/shortUrl/cache.ts (100%) rename {packages => apps/web}/lib/shortUrl/service.ts (97%) rename {packages => apps/web}/lib/slack/service.ts (100%) rename {packages => apps/web}/lib/storage/cache.ts (100%) rename {packages => apps/web}/lib/storage/service.ts (99%) rename {packages => apps/web}/lib/storage/utils.ts (100%) rename {packages => apps/web}/lib/styling/constants.ts (100%) rename {packages => apps/web}/lib/survey/auth.ts (96%) rename {packages => apps/web}/lib/survey/cache.ts (100%) rename {packages => apps/web}/lib/survey/service.ts (99%) rename {packages => apps/web}/lib/survey/tests/__mock__/survey.mock.ts (100%) rename {packages => apps/web}/lib/survey/tests/survey.test.ts (82%) rename {packages => apps/web}/lib/survey/utils.ts (100%) rename {packages => apps/web}/lib/surveyLogic/utils.ts (99%) rename {packages => apps/web}/lib/tag/auth.ts (100%) rename {packages => apps/web}/lib/tag/cache.ts (100%) rename {packages => apps/web}/lib/tag/service.ts (98%) rename {packages => apps/web}/lib/tagOnResponse/auth.ts (96%) rename {packages => apps/web}/lib/tagOnResponse/cache.ts (100%) rename {packages => apps/web}/lib/tagOnResponse/service.ts (98%) rename {packages => apps/web}/lib/telemetry.ts (97%) rename {packages => apps/web}/lib/time.ts (100%) rename {packages => apps/web}/lib/useDocumentVisibility.ts (100%) rename {packages => apps/web}/lib/user/cache.ts (100%) rename {packages => apps/web}/lib/user/service.ts (98%) rename {packages => apps/web}/lib/utils/ai.ts (93%) create mode 100644 apps/web/lib/utils/billing.test.ts create mode 100644 apps/web/lib/utils/billing.ts rename {packages => apps/web}/lib/utils/colors.ts (100%) rename {packages => apps/web}/lib/utils/contact.ts (100%) rename {packages => apps/web}/lib/utils/datetime.ts (100%) rename {packages => apps/web}/lib/utils/email.ts (100%) rename {packages => apps/web}/lib/utils/fileConversion.ts (100%) rename {packages => apps/web}/lib/utils/headers.ts (100%) rename {packages => apps/web}/lib/utils/hooks/useClickOutside.ts (100%) rename {packages => apps/web}/lib/utils/hooks/useIntervalWhenFocused.ts (100%) rename {packages => apps/web}/lib/utils/hooks/useSyncScroll.ts (100%) rename {packages => apps/web}/lib/utils/locale.ts (94%) rename {packages => apps/web}/lib/utils/promises.ts (100%) rename {packages => apps/web}/lib/utils/recall.ts (98%) rename {packages => apps/web}/lib/utils/singleUseSurveys.ts (70%) rename {packages => apps/web}/lib/utils/strings.ts (100%) rename {packages => apps/web}/lib/utils/styling.ts (100%) rename {packages => apps/web}/lib/utils/templates.ts (91%) rename {packages => apps/web}/lib/utils/url.ts (100%) rename {packages => apps/web}/lib/utils/validate.ts (100%) rename {packages => apps/web}/lib/utils/videoUpload.ts (100%) create mode 100644 apps/web/modules/account/components/DeleteAccountModal/actions.test.ts create mode 100644 apps/web/modules/account/components/DeleteAccountModal/index.test.tsx create mode 100644 apps/web/modules/analysis/components/RatingSmiley/index.test.tsx create mode 100644 apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx create mode 100644 apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx create mode 100644 apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/actions.test.ts create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx create mode 100644 apps/web/modules/analysis/components/SingleResponseCard/util.test.ts create mode 100644 apps/web/modules/analysis/utils.test.tsx create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/contact-attribute-key.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/tests/contact-attribute-key.test.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/lib/contact-attribute-key.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/contact-attribute-key.test.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/utils.test.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/lib/utils.ts create mode 100644 apps/web/modules/api/v2/management/contact-attribute-keys/route.ts create mode 100644 apps/web/modules/email/emails/lib/tests/utils.test.tsx create mode 100644 apps/web/modules/email/emails/lib/utils.tsx create mode 100644 apps/web/modules/survey/components/question-form-input/index.test.tsx create mode 100644 apps/web/modules/survey/components/template-list/components/template-tags.test.tsx create mode 100644 apps/web/modules/survey/editor/components/end-screen-form.test.tsx create mode 100644 apps/web/modules/survey/editor/lib/team.ts create mode 100644 apps/web/modules/survey/editor/lib/utils.test.tsx create mode 100644 apps/web/modules/survey/follow-ups/components/follow-up-item.test.tsx create mode 100644 apps/web/modules/ui/components/card/index.test.tsx create mode 100644 apps/web/modules/ui/components/progress-bar/index.test.tsx rename {packages/lib => apps/web}/vitestSetup.ts (88%) create mode 100644 docs/images/xm-and-surveys/surveys/website-app-surveys/quickstart/page-view-action.webp create mode 100644 docs/images/xm-and-surveys/surveys/website-app-surveys/quickstart/recontact-options.webp create mode 100644 docs/images/xm-and-surveys/surveys/website-app-surveys/quickstart/survey-type.webp create mode 100644 packages/i18n-utils/.eslintrc.cjs create mode 100644 packages/i18n-utils/.gitignore create mode 100644 packages/i18n-utils/package.json create mode 100644 packages/i18n-utils/src/index.ts rename packages/{lib/i18n => i18n-utils/src}/utils.ts (88%) create mode 100644 packages/i18n-utils/tsconfig.json create mode 100644 packages/i18n-utils/vite.config.ts create mode 100644 packages/ios/.gitignore delete mode 100644 packages/ios/Demo/Demo/ContentView.swift create mode 100644 packages/ios/Demo/Demo/SetupView.swift delete mode 100644 packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/xcuserdata/vaxi.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 packages/ios/FormbricksSDK/FormbricksSDK/Helpers/FormbricksEnvironment.swift delete mode 100644 packages/lib/.eslintrc.cjs delete mode 100644 packages/lib/crypto.ts delete mode 100644 packages/lib/language/tests/language.unit.ts delete mode 100644 packages/lib/package.json delete mode 100644 packages/lib/tsconfig.json delete mode 100644 packages/lib/vite.config.ts diff --git a/.env.example b/.env.example index 047f0cfc5e..30cad4483c 100644 --- a/.env.example +++ b/.env.example @@ -219,3 +219,8 @@ UNKEY_ROOT_KEY= # PROMETHEUS_ENABLED= # PROMETHEUS_EXPORTER_PORT= +# The SENTRY_DSN is used for error tracking and performance monitoring with Sentry. +# SENTRY_DSN= +# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin. +# It's used automatically by Sentry during the build for authentication when uploading source maps. +# SENTRY_AUTH_TOKEN= diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..91aac327de --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +# Testing Instructions + +When generating test files inside the "/app/web" path, follow these rules: + +- Use vitest +- Ensure 100% code coverage +- Add as few comments as possible +- The test file should be located in the same folder as the original file +- Use the `test` function instead of `it` +- Follow the same test pattern used for other files in the package where the file is located +- All imports should be at the top of the file, not inside individual tests +- For mocking inside "test" blocks use "vi.mocked" +- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file +- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file + +If it's a test for a ".tsx" file, follow these extra instructions: + +- Add this code inside the "describe" block and before any test: + +afterEach(() => { + cleanup(); +}); + +- the "afterEach" function should only have "cleanup()" inside it and should be adde to the "vitest" imports +- For click events, import userEvent from "@testing-library/user-event" +- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components. diff --git a/.github/workflows/release-docker-github-experimental.yml b/.github/workflows/release-docker-github-experimental.yml index 25b8e5e61e..51b6590252 100644 --- a/.github/workflows/release-docker-github-experimental.yml +++ b/.github/workflows/release-docker-github-experimental.yml @@ -82,8 +82,6 @@ jobs: secrets: | database_url=${{ secrets.DUMMY_DATABASE_URL }} encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }} - cache-from: type=gha - cache-to: type=gha,mode=max # Sign the resulting Docker image digest except on PRs. # This will only write to the public Rekor transparency log when the Docker diff --git a/.github/workflows/release-docker-github.yml b/.github/workflows/release-docker-github.yml index 457940fb7e..8ee3ac6c79 100644 --- a/.github/workflows/release-docker-github.yml +++ b/.github/workflows/release-docker-github.yml @@ -102,8 +102,6 @@ jobs: secrets: | database_url=${{ secrets.DUMMY_DATABASE_URL }} encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }} - cache-from: type=gha - cache-to: type=gha,mode=max # Sign the resulting Docker image digest except on PRs. # This will only write to the public Rekor transparency log when the Docker diff --git a/.vscode/settings.json b/.vscode/settings.json index 759352dc65..10bac75fe3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,10 @@ { + "javascript.updateImportsOnFileMove.enabled": "always", "sonarlint.connectedMode.project": { "connectionId": "formbricks", "projectKey": "formbricks_formbricks" }, "typescript.preferences.importModuleSpecifier": "non-relative", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.updateImportsOnFileMove.enabled": "always" } diff --git a/apps/demo/package.json b/apps/demo/package.json index 1ba1905f92..fa36524ea3 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -17,8 +17,8 @@ "lucide-react": "0.486.0", "next": "15.2.4", "postcss": "8.5.3", - "react": "19.0.0", - "react-dom": "19.0.0", + "react": "19.1.0", + "react-dom": "19.1.0", "tailwindcss": "4.1.3" }, "devDependencies": { diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 64a6e29852..6d2cb8fcf3 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -1,3 +1,20 @@ module.exports = { extends: ["@formbricks/eslint-config/legacy-next.js"], + ignorePatterns: ["**/package.json", "**/tsconfig.json"], + overrides: [ + { + files: ["lib/messages/**/*.json"], + plugins: ["i18n-json"], + rules: { + "i18n-json/identical-keys": [ + "error", + { + filePath: require("path").join(__dirname, "messages", "en-US.json"), + checkExtraKeys: false, + checkMissingKeys: true, + }, + ], + }, + }, + ], }; diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 09190cb1b2..8a8922548e 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -50,4 +50,4 @@ uploads/ .sentryclirc # SAML Preloaded Connections -saml-connection/ \ No newline at end of file +saml-connection/ diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 8805434826..410bdc2d5a 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base +FROM node:22-alpine3.21 AS base # ## step 1: Prune monorepo @@ -81,13 +81,14 @@ RUN corepack enable RUN apk add --no-cache curl \ && apk add --no-cache supercronic \ # && addgroup --system --gid 1001 nodejs \ - && adduser --system --uid 1001 nextjs + && addgroup -S nextjs \ + && adduser -S -u 1001 -G nextjs nextjs WORKDIR /home/nextjs # Ensure no write permissions are assigned to the copied resources -COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./ -RUN chmod -R 755 ./ +COPY --from=installer /app/apps/web/.next/standalone ./ +RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./ COPY --from=installer /app/apps/web/next.config.mjs . RUN chmod 644 ./next.config.mjs @@ -95,38 +96,38 @@ RUN chmod 644 ./next.config.mjs COPY --from=installer /app/apps/web/package.json . RUN chmod 644 ./package.json -COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static -RUN chmod -R 755 ./apps/web/.next/static +COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static +RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static -COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public -RUN chmod -R 755 ./apps/web/public +COPY --from=installer /app/apps/web/public ./apps/web/public +RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public -COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma -RUN chmod 644 ./packages/database/schema.prisma +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 -COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json -RUN chmod 644 ./packages/database/package.json +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 --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration -RUN chmod -R 755 ./packages/database/migration +COPY --from=installer /app/packages/database/migration ./packages/database/migration +RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration -COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src -RUN chmod -R 755 ./packages/database/src +COPY --from=installer /app/packages/database/src ./packages/database/src +RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src -COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules -RUN chmod -R 755 ./packages/database/node_modules +COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules +RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules -COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist -RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist +COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist +RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist -COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client -RUN chmod -R 755 ./node_modules/@prisma/client +COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client +RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client -COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma -RUN chmod -R 755 ./node_modules/.prisma +COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma +RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma -COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt . -RUN chmod 644 ./prisma_version.txt +COPY --from=installer /prisma_version.txt . +RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt COPY /docker/cronjobs /app/docker/cronjobs RUN chmod -R 755 /app/docker/cronjobs diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx index 1010f5a939..db89051d94 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx @@ -1,11 +1,11 @@ "use client"; +import { cn } from "@/lib/cn"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { ArrowRight } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { cn } from "@formbricks/lib/cn"; import { TEnvironment } from "@formbricks/types/environment"; import { TProjectConfigChannel } from "@formbricks/types/project"; import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions"; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx index cee212d799..c48d49edbc 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx @@ -1,12 +1,12 @@ import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks"; +import { WEBAPP_URL } from "@/lib/constants"; +import { getEnvironment } from "@/lib/environment/service"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { getTranslate } from "@/tolgee/server"; import { XIcon } from "lucide-react"; import Link from "next/link"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; interface ConnectPageProps { params: Promise<{ @@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => { channel={channel} /> + + ) : null, + }) +); +vi.mock("@/modules/ui/components/delete-dialog", () => ({ + DeleteDialog: ({ open, setOpen, onDelete }) => + open ? ( +
+ + +
+ ) : null, +})); +vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +const baseProps = { + environment: { id: "env1" } as TEnvironment, + environmentId: "env1", + setIsConnected: vi.fn(), + surveys: [], + airtableArray: [], + locale: "en-US" as const, +}; + +describe("ManageIntegration", () => { + afterEach(() => { + cleanup(); + }); + + test("empty state", () => { + render( + + ); + expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument(); + expect(screen.getByText(/link_new_table/)).toBeInTheDocument(); + }); + + test("open add modal", async () => { + render( + + ); + await userEvent.click(screen.getByText(/link_new_table/)); + expect(screen.getByTestId("add-modal")).toBeInTheDocument(); + }); + + test("list integrations and open edit modal", async () => { + const item = { + baseId: "b", + tableId: "t", + surveyId: "s", + surveyName: "S", + tableName: "T", + questions: "Q", + questionIds: ["x"], + createdAt: new Date(), + includeVariables: false, + includeHiddenFields: false, + includeMetadata: false, + includeCreatedAt: false, + }; + render( + + ); + expect(screen.getByText("S")).toBeInTheDocument(); + await userEvent.click(screen.getByText("S")); + expect(screen.getByTestId("add-modal")).toBeInTheDocument(); + }); + + test("delete integration success", async () => { + vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any); + render( + + ); + await userEvent.click(screen.getByText(/delete_integration/)); + expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); + await userEvent.click(screen.getByText("confirm")); + expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" }); + const { toast } = await import("react-hot-toast"); + expect(toast.success).toHaveBeenCalled(); + expect(baseProps.setIsConnected).toHaveBeenCalledWith(false); + }); + + test("delete integration error", async () => { + vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any); + render( + + ); + await userEvent.click(screen.getByText(/delete_integration/)); + await userEvent.click(screen.getByText("confirm")); + const { toast } = await import("react-hot-toast"); + expect(toast.error).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx index 87324ad134..4e0c8c937c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx @@ -5,6 +5,7 @@ import { AddIntegrationModal, IntegrationModalInputs, } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal"; +import { timeSince } from "@/lib/time"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; @@ -13,7 +14,6 @@ import { useTranslate } from "@tolgee/react"; import { Trash2Icon } from "lucide-react"; import { useState } from "react"; import { toast } from "react-hot-toast"; -import { timeSince } from "@formbricks/lib/time"; import { TEnvironment } from "@formbricks/types/environment"; import { TIntegrationItem } from "@formbricks/types/integration"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; @@ -98,17 +98,17 @@ export const ManageIntegration = (props: ManageIntegrationProps) => { {integrationData.length ? (
- {tableHeaders.map((header, idx) => ( - {integrationData.map((data, index) => ( -
{ setDefaultValues({ base: data.baseId, @@ -129,7 +129,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
{timeSince(data.createdAt.toString(), props.locale)}
-
+ ))}
) : ( diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx index 38861cd0a5..ebd184254e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx @@ -1,15 +1,15 @@ import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"; import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys"; +import { getAirtableTables } from "@/lib/airtable/service"; +import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants"; +import { getIntegrations } from "@/lib/integration/service"; +import { findMatchingLocale } from "@/lib/utils/locale"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { redirect } from "next/navigation"; -import { getAirtableTables } from "@formbricks/lib/airtable/service"; -import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; -import { getIntegrations } from "@formbricks/lib/integration/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; import { TIntegrationItem } from "@formbricks/types/integration"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts index 9871377253..037a28ea26 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts @@ -1,10 +1,10 @@ "use server"; +import { getSpreadsheetNameById } from "@/lib/googleSheet/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper"; import { z } from "zod"; -import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service"; import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; const ZGetSpreadsheetNameByIdAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx index 3c1a01314c..22ba799aef 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx @@ -8,7 +8,9 @@ import { isValidGoogleSheetsUrl, } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util"; import GoogleSheetLogo from "@/images/googleSheetsLogo.png"; +import { getLocalizedValue } from "@/lib/i18n/utils"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { replaceHeadlineRecall } from "@/lib/utils/recall"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; import { Button } from "@/modules/ui/components/button"; import { Checkbox } from "@/modules/ui/components/checkbox"; @@ -21,8 +23,6 @@ import Image from "next/image"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TIntegrationGoogleSheets, TIntegrationGoogleSheetsConfigData, @@ -255,7 +255,7 @@ export const AddIntegrationModal = ({
-
+
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.test.tsx new file mode 100644 index 0000000000..d77ac85ac8 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.test.tsx @@ -0,0 +1,162 @@ +import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; +import { ManageIntegration } from "./ManageIntegration"; + +vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({ + deleteIntegrationAction: vi.fn(), +})); + +vi.mock("react-hot-toast", () => ({ + default: { success: vi.fn(), error: vi.fn() }, +})); + +vi.mock("@/modules/ui/components/delete-dialog", () => ({ + DeleteDialog: ({ open, setOpen, onDelete }: any) => + open ? ( +
+ + +
+ ) : null, +})); + +vi.mock("@/modules/ui/components/empty-space-filler", () => ({ + EmptySpaceFiller: ({ emptyMessage }: any) =>
{emptyMessage}
, +})); + +const baseProps = { + environment: { id: "env1" } as TEnvironment, + setOpenAddIntegrationModal: vi.fn(), + setIsConnected: vi.fn(), + setSelectedIntegration: vi.fn(), + locale: "en-US" as const, +} as const; + +describe("ManageIntegration (Google Sheets)", () => { + afterEach(() => { + cleanup(); + }); + + test("empty state", () => { + render( + + ); + + expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument(); + expect(screen.getByText(/link_new_sheet/)).toBeInTheDocument(); + }); + + test("click link new sheet", async () => { + render( + + ); + + await userEvent.click(screen.getByText(/link_new_sheet/)); + + expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null); + expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); + }); + + test("list integrations and open edit", async () => { + const item = { + spreadsheetId: "sid", + spreadsheetName: "SheetName", + surveyId: "s1", + surveyName: "Survey1", + questionIds: ["q1"], + questions: "Q", + createdAt: new Date(), + }; + + render( + + ); + + expect(screen.getByText("Survey1")).toBeInTheDocument(); + + await userEvent.click(screen.getByText("Survey1")); + + expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ + ...item, + index: 0, + }); + expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); + }); + + test("delete integration success", async () => { + vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any); + + render( + + ); + + await userEvent.click(screen.getByText(/delete_integration/)); + expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); + + await userEvent.click(screen.getByText("confirm")); + + expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" }); + + const { default: toast } = await import("react-hot-toast"); + expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully"); + expect(baseProps.setIsConnected).toHaveBeenCalledWith(false); + }); + + test("delete integration error", async () => { + vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any); + + render( + + ); + + await userEvent.click(screen.getByText(/delete_integration/)); + await userEvent.click(screen.getByText("confirm")); + + const { default: toast } = await import("react-hot-toast"); + expect(toast.error).toHaveBeenCalledWith(expect.any(String)); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx index 717632c67d..a1876d3fbd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx @@ -1,6 +1,7 @@ "use client"; import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; +import { timeSince } from "@/lib/time"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; @@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react"; import { Trash2Icon } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; -import { timeSince } from "@formbricks/lib/time"; import { TEnvironment } from "@formbricks/types/environment"; import { TIntegrationGoogleSheets, @@ -36,11 +36,10 @@ export const ManageIntegration = ({ }: ManageIntegrationProps) => { const { t } = useTranslate(); const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false); - const integrationArray = googleSheetIntegration - ? googleSheetIntegration.config.data - ? googleSheetIntegration.config.data - : [] - : []; + let integrationArray: TIntegrationGoogleSheetsConfigData[] = []; + if (googleSheetIntegration?.config.data) { + integrationArray = googleSheetIntegration.config.data; + } const [isDeleting, setisDeleting] = useState(false); const handleDeleteIntegration = async () => { @@ -112,9 +111,9 @@ export const ManageIntegration = ({ {integrationArray && integrationArray.map((data, index) => { return ( -
{ editIntegration(index); }}> @@ -124,7 +123,7 @@ export const ManageIntegration = ({
{timeSince(data.createdAt.toString(), locale)}
-
+ ); })}
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx index 57ac840deb..9561d08fc8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx @@ -1,19 +1,19 @@ import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper"; import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys"; +import { + GOOGLE_SHEETS_CLIENT_ID, + GOOGLE_SHEETS_CLIENT_SECRET, + GOOGLE_SHEETS_REDIRECT_URL, + WEBAPP_URL, +} from "@/lib/constants"; +import { getIntegrations } from "@/lib/integration/service"; +import { findMatchingLocale } from "@/lib/utils/locale"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { redirect } from "next/navigation"; -import { - GOOGLE_SHEETS_CLIENT_ID, - GOOGLE_SHEETS_CLIENT_SECRET, - GOOGLE_SHEETS_REDIRECT_URL, - WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { getIntegrations } from "@formbricks/lib/integration/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; const Page = async (props) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts index 7e9127267a..cf024b2031 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/surveys.ts @@ -1,12 +1,12 @@ import "server-only"; +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; +import { selectSurvey } from "@/lib/survey/service"; +import { transformPrismaSurvey } from "@/lib/survey/utils"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { selectSurvey } from "@formbricks/lib/survey/service"; -import { transformPrismaSurvey } from "@formbricks/lib/survey/utils"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts index f7b024ed66..90ce809fe5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; import { webhookCache } from "@/lib/cache/webhook"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma, Webhook } from "@prisma/client"; import { z } from "zod"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal.tsx index 73fdb91ec8..d810c0d4b4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal.tsx @@ -7,6 +7,9 @@ import { UNSUPPORTED_TYPES_BY_NOTION, } from "@/app/(app)/environments/[environmentId]/integrations/notion/constants"; import NotionLogo from "@/images/notion.png"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { structuredClone } from "@/lib/pollyfills/structuredClone"; +import { replaceHeadlineRecall } from "@/lib/utils/recall"; import { getQuestionTypes } from "@/modules/survey/lib/questions"; import { Button } from "@/modules/ui/components/button"; import { DropdownSelector } from "@/modules/ui/components/dropdown-selector"; @@ -18,9 +21,6 @@ import Image from "next/image"; import React, { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone"; -import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TIntegrationInput } from "@formbricks/types/integration"; import { TIntegrationNotion, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.test.tsx new file mode 100644 index 0000000000..0c0c05c0a0 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.test.tsx @@ -0,0 +1,91 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import type { + TIntegrationNotion, + TIntegrationNotionConfig, + TIntegrationNotionConfigData, + TIntegrationNotionCredential, +} from "@formbricks/types/integration/notion"; +import { ManageIntegration } from "./ManageIntegration"; + +vi.mock("react-hot-toast", () => ({ success: vi.fn(), error: vi.fn() })); +vi.mock("@/lib/time", () => ({ timeSince: () => "ago" })); +vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({ + deleteIntegrationAction: vi.fn(), +})); + +describe("ManageIntegration", () => { + afterEach(() => { + cleanup(); + }); + + const defaultProps = { + environment: {} as any, + locale: "en-US" as const, + setOpenAddIntegrationModal: vi.fn(), + setIsConnected: vi.fn(), + setSelectedIntegration: vi.fn(), + handleNotionAuthorization: vi.fn(), + }; + + test("shows empty state when no databases", () => { + render( + + ); + expect(screen.getByText("environments.integrations.notion.no_databases_found")).toBeInTheDocument(); + }); + + test("renders list and handles clicks", async () => { + const data = [ + { surveyName: "S", databaseName: "D", createdAt: new Date().toISOString(), databaseId: "db" }, + ] as unknown as TIntegrationNotionConfigData[]; + render( + + ); + expect(screen.getByText("S")).toBeInTheDocument(); + await userEvent.click(screen.getByText("S")); + expect(defaultProps.setSelectedIntegration).toHaveBeenCalledWith({ ...data[0], index: 0 }); + expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled(); + }); + + test("update and link new buttons invoke handlers", async () => { + render( + + ); + await userEvent.click(screen.getByText("environments.integrations.notion.update_connection")); + expect(defaultProps.handleNotionAuthorization).toHaveBeenCalled(); + await userEvent.click(screen.getByText("environments.integrations.notion.link_new_database")); + expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.tsx index d9a33dc687..702cd02c8e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration.tsx @@ -1,6 +1,7 @@ "use client"; import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; +import { timeSince } from "@/lib/time"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; @@ -10,7 +11,6 @@ import { useTranslate } from "@tolgee/react"; import { RefreshCcwIcon, Trash2Icon } from "lucide-react"; import React, { useState } from "react"; import toast from "react-hot-toast"; -import { timeSince } from "@formbricks/lib/time"; import { TEnvironment } from "@formbricks/types/environment"; import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion"; import { TUserLocale } from "@formbricks/types/user"; @@ -39,11 +39,11 @@ export const ManageIntegration = ({ const { t } = useTranslate(); const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false); const [isDeleting, setisDeleting] = useState(false); - const integrationArray = notionIntegration - ? notionIntegration.config.data - ? notionIntegration.config.data - : [] - : []; + + let integrationArray: TIntegrationNotionConfigData[] = []; + if (notionIntegration?.config.data) { + integrationArray = notionIntegration.config.data; + } const handleDeleteIntegration = async () => { setisDeleting(true); @@ -121,9 +121,9 @@ export const ManageIntegration = ({ {integrationArray && integrationArray.map((data, index) => { return ( -
{ editIntegration(index); }}> @@ -132,7 +132,7 @@ export const ManageIntegration = ({
{timeSince(data.createdAt.toString(), locale)}
-
+ ); })}
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/constants.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/constants.ts index 5f24b5fd24..a2ef63ba6a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/constants.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/constants.ts @@ -25,6 +25,8 @@ export const TYPE_MAPPING = { [TSurveyQuestionTypeEnum.Address]: ["rich_text"], [TSurveyQuestionTypeEnum.Matrix]: ["rich_text"], [TSurveyQuestionTypeEnum.Cal]: ["checkbox"], + [TSurveyQuestionTypeEnum.ContactInfo]: ["rich_text"], + [TSurveyQuestionTypeEnum.Ranking]: ["rich_text"], }; export const UNSUPPORTED_TYPES_BY_NOTION = [ diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx index e021be1d45..9a5a296cad 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx @@ -1,21 +1,21 @@ import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys"; import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { GoBackButton } from "@/modules/ui/components/go-back-button"; -import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; -import { PageHeader } from "@/modules/ui/components/page-header"; -import { getTranslate } from "@/tolgee/server"; -import { redirect } from "next/navigation"; import { NOTION_AUTH_URL, NOTION_OAUTH_CLIENT_ID, NOTION_OAUTH_CLIENT_SECRET, NOTION_REDIRECT_URI, WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { getIntegrationByType } from "@formbricks/lib/integration/service"; -import { getNotionDatabases } from "@formbricks/lib/notion/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +} from "@/lib/constants"; +import { getIntegrationByType } from "@/lib/integration/service"; +import { getNotionDatabases } from "@/lib/notion/service"; +import { findMatchingLocale } from "@/lib/utils/locale"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; +import { GoBackButton } from "@/modules/ui/components/go-back-button"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { PageHeader } from "@/modules/ui/components/page-header"; +import { getTranslate } from "@/tolgee/server"; +import { redirect } from "next/navigation"; import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion"; const Page = async (props) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx index 49ff5836a7..4a3715fab1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx @@ -9,6 +9,7 @@ import notionLogo from "@/images/notion.png"; import SlackLogo from "@/images/slacklogo.png"; import WebhookLogo from "@/images/webhook.png"; import ZapierLogo from "@/images/zapier-small.png"; +import { getIntegrations } from "@/lib/integration/service"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { Card } from "@/modules/ui/components/integration-card"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -16,7 +17,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import Image from "next/image"; import { redirect } from "next/navigation"; -import { getIntegrations } from "@formbricks/lib/integration/service"; import { TIntegrationType } from "@formbricks/types/integration"; const Page = async (props) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts index 708a156fa9..800661f970 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts @@ -1,10 +1,10 @@ "use server"; +import { getSlackChannels } from "@/lib/slack/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper"; import { z } from "zod"; -import { getSlackChannels } from "@formbricks/lib/slack/service"; import { ZId } from "@formbricks/types/common"; const ZGetSlackChannelsAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal.tsx index c3853e3303..257959ed5d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal.tsx @@ -2,6 +2,8 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; import SlackLogo from "@/images/slacklogo.png"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { replaceHeadlineRecall } from "@/lib/utils/recall"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; import { Button } from "@/modules/ui/components/button"; import { Checkbox } from "@/modules/ui/components/checkbox"; @@ -15,8 +17,6 @@ import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TIntegrationItem } from "@formbricks/types/integration"; import { TIntegrationSlack, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.test.tsx new file mode 100644 index 0000000000..1c2f2e2712 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.test.tsx @@ -0,0 +1,158 @@ +import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack"; +import { ManageIntegration } from "./ManageIntegration"; + +vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({ + deleteIntegrationAction: vi.fn(), +})); +vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } })); +vi.mock("@/modules/ui/components/delete-dialog", () => ({ + DeleteDialog: ({ open, setOpen, onDelete }: any) => + open ? ( +
+ + +
+ ) : null, +})); +vi.mock("@/modules/ui/components/empty-space-filler", () => ({ + EmptySpaceFiller: ({ emptyMessage }: any) =>
{emptyMessage}
, +})); + +const baseProps = { + environment: { id: "env1" } as TEnvironment, + setOpenAddIntegrationModal: vi.fn(), + setIsConnected: vi.fn(), + setSelectedIntegration: vi.fn(), + refreshChannels: vi.fn(), + handleSlackAuthorization: vi.fn(), + showReconnectButton: false, + locale: "en-US" as const, +}; + +describe("ManageIntegration (Slack)", () => { + afterEach(() => cleanup()); + + test("empty state", () => { + render( + + ); + expect(screen.getByText(/connect_your_first_slack_channel/)).toBeInTheDocument(); + expect(screen.getByText(/link_channel/)).toBeInTheDocument(); + }); + + test("link channel triggers handlers", async () => { + render( + + ); + await userEvent.click(screen.getByText(/link_channel/)); + expect(baseProps.refreshChannels).toHaveBeenCalled(); + expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null); + expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); + }); + + test("show reconnect button and triggers authorization", async () => { + render( + + ); + expect(screen.getByText("environments.integrations.slack.slack_reconnect_button")).toBeInTheDocument(); + await userEvent.click(screen.getByText("environments.integrations.slack.slack_reconnect_button")); + expect(baseProps.handleSlackAuthorization).toHaveBeenCalled(); + }); + + test("list integrations and open edit", async () => { + const item = { + surveyName: "S", + channelName: "C", + questions: "Q", + createdAt: new Date().toISOString(), + surveyId: "s", + channelId: "c", + } as unknown as TIntegrationSlackConfigData; + render( + + ); + expect(screen.getByText("S")).toBeInTheDocument(); + await userEvent.click(screen.getByText("S")); + expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ ...item, index: 0 }); + expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true); + }); + + test("delete integration success", async () => { + vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any); + render( + + ); + await userEvent.click(screen.getByText(/delete_integration/)); + expect(screen.getByTestId("delete-dialog")).toBeInTheDocument(); + await userEvent.click(screen.getByText("confirm")); + expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" }); + const { default: toast } = await import("react-hot-toast"); + expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully"); + expect(baseProps.setIsConnected).toHaveBeenCalledWith(false); + }); + + test("delete integration error", async () => { + vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any); + render( + + ); + await userEvent.click(screen.getByText(/delete_integration/)); + await userEvent.click(screen.getByText("confirm")); + const { default: toast } = await import("react-hot-toast"); + expect(toast.error).toHaveBeenCalledWith(expect.any(String)); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx index 0c9127cacd..33a0693a06 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx @@ -1,16 +1,15 @@ "use client"; import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; +import { timeSince } from "@/lib/time"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler"; -import { useTranslate } from "@tolgee/react"; -import { T } from "@tolgee/react"; +import { T, useTranslate } from "@tolgee/react"; import { Trash2Icon } from "lucide-react"; import React, { useState } from "react"; import toast from "react-hot-toast"; -import { timeSince } from "@formbricks/lib/time"; import { TEnvironment } from "@formbricks/types/environment"; import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack"; import { TUserLocale } from "@formbricks/types/user"; @@ -43,11 +42,10 @@ export const ManageIntegration = ({ const { t } = useTranslate(); const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false); const [isDeleting, setisDeleting] = useState(false); - const integrationArray = slackIntegration - ? slackIntegration.config.data - ? slackIntegration.config.data - : [] - : []; + let integrationArray: TIntegrationSlackConfigData[] = []; + if (slackIntegration?.config.data) { + integrationArray = slackIntegration.config.data; + } const handleDeleteIntegration = async () => { setisDeleting(true); @@ -129,9 +127,9 @@ export const ManageIntegration = ({ {integrationArray && integrationArray.map((data, index) => { return ( -
{ editIntegration(index); }}> @@ -141,7 +139,7 @@ export const ManageIntegration = ({
{timeSince(data.createdAt.toString(), locale)}
-
+ ); })}
diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx index 8cae88faaf..86cc97399f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx @@ -1,14 +1,14 @@ import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys"; import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper"; +import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants"; +import { getIntegrationByType } from "@/lib/integration/service"; +import { findMatchingLocale } from "@/lib/utils/locale"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { redirect } from "next/navigation"; -import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { getIntegrationByType } from "@formbricks/lib/integration/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; import { TIntegrationSlack } from "@formbricks/types/integration/slack"; const Page = async (props) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx index 453e0a7573..54afe41f3c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/layout.test.tsx @@ -1,10 +1,10 @@ +import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils"; import { cleanup, render, screen } from "@testing-library/react"; import { Session } from "next-auth"; import { redirect } from "next/navigation"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { TMembership } from "@formbricks/types/memberships"; import { TOrganization } from "@formbricks/types/organizations"; import { TProject } from "@formbricks/types/project"; @@ -41,10 +41,10 @@ vi.mock("./components/EnvironmentStorageHandler", () => ({ vi.mock("@/modules/environments/lib/utils", () => ({ environmentIdLayoutChecks: vi.fn(), })); -vi.mock("@formbricks/lib/project/service", () => ({ +vi.mock("@/lib/project/service", () => ({ getProjectByEnvironmentId: vi.fn(), })); -vi.mock("@formbricks/lib/membership/service", () => ({ +vi.mock("@/lib/membership/service", () => ({ getMembershipByUserIdOrganizationId: vi.fn(), })); @@ -53,7 +53,7 @@ describe("EnvLayout", () => { cleanup(); }); - it("renders successfully when all dependencies return valid data", async () => { + test("renders successfully when all dependencies return valid data", async () => { vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test session: { user: { id: "user1" } } as Session, @@ -77,7 +77,7 @@ describe("EnvLayout", () => { expect(screen.getByTestId("child")).toHaveTextContent("Content"); }); - it("throws error if project is not found", async () => { + test("throws error if project is not found", async () => { vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ t: ((key: string) => key) as any, session: { user: { id: "user1" } } as Session, @@ -97,7 +97,7 @@ describe("EnvLayout", () => { ).rejects.toThrow("common.project_not_found"); }); - it("throws error if membership is not found", async () => { + test("throws error if membership is not found", async () => { vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ t: ((key: string) => key) as any, session: { user: { id: "user1" } } as Session, @@ -115,7 +115,7 @@ describe("EnvLayout", () => { ).rejects.toThrow("common.membership_not_found"); }); - it("calls redirect when session is null", async () => { + test("calls redirect when session is null", async () => { vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ t: ((key: string) => key) as any, session: undefined as unknown as Session, @@ -134,7 +134,7 @@ describe("EnvLayout", () => { ).rejects.toThrow("Redirect called"); }); - it("throws error if user is null", async () => { + test("throws error if user is null", async () => { vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({ t: ((key: string) => key) as any, session: { user: { id: "user1" } } as Session, diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.tsx index e1acd64421..40d34782fc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/layout.tsx @@ -1,9 +1,9 @@ import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"; +import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils"; import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout"; import { redirect } from "next/navigation"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler"; const EnvLayout = async (props: { diff --git a/apps/web/app/(app)/environments/[environmentId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/page.tsx index 062dfe781e..b71aed10a5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/page.tsx @@ -1,7 +1,7 @@ +import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; +import { getAccessFlags } from "@/lib/membership/utils"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { redirect } from "next/navigation"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; const EnvironmentPage = async (props) => { const params = await props.params; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx index 0823dc0a8d..7dbd8b6bad 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx @@ -1,8 +1,8 @@ +import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getTranslate } from "@/tolgee/server"; import { getServerSession } from "next-auth"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; const AccountSettingsLayout = async (props) => { const params = await props.params; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts index 87730fe663..4adaef9862 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts @@ -1,8 +1,8 @@ "use server"; +import { updateUser } from "@/lib/user/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { z } from "zod"; -import { updateUser } from "@formbricks/lib/user/service"; import { ZUserNotificationSettings } from "@formbricks/types/user"; const ZUpdateNotificationSettingsAction = z.object({ diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx index 51e941b4f3..e536e64d0c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx @@ -1,12 +1,12 @@ import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; +import { getUser } from "@/lib/user/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { getServerSession } from "next-auth"; import { prisma } from "@formbricks/database"; -import { getUser } from "@formbricks/lib/user/service"; import { TUserNotificationSettings } from "@formbricks/types/user"; import { EditAlerts } from "./components/EditAlerts"; import { EditWeeklySummary } from "./components/EditWeeklySummary"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts index fda5844b9f..96e7d4a2f3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts @@ -1,10 +1,10 @@ "use server"; +import { deleteFile } from "@/lib/storage/service"; +import { getFileNameWithIdFromUrl } from "@/lib/storage/utils"; +import { updateUser } from "@/lib/user/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { z } from "zod"; -import { deleteFile } from "@formbricks/lib/storage/service"; -import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils"; -import { updateUser } from "@formbricks/lib/user/service"; import { ZId } from "@formbricks/types/common"; import { ZUserUpdateInput } from "@formbricks/types/user"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx index 0af8119077..c9225594e6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx @@ -1,5 +1,6 @@ "use client"; +import { appLanguages } from "@/lib/i18n/utils"; import { Button } from "@/modules/ui/components/button"; import { DropdownMenu, @@ -23,7 +24,6 @@ import { ChevronDownIcon } from "lucide-react"; import { SubmitHandler, useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { z } from "zod"; -import { appLanguages } from "@formbricks/lib/i18n/utils"; import { TUser, ZUser } from "@formbricks/types/user"; import { updateUserAction } from "../actions"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx index 89d7a77341..d761e40718 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx @@ -1,5 +1,8 @@ import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar"; import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service"; +import { getUser } from "@/lib/user/service"; import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -7,9 +10,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { SettingsId } from "@/modules/ui/components/settings-id"; import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; import { getTranslate } from "@/tolgee/server"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service"; -import { getUser } from "@formbricks/lib/user/service"; import { SettingsCard } from "../../components/SettingsCard"; import { DeleteAccount } from "./components/DeleteAccount"; import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.tsx index b3139471f6..42fe272723 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/api-keys/loading.tsx @@ -1,5 +1,5 @@ +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import Loading from "@/modules/organization/settings/api-keys/loading"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; export default function LoadingPage() { return ; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx index 95ff1640df..623a30b52c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx @@ -1,8 +1,8 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; const Loading = async () => { const t = await getTranslate(); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx index 18a0b6737e..2f763ededa 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx @@ -1,9 +1,9 @@ "use client"; +import { getAccessFlags } from "@/lib/membership/utils"; import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation"; import { useTranslate } from "@tolgee/react"; import { usePathname } from "next/navigation"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { TOrganizationRole } from "@formbricks/types/memberships"; interface OrganizationSettingsNavbarProps { @@ -22,7 +22,7 @@ export const OrganizationSettingsNavbar = ({ loading, }: OrganizationSettingsNavbarProps) => { const pathname = usePathname(); - const { isMember } = getAccessFlags(membershipRole); + const { isMember, isOwner } = getAccessFlags(membershipRole); const isPricingDisabled = isMember; const { t } = useTranslate(); @@ -59,6 +59,7 @@ export const OrganizationSettingsNavbar = ({ label: t("common.api_keys"), href: `/environments/${environmentId}/settings/api-keys`, current: pathname?.includes("/api-keys"), + hidden: !isOwner, }, ]; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx index 87476cc337..ccd0a48bab 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx @@ -1,8 +1,8 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; const Loading = async () => { const t = await getTranslate(); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx index 9cab9f6662..bc745af717 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx @@ -1,4 +1,5 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { Button } from "@/modules/ui/components/button"; @@ -8,7 +9,6 @@ import { getTranslate } from "@/tolgee/server"; import { CheckIcon } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; const Page = async (props) => { const params = await props.params; @@ -123,7 +123,7 @@ const Page = async (props) => {
-

{title}

+

{title}

{beta && } {soon && ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts index 7c7b68503f..b1ec4eaff7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts @@ -1,12 +1,12 @@ "use server"; import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils"; +import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { getResponseCountBySurveyId, getResponses } from "@formbricks/lib/response/service"; import { ZId } from "@formbricks/types/common"; import { ZResponseFilterCriteria } from "@formbricks/types/responses"; import { getSurveySummary } from "./summary/lib/surveySummary"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx index 89614bfb94..0f43e8c07b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation.tsx @@ -7,12 +7,12 @@ import { } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions"; import { getFormattedFilters } from "@/app/lib/surveys/surveys"; import { getResponseCountBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions"; +import { useIntervalWhenFocused } from "@/lib/utils/hooks/useIntervalWhenFocused"; import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation"; import { useTranslate } from "@tolgee/react"; import { InboxIcon, PresentationIcon } from "lucide-react"; import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useIntervalWhenFocused } from "@formbricks/lib/utils/hooks/useIntervalWhenFocused"; import { TSurvey } from "@formbricks/types/surveys/types"; interface SurveyAnalysisNavigationProps { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx index fe5477082e..1eb4de6d19 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx @@ -1,8 +1,8 @@ +import { getResponseCountBySurveyId } from "@/lib/response/service"; +import { getSurvey } from "@/lib/survey/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; type Props = { params: Promise<{ surveyId: string; environmentId: string }>; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx index 16d2f3a4b1..7ed50da2ce 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx @@ -13,9 +13,9 @@ import { getResponseCountBySurveySharingKeyAction, getResponsesBySurveySharingKeyAction, } from "@/app/share/[sharingKey]/actions"; +import { replaceHeadlineRecall } from "@/lib/utils/recall"; import { useParams, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TEnvironment } from "@formbricks/types/environment"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx new file mode 100644 index 0000000000..77ce5f41ca --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.test.tsx @@ -0,0 +1,165 @@ +import type { Cell, Row } from "@tanstack/react-table"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import type { TResponse, TResponseTableData } from "@formbricks/types/responses"; +import { ResponseTableCell } from "./ResponseTableCell"; + +const makeCell = ( + id: string, + size = 100, + first = false, + last = false, + content = "CellContent" +): Cell => + ({ + column: { + id, + getSize: () => size, + getIsFirstColumn: () => first, + getIsLastColumn: () => last, + getStart: () => 0, + columnDef: { cell: () => content }, + }, + id, + getContext: () => ({}), + }) as unknown as Cell; + +const makeRow = (id: string, selected = false): Row => + ({ id, getIsSelected: () => selected }) as unknown as Row; + +describe("ResponseTableCell", () => { + afterEach(() => { + cleanup(); + }); + + test("renders cell content", () => { + const cell = makeCell("col1"); + const row = makeRow("r1"); + render( + + ); + expect(screen.getByText("CellContent")).toBeDefined(); + }); + + test("calls setSelectedResponseId on cell click when not select column", async () => { + const cell = makeCell("col1"); + const row = makeRow("r1"); + const setSel = vi.fn(); + render( + + ); + await userEvent.click(screen.getByText("CellContent")); + expect(setSel).toHaveBeenCalledWith("r1"); + }); + + test("does not call setSelectedResponseId on select column click", async () => { + const cell = makeCell("select"); + const row = makeRow("r1"); + const setSel = vi.fn(); + render( + + ); + await userEvent.click(screen.getByText("CellContent")); + expect(setSel).not.toHaveBeenCalled(); + }); + + test("renders maximize icon for createdAt column and handles click", async () => { + const cell = makeCell("createdAt", 120, false, false); + const row = makeRow("r2"); + const setSel = vi.fn(); + render( + + ); + const btn = screen.getByRole("button", { name: /expand response/i }); + expect(btn).toBeDefined(); + await userEvent.click(btn); + expect(setSel).toHaveBeenCalledWith("r2"); + }); + + test("does not apply selected style when row.getIsSelected() is false", () => { + const cell = makeCell("col1"); + const row = makeRow("r1", false); + const { container } = render( + + ); + expect(container.firstChild).not.toHaveClass("bg-slate-100"); + }); + + test("applies selected style when row.getIsSelected() is true", () => { + const cell = makeCell("col1"); + const row = makeRow("r1", true); + const { container } = render( + + ); + expect(container.firstChild).toHaveClass("bg-slate-100"); + }); + + test("renders collapsed height class when isExpanded is false", () => { + const cell = makeCell("col1"); + const row = makeRow("r1"); + const { container } = render( + + ); + const inner = container.querySelector("div > div"); + expect(inner).toHaveClass("h-10"); + }); + + test("renders expanded height class when isExpanded is true", () => { + const cell = makeCell("col1"); + const row = makeRow("r1"); + const { container } = render( + + ); + const inner = container.querySelector("div > div"); + expect(inner).toHaveClass("h-full"); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.tsx index bc7a15c784..5cdc2294f1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell.tsx @@ -1,8 +1,8 @@ +import { cn } from "@/lib/cn"; import { getCommonPinningStyles } from "@/modules/ui/components/data-table/lib/utils"; import { TableCell } from "@/modules/ui/components/table"; import { Cell, Row, flexRender } from "@tanstack/react-table"; import { Maximize2Icon } from "lucide-react"; -import { cn } from "@formbricks/lib/cn"; import { TResponse, TResponseTableData } from "@formbricks/types/responses"; interface ResponseTableCellProps { @@ -35,11 +35,13 @@ export const ResponseTableCell = ({ // Conditional rendering of maximize icon const renderMaximizeIcon = cell.column.id === "createdAt" && ( -
-
+ ); return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx index c1eb5af132..1c20ad65f9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx @@ -1,5 +1,10 @@ "use client"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { processResponseData } from "@/lib/responses"; +import { getContactIdentifier } from "@/lib/utils/contact"; +import { getFormattedDateTimeString } from "@/lib/utils/datetime"; +import { recallToHeadline } from "@/lib/utils/recall"; import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse"; import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions"; import { getSelectionColumn } from "@/modules/ui/components/data-table"; @@ -9,11 +14,6 @@ import { ColumnDef } from "@tanstack/react-table"; import { TFnType } from "@tolgee/react"; import { CircleHelpIcon, EyeOffIcon, MailIcon, TagIcon } from "lucide-react"; import Link from "next/link"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { processResponseData } from "@formbricks/lib/responses"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; -import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime"; -import { recallToHeadline } from "@formbricks/lib/utils/recall"; import { TResponseTableData } from "@formbricks/types/responses"; import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types"; @@ -71,7 +71,11 @@ const getQuestionColumnsData = (
{QUESTIONS_ICON_MAP["matrix"]} - {getLocalizedValue(matrixRow, "default")} + + {getLocalizedValue(question.headline, "default") + + " - " + + getLocalizedValue(matrixRow, "default")} +
); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index 9932a21f60..2f039bbf44 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -3,22 +3,18 @@ import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; +import { MAX_RESPONSES_FOR_INSIGHT_GENERATION, RESPONSES_PER_PAGE, WEBAPP_URL } from "@/lib/constants"; +import { getSurveyDomain } from "@/lib/getSurveyUrl"; +import { getResponseCountBySurveyId } from "@/lib/response/service"; +import { getSurvey } from "@/lib/survey/service"; +import { getTagsByEnvironmentId } from "@/lib/tag/service"; +import { getUser } from "@/lib/user/service"; +import { findMatchingLocale } from "@/lib/utils/locale"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; -import { - MAX_RESPONSES_FOR_INSIGHT_GENERATION, - RESPONSES_PER_PAGE, - WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; -import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; -import { getUser } from "@formbricks/lib/user/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; const Page = async (props) => { const params = await props.params; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts index fc1f2a2df7..ffa5a3369d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts @@ -1,6 +1,7 @@ "use server"; import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate"; +import { getSurvey, updateSurvey } from "@/lib/survey/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; @@ -8,7 +9,6 @@ import { getOrganizationLogoUrl } from "@/modules/ee/whitelabel/email-customizat import { sendEmbedSurveyPreviewEmail } from "@/modules/email"; import { customAlphabet } from "nanoid"; import { z } from "zod"; -import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { ZId } from "@formbricks/types/common"; import { ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.tsx index 79a92779de..0e9b68515b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/AddressSummary.tsx @@ -1,11 +1,11 @@ "use client"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { ArrayResponse } from "@/modules/ui/components/array-response"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { useTranslate } from "@tolgee/react"; import Link from "next/link"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; import { TSurvey, TSurveyQuestionSummaryAddress } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx new file mode 100644 index 0000000000..f97f35b5e4 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.test.tsx @@ -0,0 +1,80 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { + TSurvey, + TSurveyConsentQuestion, + TSurveyQuestionSummaryConsent, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; +import { ConsentSummary } from "./ConsentSummary"; + +vi.mock( + "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader", + () => ({ + QuestionSummaryHeader: () =>
QuestionSummaryHeader
, + }) +); + +describe("ConsentSummary", () => { + afterEach(() => { + cleanup(); + }); + + const mockSetFilter = vi.fn(); + const questionSummary = { + question: { + id: "q1", + headline: { en: "Headline" }, + type: TSurveyQuestionTypeEnum.Consent, + } as unknown as TSurveyConsentQuestion, + accepted: { percentage: 60.5, count: 61 }, + dismissed: { percentage: 39.5, count: 40 }, + } as unknown as TSurveyQuestionSummaryConsent; + const survey = {} as TSurvey; + + test("renders accepted and dismissed with correct values", () => { + render(); + expect(screen.getByText("common.accepted")).toBeInTheDocument(); + expect(screen.getByText(/60\.5%/)).toBeInTheDocument(); + expect(screen.getByText(/61/)).toBeInTheDocument(); + expect(screen.getByText("common.dismissed")).toBeInTheDocument(); + expect(screen.getByText(/39\.5%/)).toBeInTheDocument(); + expect(screen.getByText(/40/)).toBeInTheDocument(); + }); + + test("calls setFilter with correct args on accepted click", async () => { + render(); + await userEvent.click(screen.getByText("common.accepted")); + expect(mockSetFilter).toHaveBeenCalledWith( + "q1", + { en: "Headline" }, + TSurveyQuestionTypeEnum.Consent, + "is", + "common.accepted" + ); + }); + + test("calls setFilter with correct args on dismissed click", async () => { + render(); + await userEvent.click(screen.getByText("common.dismissed")); + expect(mockSetFilter).toHaveBeenCalledWith( + "q1", + { en: "Headline" }, + TSurveyQuestionTypeEnum.Consent, + "is", + "common.dismissed" + ); + }); + + test("renders singular and plural response labels", () => { + const oneAndTwo = { + ...questionSummary, + accepted: { percentage: questionSummary.accepted.percentage, count: 1 }, + dismissed: { percentage: questionSummary.dismissed.percentage, count: 2 }, + }; + render(); + expect(screen.getByText(/1 common\.response/)).toBeInTheDocument(); + expect(screen.getByText(/2 common\.responses/)).toBeInTheDocument(); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx index 1234f0f906..3bffa7e961 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx @@ -41,11 +41,11 @@ export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSu return (
-
+
{summaryItems.map((summaryItem) => { return ( -
setFilter( @@ -74,7 +74,7 @@ export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSu
-
+ ); })}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.tsx index d549e18df0..2aecef1db6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary.tsx @@ -1,11 +1,11 @@ "use client"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { ArrayResponse } from "@/modules/ui/components/array-response"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { useTranslate } from "@tolgee/react"; import Link from "next/link"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; import { TSurvey, TSurveyQuestionSummaryContactInfo } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx index 5f9b00bb37..c31a59fc04 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx @@ -1,13 +1,13 @@ "use client"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; +import { formatDateWithOrdinal } from "@/lib/utils/datetime"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import Link from "next/link"; import { useState } from "react"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; -import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime"; import { TSurvey, TSurveyQuestionSummaryDate } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx index 1803cf84ce..0ae752f183 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx @@ -1,14 +1,14 @@ "use client"; +import { getOriginalFileNameFromUrl } from "@/lib/storage/utils"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { DownloadIcon, FileIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; -import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; import { TSurvey, TSurveyQuestionSummaryFileUpload } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; @@ -80,7 +80,7 @@ export const FileUploadSummary = ({ return (
-
+
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx index 357bd1bfdf..15f6485603 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx @@ -1,12 +1,12 @@ "use client"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { InboxIcon, Link, MessageSquareTextIcon } from "lucide-react"; import { useState } from "react"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; import { TEnvironment } from "@formbricks/types/environment"; import { TSurveyQuestionSummaryHiddenFields } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; @@ -28,7 +28,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi }; return (
-
+

{questionSummary.id}

@@ -76,7 +76,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
)}
-
+
{response.value}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx new file mode 100644 index 0000000000..35e5c134a2 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.test.tsx @@ -0,0 +1,47 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { MatrixQuestionSummary } from "./MatrixQuestionSummary"; + +vi.mock( + "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader", + () => ({ + QuestionSummaryHeader: () =>
QuestionSummaryHeader
, + }) +); + +describe("MatrixQuestionSummary", () => { + afterEach(() => { + cleanup(); + }); + + const survey = { id: "s1" } as any; + const questionSummary = { + question: { id: "q1", headline: "Q Head", type: "matrix" }, + data: [ + { + rowLabel: "Row1", + totalResponsesForRow: 10, + columnPercentages: [ + { column: "Yes", percentage: 50 }, + { column: "No", percentage: 50 }, + ], + }, + ], + } as any; + + test("renders headers and buttons, click triggers setFilter", async () => { + const setFilter = vi.fn(); + render(); + + // column headers + expect(screen.getByText("Yes")).toBeInTheDocument(); + expect(screen.getByText("No")).toBeInTheDocument(); + // row label + expect(screen.getByText("Row1")).toBeInTheDocument(); + // buttons + const btn = screen.getAllByRole("button", { name: /50/ }); + await userEvent.click(btn[0]); + expect(setFilter).toHaveBeenCalledWith("q1", "Q Head", "matrix", "Row1", "Yes"); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx index 59f19364be..e038e7c0aa 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx @@ -52,7 +52,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma - + {columns.map((column) => ( {questionSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => ( - ))} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx new file mode 100644 index 0000000000..5793f8d1d9 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx @@ -0,0 +1,275 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { MultipleChoiceSummary } from "./MultipleChoiceSummary"; + +vi.mock("@/modules/ui/components/avatars", () => ({ + PersonAvatar: ({ personId }: any) =>
{personId}
, +})); +vi.mock("./QuestionSummaryHeader", () => ({ QuestionSummaryHeader: () =>
})); + +describe("MultipleChoiceSummary", () => { + afterEach(() => { + cleanup(); + }); + + const baseSurvey = { id: "s1" } as any; + const envId = "env"; + + test("renders header and choice button", async () => { + const setFilter = vi.fn(); + const q = { + question: { + id: "q", + headline: "H", + type: "multipleChoiceSingle", + choices: [{ id: "c", label: { default: "C" } }], + }, + choices: { C: { value: "C", count: 1, percentage: 100, others: [] } }, + type: "multipleChoiceSingle", + selectionCount: 0, + } as any; + render( + + ); + expect(screen.getByTestId("header")).toBeDefined(); + const btn = screen.getByText("1 - C"); + await userEvent.click(btn); + expect(setFilter).toHaveBeenCalledWith( + "q", + "H", + "multipleChoiceSingle", + "environments.surveys.summary.includes_either", + ["C"] + ); + }); + + test("renders others and load more for link", async () => { + const setFilter = vi.fn(); + const others = Array.from({ length: 12 }, (_, i) => ({ + value: `O${i}`, + contact: { id: `id${i}` }, + contactAttributes: {}, + })); + const q = { + question: { + id: "q2", + headline: "H2", + type: "multipleChoiceMulti", + choices: [{ id: "c2", label: { default: "X" } }], + }, + choices: { X: { value: "X", count: 0, percentage: 0, others } }, + type: "multipleChoiceMulti", + selectionCount: 5, + } as any; + render( + + ); + expect(screen.getByText("environments.surveys.summary.other_values_found")).toBeDefined(); + expect(screen.getAllByText(/^O/)).toHaveLength(10); + await userEvent.click(screen.getByText("common.load_more")); + expect(screen.getAllByText(/^O/)).toHaveLength(12); + }); + + test("renders others with avatar for app", () => { + const setFilter = vi.fn(); + const others = [{ value: "Val", contact: { id: "uid" }, contactAttributes: {} }]; + const q = { + question: { + id: "q3", + headline: "H3", + type: "multipleChoiceMulti", + choices: [{ id: "c3", label: { default: "L" } }], + }, + choices: { L: { value: "L", count: 0, percentage: 0, others } }, + type: "multipleChoiceMulti", + selectionCount: 1, + } as any; + render( + + ); + expect(screen.getByTestId("avatar")).toBeDefined(); + expect(screen.getByText("Val")).toBeDefined(); + }); + + test("places choice without others before one with others", () => { + const setFilter = vi.fn(); + const choices = { + A: { value: "A", count: 0, percentage: 0, others: [] }, + B: { value: "B", count: 0, percentage: 0, others: [{ value: "x" }] }, + }; + render( + + ); + const btns = screen.getAllByRole("button"); + expect(btns[0]).toHaveTextContent("2 - A"); + expect(btns[1]).toHaveTextContent("1 - B"); + }); + + test("sorts by count when neither has others", () => { + const setFilter = vi.fn(); + const choices = { + X: { value: "X", count: 1, percentage: 50, others: [] }, + Y: { value: "Y", count: 2, percentage: 50, others: [] }, + }; + render( + + ); + const btns = screen.getAllByRole("button"); + expect(btns[0]).toHaveTextContent("2 - Y50%2 common.selections"); + expect(btns[1]).toHaveTextContent("1 - X50%1 common.selection"); + }); + + test("places choice with others after one without when reversed inputs", () => { + const setFilter = vi.fn(); + const choices = { + C: { value: "C", count: 1, percentage: 0, others: [{ value: "z" }] }, + D: { value: "D", count: 1, percentage: 0, others: [] }, + }; + render( + + ); + const btns = screen.getAllByRole("button"); + expect(btns[0]).toHaveTextContent("2 - D"); + expect(btns[1]).toHaveTextContent("1 - C"); + }); + + test("multi type non-other uses includes_all", async () => { + const setFilter = vi.fn(); + const q = { + question: { + id: "q4", + headline: "H4", + type: "multipleChoiceMulti", + choices: [ + { id: "other", label: { default: "O" } }, + { id: "c4", label: { default: "C4" } }, + ], + }, + choices: { + O: { value: "O", count: 1, percentage: 10, others: [] }, + C4: { value: "C4", count: 2, percentage: 20, others: [] }, + }, + type: "multipleChoiceMulti", + selectionCount: 0, + } as any; + + render( + + ); + + const btn = screen.getByText("2 - C4"); + await userEvent.click(btn); + expect(setFilter).toHaveBeenCalledWith( + "q4", + "H4", + "multipleChoiceMulti", + "environments.surveys.summary.includes_all", + ["C4"] + ); + }); + + test("multi type other uses includes_either", async () => { + const setFilter = vi.fn(); + const q = { + question: { + id: "q5", + headline: "H5", + type: "multipleChoiceMulti", + choices: [ + { id: "other", label: { default: "O5" } }, + { id: "c5", label: { default: "C5" } }, + ], + }, + choices: { + O5: { value: "O5", count: 1, percentage: 10, others: [] }, + C5: { value: "C5", count: 0, percentage: 0, others: [] }, + }, + type: "multipleChoiceMulti", + selectionCount: 0, + } as any; + + render( + + ); + + const btn = screen.getByText("2 - O5"); + await userEvent.click(btn); + expect(setFilter).toHaveBeenCalledWith( + "q5", + "H5", + "multipleChoiceMulti", + "environments.surveys.summary.includes_either", + ["O5"] + ); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx index 846a458b57..235ba3e422 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx @@ -1,13 +1,13 @@ "use client"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { Button } from "@/modules/ui/components/button"; import { ProgressBar } from "@/modules/ui/components/progress-bar"; import { useTranslate } from "@tolgee/react"; import { InboxIcon } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; +import { Fragment, useState } from "react"; import { TI18nString, TSurvey, @@ -45,10 +45,15 @@ export const MultipleChoiceSummary = ({ const otherValue = questionSummary.question.choices.find((choice) => choice.id === "other")?.label.default; // sort by count and transform to array const results = Object.values(questionSummary.choices).sort((a, b) => { - if (a.others) return 1; // Always put a after b if a has 'others' - if (b.others) return -1; // Always put b after a if b has 'others' + const aHasOthers = (a.others?.length ?? 0) > 0; + const bHasOthers = (b.others?.length ?? 0) > 0; - return b.count - a.count; // Sort by count + // if one has “others” and the other doesn’t, push the one with others to the end + if (aHasOthers && !bHasOthers) return 1; + if (!aHasOthers && bHasOthers) return -1; + + // if they’re “tied” on having others, fall back to count + return b.count - a.count; }); const handleLoadMore = (e: React.MouseEvent) => { @@ -78,42 +83,43 @@ export const MultipleChoiceSummary = ({ ) : undefined } /> -
+
{results.map((result, resultsIdx) => ( -
- setFilter( - questionSummary.question.id, - questionSummary.question.headline, - questionSummary.question.type, - questionSummary.type === "multipleChoiceSingle" || otherValue === result.value - ? t("environments.surveys.summary.includes_either") - : t("environments.surveys.summary.includes_all"), - [result.value] - ) - }> -
-
-

- {results.length - resultsIdx} - {result.value} -

-
-

- {convertFloatToNDecimal(result.percentage, 2)}% + +

-
- -
+
+ +
+ {result.others && result.others.length > 0 && ( -
e.stopPropagation()}> +
{t("environments.surveys.summary.other_values_found")} @@ -124,11 +130,9 @@ export const MultipleChoiceSummary = ({ .filter((otherValue) => otherValue.value !== "") .slice(0, visibleOtherResponses) .map((otherValue, idx) => ( -
+
{surveyType === "link" && ( -
+
{otherValue.value}
)} @@ -139,7 +143,6 @@ export const MultipleChoiceSummary = ({ ? `/environments/${environmentId}/contacts/${otherValue.contact.id}` : { pathname: null } } - key={idx} className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
{otherValue.value} @@ -163,7 +166,7 @@ export const MultipleChoiceSummary = ({ )}
)} -
+ ))}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx new file mode 100644 index 0000000000..125c4e6754 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx @@ -0,0 +1,60 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TSurveyQuestionSummaryNps } from "@formbricks/types/surveys/types"; +import { NPSSummary } from "./NPSSummary"; + +vi.mock("@/modules/ui/components/progress-bar", () => ({ + ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => ( +
{`${progress}-${barColor}`}
+ ), + HalfCircle: ({ value }: { value: number }) =>
{value}
, +})); +vi.mock("./QuestionSummaryHeader", () => ({ + QuestionSummaryHeader: () =>
, +})); + +describe("NPSSummary", () => { + afterEach(() => { + cleanup(); + }); + + const baseQuestion = { id: "q1", headline: "Question?", type: "nps" as const }; + const summary = { + question: baseQuestion, + promoters: { count: 2, percentage: 50 }, + passives: { count: 1, percentage: 25 }, + detractors: { count: 1, percentage: 25 }, + dismissed: { count: 0, percentage: 0 }, + score: 25, + } as unknown as TSurveyQuestionSummaryNps; + const survey = {} as any; + + test("renders header, groups, ProgressBar and HalfCircle", () => { + render( {}} />); + expect(screen.getByTestId("question-summary-header")).toBeDefined(); + ["promoters", "passives", "detractors", "dismissed"].forEach((g) => + expect(screen.getByText(g)).toBeDefined() + ); + expect(screen.getAllByTestId("progress-bar")[0]).toBeDefined(); + expect(screen.getByTestId("half-circle")).toHaveTextContent("25"); + }); + + test.each([ + ["promoters", "environments.surveys.summary.includes_either", ["9", "10"]], + ["passives", "environments.surveys.summary.includes_either", ["7", "8"]], + ["detractors", "environments.surveys.summary.is_less_than", "7"], + ["dismissed", "common.skipped", undefined], + ])("clicking %s calls setFilter correctly", async (group, cmp, vals) => { + const setFilter = vi.fn(); + render(); + await userEvent.click(screen.getByText(group)); + expect(setFilter).toHaveBeenCalledWith( + baseQuestion.id, + baseQuestion.headline, + baseQuestion.type, + cmp, + vals + ); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx index dd01c999a4..948d41e34e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx @@ -62,14 +62,17 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro return (
-
+
{["promoters", "passives", "detractors", "dismissed"].map((group) => ( -
applyFilter(group)}> + ))}
-
+
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx index 3d97eea0ab..4e6b1e6c49 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx @@ -1,5 +1,7 @@ "use client"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { renderHyperlinkedContent } from "@/modules/analysis/utils"; import { InsightView } from "@/modules/ee/insights/components/insights-view"; import { PersonAvatar } from "@/modules/ui/components/avatars"; @@ -9,8 +11,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { useTranslate } from "@tolgee/react"; import Link from "next/link"; import { useState } from "react"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx new file mode 100644 index 0000000000..732f03dcdc --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.test.tsx @@ -0,0 +1,91 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; +import { PictureChoiceSummary } from "./PictureChoiceSummary"; + +vi.mock("@/modules/ui/components/progress-bar", () => ({ + ProgressBar: ({ progress }: { progress: number }) => ( +
+ ), +})); +vi.mock("./QuestionSummaryHeader", () => ({ + QuestionSummaryHeader: ({ additionalInfo }: any) =>
{additionalInfo}
, +})); + +// mock next image +vi.mock("next/image", () => ({ + __esModule: true, + // eslint-disable-next-line @next/next/no-img-element + default: ({ src }: { src: string }) => , +})); + +const survey = {} as TSurvey; + +describe("PictureChoiceSummary", () => { + afterEach(() => { + cleanup(); + }); + + test("renders choices with formatted percentages and counts", () => { + const choices = [ + { id: "1", imageUrl: "img1.png", percentage: 33.3333, count: 1 }, + { id: "2", imageUrl: "img2.png", percentage: 66.6667, count: 2 }, + ]; + const questionSummary = { + choices, + question: { id: "q1", type: TSurveyQuestionTypeEnum.PictureSelection, headline: "H", allowMulti: true }, + selectionCount: 3, + } as any; + render( {}} />); + + expect(screen.getAllByRole("button")).toHaveLength(2); + expect(screen.getByText("33.33%")).toBeInTheDocument(); + expect(screen.getByText("1 common.selection")).toBeInTheDocument(); + expect(screen.getByText("2 common.selections")).toBeInTheDocument(); + expect(screen.getAllByTestId("progress-bar")).toHaveLength(2); + }); + + test("calls setFilter with correct args on click", async () => { + const choices = [{ id: "1", imageUrl: "img1.png", percentage: 25, count: 10 }]; + const questionSummary = { + choices, + question: { + id: "q1", + type: TSurveyQuestionTypeEnum.PictureSelection, + headline: "H1", + allowMulti: true, + }, + selectionCount: 10, + } as any; + const setFilter = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button")); + expect(setFilter).toHaveBeenCalledWith( + "q1", + "H1", + TSurveyQuestionTypeEnum.PictureSelection, + "environments.surveys.summary.includes_all", + ["environments.surveys.edit.picture_idx"] + ); + }); + + test("hides additionalInfo when allowMulti is false", () => { + const choices = [{ id: "1", imageUrl: "img1.png", percentage: 50, count: 5 }]; + const questionSummary = { + choices, + question: { + id: "q1", + type: TSurveyQuestionTypeEnum.PictureSelection, + headline: "H2", + allowMulti: false, + }, + selectionCount: 5, + } as any; + render( {}} />); + + expect(screen.getByTestId("header")).toBeEmptyDOMElement(); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx index a942d1c2dd..fc7a7b2268 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx @@ -43,10 +43,10 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic ) : undefined } /> -
+
{results.map((result, index) => ( -
setFilter( @@ -79,7 +79,7 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic

-
+ ))}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.tsx index 2b6adca6d3..1500882b15 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/QuestionSummaryHeader.tsx @@ -1,10 +1,12 @@ "use client"; +import { recallToHeadline } from "@/lib/utils/recall"; +import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils"; import { getQuestionTypes } from "@/modules/survey/lib/questions"; +import { SettingsId } from "@/modules/ui/components/settings-id"; import { useTranslate } from "@tolgee/react"; import { InboxIcon } from "lucide-react"; import type { JSX } from "react"; -import { recallToHeadline } from "@formbricks/lib/utils/recall"; import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types"; interface HeadProps { @@ -22,31 +24,15 @@ export const QuestionSummaryHeader = ({ }: HeadProps) => { const { t } = useTranslate(); const questionType = getQuestionTypes(t).find((type) => type.id === questionSummary.question.type); - // formats the text to highlight specific parts of the text with slashes - const formatTextWithSlashes = (text: string): (string | JSX.Element)[] => { - const regex = /\/(.*?)\\/g; - const parts = text.split(regex); - - return parts.map((part, index) => { - // Check if the part was inside slashes - if (index % 2 !== 0) { - return ( - - @{part} - - ); - } else { - return part; - } - }); - }; return ( -
+

{formatTextWithSlashes( - recallToHeadline(questionSummary.question.headline, survey, true, "default")["default"] + recallToHeadline(questionSummary.question.headline, survey, true, "default")["default"], + "@", + ["text-lg"] )}

@@ -69,6 +55,7 @@ export const QuestionSummaryHeader = ({
)}
+
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx new file mode 100644 index 0000000000..da1e77641c --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.test.tsx @@ -0,0 +1,87 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TSurveyQuestionSummaryRating } from "@formbricks/types/surveys/types"; +import { RatingSummary } from "./RatingSummary"; + +vi.mock("./QuestionSummaryHeader", () => ({ + QuestionSummaryHeader: ({ additionalInfo }: any) =>
{additionalInfo}
, +})); + +describe("RatingSummary", () => { + afterEach(() => { + cleanup(); + }); + + test("renders overall average and choices", () => { + const questionSummary = { + question: { + id: "q1", + scale: "star", + headline: "Headline", + type: "rating", + range: [1, 5], + isColorCodingEnabled: false, + }, + average: 3.1415, + choices: [ + { rating: 1, percentage: 50, count: 2 }, + { rating: 2, percentage: 50, count: 3 }, + ], + dismissed: { count: 0 }, + } as unknown as TSurveyQuestionSummaryRating; + const survey = {}; + const setFilter = vi.fn(); + render(); + expect(screen.getByText("environments.surveys.summary.overall: 3.14")).toBeDefined(); + expect(screen.getAllByRole("button")).toHaveLength(2); + }); + + test("clicking a choice calls setFilter with correct args", async () => { + const questionSummary = { + question: { + id: "q1", + scale: "number", + headline: "Headline", + type: "rating", + range: [1, 5], + isColorCodingEnabled: false, + }, + average: 2, + choices: [{ rating: 3, percentage: 100, count: 1 }], + dismissed: { count: 0 }, + } as unknown as TSurveyQuestionSummaryRating; + const survey = {}; + const setFilter = vi.fn(); + render(); + await userEvent.click(screen.getByRole("button")); + expect(setFilter).toHaveBeenCalledWith( + "q1", + "Headline", + "rating", + "environments.surveys.summary.is_equal_to", + "3" + ); + }); + + test("renders dismissed section when dismissed count > 0", () => { + const questionSummary = { + question: { + id: "q1", + scale: "smiley", + headline: "Headline", + type: "rating", + range: [1, 5], + isColorCodingEnabled: false, + }, + average: 4, + choices: [], + dismissed: { count: 1 }, + } as unknown as TSurveyQuestionSummaryRating; + const survey = {}; + const setFilter = vi.fn(); + render(); + expect(screen.getByText("common.dismissed")).toBeDefined(); + expect(screen.getByText("1 common.response")).toBeDefined(); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx index d2de76387d..2234a70584 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx @@ -50,10 +50,10 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
} /> -
+
{questionSummary.choices.map((result) => ( -
setFilter( @@ -85,7 +85,7 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm

-
+ ))}
{questionSummary.dismissed && questionSummary.dismissed.count > 0 && ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx index 5478eff0a5..507a48cfe9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx @@ -1,11 +1,11 @@ "use client"; +import { recallToHeadline } from "@/lib/utils/recall"; +import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils"; import { getQuestionIcon } from "@/modules/survey/lib/questions"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; import { useTranslate } from "@tolgee/react"; import { TimerIcon } from "lucide-react"; -import { JSX } from "react"; -import { recallToHeadline } from "@formbricks/lib/utils/recall"; import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types"; interface SummaryDropOffsProps { @@ -20,24 +20,6 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => { return ; }; - const formatTextWithSlashes = (text: string): (string | JSX.Element)[] => { - const regex = /\/(.*?)\\/g; - const parts = text.split(regex); - - return parts.map((part, index) => { - // Check if the part was inside slashes - if (index % 2 !== 0) { - return ( - - @{part} - - ); - } else { - return part; - } - }); - }; - return (
@@ -73,14 +55,16 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => { survey, true, "default" - )["default"] + )["default"], + "@", + ["text-lg"] )}

-
+
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
-
{quesDropOff.impressions}
+
{quesDropOff.impressions}
{quesDropOff.dropOffCount} ({Math.round(quesDropOff.dropOffPercentage)}%) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx index 70be3b81b1..2a96131e87 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx @@ -21,11 +21,11 @@ import { RankingSummary } from "@/app/(app)/environments/[environmentId]/surveys import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary"; import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox"; +import { getLocalizedValue } from "@/lib/i18n/utils"; import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler"; import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader"; import { useTranslate } from "@tolgee/react"; import { toast } from "react-hot-toast"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { TEnvironment } from "@formbricks/types/environment"; import { TI18nString, TSurveyQuestionId, TSurveySummary } from "@formbricks/types/surveys/types"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx new file mode 100644 index 0000000000..6c7b0b63bf --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx @@ -0,0 +1,135 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { useState } from "react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { SummaryMetadata } from "./SummaryMetadata"; + +vi.mock("lucide-react", () => ({ + ChevronDownIcon: () =>
, + ChevronUpIcon: () =>
, +})); +vi.mock("@/modules/ui/components/tooltip", () => ({ + TooltipProvider: ({ children }) => <>{children}, + Tooltip: ({ children }) => <>{children}, + TooltipTrigger: ({ children }) => <>{children}, + TooltipContent: ({ children }) => <>{children}, +})); + +const baseSummary = { + completedPercentage: 50, + completedResponses: 2, + displayCount: 3, + dropOffPercentage: 25, + dropOffCount: 1, + startsPercentage: 75, + totalResponses: 4, + ttcAverage: 65000, +}; + +describe("SummaryMetadata", () => { + afterEach(() => { + cleanup(); + }); + + test("renders loading skeletons when isLoading=true", () => { + const { container } = render( + {}} + surveySummary={baseSummary} + isLoading={true} + /> + ); + + expect(container.getElementsByClassName("animate-pulse")).toHaveLength(5); + }); + + test("renders all stats and formats time correctly, toggles dropOffs icon", async () => { + const Wrapper = () => { + const [show, setShow] = useState(false); + return ( + + ); + }; + render(); + // impressions, starts, completed, drop_offs, ttc + expect(screen.getByText("environments.surveys.summary.impressions")).toBeInTheDocument(); + expect(screen.getByText("3")).toBeInTheDocument(); + expect(screen.getByText("75%")).toBeInTheDocument(); + expect(screen.getByText("4")).toBeInTheDocument(); + expect(screen.getByText("50%")).toBeInTheDocument(); + expect(screen.getByText("2")).toBeInTheDocument(); + expect(screen.getByText("25%")).toBeInTheDocument(); + expect(screen.getByText("1")).toBeInTheDocument(); + expect(screen.getByText("1m 5.00s")).toBeInTheDocument(); + const btn = screen.getByRole("button"); + expect(screen.queryByTestId("down")).toBeInTheDocument(); + await userEvent.click(btn); + expect(screen.queryByTestId("up")).toBeInTheDocument(); + }); + + test("formats time correctly when < 60 seconds", () => { + const smallSummary = { ...baseSummary, ttcAverage: 5000 }; + render( + {}} + surveySummary={smallSummary} + isLoading={false} + /> + ); + expect(screen.getByText("5.00s")).toBeInTheDocument(); + }); + + test("renders '-' for dropOffCount=0 and still toggles icon", async () => { + const zeroSummary = { ...baseSummary, dropOffCount: 0 }; + const Wrapper = () => { + const [show, setShow] = useState(false); + return ( + + ); + }; + render(); + expect(screen.getAllByText("-")).toHaveLength(1); + const btn = screen.getByRole("button"); + expect(screen.queryByTestId("down")).toBeInTheDocument(); + await userEvent.click(btn); + expect(screen.queryByTestId("up")).toBeInTheDocument(); + }); + + test("renders '-' for displayCount=0", () => { + const dispZero = { ...baseSummary, displayCount: 0 }; + render( + {}} + surveySummary={dispZero} + isLoading={false} + /> + ); + expect(screen.getAllByText("-")).toHaveLength(1); + }); + + test("renders '-' for totalResponses=0", () => { + const totZero = { ...baseSummary, totalResponses: 0 }; + render( + {}} + surveySummary={totZero} + isLoading={false} + /> + ); + expect(screen.getAllByText("-")).toHaveLength(1); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx index 6f3cae5f45..b1ee890bf1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx @@ -71,6 +71,8 @@ export const SummaryMetadata = ({ ttcAverage, } = surveySummary; const { t } = useTranslate(); + const displayCountValue = dropOffCount === 0 ? - : dropOffCount; + return (
@@ -99,9 +101,7 @@ export const SummaryMetadata = ({ -
setShowDropOffs(!showDropOffs)} - className="group flex h-full w-full cursor-pointer flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm"> +
{t("environments.surveys.summary.drop_offs")} {`${Math.round(dropOffPercentage)}%` !== "NaN%" && !isLoading && ( @@ -112,20 +112,20 @@ export const SummaryMetadata = ({ {isLoading ? (
- ) : dropOffCount === 0 ? ( - - ) : ( - dropOffCount + displayCountValue )}
{!isLoading && ( - + )}
@@ -135,6 +135,7 @@ export const SummaryMetadata = ({
+ ({ +vi.mock("@/lib/constants", () => ({ IS_FORMBRICKS_CLOUD: false, ENCRYPTION_KEY: "test", ENTERPRISE_LICENSE_KEY: "test", @@ -85,7 +85,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => { cleanup(); }); - it("calls copySurveyLink and clipboard.writeText on success", async () => { + test("calls copySurveyLink and clipboard.writeText on success", async () => { render( { }); }); - it("shows error toast on failure", async () => { + test("shows error toast on failure", async () => { refreshSingleUseIdSpy.mockImplementationOnce(() => Promise.reject(new Error("fail"))); render( { const t = await getTranslate(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights.ts index 81c2739313..329bd5f7c1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights.ts @@ -1,10 +1,10 @@ +import { cache } from "@/lib/cache"; import { documentCache } from "@/lib/cache/document"; +import { INSIGHTS_PER_PAGE } from "@/lib/constants"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { INSIGHTS_PER_PAGE } from "@formbricks/lib/constants"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts index 81cc7a8e73..9692a07b2b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts @@ -1,20 +1,20 @@ import "server-only"; import { getInsightsBySurveyIdQuestionId } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights"; +import { cache } from "@/lib/cache"; +import { RESPONSES_PER_PAGE } from "@/lib/constants"; +import { displayCache } from "@/lib/display/cache"; +import { getDisplayCountBySurveyId } from "@/lib/display/service"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { responseCache } from "@/lib/response/cache"; +import { getResponseCountBySurveyId } from "@/lib/response/service"; +import { buildWhereClause } from "@/lib/response/utils"; +import { surveyCache } from "@/lib/survey/cache"; +import { getSurvey } from "@/lib/survey/service"; +import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { RESPONSES_PER_PAGE } from "@formbricks/lib/constants"; -import { displayCache } from "@formbricks/lib/display/cache"; -import { getDisplayCountBySurveyId } from "@formbricks/lib/display/service"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { buildWhereClause } from "@formbricks/lib/response/utils"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { getSurvey } from "@formbricks/lib/survey/service"; -import { evaluateLogic, performActions } from "@formbricks/lib/surveyLogic/utils"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId, ZOptionalNumber } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index d098eceabf..cf14236a9e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -3,6 +3,16 @@ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/s import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; +import { + DEFAULT_LOCALE, + DOCUMENTS_PER_PAGE, + MAX_RESPONSES_FOR_INSIGHT_GENERATION, + WEBAPP_URL, +} from "@/lib/constants"; +import { getSurveyDomain } from "@/lib/getSurveyUrl"; +import { getResponseCountBySurveyId } from "@/lib/response/service"; +import { getSurvey } from "@/lib/survey/service"; +import { getUser } from "@/lib/user/service"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -10,16 +20,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { SettingsId } from "@/modules/ui/components/settings-id"; import { getTranslate } from "@/tolgee/server"; import { notFound } from "next/navigation"; -import { - DEFAULT_LOCALE, - DOCUMENTS_PER_PAGE, - MAX_RESPONSES_FOR_INSIGHT_GENERATION, - WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; -import { getUser } from "@formbricks/lib/user/service"; const SurveyPage = async (props: { params: Promise<{ environmentId: string; surveyId: string }> }) => { const params = await props.params; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts index 4e1336da47..6fb9fe7604 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts @@ -1,15 +1,15 @@ "use server"; +import { getOrganization } from "@/lib/organization/service"; +import { getResponseDownloadUrl, getResponseFilteringValues } from "@/lib/response/service"; +import { getSurvey, updateSurvey } from "@/lib/survey/service"; +import { getTagsByEnvironmentId } from "@/lib/tag/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions"; import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { z } from "zod"; -import { getOrganization } from "@formbricks/lib/organization/service"; -import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service"; -import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; -import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; import { ZId } from "@formbricks/types/common"; import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors"; import { ZResponseFilterCriteria } from "@formbricks/types/responses"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx index ef7f887151..c054b33776 100755 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx @@ -7,6 +7,7 @@ import { import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions"; import { getFormattedFilters, getTodayDate } from "@/app/lib/surveys/surveys"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { useClickOutside } from "@/lib/utils/hooks/useClickOutside"; import { Calendar } from "@/modules/ui/components/calendar"; import { DropdownMenu, @@ -34,7 +35,6 @@ import { ArrowDownToLineIcon, ChevronDown, ChevronUp, DownloadIcon } from "lucid import { useParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; -import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside"; import { TSurvey } from "@formbricks/types/surveys/types"; import { ResponseFilter } from "./ResponseFilter"; @@ -390,7 +390,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { value && handleDatePickerClose(); }}> -
+
{t("common.download")} @@ -416,14 +416,14 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { onClick={() => { handleDowndloadResponses(FilterDownload.FILTER, "csv"); }}> -

{t("environments.surveys.summary.current_selection_csv")}

+

{t("environments.surveys.summary.filtered_responses_csv")}

{ handleDowndloadResponses(FilterDownload.FILTER, "xlsx"); }}>

- {t("environments.surveys.summary.current_selection_excel")} + {t("environments.surveys.summary.filtered_responses_excel")}

diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx new file mode 100644 index 0000000000..04824a5b8a --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.test.tsx @@ -0,0 +1,88 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { QuestionFilterComboBox } from "./QuestionFilterComboBox"; + +describe("QuestionFilterComboBox", () => { + afterEach(() => { + cleanup(); + }); + + const defaultProps = { + filterOptions: ["A", "B"], + filterComboBoxOptions: ["X", "Y"], + filterValue: undefined, + filterComboBoxValue: undefined, + onChangeFilterValue: vi.fn(), + onChangeFilterComboBoxValue: vi.fn(), + handleRemoveMultiSelect: vi.fn(), + disabled: false, + }; + + test("renders select placeholders", () => { + render(); + expect(screen.getAllByText(/common.select\.../).length).toBe(2); + }); + + test("calls onChangeFilterValue when selecting filter", async () => { + render(); + await userEvent.click(screen.getAllByRole("button")[0]); + await userEvent.click(screen.getByText("A")); + expect(defaultProps.onChangeFilterValue).toHaveBeenCalledWith("A"); + }); + + test("calls onChangeFilterComboBoxValue when selecting combo box option", async () => { + render(); + await userEvent.click(screen.getAllByRole("button")[1]); + await userEvent.click(screen.getByText("X")); + expect(defaultProps.onChangeFilterComboBoxValue).toHaveBeenCalledWith("X"); + }); + + test("multi-select removal works", async () => { + const props = { + ...defaultProps, + type: "multipleChoiceMulti", + filterValue: "A", + filterComboBoxValue: ["X", "Y"], + }; + render(); + const removeButtons = screen.getAllByRole("button", { name: /X/i }); + await userEvent.click(removeButtons[0]); + expect(props.handleRemoveMultiSelect).toHaveBeenCalledWith(["Y"]); + }); + + test("disabled state prevents opening", async () => { + render(); + await userEvent.click(screen.getAllByRole("button")[0]); + expect(screen.queryByText("A")).toBeNull(); + }); + + test("handles object options correctly", async () => { + const obj = { default: "Obj1", en: "ObjEN" }; + const props = { + ...defaultProps, + type: "multipleChoiceMulti", + filterValue: "A", + filterComboBoxOptions: [obj], + filterComboBoxValue: [], + } as any; + render(); + await userEvent.click(screen.getAllByRole("button")[1]); + await userEvent.click(screen.getByText("Obj1")); + expect(props.onChangeFilterComboBoxValue).toHaveBeenCalledWith(["Obj1"]); + }); + + test("prevent combo-box opening when filterValue is Submitted", async () => { + const props = { ...defaultProps, type: "NPS", filterValue: "Submitted" } as any; + render(); + await userEvent.click(screen.getAllByRole("button")[1]); + expect(screen.queryByText("X")).toHaveClass("data-[disabled='true']:opacity-50"); + }); + + test("prevent combo-box opening when filterValue is Skipped", async () => { + const props = { ...defaultProps, type: "Rating", filterValue: "Skipped" } as any; + render(); + await userEvent.click(screen.getAllByRole("button")[1]); + expect(screen.queryByText("X")).toHaveClass("data-[disabled='true']:opacity-50"); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx index c9879e6344..5dbe2e1e1d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx @@ -1,6 +1,8 @@ "use client"; import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { useClickOutside } from "@/lib/utils/hooks/useClickOutside"; import { Command, CommandEmpty, @@ -19,8 +21,6 @@ import { useTranslate } from "@tolgee/react"; import clsx from "clsx"; import { ChevronDown, ChevronUp, X } from "lucide-react"; import * as React from "react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; type QuestionFilterComboBoxProps = { @@ -81,6 +81,39 @@ export const QuestionFilterComboBox = ({ .includes(searchQuery.toLowerCase()) ); + const filterComboBoxItem = !Array.isArray(filterComboBoxValue) ? ( +

{filterComboBoxValue}

+ ) : ( +
+ {typeof filterComboBoxValue !== "string" && + filterComboBoxValue?.map((o, index) => ( + + ))} +
+ ); + + const commandItemOnSelect = (o: string) => { + if (!isMultiple) { + onChangeFilterComboBoxValue(typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o); + } else { + onChangeFilterComboBoxValue( + Array.isArray(filterComboBoxValue) + ? [...filterComboBoxValue, typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o] + : [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o] + ); + } + if (!isMultiple) { + setOpen(false); + } + }; + return (
{filterOptions && filterOptions?.length <= 1 ? ( @@ -96,7 +129,7 @@ export const QuestionFilterComboBox = ({
@@ -130,39 +163,37 @@ export const QuestionFilterComboBox = ({ )}
!disabled && !isDisabledComboBox && filterValue && setOpen(true)} className={clsx( - "group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm", - disabled || isDisabledComboBox || !filterValue ? "opacity-50" : "cursor-pointer" + "group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm" )}> - {filterComboBoxValue && filterComboBoxValue?.length > 0 ? ( - !Array.isArray(filterComboBoxValue) ? ( -

{filterComboBoxValue}

- ) : ( -
- {typeof filterComboBoxValue !== "string" && - filterComboBoxValue?.map((o, index) => ( - - ))} -
- ) + {filterComboBoxValue && filterComboBoxValue.length > 0 ? ( + filterComboBoxItem ) : ( -

{t("common.select")}...

+ )} -
+
+
{open && ( @@ -183,21 +214,7 @@ export const QuestionFilterComboBox = ({ {filteredOptions?.map((o, index) => ( { - !isMultiple - ? onChangeFilterComboBoxValue( - typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o - ) - : onChangeFilterComboBoxValue( - Array.isArray(filterComboBoxValue) - ? [ - ...filterComboBoxValue, - typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o, - ] - : [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o] - ); - !isMultiple && setOpen(false); - }} + onSelect={() => commandItemOnSelect(o)} className="cursor-pointer"> {typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx new file mode 100644 index 0000000000..fa12d8920c --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.test.tsx @@ -0,0 +1,55 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { OptionsType, QuestionOption, QuestionOptions, QuestionsComboBox } from "./QuestionsComboBox"; + +describe("QuestionsComboBox", () => { + afterEach(() => { + cleanup(); + }); + + const mockOptions: QuestionOptions[] = [ + { + header: OptionsType.QUESTIONS, + option: [{ label: "Q1", type: OptionsType.QUESTIONS, questionType: undefined, id: "1" }], + }, + { + header: OptionsType.TAGS, + option: [{ label: "Tag1", type: OptionsType.TAGS, id: "t1" }], + }, + ]; + + test("renders selected label when closed", () => { + const selected: Partial = { label: "Q1", type: OptionsType.QUESTIONS, id: "1" }; + render( {}} />); + expect(screen.getByText("Q1")).toBeInTheDocument(); + }); + + test("opens dropdown, selects an option, and closes", async () => { + let currentSelected: Partial = {}; + const onChange = vi.fn((option) => { + currentSelected = option; + }); + + const { rerender } = render( + + ); + + // Open the dropdown + await userEvent.click(screen.getByRole("button")); + expect(screen.getByPlaceholderText("common.search...")).toBeInTheDocument(); + + // Select an option + await userEvent.click(screen.getByText("Q1")); + + // Check if onChange was called + expect(onChange).toHaveBeenCalledWith(mockOptions[0].option[0]); + + // Rerender with the new selected value + rerender(); + + // Check if the input is gone and the selected item is displayed + expect(screen.queryByPlaceholderText("common.search...")).toBeNull(); + expect(screen.getByText("Q1")).toBeInTheDocument(); // Verify the selected item is now displayed + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx index 169f310ddc..b0f8f704cd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx @@ -1,5 +1,7 @@ "use client"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { useClickOutside } from "@/lib/utils/hooks/useClickOutside"; import { Command, CommandEmpty, @@ -32,9 +34,7 @@ import { StarIcon, User, } from "lucide-react"; -import * as React from "react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside"; +import { Fragment, useRef, useState } from "react"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; export enum OptionsType { @@ -141,15 +141,15 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial { - const [open, setOpen] = React.useState(false); + const [open, setOpen] = useState(false); const { t } = useTranslate(); - const commandRef = React.useRef(null); - const [inputValue, setInputValue] = React.useState(""); + const commandRef = useRef(null); + const [inputValue, setInputValue] = useState(""); useClickOutside(commandRef, () => setOpen(false)); return ( -
setOpen(true)} className="group flex cursor-pointer items-center justify-between rounded-md bg-white px-3 py-2 text-sm"> {!open && selected.hasOwnProperty("label") && ( @@ -164,7 +164,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question value={inputValue} onValueChange={setInputValue} placeholder={t("common.search") + "..."} - className="h-5 border-none border-transparent p-0 shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent" + className="h-5 border-none border-transparent p-0 shadow-none ring-offset-transparent outline-0 focus:border-none focus:border-transparent focus:shadow-none focus:ring-offset-transparent focus:outline-0" /> )}
@@ -174,14 +174,14 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question )}
-
+
{open && (
{t("common.no_result_found")} {options?.map((data) => ( - <> + {data?.option.length > 0 && ( {data.header}

}> @@ -199,7 +199,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question ))}
)} - +
))}
diff --git a/apps/web/app/(app)/layout.test.tsx b/apps/web/app/(app)/layout.test.tsx index 542ff29df2..88034a3b73 100644 --- a/apps/web/app/(app)/layout.test.tsx +++ b/apps/web/app/(app)/layout.test.tsx @@ -1,8 +1,8 @@ +import { getUser } from "@/lib/user/service"; import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen } from "@testing-library/react"; import { getServerSession } from "next-auth"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { getUser } from "@formbricks/lib/user/service"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { TUser } from "@formbricks/types/user"; import AppLayout from "./layout"; @@ -10,11 +10,11 @@ vi.mock("next-auth", () => ({ getServerSession: vi.fn(), })); -vi.mock("@formbricks/lib/user/service", () => ({ +vi.mock("@/lib/user/service", () => ({ getUser: vi.fn(), })); -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ INTERCOM_SECRET_KEY: "test-secret-key", IS_INTERCOM_CONFIGURED: true, INTERCOM_APP_ID: "test-app-id", @@ -59,7 +59,7 @@ describe("(app) AppLayout", () => { cleanup(); }); - it("renders child content and all sub-components when user exists", async () => { + test("renders child content and all sub-components when user exists", async () => { vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } }); vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser); @@ -77,7 +77,7 @@ describe("(app) AppLayout", () => { expect(screen.getByTestId("formbricks-client")).toBeInTheDocument(); }); - it("skips FormbricksClient if no user is present", async () => { + test("skips FormbricksClient if no user is present", async () => { vi.mocked(getServerSession).mockResolvedValueOnce(null); const element = await AppLayout({ diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 4b011fc12f..eef6b727c8 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -1,12 +1,5 @@ import { FormbricksClient } from "@/app/(app)/components/FormbricksClient"; import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper"; -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { ClientLogout } from "@/modules/ui/components/client-logout"; -import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay"; -import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client"; -import { ToasterClient } from "@/modules/ui/components/toaster-client"; -import { getServerSession } from "next-auth"; -import { Suspense } from "react"; import { FORMBRICKS_API_HOST, FORMBRICKS_ENVIRONMENT_ID, @@ -14,8 +7,15 @@ import { IS_POSTHOG_CONFIGURED, POSTHOG_API_HOST, POSTHOG_API_KEY, -} from "@formbricks/lib/constants"; -import { getUser } from "@formbricks/lib/user/service"; +} from "@/lib/constants"; +import { getUser } from "@/lib/user/service"; +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { ClientLogout } from "@/modules/ui/components/client-logout"; +import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay"; +import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client"; +import { ToasterClient } from "@/modules/ui/components/toaster-client"; +import { getServerSession } from "next-auth"; +import { Suspense } from "react"; const AppLayout = async ({ children }) => { const session = await getServerSession(authOptions); diff --git a/apps/web/app/(auth)/layout.test.tsx b/apps/web/app/(auth)/layout.test.tsx index dae4f79098..daeef3c8e1 100644 --- a/apps/web/app/(auth)/layout.test.tsx +++ b/apps/web/app/(auth)/layout.test.tsx @@ -1,9 +1,9 @@ import "@testing-library/jest-dom/vitest"; import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import AppLayout from "../(auth)/layout"; -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ IS_FORMBRICKS_CLOUD: false, IS_INTERCOM_CONFIGURED: true, INTERCOM_SECRET_KEY: "mock-intercom-secret-key", @@ -18,7 +18,7 @@ vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({ })); describe("(auth) AppLayout", () => { - it("renders the NoMobileOverlay and IntercomClient, plus children", async () => { + test("renders the NoMobileOverlay and IntercomClient, plus children", async () => { const appLayoutElement = await AppLayout({ children:
Hello from children!
, }); diff --git a/apps/web/app/(redirects)/organizations/[organizationId]/route.ts b/apps/web/app/(redirects)/organizations/[organizationId]/route.ts index 9f81f7cd2d..eb0c553ec6 100644 --- a/apps/web/app/(redirects)/organizations/[organizationId]/route.ts +++ b/apps/web/app/(redirects)/organizations/[organizationId]/route.ts @@ -1,12 +1,12 @@ +import { hasOrganizationAccess } from "@/lib/auth"; +import { getEnvironments } from "@/lib/environment/service"; +import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; +import { getAccessFlags } from "@/lib/membership/utils"; +import { getUserProjects } from "@/lib/project/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { notFound } from "next/navigation"; -import { hasOrganizationAccess } from "@formbricks/lib/auth"; -import { getEnvironments } from "@formbricks/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getUserProjects } from "@formbricks/lib/project/service"; import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors"; export const GET = async (_: Request, context: { params: Promise<{ organizationId: string }> }) => { diff --git a/apps/web/app/(redirects)/projects/[projectId]/route.ts b/apps/web/app/(redirects)/projects/[projectId]/route.ts index ba4f230426..484280799c 100644 --- a/apps/web/app/(redirects)/projects/[projectId]/route.ts +++ b/apps/web/app/(redirects)/projects/[projectId]/route.ts @@ -1,9 +1,9 @@ +import { hasOrganizationAccess } from "@/lib/auth"; +import { getEnvironments } from "@/lib/environment/service"; +import { getProject } from "@/lib/project/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { notFound, redirect } from "next/navigation"; -import { hasOrganizationAccess } from "@formbricks/lib/auth"; -import { getEnvironments } from "@formbricks/lib/environment/service"; -import { getProject } from "@formbricks/lib/project/service"; import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors"; export const GET = async (_: Request, context: { params: Promise<{ projectId: string }> }) => { diff --git a/apps/web/app/ClientEnvironmentRedirect.tsx b/apps/web/app/ClientEnvironmentRedirect.tsx index d6a4c50935..8422172666 100644 --- a/apps/web/app/ClientEnvironmentRedirect.tsx +++ b/apps/web/app/ClientEnvironmentRedirect.tsx @@ -1,8 +1,8 @@ "use client"; +import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage"; interface ClientEnvironmentRedirectProps { environmentId: string; diff --git a/apps/web/app/[shortUrlId]/page.tsx b/apps/web/app/[shortUrlId]/page.tsx index 24894cc4ec..8a6a824d27 100644 --- a/apps/web/app/[shortUrlId]/page.tsx +++ b/apps/web/app/[shortUrlId]/page.tsx @@ -1,7 +1,7 @@ +import { getShortUrl } from "@/lib/shortUrl/service"; import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata"; import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; -import { getShortUrl } from "@formbricks/lib/shortUrl/service"; import { logger } from "@formbricks/logger"; import { TShortUrl, ZShortUrlId } from "@formbricks/types/short-url"; diff --git a/apps/web/app/api/(internal)/insights/lib/document.ts b/apps/web/app/api/(internal)/insights/lib/document.ts index 0b9d647135..6ba53bed60 100644 --- a/apps/web/app/api/(internal)/insights/lib/document.ts +++ b/apps/web/app/api/(internal)/insights/lib/document.ts @@ -1,10 +1,10 @@ +import { embeddingsModel, llmModel } from "@/lib/aiModels"; import { documentCache } from "@/lib/cache/document"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { embed, generateObject } from "ai"; import { z } from "zod"; import { prisma } from "@formbricks/database"; -import { embeddingsModel, llmModel } from "@formbricks/lib/aiModels"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { TDocument, TDocumentCreateInput, diff --git a/apps/web/app/api/(internal)/insights/lib/insights.ts b/apps/web/app/api/(internal)/insights/lib/insights.ts index 48df2e0374..c8263c3713 100644 --- a/apps/web/app/api/(internal)/insights/lib/insights.ts +++ b/apps/web/app/api/(internal)/insights/lib/insights.ts @@ -1,14 +1,14 @@ import { createDocument } from "@/app/api/(internal)/insights/lib/document"; import { doesResponseHasAnyOpenTextAnswer } from "@/app/api/(internal)/insights/lib/utils"; +import { embeddingsModel } from "@/lib/aiModels"; import { documentCache } from "@/lib/cache/document"; import { insightCache } from "@/lib/cache/insight"; +import { getPromptText } from "@/lib/utils/ai"; +import { parseRecallInfo } from "@/lib/utils/recall"; +import { validateInputs } from "@/lib/utils/validate"; import { Insight, InsightCategory, Prisma } from "@prisma/client"; import { embed } from "ai"; import { prisma } from "@formbricks/database"; -import { embeddingsModel } from "@formbricks/lib/aiModels"; -import { getPromptText } from "@formbricks/lib/utils/ai"; -import { parseRecallInfo } from "@formbricks/lib/utils/recall"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; import { TCreatedDocument } from "@formbricks/types/documents"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/(internal)/insights/lib/utils.test.ts b/apps/web/app/api/(internal)/insights/lib/utils.test.ts index f772f17a32..be7f51827d 100644 --- a/apps/web/app/api/(internal)/insights/lib/utils.test.ts +++ b/apps/web/app/api/(internal)/insights/lib/utils.test.ts @@ -1,8 +1,8 @@ +import { CRON_SECRET, WEBAPP_URL } from "@/lib/constants"; +import { getSurvey, updateSurvey } from "@/lib/survey/service"; +import { mockSurveyOutput } from "@/lib/survey/tests/__mock__/survey.mock"; +import { doesSurveyHasOpenTextQuestion } from "@/lib/survey/utils"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; -import { mockSurveyOutput } from "@formbricks/lib/survey/tests/__mock__/survey.mock"; -import { doesSurveyHasOpenTextQuestion } from "@formbricks/lib/survey/utils"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { @@ -12,27 +12,27 @@ import { } from "./utils"; // Mock all dependencies -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ CRON_SECRET: vi.fn(() => "mocked-cron-secret"), WEBAPP_URL: "https://mocked-webapp-url.com", })); -vi.mock("@formbricks/lib/survey/cache", () => ({ +vi.mock("@/lib/survey/cache", () => ({ surveyCache: { revalidate: vi.fn(), }, })); -vi.mock("@formbricks/lib/survey/service", () => ({ +vi.mock("@/lib/survey/service", () => ({ getSurvey: vi.fn(), updateSurvey: vi.fn(), })); -vi.mock("@formbricks/lib/survey/utils", () => ({ +vi.mock("@/lib/survey/utils", () => ({ doesSurveyHasOpenTextQuestion: vi.fn(), })); -vi.mock("@formbricks/lib/utils/validate", () => ({ +vi.mock("@/lib/utils/validate", () => ({ validateInputs: vi.fn(), })); @@ -87,7 +87,7 @@ describe("Insights Utils", () => { vi.resetModules(); // Mock CRON_SECRET as undefined - vi.doMock("@formbricks/lib/constants", () => ({ + vi.doMock("@/lib/constants", () => ({ CRON_SECRET: undefined, WEBAPP_URL: "https://mocked-webapp-url.com", })); diff --git a/apps/web/app/api/(internal)/insights/lib/utils.ts b/apps/web/app/api/(internal)/insights/lib/utils.ts index c8feaf1ab2..4ab1e29c5a 100644 --- a/apps/web/app/api/(internal)/insights/lib/utils.ts +++ b/apps/web/app/api/(internal)/insights/lib/utils.ts @@ -1,9 +1,9 @@ import "server-only"; -import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; -import { doesSurveyHasOpenTextQuestion } from "@formbricks/lib/survey/utils"; -import { validateInputs } from "@formbricks/lib/utils/validate"; +import { CRON_SECRET, WEBAPP_URL } from "@/lib/constants"; +import { surveyCache } from "@/lib/survey/cache"; +import { getSurvey, updateSurvey } from "@/lib/survey/service"; +import { doesSurveyHasOpenTextQuestion } from "@/lib/survey/utils"; +import { validateInputs } from "@/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/(internal)/insights/route.ts b/apps/web/app/api/(internal)/insights/route.ts index c4a2c8f47d..2241e38a9a 100644 --- a/apps/web/app/api/(internal)/insights/route.ts +++ b/apps/web/app/api/(internal)/insights/route.ts @@ -2,9 +2,9 @@ import { generateInsightsForSurveyResponsesConcept } from "@/app/api/(internal)/insights/lib/insights"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { CRON_SECRET } from "@/lib/constants"; import { headers } from "next/headers"; import { z } from "zod"; -import { CRON_SECRET } from "@formbricks/lib/constants"; import { logger } from "@formbricks/logger"; import { generateInsightsEnabledForSurveyQuestions } from "./lib/utils"; diff --git a/apps/web/app/api/(internal)/pipeline/lib/documents.ts b/apps/web/app/api/(internal)/pipeline/lib/documents.ts index 9a0d1ae449..0de3ffce24 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/documents.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/documents.ts @@ -1,12 +1,12 @@ import { handleInsightAssignments } from "@/app/api/(internal)/insights/lib/insights"; +import { embeddingsModel, llmModel } from "@/lib/aiModels"; import { documentCache } from "@/lib/cache/document"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { embed, generateObject } from "ai"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { ZInsight } from "@formbricks/database/zod/insights"; -import { embeddingsModel, llmModel } from "@formbricks/lib/aiModels"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { TDocument, TDocumentCreateInput, diff --git a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts index 5eea313aaa..2d11b6389f 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts @@ -1,14 +1,14 @@ import { TPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines"; -import { writeData as airtableWriteData } from "@formbricks/lib/airtable/service"; -import { NOTION_RICH_TEXT_LIMIT } from "@formbricks/lib/constants"; -import { writeData } from "@formbricks/lib/googleSheet/service"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { writeData as writeNotionData } from "@formbricks/lib/notion/service"; -import { processResponseData } from "@formbricks/lib/responses"; -import { writeDataToSlack } from "@formbricks/lib/slack/service"; -import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime"; -import { parseRecallInfo } from "@formbricks/lib/utils/recall"; -import { truncateText } from "@formbricks/lib/utils/strings"; +import { writeData as airtableWriteData } from "@/lib/airtable/service"; +import { NOTION_RICH_TEXT_LIMIT } from "@/lib/constants"; +import { writeData } from "@/lib/googleSheet/service"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { writeData as writeNotionData } from "@/lib/notion/service"; +import { processResponseData } from "@/lib/responses"; +import { writeDataToSlack } from "@/lib/slack/service"; +import { getFormattedDateTimeString } from "@/lib/utils/datetime"; +import { parseRecallInfo } from "@/lib/utils/recall"; +import { truncateText } from "@/lib/utils/strings"; import { logger } from "@formbricks/logger"; import { Result } from "@formbricks/types/error-handlers"; import { TIntegration, TIntegrationType } from "@formbricks/types/integration"; @@ -392,6 +392,19 @@ const getValue = (colType: string, value: string | string[] | Date | number | Re }, ]; } + if (Array.isArray(value)) { + const content = value.join("\n"); + return [ + { + text: { + content: + content.length > NOTION_RICH_TEXT_LIMIT + ? truncateText(content, NOTION_RICH_TEXT_LIMIT) + : content, + }, + }, + ]; + } return [ { text: { diff --git a/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts b/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts index e2d1115116..b430760922 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/survey-follow-up.ts @@ -12,9 +12,10 @@ type FollowUpResult = { error?: string; }; -const evaluateFollowUp = async ( +export const evaluateFollowUp = async ( followUpId: string, followUpAction: TSurveyFollowUpAction, + survey: TSurvey, response: TResponse, organization: TOrganization ): Promise => { @@ -22,6 +23,25 @@ const evaluateFollowUp = async ( const { to, subject, body, replyTo } = properties; const toValueFromResponse = response.data[to]; const logoUrl = organization.whitelabel?.logoUrl || ""; + + // Check if 'to' is a direct email address (team member or user email) + const parsedEmailTo = z.string().email().safeParse(to); + if (parsedEmailTo.success) { + // 'to' is a valid email address, send email directly + await sendFollowUpEmail({ + html: body, + subject, + to: parsedEmailTo.data, + replyTo, + survey, + response, + attachResponseData: properties.attachResponseData, + logoUrl, + }); + return; + } + + // If not a direct email, check if it's a question ID or hidden field ID if (!toValueFromResponse) { throw new Error(`"To" value not found in response data for followup: ${followUpId}`); } @@ -31,7 +51,16 @@ const evaluateFollowUp = async ( const parsedResult = z.string().email().safeParse(toValueFromResponse); if (parsedResult.data) { // send email to this email address - await sendFollowUpEmail(body, subject, parsedResult.data, replyTo, logoUrl); + await sendFollowUpEmail({ + html: body, + subject, + to: parsedResult.data, + replyTo, + logoUrl, + survey, + response, + attachResponseData: properties.attachResponseData, + }); } else { throw new Error(`Email address is not valid for followup: ${followUpId}`); } @@ -42,7 +71,16 @@ const evaluateFollowUp = async ( } const parsedResult = z.string().email().safeParse(emailAddress); if (parsedResult.data) { - await sendFollowUpEmail(body, subject, parsedResult.data, replyTo, logoUrl); + await sendFollowUpEmail({ + html: body, + subject, + to: parsedResult.data, + replyTo, + logoUrl, + survey, + response, + attachResponseData: properties.attachResponseData, + }); } else { throw new Error(`Email address is not valid for followup: ${followUpId}`); } @@ -53,7 +91,7 @@ export const sendSurveyFollowUps = async ( survey: TSurvey, response: TResponse, organization: TOrganization -) => { +): Promise => { const followUpPromises = survey.followUps.map(async (followUp): Promise => { const { trigger } = followUp; @@ -70,7 +108,7 @@ export const sendSurveyFollowUps = async ( } } - return evaluateFollowUp(followUp.id, followUp.action, response, organization) + return evaluateFollowUp(followUp.id, followUp.action, survey, response, organization) .then(() => ({ followUpId: followUp.id, status: "success" as const, @@ -92,4 +130,6 @@ export const sendSurveyFollowUps = async ( if (errors.length > 0) { logger.error(errors, "Follow-up processing errors"); } + + return followUpResults; }; diff --git a/apps/web/app/api/(internal)/pipeline/lib/tests/__mocks__/survey-follow-up.mock.ts b/apps/web/app/api/(internal)/pipeline/lib/tests/__mocks__/survey-follow-up.mock.ts new file mode 100644 index 0000000000..709d5b9ff7 --- /dev/null +++ b/apps/web/app/api/(internal)/pipeline/lib/tests/__mocks__/survey-follow-up.mock.ts @@ -0,0 +1,267 @@ +import { TResponse } from "@formbricks/types/responses"; +import { + TSurvey, + TSurveyContactInfoQuestion, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; + +export const mockEndingId1 = "mpkt4n5krsv2ulqetle7b9e7"; +export const mockEndingId2 = "ge0h63htnmgq6kwx1suh9cyi"; + +export const mockResponseEmailFollowUp: TSurvey["followUps"][number] = { + id: "cm9gpuazd0002192z67olbfdt", + createdAt: new Date(), + updatedAt: new Date(), + surveyId: "cm9gptbhg0000192zceq9ayuc", + name: "nice follow up", + trigger: { + type: "response", + properties: null, + }, + action: { + type: "send-email", + properties: { + to: "vjniuob08ggl8dewl0hwed41", + body: '

Hey 👋

Thanks for taking the time to respond, we will be in touch shortly.

Have a great day!

', + from: "noreply@example.com", + replyTo: ["test@user.com"], + subject: "Thanks for your answers!‌‌‍‍‌‌‌‍‌‌‌‍‍‌‌‌‌‌‌‌‍‍‍‌‌‍‌‌‌‍‍‌‍‌‌‌‌‌‌‌‍‌‍‌‌", + attachResponseData: true, + }, + }, +}; + +export const mockEndingFollowUp: TSurvey["followUps"][number] = { + id: "j0g23cue6eih6xs5m0m4cj50", + createdAt: new Date(), + updatedAt: new Date(), + surveyId: "cm9gptbhg0000192zceq9ayuc", + name: "nice follow up", + trigger: { + type: "endings", + properties: { + endingIds: [mockEndingId1], + }, + }, + action: { + type: "send-email", + properties: { + to: "vjniuob08ggl8dewl0hwed41", + body: '

Hey 👋

Thanks for taking the time to respond, we will be in touch shortly.

Have a great day!

', + from: "noreply@example.com", + replyTo: ["test@user.com"], + subject: "Thanks for your answers!‌‌‍‍‌‌‌‍‌‌‌‍‍‌‌‌‌‌‌‌‍‍‍‌‌‍‌‌‌‍‍‌‍‌‌‌‌‌‌‌‍‌‍‌‌", + attachResponseData: true, + }, + }, +}; + +export const mockDirectEmailFollowUp: TSurvey["followUps"][number] = { + id: "yyc5sq1fqofrsyw4viuypeku", + createdAt: new Date(), + updatedAt: new Date(), + surveyId: "cm9gptbhg0000192zceq9ayuc", + name: "nice follow up 1", + trigger: { + type: "response", + properties: null, + }, + action: { + type: "send-email", + properties: { + to: "direct@email.com", + body: '

Hey 👋

Thanks for taking the time to respond, we will be in touch shortly.

Have a great day!

', + from: "noreply@example.com", + replyTo: ["test@user.com"], + subject: "Thanks for your answers!‌‌‍‍‌‌‌‍‌‌‌‍‍‌‌‌‌‌‌‌‍‍‍‌‌‍‌‌‌‍‍‌‍‌‌‌‌‌‌‌‍‌‍‌‌", + attachResponseData: true, + }, + }, +}; + +export const mockFollowUps: TSurvey["followUps"] = [mockDirectEmailFollowUp, mockResponseEmailFollowUp]; + +export const mockSurvey: TSurvey = { + id: "cm9gptbhg0000192zceq9ayuc", + createdAt: new Date(), + updatedAt: new Date(), + name: "Start from scratch‌‌‍‍‌‍‍‌‌‌‌‍‍‍‌‌‌‌‌‌‌‌‍‌‍‌‌", + type: "link", + environmentId: "cm98djl8e000919hpzi6a80zp", + createdBy: "cm98dg3xm000019hpubj39vfi", + status: "inProgress", + welcomeCard: { + html: { + default: "Thanks for providing your feedback - let's go!‌‌‍‍‌‍‍‍‌‌‌‍‍‌‌‌‍‌‌‌‌‌‍‌‍‌‌", + }, + enabled: false, + headline: { + default: "Welcome!‌‌‍‍‌‍‍‍‌‌‌‍‍‌‌‌‌‌‌‌‌‌‍‌‍‌‌", + }, + buttonLabel: { + default: "Next‌‌‍‍‌‍‍‍‌‌‌‍‍‌‌‍‌‌‌‌‌‌‍‌‍‌‌", + }, + timeToFinish: false, + showResponseCount: false, + }, + questions: [ + { + id: "vjniuob08ggl8dewl0hwed41", + type: "openText" as TSurveyQuestionTypeEnum.OpenText, + headline: { + default: "What would you like to know?‌‌‍‍‌‍‍‍‌‌‌‍‍‌‍‍‌‌‌‌‌‌‍‌‍‌‌", + }, + required: true, + charLimit: {}, + inputType: "email", + longAnswer: false, + buttonLabel: { + default: "Next‌‌‍‍‌‍‍‍‌‌‌‍‍‍‌‌‌‌‌‌‌‌‍‌‍‌‌", + }, + placeholder: { + default: "example@email.com", + }, + }, + ], + endings: [ + { + id: "gt1yoaeb5a3istszxqbl08mk", + type: "endScreen", + headline: { + default: "Thank you!‌‌‍‍‌‍‍‍‌‌‌‍‍‌‌‍‍‌‌‌‌‌‍‌‍‌‌", + }, + subheader: { + default: "We appreciate your feedback.‌‌‍‍‌‍‍‍‌‌‌‍‍‌‍‌‌‌‌‌‌‌‍‌‍‌‌", + }, + buttonLink: "https://formbricks.com", + buttonLabel: { + default: "Create your own Survey‌‌‍‍‌‍‍‍‌‌‌‍‍‌‍‌‍‌‌‌‌‌‍‌‍‌‌", + }, + }, + ], + hiddenFields: { + enabled: true, + fieldIds: [], + }, + variables: [], + displayOption: "displayOnce", + recontactDays: null, + displayLimit: null, + autoClose: null, + runOnDate: null, + closeOnDate: null, + delay: 0, + displayPercentage: null, + autoComplete: null, + isVerifyEmailEnabled: false, + isSingleResponsePerEmailEnabled: false, + isBackButtonHidden: false, + projectOverwrites: null, + styling: null, + surveyClosedMessage: null, + singleUse: { + enabled: false, + isEncrypted: true, + }, + pin: null, + resultShareKey: null, + showLanguageSwitch: null, + languages: [], + triggers: [], + segment: null, + followUps: mockFollowUps, +}; + +export const mockContactQuestion: TSurveyContactInfoQuestion = { + id: "zyoobxyolyqj17bt1i4ofr37", + type: TSurveyQuestionTypeEnum.ContactInfo, + email: { + show: true, + required: true, + placeholder: { + default: "Email", + }, + }, + phone: { + show: true, + required: true, + placeholder: { + default: "Phone", + }, + }, + company: { + show: true, + required: true, + placeholder: { + default: "Company", + }, + }, + headline: { + default: "Contact Question", + }, + lastName: { + show: true, + required: true, + placeholder: { + default: "Last Name", + }, + }, + required: true, + firstName: { + show: true, + required: true, + placeholder: { + default: "First Name", + }, + }, + buttonLabel: { + default: "Next‌‌‍‍‌‌‌‍‌‌‌‍‍‌‌‌‍‌‌‌‍‍‍‌‌‌‌‌‌‌‌‍‌‍‌‌", + }, + backButtonLabel: { + default: "Back‌‌‍‍‌‌‌‍‌‌‌‍‍‌‌‌‍‌‌‌‍‍‍‌‌‍‌‌‌‌‌‍‌‍‌‌", + }, +}; + +export const mockContactEmailFollowUp: TSurvey["followUps"][number] = { + ...mockResponseEmailFollowUp, + action: { + ...mockResponseEmailFollowUp.action, + properties: { + ...mockResponseEmailFollowUp.action.properties, + to: mockContactQuestion.id, + }, + }, +}; + +export const mockSurveyWithContactQuestion: TSurvey = { + ...mockSurvey, + questions: [mockContactQuestion], + followUps: [mockContactEmailFollowUp], +}; + +export const mockResponse: TResponse = { + id: "response1", + surveyId: "survey1", + createdAt: new Date(), + updatedAt: new Date(), + variables: {}, + language: "en", + data: { + ["vjniuob08ggl8dewl0hwed41"]: "test@example.com", + }, + contact: null, + contactAttributes: {}, + meta: {}, + finished: true, + notes: [], + singleUseId: null, + tags: [], + displayId: null, +}; + +export const mockResponseWithContactQuestion: TResponse = { + ...mockResponse, + data: { + zyoobxyolyqj17bt1i4ofr37: ["test", "user1", "test@user1.com", "99999999999", "sampleCompany"], + }, +}; diff --git a/apps/web/app/api/(internal)/pipeline/lib/tests/survey-follow-up.test.ts b/apps/web/app/api/(internal)/pipeline/lib/tests/survey-follow-up.test.ts new file mode 100644 index 0000000000..1157111f07 --- /dev/null +++ b/apps/web/app/api/(internal)/pipeline/lib/tests/survey-follow-up.test.ts @@ -0,0 +1,235 @@ +import { + mockContactEmailFollowUp, + mockDirectEmailFollowUp, + mockEndingFollowUp, + mockEndingId2, + mockResponse, + mockResponseEmailFollowUp, + mockResponseWithContactQuestion, + mockSurvey, + mockSurveyWithContactQuestion, +} from "@/app/api/(internal)/pipeline/lib/tests/__mocks__/survey-follow-up.mock"; +import { sendFollowUpEmail } from "@/modules/email"; +import { describe, expect, test, vi } from "vitest"; +import { logger } from "@formbricks/logger"; +import { TOrganization } from "@formbricks/types/organizations"; +import { TResponse } from "@formbricks/types/responses"; +import { TSurvey } from "@formbricks/types/surveys/types"; +import { evaluateFollowUp, sendSurveyFollowUps } from "../survey-follow-up"; + +// Mock dependencies +vi.mock("@/modules/email", () => ({ + sendFollowUpEmail: vi.fn(), +})); + +vi.mock("@formbricks/logger", () => ({ + logger: { + error: vi.fn(), + }, +})); + +describe("Survey Follow Up", () => { + const mockOrganization: Partial = { + id: "org1", + name: "Test Org", + whitelabel: { + logoUrl: "https://example.com/logo.png", + }, + }; + + describe("evaluateFollowUp", () => { + test("sends email when to is a direct email address", async () => { + const followUpId = mockDirectEmailFollowUp.id; + const followUpAction = mockDirectEmailFollowUp.action; + + await evaluateFollowUp( + followUpId, + followUpAction, + mockSurvey, + mockResponse, + mockOrganization as TOrganization + ); + + expect(sendFollowUpEmail).toHaveBeenCalledWith({ + html: mockDirectEmailFollowUp.action.properties.body, + subject: mockDirectEmailFollowUp.action.properties.subject, + to: mockDirectEmailFollowUp.action.properties.to, + replyTo: mockDirectEmailFollowUp.action.properties.replyTo, + survey: mockSurvey, + response: mockResponse, + attachResponseData: true, + logoUrl: "https://example.com/logo.png", + }); + }); + + test("sends email when to is a question ID with valid email", async () => { + const followUpId = mockResponseEmailFollowUp.id; + const followUpAction = mockResponseEmailFollowUp.action; + + await evaluateFollowUp( + followUpId, + followUpAction, + mockSurvey as TSurvey, + mockResponse as TResponse, + mockOrganization as TOrganization + ); + + expect(sendFollowUpEmail).toHaveBeenCalledWith({ + html: mockResponseEmailFollowUp.action.properties.body, + subject: mockResponseEmailFollowUp.action.properties.subject, + to: mockResponse.data[mockResponseEmailFollowUp.action.properties.to], + replyTo: mockResponseEmailFollowUp.action.properties.replyTo, + survey: mockSurvey, + response: mockResponse, + attachResponseData: true, + logoUrl: "https://example.com/logo.png", + }); + }); + + test("sends email when to is a question ID with valid email in array", async () => { + const followUpId = mockContactEmailFollowUp.id; + const followUpAction = mockContactEmailFollowUp.action; + + await evaluateFollowUp( + followUpId, + followUpAction, + mockSurveyWithContactQuestion, + mockResponseWithContactQuestion, + mockOrganization as TOrganization + ); + + expect(sendFollowUpEmail).toHaveBeenCalledWith({ + html: mockContactEmailFollowUp.action.properties.body, + subject: mockContactEmailFollowUp.action.properties.subject, + to: mockResponseWithContactQuestion.data[mockContactEmailFollowUp.action.properties.to][2], + replyTo: mockContactEmailFollowUp.action.properties.replyTo, + survey: mockSurveyWithContactQuestion, + response: mockResponseWithContactQuestion, + attachResponseData: true, + logoUrl: "https://example.com/logo.png", + }); + }); + + test("throws error when to value is not found in response data", async () => { + const followUpId = "followup1"; + const followUpAction = { + ...mockSurvey.followUps![0].action, + properties: { + ...mockSurvey.followUps![0].action.properties, + to: "nonExistentField", + }, + }; + + await expect( + evaluateFollowUp( + followUpId, + followUpAction, + mockSurvey as TSurvey, + mockResponse as TResponse, + mockOrganization as TOrganization + ) + ).rejects.toThrow(`"To" value not found in response data for followup: ${followUpId}`); + }); + + test("throws error when email address is invalid", async () => { + const followUpId = mockResponseEmailFollowUp.id; + const followUpAction = mockResponseEmailFollowUp.action; + + const invalidResponse = { + ...mockResponse, + data: { + [mockResponseEmailFollowUp.action.properties.to]: "invalid-email", + }, + }; + + await expect( + evaluateFollowUp( + followUpId, + followUpAction, + mockSurvey, + invalidResponse, + mockOrganization as TOrganization + ) + ).rejects.toThrow(`Email address is not valid for followup: ${followUpId}`); + }); + }); + + describe("sendSurveyFollowUps", () => { + test("skips follow-up when ending Id doesn't match", async () => { + const responseWithDifferentEnding = { + ...mockResponse, + endingId: mockEndingId2, + }; + + const mockSurveyWithEndingFollowUp: TSurvey = { + ...mockSurvey, + followUps: [mockEndingFollowUp], + }; + + const results = await sendSurveyFollowUps( + mockSurveyWithEndingFollowUp, + responseWithDifferentEnding as TResponse, + mockOrganization as TOrganization + ); + + expect(results).toEqual([ + { + followUpId: mockEndingFollowUp.id, + status: "skipped", + }, + ]); + expect(sendFollowUpEmail).not.toHaveBeenCalled(); + }); + + test("processes follow-ups and log errors", async () => { + const error = new Error("Test error"); + vi.mocked(sendFollowUpEmail).mockRejectedValueOnce(error); + + const mockSurveyWithFollowUps: TSurvey = { + ...mockSurvey, + followUps: [mockResponseEmailFollowUp], + }; + + const results = await sendSurveyFollowUps( + mockSurveyWithFollowUps, + mockResponse, + mockOrganization as TOrganization + ); + + expect(results).toEqual([ + { + followUpId: mockResponseEmailFollowUp.id, + status: "error", + error: "Test error", + }, + ]); + expect(logger.error).toHaveBeenCalledWith( + [`FollowUp ${mockResponseEmailFollowUp.id} failed: Test error`], + "Follow-up processing errors" + ); + }); + + test("successfully processes follow-ups", async () => { + vi.mocked(sendFollowUpEmail).mockResolvedValueOnce(undefined); + + const mockSurveyWithFollowUp: TSurvey = { + ...mockSurvey, + followUps: [mockDirectEmailFollowUp], + }; + + const results = await sendSurveyFollowUps( + mockSurveyWithFollowUp, + mockResponse, + mockOrganization as TOrganization + ); + + expect(results).toEqual([ + { + followUpId: mockDirectEmailFollowUp.id, + status: "success", + }, + ]); + expect(logger.error).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts index e98ac5208a..ee7e0c5880 100644 --- a/apps/web/app/api/(internal)/pipeline/route.ts +++ b/apps/web/app/api/(internal)/pipeline/route.ts @@ -3,23 +3,24 @@ import { sendSurveyFollowUps } from "@/app/api/(internal)/pipeline/lib/survey-fo import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { cache } from "@/lib/cache"; import { webhookCache } from "@/lib/cache/webhook"; +import { CRON_SECRET, IS_AI_CONFIGURED } from "@/lib/constants"; +import { getIntegrations } from "@/lib/integration/service"; +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 { getPromptText } from "@/lib/utils/ai"; +import { parseRecallInfo } from "@/lib/utils/recall"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { sendResponseFinishedEmail } from "@/modules/email"; import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; import { PipelineTriggers, Webhook } from "@prisma/client"; import { headers } from "next/headers"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { CRON_SECRET, IS_AI_CONFIGURED } from "@formbricks/lib/constants"; -import { getIntegrations } from "@formbricks/lib/integration/service"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; -import { convertDatesInObject } from "@formbricks/lib/time"; -import { getPromptText } from "@formbricks/lib/utils/ai"; -import { parseRecallInfo } from "@formbricks/lib/utils/recall"; import { logger } from "@formbricks/logger"; +import { ResourceNotFoundError } from "@formbricks/types/errors"; import { handleIntegrations } from "./lib/handleIntegrations"; export const POST = async (request: Request) => { @@ -50,7 +51,7 @@ export const POST = async (request: Request) => { const organization = await getOrganizationByEnvironmentId(environmentId); if (!organization) { - throw new Error("Organization not found"); + throw new ResourceNotFoundError("Organization", "Organization not found"); } // Fetch webhooks diff --git a/apps/web/app/api/cron/ping/route.ts b/apps/web/app/api/cron/ping/route.ts index 43465af7de..3910facfe3 100644 --- a/apps/web/app/api/cron/ping/route.ts +++ b/apps/web/app/api/cron/ping/route.ts @@ -1,9 +1,9 @@ import { responses } from "@/app/lib/api/response"; +import { CRON_SECRET } from "@/lib/constants"; +import { captureTelemetry } from "@/lib/telemetry"; import packageJson from "@/package.json"; import { headers } from "next/headers"; import { prisma } from "@formbricks/database"; -import { CRON_SECRET } from "@formbricks/lib/constants"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; export const POST = async () => { const headersList = await headers(); diff --git a/apps/web/app/api/cron/survey-status/route.ts b/apps/web/app/api/cron/survey-status/route.ts index 4faefccfc0..8c4042c383 100644 --- a/apps/web/app/api/cron/survey-status/route.ts +++ b/apps/web/app/api/cron/survey-status/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { CRON_SECRET } from "@/lib/constants"; +import { surveyCache } from "@/lib/survey/cache"; import { headers } from "next/headers"; import { prisma } from "@formbricks/database"; -import { CRON_SECRET } from "@formbricks/lib/constants"; -import { surveyCache } from "@formbricks/lib/survey/cache"; export const POST = async () => { const headersList = await headers(); diff --git a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts b/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts index 69f2caabb7..b4a35ea41f 100644 --- a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts +++ b/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts @@ -1,6 +1,6 @@ -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { convertResponseValue } from "@formbricks/lib/responses"; -import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { convertResponseValue } from "@/lib/responses"; +import { replaceHeadlineRecall } from "@/lib/utils/recall"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TWeeklyEmailResponseData, diff --git a/apps/web/app/api/cron/weekly-summary/route.ts b/apps/web/app/api/cron/weekly-summary/route.ts index c5f22cc2c1..785db9ff8c 100644 --- a/apps/web/app/api/cron/weekly-summary/route.ts +++ b/apps/web/app/api/cron/weekly-summary/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { CRON_SECRET } from "@/lib/constants"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@/modules/email"; import { headers } from "next/headers"; -import { CRON_SECRET } from "@formbricks/lib/constants"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getNotificationResponse } from "./lib/notificationResponse"; import { getOrganizationIds } from "./lib/organization"; import { getProjectsByOrganizationId } from "./lib/project"; diff --git a/apps/web/app/api/google-sheet/callback/route.ts b/apps/web/app/api/google-sheet/callback/route.ts index 3220e05c45..1fa6d45aac 100644 --- a/apps/web/app/api/google-sheet/callback/route.ts +++ b/apps/web/app/api/google-sheet/callback/route.ts @@ -1,12 +1,12 @@ import { responses } from "@/app/lib/api/response"; -import { google } from "googleapis"; import { GOOGLE_SHEETS_CLIENT_ID, GOOGLE_SHEETS_CLIENT_SECRET, GOOGLE_SHEETS_REDIRECT_URL, WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { createOrUpdateIntegration } from "@formbricks/lib/integration/service"; +} from "@/lib/constants"; +import { createOrUpdateIntegration } from "@/lib/integration/service"; +import { google } from "googleapis"; export const GET = async (req: Request) => { const url = req.url; diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 72b6310c1f..aeee2a666b 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -1,14 +1,14 @@ import { responses } from "@/app/lib/api/response"; -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { google } from "googleapis"; -import { getServerSession } from "next-auth"; -import { NextRequest } from "next/server"; import { GOOGLE_SHEETS_CLIENT_ID, GOOGLE_SHEETS_CLIENT_SECRET, GOOGLE_SHEETS_REDIRECT_URL, -} from "@formbricks/lib/constants"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +} from "@/lib/constants"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { google } from "googleapis"; +import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; const scopes = [ "https://www.googleapis.com/auth/spreadsheets", diff --git a/apps/web/app/api/v1/auth.test.ts b/apps/web/app/api/v1/auth.test.ts index 6659e5583a..82dc5dd7c0 100644 --- a/apps/web/app/api/v1/auth.test.ts +++ b/apps/web/app/api/v1/auth.test.ts @@ -1,7 +1,7 @@ import { hashApiKey } from "@/modules/api/v2/management/lib/utils"; import { getApiKeyWithPermissions } from "@/modules/organization/settings/api-keys/lib/api-key"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { TAPIKeyEnvironmentPermission } from "@formbricks/types/auth"; import { authenticateRequest } from "./auth"; @@ -20,7 +20,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({ })); describe("getApiKeyWithPermissions", () => { - it("should return API key data with permissions when valid key is provided", async () => { + test("returns API key data with permissions when valid key is provided", async () => { const mockApiKeyData = { id: "api-key-id", organizationId: "org-id", @@ -51,7 +51,7 @@ describe("getApiKeyWithPermissions", () => { }); }); - it("should return null when API key is not found", async () => { + test("returns null when API key is not found", async () => { vi.mocked(prisma.apiKey.findUnique).mockResolvedValue(null); const result = await getApiKeyWithPermissions("invalid-key"); @@ -85,31 +85,31 @@ describe("hasPermission", () => { }, ]; - it("should return true for manage permission with any method", () => { + test("returns true for manage permission with any method", () => { expect(hasPermission(permissions, "env-1", "GET")).toBe(true); expect(hasPermission(permissions, "env-1", "POST")).toBe(true); expect(hasPermission(permissions, "env-1", "DELETE")).toBe(true); }); - it("should handle write permission correctly", () => { + test("handles write permission correctly", () => { expect(hasPermission(permissions, "env-2", "GET")).toBe(true); expect(hasPermission(permissions, "env-2", "POST")).toBe(true); expect(hasPermission(permissions, "env-2", "DELETE")).toBe(false); }); - it("should handle read permission correctly", () => { + test("handles read permission correctly", () => { expect(hasPermission(permissions, "env-3", "GET")).toBe(true); expect(hasPermission(permissions, "env-3", "POST")).toBe(false); expect(hasPermission(permissions, "env-3", "DELETE")).toBe(false); }); - it("should return false for non-existent environment", () => { + test("returns false for non-existent environment", () => { expect(hasPermission(permissions, "env-4", "GET")).toBe(false); }); }); describe("authenticateRequest", () => { - it("should return authentication data for valid API key", async () => { + test("should return authentication data for valid API key", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); @@ -159,13 +159,13 @@ describe("authenticateRequest", () => { }); }); - it("should return null when no API key is provided", async () => { + test("returns null when no API key is provided", async () => { const request = new Request("http://localhost"); const result = await authenticateRequest(request); expect(result).toBeNull(); }); - it("should return null when API key is invalid", async () => { + test("returns null when API key is invalid", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "invalid-api-key" }, }); diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts index 306a488ae5..9c46bb8a1f 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts @@ -5,22 +5,22 @@ import { getSyncSurveys } from "@/app/api/v1/client/[environmentId]/app/sync/lib import { replaceAttributeRecall } from "@/app/api/v1/client/[environmentId]/app/sync/lib/utils"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { getActionClasses } from "@/lib/actionClass/service"; import { contactCache } from "@/lib/cache/contact"; -import { NextRequest, userAgent } from "next/server"; -import { prisma } from "@formbricks/database"; -import { getActionClasses } from "@formbricks/lib/actionClass/service"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getEnvironment, updateEnvironment } from "@/lib/environment/service"; import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId, -} from "@formbricks/lib/organization/service"; +} from "@/lib/organization/service"; import { capturePosthogEnvironmentEvent, sendPlanLimitsReachedEventToPosthogWeekly, -} from "@formbricks/lib/posthogServer"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; -import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants"; +} from "@/lib/posthogServer"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; +import { COLOR_DEFAULTS } from "@/lib/styling/constants"; +import { NextRequest, userAgent } from "next/server"; +import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { ZJsPeopleUserIdInput } from "@formbricks/types/js"; import { TSurvey } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/contact.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/contact.ts index 13b58058dd..712896db17 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/contact.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/contact.ts @@ -1,8 +1,8 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; export const getContactByUserId = reactCache( ( diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts index 949c0d6ea1..f42c510f9a 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/survey.ts @@ -1,19 +1,19 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { contactAttributeCache } from "@/lib/cache/contact-attribute"; +import { displayCache } from "@/lib/display/cache"; +import { projectCache } from "@/lib/project/cache"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; +import { surveyCache } from "@/lib/survey/cache"; +import { getSurveys } from "@/lib/survey/service"; +import { anySurveyHasFilters } from "@/lib/survey/utils"; +import { diffInDays } from "@/lib/utils/datetime"; +import { validateInputs } from "@/lib/utils/validate"; import { evaluateSegment } from "@/modules/ee/contacts/segments/lib/segments"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { displayCache } from "@formbricks/lib/display/cache"; -import { projectCache } from "@formbricks/lib/project/cache"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { getSurveys } from "@formbricks/lib/survey/service"; -import { anySurveyHasFilters } from "@formbricks/lib/survey/utils"; -import { diffInDays } from "@formbricks/lib/utils/datetime"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/utils.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/utils.ts index 5c389cc48d..f48c6187c5 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/utils.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/lib/utils.ts @@ -1,4 +1,4 @@ -import { parseRecallInfo } from "@formbricks/lib/utils/recall"; +import { parseRecallInfo } from "@/lib/utils/recall"; import { TAttributes } from "@formbricks/types/attributes"; import { TSurvey } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/lib/contact.ts b/apps/web/app/api/v1/client/[environmentId]/displays/lib/contact.ts index 4dd2c85ff5..5b312f832e 100644 --- a/apps/web/app/api/v1/client/[environmentId]/displays/lib/contact.ts +++ b/apps/web/app/api/v1/client/[environmentId]/displays/lib/contact.ts @@ -1,7 +1,7 @@ +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; export const getContactByUserId = reactCache( ( diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/lib/display.ts b/apps/web/app/api/v1/client/[environmentId]/displays/lib/display.ts index 9756cff825..04f4818cee 100644 --- a/apps/web/app/api/v1/client/[environmentId]/displays/lib/display.ts +++ b/apps/web/app/api/v1/client/[environmentId]/displays/lib/display.ts @@ -1,7 +1,7 @@ +import { displayCache } from "@/lib/display/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { displayCache } from "@formbricks/lib/display/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { TDisplayCreateInput, ZDisplayCreateInput } from "@formbricks/types/displays"; import { DatabaseError } from "@formbricks/types/errors"; import { getContactByUserId } from "./contact"; diff --git a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts index 478ea47041..de833038f3 100644 --- a/apps/web/app/api/v1/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/displays/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; -import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; import { logger } from "@formbricks/logger"; import { ZDisplayCreateInput } from "@formbricks/types/displays"; import { InvalidInputError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/actionClass.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/actionClass.ts index 5fd53071c3..cc19eca3ff 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/actionClass.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/actionClass.ts @@ -1,8 +1,8 @@ +import { actionClassCache } from "@/lib/actionClass/cache"; +import { cache } from "@/lib/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { actionClassCache } from "@formbricks/lib/actionClass/cache"; -import { cache } from "@formbricks/lib/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { TJsEnvironmentStateActionClass } from "@formbricks/types/js"; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts index b269fbb991..63eba89c52 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts @@ -1,20 +1,20 @@ -import { prisma } from "@formbricks/database"; -import { actionClassCache } from "@formbricks/lib/actionClass/cache"; -import { cache } from "@formbricks/lib/cache"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { environmentCache } from "@formbricks/lib/environment/cache"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { organizationCache } from "@formbricks/lib/organization/cache"; +import { actionClassCache } from "@/lib/actionClass/cache"; +import { cache } from "@/lib/cache"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { environmentCache } from "@/lib/environment/cache"; +import { getEnvironment } from "@/lib/environment/service"; +import { organizationCache } from "@/lib/organization/cache"; import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId, -} from "@formbricks/lib/organization/service"; +} from "@/lib/organization/service"; import { capturePosthogEnvironmentEvent, sendPlanLimitsReachedEventToPosthogWeekly, -} from "@formbricks/lib/posthogServer"; -import { projectCache } from "@formbricks/lib/project/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; +} from "@/lib/posthogServer"; +import { projectCache } from "@/lib/project/cache"; +import { surveyCache } from "@/lib/survey/cache"; +import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TJsEnvironmentState } from "@formbricks/types/js"; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/project.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/project.ts index 65da56f019..f64df61c0e 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/project.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/project.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; +import { projectCache } from "@/lib/project/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { projectCache } from "@formbricks/lib/project/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts index f3761e3aa0..445a061340 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts @@ -1,10 +1,10 @@ +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { transformPrismaSurvey } from "@/modules/survey/lib/utils"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts index 0f99348595..0ee3a06ff2 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts @@ -1,8 +1,8 @@ import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/environment/lib/environmentState"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { environmentCache } from "@/lib/environment/cache"; import { NextRequest } from "next/server"; -import { environmentCache } from "@formbricks/lib/environment/cache"; import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZJsSyncInput } from "@formbricks/types/js"; diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts index bc54dcb4d7..1ad6be6ccc 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { sendToPipeline } from "@/app/lib/pipelines"; -import { updateResponse } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; +import { updateResponse } from "@/lib/response/service"; +import { getSurvey } from "@/lib/survey/service"; import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { ZResponseUpdateInput } from "@formbricks/types/responses"; diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/lib/contact.ts b/apps/web/app/api/v1/client/[environmentId]/responses/lib/contact.ts index e34b987b05..fa8bf9e5a9 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/lib/contact.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/lib/contact.ts @@ -1,8 +1,8 @@ +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts b/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts index d961371381..14a93586ff 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/lib/response.ts @@ -1,17 +1,17 @@ import "server-only"; -import { Prisma } from "@prisma/client"; -import { prisma } from "@formbricks/database"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId, -} from "@formbricks/lib/organization/service"; -import { sendPlanLimitsReachedEventToPosthogWeekly } from "@formbricks/lib/posthogServer"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { calculateTtcTotal } from "@formbricks/lib/response/utils"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; -import { validateInputs } from "@formbricks/lib/utils/validate"; +} from "@/lib/organization/service"; +import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer"; +import { responseCache } from "@/lib/response/cache"; +import { calculateTtcTotal } from "@/lib/response/utils"; +import { responseNoteCache } from "@/lib/responseNote/cache"; +import { captureTelemetry } from "@/lib/telemetry"; +import { validateInputs } from "@/lib/utils/validate"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts index b49186bd78..635428af35 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts @@ -1,11 +1,11 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { sendToPipeline } from "@/app/lib/pipelines"; +import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer"; +import { getSurvey } from "@/lib/survey/service"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { headers } from "next/headers"; import { UAParser } from "ua-parser-js"; -import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; -import { getSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { InvalidInputError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/client/[environmentId]/storage/lib/uploadPrivateFile.ts b/apps/web/app/api/v1/client/[environmentId]/storage/lib/uploadPrivateFile.ts index d884b5527d..0db11e8932 100644 --- a/apps/web/app/api/v1/client/[environmentId]/storage/lib/uploadPrivateFile.ts +++ b/apps/web/app/api/v1/client/[environmentId]/storage/lib/uploadPrivateFile.ts @@ -1,5 +1,5 @@ import { responses } from "@/app/lib/api/response"; -import { getUploadSignedUrl } from "@formbricks/lib/storage/service"; +import { getUploadSignedUrl } from "@/lib/storage/service"; export const uploadPrivateFile = async ( fileName: string, diff --git a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts index 36bbfd3bb8..7462c0ae25 100644 --- a/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/storage/local/route.ts @@ -2,13 +2,13 @@ // body -> should be a valid file object (buffer) // method -> PUT (to be the same as the signedUrl method) import { responses } from "@/app/lib/api/response"; +import { ENCRYPTION_KEY, UPLOADS_DIR } from "@/lib/constants"; +import { validateLocalSignedUrl } from "@/lib/crypto"; +import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; +import { putFileToLocalStorage } from "@/lib/storage/service"; +import { getSurvey } from "@/lib/survey/service"; import { getBiggerUploadFileSizePermission } from "@/modules/ee/license-check/lib/utils"; import { NextRequest } from "next/server"; -import { ENCRYPTION_KEY, UPLOADS_DIR } from "@formbricks/lib/constants"; -import { validateLocalSignedUrl } from "@formbricks/lib/crypto"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { putFileToLocalStorage } from "@formbricks/lib/storage/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; interface Context { diff --git a/apps/web/app/api/v1/client/[environmentId]/storage/route.ts b/apps/web/app/api/v1/client/[environmentId]/storage/route.ts index d449db85f6..3ece306fdd 100644 --- a/apps/web/app/api/v1/client/[environmentId]/storage/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/storage/route.ts @@ -1,9 +1,9 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; +import { getSurvey } from "@/lib/survey/service"; import { getBiggerUploadFileSizePermission } from "@/modules/ee/license-check/lib/utils"; import { NextRequest } from "next/server"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; import { ZUploadFileRequest } from "@formbricks/types/storage"; import { uploadPrivateFile } from "./lib/uploadPrivateFile"; diff --git a/apps/web/app/api/v1/integrations/airtable/callback/route.ts b/apps/web/app/api/v1/integrations/airtable/callback/route.ts index df441e79df..50a69fa3be 100644 --- a/apps/web/app/api/v1/integrations/airtable/callback/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/callback/route.ts @@ -1,12 +1,12 @@ import { responses } from "@/app/lib/api/response"; +import { fetchAirtableAuthToken } from "@/lib/airtable/service"; +import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; +import { createOrUpdateIntegration } from "@/lib/integration/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import * as z from "zod"; -import { fetchAirtableAuthToken } from "@formbricks/lib/airtable/service"; -import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; -import { createOrUpdateIntegration } from "@formbricks/lib/integration/service"; import { logger } from "@formbricks/logger"; const getEmail = async (token: string) => { diff --git a/apps/web/app/api/v1/integrations/airtable/route.ts b/apps/web/app/api/v1/integrations/airtable/route.ts index 3045ecd087..b13e675ac4 100644 --- a/apps/web/app/api/v1/integrations/airtable/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/route.ts @@ -1,10 +1,10 @@ import { responses } from "@/app/lib/api/response"; +import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; import { authOptions } from "@/modules/auth/lib/authOptions"; import crypto from "crypto"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; const scope = `data.records:read data.records:write schema.bases:read schema.bases:write user.email:read`; diff --git a/apps/web/app/api/v1/integrations/airtable/tables/route.ts b/apps/web/app/api/v1/integrations/airtable/tables/route.ts index 08056b4f0f..bf1643e1bf 100644 --- a/apps/web/app/api/v1/integrations/airtable/tables/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/tables/route.ts @@ -1,11 +1,11 @@ import { responses } from "@/app/lib/api/response"; +import { getTables } from "@/lib/airtable/service"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; +import { getIntegrationByType } from "@/lib/integration/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import * as z from "zod"; -import { getTables } from "@formbricks/lib/airtable/service"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; -import { getIntegrationByType } from "@formbricks/lib/integration/service"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; export const GET = async (req: NextRequest) => { diff --git a/apps/web/app/api/v1/integrations/notion/callback/route.ts b/apps/web/app/api/v1/integrations/notion/callback/route.ts index 5483dc639e..5e849cbe63 100644 --- a/apps/web/app/api/v1/integrations/notion/callback/route.ts +++ b/apps/web/app/api/v1/integrations/notion/callback/route.ts @@ -1,14 +1,14 @@ import { responses } from "@/app/lib/api/response"; -import { NextRequest } from "next/server"; import { ENCRYPTION_KEY, NOTION_OAUTH_CLIENT_ID, NOTION_OAUTH_CLIENT_SECRET, NOTION_REDIRECT_URI, WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { symmetricEncrypt } from "@formbricks/lib/crypto"; -import { createOrUpdateIntegration, getIntegrationByType } from "@formbricks/lib/integration/service"; +} from "@/lib/constants"; +import { symmetricEncrypt } from "@/lib/crypto"; +import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service"; +import { NextRequest } from "next/server"; import { TIntegrationNotionConfigData, TIntegrationNotionInput } from "@formbricks/types/integration/notion"; export const GET = async (req: NextRequest) => { diff --git a/apps/web/app/api/v1/integrations/notion/route.ts b/apps/web/app/api/v1/integrations/notion/route.ts index d707e583d4..f413c49236 100644 --- a/apps/web/app/api/v1/integrations/notion/route.ts +++ b/apps/web/app/api/v1/integrations/notion/route.ts @@ -1,14 +1,14 @@ import { responses } from "@/app/lib/api/response"; -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { getServerSession } from "next-auth"; -import { NextRequest } from "next/server"; import { NOTION_AUTH_URL, NOTION_OAUTH_CLIENT_ID, NOTION_OAUTH_CLIENT_SECRET, NOTION_REDIRECT_URI, -} from "@formbricks/lib/constants"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +} from "@/lib/constants"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; export const GET = async (req: NextRequest) => { const environmentId = req.headers.get("environmentId"); diff --git a/apps/web/app/api/v1/integrations/slack/callback/route.ts b/apps/web/app/api/v1/integrations/slack/callback/route.ts index 3661ae05bb..d0eefdeb90 100644 --- a/apps/web/app/api/v1/integrations/slack/callback/route.ts +++ b/apps/web/app/api/v1/integrations/slack/callback/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/app/lib/api/response"; +import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants"; +import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service"; import { NextRequest } from "next/server"; -import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { createOrUpdateIntegration, getIntegrationByType } from "@formbricks/lib/integration/service"; import { TIntegrationSlackConfig, TIntegrationSlackConfigData, diff --git a/apps/web/app/api/v1/integrations/slack/route.ts b/apps/web/app/api/v1/integrations/slack/route.ts index 46fa8fb339..d797828b30 100644 --- a/apps/web/app/api/v1/integrations/slack/route.ts +++ b/apps/web/app/api/v1/integrations/slack/route.ts @@ -1,9 +1,9 @@ import { responses } from "@/app/lib/api/response"; +import { SLACK_AUTH_URL, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET } from "@/lib/constants"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { SLACK_AUTH_URL, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET } from "@formbricks/lib/constants"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; export const GET = async (req: NextRequest) => { const environmentId = req.headers.get("environmentId"); diff --git a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts index 1a3e2c073b..0ab32ac6c6 100644 --- a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts +++ b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts @@ -1,8 +1,8 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { deleteActionClass, getActionClass, updateActionClass } from "@/lib/actionClass/service"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; -import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service"; import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; diff --git a/apps/web/app/api/v1/management/action-classes/lib/action-classes.test.ts b/apps/web/app/api/v1/management/action-classes/lib/action-classes.test.ts index a1a8f0410e..f8b4eaba8a 100644 --- a/apps/web/app/api/v1/management/action-classes/lib/action-classes.test.ts +++ b/apps/web/app/api/v1/management/action-classes/lib/action-classes.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { DatabaseError } from "@formbricks/types/errors"; import { getActionClasses } from "./action-classes"; @@ -43,7 +43,7 @@ describe("getActionClasses", () => { vi.clearAllMocks(); }); - it("should successfully fetch action classes for given environment IDs", async () => { + test("successfully fetches action classes for given environment IDs", async () => { // Mock the prisma findMany response vi.mocked(prisma.actionClass.findMany).mockResolvedValue(mockActionClasses); @@ -61,14 +61,14 @@ describe("getActionClasses", () => { }); }); - it("should throw DatabaseError when prisma query fails", async () => { + test("throws DatabaseError when prisma query fails", async () => { // Mock the prisma findMany to throw an error vi.mocked(prisma.actionClass.findMany).mockRejectedValue(new Error("Database error")); await expect(getActionClasses(mockEnvironmentIds)).rejects.toThrow(DatabaseError); }); - it("should handle empty environment IDs array", async () => { + test("handles empty environment IDs array", async () => { // Mock the prisma findMany response vi.mocked(prisma.actionClass.findMany).mockResolvedValue([]); diff --git a/apps/web/app/api/v1/management/action-classes/lib/action-classes.ts b/apps/web/app/api/v1/management/action-classes/lib/action-classes.ts index 3cd0c2263b..5b08851068 100644 --- a/apps/web/app/api/v1/management/action-classes/lib/action-classes.ts +++ b/apps/web/app/api/v1/management/action-classes/lib/action-classes.ts @@ -1,12 +1,12 @@ "use server"; import "server-only"; +import { actionClassCache } from "@/lib/actionClass/cache"; +import { cache } from "@/lib/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { actionClassCache } from "@formbricks/lib/actionClass/cache"; -import { cache } from "@formbricks/lib/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { TActionClass } from "@formbricks/types/action-classes"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/management/action-classes/route.ts b/apps/web/app/api/v1/management/action-classes/route.ts index 378f64e528..50ecd683c1 100644 --- a/apps/web/app/api/v1/management/action-classes/route.ts +++ b/apps/web/app/api/v1/management/action-classes/route.ts @@ -1,8 +1,8 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { createActionClass } from "@/lib/actionClass/service"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; -import { createActionClass } from "@formbricks/lib/actionClass/service"; import { logger } from "@formbricks/logger"; import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 43fac5e93e..93f5d8fee8 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -1,9 +1,9 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { deleteResponse, getResponse, updateResponse } from "@/lib/response/service"; +import { getSurvey } from "@/lib/survey/service"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; -import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; import { ZResponseUpdateInput } from "@formbricks/types/responses"; diff --git a/apps/web/app/api/v1/management/responses/lib/contact.ts b/apps/web/app/api/v1/management/responses/lib/contact.ts index 810f01c645..81cc45a18b 100644 --- a/apps/web/app/api/v1/management/responses/lib/contact.ts +++ b/apps/web/app/api/v1/management/responses/lib/contact.ts @@ -1,8 +1,8 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; export const getContactByUserId = reactCache( diff --git a/apps/web/app/api/v1/management/responses/lib/response.ts b/apps/web/app/api/v1/management/responses/lib/response.ts index bd5c80d567..de383dfcf7 100644 --- a/apps/web/app/api/v1/management/responses/lib/response.ts +++ b/apps/web/app/api/v1/management/responses/lib/response.ts @@ -1,20 +1,20 @@ import "server-only"; -import { Prisma } from "@prisma/client"; -import { cache as reactCache } from "react"; -import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { cache } from "@/lib/cache"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId, -} from "@formbricks/lib/organization/service"; -import { sendPlanLimitsReachedEventToPosthogWeekly } from "@formbricks/lib/posthogServer"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { getResponseContact } from "@formbricks/lib/response/service"; -import { calculateTtcTotal } from "@formbricks/lib/response/utils"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; -import { validateInputs } from "@formbricks/lib/utils/validate"; +} from "@/lib/organization/service"; +import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer"; +import { responseCache } from "@/lib/response/cache"; +import { getResponseContact } from "@/lib/response/service"; +import { calculateTtcTotal } from "@/lib/response/utils"; +import { responseNoteCache } from "@/lib/responseNote/cache"; +import { captureTelemetry } from "@/lib/telemetry"; +import { validateInputs } from "@/lib/utils/validate"; +import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { ZId, ZOptionalNumber } from "@formbricks/types/common"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; diff --git a/apps/web/app/api/v1/management/responses/route.ts b/apps/web/app/api/v1/management/responses/route.ts index fe3fb059ad..2ab88dfa7c 100644 --- a/apps/web/app/api/v1/management/responses/route.ts +++ b/apps/web/app/api/v1/management/responses/route.ts @@ -1,10 +1,10 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { getResponses } from "@/lib/response/service"; +import { getSurvey } from "@/lib/survey/service"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { NextRequest } from "next/server"; -import { getResponses } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; import { TResponse, ZResponseInput } from "@formbricks/types/responses"; diff --git a/apps/web/app/api/v1/management/storage/lib/getSignedUrl.ts b/apps/web/app/api/v1/management/storage/lib/getSignedUrl.ts index 7e44385973..8b98f1075e 100644 --- a/apps/web/app/api/v1/management/storage/lib/getSignedUrl.ts +++ b/apps/web/app/api/v1/management/storage/lib/getSignedUrl.ts @@ -1,5 +1,5 @@ import { responses } from "@/app/lib/api/response"; -import { getUploadSignedUrl } from "@formbricks/lib/storage/service"; +import { getUploadSignedUrl } from "@/lib/storage/service"; export const getSignedUrlForPublicFile = async ( fileName: string, diff --git a/apps/web/app/api/v1/management/storage/local/route.ts b/apps/web/app/api/v1/management/storage/local/route.ts index 4c1398903e..d9a9a79b40 100644 --- a/apps/web/app/api/v1/management/storage/local/route.ts +++ b/apps/web/app/api/v1/management/storage/local/route.ts @@ -2,14 +2,14 @@ // body -> should be a valid file object (buffer) // method -> PUT (to be the same as the signedUrl method) import { responses } from "@/app/lib/api/response"; +import { ENCRYPTION_KEY, UPLOADS_DIR } from "@/lib/constants"; +import { validateLocalSignedUrl } from "@/lib/crypto"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; +import { putFileToLocalStorage } from "@/lib/storage/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { headers } from "next/headers"; import { NextRequest } from "next/server"; -import { ENCRYPTION_KEY, UPLOADS_DIR } from "@formbricks/lib/constants"; -import { validateLocalSignedUrl } from "@formbricks/lib/crypto"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; -import { putFileToLocalStorage } from "@formbricks/lib/storage/service"; export const POST = async (req: NextRequest): Promise => { if (!ENCRYPTION_KEY) { diff --git a/apps/web/app/api/v1/management/storage/route.ts b/apps/web/app/api/v1/management/storage/route.ts index 9a5060b2be..fc22a34760 100644 --- a/apps/web/app/api/v1/management/storage/route.ts +++ b/apps/web/app/api/v1/management/storage/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { logger } from "@formbricks/logger"; import { getSignedUrlForPublicFile } from "./lib/getSignedUrl"; diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts index c70179f17b..7b1ccc718d 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/lib/surveys.ts @@ -1,10 +1,10 @@ +import { segmentCache } from "@/lib/cache/segment"; +import { responseCache } from "@/lib/response/cache"; +import { surveyCache } from "@/lib/survey/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { z } from "zod"; import { prisma } from "@formbricks/database"; -import { segmentCache } from "@formbricks/lib/cache/segment"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts index 1e5f46a4d6..0c2dbeb619 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts @@ -2,11 +2,11 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { deleteSurvey } from "@/app/api/v1/management/surveys/[surveyId]/lib/surveys"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; +import { getSurvey, updateSurvey } from "@/lib/survey/service"; import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { ZSurveyUpdateInput } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts index 93439f92f3..1c855c062d 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/singleUseIds/route.ts @@ -1,10 +1,10 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; +import { getSurveyDomain } from "@/lib/getSurveyUrl"; +import { getSurvey } from "@/lib/survey/service"; +import { generateSurveySingleUseIds } from "@/lib/utils/singleUseSurveys"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { NextRequest } from "next/server"; -import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl"; -import { getSurvey } from "@formbricks/lib/survey/service"; -import { generateSurveySingleUseIds } from "@formbricks/lib/utils/singleUseSurveys"; export const GET = async ( request: NextRequest, @@ -22,6 +22,10 @@ export const GET = async ( return responses.unauthorizedResponse(); } + if (survey.type !== "link") { + return responses.badRequestResponse("Single use links are only available for link surveys"); + } + if (!survey.singleUse || !survey.singleUse.enabled) { return responses.badRequestResponse("Single use links are not enabled for this survey"); } diff --git a/apps/web/app/api/v1/management/surveys/lib/surveys.ts b/apps/web/app/api/v1/management/surveys/lib/surveys.ts index 9529a51ed5..19fbaf5a1c 100644 --- a/apps/web/app/api/v1/management/surveys/lib/surveys.ts +++ b/apps/web/app/api/v1/management/surveys/lib/surveys.ts @@ -1,12 +1,12 @@ import "server-only"; +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; +import { selectSurvey } from "@/lib/survey/service"; +import { transformPrismaSurvey } from "@/lib/survey/utils"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { selectSurvey } from "@formbricks/lib/survey/service"; -import { transformPrismaSurvey } from "@formbricks/lib/survey/utils"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZOptionalNumber } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts index c9db2c4e38..65d183dfb7 100644 --- a/apps/web/app/api/v1/management/surveys/route.ts +++ b/apps/web/app/api/v1/management/surveys/route.ts @@ -1,11 +1,11 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { getOrganizationByEnvironmentId } from "@/lib/organization/service"; +import { createSurvey } from "@/lib/survey/service"; import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { createSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; import { DatabaseError } from "@formbricks/types/errors"; import { ZSurveyCreateInputWithEnvironmentId } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts b/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts index 4e7ffb9a47..66d352c449 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; import { webhookCache } from "@/lib/cache/webhook"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma, Webhook } from "@prisma/client"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { cache } from "@formbricks/lib/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v1/webhooks/lib/webhook.ts b/apps/web/app/api/v1/webhooks/lib/webhook.ts index a1dedd70fa..db5a50bd27 100644 --- a/apps/web/app/api/v1/webhooks/lib/webhook.ts +++ b/apps/web/app/api/v1/webhooks/lib/webhook.ts @@ -1,10 +1,10 @@ import { TWebhookInput, ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks"; +import { cache } from "@/lib/cache"; import { webhookCache } from "@/lib/cache/webhook"; +import { ITEMS_PER_PAGE } from "@/lib/constants"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma, Webhook } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId, ZOptionalNumber } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts b/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts index a7c02dad94..a39fb8fc67 100644 --- a/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts +++ b/apps/web/app/api/v2/client/[environmentId]/displays/lib/contact.ts @@ -1,7 +1,7 @@ +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; export const doesContactExist = reactCache( (id: string): Promise => diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts b/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts index c6ddd6479f..1d7f0a114c 100644 --- a/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts +++ b/apps/web/app/api/v2/client/[environmentId]/displays/lib/display.ts @@ -2,10 +2,10 @@ import { TDisplayCreateInputV2, ZDisplayCreateInputV2, } from "@/app/api/v2/client/[environmentId]/displays/types/display"; +import { displayCache } from "@/lib/display/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { displayCache } from "@formbricks/lib/display/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { DatabaseError } from "@formbricks/types/errors"; import { doesContactExist } from "./contact"; diff --git a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts index f91d3f1347..fd8a753aac 100644 --- a/apps/web/app/api/v2/client/[environmentId]/displays/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/displays/route.ts @@ -1,8 +1,8 @@ import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displays/types/display"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; -import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; import { logger } from "@formbricks/logger"; import { InvalidInputError } from "@formbricks/types/errors"; import { createDisplay } from "./lib/display"; diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/lib/contact.ts b/apps/web/app/api/v2/client/[environmentId]/responses/lib/contact.ts index 2fb4ec337c..90ac45fd26 100644 --- a/apps/web/app/api/v2/client/[environmentId]/responses/lib/contact.ts +++ b/apps/web/app/api/v2/client/[environmentId]/responses/lib/contact.ts @@ -1,7 +1,7 @@ +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; export const getContact = reactCache((contactId: string) => diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/lib/response.ts b/apps/web/app/api/v2/client/[environmentId]/responses/lib/response.ts index 61dd326ea6..be1f02a486 100644 --- a/apps/web/app/api/v2/client/[environmentId]/responses/lib/response.ts +++ b/apps/web/app/api/v2/client/[environmentId]/responses/lib/response.ts @@ -1,19 +1,19 @@ import "server-only"; import { responseSelection } from "@/app/api/v1/client/[environmentId]/responses/lib/response"; import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response"; -import { Prisma } from "@prisma/client"; -import { prisma } from "@formbricks/database"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId, -} from "@formbricks/lib/organization/service"; -import { sendPlanLimitsReachedEventToPosthogWeekly } from "@formbricks/lib/posthogServer"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { calculateTtcTotal } from "@formbricks/lib/response/utils"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; -import { validateInputs } from "@formbricks/lib/utils/validate"; +} from "@/lib/organization/service"; +import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer"; +import { responseCache } from "@/lib/response/cache"; +import { calculateTtcTotal } from "@/lib/response/utils"; +import { responseNoteCache } from "@/lib/responseNote/cache"; +import { captureTelemetry } from "@/lib/telemetry"; +import { validateInputs } from "@/lib/utils/validate"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/route.ts b/apps/web/app/api/v2/client/[environmentId]/responses/route.ts index 2231ad4d4e..2a412b0046 100644 --- a/apps/web/app/api/v2/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/responses/route.ts @@ -1,11 +1,11 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { sendToPipeline } from "@/app/lib/pipelines"; +import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer"; +import { getSurvey } from "@/lib/survey/service"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { headers } from "next/headers"; import { UAParser } from "ua-parser-js"; -import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer"; -import { getSurvey } from "@formbricks/lib/survey/service"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { InvalidInputError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts b/apps/web/app/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts new file mode 100644 index 0000000000..6ae62003eb --- /dev/null +++ b/apps/web/app/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts @@ -0,0 +1,7 @@ +import { + DELETE, + GET, + PUT, +} from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route"; + +export { GET, PUT, DELETE }; diff --git a/apps/web/app/api/v2/management/contact-attribute-keys/route.ts b/apps/web/app/api/v2/management/contact-attribute-keys/route.ts new file mode 100644 index 0000000000..2b7018e820 --- /dev/null +++ b/apps/web/app/api/v2/management/contact-attribute-keys/route.ts @@ -0,0 +1,3 @@ +import { GET, POST } from "@/modules/api/v2/management/contact-attribute-keys/route"; + +export { GET, POST }; diff --git a/apps/web/app/error.test.tsx b/apps/web/app/error.test.tsx new file mode 100644 index 0000000000..b2c91817ab --- /dev/null +++ b/apps/web/app/error.test.tsx @@ -0,0 +1,72 @@ +import * as Sentry from "@sentry/nextjs"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import ErrorBoundary from "./error"; + +vi.mock("@/modules/ui/components/button", () => ({ + Button: (props: any) => , +})); + +vi.mock("@/modules/ui/components/error-component", () => ({ + ErrorComponent: () =>
ErrorComponent
, +})); + +vi.mock("@sentry/nextjs", () => ({ + captureException: vi.fn(), +})); + +describe("ErrorBoundary", () => { + afterEach(() => { + cleanup(); + }); + + const dummyError = new Error("Test error"); + const resetMock = vi.fn(); + + test("logs error via console.error in development", async () => { + (process.env as { [key: string]: string }).NODE_ENV = "development"; + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); + + render(); + + await waitFor(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith("Test error"); + }); + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); + + test("captures error with Sentry in production", async () => { + (process.env as { [key: string]: string }).NODE_ENV = "production"; + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); + + render(); + + await waitFor(() => { + expect(Sentry.captureException).toHaveBeenCalled(); + }); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + test("calls reset when try again button is clicked", async () => { + render(); + const tryAgainBtn = screen.getByRole("button", { name: "common.try_again" }); + userEvent.click(tryAgainBtn); + await waitFor(() => expect(resetMock).toHaveBeenCalled()); + }); + + test("sets window.location.href to '/' when dashboard button is clicked", async () => { + const originalLocation = window.location; + delete (window as any).location; + (window as any).location = { href: "" }; + render(); + const dashBtn = screen.getByRole("button", { name: "common.go_to_dashboard" }); + userEvent.click(dashBtn); + await waitFor(() => { + expect(window.location.href).toBe("/"); + }); + window.location = originalLocation; + }); +}); diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx index a99389a6a0..b16482cd7e 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/app/error.tsx @@ -3,12 +3,15 @@ // Error components must be Client components import { Button } from "@/modules/ui/components/button"; import { ErrorComponent } from "@/modules/ui/components/error-component"; +import * as Sentry from "@sentry/nextjs"; import { useTranslate } from "@tolgee/react"; -const Error = ({ error, reset }: { error: Error; reset: () => void }) => { +const ErrorBoundary = ({ error, reset }: { error: Error; reset: () => void }) => { const { t } = useTranslate(); if (process.env.NODE_ENV === "development") { console.error(error.message); + } else { + Sentry.captureException(error); } return ( @@ -24,4 +27,4 @@ const Error = ({ error, reset }: { error: Error; reset: () => void }) => { ); }; -export default Error; +export default ErrorBoundary; diff --git a/apps/web/app/global-error.test.tsx b/apps/web/app/global-error.test.tsx new file mode 100644 index 0000000000..52b339d031 --- /dev/null +++ b/apps/web/app/global-error.test.tsx @@ -0,0 +1,41 @@ +import * as Sentry from "@sentry/nextjs"; +import { cleanup, render, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import GlobalError from "./global-error"; + +vi.mock("@sentry/nextjs", () => ({ + captureException: vi.fn(), +})); + +describe("GlobalError", () => { + const dummyError = new Error("Test error"); + + afterEach(() => { + cleanup(); + }); + + test("logs error using console.error in development", async () => { + (process.env as { [key: string]: string }).NODE_ENV = "development"; + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); + + render(); + + await waitFor(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith("Test error"); + }); + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); + + test("captures error with Sentry in production", async () => { + (process.env as { [key: string]: string }).NODE_ENV = "production"; + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + render(); + + await waitFor(() => { + expect(Sentry.captureException).toHaveBeenCalled(); + }); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx new file mode 100644 index 0000000000..077670f229 --- /dev/null +++ b/apps/web/app/global-error.tsx @@ -0,0 +1,22 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import NextError from "next/error"; +import { useEffect } from "react"; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + if (process.env.NODE_ENV === "development") { + console.error(error.message); + } else { + Sentry.captureException(error); + } + }, [error]); + return ( + + + + + + ); +} diff --git a/apps/web/app/intercom/IntercomClient.test.tsx b/apps/web/app/intercom/IntercomClient.test.tsx index 8c78cda32a..6f96920bd7 100644 --- a/apps/web/app/intercom/IntercomClient.test.tsx +++ b/apps/web/app/intercom/IntercomClient.test.tsx @@ -1,7 +1,7 @@ import Intercom from "@intercom/messenger-js-sdk"; import "@testing-library/jest-dom/vitest"; import { cleanup, render } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { TUser } from "@formbricks/types/user"; import { IntercomClient } from "./IntercomClient"; @@ -26,7 +26,7 @@ describe("IntercomClient", () => { global.window.Intercom = originalWindowIntercom; }); - it("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => { + test("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => { const testUser = { id: "test-id", name: "Test User", @@ -55,7 +55,7 @@ describe("IntercomClient", () => { }); }); - it("calls Intercom with user data without createdAt", () => { + test("calls Intercom with user data without createdAt", () => { const testUser = { id: "test-id", name: "Test User", @@ -83,7 +83,7 @@ describe("IntercomClient", () => { }); }); - it("calls Intercom with minimal params if user is not provided", () => { + test("calls Intercom with minimal params if user is not provided", () => { render( ); @@ -94,7 +94,7 @@ describe("IntercomClient", () => { }); }); - it("does not call Intercom if isIntercomConfigured is false", () => { + test("does not call Intercom if isIntercomConfigured is false", () => { render( { expect(Intercom).not.toHaveBeenCalled(); }); - it("shuts down Intercom on unmount", () => { + test("shuts down Intercom on unmount", () => { const { unmount } = render( ); @@ -120,7 +120,7 @@ describe("IntercomClient", () => { expect(mockWindowIntercom).toHaveBeenCalledWith("shutdown"); }); - it("logs an error if Intercom initialization fails", () => { + test("logs an error if Intercom initialization fails", () => { // Spy on console.error const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -141,7 +141,7 @@ describe("IntercomClient", () => { consoleErrorSpy.mockRestore(); }); - it("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => { + test("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); render( @@ -159,7 +159,7 @@ describe("IntercomClient", () => { consoleErrorSpy.mockRestore(); }); - it("logs an error if isIntercomConfigured is true but no intercomUserHash is provided", () => { + test("logs an error if isIntercomConfigured is true but no intercomUserHash is provided", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const testUser = { id: "test-id", diff --git a/apps/web/app/intercom/IntercomClientWrapper.test.tsx b/apps/web/app/intercom/IntercomClientWrapper.test.tsx index 52c8eaaf4f..59bcc1989b 100644 --- a/apps/web/app/intercom/IntercomClientWrapper.test.tsx +++ b/apps/web/app/intercom/IntercomClientWrapper.test.tsx @@ -1,9 +1,9 @@ import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { TUser } from "@formbricks/types/user"; import { IntercomClientWrapper } from "./IntercomClientWrapper"; -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ IS_INTERCOM_CONFIGURED: true, INTERCOM_APP_ID: "mock-intercom-app-id", INTERCOM_SECRET_KEY: "mock-intercom-secret-key", @@ -31,7 +31,7 @@ describe("IntercomClientWrapper", () => { cleanup(); }); - it("renders IntercomClient with computed user hash when user is provided", () => { + test("renders IntercomClient with computed user hash when user is provided", () => { const testUser = { id: "user-123", name: "Test User", email: "test@example.com" } as TUser; render(); @@ -48,7 +48,7 @@ describe("IntercomClientWrapper", () => { expect(props.user).toEqual(testUser); }); - it("renders IntercomClient without computing a hash when no user is provided", () => { + test("renders IntercomClient without computing a hash when no user is provided", () => { render(); const intercomClientEl = screen.getByTestId("mock-intercom-client"); diff --git a/apps/web/app/intercom/IntercomClientWrapper.tsx b/apps/web/app/intercom/IntercomClientWrapper.tsx index dd8daa76a5..331c93083a 100644 --- a/apps/web/app/intercom/IntercomClientWrapper.tsx +++ b/apps/web/app/intercom/IntercomClientWrapper.tsx @@ -1,5 +1,5 @@ +import { INTERCOM_APP_ID, INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@/lib/constants"; import { createHmac } from "crypto"; -import { INTERCOM_APP_ID, INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants"; import type { TUser } from "@formbricks/types/user"; import { IntercomClient } from "./IntercomClient"; diff --git a/apps/web/app/layout.test.tsx b/apps/web/app/layout.test.tsx index 62b30062a8..40527c1cd4 100644 --- a/apps/web/app/layout.test.tsx +++ b/apps/web/app/layout.test.tsx @@ -3,12 +3,12 @@ import { getTolgee } from "@/tolgee/server"; import { cleanup, render, screen } from "@testing-library/react"; import { TolgeeInstance } from "@tolgee/react"; import React from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import RootLayout from "./layout"; // Mock dependencies for the layout -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ IS_FORMBRICKS_CLOUD: false, POSTHOG_API_KEY: "mock-posthog-api-key", POSTHOG_HOST: "mock-posthog-host", @@ -81,7 +81,7 @@ describe("RootLayout", () => { process.env.VERCEL = "1"; }); - it("renders the layout with the correct structure and providers", async () => { + test("renders the layout with the correct structure and providers", async () => { const fakeLocale = "en-US"; // Mock getLocale to resolve to a fake locale vi.mocked(getLocale).mockResolvedValue(fakeLocale); diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6b541b9ddc..ee7b027e7c 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,11 +1,11 @@ import { SentryProvider } from "@/app/sentry/SentryProvider"; +import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants"; import { TolgeeNextProvider } from "@/tolgee/client"; import { getLocale } from "@/tolgee/language"; import { getTolgee } from "@/tolgee/server"; import { TolgeeStaticData } from "@tolgee/react"; import { Metadata } from "next"; import React from "react"; -import { SENTRY_DSN } from "@formbricks/lib/constants"; import "../modules/ui/globals.css"; export const metadata: Metadata = { @@ -25,7 +25,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => { return ( - + {children} diff --git a/apps/web/app/lib/formbricks.ts b/apps/web/app/lib/formbricks.ts index b1838e5f3c..989022e59e 100644 --- a/apps/web/app/lib/formbricks.ts +++ b/apps/web/app/lib/formbricks.ts @@ -1,5 +1,5 @@ +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; import formbricks from "@formbricks/js"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; export const formbricksLogout = async () => { const loggedInWith = localStorage.getItem(FORMBRICKS_LOGGED_IN_WITH_LS); diff --git a/apps/web/app/lib/pipelines.test.ts b/apps/web/app/lib/pipelines.test.ts index 306a4260d5..73e0f9bee7 100644 --- a/apps/web/app/lib/pipelines.test.ts +++ b/apps/web/app/lib/pipelines.test.ts @@ -6,7 +6,7 @@ import { TResponse } from "@formbricks/types/responses"; import { sendToPipeline } from "./pipelines"; // Mock the constants module -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ CRON_SECRET: "mocked-cron-secret", WEBAPP_URL: "https://test.formbricks.com", })); @@ -91,10 +91,10 @@ describe("pipelines", () => { test("sendToPipeline should throw error if CRON_SECRET is not set", async () => { // For this test, we need to mock CRON_SECRET as undefined // Let's use a more compatible approach to reset the mocks - const originalModule = await import("@formbricks/lib/constants"); + const originalModule = await import("@/lib/constants"); const mockConstants = { ...originalModule, CRON_SECRET: undefined }; - vi.doMock("@formbricks/lib/constants", () => mockConstants); + vi.doMock("@/lib/constants", () => mockConstants); // Re-import the module to get the new mocked values const { sendToPipeline: sendToPipelineNoSecret } = await import("./pipelines"); diff --git a/apps/web/app/lib/pipelines.ts b/apps/web/app/lib/pipelines.ts index d1f040efa2..b80bf59ef7 100644 --- a/apps/web/app/lib/pipelines.ts +++ b/apps/web/app/lib/pipelines.ts @@ -1,5 +1,5 @@ import { TPipelineInput } from "@/app/lib/types/pipelines"; -import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { CRON_SECRET, WEBAPP_URL } from "@/lib/constants"; import { logger } from "@formbricks/logger"; export const sendToPipeline = async ({ event, surveyId, environmentId, response }: TPipelineInput) => { diff --git a/apps/web/app/lib/singleUseSurveys.test.ts b/apps/web/app/lib/singleUseSurveys.test.ts index c941c135d4..b9505ce72c 100644 --- a/apps/web/app/lib/singleUseSurveys.test.ts +++ b/apps/web/app/lib/singleUseSurveys.test.ts @@ -1,19 +1,17 @@ +import * as crypto from "@/lib/crypto"; import cuid2 from "@paralleldrive/cuid2"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import * as crypto from "@formbricks/lib/crypto"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { generateSurveySingleUseId, validateSurveySingleUseId } from "./singleUseSurveys"; // Mock the crypto module -vi.mock("@formbricks/lib/crypto", () => ({ +vi.mock("@/lib/crypto", () => ({ symmetricEncrypt: vi.fn(), symmetricDecrypt: vi.fn(), - decryptAES128: vi.fn(), })); // Mock constants -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ ENCRYPTION_KEY: "test-encryption-key", - FORMBRICKS_ENCRYPTION_KEY: "test-formbricks-encryption-key", })); // Mock cuid2 @@ -45,21 +43,21 @@ describe("generateSurveySingleUseId", () => { vi.resetAllMocks(); }); - it("returns unencrypted cuid when isEncrypted is false", () => { + test("returns unencrypted cuid when isEncrypted is false", () => { const result = generateSurveySingleUseId(false); expect(result).toBe(mockCuid); expect(crypto.symmetricEncrypt).not.toHaveBeenCalled(); }); - it("returns encrypted cuid when isEncrypted is true", () => { + test("returns encrypted cuid when isEncrypted is true", () => { const result = generateSurveySingleUseId(true); expect(result).toBe(mockEncryptedCuid); expect(crypto.symmetricEncrypt).toHaveBeenCalledWith(mockCuid, "test-encryption-key"); }); - it("returns undefined when cuid is not valid", () => { + test("returns undefined when cuid is not valid", () => { vi.mocked(cuid2.isCuid).mockReturnValue(false); const result = validateSurveySingleUseId(mockEncryptedCuid); @@ -67,7 +65,7 @@ describe("generateSurveySingleUseId", () => { expect(result).toBeUndefined(); }); - it("returns undefined when decryption fails", () => { + test("returns undefined when decryption fails", () => { vi.mocked(crypto.symmetricDecrypt).mockImplementation(() => { throw new Error("Decryption failed"); }); @@ -77,11 +75,10 @@ describe("generateSurveySingleUseId", () => { expect(result).toBeUndefined(); }); - it("throws error when ENCRYPTION_KEY is not set in generateSurveySingleUseId", async () => { + test("throws error when ENCRYPTION_KEY is not set in generateSurveySingleUseId", async () => { // Temporarily mock ENCRYPTION_KEY as undefined - vi.doMock("@formbricks/lib/constants", () => ({ + vi.doMock("@/lib/constants", () => ({ ENCRYPTION_KEY: undefined, - FORMBRICKS_ENCRYPTION_KEY: "test-formbricks-encryption-key", })); // Re-import to get the new mock values @@ -90,11 +87,10 @@ describe("generateSurveySingleUseId", () => { expect(() => generateSurveySingleUseIdNoKey(true)).toThrow("ENCRYPTION_KEY is not set"); }); - it("throws error when ENCRYPTION_KEY is not set in validateSurveySingleUseId for symmetric encryption", async () => { + test("throws error when ENCRYPTION_KEY is not set in validateSurveySingleUseId for symmetric encryption", async () => { // Temporarily mock ENCRYPTION_KEY as undefined - vi.doMock("@formbricks/lib/constants", () => ({ + vi.doMock("@/lib/constants", () => ({ ENCRYPTION_KEY: undefined, - FORMBRICKS_ENCRYPTION_KEY: "test-formbricks-encryption-key", })); // Re-import to get the new mock values @@ -102,19 +98,4 @@ describe("generateSurveySingleUseId", () => { expect(() => validateSurveySingleUseIdNoKey(mockEncryptedCuid)).toThrow("ENCRYPTION_KEY is not set"); }); - - it("throws error when FORMBRICKS_ENCRYPTION_KEY is not set in validateSurveySingleUseId for AES128", async () => { - // Temporarily mock FORMBRICKS_ENCRYPTION_KEY as undefined - vi.doMock("@formbricks/lib/constants", () => ({ - ENCRYPTION_KEY: "test-encryption-key", - FORMBRICKS_ENCRYPTION_KEY: undefined, - })); - - // Re-import to get the new mock values - const { validateSurveySingleUseId: validateSurveySingleUseIdNoKey } = await import("./singleUseSurveys"); - - expect(() => - validateSurveySingleUseIdNoKey("M(.Bob=dS1!wUSH2lb,E7hxO=He1cnnitmXrG|Su/DKYZrPy~zgS)u?dgI53sfs/") - ).toThrow("FORMBRICKS_ENCRYPTION_KEY is not defined"); - }); }); diff --git a/apps/web/app/lib/singleUseSurveys.ts b/apps/web/app/lib/singleUseSurveys.ts index aaceacd6d9..eee1005fe5 100644 --- a/apps/web/app/lib/singleUseSurveys.ts +++ b/apps/web/app/lib/singleUseSurveys.ts @@ -1,6 +1,6 @@ +import { ENCRYPTION_KEY } from "@/lib/constants"; +import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto"; import cuid2 from "@paralleldrive/cuid2"; -import { ENCRYPTION_KEY, FORMBRICKS_ENCRYPTION_KEY } from "@formbricks/lib/constants"; -import { decryptAES128, symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto"; // generate encrypted single use id for the survey export const generateSurveySingleUseId = (isEncrypted: boolean): string => { @@ -21,25 +21,13 @@ export const generateSurveySingleUseId = (isEncrypted: boolean): string => { export const validateSurveySingleUseId = (surveySingleUseId: string): string | undefined => { let decryptedCuid: string | null = null; - if (surveySingleUseId.length === 64) { - if (!FORMBRICKS_ENCRYPTION_KEY) { - throw new Error("FORMBRICKS_ENCRYPTION_KEY is not defined"); - } - - try { - decryptedCuid = decryptAES128(FORMBRICKS_ENCRYPTION_KEY, surveySingleUseId); - } catch (error) { - return undefined; - } - } else { - if (!ENCRYPTION_KEY) { - throw new Error("ENCRYPTION_KEY is not set"); - } - try { - decryptedCuid = symmetricDecrypt(surveySingleUseId, ENCRYPTION_KEY); - } catch (error) { - return undefined; - } + if (!ENCRYPTION_KEY) { + throw new Error("ENCRYPTION_KEY is not set"); + } + try { + decryptedCuid = symmetricDecrypt(surveySingleUseId, ENCRYPTION_KEY); + } catch (error) { + return undefined; } if (cuid2.isCuid(decryptedCuid)) { diff --git a/apps/web/app/lib/survey-builder.test.ts b/apps/web/app/lib/survey-builder.test.ts new file mode 100644 index 0000000000..5a78d2e0a8 --- /dev/null +++ b/apps/web/app/lib/survey-builder.test.ts @@ -0,0 +1,612 @@ +import { describe, expect, test } from "vitest"; +import { TShuffleOption, TSurveyLogic, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; +import { TTemplateRole } from "@formbricks/types/templates"; +import { + buildCTAQuestion, + buildConsentQuestion, + buildMultipleChoiceQuestion, + buildNPSQuestion, + buildOpenTextQuestion, + buildRatingQuestion, + buildSurvey, + createChoiceJumpLogic, + createJumpLogic, + getDefaultEndingCard, + getDefaultSurveyPreset, + getDefaultWelcomeCard, + hiddenFieldsDefault, +} from "./survey-builder"; + +// Mock the TFnType from @tolgee/react +const mockT = (props: any): string => (typeof props === "string" ? props : props.key); + +describe("Survey Builder", () => { + describe("buildMultipleChoiceQuestion", () => { + test("creates a single choice question with required fields", () => { + const question = buildMultipleChoiceQuestion({ + headline: "Test Question", + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + choices: ["Option 1", "Option 2", "Option 3"], + t: mockT, + }); + + expect(question).toMatchObject({ + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + headline: { default: "Test Question" }, + choices: expect.arrayContaining([ + expect.objectContaining({ label: { default: "Option 1" } }), + expect.objectContaining({ label: { default: "Option 2" } }), + expect.objectContaining({ label: { default: "Option 3" } }), + ]), + buttonLabel: { default: "common.next" }, + backButtonLabel: { default: "common.back" }, + shuffleOption: "none", + required: true, + }); + expect(question.choices.length).toBe(3); + expect(question.id).toBeDefined(); + }); + + test("creates a multiple choice question with provided ID", () => { + const customId = "custom-id-123"; + const question = buildMultipleChoiceQuestion({ + id: customId, + headline: "Test Question", + type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, + choices: ["Option 1", "Option 2"], + t: mockT, + }); + + expect(question.id).toBe(customId); + expect(question.type).toBe(TSurveyQuestionTypeEnum.MultipleChoiceMulti); + }); + + test("handles 'other' option correctly", () => { + const choices = ["Option 1", "Option 2", "Other"]; + const question = buildMultipleChoiceQuestion({ + headline: "Test Question", + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + choices, + containsOther: true, + t: mockT, + }); + + expect(question.choices.length).toBe(3); + expect(question.choices[2].id).toBe("other"); + }); + + test("uses provided choice IDs when available", () => { + const choiceIds = ["id1", "id2", "id3"]; + const question = buildMultipleChoiceQuestion({ + headline: "Test Question", + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + choices: ["Option 1", "Option 2", "Option 3"], + choiceIds, + t: mockT, + }); + + expect(question.choices[0].id).toBe(choiceIds[0]); + expect(question.choices[1].id).toBe(choiceIds[1]); + expect(question.choices[2].id).toBe(choiceIds[2]); + }); + + test("applies all optional parameters correctly", () => { + const logic: TSurveyLogic[] = [ + { + id: "logic-1", + conditions: { + id: "cond-1", + connector: "and", + conditions: [], + }, + actions: [], + }, + ]; + + const shuffleOption: TShuffleOption = "all"; + + const question = buildMultipleChoiceQuestion({ + headline: "Test Question", + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + subheader: "This is a subheader", + choices: ["Option 1", "Option 2"], + buttonLabel: "Custom Next", + backButtonLabel: "Custom Back", + shuffleOption, + required: false, + logic, + t: mockT, + }); + + expect(question.subheader).toEqual({ default: "This is a subheader" }); + expect(question.buttonLabel).toEqual({ default: "Custom Next" }); + expect(question.backButtonLabel).toEqual({ default: "Custom Back" }); + expect(question.shuffleOption).toBe("all"); + expect(question.required).toBe(false); + expect(question.logic).toBe(logic); + }); + }); + + describe("buildOpenTextQuestion", () => { + test("creates an open text question with required fields", () => { + const question = buildOpenTextQuestion({ + headline: "Open Question", + inputType: "text", + t: mockT, + }); + + expect(question).toMatchObject({ + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Open Question" }, + inputType: "text", + buttonLabel: { default: "common.next" }, + backButtonLabel: { default: "common.back" }, + required: true, + charLimit: { + enabled: false, + }, + }); + expect(question.id).toBeDefined(); + }); + + test("applies all optional parameters correctly", () => { + const logic: TSurveyLogic[] = [ + { + id: "logic-1", + conditions: { + id: "cond-1", + connector: "and", + conditions: [], + }, + actions: [], + }, + ]; + + const question = buildOpenTextQuestion({ + id: "custom-id", + headline: "Open Question", + subheader: "Answer this question", + placeholder: "Type here", + buttonLabel: "Submit", + backButtonLabel: "Previous", + required: false, + longAnswer: true, + inputType: "email", + logic, + t: mockT, + }); + + expect(question.id).toBe("custom-id"); + expect(question.subheader).toEqual({ default: "Answer this question" }); + expect(question.placeholder).toEqual({ default: "Type here" }); + expect(question.buttonLabel).toEqual({ default: "Submit" }); + expect(question.backButtonLabel).toEqual({ default: "Previous" }); + expect(question.required).toBe(false); + expect(question.longAnswer).toBe(true); + expect(question.inputType).toBe("email"); + expect(question.logic).toBe(logic); + }); + }); + + describe("buildRatingQuestion", () => { + test("creates a rating question with required fields", () => { + const question = buildRatingQuestion({ + headline: "Rating Question", + scale: "number", + range: 5, + t: mockT, + }); + + expect(question).toMatchObject({ + type: TSurveyQuestionTypeEnum.Rating, + headline: { default: "Rating Question" }, + scale: "number", + range: 5, + buttonLabel: { default: "common.next" }, + backButtonLabel: { default: "common.back" }, + required: true, + isColorCodingEnabled: false, + }); + expect(question.id).toBeDefined(); + }); + + test("applies all optional parameters correctly", () => { + const logic: TSurveyLogic[] = [ + { + id: "logic-1", + conditions: { + id: "cond-1", + connector: "and", + conditions: [], + }, + actions: [], + }, + ]; + + const question = buildRatingQuestion({ + id: "custom-id", + headline: "Rating Question", + subheader: "Rate us", + scale: "star", + range: 10, + lowerLabel: "Poor", + upperLabel: "Excellent", + buttonLabel: "Submit", + backButtonLabel: "Previous", + required: false, + isColorCodingEnabled: true, + logic, + t: mockT, + }); + + expect(question.id).toBe("custom-id"); + expect(question.subheader).toEqual({ default: "Rate us" }); + expect(question.scale).toBe("star"); + expect(question.range).toBe(10); + expect(question.lowerLabel).toEqual({ default: "Poor" }); + expect(question.upperLabel).toEqual({ default: "Excellent" }); + expect(question.buttonLabel).toEqual({ default: "Submit" }); + expect(question.backButtonLabel).toEqual({ default: "Previous" }); + expect(question.required).toBe(false); + expect(question.isColorCodingEnabled).toBe(true); + expect(question.logic).toBe(logic); + }); + }); + + describe("buildNPSQuestion", () => { + test("creates an NPS question with required fields", () => { + const question = buildNPSQuestion({ + headline: "NPS Question", + t: mockT, + }); + + expect(question).toMatchObject({ + type: TSurveyQuestionTypeEnum.NPS, + headline: { default: "NPS Question" }, + buttonLabel: { default: "common.next" }, + backButtonLabel: { default: "common.back" }, + required: true, + isColorCodingEnabled: false, + }); + expect(question.id).toBeDefined(); + }); + + test("applies all optional parameters correctly", () => { + const logic: TSurveyLogic[] = [ + { + id: "logic-1", + conditions: { + id: "cond-1", + connector: "and", + conditions: [], + }, + actions: [], + }, + ]; + + const question = buildNPSQuestion({ + id: "custom-id", + headline: "NPS Question", + subheader: "How likely are you to recommend us?", + lowerLabel: "Not likely", + upperLabel: "Very likely", + buttonLabel: "Submit", + backButtonLabel: "Previous", + required: false, + isColorCodingEnabled: true, + logic, + t: mockT, + }); + + expect(question.id).toBe("custom-id"); + expect(question.subheader).toEqual({ default: "How likely are you to recommend us?" }); + expect(question.lowerLabel).toEqual({ default: "Not likely" }); + expect(question.upperLabel).toEqual({ default: "Very likely" }); + expect(question.buttonLabel).toEqual({ default: "Submit" }); + expect(question.backButtonLabel).toEqual({ default: "Previous" }); + expect(question.required).toBe(false); + expect(question.isColorCodingEnabled).toBe(true); + expect(question.logic).toBe(logic); + }); + }); + + describe("buildConsentQuestion", () => { + test("creates a consent question with required fields", () => { + const question = buildConsentQuestion({ + headline: "Consent Question", + label: "I agree to terms", + t: mockT, + }); + + expect(question).toMatchObject({ + type: TSurveyQuestionTypeEnum.Consent, + headline: { default: "Consent Question" }, + label: { default: "I agree to terms" }, + buttonLabel: { default: "common.next" }, + backButtonLabel: { default: "common.back" }, + required: true, + }); + expect(question.id).toBeDefined(); + }); + + test("applies all optional parameters correctly", () => { + const logic: TSurveyLogic[] = [ + { + id: "logic-1", + conditions: { + id: "cond-1", + connector: "and", + conditions: [], + }, + actions: [], + }, + ]; + + const question = buildConsentQuestion({ + id: "custom-id", + headline: "Consent Question", + subheader: "Please read the terms", + label: "I agree to terms", + buttonLabel: "Submit", + backButtonLabel: "Previous", + required: false, + logic, + t: mockT, + }); + + expect(question.id).toBe("custom-id"); + expect(question.subheader).toEqual({ default: "Please read the terms" }); + expect(question.label).toEqual({ default: "I agree to terms" }); + expect(question.buttonLabel).toEqual({ default: "Submit" }); + expect(question.backButtonLabel).toEqual({ default: "Previous" }); + expect(question.required).toBe(false); + expect(question.logic).toBe(logic); + }); + }); + + describe("buildCTAQuestion", () => { + test("creates a CTA question with required fields", () => { + const question = buildCTAQuestion({ + headline: "CTA Question", + buttonExternal: false, + t: mockT, + }); + + expect(question).toMatchObject({ + type: TSurveyQuestionTypeEnum.CTA, + headline: { default: "CTA Question" }, + buttonLabel: { default: "common.next" }, + backButtonLabel: { default: "common.back" }, + required: true, + buttonExternal: false, + }); + expect(question.id).toBeDefined(); + }); + + test("applies all optional parameters correctly", () => { + const logic: TSurveyLogic[] = [ + { + id: "logic-1", + conditions: { + id: "cond-1", + connector: "and", + conditions: [], + }, + actions: [], + }, + ]; + + const question = buildCTAQuestion({ + id: "custom-id", + headline: "CTA Question", + html: "

Click the button

", + buttonLabel: "Click me", + buttonExternal: true, + buttonUrl: "https://example.com", + backButtonLabel: "Previous", + required: false, + dismissButtonLabel: "No thanks", + logic, + t: mockT, + }); + + expect(question.id).toBe("custom-id"); + expect(question.html).toEqual({ default: "

Click the button

" }); + expect(question.buttonLabel).toEqual({ default: "Click me" }); + expect(question.buttonExternal).toBe(true); + expect(question.buttonUrl).toBe("https://example.com"); + expect(question.backButtonLabel).toEqual({ default: "Previous" }); + expect(question.required).toBe(false); + expect(question.dismissButtonLabel).toEqual({ default: "No thanks" }); + expect(question.logic).toBe(logic); + }); + + test("handles external button with URL", () => { + const question = buildCTAQuestion({ + headline: "CTA Question", + buttonExternal: true, + buttonUrl: "https://formbricks.com", + t: mockT, + }); + + expect(question.buttonExternal).toBe(true); + expect(question.buttonUrl).toBe("https://formbricks.com"); + }); + }); + + // Test combinations of parameters for edge cases + describe("Edge cases", () => { + test("multiple choice question with empty choices array", () => { + const question = buildMultipleChoiceQuestion({ + headline: "Test Question", + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + choices: [], + t: mockT, + }); + + expect(question.choices).toEqual([]); + }); + + test("open text question with all parameters", () => { + const question = buildOpenTextQuestion({ + id: "custom-id", + headline: "Open Question", + subheader: "Answer this question", + placeholder: "Type here", + buttonLabel: "Submit", + backButtonLabel: "Previous", + required: false, + longAnswer: true, + inputType: "email", + logic: [], + t: mockT, + }); + + expect(question).toMatchObject({ + id: "custom-id", + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Open Question" }, + subheader: { default: "Answer this question" }, + placeholder: { default: "Type here" }, + buttonLabel: { default: "Submit" }, + backButtonLabel: { default: "Previous" }, + required: false, + longAnswer: true, + inputType: "email", + logic: [], + }); + }); + }); +}); + +describe("Helper Functions", () => { + test("createJumpLogic returns valid jump logic", () => { + const sourceId = "q1"; + const targetId = "q2"; + const operator: "isClicked" = "isClicked"; + const logic = createJumpLogic(sourceId, targetId, operator); + + // Check structure + expect(logic).toHaveProperty("id"); + expect(logic).toHaveProperty("conditions"); + expect(logic.conditions).toHaveProperty("conditions"); + expect(Array.isArray(logic.conditions.conditions)).toBe(true); + + // Check one of the inner conditions + const condition = logic.conditions.conditions[0]; + // Need to use type checking to ensure condition is a TSingleCondition not a TConditionGroup + if (!("connector" in condition)) { + expect(condition.leftOperand.value).toBe(sourceId); + expect(condition.operator).toBe(operator); + } + + // Check actions + expect(Array.isArray(logic.actions)).toBe(true); + const action = logic.actions[0]; + if (action.objective === "jumpToQuestion") { + expect(action.target).toBe(targetId); + } + }); + + test("createChoiceJumpLogic returns valid jump logic based on choice selection", () => { + const sourceId = "q1"; + const choiceId = "choice1"; + const targetId = "q2"; + const logic = createChoiceJumpLogic(sourceId, choiceId, targetId); + + expect(logic).toHaveProperty("id"); + expect(logic.conditions).toHaveProperty("conditions"); + + const condition = logic.conditions.conditions[0]; + if (!("connector" in condition)) { + expect(condition.leftOperand.value).toBe(sourceId); + expect(condition.operator).toBe("equals"); + expect(condition.rightOperand?.value).toBe(choiceId); + } + + const action = logic.actions[0]; + if (action.objective === "jumpToQuestion") { + expect(action.target).toBe(targetId); + } + }); + + test("getDefaultWelcomeCard returns expected welcome card", () => { + const card = getDefaultWelcomeCard(mockT); + expect(card.enabled).toBe(false); + expect(card.headline).toEqual({ default: "templates.default_welcome_card_headline" }); + expect(card.html).toEqual({ default: "templates.default_welcome_card_html" }); + expect(card.buttonLabel).toEqual({ default: "templates.default_welcome_card_button_label" }); + // boolean flags + expect(card.timeToFinish).toBe(false); + expect(card.showResponseCount).toBe(false); + }); + + test("getDefaultEndingCard returns expected end screen card", () => { + // Pass empty languages array to simulate no languages + const card = getDefaultEndingCard([], mockT); + expect(card).toHaveProperty("id"); + expect(card.type).toBe("endScreen"); + expect(card.headline).toEqual({ default: "templates.default_ending_card_headline" }); + expect(card.subheader).toEqual({ default: "templates.default_ending_card_subheader" }); + expect(card.buttonLabel).toEqual({ default: "templates.default_ending_card_button_label" }); + expect(card.buttonLink).toBe("https://formbricks.com"); + }); + + test("getDefaultSurveyPreset returns expected default survey preset", () => { + const preset = getDefaultSurveyPreset(mockT); + expect(preset.name).toBe("New Survey"); + expect(preset.questions).toEqual([]); + // test welcomeCard and endings + expect(preset.welcomeCard).toHaveProperty("headline"); + expect(Array.isArray(preset.endings)).toBe(true); + expect(preset.hiddenFields).toEqual(hiddenFieldsDefault); + }); + + test("buildSurvey returns built survey with overridden preset properties", () => { + const config = { + name: "Custom Survey", + role: "productManager" as TTemplateRole, + industries: ["eCommerce"] as string[], + channels: ["link"], + description: "Test survey", + questions: [ + { + id: "q1", + type: TSurveyQuestionTypeEnum.OpenText, // changed from "OpenText" + headline: { default: "Question 1" }, + inputType: "text", + buttonLabel: { default: "Next" }, + backButtonLabel: { default: "Back" }, + required: true, + }, + ], + endings: [ + { + id: "end1", + type: "endScreen", + headline: { default: "End Screen" }, + subheader: { default: "Thanks" }, + buttonLabel: { default: "Finish" }, + buttonLink: "https://formbricks.com", + }, + ], + hiddenFields: { enabled: false, fieldIds: ["f1"] }, + }; + + const survey = buildSurvey(config as any, mockT); + expect(survey.name).toBe(config.name); + expect(survey.role).toBe(config.role); + expect(survey.industries).toEqual(config.industries); + expect(survey.channels).toEqual(config.channels); + expect(survey.description).toBe(config.description); + // preset overrides + expect(survey.preset.name).toBe(config.name); + expect(survey.preset.questions).toEqual(config.questions); + expect(survey.preset.endings).toEqual(config.endings); + expect(survey.preset.hiddenFields).toEqual(config.hiddenFields); + }); + + test("hiddenFieldsDefault has expected default configuration", () => { + expect(hiddenFieldsDefault).toEqual({ enabled: true, fieldIds: [] }); + }); +}); diff --git a/apps/web/app/lib/survey-builder.ts b/apps/web/app/lib/survey-builder.ts new file mode 100644 index 0000000000..8abe858092 --- /dev/null +++ b/apps/web/app/lib/survey-builder.ts @@ -0,0 +1,414 @@ +import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils"; +import { createId } from "@paralleldrive/cuid2"; +import { TFnType } from "@tolgee/react"; +import { + TShuffleOption, + TSurveyCTAQuestion, + TSurveyConsentQuestion, + TSurveyEndScreenCard, + TSurveyEnding, + TSurveyHiddenFields, + TSurveyLanguage, + TSurveyLogic, + TSurveyMultipleChoiceQuestion, + TSurveyNPSQuestion, + TSurveyOpenTextQuestion, + TSurveyOpenTextQuestionInputType, + TSurveyQuestion, + TSurveyQuestionTypeEnum, + TSurveyRatingQuestion, + TSurveyWelcomeCard, +} from "@formbricks/types/surveys/types"; +import { TTemplate, TTemplateRole } from "@formbricks/types/templates"; + +const defaultButtonLabel = "common.next"; +const defaultBackButtonLabel = "common.back"; + +export const buildMultipleChoiceQuestion = ({ + id, + headline, + type, + subheader, + choices, + choiceIds, + buttonLabel, + backButtonLabel, + shuffleOption, + required, + logic, + containsOther = false, + t, +}: { + id?: string; + headline: string; + type: TSurveyQuestionTypeEnum.MultipleChoiceMulti | TSurveyQuestionTypeEnum.MultipleChoiceSingle; + subheader?: string; + choices: string[]; + choiceIds?: string[]; + buttonLabel?: string; + backButtonLabel?: string; + shuffleOption?: TShuffleOption; + required?: boolean; + logic?: TSurveyLogic[]; + containsOther?: boolean; + t: TFnType; +}): TSurveyMultipleChoiceQuestion => { + return { + id: id ?? createId(), + type, + subheader: subheader ? { default: subheader } : undefined, + headline: { default: headline }, + choices: choices.map((choice, index) => { + const isLastIndex = index === choices.length - 1; + const id = containsOther && isLastIndex ? "other" : choiceIds ? choiceIds[index] : createId(); + return { id, label: { default: choice } }; + }), + buttonLabel: { default: buttonLabel || t(defaultButtonLabel) }, + backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) }, + shuffleOption: shuffleOption || "none", + required: required ?? true, + logic, + }; +}; + +export const buildOpenTextQuestion = ({ + id, + headline, + subheader, + placeholder, + inputType, + buttonLabel, + backButtonLabel, + required, + logic, + longAnswer, + t, +}: { + id?: string; + headline: string; + subheader?: string; + placeholder?: string; + buttonLabel?: string; + backButtonLabel?: string; + required?: boolean; + logic?: TSurveyLogic[]; + inputType: TSurveyOpenTextQuestionInputType; + longAnswer?: boolean; + t: TFnType; +}): TSurveyOpenTextQuestion => { + return { + id: id ?? createId(), + type: TSurveyQuestionTypeEnum.OpenText, + inputType, + subheader: subheader ? { default: subheader } : undefined, + placeholder: placeholder ? { default: placeholder } : undefined, + headline: { default: headline }, + buttonLabel: { default: buttonLabel || t(defaultButtonLabel) }, + backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) }, + required: required ?? true, + longAnswer, + logic, + charLimit: { + enabled: false, + }, + }; +}; + +export const buildRatingQuestion = ({ + id, + headline, + subheader, + scale, + range, + lowerLabel, + upperLabel, + buttonLabel, + backButtonLabel, + required, + logic, + isColorCodingEnabled = false, + t, +}: { + id?: string; + headline: string; + scale: TSurveyRatingQuestion["scale"]; + range: TSurveyRatingQuestion["range"]; + lowerLabel?: string; + upperLabel?: string; + subheader?: string; + placeholder?: string; + buttonLabel?: string; + backButtonLabel?: string; + required?: boolean; + logic?: TSurveyLogic[]; + isColorCodingEnabled?: boolean; + t: TFnType; +}): TSurveyRatingQuestion => { + return { + id: id ?? createId(), + type: TSurveyQuestionTypeEnum.Rating, + subheader: subheader ? { default: subheader } : undefined, + headline: { default: headline }, + scale, + range, + buttonLabel: { default: buttonLabel || t(defaultButtonLabel) }, + backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) }, + required: required ?? true, + isColorCodingEnabled, + lowerLabel: lowerLabel ? { default: lowerLabel } : undefined, + upperLabel: upperLabel ? { default: upperLabel } : undefined, + logic, + }; +}; + +export const buildNPSQuestion = ({ + id, + headline, + subheader, + lowerLabel, + upperLabel, + buttonLabel, + backButtonLabel, + required, + logic, + isColorCodingEnabled = false, + t, +}: { + id?: string; + headline: string; + lowerLabel?: string; + upperLabel?: string; + subheader?: string; + placeholder?: string; + buttonLabel?: string; + backButtonLabel?: string; + required?: boolean; + logic?: TSurveyLogic[]; + isColorCodingEnabled?: boolean; + t: TFnType; +}): TSurveyNPSQuestion => { + return { + id: id ?? createId(), + type: TSurveyQuestionTypeEnum.NPS, + subheader: subheader ? { default: subheader } : undefined, + headline: { default: headline }, + buttonLabel: { default: buttonLabel || t(defaultButtonLabel) }, + backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) }, + required: required ?? true, + isColorCodingEnabled, + lowerLabel: lowerLabel ? { default: lowerLabel } : undefined, + upperLabel: upperLabel ? { default: upperLabel } : undefined, + logic, + }; +}; + +export const buildConsentQuestion = ({ + id, + headline, + subheader, + label, + buttonLabel, + backButtonLabel, + required, + logic, + t, +}: { + id?: string; + headline: string; + subheader?: string; + buttonLabel?: string; + backButtonLabel?: string; + required?: boolean; + logic?: TSurveyLogic[]; + label: string; + t: TFnType; +}): TSurveyConsentQuestion => { + return { + id: id ?? createId(), + type: TSurveyQuestionTypeEnum.Consent, + subheader: subheader ? { default: subheader } : undefined, + headline: { default: headline }, + buttonLabel: { default: buttonLabel || t(defaultButtonLabel) }, + backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) }, + required: required ?? true, + label: { default: label }, + logic, + }; +}; + +export const buildCTAQuestion = ({ + id, + headline, + html, + buttonLabel, + buttonExternal, + backButtonLabel, + required, + logic, + dismissButtonLabel, + buttonUrl, + t, +}: { + id?: string; + headline: string; + buttonExternal: boolean; + html?: string; + buttonLabel?: string; + backButtonLabel?: string; + required?: boolean; + logic?: TSurveyLogic[]; + dismissButtonLabel?: string; + buttonUrl?: string; + t: TFnType; +}): TSurveyCTAQuestion => { + return { + id: id ?? createId(), + type: TSurveyQuestionTypeEnum.CTA, + html: html ? { default: html } : undefined, + headline: { default: headline }, + buttonLabel: { default: buttonLabel || t(defaultButtonLabel) }, + backButtonLabel: { default: backButtonLabel || t(defaultBackButtonLabel) }, + dismissButtonLabel: dismissButtonLabel ? { default: dismissButtonLabel } : undefined, + required: required ?? true, + buttonExternal, + buttonUrl, + logic, + }; +}; + +// Helper function to create standard jump logic based on operator +export const createJumpLogic = ( + sourceQuestionId: string, + targetId: string, + operator: "isSkipped" | "isSubmitted" | "isClicked" +): TSurveyLogic => ({ + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: sourceQuestionId, + type: "question", + }, + operator: operator, + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: targetId, + }, + ], +}); + +// Helper function to create jump logic based on choice selection +export const createChoiceJumpLogic = ( + sourceQuestionId: string, + choiceId: string, + targetId: string +): TSurveyLogic => ({ + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: sourceQuestionId, + type: "question", + }, + operator: "equals", + rightOperand: { + type: "static", + value: choiceId, + }, + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: targetId, + }, + ], +}); + +export const getDefaultEndingCard = (languages: TSurveyLanguage[], t: TFnType): TSurveyEndScreenCard => { + const languageCodes = extractLanguageCodes(languages); + return { + id: createId(), + type: "endScreen", + headline: createI18nString(t("templates.default_ending_card_headline"), languageCodes), + subheader: createI18nString(t("templates.default_ending_card_subheader"), languageCodes), + buttonLabel: createI18nString(t("templates.default_ending_card_button_label"), languageCodes), + buttonLink: "https://formbricks.com", + }; +}; + +export const hiddenFieldsDefault: TSurveyHiddenFields = { + enabled: true, + fieldIds: [], +}; + +export const getDefaultWelcomeCard = (t: TFnType): TSurveyWelcomeCard => { + return { + enabled: false, + headline: { default: t("templates.default_welcome_card_headline") }, + html: { default: t("templates.default_welcome_card_html") }, + buttonLabel: { default: t("templates.default_welcome_card_button_label") }, + timeToFinish: false, + showResponseCount: false, + }; +}; + +export const getDefaultSurveyPreset = (t: TFnType): TTemplate["preset"] => { + return { + name: "New Survey", + welcomeCard: getDefaultWelcomeCard(t), + endings: [getDefaultEndingCard([], t)], + hiddenFields: hiddenFieldsDefault, + questions: [], + }; +}; + +/** + * Generic builder for survey. + * @param config - The configuration for survey settings and questions. + * @param t - The translation function. + */ +export const buildSurvey = ( + config: { + name: string; + role: TTemplateRole; + industries: ("eCommerce" | "saas" | "other")[]; + channels: ("link" | "app" | "website")[]; + description: string; + questions: TSurveyQuestion[]; + endings?: TSurveyEnding[]; + hiddenFields?: TSurveyHiddenFields; + }, + t: TFnType +): TTemplate => { + const localSurvey = getDefaultSurveyPreset(t); + return { + name: config.name, + role: config.role, + industries: config.industries, + channels: config.channels, + description: config.description, + preset: { + ...localSurvey, + name: config.name, + questions: config.questions, + endings: config.endings ?? localSurvey.endings, + hiddenFields: config.hiddenFields ?? hiddenFieldsDefault, + }, + }; +}; diff --git a/apps/web/app/lib/templates.ts b/apps/web/app/lib/templates.ts index f8042c6ad5..c377bc4041 100644 --- a/apps/web/app/lib/templates.ts +++ b/apps/web/app/lib/templates.ts @@ -1,1288 +1,523 @@ +import { + buildCTAQuestion, + buildConsentQuestion, + buildMultipleChoiceQuestion, + buildNPSQuestion, + buildOpenTextQuestion, + buildRatingQuestion, + buildSurvey, + createChoiceJumpLogic, + createJumpLogic, + getDefaultEndingCard, + getDefaultSurveyPreset, + hiddenFieldsDefault, +} from "@/app/lib/survey-builder"; import { createId } from "@paralleldrive/cuid2"; import { TFnType } from "@tolgee/react"; -import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; -import { - TSurvey, - TSurveyEndScreenCard, - TSurveyHiddenFields, - TSurveyLanguage, - TSurveyOpenTextQuestion, - TSurveyQuestionTypeEnum, - TSurveyWelcomeCard, -} from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyOpenTextQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TTemplate } from "@formbricks/types/templates"; -export const getDefaultEndingCard = (languages: TSurveyLanguage[], t: TFnType): TSurveyEndScreenCard => { - const languageCodes = extractLanguageCodes(languages); - return { - id: createId(), - type: "endScreen", - headline: createI18nString(t("templates.default_ending_card_headline"), languageCodes), - subheader: createI18nString(t("templates.default_ending_card_subheader"), languageCodes), - buttonLabel: createI18nString(t("templates.default_ending_card_button_label"), languageCodes), - buttonLink: "https://formbricks.com", - }; -}; - -const hiddenFieldsDefault: TSurveyHiddenFields = { - enabled: true, - fieldIds: [], -}; - -export const getDefaultWelcomeCard = (t: TFnType): TSurveyWelcomeCard => { - return { - enabled: false, - headline: { default: t("templates.default_welcome_card_headline") }, - html: { default: t("templates.default_welcome_card_html") }, - buttonLabel: { default: t("templates.default_welcome_card_button_label") }, - timeToFinish: false, - showResponseCount: false, - }; -}; - -export const getDefaultSurveyPreset = (t: TFnType): TTemplate["preset"] => { - return { - name: "New Survey", - welcomeCard: getDefaultWelcomeCard(t), - endings: [getDefaultEndingCard([], t)], - hiddenFields: hiddenFieldsDefault, - questions: [], - }; -}; - const cartAbandonmentSurvey = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.card_abandonment_survey"), - role: "productManager", - industries: ["eCommerce"], - channels: ["app", "website", "link"], - description: t("templates.card_abandonment_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.card_abandonment_survey"), + role: "productManager", + industries: ["eCommerce"], + channels: ["app", "website", "link"], + description: t("templates.card_abandonment_survey_description"), + endings: localSurvey.endings, questions: [ - { + buildCTAQuestion({ id: reusableQuestionIds[0], - html: { - default: t("templates.card_abandonment_survey_question_1_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.card_abandonment_survey_question_1_headline") }, + html: t("templates.card_abandonment_survey_question_1_html"), + logic: [createJumpLogic(reusableQuestionIds[0], localSurvey.endings[0].id, "isSkipped")], + headline: t("templates.card_abandonment_survey_question_1_headline"), required: false, - buttonLabel: { default: t("templates.card_abandonment_survey_question_1_button_label") }, + buttonLabel: t("templates.card_abandonment_survey_question_1_button_label"), buttonExternal: false, - dismissButtonLabel: { - default: t("templates.card_abandonment_survey_question_1_dismiss_button_label"), - }, - }, - { - id: createId(), + dismissButtonLabel: t("templates.card_abandonment_survey_question_1_dismiss_button_label"), + t, + }), + buildMultipleChoiceQuestion({ + headline: t("templates.card_abandonment_survey_question_2_headline"), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.card_abandonment_survey_question_2_headline") }, - subheader: { default: t("templates.card_abandonment_survey_question_2_subheader") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - required: true, - shuffleOption: "none", + subheader: t("templates.card_abandonment_survey_question_2_subheader"), choices: [ - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_2_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_2_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_2_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.card_abandonment_survey_question_2_choice_6") }, - }, + t("templates.card_abandonment_survey_question_2_choice_1"), + t("templates.card_abandonment_survey_question_2_choice_2"), + t("templates.card_abandonment_survey_question_2_choice_3"), + t("templates.card_abandonment_survey_question_2_choice_4"), + t("templates.card_abandonment_survey_question_2_choice_5"), + t("templates.card_abandonment_survey_question_2_choice_6"), ], - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.card_abandonment_survey_question_3_headline"), - }, + containsOther: true, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.card_abandonment_survey_question_3_headline"), required: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, inputType: "text", - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: t("templates.card_abandonment_survey_question_4_headline") }, + t, + }), + buildRatingQuestion({ + headline: t("templates.card_abandonment_survey_question_4_headline"), required: true, scale: "number", range: 5, - lowerLabel: { default: t("templates.card_abandonment_survey_question_4_lower_label") }, - upperLabel: { default: t("templates.card_abandonment_survey_question_4_upper_label") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - isColorCodingEnabled: false, - }, - { - id: createId(), + lowerLabel: t("templates.card_abandonment_survey_question_4_lower_label"), + upperLabel: t("templates.card_abandonment_survey_question_4_upper_label"), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - headline: { - default: t("templates.card_abandonment_survey_question_5_headline"), - }, - subheader: { default: t("templates.card_abandonment_survey_question_5_subheader") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, + headline: t("templates.card_abandonment_survey_question_5_headline"), + subheader: t("templates.card_abandonment_survey_question_5_subheader"), + required: true, choices: [ - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_5_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_5_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_5_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_5_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.card_abandonment_survey_question_5_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.card_abandonment_survey_question_5_choice_6") }, - }, + t("templates.card_abandonment_survey_question_5_choice_1"), + t("templates.card_abandonment_survey_question_5_choice_2"), + t("templates.card_abandonment_survey_question_5_choice_3"), + t("templates.card_abandonment_survey_question_5_choice_4"), + t("templates.card_abandonment_survey_question_5_choice_5"), + t("templates.card_abandonment_survey_question_5_choice_6"), ], - }, - { + containsOther: true, + t, + }), + buildConsentQuestion({ id: reusableQuestionIds[1], - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - ], - type: TSurveyQuestionTypeEnum.Consent, - headline: { default: t("templates.card_abandonment_survey_question_6_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], reusableQuestionIds[2], "isSkipped")], + headline: t("templates.card_abandonment_survey_question_6_headline"), required: false, - label: { default: t("templates.card_abandonment_survey_question_6_label") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.card_abandonment_survey_question_7_headline") }, + label: t("templates.card_abandonment_survey_question_6_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.card_abandonment_survey_question_7_headline"), required: true, inputType: "email", longAnswer: false, - placeholder: { default: "example@email.com" }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + placeholder: "example@email.com", + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.card_abandonment_survey_question_8_headline") }, + headline: t("templates.card_abandonment_survey_question_8_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const siteAbandonmentSurvey = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.site_abandonment_survey"), - role: "productManager", - industries: ["eCommerce"], - channels: ["app", "website"], - description: t("templates.site_abandonment_survey_description"), - preset: { - ...localSurvey, + + return buildSurvey( + { name: t("templates.site_abandonment_survey"), + role: "productManager", + industries: ["eCommerce"], + channels: ["app", "website"], + description: t("templates.site_abandonment_survey_description"), + endings: localSurvey.endings, questions: [ - { + buildCTAQuestion({ id: reusableQuestionIds[0], - html: { - default: t("templates.site_abandonment_survey_question_1_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.site_abandonment_survey_question_2_headline") }, + html: t("templates.site_abandonment_survey_question_1_html"), + logic: [createJumpLogic(reusableQuestionIds[0], localSurvey.endings[0].id, "isSkipped")], + headline: t("templates.site_abandonment_survey_question_2_headline"), required: false, - buttonLabel: { default: t("templates.site_abandonment_survey_question_2_button_label") }, - backButtonLabel: { default: t("templates.back") }, + buttonLabel: t("templates.site_abandonment_survey_question_2_button_label"), buttonExternal: false, - dismissButtonLabel: { - default: t("templates.site_abandonment_survey_question_2_dismiss_button_label"), - }, - }, - { - id: createId(), + dismissButtonLabel: t("templates.site_abandonment_survey_question_2_dismiss_button_label"), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.site_abandonment_survey_question_3_headline") }, - subheader: { default: t("templates.site_abandonment_survey_question_3_subheader") }, + headline: t("templates.site_abandonment_survey_question_3_headline"), + subheader: t("templates.site_abandonment_survey_question_3_subheader"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_3_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_3_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_3_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_3_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_3_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.site_abandonment_survey_question_3_choice_6") }, - }, + t("templates.site_abandonment_survey_question_3_choice_1"), + t("templates.site_abandonment_survey_question_3_choice_2"), + t("templates.site_abandonment_survey_question_3_choice_3"), + t("templates.site_abandonment_survey_question_3_choice_4"), + t("templates.site_abandonment_survey_question_3_choice_5"), + t("templates.site_abandonment_survey_question_3_choice_6"), ], - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.site_abandonment_survey_question_4_headline"), - }, + containsOther: true, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.site_abandonment_survey_question_4_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: t("templates.site_abandonment_survey_question_5_headline") }, + t, + }), + buildRatingQuestion({ + headline: t("templates.site_abandonment_survey_question_5_headline"), required: true, scale: "number", range: 5, - lowerLabel: { default: t("templates.site_abandonment_survey_question_5_lower_label") }, - upperLabel: { default: t("templates.site_abandonment_survey_question_5_upper_label") }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + lowerLabel: t("templates.site_abandonment_survey_question_5_lower_label"), + upperLabel: t("templates.site_abandonment_survey_question_5_upper_label"), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - headline: { - default: t("templates.site_abandonment_survey_question_6_headline"), - }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - subheader: { default: t("templates.site_abandonment_survey_question_6_subheader") }, + headline: t("templates.site_abandonment_survey_question_6_headline"), + subheader: t("templates.site_abandonment_survey_question_6_subheader"), required: true, choices: [ - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_6_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_6_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_6_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_6_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.site_abandonment_survey_question_6_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.site_abandonment_survey_question_6_choice_6") }, - }, + t("templates.site_abandonment_survey_question_6_choice_1"), + t("templates.site_abandonment_survey_question_6_choice_2"), + t("templates.site_abandonment_survey_question_6_choice_3"), + t("templates.site_abandonment_survey_question_6_choice_4"), + t("templates.site_abandonment_survey_question_6_choice_5"), ], - }, - { + t, + }), + buildConsentQuestion({ id: reusableQuestionIds[1], - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - ], - type: TSurveyQuestionTypeEnum.Consent, - headline: { default: t("templates.site_abandonment_survey_question_7_headline") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, + logic: [createJumpLogic(reusableQuestionIds[1], reusableQuestionIds[2], "isSkipped")], + headline: t("templates.site_abandonment_survey_question_7_headline"), required: false, - label: { default: t("templates.site_abandonment_survey_question_7_label") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.site_abandonment_survey_question_8_headline") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, + label: t("templates.site_abandonment_survey_question_7_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.site_abandonment_survey_question_8_headline"), required: true, inputType: "email", longAnswer: false, - placeholder: { default: "example@email.com" }, - }, - { + placeholder: "example@email.com", + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.site_abandonment_survey_question_9_headline") }, - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, + headline: t("templates.site_abandonment_survey_question_9_headline"), required: false, inputType: "text", - }, + t, + }), ], }, - }; + t + ); }; const productMarketFitSuperhuman = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.product_market_fit_superhuman"), - role: "productManager", - industries: ["saas"], - channels: ["app", "link"], - description: t("templates.product_market_fit_superhuman_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.product_market_fit_superhuman"), + role: "productManager", + industries: ["saas"], + channels: ["app", "link"], + description: t("templates.product_market_fit_superhuman_description"), + endings: localSurvey.endings, questions: [ - { + buildCTAQuestion({ id: reusableQuestionIds[0], - html: { - default: t("templates.product_market_fit_superhuman_question_1_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.product_market_fit_superhuman_question_1_headline") }, + html: t("templates.product_market_fit_superhuman_question_1_html"), + logic: [createJumpLogic(reusableQuestionIds[0], localSurvey.endings[0].id, "isSkipped")], + headline: t("templates.product_market_fit_superhuman_question_1_headline"), required: false, - buttonLabel: { - default: t("templates.product_market_fit_superhuman_question_1_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, + buttonLabel: t("templates.product_market_fit_superhuman_question_1_button_label"), buttonExternal: false, - dismissButtonLabel: { - default: t("templates.product_market_fit_superhuman_question_1_dismiss_button_label"), - }, - }, - { - id: createId(), + dismissButtonLabel: t("templates.product_market_fit_superhuman_question_1_dismiss_button_label"), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.product_market_fit_superhuman_question_2_headline") }, - subheader: { default: t("templates.product_market_fit_superhuman_question_2_subheader") }, + headline: t("templates.product_market_fit_superhuman_question_2_headline"), + subheader: t("templates.product_market_fit_superhuman_question_2_subheader"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_2_choice_3") }, - }, + t("templates.product_market_fit_superhuman_question_2_choice_1"), + t("templates.product_market_fit_superhuman_question_2_choice_2"), + t("templates.product_market_fit_superhuman_question_2_choice_3"), ], - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.product_market_fit_superhuman_question_3_headline") }, - subheader: { default: t("templates.product_market_fit_superhuman_question_3_subheader") }, + headline: "templates.product_market_fit_superhuman_question_3_headline", + subheader: t("templates.product_market_fit_superhuman_question_3_subheader"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_3_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_3_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_3_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_3_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_superhuman_question_3_choice_5") }, - }, + t("templates.product_market_fit_superhuman_question_3_choice_1"), + t("templates.product_market_fit_superhuman_question_3_choice_2"), + t("templates.product_market_fit_superhuman_question_3_choice_3"), + t("templates.product_market_fit_superhuman_question_3_choice_4"), + t("templates.product_market_fit_superhuman_question_3_choice_5"), ], - }, - { + t, + }), + buildOpenTextQuestion({ id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.product_market_fit_superhuman_question_4_headline") }, + headline: t("templates.product_market_fit_superhuman_question_4_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, inputType: "text", - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.product_market_fit_superhuman_question_5_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.product_market_fit_superhuman_question_5_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, inputType: "text", - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.product_market_fit_superhuman_question_6_headline") }, - subheader: { default: t("templates.product_market_fit_superhuman_question_6_subheader") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.product_market_fit_superhuman_question_6_headline"), + subheader: t("templates.product_market_fit_superhuman_question_6_subheader"), required: true, - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, inputType: "text", - }, + t, + }), ], }, - }; + t + ); }; const onboardingSegmentation = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.onboarding_segmentation"), - role: "productManager", - industries: ["saas"], - channels: ["app", "link"], - description: t("templates.onboarding_segmentation_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.onboarding_segmentation"), + role: "productManager", + industries: ["saas"], + channels: ["app", "link"], + description: t("templates.onboarding_segmentation_description"), questions: [ - { - id: createId(), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.onboarding_segmentation_question_1_headline") }, - subheader: { default: t("templates.onboarding_segmentation_question_1_subheader") }, + headline: t("templates.onboarding_segmentation_question_1_headline"), + subheader: t("templates.onboarding_segmentation_question_1_subheader"), required: true, shuffleOption: "none", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, choices: [ - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_1_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_1_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_1_choice_5") }, - }, + t("templates.onboarding_segmentation_question_1_choice_1"), + t("templates.onboarding_segmentation_question_1_choice_2"), + t("templates.onboarding_segmentation_question_1_choice_3"), + t("templates.onboarding_segmentation_question_1_choice_4"), + t("templates.onboarding_segmentation_question_1_choice_5"), ], - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.onboarding_segmentation_question_2_headline") }, - subheader: { default: t("templates.onboarding_segmentation_question_2_subheader") }, + headline: t("templates.onboarding_segmentation_question_2_headline"), + subheader: t("templates.onboarding_segmentation_question_2_subheader"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_2_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_2_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_2_choice_5") }, - }, + t("templates.onboarding_segmentation_question_2_choice_1"), + t("templates.onboarding_segmentation_question_2_choice_2"), + t("templates.onboarding_segmentation_question_2_choice_3"), + t("templates.onboarding_segmentation_question_2_choice_4"), + t("templates.onboarding_segmentation_question_2_choice_5"), ], - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.onboarding_segmentation_question_3_headline") }, - subheader: { default: t("templates.onboarding_segmentation_question_3_subheader") }, + headline: t("templates.onboarding_segmentation_question_3_headline"), + subheader: t("templates.onboarding_segmentation_question_3_subheader"), required: true, - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, + buttonLabel: t("templates.finish"), shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_3_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_3_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_3_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_3_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.onboarding_segmentation_question_3_choice_5") }, - }, + t("templates.onboarding_segmentation_question_3_choice_1"), + t("templates.onboarding_segmentation_question_3_choice_2"), + t("templates.onboarding_segmentation_question_3_choice_3"), + t("templates.onboarding_segmentation_question_3_choice_4"), + t("templates.onboarding_segmentation_question_3_choice_5"), ], - }, + t, + }), ], }, - }; + t + ); }; const churnSurvey = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId(), createId(), createId()]; const reusableOptionIds = [createId(), createId(), createId(), createId(), createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.churn_survey"), - role: "sales", - industries: ["saas", "eCommerce", "other"], - channels: ["app", "link"], - description: t("templates.churn_survey_description"), - preset: { - ...localSurvey, - name: "Churn Survey", + return buildSurvey( + { + name: t("templates.churn_survey"), + role: "sales", + industries: ["saas", "eCommerce", "other"], + channels: ["app", "link"], + description: t("templates.churn_survey_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[0], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[1], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[2], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[3], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[4], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[4], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[0], reusableQuestionIds[1]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[2], reusableQuestionIds[3]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[3], reusableQuestionIds[4]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[4], localSurvey.endings[0].id), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.churn_survey_question_1_choice_1") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.churn_survey_question_1_choice_2") }, - }, - { - id: reusableOptionIds[2], - label: { default: t("templates.churn_survey_question_1_choice_3") }, - }, - { - id: reusableOptionIds[3], - label: { default: t("templates.churn_survey_question_1_choice_4") }, - }, - { - id: reusableOptionIds[4], - label: { default: t("templates.churn_survey_question_1_choice_5") }, - }, + t("templates.churn_survey_question_1_choice_1"), + t("templates.churn_survey_question_1_choice_2"), + t("templates.churn_survey_question_1_choice_3"), + t("templates.churn_survey_question_1_choice_4"), + t("templates.churn_survey_question_1_choice_5"), ], - headline: { default: t("templates.churn_survey_question_1_headline") }, + choiceIds: [ + reusableOptionIds[0], + reusableOptionIds[1], + reusableOptionIds[2], + reusableOptionIds[3], + reusableOptionIds[4], + ], + headline: t("templates.churn_survey_question_1_headline"), required: true, - subheader: { default: t("templates.churn_survey_question_1_subheader") }, - }, - { + subheader: t("templates.churn_survey_question_1_subheader"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.churn_survey_question_2_headline") }, - backButtonLabel: { default: t("templates.back") }, + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.churn_survey_question_2_headline"), required: true, - buttonLabel: { default: t("templates.churn_survey_question_2_button_label") }, + buttonLabel: t("templates.churn_survey_question_2_button_label"), inputType: "text", - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[2], - html: { - default: t("templates.churn_survey_question_3_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "isClicked", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.churn_survey_question_3_headline") }, + html: t("templates.churn_survey_question_3_html"), + logic: [createJumpLogic(reusableQuestionIds[2], localSurvey.endings[0].id, "isClicked")], + headline: t("templates.churn_survey_question_3_headline"), required: true, buttonUrl: "https://formbricks.com", - buttonLabel: { default: t("templates.churn_survey_question_3_button_label") }, + buttonLabel: t("templates.churn_survey_question_3_button_label"), buttonExternal: true, - backButtonLabel: { default: t("templates.back") }, - dismissButtonLabel: { default: t("templates.churn_survey_question_3_dismiss_button_label") }, - }, - { + dismissButtonLabel: t("templates.churn_survey_question_3_dismiss_button_label"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[3], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.churn_survey_question_4_headline") }, + logic: [createJumpLogic(reusableQuestionIds[3], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.churn_survey_question_4_headline"), required: true, inputType: "text", - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[4], - html: { - default: t("templates.churn_survey_question_5_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isClicked", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.churn_survey_question_5_headline") }, + html: t("templates.churn_survey_question_5_html"), + logic: [createJumpLogic(reusableQuestionIds[4], localSurvey.endings[0].id, "isClicked")], + headline: t("templates.churn_survey_question_5_headline"), required: true, buttonUrl: "mailto:ceo@company.com", - buttonLabel: { default: t("templates.churn_survey_question_5_button_label") }, + buttonLabel: t("templates.churn_survey_question_5_button_label"), buttonExternal: true, - dismissButtonLabel: { default: t("templates.churn_survey_question_5_dismiss_button_label") }, - backButtonLabel: { default: t("templates.back") }, - }, + dismissButtonLabel: t("templates.churn_survey_question_5_dismiss_button_label"), + t, + }), ], }, - }; + t + ); }; const earnedAdvocacyScore = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId(), createId()]; const reusableOptionIds = [createId(), createId(), createId(), createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.earned_advocacy_score_name"), - role: "customerSuccess", - industries: ["saas", "eCommerce", "other"], - channels: ["app", "link"], - description: t("templates.earned_advocacy_score_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.earned_advocacy_score_name"), + role: "customerSuccess", + industries: ["saas", "eCommerce", "other"], + channels: ["app", "link"], + description: t("templates.earned_advocacy_score_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[2]), ], shuffleOption: "none", choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.earned_advocacy_score_question_1_choice_1") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.earned_advocacy_score_question_1_choice_2") }, - }, + t("templates.earned_advocacy_score_question_1_choice_1"), + t("templates.earned_advocacy_score_question_1_choice_2"), ], - headline: { default: t("templates.earned_advocacy_score_question_1_headline") }, + choiceIds: [reusableOptionIds[0], reusableOptionIds[1]], + headline: t("templates.earned_advocacy_score_question_1_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - ], - headline: { default: t("templates.earned_advocacy_score_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], reusableQuestionIds[3], "isSubmitted")], + headline: t("templates.earned_advocacy_score_question_2_headline"), required: true, - placeholder: { default: t("templates.earned_advocacy_score_question_2_placeholder") }, + placeholder: t("templates.earned_advocacy_score_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.earned_advocacy_score_question_3_headline") }, + headline: t("templates.earned_advocacy_score_question_3_headline"), required: true, - placeholder: { default: t("templates.earned_advocacy_score_question_3_placeholder") }, + placeholder: t("templates.earned_advocacy_score_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[3], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[3], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[3], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[3], reusableOptionIds[3], localSurvey.endings[0].id), ], shuffleOption: "none", choices: [ - { - id: reusableOptionIds[2], - label: { default: t("templates.earned_advocacy_score_question_4_choice_1") }, - }, - { - id: reusableOptionIds[3], - label: { default: t("templates.earned_advocacy_score_question_4_choice_2") }, - }, + t("templates.earned_advocacy_score_question_4_choice_1"), + t("templates.earned_advocacy_score_question_4_choice_2"), ], - headline: { default: t("templates.earned_advocacy_score_question_4_headline") }, + choiceIds: [reusableOptionIds[2], reusableOptionIds[3]], + headline: t("templates.earned_advocacy_score_question_4_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.earned_advocacy_score_question_5_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.earned_advocacy_score_question_5_headline"), required: true, - placeholder: { default: t("templates.earned_advocacy_score_question_5_placeholder") }, + placeholder: t("templates.earned_advocacy_score_question_5_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const improveTrialConversion = (t: TFnType): TTemplate => { @@ -1297,432 +532,119 @@ const improveTrialConversion = (t: TFnType): TTemplate => { createId(), ]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.improve_trial_conversion_name"), - role: "sales", - industries: ["saas"], - channels: ["link", "app"], - description: t("templates.improve_trial_conversion_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.improve_trial_conversion_name"), + role: "sales", + industries: ["saas"], + channels: ["link", "app"], + description: t("templates.improve_trial_conversion_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[0], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[1], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[2], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[3], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[4], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[4], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[0], reusableQuestionIds[1]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[2], reusableQuestionIds[3]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[3], reusableQuestionIds[4]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[4], localSurvey.endings[0].id), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.improve_trial_conversion_question_1_choice_1") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.improve_trial_conversion_question_1_choice_2") }, - }, - { - id: reusableOptionIds[2], - label: { default: t("templates.improve_trial_conversion_question_1_choice_3") }, - }, - { - id: reusableOptionIds[3], - label: { default: t("templates.improve_trial_conversion_question_1_choice_4") }, - }, - { - id: reusableOptionIds[4], - label: { default: t("templates.improve_trial_conversion_question_1_choice_5") }, - }, + t("templates.improve_trial_conversion_question_1_choice_1"), + t("templates.improve_trial_conversion_question_1_choice_2"), + t("templates.improve_trial_conversion_question_1_choice_3"), + t("templates.improve_trial_conversion_question_1_choice_4"), + t("templates.improve_trial_conversion_question_1_choice_5"), ], - headline: { default: t("templates.improve_trial_conversion_question_1_headline") }, + choiceIds: [ + reusableOptionIds[0], + reusableOptionIds[1], + reusableOptionIds[2], + reusableOptionIds[3], + reusableOptionIds[4], + ], + headline: t("templates.improve_trial_conversion_question_1_headline"), required: true, - subheader: { default: t("templates.improve_trial_conversion_question_1_subheader") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + subheader: t("templates.improve_trial_conversion_question_1_subheader"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, - ], - headline: { default: t("templates.improve_trial_conversion_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], reusableQuestionIds[5], "isSubmitted")], + headline: t("templates.improve_trial_conversion_question_2_headline"), required: true, - buttonLabel: { default: t("templates.improve_trial_conversion_question_2_button_label") }, + buttonLabel: t("templates.improve_trial_conversion_question_2_button_label"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, - ], - headline: { default: t("templates.improve_trial_conversion_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[2], reusableQuestionIds[5], "isSubmitted")], + headline: t("templates.improve_trial_conversion_question_2_headline"), required: true, - buttonLabel: { default: t("templates.improve_trial_conversion_question_2_button_label") }, + buttonLabel: t("templates.improve_trial_conversion_question_2_button_label"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[3], - html: { - default: t("templates.improve_trial_conversion_question_4_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[3], - type: "question", - }, - operator: "isClicked", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.improve_trial_conversion_question_4_headline") }, + html: t("templates.improve_trial_conversion_question_4_html"), + logic: [createJumpLogic(reusableQuestionIds[3], localSurvey.endings[0].id, "isClicked")], + headline: t("templates.improve_trial_conversion_question_4_headline"), required: true, buttonUrl: "https://formbricks.com/github", - buttonLabel: { default: t("templates.improve_trial_conversion_question_4_button_label") }, + buttonLabel: t("templates.improve_trial_conversion_question_4_button_label"), buttonExternal: true, - dismissButtonLabel: { - default: t("templates.improve_trial_conversion_question_4_dismiss_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, - }, - { + dismissButtonLabel: t("templates.improve_trial_conversion_question_4_dismiss_button_label"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, - ], - headline: { default: t("templates.improve_trial_conversion_question_5_headline") }, + logic: [createJumpLogic(reusableQuestionIds[4], reusableQuestionIds[5], "isSubmitted")], + headline: t("templates.improve_trial_conversion_question_5_headline"), required: true, - subheader: { default: t("templates.improve_trial_conversion_question_5_subheader") }, - buttonLabel: { default: t("templates.improve_trial_conversion_question_5_button_label") }, + subheader: t("templates.improve_trial_conversion_question_5_subheader"), + buttonLabel: t("templates.improve_trial_conversion_question_5_button_label"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[5], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[5], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[5], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createJumpLogic(reusableQuestionIds[5], localSurvey.endings[0].id, "isSubmitted"), + createJumpLogic(reusableQuestionIds[5], localSurvey.endings[0].id, "isSkipped"), ], - headline: { default: t("templates.improve_trial_conversion_question_6_headline") }, + headline: t("templates.improve_trial_conversion_question_6_headline"), required: false, - subheader: { default: t("templates.improve_trial_conversion_question_6_subheader") }, + subheader: t("templates.improve_trial_conversion_question_6_subheader"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const reviewPrompt = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.review_prompt_name"), - role: "marketing", - industries: ["saas", "eCommerce", "other"], - channels: ["link", "app"], - description: t("templates.review_prompt_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.review_prompt_name"), + role: "marketing", + industries: ["saas", "eCommerce", "other"], + channels: ["link", "app"], + description: t("templates.review_prompt_description"), + endings: localSurvey.endings, questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -1755,1206 +677,596 @@ const reviewPrompt = (t: TFnType): TTemplate => { ], range: 5, scale: "star", - headline: { default: t("templates.review_prompt_question_1_headline") }, + headline: t("templates.review_prompt_question_1_headline"), required: true, - lowerLabel: { default: t("templates.review_prompt_question_1_lower_label") }, - upperLabel: { default: t("templates.review_prompt_question_1_upper_label") }, + lowerLabel: t("templates.review_prompt_question_1_lower_label"), + upperLabel: t("templates.review_prompt_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[1], - html: { default: t("templates.review_prompt_question_2_html") }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isClicked", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.review_prompt_question_2_headline") }, + html: t("templates.review_prompt_question_2_html"), + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isClicked")], + headline: t("templates.review_prompt_question_2_headline"), required: true, buttonUrl: "https://formbricks.com/github", - buttonLabel: { default: t("templates.review_prompt_question_2_button_label") }, + buttonLabel: t("templates.review_prompt_question_2_button_label"), buttonExternal: true, - backButtonLabel: { default: t("templates.back") }, - }, - { + backButtonLabel: t("templates.back"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.review_prompt_question_3_headline") }, + headline: t("templates.review_prompt_question_3_headline"), required: true, - subheader: { default: t("templates.review_prompt_question_3_subheader") }, - buttonLabel: { default: t("templates.review_prompt_question_3_button_label") }, - placeholder: { default: t("templates.review_prompt_question_3_placeholder") }, + subheader: t("templates.review_prompt_question_3_subheader"), + buttonLabel: t("templates.review_prompt_question_3_button_label"), + placeholder: t("templates.review_prompt_question_3_placeholder"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const interviewPrompt = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.interview_prompt_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.interview_prompt_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.interview_prompt_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.interview_prompt_description"), questions: [ - { + buildCTAQuestion({ id: createId(), - type: TSurveyQuestionTypeEnum.CTA, - headline: { default: t("templates.interview_prompt_question_1_headline") }, - html: { default: t("templates.interview_prompt_question_1_html") }, - buttonLabel: { default: t("templates.interview_prompt_question_1_button_label") }, + headline: t("templates.interview_prompt_question_1_headline"), + html: t("templates.interview_prompt_question_1_html"), + buttonLabel: t("templates.interview_prompt_question_1_button_label"), buttonUrl: "https://cal.com/johannes", buttonExternal: true, required: false, - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const improveActivationRate = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId(), createId(), createId(), createId()]; const reusableOptionIds = [createId(), createId(), createId(), createId(), createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.improve_activation_rate_name"), - role: "productManager", - industries: ["saas"], - channels: ["link"], - description: t("templates.improve_activation_rate_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.improve_activation_rate_name"), + role: "productManager", + industries: ["saas"], + channels: ["link"], + description: t("templates.improve_activation_rate_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[2], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[3], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[4], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[4], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[2], reusableQuestionIds[3]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[3], reusableQuestionIds[4]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[4], reusableQuestionIds[5]), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.improve_activation_rate_question_1_choice_1") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.improve_activation_rate_question_1_choice_2") }, - }, - { - id: reusableOptionIds[2], - label: { default: t("templates.improve_activation_rate_question_1_choice_3") }, - }, - { - id: reusableOptionIds[3], - label: { default: t("templates.improve_activation_rate_question_1_choice_4") }, - }, - { - id: reusableOptionIds[4], - label: { default: t("templates.improve_activation_rate_question_1_choice_5") }, - }, + t("templates.improve_activation_rate_question_1_choice_1"), + t("templates.improve_activation_rate_question_1_choice_2"), + t("templates.improve_activation_rate_question_1_choice_3"), + t("templates.improve_activation_rate_question_1_choice_4"), + t("templates.improve_activation_rate_question_1_choice_5"), ], - headline: { - default: t("templates.improve_activation_rate_question_1_headline"), - }, + choiceIds: [ + reusableOptionIds[0], + reusableOptionIds[1], + reusableOptionIds[2], + reusableOptionIds[3], + reusableOptionIds[4], + ], + headline: t("templates.improve_activation_rate_question_1_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.improve_activation_rate_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.improve_activation_rate_question_2_headline"), required: true, - placeholder: { default: t("templates.improve_activation_rate_question_2_placeholder") }, + placeholder: t("templates.improve_activation_rate_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.improve_activation_rate_question_3_headline") }, + logic: [createJumpLogic(reusableQuestionIds[2], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.improve_activation_rate_question_3_headline"), required: true, - placeholder: { default: t("templates.improve_activation_rate_question_3_placeholder") }, + placeholder: t("templates.improve_activation_rate_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[3], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.improve_activation_rate_question_4_headline") }, + logic: [createJumpLogic(reusableQuestionIds[3], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.improve_activation_rate_question_4_headline"), required: true, - placeholder: { default: t("templates.improve_activation_rate_question_4_placeholder") }, + placeholder: t("templates.improve_activation_rate_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.improve_activation_rate_question_5_headline") }, + logic: [createJumpLogic(reusableQuestionIds[4], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.improve_activation_rate_question_5_headline"), required: true, - placeholder: { default: t("templates.improve_activation_rate_question_5_placeholder") }, + placeholder: t("templates.improve_activation_rate_question_5_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[5], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [], - headline: { default: t("templates.improve_activation_rate_question_6_headline") }, + headline: t("templates.improve_activation_rate_question_6_headline"), required: false, - subheader: { default: t("templates.improve_activation_rate_question_6_subheader") }, - placeholder: { default: t("templates.improve_activation_rate_question_6_placeholder") }, + subheader: t("templates.improve_activation_rate_question_6_subheader"), + placeholder: t("templates.improve_activation_rate_question_6_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const employeeSatisfaction = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.employee_satisfaction_name"), - role: "peopleManager", - industries: ["saas", "eCommerce", "other"], - channels: ["app", "link"], - description: t("templates.employee_satisfaction_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.employee_satisfaction_name"), + role: "peopleManager", + industries: ["saas", "eCommerce", "other"], + channels: ["app", "link"], + description: t("templates.employee_satisfaction_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + buildRatingQuestion({ range: 5, scale: "star", - headline: { default: t("templates.employee_satisfaction_question_1_headline") }, + headline: t("templates.employee_satisfaction_question_1_headline"), required: true, - lowerLabel: { default: t("templates.employee_satisfaction_question_1_lower_label") }, - upperLabel: { default: t("templates.employee_satisfaction_question_1_upper_label") }, + lowerLabel: t("templates.employee_satisfaction_question_1_lower_label"), + upperLabel: t("templates.employee_satisfaction_question_1_upper_label"), isColorCodingEnabled: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_2_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_2_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_2_choice_5") }, - }, + t("templates.employee_satisfaction_question_2_choice_1"), + t("templates.employee_satisfaction_question_2_choice_2"), + t("templates.employee_satisfaction_question_2_choice_3"), + t("templates.employee_satisfaction_question_2_choice_4"), + t("templates.employee_satisfaction_question_2_choice_5"), ], - headline: { default: t("templates.employee_satisfaction_question_2_headline") }, + headline: t("templates.employee_satisfaction_question_2_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.employee_satisfaction_question_3_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.employee_satisfaction_question_3_headline"), required: false, - placeholder: { default: t("templates.employee_satisfaction_question_3_placeholder") }, + placeholder: t("templates.employee_satisfaction_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "number", - headline: { default: t("templates.employee_satisfaction_question_5_headline") }, + headline: t("templates.employee_satisfaction_question_5_headline"), required: true, - lowerLabel: { default: t("templates.employee_satisfaction_question_5_lower_label") }, - upperLabel: { default: t("templates.employee_satisfaction_question_5_upper_label") }, + lowerLabel: t("templates.employee_satisfaction_question_5_lower_label"), + upperLabel: t("templates.employee_satisfaction_question_5_upper_label"), isColorCodingEnabled: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.employee_satisfaction_question_6_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.employee_satisfaction_question_6_headline"), required: false, - placeholder: { default: t("templates.employee_satisfaction_question_6_placeholder") }, + placeholder: t("templates.employee_satisfaction_question_6_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_7_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_7_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_7_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_7_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.employee_satisfaction_question_7_choice_5") }, - }, + t("templates.employee_satisfaction_question_7_choice_1"), + t("templates.employee_satisfaction_question_7_choice_2"), + t("templates.employee_satisfaction_question_7_choice_3"), + t("templates.employee_satisfaction_question_7_choice_4"), + t("templates.employee_satisfaction_question_7_choice_5"), ], - headline: { default: t("templates.employee_satisfaction_question_7_headline") }, + headline: t("templates.employee_satisfaction_question_7_headline"), required: true, - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const uncoverStrengthsAndWeaknesses = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.uncover_strengths_and_weaknesses_name"), - role: "productManager", - industries: ["saas", "other"], - channels: ["app", "link"], - description: t("templates.uncover_strengths_and_weaknesses_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.uncover_strengths_and_weaknesses_name"), + role: "productManager", + industries: ["saas", "other"], + channels: ["app", "link"], + description: t("templates.uncover_strengths_and_weaknesses_description"), questions: [ - { + buildMultipleChoiceQuestion({ + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + shuffleOption: "none", + choices: [ + t("templates.uncover_strengths_and_weaknesses_question_1_choice_1"), + t("templates.uncover_strengths_and_weaknesses_question_1_choice_2"), + t("templates.uncover_strengths_and_weaknesses_question_1_choice_3"), + t("templates.uncover_strengths_and_weaknesses_question_1_choice_4"), + t("templates.uncover_strengths_and_weaknesses_question_1_choice_5"), + ], + headline: t("templates.uncover_strengths_and_weaknesses_question_1_headline"), + required: true, + containsOther: true, + t, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_1_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_1_choice_4") }, - }, - { - id: "other", - label: { default: t("templates.uncover_strengths_and_weaknesses_question_1_choice_5") }, - }, + t("templates.uncover_strengths_and_weaknesses_question_2_choice_1"), + t("templates.uncover_strengths_and_weaknesses_question_2_choice_2"), + t("templates.uncover_strengths_and_weaknesses_question_2_choice_3"), + t("templates.uncover_strengths_and_weaknesses_question_2_choice_4"), ], - headline: { default: t("templates.uncover_strengths_and_weaknesses_question_1_headline") }, + headline: t("templates.uncover_strengths_and_weaknesses_question_2_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - shuffleOption: "none", - choices: [ - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.uncover_strengths_and_weaknesses_question_2_choice_3") }, - }, - { - id: "other", - label: { default: t("templates.uncover_strengths_and_weaknesses_question_2_choice_4") }, - }, - ], - headline: { default: t("templates.uncover_strengths_and_weaknesses_question_2_headline") }, - required: true, - subheader: { default: t("templates.uncover_strengths_and_weaknesses_question_2_subheader") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.uncover_strengths_and_weaknesses_question_3_headline") }, + subheader: t("templates.uncover_strengths_and_weaknesses_question_2_subheader"), + containsOther: true, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.uncover_strengths_and_weaknesses_question_3_headline"), required: false, - subheader: { default: t("templates.uncover_strengths_and_weaknesses_question_3_subheader") }, + subheader: t("templates.uncover_strengths_and_weaknesses_question_3_subheader"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const productMarketFitShort = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.product_market_fit_short_name"), - role: "productManager", - industries: ["saas"], - channels: ["app", "link"], - description: t("templates.product_market_fit_short_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.product_market_fit_short_name"), + role: "productManager", + industries: ["saas"], + channels: ["app", "link"], + description: t("templates.product_market_fit_short_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.product_market_fit_short_question_1_headline") }, - subheader: { default: t("templates.product_market_fit_short_question_1_subheader") }, + headline: t("templates.product_market_fit_short_question_1_headline"), + subheader: t("templates.product_market_fit_short_question_1_subheader"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.product_market_fit_short_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_short_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.product_market_fit_short_question_1_choice_3") }, - }, + t("templates.product_market_fit_short_question_1_choice_1"), + t("templates.product_market_fit_short_question_1_choice_2"), + t("templates.product_market_fit_short_question_1_choice_3"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.product_market_fit_short_question_2_headline") }, - subheader: { default: t("templates.product_market_fit_short_question_2_subheader") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.product_market_fit_short_question_2_headline"), + subheader: t("templates.product_market_fit_short_question_2_subheader"), required: true, inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const marketAttribution = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.market_attribution_name"), - role: "marketing", - industries: ["saas", "eCommerce"], - channels: ["website", "app", "link"], - description: t("templates.market_attribution_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.market_attribution_name"), + role: "marketing", + industries: ["saas", "eCommerce"], + channels: ["website", "app", "link"], + description: t("templates.market_attribution_description"), questions: [ - { - id: createId(), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.market_attribution_question_1_headline") }, - subheader: { default: t("templates.market_attribution_question_1_subheader") }, + headline: t("templates.market_attribution_question_1_headline"), + subheader: t("templates.market_attribution_question_1_subheader"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.market_attribution_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.market_attribution_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.market_attribution_question_1_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.market_attribution_question_1_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.market_attribution_question_1_choice_5") }, - }, + t("templates.market_attribution_question_1_choice_1"), + t("templates.market_attribution_question_1_choice_2"), + t("templates.market_attribution_question_1_choice_3"), + t("templates.market_attribution_question_1_choice_4"), + t("templates.market_attribution_question_1_choice_5"), ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const changingSubscriptionExperience = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.changing_subscription_experience_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.changing_subscription_experience_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.changing_subscription_experience_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.changing_subscription_experience_description"), questions: [ - { - id: createId(), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.changing_subscription_experience_question_1_headline") }, + headline: t("templates.changing_subscription_experience_question_1_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_1_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_1_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_1_choice_5") }, - }, + t("templates.changing_subscription_experience_question_1_choice_1"), + t("templates.changing_subscription_experience_question_1_choice_2"), + t("templates.changing_subscription_experience_question_1_choice_3"), + t("templates.changing_subscription_experience_question_1_choice_4"), + t("templates.changing_subscription_experience_question_1_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + buttonLabel: t("templates.next"), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.changing_subscription_experience_question_2_headline") }, + headline: t("templates.changing_subscription_experience_question_2_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.changing_subscription_experience_question_2_choice_3") }, - }, + t("templates.changing_subscription_experience_question_2_choice_1"), + t("templates.changing_subscription_experience_question_2_choice_2"), + t("templates.changing_subscription_experience_question_2_choice_3"), ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const identifyCustomerGoals = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.identify_customer_goals_name"), - role: "productManager", - industries: ["saas", "other"], - channels: ["app", "website"], - description: t("templates.identify_customer_goals_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.identify_customer_goals_name"), + role: "productManager", + industries: ["saas", "other"], + channels: ["app", "website"], + description: t("templates.identify_customer_goals_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: "What's your primary goal for using $[projectName]?" }, + headline: "What's your primary goal for using $[projectName]?", required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: "Understand my user base deeply" }, - }, - { - id: createId(), - label: { default: "Identify upselling opportunities" }, - }, - { - id: createId(), - label: { default: "Build the best possible product" }, - }, - { - id: createId(), - label: { default: "Rule the world to make everyone breakfast brussels sprouts." }, - }, + "Understand my user base deeply", + "Identify upselling opportunities", + "Build the best possible product", + "Rule the world to make everyone breakfast brussels sprouts.", ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const featureChaser = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.feature_chaser_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.feature_chaser_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.feature_chaser_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.feature_chaser_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + buildRatingQuestion({ range: 5, scale: "number", - headline: { default: t("templates.feature_chaser_question_1_headline") }, + headline: t("templates.feature_chaser_question_1_headline"), required: true, - lowerLabel: { default: t("templates.feature_chaser_question_1_lower_label") }, - upperLabel: { default: t("templates.feature_chaser_question_1_upper_label") }, + lowerLabel: t("templates.feature_chaser_question_1_lower_label"), + upperLabel: t("templates.feature_chaser_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", choices: [ - { id: createId(), label: { default: t("templates.feature_chaser_question_2_choice_1") } }, - { id: createId(), label: { default: t("templates.feature_chaser_question_2_choice_2") } }, - { id: createId(), label: { default: t("templates.feature_chaser_question_2_choice_3") } }, - { id: createId(), label: { default: t("templates.feature_chaser_question_2_choice_4") } }, + t("templates.feature_chaser_question_2_choice_1"), + t("templates.feature_chaser_question_2_choice_2"), + t("templates.feature_chaser_question_2_choice_3"), + t("templates.feature_chaser_question_2_choice_4"), ], - headline: { default: t("templates.feature_chaser_question_2_headline") }, + headline: t("templates.feature_chaser_question_2_headline"), required: true, - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const fakeDoorFollowUp = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.fake_door_follow_up_name"), - role: "productManager", - industries: ["saas", "eCommerce"], - channels: ["app", "website"], - description: t("templates.fake_door_follow_up_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.fake_door_follow_up_name"), + role: "productManager", + industries: ["saas", "eCommerce"], + channels: ["app", "website"], + description: t("templates.fake_door_follow_up_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: t("templates.fake_door_follow_up_question_1_headline") }, + buildRatingQuestion({ + headline: t("templates.fake_door_follow_up_question_1_headline"), required: true, - lowerLabel: { default: t("templates.fake_door_follow_up_question_1_lower_label") }, - upperLabel: { default: t("templates.fake_door_follow_up_question_1_upper_label") }, + lowerLabel: t("templates.fake_door_follow_up_question_1_lower_label"), + upperLabel: t("templates.fake_door_follow_up_question_1_upper_label"), range: 5, scale: "number", isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + buttonLabel: t("templates.next"), + t, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - headline: { default: t("templates.fake_door_follow_up_question_2_headline") }, + headline: t("templates.fake_door_follow_up_question_2_headline"), required: false, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.fake_door_follow_up_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.fake_door_follow_up_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.fake_door_follow_up_question_2_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.fake_door_follow_up_question_2_choice_4") }, - }, + t("templates.fake_door_follow_up_question_2_choice_1"), + t("templates.fake_door_follow_up_question_2_choice_2"), + t("templates.fake_door_follow_up_question_2_choice_3"), + t("templates.fake_door_follow_up_question_2_choice_4"), ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const feedbackBox = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId(), createId()]; const reusableOptionIds = [createId(), createId()]; const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.feedback_box_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.feedback_box_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.feedback_box_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.feedback_box_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[0], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[1], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[0], reusableQuestionIds[1]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[3]), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.feedback_box_question_1_choice_1") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.feedback_box_question_1_choice_2") }, - }, + t("templates.feedback_box_question_1_choice_1"), + t("templates.feedback_box_question_1_choice_2"), ], - headline: { default: t("templates.feedback_box_question_1_headline") }, + headline: t("templates.feedback_box_question_1_headline"), required: true, - subheader: { default: t("templates.feedback_box_question_1_subheader") }, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + subheader: t("templates.feedback_box_question_1_subheader"), + buttonLabel: t("templates.next"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - ], - headline: { default: t("templates.feedback_box_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], reusableQuestionIds[2], "isSubmitted")], + headline: t("templates.feedback_box_question_2_headline"), required: true, - subheader: { default: t("templates.feedback_box_question_2_subheader") }, + subheader: t("templates.feedback_box_question_2_subheader"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[2], - html: { - default: t("templates.feedback_box_question_3_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, + html: t("templates.feedback_box_question_3_html"), logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "isClicked", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createJumpLogic(reusableQuestionIds[2], localSurvey.endings[0].id, "isClicked"), + createJumpLogic(reusableQuestionIds[2], localSurvey.endings[0].id, "isSkipped"), ], - headline: { default: t("templates.feedback_box_question_3_headline") }, + headline: t("templates.feedback_box_question_3_headline"), required: false, - buttonLabel: { default: t("templates.feedback_box_question_3_button_label") }, + buttonLabel: t("templates.feedback_box_question_3_button_label"), buttonExternal: false, - dismissButtonLabel: { default: t("templates.feedback_box_question_3_dismiss_button_label") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + dismissButtonLabel: t("templates.feedback_box_question_3_dismiss_button_label"), + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.feedback_box_question_4_headline") }, + headline: t("templates.feedback_box_question_4_headline"), required: true, - subheader: { default: t("templates.feedback_box_question_4_subheader") }, - buttonLabel: { default: t("templates.feedback_box_question_4_button_label") }, - placeholder: { default: t("templates.feedback_box_question_4_placeholder") }, + subheader: t("templates.feedback_box_question_4_subheader"), + buttonLabel: t("templates.feedback_box_question_4_button_label"), + placeholder: t("templates.feedback_box_question_4_placeholder"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const integrationSetupSurvey = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.integration_setup_survey_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.integration_setup_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.integration_setup_survey_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.integration_setup_survey_description"), questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -2987,195 +1299,138 @@ const integrationSetupSurvey = (t: TFnType): TTemplate => { ], range: 5, scale: "number", - headline: { default: t("templates.integration_setup_survey_question_1_headline") }, + headline: t("templates.integration_setup_survey_question_1_headline"), required: true, - lowerLabel: { default: t("templates.integration_setup_survey_question_1_lower_label") }, - upperLabel: { default: t("templates.integration_setup_survey_question_1_upper_label") }, + lowerLabel: t("templates.integration_setup_survey_question_1_lower_label"), + upperLabel: t("templates.integration_setup_survey_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.integration_setup_survey_question_2_headline") }, + headline: t("templates.integration_setup_survey_question_2_headline"), required: false, - placeholder: { default: t("templates.integration_setup_survey_question_2_placeholder") }, + placeholder: t("templates.integration_setup_survey_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.integration_setup_survey_question_3_headline") }, + headline: t("templates.integration_setup_survey_question_3_headline"), required: false, - subheader: { default: t("templates.integration_setup_survey_question_3_subheader") }, + subheader: t("templates.integration_setup_survey_question_3_subheader"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const newIntegrationSurvey = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.new_integration_survey_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.new_integration_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.new_integration_survey_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.new_integration_survey_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.new_integration_survey_question_1_headline") }, + headline: t("templates.new_integration_survey_question_1_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.new_integration_survey_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.new_integration_survey_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.new_integration_survey_question_1_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.new_integration_survey_question_1_choice_4") }, - }, - { - id: "other", - label: { default: t("templates.new_integration_survey_question_1_choice_5") }, - }, + t("templates.new_integration_survey_question_1_choice_1"), + t("templates.new_integration_survey_question_1_choice_2"), + t("templates.new_integration_survey_question_1_choice_3"), + t("templates.new_integration_survey_question_1_choice_4"), + t("templates.new_integration_survey_question_1_choice_5"), ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + containsOther: true, + t, + }), ], }, - }; + t + ); }; const docsFeedback = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.docs_feedback_name"), - role: "productManager", - industries: ["saas"], - channels: ["app", "website", "link"], - description: t("templates.docs_feedback_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.docs_feedback_name"), + role: "productManager", + industries: ["saas"], + channels: ["app", "website", "link"], + description: t("templates.docs_feedback_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.docs_feedback_question_1_headline") }, + headline: t("templates.docs_feedback_question_1_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.docs_feedback_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.docs_feedback_question_1_choice_2") }, - }, + t("templates.docs_feedback_question_1_choice_1"), + t("templates.docs_feedback_question_1_choice_2"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.docs_feedback_question_2_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.docs_feedback_question_2_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.docs_feedback_question_3_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.docs_feedback_question_3_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const nps = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.nps_name"), - role: "customerSuccess", - industries: ["saas", "eCommerce", "other"], - channels: ["app", "link", "website"], - description: t("templates.nps_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.nps_name"), + role: "customerSuccess", + industries: ["saas", "eCommerce", "other"], + channels: ["app", "link", "website"], + description: t("templates.nps_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.NPS, - headline: { default: t("templates.nps_question_1_headline") }, + buildNPSQuestion({ + headline: t("templates.nps_question_1_headline"), required: false, - lowerLabel: { default: t("templates.nps_question_1_lower_label") }, - upperLabel: { default: t("templates.nps_question_1_upper_label") }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.nps_question_2_headline") }, + lowerLabel: t("templates.nps_question_1_lower_label"), + upperLabel: t("templates.nps_question_1_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.nps_question_2_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const customerSatisfactionScore = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [ createId(), createId(), @@ -3188,188 +1443,166 @@ const customerSatisfactionScore = (t: TFnType): TTemplate => { createId(), createId(), ]; - return { - name: t("templates.csat_name"), - role: "customerSuccess", - industries: ["saas", "eCommerce", "other"], - channels: ["app", "link", "website"], - description: t("templates.csat_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.csat_name"), + role: "customerSuccess", + industries: ["saas", "eCommerce", "other"], + channels: ["app", "link", "website"], + description: t("templates.csat_description"), questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, range: 10, scale: "number", - headline: { - default: t("templates.csat_question_1_headline"), - }, + headline: t("templates.csat_question_1_headline"), required: true, - lowerLabel: { default: t("templates.csat_question_1_lower_label") }, - upperLabel: { default: t("templates.csat_question_1_upper_label") }, + lowerLabel: t("templates.csat_question_1_lower_label"), + upperLabel: t("templates.csat_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[1], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_2_headline") }, - subheader: { default: t("templates.csat_question_2_subheader") }, + headline: t("templates.csat_question_2_headline"), + subheader: t("templates.csat_question_2_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_2_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_2_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_2_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_2_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_2_choice_5") } }, + t("templates.csat_question_2_choice_1"), + t("templates.csat_question_2_choice_2"), + t("templates.csat_question_2_choice_3"), + t("templates.csat_question_2_choice_4"), + t("templates.csat_question_2_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[2], type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - headline: { - default: t("templates.csat_question_3_headline"), - }, - subheader: { default: t("templates.csat_question_3_subheader") }, + headline: t("templates.csat_question_3_headline"), + subheader: t("templates.csat_question_3_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_3_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_5") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_6") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_7") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_8") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_9") } }, - { id: createId(), label: { default: t("templates.csat_question_3_choice_10") } }, + t("templates.csat_question_3_choice_1"), + t("templates.csat_question_3_choice_2"), + t("templates.csat_question_3_choice_3"), + t("templates.csat_question_3_choice_4"), + t("templates.csat_question_3_choice_5"), + t("templates.csat_question_3_choice_6"), + t("templates.csat_question_3_choice_7"), + t("templates.csat_question_3_choice_8"), + t("templates.csat_question_3_choice_9"), + t("templates.csat_question_3_choice_10"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[3], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_4_headline") }, - subheader: { default: t("templates.csat_question_4_subheader") }, + headline: t("templates.csat_question_4_headline"), + subheader: t("templates.csat_question_4_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_4_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_4_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_4_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_4_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_4_choice_5") } }, + t("templates.csat_question_4_choice_1"), + t("templates.csat_question_4_choice_2"), + t("templates.csat_question_4_choice_3"), + t("templates.csat_question_4_choice_4"), + t("templates.csat_question_4_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[4], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_5_headline") }, - subheader: { default: t("templates.csat_question_5_subheader") }, + headline: t("templates.csat_question_5_headline"), + subheader: t("templates.csat_question_5_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_5_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_5_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_5_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_5_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_5_choice_5") } }, + t("templates.csat_question_5_choice_1"), + t("templates.csat_question_5_choice_2"), + t("templates.csat_question_5_choice_3"), + t("templates.csat_question_5_choice_4"), + t("templates.csat_question_5_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[5], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_6_headline") }, - subheader: { default: t("templates.csat_question_6_subheader") }, + headline: t("templates.csat_question_6_headline"), + subheader: t("templates.csat_question_6_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_6_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_6_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_6_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_6_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_6_choice_5") } }, + t("templates.csat_question_6_choice_1"), + t("templates.csat_question_6_choice_2"), + t("templates.csat_question_6_choice_3"), + t("templates.csat_question_6_choice_4"), + t("templates.csat_question_6_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[6], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_7_headline") }, - subheader: { default: t("templates.csat_question_7_subheader") }, + headline: t("templates.csat_question_7_headline"), + subheader: t("templates.csat_question_7_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_7_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_7_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_7_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_7_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_7_choice_5") } }, - { id: createId(), label: { default: t("templates.csat_question_7_choice_6") } }, + t("templates.csat_question_7_choice_1"), + t("templates.csat_question_7_choice_2"), + t("templates.csat_question_7_choice_3"), + t("templates.csat_question_7_choice_4"), + t("templates.csat_question_7_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[7], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_8_headline") }, - subheader: { default: t("templates.csat_question_8_subheader") }, + headline: t("templates.csat_question_8_headline"), + subheader: t("templates.csat_question_8_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_8_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_8_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_8_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_8_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_8_choice_5") } }, - { id: createId(), label: { default: t("templates.csat_question_8_choice_6") } }, + t("templates.csat_question_8_choice_1"), + t("templates.csat_question_8_choice_2"), + t("templates.csat_question_8_choice_3"), + t("templates.csat_question_8_choice_4"), + t("templates.csat_question_8_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[8], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.csat_question_9_headline") }, - subheader: { default: t("templates.csat_question_9_subheader") }, + headline: t("templates.csat_question_9_headline"), + subheader: t("templates.csat_question_9_subheader"), required: true, choices: [ - { id: createId(), label: { default: t("templates.csat_question_9_choice_1") } }, - { id: createId(), label: { default: t("templates.csat_question_9_choice_2") } }, - { id: createId(), label: { default: t("templates.csat_question_9_choice_3") } }, - { id: createId(), label: { default: t("templates.csat_question_9_choice_4") } }, - { id: createId(), label: { default: t("templates.csat_question_9_choice_5") } }, + t("templates.csat_question_9_choice_1"), + t("templates.csat_question_9_choice_2"), + t("templates.csat_question_9_choice_3"), + t("templates.csat_question_9_choice_4"), + t("templates.csat_question_9_choice_5"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[9], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.csat_question_10_headline") }, + headline: t("templates.csat_question_10_headline"), required: false, - placeholder: { default: t("templates.csat_question_10_placeholder") }, + placeholder: t("templates.csat_question_10_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const collectFeedback = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [ createId(), createId(), @@ -3379,19 +1612,16 @@ const collectFeedback = (t: TFnType): TTemplate => { createId(), createId(), ]; - return { - name: t("templates.collect_feedback_name"), - role: "productManager", - industries: ["other", "eCommerce"], - channels: ["website", "link"], - description: t("templates.collect_feedback_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.collect_feedback_name"), + role: "productManager", + industries: ["other", "eCommerce"], + channels: ["website", "link"], + description: t("templates.collect_feedback_description"), questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -3424,21 +1654,16 @@ const collectFeedback = (t: TFnType): TTemplate => { ], range: 5, scale: "star", - headline: { default: t("templates.collect_feedback_question_1_headline") }, - subheader: { default: t("templates.collect_feedback_question_1_subheader") }, + headline: t("templates.collect_feedback_question_1_headline"), + subheader: t("templates.collect_feedback_question_1_subheader"), required: true, - lowerLabel: { default: t("templates.collect_feedback_question_1_lower_label") }, - upperLabel: { default: t("templates.collect_feedback_question_1_upper_label") }, + lowerLabel: t("templates.collect_feedback_question_1_lower_label"), + upperLabel: t("templates.collect_feedback_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [ { id: createId(), @@ -3465,669 +1690,452 @@ const collectFeedback = (t: TFnType): TTemplate => { ], }, ], - headline: { default: t("templates.collect_feedback_question_2_headline") }, + headline: t("templates.collect_feedback_question_2_headline"), required: true, longAnswer: true, - placeholder: { default: t("templates.collect_feedback_question_2_placeholder") }, + placeholder: t("templates.collect_feedback_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.collect_feedback_question_3_headline") }, + headline: t("templates.collect_feedback_question_3_headline"), required: true, longAnswer: true, - placeholder: { default: t("templates.collect_feedback_question_3_placeholder") }, + placeholder: t("templates.collect_feedback_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildRatingQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.Rating, range: 5, scale: "smiley", - headline: { default: t("templates.collect_feedback_question_4_headline") }, + headline: t("templates.collect_feedback_question_4_headline"), required: true, - lowerLabel: { default: t("templates.collect_feedback_question_4_lower_label") }, - upperLabel: { default: t("templates.collect_feedback_question_4_upper_label") }, + lowerLabel: t("templates.collect_feedback_question_4_lower_label"), + upperLabel: t("templates.collect_feedback_question_4_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.collect_feedback_question_5_headline") }, + headline: t("templates.collect_feedback_question_5_headline"), required: false, longAnswer: true, - placeholder: { default: t("templates.collect_feedback_question_5_placeholder") }, + placeholder: t("templates.collect_feedback_question_5_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[5], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, choices: [ - { id: createId(), label: { default: t("templates.collect_feedback_question_6_choice_1") } }, - { id: createId(), label: { default: t("templates.collect_feedback_question_6_choice_2") } }, - { id: createId(), label: { default: t("templates.collect_feedback_question_6_choice_3") } }, - { id: createId(), label: { default: t("templates.collect_feedback_question_6_choice_4") } }, - { id: "other", label: { default: t("templates.collect_feedback_question_6_choice_5") } }, + t("templates.collect_feedback_question_6_choice_1"), + t("templates.collect_feedback_question_6_choice_2"), + t("templates.collect_feedback_question_6_choice_3"), + t("templates.collect_feedback_question_6_choice_4"), + t("templates.collect_feedback_question_6_choice_5"), ], - headline: { default: t("templates.collect_feedback_question_6_headline") }, + headline: t("templates.collect_feedback_question_6_headline"), required: true, shuffleOption: "none", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + containsOther: true, + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[6], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.collect_feedback_question_7_headline") }, + headline: t("templates.collect_feedback_question_7_headline"), required: false, inputType: "email", longAnswer: false, - placeholder: { default: t("templates.collect_feedback_question_7_placeholder") }, - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + placeholder: t("templates.collect_feedback_question_7_placeholder"), + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const identifyUpsellOpportunities = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.identify_upsell_opportunities_name"), - role: "sales", - industries: ["saas"], - channels: ["app", "link"], - description: t("templates.identify_upsell_opportunities_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.identify_upsell_opportunities_name"), + role: "sales", + industries: ["saas"], + channels: ["app", "link"], + description: t("templates.identify_upsell_opportunities_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.identify_upsell_opportunities_question_1_headline") }, + headline: t("templates.identify_upsell_opportunities_question_1_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.identify_upsell_opportunities_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.identify_upsell_opportunities_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.identify_upsell_opportunities_question_1_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.identify_upsell_opportunities_question_1_choice_4") }, - }, + t("templates.identify_upsell_opportunities_question_1_choice_1"), + t("templates.identify_upsell_opportunities_question_1_choice_2"), + t("templates.identify_upsell_opportunities_question_1_choice_3"), + t("templates.identify_upsell_opportunities_question_1_choice_4"), ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const prioritizeFeatures = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.prioritize_features_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.prioritize_features_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.prioritize_features_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.prioritize_features_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, logic: [], shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.prioritize_features_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.prioritize_features_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.prioritize_features_question_1_choice_3") }, - }, - { id: "other", label: { default: t("templates.prioritize_features_question_1_choice_4") } }, + t("templates.prioritize_features_question_1_choice_1"), + t("templates.prioritize_features_question_1_choice_2"), + t("templates.prioritize_features_question_1_choice_3"), + t("templates.prioritize_features_question_1_choice_4"), ], - headline: { default: t("templates.prioritize_features_question_1_headline") }, + headline: t("templates.prioritize_features_question_1_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + containsOther: true, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, logic: [], shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.prioritize_features_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.prioritize_features_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.prioritize_features_question_2_choice_3") }, - }, + t("templates.prioritize_features_question_2_choice_1"), + t("templates.prioritize_features_question_2_choice_2"), + t("templates.prioritize_features_question_2_choice_3"), ], - headline: { default: t("templates.prioritize_features_question_2_headline") }, + headline: t("templates.prioritize_features_question_2_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.prioritize_features_question_3_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.prioritize_features_question_3_headline"), required: true, - placeholder: { default: t("templates.prioritize_features_question_3_placeholder") }, + placeholder: t("templates.prioritize_features_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const gaugeFeatureSatisfaction = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.gauge_feature_satisfaction_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.gauge_feature_satisfaction_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.gauge_feature_satisfaction_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.gauge_feature_satisfaction_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: t("templates.gauge_feature_satisfaction_question_1_headline") }, + buildRatingQuestion({ + headline: t("templates.gauge_feature_satisfaction_question_1_headline"), required: true, - lowerLabel: { default: t("templates.gauge_feature_satisfaction_question_1_lower_label") }, - upperLabel: { default: t("templates.gauge_feature_satisfaction_question_1_upper_label") }, + lowerLabel: t("templates.gauge_feature_satisfaction_question_1_lower_label"), + upperLabel: t("templates.gauge_feature_satisfaction_question_1_upper_label"), scale: "number", range: 5, isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.gauge_feature_satisfaction_question_2_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.gauge_feature_satisfaction_question_2_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], endings: [getDefaultEndingCard([], t)], hiddenFields: hiddenFieldsDefault, }, - }; + t + ); }; const marketSiteClarity = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.market_site_clarity_name"), - role: "marketing", - industries: ["saas", "eCommerce", "other"], - channels: ["website"], - description: t("templates.market_site_clarity_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.market_site_clarity_name"), + role: "marketing", + industries: ["saas", "eCommerce", "other"], + channels: ["website"], + description: t("templates.market_site_clarity_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.market_site_clarity_question_1_headline") }, + headline: t("templates.market_site_clarity_question_1_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.market_site_clarity_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.market_site_clarity_question_1_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.market_site_clarity_question_1_choice_3") }, - }, + t("templates.market_site_clarity_question_1_choice_1"), + t("templates.market_site_clarity_question_1_choice_2"), + t("templates.market_site_clarity_question_1_choice_3"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.market_site_clarity_question_2_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.market_site_clarity_question_2_headline"), required: false, inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.CTA, - headline: { default: t("templates.market_site_clarity_question_3_headline") }, + t, + }), + buildCTAQuestion({ + headline: t("templates.market_site_clarity_question_3_headline"), required: false, - buttonLabel: { default: t("templates.market_site_clarity_question_3_button_label") }, + buttonLabel: t("templates.market_site_clarity_question_3_button_label"), buttonUrl: "https://app.formbricks.com/auth/signup", buttonExternal: true, - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const customerEffortScore = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.customer_effort_score_name"), - role: "productManager", - industries: ["saas"], - channels: ["app"], - description: t("templates.customer_effort_score_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.customer_effort_score_name"), + role: "productManager", + industries: ["saas"], + channels: ["app"], + description: t("templates.customer_effort_score_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + buildRatingQuestion({ range: 5, scale: "number", - headline: { default: t("templates.customer_effort_score_question_1_headline") }, + headline: t("templates.customer_effort_score_question_1_headline"), required: true, - lowerLabel: { default: t("templates.customer_effort_score_question_1_lower_label") }, - upperLabel: { default: t("templates.customer_effort_score_question_1_upper_label") }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.customer_effort_score_question_2_headline") }, + lowerLabel: t("templates.customer_effort_score_question_1_lower_label"), + upperLabel: t("templates.customer_effort_score_question_1_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.customer_effort_score_question_2_headline"), required: true, - placeholder: { default: t("templates.customer_effort_score_question_2_placeholder") }, + placeholder: t("templates.customer_effort_score_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const careerDevelopmentSurvey = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.career_development_survey_name"), - role: "productManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.career_development_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.career_development_survey_name"), + role: "productManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.career_development_survey_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + buildRatingQuestion({ range: 5, scale: "number", - headline: { - default: t("templates.career_development_survey_question_1_headline"), - }, - lowerLabel: { default: t("templates.career_development_survey_question_1_lower_label") }, - upperLabel: { default: t("templates.career_development_survey_question_1_upper_label") }, + headline: t("templates.career_development_survey_question_1_headline"), + lowerLabel: t("templates.career_development_survey_question_1_lower_label"), + upperLabel: t("templates.career_development_survey_question_1_upper_label"), required: true, - isColorCodingEnabled: false, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "number", - headline: { - default: t("templates.career_development_survey_question_2_headline"), - }, - lowerLabel: { default: t("templates.career_development_survey_question_2_lower_label") }, - upperLabel: { default: t("templates.career_development_survey_question_2_upper_label") }, + headline: t("templates.career_development_survey_question_2_headline"), + lowerLabel: t("templates.career_development_survey_question_2_lower_label"), + upperLabel: t("templates.career_development_survey_question_2_upper_label"), required: true, - isColorCodingEnabled: false, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "number", - headline: { - default: t("templates.career_development_survey_question_3_headline"), - }, - lowerLabel: { default: t("templates.career_development_survey_question_3_lower_label") }, - upperLabel: { default: t("templates.career_development_survey_question_3_upper_label") }, + headline: t("templates.career_development_survey_question_3_headline"), + lowerLabel: t("templates.career_development_survey_question_3_lower_label"), + upperLabel: t("templates.career_development_survey_question_3_upper_label"), required: true, - isColorCodingEnabled: false, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "number", - headline: { - default: t("templates.career_development_survey_question_4_headline"), - }, - lowerLabel: { default: t("templates.career_development_survey_question_4_lower_label") }, - upperLabel: { default: t("templates.career_development_survey_question_4_upper_label") }, + headline: t("templates.career_development_survey_question_4_headline"), + lowerLabel: t("templates.career_development_survey_question_4_lower_label"), + upperLabel: t("templates.career_development_survey_question_4_upper_label"), required: true, - isColorCodingEnabled: false, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.career_development_survey_question_5_headline") }, - subheader: { default: t("templates.career_development_survey_question_5_subheader") }, + headline: t("templates.career_development_survey_question_5_headline"), + subheader: t("templates.career_development_survey_question_5_subheader"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.career_development_survey_question_5_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_5_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_5_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_5_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_5_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.career_development_survey_question_5_choice_6") }, - }, + t("templates.career_development_survey_question_5_choice_1"), + t("templates.career_development_survey_question_5_choice_2"), + t("templates.career_development_survey_question_5_choice_3"), + t("templates.career_development_survey_question_5_choice_4"), + t("templates.career_development_survey_question_5_choice_5"), + t("templates.career_development_survey_question_5_choice_6"), ], - }, - { + containsOther: true, + t, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { default: t("templates.career_development_survey_question_6_headline") }, - subheader: { default: t("templates.career_development_survey_question_6_subheader") }, + headline: t("templates.career_development_survey_question_6_headline"), + subheader: t("templates.career_development_survey_question_6_subheader"), required: true, shuffleOption: "exceptLast", choices: [ - { - id: createId(), - label: { default: t("templates.career_development_survey_question_6_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_6_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_6_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_6_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.career_development_survey_question_6_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.career_development_survey_question_6_choice_6") }, - }, + t("templates.career_development_survey_question_6_choice_1"), + t("templates.career_development_survey_question_6_choice_2"), + t("templates.career_development_survey_question_6_choice_3"), + t("templates.career_development_survey_question_6_choice_4"), + t("templates.career_development_survey_question_6_choice_5"), + t("templates.career_development_survey_question_6_choice_6"), ], - }, + containsOther: true, + t, + }), ], }, - }; + t + ); }; const professionalDevelopmentSurvey = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.professional_development_survey_name"), - role: "productManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.professional_development_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.professional_development_survey_name"), + role: "productManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.professional_development_survey_description"), questions: [ - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { - default: t("templates.professional_development_survey_question_1_headline"), - }, + headline: t("templates.professional_development_survey_question_1_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_1_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_1_choice_2") }, - }, + t("templates.professional_development_survey_question_1_choice_1"), + t("templates.professional_development_survey_question_1_choice_1"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), - { + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - headline: { - default: t("templates.professional_development_survey_question_2_headline"), - }, - subheader: { default: t("templates.professional_development_survey_question_2_subheader") }, + headline: t("templates.professional_development_survey_question_2_headline"), + subheader: t("templates.professional_development_survey_question_2_subheader"), required: true, shuffleOption: "exceptLast", choices: [ - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_2_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_2_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_2_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_2_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_2_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.professional_development_survey_question_2_choice_6") }, - }, + t("templates.professional_development_survey_question_2_choice_1"), + t("templates.professional_development_survey_question_2_choice_2"), + t("templates.professional_development_survey_question_2_choice_3"), + t("templates.professional_development_survey_question_2_choice_4"), + t("templates.professional_development_survey_question_2_choice_5"), + t("templates.professional_development_survey_question_2_choice_6"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + containsOther: true, + t, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, - headline: { - default: t("templates.professional_development_survey_question_3_headline"), - }, + headline: t("templates.professional_development_survey_question_3_headline"), required: true, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_3_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_3_choice_2") }, - }, + t("templates.professional_development_survey_question_3_choice_1"), + t("templates.professional_development_survey_question_3_choice_2"), ], - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "number", - headline: { - default: t("templates.professional_development_survey_question_4_headline"), - }, - lowerLabel: { - default: t("templates.professional_development_survey_question_4_lower_label"), - }, - upperLabel: { - default: t("templates.professional_development_survey_question_4_upper_label"), - }, + headline: t("templates.professional_development_survey_question_4_headline"), + lowerLabel: t("templates.professional_development_survey_question_4_lower_label"), + upperLabel: t("templates.professional_development_survey_question_4_upper_label"), required: true, isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: createId(), type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, - headline: { - default: t("templates.professional_development_survey_question_5_headline"), - }, + headline: t("templates.professional_development_survey_question_5_headline"), required: true, shuffleOption: "exceptLast", choices: [ - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_5_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_5_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_5_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_5_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.professional_development_survey_question_5_choice_5") }, - }, - { - id: "other", - label: { default: t("templates.professional_development_survey_question_5_choice_6") }, - }, + t("templates.professional_development_survey_question_5_choice_1"), + t("templates.professional_development_survey_question_5_choice_2"), + t("templates.professional_development_survey_question_5_choice_3"), + t("templates.professional_development_survey_question_5_choice_4"), + t("templates.professional_development_survey_question_5_choice_5"), + t("templates.professional_development_survey_question_5_choice_6"), ], - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + containsOther: true, + t, + }), ], }, - }; + t + ); }; const rateCheckoutExperience = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.rate_checkout_experience_name"), - role: "productManager", - industries: ["eCommerce"], - channels: ["website", "app"], - description: t("templates.rate_checkout_experience_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.rate_checkout_experience_name"), + role: "productManager", + industries: ["eCommerce"], + channels: ["website", "app"], + description: t("templates.rate_checkout_experience_description"), + endings: localSurvey.endings, questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -4160,87 +2168,51 @@ const rateCheckoutExperience = (t: TFnType): TTemplate => { ], range: 5, scale: "number", - headline: { default: t("templates.rate_checkout_experience_question_1_headline") }, + headline: t("templates.rate_checkout_experience_question_1_headline"), required: true, - lowerLabel: { default: t("templates.rate_checkout_experience_question_1_lower_label") }, - upperLabel: { default: t("templates.rate_checkout_experience_question_1_upper_label") }, + lowerLabel: t("templates.rate_checkout_experience_question_1_lower_label"), + upperLabel: t("templates.rate_checkout_experience_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.rate_checkout_experience_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.rate_checkout_experience_question_2_headline"), required: true, - placeholder: { default: t("templates.rate_checkout_experience_question_2_placeholder") }, + placeholder: t("templates.rate_checkout_experience_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.rate_checkout_experience_question_3_headline") }, + headline: t("templates.rate_checkout_experience_question_3_headline"), required: true, - placeholder: { default: t("templates.rate_checkout_experience_question_3_placeholder") }, + placeholder: t("templates.rate_checkout_experience_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const measureSearchExperience = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.measure_search_experience_name"), - role: "productManager", - industries: ["saas", "eCommerce"], - channels: ["app", "website"], - description: t("templates.measure_search_experience_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.measure_search_experience_name"), + role: "productManager", + industries: ["saas", "eCommerce"], + channels: ["app", "website"], + description: t("templates.measure_search_experience_description"), + endings: localSurvey.endings, questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -4273,87 +2245,51 @@ const measureSearchExperience = (t: TFnType): TTemplate => { ], range: 5, scale: "number", - headline: { default: t("templates.measure_search_experience_question_1_headline") }, + headline: t("templates.measure_search_experience_question_1_headline"), required: true, - lowerLabel: { default: t("templates.measure_search_experience_question_1_lower_label") }, - upperLabel: { default: t("templates.measure_search_experience_question_1_upper_label") }, + lowerLabel: t("templates.measure_search_experience_question_1_lower_label"), + upperLabel: t("templates.measure_search_experience_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.measure_search_experience_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.measure_search_experience_question_2_headline"), required: true, - placeholder: { default: t("templates.measure_search_experience_question_2_placeholder") }, + placeholder: t("templates.measure_search_experience_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.measure_search_experience_question_3_headline") }, + headline: t("templates.measure_search_experience_question_3_headline"), required: true, - placeholder: { default: t("templates.measure_search_experience_question_3_placeholder") }, + placeholder: t("templates.measure_search_experience_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const evaluateContentQuality = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.evaluate_content_quality_name"), - role: "marketing", - industries: ["other"], - channels: ["website"], - description: t("templates.evaluate_content_quality_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.evaluate_content_quality_name"), + role: "marketing", + industries: ["other"], + channels: ["website"], + description: t("templates.evaluate_content_quality_description"), + endings: localSurvey.endings, questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -4386,197 +2322,70 @@ const evaluateContentQuality = (t: TFnType): TTemplate => { ], range: 5, scale: "number", - headline: { default: t("templates.evaluate_content_quality_question_1_headline") }, + headline: t("templates.evaluate_content_quality_question_1_headline"), required: true, - lowerLabel: { default: t("templates.evaluate_content_quality_question_1_lower_label") }, - upperLabel: { default: t("templates.evaluate_content_quality_question_1_upper_label") }, + lowerLabel: t("templates.evaluate_content_quality_question_1_lower_label"), + upperLabel: t("templates.evaluate_content_quality_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.evaluate_content_quality_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.evaluate_content_quality_question_2_headline"), required: true, - placeholder: { default: t("templates.evaluate_content_quality_question_2_placeholder") }, + placeholder: t("templates.evaluate_content_quality_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.evaluate_content_quality_question_3_headline") }, + headline: t("templates.evaluate_content_quality_question_3_headline"), required: true, - placeholder: { default: t("templates.evaluate_content_quality_question_3_placeholder") }, + placeholder: t("templates.evaluate_content_quality_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const measureTaskAccomplishment = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId(), createId(), createId()]; const reusableOptionIds = [createId(), createId(), createId()]; - return { - name: t("templates.measure_task_accomplishment_name"), - role: "productManager", - industries: ["saas"], - channels: ["app", "website"], - description: t("templates.measure_task_accomplishment_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.measure_task_accomplishment_name"), + role: "productManager", + industries: ["saas"], + channels: ["app", "website"], + description: t("templates.measure_task_accomplishment_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[0], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[1], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[2], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[4], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[3]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[0], reusableQuestionIds[1]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[2], reusableQuestionIds[4]), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.measure_task_accomplishment_question_1_option_1_label") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.measure_task_accomplishment_question_1_option_2_label") }, - }, - { - id: reusableOptionIds[2], - label: { default: t("templates.measure_task_accomplishment_question_1_option_3_label") }, - }, + t("templates.measure_task_accomplishment_question_1_option_1_label"), + t("templates.measure_task_accomplishment_question_1_option_2_label"), + t("templates.measure_task_accomplishment_question_1_option_3_label"), ], - headline: { default: t("templates.measure_task_accomplishment_question_1_headline") }, + headline: t("templates.measure_task_accomplishment_question_1_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildRatingQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -4609,20 +2418,15 @@ const measureTaskAccomplishment = (t: TFnType): TTemplate => { ], range: 5, scale: "number", - headline: { default: t("templates.measure_task_accomplishment_question_2_headline") }, + headline: t("templates.measure_task_accomplishment_question_2_headline"), required: false, - lowerLabel: { default: t("templates.measure_task_accomplishment_question_2_lower_label") }, - upperLabel: { default: t("templates.measure_task_accomplishment_question_2_upper_label") }, + lowerLabel: t("templates.measure_task_accomplishment_question_2_lower_label"), + upperLabel: t("templates.measure_task_accomplishment_question_2_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [ { id: createId(), @@ -4657,19 +2461,14 @@ const measureTaskAccomplishment = (t: TFnType): TTemplate => { ], }, ], - headline: { default: t("templates.measure_task_accomplishment_question_3_headline") }, + headline: t("templates.measure_task_accomplishment_question_3_headline"), required: false, - placeholder: { default: t("templates.measure_task_accomplishment_question_3_placeholder") }, + placeholder: t("templates.measure_task_accomplishment_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [ { id: createId(), @@ -4704,28 +2503,25 @@ const measureTaskAccomplishment = (t: TFnType): TTemplate => { ], }, ], - headline: { default: t("templates.measure_task_accomplishment_question_4_headline") }, + headline: t("templates.measure_task_accomplishment_question_4_headline"), required: false, - buttonLabel: { default: t("templates.measure_task_accomplishment_question_4_button_label") }, + buttonLabel: t("templates.measure_task_accomplishment_question_4_button_label"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.measure_task_accomplishment_question_5_headline") }, + headline: t("templates.measure_task_accomplishment_question_5_headline"), required: true, - buttonLabel: { default: t("templates.measure_task_accomplishment_question_5_button_label") }, - placeholder: { default: t("templates.measure_task_accomplishment_question_5_placeholder") }, + buttonLabel: t("templates.measure_task_accomplishment_question_5_button_label"), + placeholder: t("templates.measure_task_accomplishment_question_5_placeholder"), inputType: "text", - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const identifySignUpBarriers = (t: TFnType): TTemplate => { @@ -4743,60 +2539,28 @@ const identifySignUpBarriers = (t: TFnType): TTemplate => { ]; const reusableOptionIds = [createId(), createId(), createId(), createId(), createId()]; - return { - name: t("templates.identify_sign_up_barriers_name"), - role: "marketing", - industries: ["saas", "eCommerce", "other"], - channels: ["website"], - description: t("templates.identify_sign_up_barriers_description"), - preset: { - ...localSurvey, - name: t("templates.identify_sign_up_barriers_with_project_name"), + return buildSurvey( + { + name: t("templates.identify_sign_up_barriers_name"), + role: "marketing", + industries: ["saas", "eCommerce", "other"], + channels: ["website"], + description: t("templates.identify_sign_up_barriers_description"), + endings: localSurvey.endings, questions: [ - { + buildCTAQuestion({ id: reusableQuestionIds[0], - html: { - default: t("templates.identify_sign_up_barriers_question_1_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.identify_sign_up_barriers_question_1_headline") }, + html: t("templates.identify_sign_up_barriers_question_1_html"), + logic: [createJumpLogic(reusableQuestionIds[0], localSurvey.endings[0].id, "isSkipped")], + headline: t("templates.identify_sign_up_barriers_question_1_headline"), required: false, - buttonLabel: { default: t("templates.identify_sign_up_barriers_question_1_button_label") }, + buttonLabel: t("templates.identify_sign_up_barriers_question_1_button_label"), buttonExternal: false, - dismissButtonLabel: { - default: t("templates.identify_sign_up_barriers_question_1_dismiss_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, - }, - { + dismissButtonLabel: t("templates.identify_sign_up_barriers_question_1_dismiss_button_label"), + t, + }), + buildRatingQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.Rating, logic: [ { id: createId(), @@ -4829,674 +2593,208 @@ const identifySignUpBarriers = (t: TFnType): TTemplate => { ], range: 5, scale: "number", - headline: { default: t("templates.identify_sign_up_barriers_question_2_headline") }, + headline: t("templates.identify_sign_up_barriers_question_2_headline"), required: true, - lowerLabel: { default: t("templates.identify_sign_up_barriers_question_2_lower_label") }, - upperLabel: { default: t("templates.identify_sign_up_barriers_question_2_upper_label") }, + lowerLabel: t("templates.identify_sign_up_barriers_question_2_lower_label"), + upperLabel: t("templates.identify_sign_up_barriers_question_2_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildMultipleChoiceQuestion({ id: reusableQuestionIds[2], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[0], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[4], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[2], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[3], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[6], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[4], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[7], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[2], reusableOptionIds[0], reusableQuestionIds[3]), + createChoiceJumpLogic(reusableQuestionIds[2], reusableOptionIds[1], reusableQuestionIds[4]), + createChoiceJumpLogic(reusableQuestionIds[2], reusableOptionIds[2], reusableQuestionIds[5]), + createChoiceJumpLogic(reusableQuestionIds[2], reusableOptionIds[3], reusableQuestionIds[6]), + createChoiceJumpLogic(reusableQuestionIds[2], reusableOptionIds[4], reusableQuestionIds[7]), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.identify_sign_up_barriers_question_3_choice_1_label") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.identify_sign_up_barriers_question_3_choice_2_label") }, - }, - { - id: reusableOptionIds[2], - label: { default: t("templates.identify_sign_up_barriers_question_3_choice_3_label") }, - }, - { - id: reusableOptionIds[3], - label: { default: t("templates.identify_sign_up_barriers_question_3_choice_4_label") }, - }, - { - id: reusableOptionIds[4], - label: { default: t("templates.identify_sign_up_barriers_question_3_choice_5_label") }, - }, + t("templates.identify_sign_up_barriers_question_3_choice_1_label"), + t("templates.identify_sign_up_barriers_question_3_choice_2_label"), + t("templates.identify_sign_up_barriers_question_3_choice_3_label"), + t("templates.identify_sign_up_barriers_question_3_choice_4_label"), + t("templates.identify_sign_up_barriers_question_3_choice_5_label"), ], - headline: { default: t("templates.identify_sign_up_barriers_question_3_headline") }, + choiceIds: [ + reusableOptionIds[0], + reusableOptionIds[1], + reusableOptionIds[2], + reusableOptionIds[3], + reusableOptionIds[4], + ], + headline: t("templates.identify_sign_up_barriers_question_3_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[3], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[8], - }, - ], - }, - ], - headline: { default: t("templates.identify_sign_up_barriers_question_4_headline") }, + logic: [createJumpLogic(reusableQuestionIds[3], reusableQuestionIds[8], "isSubmitted")], + headline: t("templates.identify_sign_up_barriers_question_4_headline"), required: true, - placeholder: { default: t("templates.identify_sign_up_barriers_question_4_placeholder") }, + placeholder: t("templates.identify_sign_up_barriers_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[8], - }, - ], - }, - ], - headline: { default: t("templates.identify_sign_up_barriers_question_5_headline") }, + logic: [createJumpLogic(reusableQuestionIds[4], reusableQuestionIds[8], "isSubmitted")], + headline: t("templates.identify_sign_up_barriers_question_5_headline"), required: true, - placeholder: { default: t("templates.identify_sign_up_barriers_question_5_placeholder") }, + placeholder: t("templates.identify_sign_up_barriers_question_5_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[5], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[5], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[8], - }, - ], - }, - ], - headline: { default: t("templates.identify_sign_up_barriers_question_6_headline") }, + logic: [createJumpLogic(reusableQuestionIds[5], reusableQuestionIds[8], "isSubmitted")], + headline: t("templates.identify_sign_up_barriers_question_6_headline"), required: true, - placeholder: { default: t("templates.identify_sign_up_barriers_question_6_placeholder") }, + placeholder: t("templates.identify_sign_up_barriers_question_6_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[6], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[6], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[8], - }, - ], - }, - ], - headline: { default: t("templates.identify_sign_up_barriers_question_7_headline") }, + logic: [createJumpLogic(reusableQuestionIds[6], reusableQuestionIds[8], "isSubmitted")], + headline: t("templates.identify_sign_up_barriers_question_7_headline"), required: true, - placeholder: { default: t("templates.identify_sign_up_barriers_question_7_placeholder") }, + placeholder: t("templates.identify_sign_up_barriers_question_7_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[7], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.identify_sign_up_barriers_question_8_headline") }, + headline: t("templates.identify_sign_up_barriers_question_8_headline"), required: true, - placeholder: { default: t("templates.identify_sign_up_barriers_question_8_placeholder") }, + placeholder: t("templates.identify_sign_up_barriers_question_8_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[8], - html: { - default: t("templates.identify_sign_up_barriers_question_9_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - headline: { default: t("templates.identify_sign_up_barriers_question_9_headline") }, + html: t("templates.identify_sign_up_barriers_question_9_html"), + headline: t("templates.identify_sign_up_barriers_question_9_headline"), required: false, buttonUrl: "https://app.formbricks.com/auth/signup", - buttonLabel: { default: t("templates.identify_sign_up_barriers_question_9_button_label") }, + buttonLabel: t("templates.identify_sign_up_barriers_question_9_button_label"), buttonExternal: true, - dismissButtonLabel: { - default: t("templates.identify_sign_up_barriers_question_9_dismiss_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, - }, + dismissButtonLabel: t("templates.identify_sign_up_barriers_question_9_dismiss_button_label"), + t, + }), ], }, - }; + t + ); }; const buildProductRoadmap = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.build_product_roadmap_name"), - role: "productManager", - industries: ["saas"], - channels: ["app", "link"], - description: t("templates.build_product_roadmap_description"), - preset: { - ...localSurvey, - name: t("templates.build_product_roadmap_name_with_project_name"), + return buildSurvey( + { + name: t("templates.build_product_roadmap_name"), + role: "productManager", + industries: ["saas"], + channels: ["app", "link"], + description: t("templates.build_product_roadmap_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + buildRatingQuestion({ range: 5, scale: "number", - headline: { - default: t("templates.build_product_roadmap_question_1_headline"), - }, + headline: t("templates.build_product_roadmap_question_1_headline"), required: true, - lowerLabel: { default: t("templates.build_product_roadmap_question_1_lower_label") }, - upperLabel: { default: t("templates.build_product_roadmap_question_1_upper_label") }, + lowerLabel: t("templates.build_product_roadmap_question_1_lower_label"), + upperLabel: t("templates.build_product_roadmap_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.build_product_roadmap_question_2_headline"), - }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.build_product_roadmap_question_2_headline"), required: true, - placeholder: { default: t("templates.build_product_roadmap_question_2_placeholder") }, + placeholder: t("templates.build_product_roadmap_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const understandPurchaseIntention = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.understand_purchase_intention_name"), - role: "sales", - industries: ["eCommerce"], - channels: ["website", "link", "app"], - description: t("templates.understand_purchase_intention_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.understand_purchase_intention_name"), + role: "sales", + industries: ["eCommerce"], + channels: ["website", "link", "app"], + description: t("templates.understand_purchase_intention_description"), + endings: localSurvey.endings, questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "isLessThanOrEqual", - rightOperand: { - type: "static", - value: 2, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[1], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: 3, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: 4, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: 5, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], "2", reusableQuestionIds[1]), + createChoiceJumpLogic(reusableQuestionIds[0], "3", reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[0], "4", reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[0], "5", localSurvey.endings[0].id), ], range: 5, scale: "number", - headline: { default: t("templates.understand_purchase_intention_question_1_headline") }, + headline: t("templates.understand_purchase_intention_question_1_headline"), required: true, - lowerLabel: { default: t("templates.understand_purchase_intention_question_1_lower_label") }, - upperLabel: { default: t("templates.understand_purchase_intention_question_1_upper_label") }, + lowerLabel: t("templates.understand_purchase_intention_question_1_lower_label"), + upperLabel: t("templates.understand_purchase_intention_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "or", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted"), + createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSkipped"), ], - headline: { default: t("templates.understand_purchase_intention_question_2_headline") }, + headline: t("templates.understand_purchase_intention_question_2_headline"), required: false, - placeholder: { default: t("templates.understand_purchase_intention_question_2_placeholder") }, + placeholder: t("templates.understand_purchase_intention_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.understand_purchase_intention_question_3_headline") }, + headline: t("templates.understand_purchase_intention_question_3_headline"), required: true, - placeholder: { default: t("templates.understand_purchase_intention_question_3_placeholder") }, + placeholder: t("templates.understand_purchase_intention_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const improveNewsletterContent = (t: TFnType): TTemplate => { const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [createId(), createId(), createId()]; - return { - name: t("templates.improve_newsletter_content_name"), - role: "marketing", - industries: ["eCommerce", "saas", "other"], - channels: ["link"], - description: t("templates.improve_newsletter_content_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.improve_newsletter_content_name"), + role: "marketing", + industries: ["eCommerce", "saas", "other"], + channels: ["link"], + description: t("templates.improve_newsletter_content_description"), + endings: localSurvey.endings, questions: [ - { + buildRatingQuestion({ id: reusableQuestionIds[0], - type: TSurveyQuestionTypeEnum.Rating, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: 5, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], "5", reusableQuestionIds[2]), { id: createId(), conditions: { @@ -5528,84 +2826,43 @@ const improveNewsletterContent = (t: TFnType): TTemplate => { ], range: 5, scale: "smiley", - headline: { default: t("templates.improve_newsletter_content_question_1_headline") }, + headline: t("templates.improve_newsletter_content_question_1_headline"), required: true, - lowerLabel: { default: t("templates.improve_newsletter_content_question_1_lower_label") }, - upperLabel: { default: t("templates.improve_newsletter_content_question_1_upper_label") }, + lowerLabel: t("templates.improve_newsletter_content_question_1_lower_label"), + upperLabel: t("templates.improve_newsletter_content_question_1_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "or", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSkipped", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, + createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted"), + createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSkipped"), ], - headline: { default: t("templates.improve_newsletter_content_question_2_headline") }, + headline: t("templates.improve_newsletter_content_question_2_headline"), required: false, - placeholder: { default: t("templates.improve_newsletter_content_question_2_placeholder") }, + placeholder: t("templates.improve_newsletter_content_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[2], - html: { - default: t("templates.improve_newsletter_content_question_3_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - headline: { default: t("templates.improve_newsletter_content_question_3_headline") }, + html: t("templates.improve_newsletter_content_question_3_html"), + headline: t("templates.improve_newsletter_content_question_3_headline"), required: false, buttonUrl: "https://formbricks.com", - buttonLabel: { default: t("templates.improve_newsletter_content_question_3_button_label") }, + buttonLabel: t("templates.improve_newsletter_content_question_3_button_label"), buttonExternal: true, - dismissButtonLabel: { - default: t("templates.improve_newsletter_content_question_3_dismiss_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, - }, + dismissButtonLabel: t("templates.improve_newsletter_content_question_3_dismiss_button_label"), + t, + }), ], }, - }; + t + ); }; const evaluateAProductIdea = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); const reusableQuestionIds = [ createId(), createId(), @@ -5616,272 +2873,102 @@ const evaluateAProductIdea = (t: TFnType): TTemplate => { createId(), createId(), ]; - return { - name: t("templates.evaluate_a_product_idea_name"), - role: "productManager", - industries: ["saas", "other"], - channels: ["link", "app"], - description: t("templates.evaluate_a_product_idea_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.evaluate_a_product_idea_name"), + role: "productManager", + industries: ["saas", "other"], + channels: ["link", "app"], + description: t("templates.evaluate_a_product_idea_description"), questions: [ - { + buildCTAQuestion({ id: reusableQuestionIds[0], - html: { - default: t("templates.evaluate_a_product_idea_question_1_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - headline: { - default: t("templates.evaluate_a_product_idea_question_1_headline"), - }, + html: t("templates.evaluate_a_product_idea_question_1_html"), + headline: t("templates.evaluate_a_product_idea_question_1_headline"), required: true, - buttonLabel: { default: t("templates.evaluate_a_product_idea_question_1_button_label") }, + buttonLabel: t("templates.evaluate_a_product_idea_question_1_button_label"), buttonExternal: false, - dismissButtonLabel: { - default: t("templates.evaluate_a_product_idea_question_1_dismiss_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, - }, - { + dismissButtonLabel: t("templates.evaluate_a_product_idea_question_1_dismiss_button_label"), + t, + }), + buildRatingQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.Rating, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isLessThanOrEqual", - rightOperand: { - type: "static", - value: 3, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isGreaterThanOrEqual", - rightOperand: { - type: "static", - value: 4, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[1], "3", reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[1], "4", reusableQuestionIds[3]), ], range: 5, scale: "number", - headline: { default: t("templates.evaluate_a_product_idea_question_2_headline") }, + headline: t("templates.evaluate_a_product_idea_question_2_headline"), required: true, - lowerLabel: { default: t("templates.evaluate_a_product_idea_question_2_lower_label") }, - upperLabel: { default: t("templates.evaluate_a_product_idea_question_2_upper_label") }, + lowerLabel: t("templates.evaluate_a_product_idea_question_2_lower_label"), + upperLabel: t("templates.evaluate_a_product_idea_question_2_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.evaluate_a_product_idea_question_3_headline") }, + headline: t("templates.evaluate_a_product_idea_question_3_headline"), required: true, - placeholder: { default: t("templates.evaluate_a_product_idea_question_3_placeholder") }, + placeholder: t("templates.evaluate_a_product_idea_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildCTAQuestion({ id: reusableQuestionIds[3], - html: { - default: t("templates.evaluate_a_product_idea_question_4_html"), - }, - type: TSurveyQuestionTypeEnum.CTA, - headline: { default: t("templates.evaluate_a_product_idea_question_4_headline") }, + html: t("templates.evaluate_a_product_idea_question_4_html"), + headline: t("templates.evaluate_a_product_idea_question_4_headline"), required: true, - buttonLabel: { default: t("templates.evaluate_a_product_idea_question_4_button_label") }, + buttonLabel: t("templates.evaluate_a_product_idea_question_4_button_label"), buttonExternal: false, - dismissButtonLabel: { - default: t("templates.evaluate_a_product_idea_question_4_dismiss_button_label"), - }, - backButtonLabel: { default: t("templates.back") }, - }, - { + dismissButtonLabel: t("templates.evaluate_a_product_idea_question_4_dismiss_button_label"), + t, + }), + buildRatingQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.Rating, logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isLessThanOrEqual", - rightOperand: { - type: "static", - value: 3, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isGreaterThanOrEqual", - rightOperand: { - type: "static", - value: 4, - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[6], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[4], "3", reusableQuestionIds[5]), + createChoiceJumpLogic(reusableQuestionIds[4], "4", reusableQuestionIds[6]), ], range: 5, scale: "number", - headline: { default: t("templates.evaluate_a_product_idea_question_5_headline") }, + headline: t("templates.evaluate_a_product_idea_question_5_headline"), required: true, - lowerLabel: { default: t("templates.evaluate_a_product_idea_question_5_lower_label") }, - upperLabel: { default: t("templates.evaluate_a_product_idea_question_5_upper_label") }, + lowerLabel: t("templates.evaluate_a_product_idea_question_5_lower_label"), + upperLabel: t("templates.evaluate_a_product_idea_question_5_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[5], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[5], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[7], - }, - ], - }, - ], - headline: { default: t("templates.evaluate_a_product_idea_question_6_headline") }, + logic: [createJumpLogic(reusableQuestionIds[5], reusableQuestionIds[7], "isSubmitted")], + headline: t("templates.evaluate_a_product_idea_question_6_headline"), required: true, - placeholder: { default: t("templates.evaluate_a_product_idea_question_6_placeholder") }, + placeholder: t("templates.evaluate_a_product_idea_question_6_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[6], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.evaluate_a_product_idea_question_7_headline") }, + headline: t("templates.evaluate_a_product_idea_question_7_headline"), required: true, - placeholder: { default: t("templates.evaluate_a_product_idea_question_7_placeholder") }, + placeholder: t("templates.evaluate_a_product_idea_question_7_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[7], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.evaluate_a_product_idea_question_8_headline") }, + headline: t("templates.evaluate_a_product_idea_question_8_headline"), required: false, - placeholder: { default: t("templates.evaluate_a_product_idea_question_8_placeholder") }, + placeholder: t("templates.evaluate_a_product_idea_question_8_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const understandLowEngagement = (t: TFnType): TTemplate => { @@ -5889,994 +2976,445 @@ const understandLowEngagement = (t: TFnType): TTemplate => { const reusableQuestionIds = [createId(), createId(), createId(), createId(), createId(), createId()]; const reusableOptionIds = [createId(), createId(), createId(), createId()]; - return { - name: t("templates.understand_low_engagement_name"), - role: "productManager", - industries: ["saas"], - channels: ["link"], - description: t("templates.understand_low_engagement_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.understand_low_engagement_name"), + role: "productManager", + industries: ["saas"], + channels: ["link"], + description: t("templates.understand_low_engagement_description"), + endings: localSurvey.endings, questions: [ - { + buildMultipleChoiceQuestion({ id: reusableQuestionIds[0], type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[0], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[1], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[1], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[2], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[2], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[3], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: reusableOptionIds[3], - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[4], - }, - ], - }, - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[0], - type: "question", - }, - operator: "equals", - rightOperand: { - type: "static", - value: "other", - }, - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: reusableQuestionIds[5], - }, - ], - }, + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[0], reusableQuestionIds[1]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[1], reusableQuestionIds[2]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[2], reusableQuestionIds[3]), + createChoiceJumpLogic(reusableQuestionIds[0], reusableOptionIds[3], reusableQuestionIds[4]), + createChoiceJumpLogic(reusableQuestionIds[0], "other", reusableQuestionIds[5]), ], choices: [ - { - id: reusableOptionIds[0], - label: { default: t("templates.understand_low_engagement_question_1_choice_1") }, - }, - { - id: reusableOptionIds[1], - label: { default: t("templates.understand_low_engagement_question_1_choice_2") }, - }, - { - id: reusableOptionIds[2], - label: { default: t("templates.understand_low_engagement_question_1_choice_3") }, - }, - { - id: reusableOptionIds[3], - label: { default: t("templates.understand_low_engagement_question_1_choice_4") }, - }, - { - id: "other", - label: { default: t("templates.understand_low_engagement_question_1_choice_5") }, - }, + t("templates.understand_low_engagement_question_1_choice_1"), + t("templates.understand_low_engagement_question_1_choice_2"), + t("templates.understand_low_engagement_question_1_choice_3"), + t("templates.understand_low_engagement_question_1_choice_4"), + t("templates.understand_low_engagement_question_1_choice_5"), ], - headline: { default: t("templates.understand_low_engagement_question_1_headline") }, + headline: t("templates.understand_low_engagement_question_1_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + containsOther: true, + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[1], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[1], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.understand_low_engagement_question_2_headline") }, + logic: [createJumpLogic(reusableQuestionIds[1], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.understand_low_engagement_question_2_headline"), required: true, - placeholder: { default: t("templates.understand_low_engagement_question_2_placeholder") }, + placeholder: t("templates.understand_low_engagement_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[2], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[2], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.understand_low_engagement_question_3_headline") }, + logic: [createJumpLogic(reusableQuestionIds[2], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.understand_low_engagement_question_3_headline"), required: true, - placeholder: { default: t("templates.understand_low_engagement_question_3_placeholder") }, + placeholder: t("templates.understand_low_engagement_question_3_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[3], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[3], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.understand_low_engagement_question_4_headline") }, + logic: [createJumpLogic(reusableQuestionIds[3], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.understand_low_engagement_question_4_headline"), required: true, - placeholder: { default: t("templates.understand_low_engagement_question_4_placeholder") }, + placeholder: t("templates.understand_low_engagement_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[4], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - logic: [ - { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [ - { - id: createId(), - leftOperand: { - value: reusableQuestionIds[4], - type: "question", - }, - operator: "isSubmitted", - }, - ], - }, - actions: [ - { - id: createId(), - objective: "jumpToQuestion", - target: localSurvey.endings[0].id, - }, - ], - }, - ], - headline: { default: t("templates.understand_low_engagement_question_5_headline") }, + logic: [createJumpLogic(reusableQuestionIds[4], localSurvey.endings[0].id, "isSubmitted")], + headline: t("templates.understand_low_engagement_question_5_headline"), required: true, - placeholder: { default: t("templates.understand_low_engagement_question_5_placeholder") }, + placeholder: t("templates.understand_low_engagement_question_5_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { + t, + }), + buildOpenTextQuestion({ id: reusableQuestionIds[5], - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, logic: [], - headline: { default: t("templates.understand_low_engagement_question_6_headline") }, + headline: t("templates.understand_low_engagement_question_6_headline"), required: false, - placeholder: { default: t("templates.understand_low_engagement_question_6_placeholder") }, + placeholder: t("templates.understand_low_engagement_question_6_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const employeeWellBeing = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.employee_well_being_name"), - role: "peopleManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.employee_well_being_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.employee_well_being_name"), + role: "peopleManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.employee_well_being_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: t("templates.employee_well_being_question_1_headline") }, + buildRatingQuestion({ + headline: t("templates.employee_well_being_question_1_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.employee_well_being_question_1_lower_label"), - }, - upperLabel: { - default: t("templates.employee_well_being_question_1_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.employee_well_being_question_2_headline"), - }, + lowerLabel: t("templates.employee_well_being_question_1_lower_label"), + upperLabel: t("templates.employee_well_being_question_1_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.employee_well_being_question_2_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.employee_well_being_question_2_lower_label"), - }, - upperLabel: { - default: t("templates.employee_well_being_question_2_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { default: t("templates.employee_well_being_question_3_headline") }, + lowerLabel: t("templates.employee_well_being_question_2_lower_label"), + upperLabel: t("templates.employee_well_being_question_2_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.employee_well_being_question_3_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.employee_well_being_question_3_lower_label"), - }, - upperLabel: { - default: t("templates.employee_well_being_question_3_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.employee_well_being_question_4_headline") }, + lowerLabel: t("templates.employee_well_being_question_3_lower_label"), + upperLabel: t("templates.employee_well_being_question_3_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.employee_well_being_question_4_headline"), required: false, - placeholder: { default: t("templates.employee_well_being_question_4_placeholder") }, + placeholder: t("templates.employee_well_being_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const longTermRetentionCheckIn = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.long_term_retention_check_in_name"), - role: "peopleManager", - industries: ["saas", "other"], - channels: ["app", "link"], - description: t("templates.long_term_retention_check_in_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.long_term_retention_check_in_name"), + role: "peopleManager", + industries: ["saas", "other"], + channels: ["app", "link"], + description: t("templates.long_term_retention_check_in_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + buildRatingQuestion({ range: 5, scale: "star", - headline: { default: t("templates.long_term_retention_check_in_question_1_headline") }, + headline: t("templates.long_term_retention_check_in_question_1_headline"), required: true, - lowerLabel: { default: t("templates.long_term_retention_check_in_question_1_lower_label") }, - upperLabel: { default: t("templates.long_term_retention_check_in_question_1_upper_label") }, + lowerLabel: t("templates.long_term_retention_check_in_question_1_lower_label"), + upperLabel: t("templates.long_term_retention_check_in_question_1_upper_label"), isColorCodingEnabled: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.long_term_retention_check_in_question_2_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.long_term_retention_check_in_question_2_headline"), required: false, - placeholder: { default: t("templates.long_term_retention_check_in_question_2_placeholder") }, + placeholder: t("templates.long_term_retention_check_in_question_2_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_3_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_3_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_3_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_3_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_3_choice_5") }, - }, + t("templates.long_term_retention_check_in_question_3_choice_1"), + t("templates.long_term_retention_check_in_question_3_choice_2"), + t("templates.long_term_retention_check_in_question_3_choice_3"), + t("templates.long_term_retention_check_in_question_3_choice_4"), + t("templates.long_term_retention_check_in_question_3_choice_5"), ], - headline: { - default: t("templates.long_term_retention_check_in_question_3_headline"), - }, + headline: t("templates.long_term_retention_check_in_question_3_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "number", - headline: { default: t("templates.long_term_retention_check_in_question_4_headline") }, + headline: t("templates.long_term_retention_check_in_question_4_headline"), required: true, - lowerLabel: { default: t("templates.long_term_retention_check_in_question_4_lower_label") }, - upperLabel: { default: t("templates.long_term_retention_check_in_question_4_upper_label") }, + lowerLabel: t("templates.long_term_retention_check_in_question_4_lower_label"), + upperLabel: t("templates.long_term_retention_check_in_question_4_upper_label"), isColorCodingEnabled: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.long_term_retention_check_in_question_5_headline"), - }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.long_term_retention_check_in_question_5_headline"), required: false, - placeholder: { default: t("templates.long_term_retention_check_in_question_5_placeholder") }, + placeholder: t("templates.long_term_retention_check_in_question_5_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.NPS, - headline: { default: t("templates.long_term_retention_check_in_question_6_headline") }, + t, + }), + buildNPSQuestion({ + headline: t("templates.long_term_retention_check_in_question_6_headline"), required: false, - lowerLabel: { default: t("templates.long_term_retention_check_in_question_6_lower_label") }, - upperLabel: { default: t("templates.long_term_retention_check_in_question_6_upper_label") }, + lowerLabel: t("templates.long_term_retention_check_in_question_6_lower_label"), + upperLabel: t("templates.long_term_retention_check_in_question_6_upper_label"), isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), + t, + }), + buildMultipleChoiceQuestion({ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, shuffleOption: "none", choices: [ - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_7_choice_1") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_7_choice_2") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_7_choice_3") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_7_choice_4") }, - }, - { - id: createId(), - label: { default: t("templates.long_term_retention_check_in_question_7_choice_5") }, - }, + t("templates.long_term_retention_check_in_question_7_choice_1"), + t("templates.long_term_retention_check_in_question_7_choice_2"), + t("templates.long_term_retention_check_in_question_7_choice_3"), + t("templates.long_term_retention_check_in_question_7_choice_4"), + t("templates.long_term_retention_check_in_question_7_choice_5"), ], - headline: { default: t("templates.long_term_retention_check_in_question_7_headline") }, + headline: t("templates.long_term_retention_check_in_question_7_headline"), required: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.long_term_retention_check_in_question_8_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.long_term_retention_check_in_question_8_headline"), required: false, - placeholder: { default: t("templates.long_term_retention_check_in_question_8_placeholder") }, + placeholder: t("templates.long_term_retention_check_in_question_8_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, + t, + }), + buildRatingQuestion({ range: 5, scale: "smiley", - headline: { default: t("templates.long_term_retention_check_in_question_9_headline") }, + headline: t("templates.long_term_retention_check_in_question_9_headline"), required: true, - lowerLabel: { default: t("templates.long_term_retention_check_in_question_9_lower_label") }, - upperLabel: { default: t("templates.long_term_retention_check_in_question_9_upper_label") }, + lowerLabel: t("templates.long_term_retention_check_in_question_9_lower_label"), + upperLabel: t("templates.long_term_retention_check_in_question_9_upper_label"), isColorCodingEnabled: true, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { default: t("templates.long_term_retention_check_in_question_10_headline") }, + t, + }), + buildOpenTextQuestion({ + headline: t("templates.long_term_retention_check_in_question_10_headline"), required: false, - placeholder: { default: t("templates.long_term_retention_check_in_question_10_placeholder") }, + placeholder: t("templates.long_term_retention_check_in_question_10_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const professionalDevelopmentGrowth = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.professional_development_growth_survey_name"), - role: "peopleManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.professional_development_growth_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.professional_development_growth_survey_name"), + role: "peopleManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.professional_development_growth_survey_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.professional_development_growth_survey_question_1_headline"), - }, + buildRatingQuestion({ + headline: t("templates.professional_development_growth_survey_question_1_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.professional_development_growth_survey_question_1_lower_label"), - }, - upperLabel: { - default: t("templates.professional_development_growth_survey_question_1_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.professional_development_growth_survey_question_2_headline"), - }, + lowerLabel: t("templates.professional_development_growth_survey_question_1_lower_label"), + upperLabel: t("templates.professional_development_growth_survey_question_1_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.professional_development_growth_survey_question_2_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.professional_development_growth_survey_question_2_lower_label"), - }, - upperLabel: { - default: t("templates.professional_development_growth_survey_question_2_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.professional_development_growth_survey_question_3_headline"), - }, + lowerLabel: t("templates.professional_development_growth_survey_question_2_lower_label"), + upperLabel: t("templates.professional_development_growth_survey_question_2_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.professional_development_growth_survey_question_3_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.professional_development_growth_survey_question_3_lower_label"), - }, - upperLabel: { - default: t("templates.professional_development_growth_survey_question_3_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.professional_development_growth_survey_question_4_headline"), - }, + lowerLabel: t("templates.professional_development_growth_survey_question_3_lower_label"), + upperLabel: t("templates.professional_development_growth_survey_question_3_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.professional_development_growth_survey_question_4_headline"), required: false, - placeholder: { - default: t("templates.professional_development_growth_survey_question_4_placeholder"), - }, + placeholder: t("templates.professional_development_growth_survey_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const recognitionAndReward = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.recognition_and_reward_survey_name"), - role: "peopleManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.recognition_and_reward_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.recognition_and_reward_survey_name"), + role: "peopleManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.recognition_and_reward_survey_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.recognition_and_reward_survey_question_1_headline"), - }, + buildRatingQuestion({ + headline: t("templates.recognition_and_reward_survey_question_1_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.recognition_and_reward_survey_question_1_lower_label"), - }, - upperLabel: { - default: t("templates.recognition_and_reward_survey_question_1_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.recognition_and_reward_survey_question_2_headline"), - }, + lowerLabel: t("templates.recognition_and_reward_survey_question_1_lower_label"), + upperLabel: t("templates.recognition_and_reward_survey_question_1_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.recognition_and_reward_survey_question_2_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.recognition_and_reward_survey_question_2_lower_label"), - }, - upperLabel: { - default: t("templates.recognition_and_reward_survey_question_2_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.recognition_and_reward_survey_question_3_headline"), - }, + lowerLabel: t("templates.recognition_and_reward_survey_question_2_lower_label"), + upperLabel: t("templates.recognition_and_reward_survey_question_2_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.recognition_and_reward_survey_question_3_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.recognition_and_reward_survey_question_3_lower_label"), - }, - upperLabel: { - default: t("templates.recognition_and_reward_survey_question_3_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.recognition_and_reward_survey_question_4_headline"), - }, + lowerLabel: t("templates.recognition_and_reward_survey_question_3_lower_label"), + upperLabel: t("templates.recognition_and_reward_survey_question_3_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.recognition_and_reward_survey_question_4_headline"), required: false, - placeholder: { - default: t("templates.recognition_and_reward_survey_question_4_placeholder"), - }, + placeholder: t("templates.recognition_and_reward_survey_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + t, + }), ], }, - }; + t + ); }; const alignmentAndEngagement = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.alignment_and_engagement_survey_name"), - role: "peopleManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.alignment_and_engagement_survey_description"), - preset: { - ...localSurvey, - name: "Alignment and Engagement with Company Vision", + return buildSurvey( + { + name: t("templates.alignment_and_engagement_survey_name"), + role: "peopleManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.alignment_and_engagement_survey_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.alignment_and_engagement_survey_question_1_headline"), - }, + buildRatingQuestion({ + headline: t("templates.alignment_and_engagement_survey_question_1_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.alignment_and_engagement_survey_question_1_lower_label"), - }, - upperLabel: { - default: t("templates.alignment_and_engagement_survey_question_1_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.alignment_and_engagement_survey_question_2_headline"), - }, + lowerLabel: t("templates.alignment_and_engagement_survey_question_1_lower_label"), + upperLabel: t("templates.alignment_and_engagement_survey_question_1_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.alignment_and_engagement_survey_question_2_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.alignment_and_engagement_survey_question_2_lower_label"), - }, - upperLabel: { - default: t("templates.alignment_and_engagement_survey_question_2_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.alignment_and_engagement_survey_question_3_headline"), - }, + lowerLabel: t("templates.alignment_and_engagement_survey_question_2_lower_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.alignment_and_engagement_survey_question_3_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.alignment_and_engagement_survey_question_3_lower_label"), - }, - upperLabel: { - default: t("templates.alignment_and_engagement_survey_question_3_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.alignment_and_engagement_survey_question_4_headline"), - }, + lowerLabel: t("templates.alignment_and_engagement_survey_question_3_lower_label"), + upperLabel: t("templates.alignment_and_engagement_survey_question_3_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.alignment_and_engagement_survey_question_4_headline"), required: false, - placeholder: { - default: t("templates.alignment_and_engagement_survey_question_4_placeholder"), - }, + placeholder: t("templates.alignment_and_engagement_survey_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; const supportiveWorkCulture = (t: TFnType): TTemplate => { - const localSurvey = getDefaultSurveyPreset(t); - return { - name: t("templates.supportive_work_culture_survey_name"), - role: "peopleManager", - industries: ["saas", "eCommerce", "other"], - channels: ["link"], - description: t("templates.supportive_work_culture_survey_description"), - preset: { - ...localSurvey, + return buildSurvey( + { name: t("templates.supportive_work_culture_survey_name"), + role: "peopleManager", + industries: ["saas", "eCommerce", "other"], + channels: ["link"], + description: t("templates.supportive_work_culture_survey_description"), questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.supportive_work_culture_survey_question_1_headline"), - }, + buildRatingQuestion({ + headline: t("templates.supportive_work_culture_survey_question_1_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.supportive_work_culture_survey_question_1_lower_label"), - }, - upperLabel: { - default: t("templates.supportive_work_culture_survey_question_1_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.supportive_work_culture_survey_question_2_headline"), - }, + lowerLabel: t("templates.supportive_work_culture_survey_question_1_lower_label"), + upperLabel: t("templates.supportive_work_culture_survey_question_1_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.supportive_work_culture_survey_question_2_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.supportive_work_culture_survey_question_2_lower_label"), - }, - upperLabel: { - default: t("templates.supportive_work_culture_survey_question_2_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - headline: { - default: t("templates.supportive_work_culture_survey_question_3_headline"), - }, + lowerLabel: t("templates.supportive_work_culture_survey_question_2_lower_label"), + upperLabel: t("templates.supportive_work_culture_survey_question_2_upper_label"), + t, + }), + buildRatingQuestion({ + headline: t("templates.supportive_work_culture_survey_question_3_headline"), required: true, scale: "number", range: 10, - lowerLabel: { - default: t("templates.supportive_work_culture_survey_question_3_lower_label"), - }, - upperLabel: { - default: t("templates.supportive_work_culture_survey_question_3_upper_label"), - }, - isColorCodingEnabled: false, - buttonLabel: { default: t("templates.next") }, - backButtonLabel: { default: t("templates.back") }, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - charLimit: { - enabled: false, - }, - headline: { - default: t("templates.supportive_work_culture_survey_question_4_headline"), - }, + lowerLabel: t("templates.supportive_work_culture_survey_question_3_lower_label"), + upperLabel: t("templates.supportive_work_culture_survey_question_3_upper_label"), + t, + }), + buildOpenTextQuestion({ + headline: t("templates.supportive_work_culture_survey_question_4_headline"), required: false, - placeholder: { - default: t("templates.supportive_work_culture_survey_question_4_placeholder"), - }, + placeholder: t("templates.supportive_work_culture_survey_question_4_placeholder"), inputType: "text", - buttonLabel: { default: t("templates.finish") }, - backButtonLabel: { default: t("templates.back") }, - }, + buttonLabel: t("templates.finish"), + t, + }), ], }, - }; + t + ); }; export const templates = (t: TFnType): TTemplate[] => [ @@ -6980,51 +3518,35 @@ export const previewSurvey = (projectName: string, t: TFnType) => { segment: null, questions: [ { - id: "lbdxozwikh838yc6a8vbwuju", - type: "rating", - range: 5, - scale: "star", + ...buildRatingQuestion({ + id: "lbdxozwikh838yc6a8vbwuju", + range: 5, + scale: "star", + headline: t("templates.preview_survey_question_1_headline", { projectName }), + required: true, + subheader: t("templates.preview_survey_question_1_subheader"), + lowerLabel: t("templates.preview_survey_question_1_lower_label"), + upperLabel: t("templates.preview_survey_question_1_upper_label"), + t, + }), isDraft: true, - headline: { - default: t("templates.preview_survey_question_1_headline", { projectName }), - }, - required: true, - subheader: { - default: t("templates.preview_survey_question_1_subheader"), - }, - lowerLabel: { - default: t("templates.preview_survey_question_1_lower_label"), - }, - upperLabel: { - default: t("templates.preview_survey_question_1_upper_label"), - }, }, { - id: "rjpu42ps6dzirsn9ds6eydgt", - type: "multipleChoiceSingle", - choices: [ - { - id: "x6wty2s72v7vd538aadpurqx", - label: { - default: t("templates.preview_survey_question_2_choice_1_label"), - }, - }, - { - id: "fbcj4530t2n357ymjp2h28d6", - label: { - default: t("templates.preview_survey_question_2_choice_2_label"), - }, - }, - ], + ...buildMultipleChoiceQuestion({ + id: "rjpu42ps6dzirsn9ds6eydgt", + type: TSurveyQuestionTypeEnum.MultipleChoiceSingle, + choiceIds: ["x6wty2s72v7vd538aadpurqx", "fbcj4530t2n357ymjp2h28d6"], + choices: [ + t("templates.preview_survey_question_2_choice_1_label"), + t("templates.preview_survey_question_2_choice_2_label"), + ], + headline: t("templates.preview_survey_question_2_headline"), + backButtonLabel: t("templates.preview_survey_question_2_back_button_label"), + required: true, + shuffleOption: "none", + t, + }), isDraft: true, - headline: { - default: t("templates.preview_survey_question_2_headline"), - }, - backButtonLabel: { - default: t("templates.preview_survey_question_2_back_button_label"), - }, - required: true, - shuffleOption: "none", }, ], endings: [ diff --git a/apps/web/app/middleware/bucket.ts b/apps/web/app/middleware/bucket.ts index 3b11f583d5..d75934d6d0 100644 --- a/apps/web/app/middleware/bucket.ts +++ b/apps/web/app/middleware/bucket.ts @@ -7,7 +7,7 @@ import { SIGNUP_RATE_LIMIT, SYNC_USER_IDENTIFICATION_RATE_LIMIT, VERIFY_EMAIL_RATE_LIMIT, -} from "@formbricks/lib/constants"; +} from "@/lib/constants"; export const loginLimiter = rateLimit({ interval: LOGIN_RATE_LIMIT.interval, diff --git a/apps/web/app/middleware/rate-limit.ts b/apps/web/app/middleware/rate-limit.ts index 4c9dc467a7..a279a47760 100644 --- a/apps/web/app/middleware/rate-limit.ts +++ b/apps/web/app/middleware/rate-limit.ts @@ -1,5 +1,5 @@ +import { ENTERPRISE_LICENSE_KEY, REDIS_HTTP_URL } from "@/lib/constants"; import { LRUCache } from "lru-cache"; -import { ENTERPRISE_LICENSE_KEY, REDIS_HTTP_URL } from "@formbricks/lib/constants"; import { logger } from "@formbricks/logger"; interface Options { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 4d094ba18f..a305150a17 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,15 +1,15 @@ import ClientEnvironmentRedirect from "@/app/ClientEnvironmentRedirect"; +import { getFirstEnvironmentIdByUserId } from "@/lib/environment/service"; +import { getIsFreshInstance } from "@/lib/instance/service"; +import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; +import { getAccessFlags } from "@/lib/membership/utils"; +import { getOrganizationsByUserId } from "@/lib/organization/service"; +import { getUser } from "@/lib/user/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { ClientLogout } from "@/modules/ui/components/client-logout"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import { getFirstEnvironmentIdByUserId } from "@formbricks/lib/environment/service"; -import { getIsFreshInstance } from "@formbricks/lib/instance/service"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationsByUserId } from "@formbricks/lib/organization/service"; -import { getUser } from "@formbricks/lib/user/service"; const Page = async () => { const session: Session | null = await getServerSession(authOptions); diff --git a/apps/web/app/sentry/SentryProvider.test.tsx b/apps/web/app/sentry/SentryProvider.test.tsx index 89a44fa396..70c66b793e 100644 --- a/apps/web/app/sentry/SentryProvider.test.tsx +++ b/apps/web/app/sentry/SentryProvider.test.tsx @@ -1,6 +1,6 @@ import * as Sentry from "@sentry/nextjs"; import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { SentryProvider } from "./SentryProvider"; vi.mock("@sentry/nextjs", async () => { @@ -17,17 +17,18 @@ vi.mock("@sentry/nextjs", async () => { }; }); +const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0"; + describe("SentryProvider", () => { afterEach(() => { cleanup(); }); - it("calls Sentry.init when sentryDsn is provided", () => { - const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0"; + test("calls Sentry.init when sentryDsn is provided", () => { const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); render( - +
Test Content
); @@ -47,7 +48,7 @@ describe("SentryProvider", () => { ); }); - it("does not call Sentry.init when sentryDsn is not provided", () => { + test("does not call Sentry.init when sentryDsn is not provided", () => { const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); render( @@ -59,22 +60,32 @@ describe("SentryProvider", () => { expect(initSpy).not.toHaveBeenCalled(); }); - it("renders children", () => { - const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0"; + test("does not call Sentry.init when isEnabled is not provided", () => { + const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); + render(
Test Content
); + + expect(initSpy).not.toHaveBeenCalled(); + }); + + test("renders children", () => { + render( + +
Test Content
+
+ ); expect(screen.getByTestId("child")).toHaveTextContent("Test Content"); }); - it("processes beforeSend correctly", () => { - const sentryDsn = "https://examplePublicKey@o0.ingest.sentry.io/0"; + test("processes beforeSend correctly", () => { const initSpy = vi.spyOn(Sentry, "init").mockImplementation(() => undefined); render( - +
Test Content
); diff --git a/apps/web/app/sentry/SentryProvider.tsx b/apps/web/app/sentry/SentryProvider.tsx index cdaf6bc1ac..beb2d6c06f 100644 --- a/apps/web/app/sentry/SentryProvider.tsx +++ b/apps/web/app/sentry/SentryProvider.tsx @@ -6,11 +6,12 @@ import { useEffect } from "react"; interface SentryProviderProps { children: React.ReactNode; sentryDsn?: string; + isEnabled?: boolean; } -export const SentryProvider = ({ children, sentryDsn }: SentryProviderProps) => { +export const SentryProvider = ({ children, sentryDsn, isEnabled }: SentryProviderProps) => { useEffect(() => { - if (sentryDsn) { + if (sentryDsn && isEnabled) { Sentry.init({ dsn: sentryDsn, diff --git a/apps/web/app/setup/organization/create/actions.ts b/apps/web/app/setup/organization/create/actions.ts index 11261b081a..d53f175b05 100644 --- a/apps/web/app/setup/organization/create/actions.ts +++ b/apps/web/app/setup/organization/create/actions.ts @@ -1,11 +1,11 @@ "use server"; +import { gethasNoOrganizations } from "@/lib/instance/service"; +import { createMembership } from "@/lib/membership/service"; +import { createOrganization } from "@/lib/organization/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; import { z } from "zod"; -import { gethasNoOrganizations } from "@formbricks/lib/instance/service"; -import { createMembership } from "@formbricks/lib/membership/service"; -import { createOrganization } from "@formbricks/lib/organization/service"; import { OperationNotAllowedError } from "@formbricks/types/errors"; const ZCreateOrganizationAction = z.object({ diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx index 0e95005ffd..5baf5f1c05 100644 --- a/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx +++ b/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx @@ -1,16 +1,16 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation"; import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; +import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@/lib/constants"; +import { getEnvironment } from "@/lib/environment/service"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; +import { getResponseCountBySurveyId } from "@/lib/response/service"; +import { getSurvey, getSurveyIdByResultShareKey } from "@/lib/survey/service"; +import { getTagsByEnvironmentId } from "@/lib/tag/service"; +import { findMatchingLocale } from "@/lib/utils/locale"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { notFound } from "next/navigation"; -import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service"; -import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; type Params = Promise<{ sharingKey: string; diff --git a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx index fbd78487d4..7f1e073130 100644 --- a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx +++ b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx @@ -1,14 +1,14 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation"; import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage"; +import { DEFAULT_LOCALE, WEBAPP_URL } from "@/lib/constants"; +import { getEnvironment } from "@/lib/environment/service"; +import { getProjectByEnvironmentId } from "@/lib/project/service"; +import { getResponseCountBySurveyId } from "@/lib/response/service"; +import { getSurvey, getSurveyIdByResultShareKey } from "@/lib/survey/service"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getTranslate } from "@/tolgee/server"; import { notFound } from "next/navigation"; -import { DEFAULT_LOCALE, WEBAPP_URL } from "@formbricks/lib/constants"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; -import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; -import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service"; type Params = Promise<{ sharingKey: string; diff --git a/apps/web/app/share/[sharingKey]/actions.ts b/apps/web/app/share/[sharingKey]/actions.ts index d1fc75ed5b..4b5e8ef7aa 100644 --- a/apps/web/app/share/[sharingKey]/actions.ts +++ b/apps/web/app/share/[sharingKey]/actions.ts @@ -1,14 +1,10 @@ "use server"; +import { getResponseCountBySurveyId, getResponseFilteringValues, getResponses } from "@/lib/response/service"; +import { getSurveyIdByResultShareKey } from "@/lib/survey/service"; +import { getTagsByEnvironmentId } from "@/lib/tag/service"; import { actionClient } from "@/lib/utils/action-client"; import { z } from "zod"; -import { - getResponseCountBySurveyId, - getResponseFilteringValues, - getResponses, -} from "@formbricks/lib/response/service"; -import { getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service"; -import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; import { ZId } from "@formbricks/types/common"; import { AuthorizationError } from "@formbricks/types/errors"; import { ZResponseFilterCriteria } from "@formbricks/types/responses"; diff --git a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/delete-file.ts b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/delete-file.ts index 2e837d9233..049af32a4e 100644 --- a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/delete-file.ts +++ b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/delete-file.ts @@ -1,6 +1,6 @@ import { responses } from "@/app/lib/api/response"; -import { storageCache } from "@formbricks/lib/storage/cache"; -import { deleteFile } from "@formbricks/lib/storage/service"; +import { storageCache } from "@/lib/storage/cache"; +import { deleteFile } from "@/lib/storage/service"; import { type TAccessType } from "@formbricks/types/storage"; export const handleDeleteFile = async (environmentId: string, accessType: TAccessType, fileName: string) => { diff --git a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/get-file.ts b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/get-file.ts index cfdebe5bbb..524cca5810 100644 --- a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/get-file.ts +++ b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/lib/get-file.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { UPLOADS_DIR, isS3Configured } from "@/lib/constants"; +import { getLocalFile, getS3File } from "@/lib/storage/service"; import { notFound } from "next/navigation"; import path from "node:path"; -import { UPLOADS_DIR, isS3Configured } from "@formbricks/lib/constants"; -import { getLocalFile, getS3File } from "@formbricks/lib/storage/service"; export const getFile = async ( environmentId: string, diff --git a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts index 5a3f70ef78..f567e6de54 100644 --- a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts +++ b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts @@ -2,10 +2,10 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { handleDeleteFile } from "@/app/storage/[environmentId]/[accessType]/[fileName]/lib/delete-file"; +import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { type NextRequest } from "next/server"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { ZStorageRetrievalParams } from "@formbricks/types/storage"; import { getFile } from "./lib/get-file"; diff --git a/apps/web/instrumentation-node.ts b/apps/web/instrumentation-node.ts index 55eeac233f..ef3de4c70c 100644 --- a/apps/web/instrumentation-node.ts +++ b/apps/web/instrumentation-node.ts @@ -1,4 +1,5 @@ // instrumentation-node.ts +import { env } from "@/lib/env"; import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; import { HostMetrics } from "@opentelemetry/host-metrics"; import { registerInstrumentations } from "@opentelemetry/instrumentation"; @@ -12,7 +13,6 @@ import { processDetector, } from "@opentelemetry/resources"; import { MeterProvider } from "@opentelemetry/sdk-metrics"; -import { env } from "@formbricks/lib/env"; import { logger } from "@formbricks/logger"; const exporter = new PrometheusExporter({ diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts index e86284efd3..c470953ee3 100644 --- a/apps/web/instrumentation.ts +++ b/apps/web/instrumentation.ts @@ -1,14 +1,17 @@ -import { PROMETHEUS_ENABLED, SENTRY_DSN } from "@formbricks/lib/constants"; +import { IS_PRODUCTION, PROMETHEUS_ENABLED, SENTRY_DSN } from "@/lib/constants"; +import * as Sentry from "@sentry/nextjs"; + +export const onRequestError = Sentry.captureRequestError; // instrumentation.ts export const register = async () => { if (process.env.NEXT_RUNTIME === "nodejs" && PROMETHEUS_ENABLED) { await import("./instrumentation-node"); } - if (process.env.NEXT_RUNTIME === "nodejs" && SENTRY_DSN) { + if (process.env.NEXT_RUNTIME === "nodejs" && IS_PRODUCTION && SENTRY_DSN) { await import("./sentry.server.config"); } - if (process.env.NEXT_RUNTIME === "edge" && SENTRY_DSN) { + if (process.env.NEXT_RUNTIME === "edge" && IS_PRODUCTION && SENTRY_DSN) { await import("./sentry.edge.config"); } }; diff --git a/packages/lib/__mocks__/database.ts b/apps/web/lib/__mocks__/database.ts similarity index 100% rename from packages/lib/__mocks__/database.ts rename to apps/web/lib/__mocks__/database.ts diff --git a/packages/lib/account/service.ts b/apps/web/lib/account/service.ts similarity index 100% rename from packages/lib/account/service.ts rename to apps/web/lib/account/service.ts diff --git a/packages/lib/account/utils.ts b/apps/web/lib/account/utils.ts similarity index 100% rename from packages/lib/account/utils.ts rename to apps/web/lib/account/utils.ts diff --git a/packages/lib/actionClass/auth.ts b/apps/web/lib/actionClass/auth.ts similarity index 96% rename from packages/lib/actionClass/auth.ts rename to apps/web/lib/actionClass/auth.ts index c00f0a5579..499498de89 100644 --- a/packages/lib/actionClass/auth.ts +++ b/apps/web/lib/actionClass/auth.ts @@ -1,6 +1,6 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; import { hasUserEnvironmentAccess } from "../environment/auth"; import { validateInputs } from "../utils/validate"; import { actionClassCache } from "./cache"; diff --git a/packages/lib/actionClass/cache.ts b/apps/web/lib/actionClass/cache.ts similarity index 100% rename from packages/lib/actionClass/cache.ts rename to apps/web/lib/actionClass/cache.ts diff --git a/packages/lib/actionClass/service.ts b/apps/web/lib/actionClass/service.ts similarity index 99% rename from packages/lib/actionClass/service.ts rename to apps/web/lib/actionClass/service.ts index 50c6c87972..c0ad6073a6 100644 --- a/packages/lib/actionClass/service.ts +++ b/apps/web/lib/actionClass/service.ts @@ -1,6 +1,7 @@ "use server"; import "server-only"; +import { cache } from "@/lib/cache"; import { ActionClass, Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -8,7 +9,6 @@ import { PrismaErrorType } from "@formbricks/database/types/error"; import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/action-classes"; import { ZId, ZOptionalNumber, ZString } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { cache } from "../cache"; import { ITEMS_PER_PAGE } from "../constants"; import { surveyCache } from "../survey/cache"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/aiModels.ts b/apps/web/lib/aiModels.ts similarity index 100% rename from packages/lib/aiModels.ts rename to apps/web/lib/aiModels.ts diff --git a/packages/lib/airtable/service.ts b/apps/web/lib/airtable/service.ts similarity index 100% rename from packages/lib/airtable/service.ts rename to apps/web/lib/airtable/service.ts diff --git a/packages/lib/auth.ts b/apps/web/lib/auth.ts similarity index 100% rename from packages/lib/auth.ts rename to apps/web/lib/auth.ts diff --git a/packages/lib/cache.ts b/apps/web/lib/cache.ts similarity index 100% rename from packages/lib/cache.ts rename to apps/web/lib/cache.ts diff --git a/packages/lib/cache/segment.ts b/apps/web/lib/cache/segment.ts similarity index 100% rename from packages/lib/cache/segment.ts rename to apps/web/lib/cache/segment.ts diff --git a/packages/lib/cn.ts b/apps/web/lib/cn.ts similarity index 100% rename from packages/lib/cn.ts rename to apps/web/lib/cn.ts diff --git a/packages/lib/constants.ts b/apps/web/lib/constants.ts similarity index 99% rename from packages/lib/constants.ts rename to apps/web/lib/constants.ts index aa7da4b99d..3ea031cbb4 100644 --- a/packages/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -14,7 +14,6 @@ export const WEBAPP_URL = export const SURVEY_URL = env.SURVEY_URL; // encryption keys -export const FORMBRICKS_ENCRYPTION_KEY = env.FORMBRICKS_ENCRYPTION_KEY || undefined; export const ENCRYPTION_KEY = env.ENCRYPTION_KEY; // Other diff --git a/apps/web/lib/crypto.test.ts b/apps/web/lib/crypto.test.ts new file mode 100644 index 0000000000..6592fcf1c8 --- /dev/null +++ b/apps/web/lib/crypto.test.ts @@ -0,0 +1,59 @@ +import { createCipheriv, randomBytes } from "crypto"; +import { describe, expect, test, vi } from "vitest"; +import { + generateLocalSignedUrl, + getHash, + symmetricDecrypt, + symmetricEncrypt, + validateLocalSignedUrl, +} from "./crypto"; + +vi.mock("./constants", () => ({ ENCRYPTION_KEY: "0".repeat(32) })); + +const key = "0".repeat(32); +const plain = "hello"; + +describe("crypto", () => { + test("encrypt + decrypt roundtrip", () => { + const cipher = symmetricEncrypt(plain, key); + expect(symmetricDecrypt(cipher, key)).toBe(plain); + }); + + test("decrypt V2 GCM payload", () => { + const iv = randomBytes(16); + const bufKey = Buffer.from(key, "utf8"); + const cipher = createCipheriv("aes-256-gcm", bufKey, iv); + let enc = cipher.update(plain, "utf8", "hex"); + enc += cipher.final("hex"); + const tag = cipher.getAuthTag().toString("hex"); + const payload = `${iv.toString("hex")}:${enc}:${tag}`; + expect(symmetricDecrypt(payload, key)).toBe(plain); + }); + + test("decrypt legacy (single-colon) payload", () => { + const iv = randomBytes(16); + const cipher = createCipheriv("aes256", Buffer.from(key, "utf8"), iv); // NOSONAR typescript:S5542 // We are testing backwards compatibility + let enc = cipher.update(plain, "utf8", "hex"); + enc += cipher.final("hex"); + const legacy = `${iv.toString("hex")}:${enc}`; + expect(symmetricDecrypt(legacy, key)).toBe(plain); + }); + + test("getHash returns a non-empty string", () => { + const h = getHash("abc"); + expect(typeof h).toBe("string"); + expect(h.length).toBeGreaterThan(0); + }); + + test("signed URL generation & validation", () => { + const { uuid, timestamp, signature } = generateLocalSignedUrl("f", "e", "t"); + expect(uuid).toHaveLength(32); + expect(typeof timestamp).toBe("number"); + expect(typeof signature).toBe("string"); + expect(validateLocalSignedUrl(uuid, "f", "e", "t", timestamp, signature, key)).toBe(true); + expect(validateLocalSignedUrl(uuid, "f", "e", "t", timestamp, "bad", key)).toBe(false); + expect(validateLocalSignedUrl(uuid, "f", "e", "t", timestamp - 1000 * 60 * 6, signature, key)).toBe( + false + ); + }); +}); diff --git a/apps/web/lib/crypto.ts b/apps/web/lib/crypto.ts new file mode 100644 index 0000000000..bc46509e4b --- /dev/null +++ b/apps/web/lib/crypto.ts @@ -0,0 +1,130 @@ +import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "crypto"; +import { logger } from "@formbricks/logger"; +import { ENCRYPTION_KEY } from "./constants"; + +const ALGORITHM_V1 = "aes256"; +const ALGORITHM_V2 = "aes-256-gcm"; +const INPUT_ENCODING = "utf8"; +const OUTPUT_ENCODING = "hex"; +const BUFFER_ENCODING = ENCRYPTION_KEY.length === 32 ? "latin1" : "hex"; +const IV_LENGTH = 16; // AES blocksize + +/** + * + * @param text Value to be encrypted + * @param key Key used to encrypt value must be 32 bytes for AES256 encryption algorithm + * + * @returns Encrypted value using key + */ +export const symmetricEncrypt = (text: string, key: string) => { + const _key = Buffer.from(key, BUFFER_ENCODING); + const iv = randomBytes(IV_LENGTH); + const cipher = createCipheriv(ALGORITHM_V2, _key, iv); + let ciphered = cipher.update(text, INPUT_ENCODING, OUTPUT_ENCODING); + ciphered += cipher.final(OUTPUT_ENCODING); + const tag = cipher.getAuthTag().toString(OUTPUT_ENCODING); + return `${iv.toString(OUTPUT_ENCODING)}:${ciphered}:${tag}`; +}; + +/** + * + * @param text Value to decrypt + * @param key Key used to decrypt value must be 32 bytes for AES256 encryption algorithm + */ + +const symmetricDecryptV1 = (text: string, key: string): string => { + const _key = Buffer.from(key, BUFFER_ENCODING); + + const components = text.split(":"); + const iv_from_ciphertext = Buffer.from(components.shift() ?? "", OUTPUT_ENCODING); + const decipher = createDecipheriv(ALGORITHM_V1, _key, iv_from_ciphertext); + let deciphered = decipher.update(components.join(":"), OUTPUT_ENCODING, INPUT_ENCODING); + deciphered += decipher.final(INPUT_ENCODING); + + return deciphered; +}; + +/** + * + * @param text Value to decrypt + * @param key Key used to decrypt value must be 32 bytes for AES256 encryption algorithm + */ + +const symmetricDecryptV2 = (text: string, key: string): string => { + // split into [ivHex, encryptedHex, tagHex] + const [ivHex, encryptedHex, tagHex] = text.split(":"); + const _key = Buffer.from(key, BUFFER_ENCODING); + const iv = Buffer.from(ivHex, OUTPUT_ENCODING); + const decipher = createDecipheriv(ALGORITHM_V2, _key, iv); + decipher.setAuthTag(Buffer.from(tagHex, OUTPUT_ENCODING)); + let decrypted = decipher.update(encryptedHex, OUTPUT_ENCODING, INPUT_ENCODING); + decrypted += decipher.final(INPUT_ENCODING); + return decrypted; +}; + +/** + * Decrypts an encrypted payload, automatically handling multiple encryption versions. + * + * If the payload contains exactly one “:”, it is treated as a legacy V1 format + * and `symmetricDecryptV1` is invoked. Otherwise, it attempts a V2 GCM decryption + * via `symmetricDecryptV2`, falling back to V1 on failure (e.g., authentication + * errors or bad formats). + * + * @param payload - The encrypted string to decrypt. + * @param key - The secret key used for decryption. + * @returns The decrypted plaintext. + */ + +export function symmetricDecrypt(payload: string, key: string): string { + // If it's clearly V1 (only one “:”), skip straight to V1 + if (payload.split(":").length === 2) { + return symmetricDecryptV1(payload, key); + } + + // Otherwise try GCM first, then fall back to CBC + try { + return symmetricDecryptV2(payload, key); + } catch (err) { + logger.warn("AES-GCM decryption failed; refusing to fall back to insecure CBC", err); + + throw err; + } +} + +export const getHash = (key: string): string => createHash("sha256").update(key).digest("hex"); + +export const generateLocalSignedUrl = ( + fileName: string, + environmentId: string, + fileType: string +): { signature: string; uuid: string; timestamp: number } => { + const uuid = randomBytes(16).toString("hex"); + const timestamp = Date.now(); + const data = `${uuid}:${fileName}:${environmentId}:${fileType}:${timestamp}`; + const signature = createHmac("sha256", ENCRYPTION_KEY).update(data).digest("hex"); + return { signature, uuid, timestamp }; +}; + +export const validateLocalSignedUrl = ( + uuid: string, + fileName: string, + environmentId: string, + fileType: string, + timestamp: number, + signature: string, + secret: string +): boolean => { + const data = `${uuid}:${fileName}:${environmentId}:${fileType}:${timestamp}`; + const expectedSignature = createHmac("sha256", secret).update(data).digest("hex"); + + if (expectedSignature !== signature) { + return false; + } + + // valid for 5 minutes + if (Date.now() - timestamp > 1000 * 60 * 5) { + return false; + } + + return true; +}; diff --git a/packages/lib/display/cache.ts b/apps/web/lib/display/cache.ts similarity index 100% rename from packages/lib/display/cache.ts rename to apps/web/lib/display/cache.ts diff --git a/packages/lib/display/service.ts b/apps/web/lib/display/service.ts similarity index 100% rename from packages/lib/display/service.ts rename to apps/web/lib/display/service.ts diff --git a/packages/lib/display/tests/__mocks__/data.mock.ts b/apps/web/lib/display/tests/__mocks__/data.mock.ts similarity index 100% rename from packages/lib/display/tests/__mocks__/data.mock.ts rename to apps/web/lib/display/tests/__mocks__/data.mock.ts diff --git a/packages/lib/display/tests/display.test.ts b/apps/web/lib/display/tests/display.test.ts similarity index 79% rename from packages/lib/display/tests/display.test.ts rename to apps/web/lib/display/tests/display.test.ts index bf3b15a279..20913c988d 100644 --- a/packages/lib/display/tests/display.test.ts +++ b/apps/web/lib/display/tests/display.test.ts @@ -1,4 +1,3 @@ -import { prisma } from "../../__mocks__/database"; import { mockContact } from "../../response/tests/__mocks__/data.mock"; import { mockDisplay, @@ -7,12 +6,13 @@ import { mockDisplayWithPersonId, mockEnvironment, } from "./__mocks__/data.mock"; +import { prisma } from "@/lib/__mocks__/database"; +import { createDisplay } from "@/app/api/v1/client/[environmentId]/displays/lib/display"; import { Prisma } from "@prisma/client"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { testInputValidation } from "vitestSetup"; import { PrismaErrorType } from "@formbricks/database/types/error"; import { DatabaseError } from "@formbricks/types/errors"; -import { createDisplay } from "../../../../apps/web/app/api/v1/client/[environmentId]/displays/lib/display"; import { deleteDisplay } from "../service"; beforeEach(() => { @@ -30,7 +30,7 @@ beforeEach(() => { describe("Tests for createDisplay service", () => { describe("Happy Path", () => { - it("Creates a new display when a userId exists", async () => { + test("Creates a new display when a userId exists", async () => { prisma.environment.findUnique.mockResolvedValue(mockEnvironment); prisma.display.create.mockResolvedValue(mockDisplayWithPersonId); @@ -38,7 +38,7 @@ describe("Tests for createDisplay service", () => { expect(display).toEqual(mockDisplayWithPersonId); }); - it("Creates a new display when a userId does not exists", async () => { + test("Creates a new display when a userId does not exists", async () => { prisma.display.create.mockResolvedValue(mockDisplay); const display = await createDisplay(mockDisplayInput); @@ -49,7 +49,7 @@ describe("Tests for createDisplay service", () => { describe("Sad Path", () => { testInputValidation(createDisplay, "123"); - it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => { const mockErrorMessage = "Mock error message"; prisma.environment.findUnique.mockResolvedValue(mockEnvironment); const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { @@ -62,7 +62,7 @@ describe("Tests for createDisplay service", () => { await expect(createDisplay(mockDisplayInputWithUserId)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for other exceptions", async () => { + test("Throws a generic Error for other exceptions", async () => { const mockErrorMessage = "Mock error message"; prisma.display.create.mockRejectedValue(new Error(mockErrorMessage)); @@ -73,7 +73,7 @@ describe("Tests for createDisplay service", () => { describe("Tests for delete display service", () => { describe("Happy Path", () => { - it("Deletes a display", async () => { + test("Deletes a display", async () => { prisma.display.delete.mockResolvedValue(mockDisplay); const display = await deleteDisplay(mockDisplay.id); @@ -81,7 +81,7 @@ describe("Tests for delete display service", () => { }); }); describe("Sad Path", () => { - it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -93,7 +93,7 @@ describe("Tests for delete display service", () => { await expect(deleteDisplay(mockDisplay.id)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for other exceptions", async () => { + test("Throws a generic Error for other exceptions", async () => { const mockErrorMessage = "Mock error message"; prisma.display.delete.mockRejectedValue(new Error(mockErrorMessage)); diff --git a/packages/lib/env.d.ts b/apps/web/lib/env.d.ts similarity index 100% rename from packages/lib/env.d.ts rename to apps/web/lib/env.d.ts diff --git a/packages/lib/env.ts b/apps/web/lib/env.ts similarity index 98% rename from packages/lib/env.ts rename to apps/web/lib/env.ts index d8017affa7..f3037d1dd8 100644 --- a/packages/lib/env.ts +++ b/apps/web/lib/env.ts @@ -30,7 +30,6 @@ export const env = createEnv({ EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(), ENCRYPTION_KEY: z.string(), ENTERPRISE_LICENSE_KEY: z.string().optional(), - FORMBRICKS_ENCRYPTION_KEY: z.string().length(24).or(z.string().length(0)).optional(), FORMBRICKS_API_HOST: z .string() .url() @@ -155,7 +154,6 @@ export const env = createEnv({ EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY, - FORMBRICKS_ENCRYPTION_KEY: process.env.FORMBRICKS_ENCRYPTION_KEY, FORMBRICKS_API_HOST: process.env.FORMBRICKS_API_HOST, FORMBRICKS_ENVIRONMENT_ID: process.env.FORMBRICKS_ENVIRONMENT_ID, GITHUB_ID: process.env.GITHUB_ID, diff --git a/packages/lib/environment/auth.ts b/apps/web/lib/environment/auth.ts similarity index 100% rename from packages/lib/environment/auth.ts rename to apps/web/lib/environment/auth.ts diff --git a/packages/lib/environment/cache.ts b/apps/web/lib/environment/cache.ts similarity index 100% rename from packages/lib/environment/cache.ts rename to apps/web/lib/environment/cache.ts diff --git a/packages/lib/environment/service.ts b/apps/web/lib/environment/service.ts similarity index 100% rename from packages/lib/environment/service.ts rename to apps/web/lib/environment/service.ts diff --git a/packages/lib/fetcher.ts b/apps/web/lib/fetcher.ts similarity index 100% rename from packages/lib/fetcher.ts rename to apps/web/lib/fetcher.ts diff --git a/packages/lib/getSurveyUrl.ts b/apps/web/lib/getSurveyUrl.ts similarity index 100% rename from packages/lib/getSurveyUrl.ts rename to apps/web/lib/getSurveyUrl.ts diff --git a/packages/lib/googleSheet/service.ts b/apps/web/lib/googleSheet/service.ts similarity index 96% rename from packages/lib/googleSheet/service.ts rename to apps/web/lib/googleSheet/service.ts index b9ff601158..b927e6f134 100644 --- a/packages/lib/googleSheet/service.ts +++ b/apps/web/lib/googleSheet/service.ts @@ -1,4 +1,11 @@ import "server-only"; +import { + GOOGLE_SHEETS_CLIENT_ID, + GOOGLE_SHEETS_CLIENT_SECRET, + GOOGLE_SHEETS_REDIRECT_URL, +} from "@/lib/constants"; +import { GOOGLE_SHEET_MESSAGE_LIMIT } from "@/lib/constants"; +import { createOrUpdateIntegration } from "@/lib/integration/service"; import { Prisma } from "@prisma/client"; import { z } from "zod"; import { ZString } from "@formbricks/types/common"; @@ -7,13 +14,6 @@ import { TIntegrationGoogleSheets, ZIntegrationGoogleSheets, } from "@formbricks/types/integration/google-sheet"; -import { - GOOGLE_SHEETS_CLIENT_ID, - GOOGLE_SHEETS_CLIENT_SECRET, - GOOGLE_SHEETS_REDIRECT_URL, -} from "../constants"; -import { GOOGLE_SHEET_MESSAGE_LIMIT } from "../constants"; -import { createOrUpdateIntegration } from "../integration/service"; import { truncateText } from "../utils/strings"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/hashString.ts b/apps/web/lib/hashString.ts similarity index 100% rename from packages/lib/hashString.ts rename to apps/web/lib/hashString.ts diff --git a/packages/lib/i18n/i18n.mock.ts b/apps/web/lib/i18n/i18n.mock.ts similarity index 98% rename from packages/lib/i18n/i18n.mock.ts rename to apps/web/lib/i18n/i18n.mock.ts index ef813b5e18..af22ba4fa7 100644 --- a/packages/lib/i18n/i18n.mock.ts +++ b/apps/web/lib/i18n/i18n.mock.ts @@ -1,4 +1,4 @@ -import { mockSurveyLanguages } from "survey/tests/__mock__/survey.mock"; +import { mockSurveyLanguages } from "@/lib/survey/tests/__mock__/survey.mock"; import { TSurvey, TSurveyCTAQuestion, @@ -44,6 +44,11 @@ export const mockOpenTextQuestion: TSurveyOpenTextQuestion = { placeholder: { default: "Type your answer here...", }, + charLimit: { + min: 0, + max: 1000, + enabled: true, + }, }; export const mockSingleSelectQuestion: TSurveyMultipleChoiceQuestion = { diff --git a/packages/lib/i18n/i18n.test.ts b/apps/web/lib/i18n/i18n.test.ts similarity index 68% rename from packages/lib/i18n/i18n.test.ts rename to apps/web/lib/i18n/i18n.test.ts index 28f19b4336..3ea1f46779 100644 --- a/packages/lib/i18n/i18n.test.ts +++ b/apps/web/lib/i18n/i18n.test.ts @@ -1,18 +1,18 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { createI18nString } from "./utils"; describe("createI18nString", () => { - it("should create an i18n string from a regular string", () => { + test("should create an i18n string from a regular string", () => { const result = createI18nString("Hello", ["default"]); expect(result).toEqual({ default: "Hello" }); }); - it("should create a new i18n string with i18n enabled from a previous i18n string", () => { + test("should create a new i18n string with i18n enabled from a previous i18n string", () => { const result = createI18nString({ default: "Hello" }, ["default", "es"]); expect(result).toEqual({ default: "Hello", es: "" }); }); - it("should add a new field key value pair when a new language is added", () => { + test("should add a new field key value pair when a new language is added", () => { const i18nObject = { default: "Hello", es: "Hola" }; const newLanguages = ["default", "es", "de"]; const result = createI18nString(i18nObject, newLanguages); @@ -23,7 +23,7 @@ describe("createI18nString", () => { }); }); - it("should remove the translation that are not present in newLanguages", () => { + test("should remove the translation that are not present in newLanguages", () => { const i18nObject = { default: "Hello", es: "hola" }; const newLanguages = ["default"]; const result = createI18nString(i18nObject, newLanguages); diff --git a/packages/lib/i18n/reverseTranslation.ts b/apps/web/lib/i18n/reverseTranslation.ts similarity index 94% rename from packages/lib/i18n/reverseTranslation.ts rename to apps/web/lib/i18n/reverseTranslation.ts index 0a5502202c..f4f33b3bdc 100644 --- a/packages/lib/i18n/reverseTranslation.ts +++ b/apps/web/lib/i18n/reverseTranslation.ts @@ -1,6 +1,6 @@ import "server-only"; +import { structuredClone } from "@/lib/pollyfills/structuredClone"; import { TI18nString } from "@formbricks/types/surveys/types"; -import { structuredClone } from "../pollyfills/structuredClone"; import { isI18nObject } from "./utils"; // Helper function to extract a regular string from an i18nString. diff --git a/apps/web/lib/i18n/utils.ts b/apps/web/lib/i18n/utils.ts new file mode 100644 index 0000000000..e8575bc388 --- /dev/null +++ b/apps/web/lib/i18n/utils.ts @@ -0,0 +1,195 @@ +import { structuredClone } from "@/lib/pollyfills/structuredClone"; +import { iso639Languages } from "@formbricks/i18n-utils/src/utils"; +import { TLanguage } from "@formbricks/types/project"; +import { TI18nString, TSurveyLanguage } from "@formbricks/types/surveys/types"; + +// Helper function to create an i18nString from a regular string. +export const createI18nString = ( + text: string | TI18nString, + languages: string[], + targetLanguageCode?: string +): TI18nString => { + if (typeof text === "object") { + // It's already an i18n object, so clone it + const i18nString: TI18nString = structuredClone(text); + // Add new language keys with empty strings if they don't exist + languages?.forEach((language) => { + if (!(language in i18nString)) { + i18nString[language] = ""; + } + }); + + // Remove language keys that are not in the languages array + Object.keys(i18nString).forEach((key) => { + if (key !== (targetLanguageCode ?? "default") && languages && !languages.includes(key)) { + delete i18nString[key]; + } + }); + + return i18nString; + } else { + // It's a regular string, so create a new i18n object + const i18nString: any = { + [targetLanguageCode ?? "default"]: text as string, // Type assertion to assure TypeScript `text` is a string + }; + + // Initialize all provided languages with empty strings + languages?.forEach((language) => { + if (language !== (targetLanguageCode ?? "default")) { + i18nString[language] = ""; + } + }); + + return i18nString; + } +}; + +// Type guard to check if an object is an I18nString +export const isI18nObject = (obj: any): obj is TI18nString => { + return typeof obj === "object" && obj !== null && Object.keys(obj).includes("default"); +}; + +export const isLabelValidForAllLanguages = (label: TI18nString, languages: string[]): boolean => { + return languages.every((language) => label[language] && label[language].trim() !== ""); +}; + +export const getLocalizedValue = (value: TI18nString | undefined, languageId: string): string => { + if (!value) { + return ""; + } + if (isI18nObject(value)) { + if (value[languageId]) { + return value[languageId]; + } + return ""; + } + return ""; +}; + +export const extractLanguageCodes = (surveyLanguages: TSurveyLanguage[]): string[] => { + if (!surveyLanguages) return []; + return surveyLanguages.map((surveyLanguage) => + surveyLanguage.default ? "default" : surveyLanguage.language.code + ); +}; + +export const getEnabledLanguages = (surveyLanguages: TSurveyLanguage[]) => { + return surveyLanguages.filter((surveyLanguage) => surveyLanguage.enabled); +}; + +export const extractLanguageIds = (languages: TLanguage[]): string[] => { + return languages.map((language) => language.code); +}; + +export const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => { + if (!surveyLanguages?.length || !languageCode) return "default"; + const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode); + return language?.default ? "default" : language?.language.code || "default"; +}; + +export const iso639Identifiers = iso639Languages.map((language) => language.alpha2); + +// Helper function to add language keys to a multi-language object (e.g. survey or question) +// Iterates over the object recursively and adds empty strings for new language keys +export const addMultiLanguageLabels = (object: any, languageSymbols: string[]): any => { + // Helper function to add language keys to a multi-language object + function addLanguageKeys(obj: { default: string; [key: string]: string }) { + languageSymbols.forEach((lang) => { + if (!obj.hasOwnProperty(lang)) { + obj[lang] = ""; // Add empty string for new language keys + } + }); + } + + // Recursive function to process an object or array + function processObject(obj: any) { + if (Array.isArray(obj)) { + obj.forEach((item) => processObject(item)); + } else if (obj && typeof obj === "object") { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === "default" && typeof obj[key] === "string") { + addLanguageKeys(obj); + } else { + processObject(obj[key]); + } + } + } + } + } + + // Start processing the question object + processObject(object); + + return object; +}; + +export const appLanguages = [ + { + code: "en-US", + label: { + "en-US": "English (US)", + "de-DE": "Englisch (US)", + "pt-BR": "Inglês (EUA)", + "fr-FR": "Anglais (États-Unis)", + "zh-Hant-TW": "英文 (美國)", + "pt-PT": "Inglês (EUA)", + }, + }, + { + code: "de-DE", + label: { + "en-US": "German", + "de-DE": "Deutsch", + "pt-BR": "Alemão", + "fr-FR": "Allemand", + "zh-Hant-TW": "德語", + "pt-PT": "Alemão", + }, + }, + { + code: "pt-BR", + label: { + "en-US": "Portuguese (Brazil)", + "de-DE": "Portugiesisch (Brasilien)", + "pt-BR": "Português (Brasil)", + "fr-FR": "Portugais (Brésil)", + "zh-Hant-TW": "葡萄牙語 (巴西)", + "pt-PT": "Português (Brasil)", + }, + }, + { + code: "fr-FR", + label: { + "en-US": "French", + "de-DE": "Französisch", + "pt-BR": "Francês", + "fr-FR": "Français", + "zh-Hant-TW": "法語", + "pt-PT": "Francês", + }, + }, + { + code: "zh-Hant-TW", + label: { + "en-US": "Chinese (Traditional)", + "de-DE": "Chinesisch (Traditionell)", + "pt-BR": "Chinês (Tradicional)", + "fr-FR": "Chinois (Traditionnel)", + "zh-Hant-TW": "繁體中文", + "pt-PT": "Chinês (Tradicional)", + }, + }, + { + code: "pt-PT", + label: { + "en-US": "Portuguese (Portugal)", + "de-DE": "Portugiesisch (Portugal)", + "pt-BR": "Português (Portugal)", + "fr-FR": "Portugais (Portugal)", + "zh-Hant-TW": "葡萄牙語 (葡萄牙)", + "pt-PT": "Português (Portugal)", + }, + }, +]; +export { iso639Languages }; diff --git a/packages/lib/instance/service.ts b/apps/web/lib/instance/service.ts similarity index 90% rename from packages/lib/instance/service.ts rename to apps/web/lib/instance/service.ts index 57e1512b40..e6a6730c70 100644 --- a/packages/lib/instance/service.ts +++ b/apps/web/lib/instance/service.ts @@ -1,11 +1,11 @@ import "server-only"; +import { cache } from "@/lib/cache"; +import { organizationCache } from "@/lib/organization/cache"; +import { userCache } from "@/lib/user/cache"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { DatabaseError } from "@formbricks/types/errors"; -import { cache } from "../cache"; -import { organizationCache } from "../organization/cache"; -import { userCache } from "../user/cache"; // Function to check if there are any users in the database export const getIsFreshInstance = reactCache( diff --git a/packages/lib/integration/auth.ts b/apps/web/lib/integration/auth.ts similarity index 100% rename from packages/lib/integration/auth.ts rename to apps/web/lib/integration/auth.ts diff --git a/packages/lib/integration/cache.ts b/apps/web/lib/integration/cache.ts similarity index 100% rename from packages/lib/integration/cache.ts rename to apps/web/lib/integration/cache.ts diff --git a/packages/lib/integration/service.ts b/apps/web/lib/integration/service.ts similarity index 100% rename from packages/lib/integration/service.ts rename to apps/web/lib/integration/service.ts diff --git a/packages/lib/jwt.ts b/apps/web/lib/jwt.ts similarity index 97% rename from packages/lib/jwt.ts rename to apps/web/lib/jwt.ts index 07af577b13..bff3289440 100644 --- a/packages/lib/jwt.ts +++ b/apps/web/lib/jwt.ts @@ -1,8 +1,8 @@ +import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto"; +import { env } from "@/lib/env"; import jwt, { JwtPayload } from "jsonwebtoken"; import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; -import { symmetricDecrypt, symmetricEncrypt } from "./crypto"; -import { env } from "./env"; export const createToken = (userId: string, userEmail: string, options = {}): string => { if (!env.ENCRYPTION_KEY) { diff --git a/packages/lib/language/service.ts b/apps/web/lib/language/service.ts similarity index 100% rename from packages/lib/language/service.ts rename to apps/web/lib/language/service.ts diff --git a/packages/lib/language/tests/__mocks__/data.mock.ts b/apps/web/lib/language/tests/__mocks__/data.mock.ts similarity index 100% rename from packages/lib/language/tests/__mocks__/data.mock.ts rename to apps/web/lib/language/tests/__mocks__/data.mock.ts diff --git a/apps/web/lib/language/tests/language.test.ts b/apps/web/lib/language/tests/language.test.ts new file mode 100644 index 0000000000..c02cb6b885 --- /dev/null +++ b/apps/web/lib/language/tests/language.test.ts @@ -0,0 +1,143 @@ +import { + mockLanguage, + mockLanguageId, + mockLanguageInput, + mockLanguageUpdate, + mockProjectId, + mockUpdatedLanguage, +} from "./__mocks__/data.mock"; +import { projectCache } from "@/lib/project/cache"; +import { getProject } from "@/lib/project/service"; +import { surveyCache } from "@/lib/survey/cache"; +import { Prisma } from "@prisma/client"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { DatabaseError, ValidationError } from "@formbricks/types/errors"; +import { TProject } from "@formbricks/types/project"; +import { createLanguage, deleteLanguage, updateLanguage } from "../service"; + +vi.mock("@formbricks/database", () => ({ + prisma: { + language: { + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + }, +})); + +// stub out project/service and caches +vi.mock("@/lib/project/service", () => ({ + getProject: vi.fn(), +})); +vi.mock("@/lib/project/cache", () => ({ + projectCache: { revalidate: vi.fn() }, +})); +vi.mock("@/lib/survey/cache", () => ({ + surveyCache: { revalidate: vi.fn() }, +})); + +const fakeProject = { + id: mockProjectId, + environments: [{ id: "env1" }, { id: "env2" }], +} as TProject; + +const testInputValidation = async ( + service: (projectId: string, ...functionArgs: any[]) => Promise, + ...args: any[] +): Promise => { + test("throws ValidationError on bad input", async () => { + await expect(service(...args)).rejects.toThrow(ValidationError); + }); +}; + +describe("createLanguage", () => { + beforeEach(() => { + vi.mocked(getProject).mockResolvedValue(fakeProject); + }); + + test("happy path creates a new Language", async () => { + vi.mocked(prisma.language.create).mockResolvedValue(mockLanguage); + const result = await createLanguage(mockProjectId, mockLanguageInput); + expect(result).toEqual(mockLanguage); + // projectCache.revalidate called for each env + expect(projectCache.revalidate).toHaveBeenCalledTimes(2); + }); + + describe("sad path", () => { + testInputValidation(createLanguage, "bad-id", {}); + + test("throws DatabaseError when PrismaKnownRequestError", async () => { + const err = new Prisma.PrismaClientKnownRequestError("dup", { + code: "P2002", + clientVersion: "1", + }); + vi.mocked(prisma.language.create).mockRejectedValue(err); + await expect(createLanguage(mockProjectId, mockLanguageInput)).rejects.toThrow(DatabaseError); + }); + }); +}); + +describe("updateLanguage", () => { + beforeEach(() => { + vi.mocked(getProject).mockResolvedValue(fakeProject); + }); + + test("happy path updates a language", async () => { + const mockUpdatedLanguageWithSurveyLanguage = { + ...mockUpdatedLanguage, + surveyLanguages: [ + { + id: "surveyLanguageId", + }, + ], + }; + vi.mocked(prisma.language.update).mockResolvedValue(mockUpdatedLanguageWithSurveyLanguage); + const result = await updateLanguage(mockProjectId, mockLanguageId, mockLanguageUpdate); + expect(result).toEqual(mockUpdatedLanguage); + // caches revalidated + expect(projectCache.revalidate).toHaveBeenCalled(); + expect(surveyCache.revalidate).toHaveBeenCalled(); + }); + + describe("sad path", () => { + testInputValidation(updateLanguage, "bad-id", mockLanguageId, {}); + + test("throws DatabaseError on PrismaKnownRequestError", async () => { + const err = new Prisma.PrismaClientKnownRequestError("dup", { + code: "P2002", + clientVersion: "1", + }); + vi.mocked(prisma.language.update).mockRejectedValue(err); + await expect(updateLanguage(mockProjectId, mockLanguageId, mockLanguageUpdate)).rejects.toThrow( + DatabaseError + ); + }); + }); +}); + +describe("deleteLanguage", () => { + beforeEach(() => { + vi.mocked(getProject).mockResolvedValue(fakeProject); + }); + + test("happy path deletes a language", async () => { + vi.mocked(prisma.language.delete).mockResolvedValue(mockLanguage); + const result = await deleteLanguage(mockLanguageId, mockProjectId); + expect(result).toEqual(mockLanguage); + expect(projectCache.revalidate).toHaveBeenCalledTimes(2); + }); + + describe("sad path", () => { + testInputValidation(deleteLanguage, "bad-id", mockProjectId); + + test("throws DatabaseError on PrismaKnownRequestError", async () => { + const err = new Prisma.PrismaClientKnownRequestError("dup", { + code: "P2002", + clientVersion: "1", + }); + vi.mocked(prisma.language.delete).mockRejectedValue(err); + await expect(deleteLanguage(mockLanguageId, mockProjectId)).rejects.toThrow(DatabaseError); + }); + }); +}); diff --git a/packages/lib/localStorage.ts b/apps/web/lib/localStorage.ts similarity index 100% rename from packages/lib/localStorage.ts rename to apps/web/lib/localStorage.ts diff --git a/packages/lib/markdownIt.ts b/apps/web/lib/markdownIt.ts similarity index 100% rename from packages/lib/markdownIt.ts rename to apps/web/lib/markdownIt.ts diff --git a/packages/lib/membership/cache.ts b/apps/web/lib/membership/cache.ts similarity index 100% rename from packages/lib/membership/cache.ts rename to apps/web/lib/membership/cache.ts diff --git a/packages/lib/membership/hooks/actions.ts b/apps/web/lib/membership/hooks/actions.ts similarity index 100% rename from packages/lib/membership/hooks/actions.ts rename to apps/web/lib/membership/hooks/actions.ts diff --git a/packages/lib/membership/hooks/useMembershipRole.tsx b/apps/web/lib/membership/hooks/useMembershipRole.tsx similarity index 100% rename from packages/lib/membership/hooks/useMembershipRole.tsx rename to apps/web/lib/membership/hooks/useMembershipRole.tsx diff --git a/packages/lib/membership/service.ts b/apps/web/lib/membership/service.ts similarity index 100% rename from packages/lib/membership/service.ts rename to apps/web/lib/membership/service.ts diff --git a/packages/lib/membership/utils.ts b/apps/web/lib/membership/utils.ts similarity index 100% rename from packages/lib/membership/utils.ts rename to apps/web/lib/membership/utils.ts diff --git a/apps/web/lib/messages/de-DE.json b/apps/web/lib/messages/de-DE.json new file mode 100644 index 0000000000..6930448e1c --- /dev/null +++ b/apps/web/lib/messages/de-DE.json @@ -0,0 +1,2845 @@ +{ + "auth": { + "continue_with_azure": "Login mit Azure", + "continue_with_email": "Login mit E-Mail", + "continue_with_github": "Login mit GitHub", + "continue_with_google": "Login mit Google", + "continue_with_oidc": "Weiter mit {oidcDisplayName}", + "continue_with_openid": "Login mit OpenID", + "continue_with_saml": "Login mit SAML SSO", + "forgot-password": { + "back_to_login": "Zurück zum Login", + "email-sent": { + "heading": "Passwort erfolgreich angefordert", + "text": "Wenn ein Konto mit dieser E-Mail-Adresse existiert, erhälst du in Kürze Anweisungen zum Zurücksetzen deines Passworts." + }, + "reset": { + "confirm_password": "Passwort bestätigen", + "new_password": "Neues Passwort", + "no_token_provided": "Kein Token bereitgestellt", + "passwords_do_not_match": "Passwörter stimmen nicht überein", + "success": { + "heading": "Passwort erfolgreich zurückgesetzt", + "text": "Du kannst Dich jetzt mit deinem neuen Passwort einloggen" + } + }, + "reset_password": "Passwort zurücksetzen" + }, + "invite": { + "create_account": "Konto erstellen", + "email_does_not_match": "Ooops! Falsche E-Mail-Adresse \uD83E\uDD26", + "email_does_not_match_description": "Die E-Mail-Adresse aus der Einladung stimmt nicht mit der E-Mail-Adresse deines Kontos überein.", + "go_to_app": "Zur App gehen", + "happy_to_have_you": "Schön, dass Du da bist \uD83E\uDD17", + "happy_to_have_you_description": "Bitte erstelle einen Account oder logge Dich ein.", + "invite_expired": "Einladung abgelaufen \uD83D\uDE25", + "invite_expired_description": "Einladungen sind 7 Tage gültig. Bitte fordere eine neue Einladung an.", + "invite_not_found": "Einladung nicht gefunden \uD83D\uDE25", + "invite_not_found_description": "Der Einladungscode kann nicht gefunden werden oder wurde bereits verwendet.", + "login": "Anmelden", + "welcome_to_organization": "Du bist dabei \uD83C\uDF89", + "welcome_to_organization_description": "Willkommen in der Organisation." + }, + "last_used": "Last used", + "login": { + "backup_code": "Backup-Code", + "create_an_account": "Konto erstellen", + "enter_your_backup_code": "Gib deinen Backup-Code ein", + "enter_your_two_factor_authentication_code": "Gib deinen Zwei-Faktor-Authentifizierungscode ein", + "forgot_your_password": "Passwort vergessen?", + "login_to_your_account": "Melde Dich bei deinem Konto an", + "login_with_email": "Mit E-Mail einloggen", + "lost_access": "Zugang verloren?", + "new_to_formbricks": "Neu bei Formbricks?", + "use_a_backup_code": "Einen Backup-Code verwenden" + }, + "saml_connection_error": "Etwas ist schiefgelaufen. Bitte überprüfe die App-Konsole für weitere Details.", + "signup": { + "captcha_failed": "reCAPTCHA fehlgeschlagen", + "have_an_account": "Hast Du ein Konto?", + "log_in": "Einloggen", + "password_validation_contain_at_least_1_number": "Enthält mindestens 1 Zahl", + "password_validation_minimum_8_and_maximum_128_characters": "Mindestens 8 & höchstens 128 Zeichen", + "password_validation_uppercase_and_lowercase": "Mix aus Groß- und Kleinbuchstaben", + "please_verify_captcha": "Bitte bestätige reCAPTCHA", + "privacy_policy": "Datenschutzerklärung", + "terms_of_service": "Nutzungsbedingungen", + "title": "Erstelle dein Formbricks-Konto" + }, + "signup_without_verification_success": { + "user_successfully_created": "Benutzer erfolgreich erstellt", + "user_successfully_created_description": "Dein neuer Benutzer wurde erfolgreich erstellt. Bitte klicke auf den untenstehenden Button und melde Dich in deinem Konto an." + }, + "testimonial_1": "Als open-source Firma ist uns Datenschutz extrem wichtig! Formbricks bietet die perfekte Mischung aus modernster Technologie und solidem Datenschutz.", + "testimonial_all_features_included": "Alle Funktionen enthalten", + "testimonial_free_and_open_source": "Kostenlos und Open Source", + "testimonial_no_credit_card_required": "Keine Kreditkarte erforderlich", + "testimonial_title": "Erschaffe einzigartige Kundenerlebnisse", + "verification-requested": { + "invalid_email_address": "Ungültige E-Mail-Adresse", + "invalid_token": "Ungültiges Token ☹️", + "no_email_provided": "Keine E-Mail bereitgestellt", + "please_click_the_link_in_the_email_to_activate_your_account": "Bitte klicke auf den Link in der E-Mail, um dein Konto zu aktivieren.", + "please_confirm_your_email_address": "Bitte bestätige deine E-Mail-Adresse", + "resend_verification_email": "Bestätigungs-E-Mail erneut senden", + "verification_email_successfully_sent": "Bestätigungs-E-Mail erfolgreich gesendet. Bitte überprüfe dein Postfach.", + "we_sent_an_email_to": "Wir haben eine E-Mail an {email} gesendet", + "you_didnt_receive_an_email_or_your_link_expired": "Hast Du keine E-Mail erhalten oder ist dein Link abgelaufen?" + }, + "verify": { + "no_token_provided": "Kein Token bereitgestellt", + "verifying": "Wird überprüft..." + } + }, + "billing_confirmation": { + "back_to_billing_overview": "Zurück zur Abrechnungsübersicht", + "thanks_for_upgrading": "Vielen Dank, dass Du dein Formbricks-Abonnement aktualisiert hast.", + "upgrade_successful": "Upgrade erfolgreich" + }, + "common": { + "accepted": "Akzeptiert", + "account": "Konto", + "account_settings": "Kontoeinstellungen", + "action": "Aktion", + "actions": "Aktionen", + "active_surveys": "Aktive Umfragen", + "activity": "Aktivität", + "add": "Hinzufügen", + "add_action": "Aktion hinzufügen", + "add_filter": "Filter hinzufügen", + "add_logo": "Logo hinzufügen", + "add_project": "Projekt hinzufügen", + "add_to_team": "Zum Team hinzufügen", + "all": "Alle", + "all_questions": "Alle Fragen", + "allow": "erlauben", + "allow_users_to_exit_by_clicking_outside_the_survey": "Erlaube Nutzern, die Umfrage zu verlassen, indem sie außerhalb klicken", + "an_unknown_error_occurred_while_deleting_table_items": "Beim Löschen von {type}s ist ein unbekannter Fehler aufgetreten", + "and": "und", + "and_response_limit_of": "und Antwortlimit von", + "anonymous": "Anonym", + "api_keys": "API-Schlüssel", + "app": "App", + "app_survey": "App-Umfrage", + "apply_filters": "Filter anwenden", + "are_you_sure": "Bist Du sicher?", + "are_you_sure_this_action_cannot_be_undone": "Bist Du sicher? Diese Aktion kann nicht rückgängig gemacht werden.", + "attributes": "Attribute", + "avatar": "Avatar", + "back": "Zurück", + "billing": "Abrechnung", + "booked": "Gebucht", + "bottom_left": "Unten links", + "bottom_right": "Unten rechts", + "cancel": "Abbrechen", + "centered_modal": "Zentriertes Modalfenster", + "choices": "Entscheidungen", + "clear_all": "Alles löschen", + "clear_filters": "Filter löschen", + "clear_selection": "Auswahl aufheben", + "click": "Klick", + "clicks": "Klicks", + "close": "Schließen", + "code": "Code", + "collapse_rows": "Zeilen einklappen", + "completed": "Abgeschlossen", + "configuration": "Konfiguration", + "confirm": "Bestätigen", + "connect": "Verbinden", + "connect_formbricks": "Formbricks verbinden", + "connected": "Verbunden", + "contacts": "Kontakte", + "copied_to_clipboard": "In die Zwischenablage kopiert", + "copy": "Kopieren", + "copy_code": "Code kopieren", + "copy_link": "Link kopieren", + "create_new_organization": "Neue Organisation erstellen", + "create_segment": "Segment erstellen", + "create_survey": "Umfrage erstellen", + "created": "Erstellt", + "created_at": "Erstellt am", + "created_by": "Erstellt von", + "customer_success": "Kundenerfolg", + "danger_zone": "Gefahrenzone", + "dark_overlay": "Dunkle Überlagerung", + "date": "Datum", + "default": "Standard", + "delete": "Löschen", + "description": "Beschreibung", + "dev_env": "Entwicklungsumgebung", + "development_environment_banner": "Du bist in einer Entwicklungsumgebung. Richte sie ein, um Umfragen, Aktionen und Attribute zu testen.", + "disable": "Deaktivieren", + "disallow": "Nicht erlauben", + "discard": "Verwerfen", + "dismissed": "Entlassen", + "docs": "Dokumentation", + "documentation": "Dokumentation", + "download": "Herunterladen", + "draft": "Entwurf", + "duplicate": "Duplikat", + "e_commerce": "E-Commerce", + "edit": "Bearbeiten", + "email": "E-Mail", + "embed": "Einbetten", + "enterprise_license": "Enterprise Lizenz", + "environment_not_found": "Umgebung nicht gefunden", + "environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.", + "error": "Fehler", + "error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.", + "error_component_title": "Fehler beim Laden der Ressourcen", + "expand_rows": "Zeilen erweitern", + "finish": "Fertigstellen", + "follow_these": "Folge diesen", + "formbricks_version": "Formbricks Version", + "full_name": "Name", + "gathering_responses": "Antworten sammeln", + "general": "Allgemein", + "go_back": "Geh zurück", + "go_to_dashboard": "Zum Dashboard gehen", + "hidden": "Versteckt", + "hidden_field": "Verstecktes Feld", + "hidden_fields": "Versteckte Felder", + "hide": "Verstecken", + "hide_column": "Spalte ausblenden", + "image": "Bild", + "images": "Bilder", + "import": "Importieren", + "impressions": "Eindrücke", + "imprint": "Impressum", + "in_progress": "Im Gange", + "inactive_surveys": "Inaktive Umfragen", + "input_type": "Eingabetyp", + "insights": "Einblicke", + "integration": "Integration", + "integrations": "Integrationen", + "invalid_date": "Ungültiges Datum", + "invalid_file_type": "Ungültiger Dateityp", + "invite": "Einladen", + "invite_them": "Lade sie ein", + "key": "Schlüssel", + "label": "Bezeichnung", + "language": "Sprache", + "learn_more": "Mehr erfahren", + "license": "Lizenz", + "light_overlay": "Helle Überlagerung", + "limits_reached": "Limits erreicht", + "link": "Link", + "link_and_email": "Link & E-Mail", + "link_copied": "Link in die Zwischenablage kopiert!", + "link_survey": "Link-Umfrage", + "link_surveys": "Umfragen verknüpfen", + "load_more": "Mehr laden", + "loading": "Lädt", + "logo": "Logo", + "logout": "Abmelden", + "look_and_feel": "Darstellung", + "manage": "Verwalten", + "marketing": "Marketing", + "maximum": "Maximal", + "member": "Mitglied", + "members": "Mitglieder", + "membership_not_found": "Mitgliedschaft nicht gefunden", + "metadata": "Metadaten", + "minimum": "Minimum", + "mobile_overlay_text": "Formbricks ist für Geräte mit kleineren Auflösungen nicht verfügbar.", + "move_down": "Nach unten bewegen", + "move_up": "Nach oben bewegen", + "multiple_languages": "Mehrsprachigkeit", + "name": "Name", + "negative": "Negativ", + "neutral": "Neutral", + "new": "Neu", + "new_survey": "Neue Umfrage", + "new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!", + "next": "Weiter", + "no_background_image_found": "Kein Hintergrundbild gefunden.", + "no_code": "No Code", + "no_files_uploaded": "Keine Dateien hochgeladen", + "no_result_found": "Kein Ergebnis gefunden", + "no_results": "Keine Ergebnisse", + "no_surveys_found": "Keine Umfragen gefunden.", + "not_authenticated": "Du bist nicht authentifiziert, um diese Aktion durchzuführen.", + "not_authorized": "Nicht berechtigt", + "not_connected": "Nicht verbunden", + "note": "Notiz", + "notes": "Notizen", + "notifications": "Benachrichtigungen", + "number": "Nummer", + "off": "Aus", + "on": "An", + "only_one_file_allowed": "Es ist nur eine Datei erlaubt", + "only_owners_managers_and_manage_access_members_can_perform_this_action": "Nur Eigentümer, Manager und Mitglieder mit Zugriff auf das Management können diese Aktion ausführen.", + "or": "oder", + "organization": "Organisation", + "organization_id": "Organisations-ID", + "organization_not_found": "Organisation nicht gefunden", + "organization_teams_not_found": "Organisations-Teams nicht gefunden", + "other": "Andere", + "others": "Andere", + "overview": "Überblick", + "password": "Passwort", + "paused": "Pausiert", + "pending_downgrade": "Herabstufung ausstehend", + "people_manager": "Mitarbeiterverwaltung", + "person": "Person", + "phone": "Handy", + "photo_by": "Foto von", + "pick_a_date": "Wähl ein Datum", + "placeholder": "Platzhalter", + "please_select_at_least_one_survey": "Bitte wähle mindestens eine Umfrage aus", + "please_select_at_least_one_trigger": "Bitte wähle mindestens einen Auslöser aus", + "please_upgrade_your_plan": "Bitte upgrade deinen Plan.", + "positive": "Positiv", + "preview": "Vorschau", + "preview_survey": "Umfragevorschau", + "privacy": "Datenschutz", + "privacy_policy": "Datenschutzerklärung", + "product_manager": "Produktmanager", + "profile": "Profil", + "project": "Projekt", + "project_configuration": "Projektkonfiguration", + "project_id": "Projekt-ID", + "project_name": "Projektname", + "project_not_found": "Projekt nicht gefunden", + "project_permission_not_found": "Projekt-Berechtigung nicht gefunden", + "projects": "Projekte", + "projects_limit_reached": "Projektlimit erreicht", + "question": "Frage", + "question_id": "Frage-ID", + "questions": "Fragen", + "read_docs": "Dokumentation lesen", + "remove": "Entfernen", + "reorder_and_hide_columns": "Spalten neu anordnen und ausblenden", + "report_survey": "Umfrage melden", + "request_trial_license": "Testlizenz anfordern", + "reset_to_default": "Auf Standard zurücksetzen", + "response": "Antwort", + "responses": "Antworten", + "restart": "Neustart", + "role": "Rolle", + "role_organization": "Rolle (Organisation)", + "saas": "SaaS", + "sales": "Vertrieb", + "save": "Speichern", + "save_changes": "Änderungen speichern", + "scheduled": "Geplant", + "search": "Suchen", + "security": "Sicherheit", + "segment": "Segment", + "segments": "Segmente", + "select": "Auswählen", + "select_all": "Alles auswählen", + "select_survey": "Umfrage auswählen", + "selected": "Ausgewählt", + "selected_questions": "Ausgewählte Fragen", + "selection": "Auswahl", + "selections": "Auswahlen", + "send": "Senden", + "send_test_email": "Test-E-Mail senden", + "session_not_found": "Sitzung nicht gefunden", + "settings": "Einstellungen", + "share_feedback": "Feedback geben", + "show": "zeigen", + "show_response_count": "Antwortanzahl anzeigen", + "shown": "Angezeigt", + "size": "Größe", + "skipped": "Übersprungen", + "skips": "Übersprungen", + "some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden", + "something_went_wrong_please_try_again": "Etwas ist schiefgelaufen. Bitte versuche es noch einmal.", + "sort_by": "Sortieren nach", + "start_free_trial": "Kostenlos starten", + "status": "Status", + "step_by_step_manual": "Schritt-für-Schritt-Anleitung", + "styling": "Styling", + "submit": "Abschicken", + "summary": "Zusammenfassung", + "survey": "Umfrage", + "survey_completed": "Umfrage abgeschlossen.", + "survey_id": "Umfrage-ID", + "survey_languages": "Umfragesprachen", + "survey_live": "Umfrage live", + "survey_not_found": "Umfrage nicht gefunden", + "survey_paused": "Umfrage pausiert.", + "survey_scheduled": "Umfrage geplant.", + "survey_type": "Umfragetyp", + "surveys": "Umfragen", + "switch_organization": "Organisation wechseln", + "switch_to": "Wechseln zu {environment}", + "table_items_deleted_successfully": "{type}s erfolgreich gelöscht", + "table_settings": "Tabelleinstellungen", + "tags": "Tags", + "targeting": "Targeting", + "team": "Team", + "team_access": "Teamzugriff", + "team_name": "Teamname", + "teams": "Zugriffskontrolle", + "teams_not_found": "Teams nicht gefunden", + "text": "Text", + "time": "Zeit", + "time_to_finish": "Zeit zum Fertigstellen", + "title": "Titel", + "top_left": "Oben links", + "top_right": "Oben rechts", + "try_again": "Versuch's nochmal", + "type": "Typ", + "unlock_more_projects_with_a_higher_plan": "Schalte mehr Projekte mit einem höheren Plan frei.", + "update": "Aktualisierung", + "updated": "Aktualisiert", + "updated_at": "Aktualisiert am", + "upload": "Hochladen", + "upload_input_description": "Klicke oder ziehe, um Dateien hochzuladen.", + "url": "URL", + "user": "Benutzer", + "user_id": "Benutzer-ID", + "user_not_found": "Benutzer nicht gefunden", + "variable": "Variable", + "variables": "Variablen", + "verified_email": "Verifizierte E-Mail", + "video": "Video", + "warning": "Warnung", + "we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Wir konnten Ihre Lizenz nicht überprüfen, da der Lizenzserver nicht erreichbar ist.", + "webhook": "Webhook", + "webhooks": "Webhooks", + "website_and_app_connection": "Website & App Verbindung", + "website_app_survey": "Website- & App-Umfrage", + "website_survey": "Website-Umfrage", + "weekly_summary": "Wöchentliche Zusammenfassung", + "welcome_card": "Willkommenskarte", + "yes": "Ja", + "you": "Du", + "you_are_downgraded_to_the_community_edition": "Du wurdest auf die Community Edition herabgestuft.", + "you_are_not_authorised_to_perform_this_action": "Du bist nicht berechtigt, diese Aktion auszuführen.", + "you_have_reached_your_limit_of_project_limit": "Du hast dein Limit von {projectLimit} Projekten erreicht.", + "you_have_reached_your_monthly_miu_limit_of": "Du hast dein monatliches MIU-Limit erreicht", + "you_have_reached_your_monthly_response_limit_of": "Du hast dein monatliches Antwortlimit erreicht", + "you_will_be_downgraded_to_the_community_edition_on_date": "Du wirst am {date} auf die Community Edition herabgestuft." + }, + "emails": { + "accept": "Annehmen", + "click_or_drag_to_upload_files": "Klicke oder ziehe, um Dateien hochzuladen.", + "email_customization_preview_email_heading": "Hey {userName}", + "email_customization_preview_email_subject": "Formbricks E-Mail-Umfrage Vorschau", + "email_customization_preview_email_text": "Dies ist eine E-Mail-Vorschau, um dir zu zeigen, welches Logo in den E-Mails gerendert wird.", + "email_footer_text_1": "Einen schönen Tag noch!", + "email_footer_text_2": "Dein Formbricks Team", + "email_template_text_1": "Diese E-Mail wurde via Formbricks gesendet.", + "embed_survey_preview_email_didnt_request": "Kein Interesse?", + "embed_survey_preview_email_environment_id": "Umgebungs-ID", + "embed_survey_preview_email_fight_spam": "Hilf uns, Spam zu bekämpfen, und leite diese Mail an hola@formbricks.com weiter.", + "embed_survey_preview_email_heading": "Vorschau Einbettung in E-Mail", + "embed_survey_preview_email_subject": "Formbricks E-Mail-Umfrage Vorschau", + "embed_survey_preview_email_text": "So sieht die Umfrage eingebettet in eine E-Mail aus:", + "forgot_password_email_change_password": "Passwort ändern", + "forgot_password_email_did_not_request": "Wenn Du sie nicht angefordert hast, ignoriere bitte diese E-Mail.", + "forgot_password_email_heading": "Passwort ändern", + "forgot_password_email_link_valid_for_24_hours": "Der Link ist 24 Stunden gültig.", + "forgot_password_email_subject": "Setz dein Formbricks-Passwort zurück", + "forgot_password_email_text": "Du hast einen Link angefordert, um dein Passwort zu ändern. Du kannst dies tun, indem Du auf den untenstehenden Link klickst:", + "imprint": "Impressum", + "invite_accepted_email_heading": "Hey", + "invite_accepted_email_subject": "Du hast einen neuen Organisation-Mitglied!", + "invite_accepted_email_text_par1": "Wollte dir nur Bescheid geben, dass", + "invite_accepted_email_text_par2": "deine Einladung angenommen hat. Viel Spaß bei der Zusammenarbeit!", + "invite_email_button_label": "Organisation beitreten", + "invite_email_heading": "Hey", + "invite_email_text_par1": "Dein Kollege", + "invite_email_text_par2": "hat Dich eingeladen, Formbricks zu nutzen. Um die Einladung anzunehmen, klicke bitte auf den untenstehenden Link:", + "invite_member_email_subject": "Du wurdest eingeladen, Formbricks zu nutzen!", + "live_survey_notification_completed": "Abgeschlossen", + "live_survey_notification_draft": "Entwurf", + "live_survey_notification_in_progress": "In Bearbeitung", + "live_survey_notification_no_new_response": "Diese Woche keine neue Antwort erhalten \uD83D\uDD75️", + "live_survey_notification_no_responses_yet": "Noch keine Antworten!", + "live_survey_notification_paused": "Pausiert", + "live_survey_notification_scheduled": "Geplant", + "live_survey_notification_view_more_responses": "Zeige {responseCount} weitere Antworten", + "live_survey_notification_view_previous_responses": "Vorherige Antworten anzeigen", + "live_survey_notification_view_response": "Antwort anzeigen", + "notification_footer_all_the_best": "Alles Gute,", + "notification_footer_in_your_settings": "in deinen Einstellungen \uD83D\uDE4F", + "notification_footer_please_turn_them_off": "Bitte ausstellen", + "notification_footer_the_formbricks_team": "Dein Formbricks Team \uD83E\uDD0D", + "notification_footer_to_halt_weekly_updates": "Um wöchentliche Updates zu stoppen,", + "notification_header_hey": "Hey \uD83D\uDC4B", + "notification_header_weekly_report_for": "Wöchentlicher Bericht für", + "notification_insight_completed": "Abgeschlossen", + "notification_insight_completion_rate": "Completion Rate %", + "notification_insight_displays": "Displays", + "notification_insight_responses": "Antworten", + "notification_insight_surveys": "Umfragen", + "onboarding_invite_email_button_label": "Tritt {inviterName}s Organisation bei", + "onboarding_invite_email_connect_formbricks": "Verbinde Formbricks in nur wenigen Minuten über ein HTML-Snippet oder via NPM mit deiner App oder Website.", + "onboarding_invite_email_create_account": "Erstelle ein Konto, um {inviterName}s Organisation beizutreten.", + "onboarding_invite_email_done": "Erledigt ✅", + "onboarding_invite_email_get_started_in_minutes": "Dauert nur wenige Minuten", + "onboarding_invite_email_heading": "Hey ", + "onboarding_invite_email_subject": "{inviterName} braucht Hilfe bei Formbricks. Kannst Du ihm helfen?", + "password_changed_email_heading": "Passwort geändert", + "password_changed_email_text": "Dein Passwort wurde erfolgreich geändert.", + "password_reset_notify_email_subject": "Dein Formbricks-Passwort wurde geändert", + "privacy_policy": "Datenschutzerklärung", + "reject": "Ablehnen", + "render_email_response_value_file_upload_response_link_not_included": "Link zur hochgeladenen Datei ist aus Datenschutzgründen nicht enthalten", + "response_finished_email_subject": "Eine Antwort für {surveyName} wurde abgeschlossen ✅", + "response_finished_email_subject_with_email": "{personEmail} hat deine Umfrage {surveyName} abgeschlossen ✅", + "schedule_your_meeting": "Termin planen", + "select_a_date": "Datum auswählen", + "survey_response_finished_email_congrats": "Glückwunsch, Du hast eine neue Antwort auf deine Umfrage {surveyName} erhalten!", + "survey_response_finished_email_dont_want_notifications": "Möchtest Du diese Benachrichtigungen nicht erhalten?", + "survey_response_finished_email_hey": "Hey \uD83D\uDC4B", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Benachrichtigungen für alle neu erstellten Formulare ausschalten", + "survey_response_finished_email_turn_off_notifications_for_this_form": "Benachrichtigungen für dieses Formular ausschalten", + "survey_response_finished_email_view_more_responses": "Zeige {responseCount} weitere Antworten", + "survey_response_finished_email_view_survey_summary": "Umfragezusammenfassung anzeigen", + "verification_email_click_on_this_link": "Du kannst auch auf diesen Link klicken:", + "verification_email_heading": "Fast geschafft!", + "verification_email_hey": "Hey \uD83D\uDC4B", + "verification_email_if_expired_request_new_token": "Wenn es abgelaufen ist, fordere hier ein neues Token an:", + "verification_email_link_valid_for_24_hours": "Der Link ist 24 Stunden gültig.", + "verification_email_request_new_verification": "Neue Verifizierung anfordern", + "verification_email_subject": "Bitte bestätige deine E-Mail-Adresse", + "verification_email_survey_name": "Umfragename", + "verification_email_take_survey": "Umfrage ausfüllen", + "verification_email_text": "Um Formbricks zu nutzen, bestätige bitte deine E-Mail-Adresse:", + "verification_email_thanks": "Danke, dass Du deine E-Mail bestätigt hast!", + "verification_email_to_fill_survey": "Um die Umfrage auszufüllen, klicke bitte auf den untenstehenden Button:", + "verification_email_verify_email": "E-Mail bestätigen", + "verified_link_survey_email_subject": "Deine Umfrage ist bereit zum Ausfüllen.", + "weekly_summary_create_reminder_notification_body_cal_slot": "Wähle einen 15-minütigen Termin im Kalender unseres Gründers aus.", + "weekly_summary_create_reminder_notification_body_dont_let_a_week_pass": "Lass keine Woche vergehen, ohne etwas über deine Nutzer zu lernen:", + "weekly_summary_create_reminder_notification_body_need_help": "Brauchst Du Hilfe, die richtige Umfrage für dein Produkt zu finden?", + "weekly_summary_create_reminder_notification_body_reply_email": "oder antworte auf diese E-Mail :)", + "weekly_summary_create_reminder_notification_body_setup_a_new_survey": "Neue Umfrage einrichten", + "weekly_summary_create_reminder_notification_body_text": "Wir würden dir gerne eine wöchentliche Zusammenfassung schicken, aber momentan laufen keine Umfragen für {projectName}.", + "weekly_summary_email_subject": "{projectName} Nutzer-Insights – Letzte Woche von Formbricks" + }, + "environments": { + "actions": { + "action_copied_successfully": "Aktion erfolgreich kopiert", + "action_copy_failed": "Aktion konnte nicht kopiert werden", + "action_created_successfully": "Aktion erfolgreich erstellt", + "action_deleted_successfully": "Aktion erfolgreich gelöscht", + "action_type": "Aktionstyp", + "action_updated_successfully": "Aktion erfolgreich aktualisiert", + "action_with_key_already_exists": "Aktion mit dem Schlüssel {key} existiert bereits", + "action_with_name_already_exists": "Aktion mit dem Namen {name} existiert bereits", + "add_css_class_or_id": "CSS-Klasse oder ID hinzufügen", + "add_url": "URL hinzufügen", + "click": "Klicken", + "contains": "enthält", + "create_action": "Aktion erstellen", + "css_selector": "CSS-Selektor", + "delete_action_text": "Bist Du sicher, dass Du diese Aktion löschen möchtest? Dadurch wird diese Aktion auch als Auslöser aus all deinen Umfragen entfernt.", + "display_name": "Anzeigename", + "does_not_contain": "Enthält nicht", + "does_not_exactly_match": "Stimmt nicht genau überein", + "eg_clicked_download": "z.B. 'Herunterladen' geklickt", + "eg_download_cta_click_on_home": "z.B. Download-CTA-Klick auf der Startseite", + "eg_install_app": "z.B. App installieren", + "eg_user_clicked_download_button": "z.B. Benutzer hat auf 'Herunterladen' geklickt", + "ends_with": "endet mit", + "enter_a_url_to_see_if_a_user_visiting_it_would_be_tracked": "Teste eine URL, um zu sehen, ob der Nutzer deine Umfrage sehen würde.", + "exactly_matches": "Stimmt exakt überein", + "exit_intent": "Will Seite verlassen", + "fifty_percent_scroll": "50% Scroll", + "how_do_code_actions_work": "Wie funktionieren Code-Aktionen?", + "if_a_user_clicks_a_button_with_a_specific_css_class_or_id": "Wenn ein Benutzer auf einen Button mit einer bestimmten CSS-Klasse oder ID klickt", + "if_a_user_clicks_a_button_with_a_specific_text": "Wenn ein Benutzer auf einen Button mit einem bestimmten Text klickt", + "in_your_code_read_more_in_our": "in deinem Code. Lies mehr in unserem", + "inner_text": "Innerer Text", + "invalid_css_selector": "Ungültiger CSS-Selektor", + "limit_the_pages_on_which_this_action_gets_captured": "Begrenze die Seiten, auf denen diese Aktion erfasst wird", + "limit_to_specific_pages": "Auf bestimmte Seiten beschränken", + "on_all_pages": "Auf allen Seiten", + "page_filter": "Seitenfilter", + "page_view": "Seitenansicht", + "select_match_type": "Wähle den Spieltyp aus", + "starts_with": "Fängt an mit", + "test_match": "Testspiel", + "test_your_url": "Teste deine URL", + "this_action_was_created_automatically_you_cannot_make_changes_to_it": "Diese Aktion wurde automatisch erstellt. Du kannst keine Änderungen daran vornehmen.", + "this_action_will_be_triggered_when_the_page_is_loaded": "Diese Aktion wird ausgelöst, wenn die Seite geladen ist.", + "this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Diese Aktion wird ausgelöst, wenn der Benutzer 50% der Seite scrollt.", + "this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Diese Aktion wird ausgelöst, wenn der Benutzer versucht, die Seite zu verlassen.", + "this_is_a_code_action_please_make_changes_in_your_code_base": "Dies ist eine Code-Aktion. Bitte nehmen Sie Änderungen an Ihrem Code vor.", + "track_new_user_action": "Neue Benutzeraktion verfolgen", + "track_user_action_to_display_surveys_or_create_user_segment": "Benutzeraktionen verfolgen, um Umfragen anzuzeigen oder Benutzersegmente zu erstellen.", + "url": "URL", + "user_actions": "Benutzeraktionen", + "user_clicked_download_button": "Benutzer hat auf 'Herunterladen' geklickt", + "what_did_your_user_do": "Was hat dein Nutzer gemacht?", + "what_is_the_user_doing": "Was macht der Nutzer?", + "you_can_track_code_action_anywhere_in_your_app_using": "Du kannst Code-Aktionen überall in deiner App tracken mit" + }, + "connect": { + "congrats": "Glückwunsch!", + "connection_successful_message": "Gut gemacht! Wir sind verbunden.", + "do_it_later": "Ich mache es später", + "finish_onboarding": "Onboarding abschließen", + "headline": "Verbinde deine App oder Website", + "import_formbricks_and_initialize_the_widget_in_your_component": "Importiere Formbricks und initialisiere das Widget in deiner Komponente (z.B. App.tsx):", + "insert_this_code_into_the_head_tag_of_your_website": "Füge diesen Code in den head-Tag deiner Website ein:", + "subtitle": "Das dauert keine 4 Minuten.", + "waiting_for_your_signal": "Warte auf ein Signal von dir..." + }, + "contacts": { + "contact_deleted_successfully": "Kontakt erfolgreich gelöscht", + "contact_not_found": "Kein solcher Kontakt gefunden", + "contacts_table_refresh": "Kontakte aktualisieren", + "contacts_table_refresh_error": "Beim Aktualisieren der Kontakte ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", + "contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert", + "first_name": "Vorname", + "last_name": "Nachname", + "no_responses_found": "Keine Antworten gefunden", + "not_provided": "Nicht angegeben", + "search_contact": "Kontakt suchen", + "select_attribute": "Attribut auswählen", + "unlock_contacts_description": "Verwalte Kontakte und sende gezielte Umfragen", + "unlock_contacts_title": "Kontakte mit einem höheren Plan freischalten", + "upload_contacts_modal_attributes_description": "Ordne die Spalten in deiner CSV den Attributen in Formbricks zu.", + "upload_contacts_modal_attributes_new": "Neues Attribut", + "upload_contacts_modal_attributes_search_or_add": "Attribut suchen oder hinzufügen", + "upload_contacts_modal_attributes_should_be_mapped_to": "sollte zugeordnet werden zu", + "upload_contacts_modal_attributes_title": "Attribute", + "upload_contacts_modal_description": "Lade eine CSV hoch, um Kontakte mit Attributen schnell zu importieren", + "upload_contacts_modal_download_example_csv": "Beispiel-CSV herunterladen", + "upload_contacts_modal_duplicates_description": "Wie sollen wir vorgehen, wenn ein Kontakt bereits existiert?", + "upload_contacts_modal_duplicates_overwrite_description": "Überschreibt die bestehenden Kontakte", + "upload_contacts_modal_duplicates_overwrite_title": "Überschreiben", + "upload_contacts_modal_duplicates_skip_description": "Überspringt doppelte Kontakte", + "upload_contacts_modal_duplicates_skip_title": "Überspringen", + "upload_contacts_modal_duplicates_title": "Duplikate", + "upload_contacts_modal_duplicates_update_description": "Aktualisiert die bestehenden Kontakte", + "upload_contacts_modal_duplicates_update_title": "Aktualisieren", + "upload_contacts_modal_pick_different_file": "Wähle eine andere Datei", + "upload_contacts_modal_preview": "Hier ist eine Vorschau deiner Daten.", + "upload_contacts_modal_upload_btn": "Kontakte hochladen" + }, + "experience": { + "all": "Alle", + "all_time": "Gesamt", + "analysed_feedbacks": "Analysierte Rückmeldungen", + "category": "Kategorie", + "category_updated_successfully": "Kategorie erfolgreich aktualisiert!", + "complaint": "Beschwerde", + "did_you_find_this_insight_helpful": "War diese Erkenntnis hilfreich?", + "failed_to_update_category": "Kategorie konnte nicht aktualisiert werden", + "feature_request": "Anfrage", + "good_afternoon": "\uD83C\uDF24️ Guten Nachmittag", + "good_evening": "\uD83C\uDF19 Guten Abend", + "good_morning": "☀️ Guten Morgen", + "insights_description": "Erkenntnisse, die aus den Antworten aller Umfragen gewonnen wurden", + "insights_for_project": "Einblicke für {projectName}", + "new_responses": "Neue Antworten", + "no_insights_for_this_filter": "Keine Erkenntnisse für diesen Filter", + "no_insights_found": "Keine Erkenntnisse gefunden. Sammle mehr Umfrageantworten oder aktiviere Erkenntnisse für deine bestehenden Umfragen, um loszulegen.", + "praise": "Lob", + "sentiment_score": "Stimmungswert", + "templates_card_description": "Wähle deine Vorlage oder starte von Grund auf neu", + "templates_card_title": "Miss die Kundenerfahrung", + "this_month": "Dieser Monat", + "this_quarter": "Dieses Quartal", + "this_week": "Diese Woche", + "today": "Heute" + }, + "formbricks_logo": "Formbricks-Logo", + "integrations": { + "activepieces_integration_description": "Verbinde Formbricks sofort mit beliebten Apps, um Aufgaben ohne Programmierung zu automatisieren.", + "additional_settings": "Weitere Einstellungen", + "airtable": { + "airtable_base": "Airtable Basis", + "airtable_integration": "Airtable Integration", + "airtable_integration_description": "Synchronisiere Antworten direkt mit Airtable.", + "airtable_integration_is_not_configured": "Airtable Integration ist nicht konfiguriert", + "connect_with_airtable": "Mit Airtable verbinden", + "link_airtable_table": "Airtable Tabelle verknüpfen", + "link_new_table": "Neue Tabelle verknüpfen", + "no_bases_found": "Keine Airtable Bases gefunden", + "no_integrations_yet": "Deine Airtable-Integrationen werden hier angezeigt, sobald Du sie hinzufügst ⏲️", + "please_create_a_base": "Bitte erstelle eine Base auf Airtable", + "please_select_a_base": "Bitte wähle eine Base aus", + "please_select_a_table": "Bitte wähle eine Tabelle aus", + "sync_responses_with_airtable": "Antworten mit Airtable synchronisieren", + "table_name": "Tabellenname" + }, + "airtable_integration_description": "Synchronisiere deine Airtable-Tabelle mit Umfragedaten", + "connected_with_email": "Verbunden mit {email}", + "connecting_integration_failed_please_try_again": "Verbindung der Integration fehlgeschlagen. Bitte versuche es erneut!", + "create_survey_warning": "Du musst eine Umfrage erstellen, um diese Integration einrichten zu können", + "delete_integration": "Integration löschen", + "delete_integration_confirmation": "Bist Du sicher, dass Du diese Integration löschen möchtest?", + "google_sheet_integration_description": "Synchronisiere deine Tabelle mit Umfragedaten", + "google_sheets": { + "connect_with_google_sheets": "Mit Google Sheets verbinden", + "enter_a_valid_spreadsheet_url_error": "Bitte gib eine gültige Tabellen-URL ein", + "google_connection": "Google Verbindung", + "google_connection_deletion_description": "Synchronisiere Antworten direkt mit Google Sheets.", + "google_sheet_integration_is_not_configured": "Die Google Sheet-Integration ist in deiner Formbricks Instanz nicht konfiguriert.", + "google_sheet_logo": "Google Tabellen-Logo", + "google_sheet_name": "Google Tabellenname", + "google_sheets_integration": "Google Tabellen Integration", + "google_sheets_integration_description": "Synchronisiere Antworten direkt mit Google Sheets.", + "link_google_sheet": "Tabelle verlinken", + "link_new_sheet": "Neues Blatt verknüpfen", + "no_integrations_yet": "Deine verknüpften Tabellen werden hier angezeigt, sobald Du sie hinzufügst ⏲️", + "spreadsheet_url": "Tabellen-URL" + }, + "include_created_at": "Erstellungsdatum einbeziehen", + "include_hidden_fields": "Versteckte Felder (hidden fields) einbeziehen", + "include_metadata": "Metadaten einbeziehen (Browser, Land, etc.)", + "include_variables": "Variablen einbeziehen", + "integration_added_successfully": "Integration erfolgreich hinzugefügt", + "integration_removed_successfully": "Integration erfolgreich entfernt", + "integration_updated_successfully": "Integration erfolgreich aktualisiert", + "make_integration_description": "Integriere Formbricks mit über 1000 Apps über Make", + "manage_webhooks": "Webhooks verwalten", + "n8n_integration_description": "Integriere Formbricks mit über 350 Apps über n8n", + "notion": { + "col_name_of_type_is_not_supported": "{col_name} vom Typ {type} wird von der Notion-API nicht unterstützt. Die Daten werden nicht in deiner Notion-Datenbank angezeigt.", + "connect_with_notion": "Mit Notion verbinden", + "connected_with_workspace": "Verbunden mit {workspace} workspace", + "create_at_least_one_database_to_setup_this_integration": "Du musst mindestens eine Datenbank erstellen, um diese Integration einrichten zu können", + "database_name": "Datenbankname", + "duplicate_connection_warning": "Eine Verbindung zu dieser Datenbank ist aktiv. Bitte Änderungen mit Vorsicht vornehmen.", + "link_database": "Datenbank verknüpfen", + "link_new_database": "Neue Datenbank verknüpfen", + "link_notion_database": "Notion Datenbank verknüpfen", + "map_formbricks_fields_to_notion_property": "Formbricks Felder auf Notion Eigenschaft abbilden", + "no_databases_found": "Deine verknüpften Datenbanken werden hier angezeigt, sobald Du sie hinzufügst ⏲️", + "notion_integration": "Notion Integration", + "notion_integration_description": "Sende Antworten direkt an Notion.", + "notion_integration_is_not_configured": "Die Notion-Integration ist in deiner Formbricks Instanz nicht konfiguriert.", + "notion_logo": "Notion Logo", + "please_complete_mapping_fields_with_notion_property": "Bitte vervollständige die Zuordnung der Felder mit der Eigenschaft (Property) in Notion", + "please_resolve_mapping_errors": "Bitte behebe die Zuordnungsfehler", + "please_select_a_database": "Bitte wähle eine Datenbank aus", + "please_select_at_least_one_mapping": "Bitte wähle mindestens eine Zuordnung aus", + "que_name_of_type_cant_be_mapped_to": "{que_name} vom Typ {question_label} kann nicht der Spalte {col_name} vom Typ {col_type} zugeordnet werden. Verwende stattdessen eine Spalte vom Typ {mapped_type}.", + "select_a_database": "Datenbank auswählen", + "select_a_field_to_map": "Wähle ein Feld zum Zuordnen aus", + "select_a_survey_question": "Wähle eine Umfragefrage aus", + "sync_responses_with_a_notion_database": "Antworten mit einer Datenbank in Notion synchronisieren", + "update_connection": "Notion erneut verbinden", + "update_connection_tooltip": "Verbinde die Integration erneut, um neu hinzugefügte Datenbanken einzuschließen. Deine bestehenden Integrationen bleiben erhalten." + }, + "notion_integration_description": "Sende Daten an deine Notion Datenbank", + "please_select_a_survey_error": "Bitte wähle eine Umfrage aus", + "select_at_least_one_question_error": "Bitte wähle mindestens eine Frage aus", + "slack": { + "already_connected_another_survey": "Du hast bereits eine andere Umfrage mit diesem Kanal verbunden.", + "channel_name": "Kanalname", + "connect_with_slack": "Mit Slack verbinden", + "connect_your_first_slack_channel": "Verbinde deinen ersten Slack Kanal, um loszulegen.", + "connected_with_team": "Verbunden mit {team}", + "create_at_least_one_channel_error": "Du musst mindestens einen Kanal erstellen, um diese Integration einrichten zu können.", + "dont_see_your_channel": "Siehst du deinen Kanal nicht?", + "link_channel": "Kanal verknüpfen", + "link_slack_channel": "Slack Kanal verknüpfen", + "please_select_a_channel": "Bitte wähle einen Kanal aus", + "select_channel": "Kanal auswählen", + "slack_integration": "Slack Integration", + "slack_integration_description": "Sende Antworten direkt an Slack.", + "slack_integration_is_not_configured": "Slack Integration ist in deiner Instanz von Formbricks nicht konfiguriert.", + "slack_reconnect_button": "Erneut verbinden", + "slack_reconnect_button_description": "Hinweis: Wir haben kürzlich unsere Slack-Integration geändert, um auch private Kanäle zu unterstützen. Bitte verbinden Sie Ihren Slack-Workspace erneut." + }, + "slack_integration_description": "Verbinde deinen Slack Arbeitsbereich sofort mit Formbricks", + "to_configure_it": "es zu konfigurieren.", + "webhook_integration_description": "Webhooks basierend auf Aktionen in deinen Umfragen auslösen", + "webhooks": { + "add_webhook": "Webhook hinzufügen", + "add_webhook_description": "Sende Umfragedaten an einen benutzerdefinierten Endpunkt", + "all_current_and_new_surveys": "Alle aktuellen und neuen Umfragen", + "created_by_third_party": "Erstellt von einer dritten Partei", + "discord_webhook_not_supported": "Discord-Webhooks werden derzeit nicht unterstützt.", + "empty_webhook_message": "Deine Webhooks werden hier angezeigt, sobald Du sie hinzufügst ⏲️", + "endpoint_pinged": "Juhu! Wir können den Webhook anpingen!", + "endpoint_pinged_error": "Kann den Webhook nicht anpingen!", + "please_check_console": "Bitte überprüfe die Konsole für weitere Details", + "please_enter_a_url": "Bitte gib eine URL ein", + "response_created": "Antwort erstellt", + "response_finished": "Antwort abgeschlossen", + "response_updated": "Antwort aktualisiert", + "source": "Quelle", + "test_endpoint": "Test-Endpunkt", + "triggers": "Auslöser", + "webhook_added_successfully": "Webhook wurde erfolgreich hinzugefügt", + "webhook_delete_confirmation": "Bist Du sicher, dass Du diesen Webhook löschen möchtest? Dadurch werden dir keine weiteren Benachrichtigungen mehr gesendet.", + "webhook_deleted_successfully": "Webhook erfolgreich gelöscht", + "webhook_name_placeholder": "Optional: Benenne deinen Webhook zur einfachen Identifizierung", + "webhook_test_failed_due_to": "Webhook Test fehlgeschlagen aufgrund von", + "webhook_updated_successfully": "Webhook erfolgreich aktualisiert.", + "webhook_url_placeholder": "Füge die URL ein, bei der das Ereignis ausgelöst werden soll" + }, + "website_or_app_integration_description": "Integriere Formbricks in deine Website oder App", + "zapier_integration_description": "Integriere Formbricks mit über 5000 Apps über Zapier" + }, + "project": { + "api_keys": { + "add_api_key": "API-Schlüssel hinzufügen", + "api_key": "API-Schlüssel", + "api_key_copied_to_clipboard": "API-Schlüssel in die Zwischenablage kopiert", + "api_key_created": "API-Schlüssel erstellt", + "api_key_deleted": "API-Schlüssel gelöscht", + "api_key_label": "API-Schlüssel Label", + "api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.", + "api_key_updated": "API-Schlüssel aktualisiert", + "duplicate_access": "Doppelter Projektzugriff nicht erlaubt", + "no_api_keys_yet": "Du hast noch keine API-Schlüssel", + "no_env_permissions_found": "Keine Umgebungsberechtigungen gefunden", + "organization_access": "Organisationszugang", + "permissions": "Berechtigungen", + "project_access": "Projektzugriff", + "secret": "Geheimnis", + "unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden" + }, + "app-connection": { + "api_host_description": "Dies ist die URL deines Formbricks Backends.", + "app_connection": "App-Verbindung", + "app_connection_description": "Verbinde deine App mit Formbricks.", + "check_out_the_docs": "Schau dir die Docs an.", + "dive_into_the_docs": "Tauch in die Docs ein.", + "does_your_widget_work": "Funktioniert dein Widget?", + "environment_id": "Deine Umgebungs-ID", + "environment_id_description": "Diese ID identifiziert eindeutig diese Formbricks Umgebung.", + "environment_id_description_with_environment_id": "Wird verwendet, um die richtige Umgebung zu identifizieren: {environmentId} ist deine.", + "formbricks_sdk": "Formbricks SDK", + "formbricks_sdk_connected": "Formbricks SDK ist verbunden", + "formbricks_sdk_not_connected": "Formbricks SDK ist noch nicht verbunden.", + "formbricks_sdk_not_connected_description": "Verbinde deine Website oder App mit Formbricks", + "have_a_problem": "Hast Du ein Problem?", + "how_to_setup": "Wie einrichten", + "how_to_setup_description": "Befolge diese Schritte, um das Formbricks Widget in deiner App einzurichten.", + "identifying_your_users": "deine Nutzer identifizieren", + "if_you_are_planning_to": "Wenn Du planst zu", + "insert_this_code_into_the": "Füge diesen Code in die", + "need_a_more_detailed_setup_guide_for": "Brauche eine detailliertere Anleitung für", + "not_working": "Klappt nicht?", + "open_an_issue_on_github": "Eine Issue auf GitHub öffnen", + "open_the_browser_console_to_see_the_logs": "Öffne die Browser Konsole, um die Logs zu sehen.", + "receiving_data": "Daten werden empfangen \uD83D\uDC83\uD83D\uDD7A", + "recheck": "Erneut prüfen", + "scroll_to_the_top": "Scroll nach oben!", + "step_1": "Schritt 1: Installiere mit pnpm, npm oder yarn", + "step_2": "Schritt 2: Widget initialisieren", + "step_2_description": "Importiere Formbricks und initialisiere das Widget in deiner Komponente (z.B. App.tsx):", + "step_3": "Schritt 3: Debug-Modus", + "switch_on_the_debug_mode_by_appending": "Schalte den Debug-Modus ein, indem Du anhängst", + "tag_of_your_app": "Tag deiner App", + "to_the_url_where_you_load_the": "URL, wo Du die lädst", + "want_to_learn_how_to_add_user_attributes": "Willst Du lernen, wie man Attribute hinzufügt?", + "you_are_done": "Du bist fertig \uD83C\uDF89", + "you_can_set_the_user_id_with": "du kannst die Benutzer-ID festlegen mit", + "your_app_now_communicates_with_formbricks": "Deine App kommuniziert jetzt mit Formbricks - sie sendet Ereignisse und lädt Umfragen automatisch!" + }, + "general": { + "cannot_delete_only_project": "Dies ist dein einziges Projekt, es kann nicht gelöscht werden. Erstelle zuerst ein neues Projekt.", + "delete_project": "Projekt löschen", + "delete_project_confirmation": "Bist Du sicher, dass Du {projectName} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "delete_project_name_includes_surveys_responses_people_and_more": "{projectName} löschen inkl. aller Umfragen, Antworten, Personen, Aktionen und Attribute.", + "delete_project_settings_description": "Projekt mit allen Umfragen, Antworten, Personen, Aktionen und Attributen löschen. Dies kann nicht rückgängig gemacht werden.", + "error_saving_project_information": "Fehler beim Speichern der Projektinformationen", + "only_owners_or_managers_can_delete_projects": "Nur Eigentümer oder Manager können Projekte löschen", + "project_deleted_successfully": "Projekt erfolgreich gelöscht", + "project_name_settings_description": "Ändere den Namen deines Projekts.", + "project_name_updated_successfully": "Projektname erfolgreich aktualisiert", + "recontact_waiting_time": "Wartezeit für erneuten Kontakt", + "recontact_waiting_time_settings_description": "Steuere, wie oft Nutzer in allen App-Umfragen eine Umfrage angezeigt bekommen können.", + "this_action_cannot_be_undone": "Diese Aktion kann nicht rückgängig gemacht werden.", + "wait_x_days_before_showing_next_survey": "Warte X Tage, bevor die nächste Umfrage angezeigt wird:", + "waiting_period_updated_successfully": "Wartezeit erfolgreich aktualisiert", + "whats_your_project_called": "Wie heißt dein Projekt?" + }, + "languages": { + "add_language": "Sprache hinzufügen", + "alias": "Alias", + "alias_tooltip": "Das Alias ist ein alternativer Name, um die Sprache identifizieren (optional)", + "cannot_remove_language_warning": "Du kannst diese Sprache nicht entfernen, da sie noch in folgenden Umfragen verwendet wird:", + "conflict_between_identifier_and_alias": "Es gibt einen Konflikt zwischen der ID einer hinzugefügten Sprache und einem deiner Aliase. Aliase und IDs dürfen nicht identisch sein.", + "conflict_between_selected_alias_and_another_language": "Es gibt einen Konflikt zwischen dem ausgewählten Alias und einer anderen Sprache, die diese ID hat. Bitte füge stattdessen die Sprache mit dieser ID zu deinem Projekt hinzu, um Unstimmigkeiten zu vermeiden.", + "delete_language_confirmation": "Bist Du sicher, dass Du diese Sprache löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "duplicate_language_or_language_id": "Doppelte Sprache oder Sprach-ID", + "edit_languages": "Sprachen bearbeiten", + "identifier": "Kennung (ISO)", + "incomplete_translations": "Unvollständige Übersetzungen", + "language": "Sprache", + "language_deleted_successfully": "Sprache erfolgreich gelöscht", + "languages_updated_successfully": "Sprachen erfolgreich aktualisiert", + "multi_language_surveys": "Mehrsprachige Umfragen", + "multi_language_surveys_description": "Füge Sprachen hinzu, um mehrsprachige Umfragen zu erstellen.", + "no_language_found": "Keine Sprache gefunden. Füge unten deine erste Sprache hinzu.", + "please_select_a_language": "Bitte wähle eine Sprache aus", + "remove_language": "Sprache entfernen", + "remove_language_from_surveys_to_remove_it_from_project": "Bitte entferne die Sprache aus diesen Umfragen, um sie aus dem Projekt zu entfernen.", + "search_items": "Artikel suchen", + "translate": "übersetzen" + }, + "look": { + "add_background_color": "Hintergrundfarbe hinzufügen", + "add_background_color_description": "Füge dem Logo eine Hintergrundfarbe hinzu.", + "app_survey_placement": "Platzierung der Umfragen", + "app_survey_placement_settings_description": "Ändere, wo Umfragen in deiner App oder Website angezeigt werden.", + "centered_modal_overlay_color": "Hintergrundfarbe bei Modal-Ansicht", + "email_customization": "E-Mail-Anpassung", + "email_customization_description": "Ändere das Aussehen und das Gefühl der E-Mails, die Formbricks in deinem Namen sendet.", + "enable_custom_styling": "Eigenes Styling aktivieren", + "enable_custom_styling_description": "Erlaube Nutzern, dieses Styling im Umfrageditor zu überschreiben.", + "failed_to_remove_logo": "Logo konnte nicht entfernt werden", + "failed_to_update_logo": "Logo konnte nicht aktualisiert werden", + "formbricks_branding": "Formbricks Branding", + "formbricks_branding_hidden": "Formbricks Branding ist versteckt.", + "formbricks_branding_settings_description": "Wir schätzen deine Unterstützung, aber verstehen, wenn Du sie ausschaltest.", + "formbricks_branding_shown": "Formbricks Branding wird angezeigt.", + "logo_removed_successfully": "Logo erfolgreich entfernt", + "logo_settings_description": "Lade dein Firmenlogo hoch, um Umfragen zu branden.", + "logo_updated_successfully": "Logo erfolgreich aktualisiert", + "logo_upload_failed": "Logo Upload fehlgeschlagen. Bitte versuche es erneut.", + "placement_updated_successfully": "Platzierung erfolgreich aktualisiert", + "remove_branding_with_a_higher_plan": "Branding entfernen mit einem höheren Plan", + "remove_logo": "Logo entfernen", + "remove_logo_confirmation": "Bist Du sicher, dass Du das Logo entfernen möchtest?", + "replace_logo": "Logo ersetzen", + "reset_styling": "Styling zurücksetzen", + "reset_styling_confirmation": "Bist Du sicher, dass Du das Styling auf die Standardeinstellungen zurücksetzen möchtest?", + "show_formbricks_branding_in": "Formbricks Branding in {type} Umfragen anzeigen", + "show_powered_by_formbricks": "Zeige 'Powered by Formbricks' Signatur", + "styling_updated_successfully": "Styling erfolgreich aktualisiert", + "theme": "Styling", + "theme_settings_description": "Erstelle ein Styling für alle Umfragen. Du kannst eigenes Styling für jede Umfrage aktivieren." + }, + "tags": { + "add": "Hinzufügen", + "add_tag": "Tag hinzufügen", + "count": "zählen", + "delete_tag_confirmation": "Bist Du sicher, dass Du diesen Tag löschen möchtest?", + "empty_message": "Markiere eine Antwort, um deine Liste der Tags hier zu finden.", + "manage_tags": "Tags verwalten", + "manage_tags_description": "Zusammenführen und Antwort-Tags entfernen.", + "merge": "Zusammenführen", + "no_tag_found": "Kein Tag gefunden", + "search_tags": "Such-Tags...", + "tag": "Tag", + "tag_already_exists": "Tag existiert bereits", + "tag_deleted": "Tag gelöscht", + "tag_updated": "Tag aktualisiert", + "tags_merged": "Tags zusammengeführt", + "unique_constraint_failed_on_the_fields": "Eindeutige Einschränkung für die Felder fehlgeschlagen" + }, + "teams": { + "manage_teams": "Teams verwalten", + "no_teams_found": "Keine Teams gefunden", + "only_organization_owners_and_managers_can_manage_teams": "Nur Organisationsinhaber und -manager können Teams verwalten.", + "permission": "Berechtigung", + "team_name": "Teamname", + "team_settings_description": "Teams und ihre Mitglieder können auf dieses Projekt und seine Umfragen zugreifen. Organisationsbesitzer und Manager können diesen Zugriff gewähren." + } + }, + "projects_environments_organizations_not_found": "Projekte, Umgebungen oder Organisationen nicht gefunden", + "segments": { + "add_filter_below": "Filter unten hinzufügen", + "add_your_first_filter_to_get_started": "Füge deinen ersten Filter hinzu, um loszulegen", + "cannot_delete_segment_used_in_surveys": "Du kannst dieses Segment nicht löschen, da es noch in folgenden Umfragen verwendet wird:", + "clone_and_edit_segment": "Duplizieren & bearbeiten", + "create_group": "Gruppe erstellen", + "create_your_first_segment": "Erstelle dein erstes Segment, um loszulegen", + "delete_segment": "Segment löschen", + "desktop": "Desktop", + "devices": "Geräte", + "edit_segment": "Segment bearbeiten", + "error_resetting_filters": "Fehler beim Zurücksetzen der Filter", + "error_saving_segment": "Fehler beim Speichern des Segments", + "ex_fully_activated_recurring_users": "Beispiel: Wiederkehrende Nutzer", + "ex_power_users": "Ex-Power-User", + "filters_reset_successfully": "Filter erfolgreich zurückgesetzt", + "here": "hier", + "hide_filters": "Filter ausblenden", + "identifying_users": "Benutzer identifizieren", + "invalid_segment": "Ungültiges Segment", + "invalid_segment_filters": "Ungültige Filter. Bitte überprüfe die Filter und versuche es erneut.", + "load_segment": "Segment laden", + "most_active_users_in_the_last_30_days": "Die aktivsten Nutzer in den letzten 30 Tagen", + "no_attributes_yet": "Noch keine Attribute", + "no_filters_yet": "Es gibt noch keine Filter", + "no_segments_yet": "Du hast momentan keine gespeicherten Segmente.", + "person_and_attributes": "Person & Attribute", + "phone": "Handy", + "please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Bitte entferne das Segment aus diesen Umfragen, um es zu löschen.", + "pre_segment_users": "Segmentiere deine Nutzer im Voraus mit Attributfiltern.", + "remove_all_filters": "Alle Filter entfernen", + "reset_all_filters": "Alle Filter zurücksetzen", + "save_as_new_segment": "Als neues Segment speichern", + "save_your_filters_as_a_segment_to_use_it_in_other_surveys": "Speichere deine Filter als Segment, um sie in anderen Umfragen zu verwenden", + "segment_created_successfully": "Segment erfolgreich erstellt", + "segment_deleted_successfully": "Segment erfolgreich gelöscht", + "segment_id": "Segment-ID", + "segment_saved_successfully": "Segment erfolgreich gespeichert", + "segment_updated_successfully": "Segment erfolgreich aktualisiert", + "segments_help_you_target_users_with_same_characteristics_easily": "Segmente helfen dir, Nutzer mit denselben Merkmalen zu erreichen", + "target_audience": "Zielgruppe", + "this_action_resets_all_filters_in_this_survey": "Diese Aktion setzt alle Filter in dieser Umfrage zurück", + "this_segment_is_used_in_other_surveys": "Dieser Abschnitt wird in anderen Umfragen verwendet. Änderungen vornehmen", + "title_is_required": "Der Titel ist erforderlich.", + "unknown_filter_type": "Unbekannter Filtertyp", + "unlock_segments_description": "Organisiere Kontakte in Segmente, um spezifische Nutzergruppen anzusprechen", + "unlock_segments_title": "Segmente mit einem höheren Plan freischalten", + "user_targeting_is_currently_only_available_when": "Benutzerzielgruppen sind derzeit nur verfügbar, wenn", + "value_cannot_be_empty": "Wert darf nicht leer sein.", + "value_must_be_a_number": "Wert muss eine Zahl sein.", + "view_filters": "Filter anzeigen", + "where": "Wo", + "with_the_formbricks_sdk": "mit dem Formbricks SDK" + }, + "settings": { + "api_keys": { + "add_api_key": "API-Schlüssel hinzufügen", + "add_permission": "Berechtigung hinzufügen", + "api_keys_description": "Verwalte API-Schlüssel, um auf die Formbricks-Management-APIs zuzugreifen" + }, + "billing": { + "10000_monthly_responses": "10,000 monatliche Antworten", + "1500_monthly_responses": "1,500 monatliche Antworten", + "2000_monthly_identified_users": "2,000 monatlich identifizierte Nutzer", + "30000_monthly_identified_users": "30,000 monatlich identifizierte Nutzer", + "3_projects": "3 Projekte", + "5000_monthly_responses": "5,000 monatliche Antworten", + "5_projects": "5 Projekte", + "7500_monthly_identified_users": "7,500 monatlich identifizierte Nutzer", + "advanced_targeting": "Erweitertes Targeting", + "all_integrations": "Alle Integrationen", + "all_surveying_features": "Alle Umfragefunktionen", + "annually": "Jährlich", + "api_webhooks": "API & Webhooks", + "app_surveys": "In-app Umfragen", + "contact_us": "Kontaktiere uns", + "current": "aktuell", + "current_plan": "Aktueller Plan", + "current_tier_limit": "Aktuelles Limit", + "custom_miu_limit": "Benutzerdefiniertes MIU-Limit", + "custom_project_limit": "Benutzerdefiniertes Projektlimit", + "customer_success_manager": "Customer Success Manager", + "email_embedded_surveys": "Eingebettete Umfragen in E-Mails", + "email_support": "E-Mail-Support", + "enterprise": "Enterprise", + "enterprise_description": "Premium-Support und benutzerdefinierte Limits.", + "everybody_has_the_free_plan_by_default": "Jeder hat standardmäßig den kostenlosen Plan!", + "everything_in_free": "Alles in 'Free''", + "everything_in_scale": "Alles in 'Scale''", + "everything_in_startup": "Alles in 'Startup''", + "free": "Kostenlos", + "free_description": "Unbegrenzte Umfragen, Teammitglieder und mehr.", + "get_2_months_free": "2 Monate gratis", + "get_in_touch": "Kontaktiere uns", + "link_surveys": "Umfragen verlinken (teilbar)", + "logic_jumps_hidden_fields_recurring_surveys": "Logik, versteckte Felder, wiederkehrende Umfragen, usw.", + "manage_card_details": "Karteninformationen verwalten", + "manage_subscription": "Abonnement verwalten", + "monthly": "Monatlich", + "monthly_identified_users": "Monatlich identifizierte Nutzer", + "multi_language_surveys": "Mehrsprachige Umfragen", + "per_month": "pro Monat", + "per_year": "pro Jahr", + "plan_upgraded_successfully": "Plan erfolgreich aktualisiert", + "premium_support_with_slas": "Premium-Support mit SLAs", + "priority_support": "Priorisierter Support", + "remove_branding": "Branding entfernen", + "say_hi": "Sag Hi!", + "scale": "Scale", + "scale_description": "Erweiterte Funktionen für größere Unternehmen.", + "startup": "Start-up", + "startup_description": "Alles in 'Free' mit zusätzlichen Funktionen.", + "switch_plan": "Plan wechseln", + "switch_plan_confirmation_text": "Bist du sicher, dass du zum {plan}-Plan wechseln möchtest? Dir werden {price} {period} berechnet.", + "team_access_roles": "Rollen für Teammitglieder", + "technical_onboarding": "Technische Einführung", + "unable_to_upgrade_plan": "Plan kann nicht aktualisiert werden", + "unlimited_apps_websites": "Unbegrenzte Apps & Websites", + "unlimited_miu": "Unbegrenzte MIU", + "unlimited_projects": "Unbegrenzte Projekte", + "unlimited_responses": "Unbegrenzte Antworten", + "unlimited_surveys": "Unbegrenzte Umfragen", + "unlimited_team_members": "Unbegrenzte Teammitglieder", + "upgrade": "Upgrade", + "uptime_sla_99": "Betriebszeit SLA (99%)", + "website_surveys": "Website-Umfragen" + }, + "enterprise": { + "ai": "KI-Analyse", + "audit_logs": "Audit Logs", + "coming_soon": "Kommt bald", + "contacts_and_segments": "Kontaktverwaltung & Segmente", + "enterprise_features": "Unternehmensfunktionen", + "get_an_enterprise_license_to_get_access_to_all_features": "Hol dir eine Enterprise-Lizenz, um Zugriff auf alle Funktionen zu erhalten.", + "keep_full_control_over_your_data_privacy_and_security": "Behalte die volle Kontrolle über deine Daten, Privatsphäre und Sicherheit.", + "no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Ganz unkompliziert: Fordere eine kostenlose 30-Tage-Testlizenz an, um alle Funktionen zu testen, indem Du dieses Formular ausfüllst:", + "no_credit_card_no_sales_call_just_test_it": "Keine Kreditkarte. Kein Verkaufsgespräch. Einfach testen :)", + "on_request": "Auf Anfrage", + "organization_roles": "Organisationsrollen (Admin, Editor, Entwickler, etc.)", + "questions_please_reach_out_to": "Fragen? Bitte melde Dich bei", + "request_30_day_trial_license": "30-Tage-Testlizenz anfordern", + "saml_sso": "SAML-SSO", + "service_level_agreement": "Service-Level-Vereinbarung", + "soc2_hipaa_iso_27001_compliance_check": "SOC2-, HIPAA- und ISO 27001-Konformitätsprüfung", + "sso": "SSO (Google, Microsoft, OpenID Connect)", + "teams": "Teams & Zugriffskontrolle (Lesen, Lesen & Schreiben, Verwalten)", + "unlock_the_full_power_of_formbricks_free_for_30_days": "Schalte die volle Power von Formbricks frei. 30 Tage kostenlos.", + "your_enterprise_license_is_active_all_features_unlocked": "Deine Unternehmenslizenz ist aktiv. Alle Funktionen freigeschaltet." + }, + "general": { + "bulk_invite_warning_description": "Bitte beachte, dass im Free-Plan alle Organisationsmitglieder automatisch die Rolle \"Owner\" zugewiesen bekommen, unabhängig von der im CSV-File angegebenen Rolle.", + "cannot_delete_only_organization": "Das ist deine einzige Organisation, sie kann nicht gelöscht werden. Erstelle zuerst eine neue Organisation.", + "cannot_leave_only_organization": "Du kannst diese Organisation nicht verlassen, da es deine einzige Organisation ist. Erstelle zuerst eine neue Organisation.", + "copy_invite_link_to_clipboard": "Einladungslink in die Zwischenablage kopieren", + "create_new_organization": "Neue Organisation erstellen", + "create_new_organization_description": "Erstelle eine neue Organisation, um weitere Projekte zu verwalten.", + "customize_email_with_a_higher_plan": "E-Mail-Anpassung mit einem höheren Plan", + "delete_organization": "Organisation löschen", + "delete_organization_description": "Organisation mit allen Projekten einschließlich aller Umfragen, Antworten, Personen, Aktionen und Attribute löschen", + "delete_organization_warning": "Bevor Du mit dem Löschen dieser Organisation fortfährst, sei dir bitte der folgenden Konsequenzen bewusst:", + "delete_organization_warning_1": "Dauerhafte Entfernung aller Projekte, die mit dieser Organisation verbunden sind.", + "delete_organization_warning_2": "Diese Aktion kann nicht rückgängig gemacht werden. Wenn es weg ist, ist es weg.", + "delete_organization_warning_3": "Bitte gib {organizationName} in das folgende Feld ein, um die endgültige Löschung dieser Organisation zu bestätigen:", + "eliminate_branding_with_whitelabel": "Entferne Formbricks Branding und aktiviere zusätzliche White-Label-Anpassungsoptionen.", + "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.", + "enable_formbricks_ai": "Formbricks KI aktivieren", + "error_deleting_organization_please_try_again": "Fehler beim Löschen der Organisation. Bitte versuche es erneut.", + "formbricks_ai": "Formbricks KI", + "formbricks_ai_description": "Erhalte personalisierte Einblicke aus deinen Umfrageantworten mit Formbricks KI", + "formbricks_ai_disable_success_message": "Formbricks KI wurde erfolgreich deaktiviert.", + "formbricks_ai_enable_success_message": "Formbricks KI erfolgreich aktiviert.", + "formbricks_ai_privacy_policy_text": "Durch die Aktivierung von Formbricks KI stimmst Du den aktualisierten", + "from_your_organization": "von deiner Organisation", + "invitation_sent_once_more": "Einladung nochmal gesendet.", + "invite_deleted_successfully": "Einladung erfolgreich gelöscht", + "invited_on": "Eingeladen am {date}", + "invites_failed": "Einladungen fehlgeschlagen", + "leave_organization": "Organisation verlassen", + "leave_organization_description": "Du wirst diese Organisation verlassen und den Zugriff auf alle Umfragen und Antworten verlieren. Du kannst nur wieder beitreten, wenn Du erneut eingeladen wirst.", + "leave_organization_ok_btn_text": "Ja, Organisation verlassen", + "leave_organization_title": "Bist Du sicher?", + "logo_in_email_header": "Logo in der E-Mail-Kopfzeile", + "logo_removed_successfully": "Logo erfolgreich entfernt", + "logo_saved_successfully": "Logo erfolgreich gespeichert", + "manage_members": "Mitglieder verwalten", + "manage_members_description": "Mitglieder in deiner Organisation hinzufügen oder entfernen.", + "member_deleted_successfully": "Mitglied erfolgreich gelöscht", + "member_invited_successfully": "Mitglied erfolgreich eingeladen", + "once_its_gone_its_gone": "Watt fott is, is fott.", + "only_org_owner_can_perform_action": "Nur der Besitzer kann die Organisation löschen.", + "organization_created_successfully": "Organisation erfolgreich erstellt!", + "organization_deleted_successfully": "Organisation erfolgreich gelöscht.", + "organization_invite_link_ready": "Dein Einladungslink für die Organisation ist fertig!", + "organization_name": "Organisationsname", + "organization_name_description": "Gib deiner Organisation einen Namen.", + "organization_name_placeholder": "z. B. Powerpuff Girls", + "organization_name_updated_successfully": "Organisationsname erfolgreich aktualisiert", + "organization_settings": "Organisationseinstellungen", + "please_add_a_logo": "Bitte füge ein Logo hinzu", + "please_check_csv_file": "Bitte überprüfe die CSV-Datei und stelle sicher, dass sie unserem Format entspricht", + "please_save_logo_before_sending_test_email": "Bitte speichere das Logo, bevor Du einen Test-E-Mail sendest.", + "remove_logo": "Logo entfernen", + "replace_logo": "Logo ersetzen", + "resend_invitation_email": "Einladungsemail erneut senden", + "share_invite_link": "Einladungslink teilen", + "share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:", + "test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet", + "use_multi_language_surveys_with_a_higher_plan": "Nutze mehrsprachige Umfragen mit einem höheren Plan", + "use_multi_language_surveys_with_a_higher_plan_description": "Befrage deine Nutzer in verschiedenen Sprachen." + }, + "notifications": { + "auto_subscribe_to_new_surveys": "Neue Umfragenbenachrichtigungen abonnieren", + "email_alerts_surveys": "E-Mail-Benachrichtigungen (Umfragen)", + "every_response": "Für jede Antwort", + "every_response_tooltip": "Sendet vollständige Antworten, keine Teilantworten.", + "need_slack_or_discord_notifications": "Brauchst Du Slack- oder Discord-Benachrichtigungen", + "notification_settings_updated": "Benachrichtigungseinstellungen aktualisiert", + "set_up_an_alert_to_get_an_email_on_new_responses": "Richte eine Benachrichtigung ein, um eine E-Mail bei neuen Antworten zu erhalten", + "stay_up_to_date_with_a_Weekly_every_Monday": "Bleib auf dem Laufenden mit einem wöchentlichen Update jeden Montag", + "use_the_integration": "Integration nutzen", + "want_to_loop_in_organization_mates": "Willst Du die Organisationskollegen einbeziehen?", + "weekly_summary_projects": "Wöchentliche Zusammenfassung (Projekte)", + "you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore": "Du wirst nicht mehr automatisch zu den Umfragen dieser Organisation angemeldet!", + "you_will_not_receive_any_more_emails_for_responses_on_this_survey": "Du wirst keine weiteren E-Mails für Antworten auf diese Umfrage erhalten!" + }, + "profile": { + "account_deletion_consequences_warning": "Was passiert, wenn Du das Konto löschst", + "avatar_update_failed": "Aktualisierung des Avatars fehlgeschlagen. Bitte versuche es erneut.", + "backup_code": "Backup-Code", + "change_image": "Bild ändern", + "confirm_delete_account": "Lösche dein Konto mit all deinen persönlichen Informationen und Daten", + "confirm_delete_my_account": "Konto löschen", + "confirm_your_current_password_to_get_started": "Bestätige dein aktuelles Passwort, um loszulegen.", + "delete_account": "Konto löschen", + "disable_two_factor_authentication": "Zwei-Faktor-Authentifizierung deaktivieren", + "disable_two_factor_authentication_description": "Wenn Du die Zwei-Faktor-Authentifizierung deaktivieren musst, empfehlen wir, sie so schnell wie möglich wieder zu aktivieren.", + "each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Jeder Backup-Code kann genau einmal verwendet werden, um Zugang ohne deinen Authenticator zu gewähren.", + "enable_two_factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren", + "enter_the_code_from_your_authenticator_app_below": "Gib den Code aus deiner Authentifizierungs-App unten ein.", + "file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.", + "invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.", + "lost_access": "Zugriff verloren", + "or_enter_the_following_code_manually": "Oder gib den folgenden Code manuell ein:", + "organization_identification": "Hilf deiner Organisation, Dich auf Formbricks zu identifizieren", + "organizations_delete_message": "Du bist der einzige Besitzer dieser Organisationen, also werden sie auch gelöscht.", + "permanent_removal_of_all_of_your_personal_information_and_data": "Dauerhafte Entfernung all deiner persönlichen Informationen und Daten", + "personal_information": "Persönliche Informationen", + "please_enter_email_to_confirm_account_deletion": "Bitte gib {email} in das folgende Feld ein, um die endgültige Löschung deines Kontos zu bestätigen:", + "profile_updated_successfully": "Dein Profil wurde erfolgreich aktualisiert", + "remove_image": "Bild entfernen", + "save_the_following_backup_codes_in_a_safe_place": "Speichere die folgenden Backup-Codes an einem sicheren Ort.", + "scan_the_qr_code_below_with_your_authenticator_app": "Scanne den QR-Code unten mit deiner Authentifizierungs-App.", + "security_description": "Verwalte dein Passwort und andere Sicherheitseinstellungen wie Zwei-Faktor-Authentifizierung (2FA).", + "two_factor_authentication": "Zwei-Faktor-Authentifizierung", + "two_factor_authentication_description": "Füge eine zusätzliche Sicherheitsebene zu deinem Konto hinzu, falls dein Passwort gestohlen wird.", + "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Zwei-Faktor-Authentifizierung aktiviert. Bitte gib den sechsstelligen Code aus deiner Authentifizierungs-App ein.", + "two_factor_code": "Zwei-Faktor-Code", + "unlock_two_factor_authentication": "Zwei-Faktor-Authentifizierung mit einem höheren Plan freischalten", + "update_personal_info": "Persönliche Daten aktualisieren", + "upload_image": "Bild hochladen", + "warning_cannot_delete_account": "Du bist der einzige Besitzer dieser Organisation. Bitte übertrage das Eigentum zuerst an ein anderes Mitglied.", + "warning_cannot_undo": "Das kann nicht rückgängig gemacht werden", + "you_must_select_a_file": "Du musst eine Datei auswählen." + }, + "teams": { + "add_members_description": "Füge Mitglieder zum Team hinzu und bestimme ihre Rolle.", + "add_projects_description": "Kontrolliere, auf welche Projekte die Teammitglieder zugreifen können.", + "all_members_added": "Alle Mitglieder zu diesem Team hinzugefügt.", + "all_projects_added": "Alle Projekte zu diesem Team hinzugefügt.", + "are_you_sure_you_want_to_delete_this_team": "Sind Sie sicher, dass Sie dieses Team löschen möchten? Dadurch wird auch der Zugriff auf alle Projekte und Umfragen entfernt, die mit diesem Team verbunden sind.", + "billing_role_description": "Haben nur Zugriff auf Abrechnungsinformationen.", + "bulk_invite": "Sammel-Einladung", + "contributor": "Mitwirkender", + "create": "Erstellen", + "create_first_team_message": "Du musst zuerst ein Team erstellen.", + "create_new_team": "Neues Team erstellen", + "delete_team": "Team löschen", + "empty_teams_state": "Erstelle dein erstes Team.", + "enter_team_name": "Teamname eingeben", + "individual": "Individuelle", + "invite_member": "Mitglied einladen", + "invite_member_description": "Füge deine Kollegen zu dieser Organisation hinzu.", + "manage": "Verwalten", + "manage_team": "Team verwalten", + "manage_team_disabled": "Nur Organisationsbesitzer, Manager und Team-Admins können Teams verwalten.", + "manager_role_description": "Manager können auf alle Projekte zugreifen und Mitglieder hinzufügen und entfernen.", + "member_role_description": "Mitglieder können in ausgewählten Projekten arbeiten.", + "member_role_info_message": "Um neuen Mitgliedern Zugriff auf ein Projekt zu geben, füge sie bitte unten einem Team hinzu. Mit Teams kannst du steuern, wer auf welches Projekt zugreifen kann.", + "owner_role_description": "Besitzer haben die volle Kontrolle über die Organisation.", + "please_fill_all_member_fields": "Bitte fülle alle Felder aus, um ein neues Mitglied hinzuzufügen.", + "please_fill_all_project_fields": "Bitte fülle alle Felder aus, um ein neues Projekt hinzuzufügen.", + "read": "Lesen", + "read_write": "Lesen & Schreiben", + "team_admin": "Team-Admin", + "team_created_successfully": "Team erfolgreich erstellt.", + "team_deleted_successfully": "Team erfolgreich gelöscht.", + "team_deletion_not_allowed": "Du darfst dieses Team nicht löschen.", + "team_name": "Teamname", + "team_name_settings_title": "{teamName} Einstellungen", + "team_select_placeholder": "Teamnamen suchen...", + "team_settings_description": "Teammitglieder, Zugriffsrechte und mehr verwalten.", + "team_updated_successfully": "Team erfolgreich aktualisiert", + "teams": "Teams", + "teams_description": "Mitglieder in Teams einteilen und Teams Zugriff auf Projekte gewähren.", + "unlock_teams_description": "Verwalten Sie, welche Organisationsmitglieder Zugriff auf bestimmte Projekte und Umfragen haben.", + "unlock_teams_title": "Schalten Sie Teams mit einem höheren Plan frei.", + "upgrade_plan_notice_message": "Freischalten von Organisationsrollen mit einem höheren Plan.", + "you_are_a_member": "Du bist Mitglied" + } + }, + "surveys": { + "all_set_time_to_create_first_survey": "Alles klar! Zeit, deine erste Umfrage zu erstellen", + "alphabetical": "alphabetisch", + "copy_survey": "Umfrage kopieren", + "copy_survey_description": "Kopiere diese Umfrage in eine andere Umgebung", + "copy_survey_error": "Kopieren der Umfrage fehlgeschlagen", + "copy_survey_link_to_clipboard": "Umfragelink in die Zwischenablage kopieren", + "copy_survey_success": "Umfrage erfolgreich kopiert!", + "delete_survey_and_responses_warning": "Bist Du sicher, dass Du diese Umfrage und alle ihre Antworten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "edit": { + "1_choose_the_default_language_for_this_survey": "1. Wähle die Standardsprache für diese Umfrage:", + "2_activate_translation_for_specific_languages": "2. Übersetzung für bestimmte Sprachen aktivieren:", + "add": "+ hinzufügen", + "add_a_delay_or_auto_close_the_survey": "Füge eine Verzögerung hinzu oder schließe die Umfrage automatisch.", + "add_a_four_digit_pin": "Füge eine vierstellige PIN hinzu", + "add_a_new_question_to_your_survey": "Neue Frage hinzufügen", + "add_a_variable_to_calculate": "Variable hinzufügen", + "add_action_below": "Aktion unten hinzufügen", + "add_choice_below": "Auswahl unten hinzufügen", + "add_color_coding": "Farbkodierung hinzufügen", + "add_color_coding_description": "Füge rote, orange und grüne Farbcodes zu den Optionen hinzu.", + "add_column": "Spalte hinzufügen", + "add_condition_below": "Bedingung unten hinzufügen", + "add_custom_styles": "Benutzerdefinierte Stile hinzufügen", + "add_delay_before_showing_survey": "Verzögerung vor dem Anzeigen der Umfrage hinzufügen", + "add_description": "Beschreibung hinzufügen", + "add_ending": "Abschluss hinzufügen", + "add_ending_below": "Abschluss unten hinzufügen", + "add_hidden_field_id": "Verstecktes Feld ID hinzufügen", + "add_highlight_border": "Rahmen hinzufügen", + "add_highlight_border_description": "Füge deiner Umfragekarte einen äußeren Rahmen hinzu.", + "add_logic": "Logik hinzufügen", + "add_option": "Option hinzufügen", + "add_other": "Anderes hinzufügen", + "add_photo_or_video": "Foto oder Video hinzufügen", + "add_pin": "PIN hinzufügen", + "add_question": "Frage hinzufügen", + "add_question_below": "Frage unten hinzufügen", + "add_row": "Zeile hinzufügen", + "add_variable": "Variable hinzufügen", + "address_fields": "Adressfelder", + "address_line_1": "Adresszeile 1", + "address_line_2": "Adresszeile 2", + "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_other_answers_will_continue_to": "Alle anderen Antworten werden weiterhin", + "allow_file_type": "Dateityp begrenzen", + "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", + "always_show_survey": "Umfrage immer anzeigen", + "and_launch_surveys_in_your_website_or_app": "und Umfragen auf deiner Website oder App starten.", + "animation": "Animation", + "app_survey_description": "Bette eine Umfrage in deine Web-App oder Website ein, um Antworten zu sammeln.", + "assign": "Zuweisen =", + "audience": "Publikum", + "auto_close_on_inactivity": "Automatisches Schließen bei Inaktivität", + "automatically_close_survey_after": "Umfrage automatisch schließen nach", + "automatically_close_the_survey_after_a_certain_number_of_responses": "Schließe die Umfrage automatisch nach einer bestimmten Anzahl von Antworten.", + "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Schließe die Umfrage automatisch, wenn der Benutzer nach einer bestimmten Anzahl von Sekunden nicht antwortet.", + "automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Schließt die Umfrage automatisch zu Beginn des Tages (UTC).", + "automatically_mark_the_survey_as_complete_after": "Umfrage automatisch als abgeschlossen markieren nach", + "automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Umfrage automatisch zu Beginn des Tages (UTC) freigeben.", + "back_button_label": "Zurück\"- Button ", + "background_styling": "Hintergründe", + "blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "Blockiert die Umfrage, wenn bereits eine Antwort mit der Single Use Id (suId) existiert.", + "blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "Blockiert Umfrage, wenn die Umfrage-URL keine Single-Use-ID (suId) hat.", + "brand_color": "Markenfarbe", + "brightness": "Helligkeit", + "button_label": "Beschriftung", + "button_to_continue_in_survey": "Fahre in der Umfrage fort", + "button_to_link_to_external_url": "Verlinke auf externe URL", + "button_url": "URL", + "cal_username": "Cal.com Benutzername oder Benutzername/Ereignis", + "calculate": "Berechnen", + "capture_a_new_action_to_trigger_a_survey_on": "Erfasse eine neue Aktion, um eine Umfrage auszulösen.", + "capture_new_action": "Neue Aktion erfassen", + "card_arrangement_for_survey_type_derived": "Kartenanordnung für {surveyTypeDerived} Umfragen", + "card_background_color": "Hintergrundfarbe der Karte", + "card_border_color": "Farbe des Kartenrandes", + "card_shadow_color": "Farbton des Kartenschattens", + "card_styling": "Kartenstil", + "casual": "Lässig", + "caution_text": "Änderungen werden zu Inkonsistenzen führen", + "centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe", + "change_anyway": "Trotzdem ändern", + "change_background": "Hintergrund ändern", + "change_question_type": "Fragetyp ändern", + "change_the_background_color_of_the_card": "Hintergrundfarbe der Karte ändern.", + "change_the_background_color_of_the_input_fields": "Hintergrundfarbe der Eingabefelder ändern.", + "change_the_background_to_a_color_image_or_animation": "Hintergrund zu einer Farbe, einem Bild oder einer Animation ändern.", + "change_the_border_color_of_the_card": "Randfarbe der Karte ändern.", + "change_the_border_color_of_the_input_fields": "Randfarbe der Eingabefelder ändern.", + "change_the_border_radius_of_the_card_and_the_inputs": "Radius der Ränder der Karte und der Eingabefelder ändern.", + "change_the_brand_color_of_the_survey": "Markenfarbe der Umfrage ändern.", + "change_the_placement_of_this_survey": "Platzierung dieser Umfrage ändern.", + "change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.", + "change_the_shadow_color_of_the_card": "Schattenfarbe der Karte ändern.", + "changes_saved": "Änderungen gespeichert.", + "character_limit_toggle_description": "Begrenzen Sie, wie kurz oder lang eine Antwort sein kann.", + "character_limit_toggle_title": "Fügen Sie Zeichenbeschränkungen hinzu", + "checkbox_label": "Checkbox-Beschriftung", + "choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.", + "choose_where_to_run_the_survey": "Wähle, wo die Umfrage durchgeführt werden soll.", + "city": "Stadt", + "close_survey_on_date": "Umfrage am Datum schließen", + "close_survey_on_response_limit": "Umfrage bei Erreichen des Antwortlimits schließen", + "color": "Farbe", + "columns": "Spalten", + "company": "Firma", + "company_logo": "Firmenlogo", + "completed_responses": "abgeschlossene Antworten", + "concat": "Verketten +", + "conditional_logic": "Bedingte Logik", + "confirm_default_language": "Standardsprache bestätigen", + "confirm_survey_changes": "Änderungen der Umfrage bestätigen", + "contact_fields": "Kontaktfelder", + "contains": "enthält", + "continue_to_settings": "Weiter zu den Einstellungen", + "control_which_file_types_can_be_uploaded": "Steuere, welche Dateitypen hochgeladen werden können.", + "convert_to_multiple_choice": "In Multiple-Choice umwandeln", + "convert_to_single_choice": "In Einzelauswahl umwandeln", + "country": "Land", + "create_group": "Gruppe erstellen", + "create_your_own_survey": "Erstelle deine eigene Umfrage", + "css_selector": "CSS-Selektor", + "custom_hostname": "Benutzerdefinierter Hostname", + "darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.", + "date_format": "Datumsformat", + "days_before_showing_this_survey_again": "Tage, bevor diese Umfrage erneut angezeigt wird.", + "decide_how_often_people_can_answer_this_survey": "Entscheide, wie oft Leute diese Umfrage beantworten können.", + "delete_choice": "Auswahl löschen", + "description": "Beschreibung", + "disable_the_visibility_of_survey_progress": "Deaktiviere die Sichtbarkeit des Umfragefortschritts.", + "display_an_estimate_of_completion_time_for_survey": "Zeige eine Schätzung der Fertigstellungszeit für die Umfrage an", + "display_number_of_responses_for_survey": "Anzahl der Antworten für Umfrage anzeigen", + "divide": "Teilen /", + "does_not_contain": "Enthält nicht", + "does_not_end_with": "Endet nicht mit", + "does_not_equal": "Ungleich", + "does_not_include_all_of": "Enthält nicht alle von", + "does_not_include_one_of": "Enthält nicht eines von", + "does_not_start_with": "Fängt nicht an mit", + "edit_recall": "Erinnerung bearbeiten", + "edit_translations": "{lang} -Übersetzungen bearbeiten", + "enable_encryption_of_single_use_id_suid_in_survey_url": "Single Use Id (suId) in der Umfrage-URL verschlüsseln.", + "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Teilnehmer können die Umfragesprache jederzeit während der Umfrage ändern.", + "end_screen_card": "Abschluss-Karte", + "ending_card": "Abschluss-Karte", + "ending_card_used_in_logic": "Diese Abschlusskarte wird in der Logik der Frage {questionIndex} verwendet.", + "ends_with": "endet mit", + "equals": "Gleich", + "equals_one_of": "Entspricht einem von", + "error_publishing_survey": "Beim Veröffentlichen der Umfrage ist ein Fehler aufgetreten.", + "error_saving_changes": "Fehler beim Speichern der Änderungen", + "even_after_they_submitted_a_response_e_g_feedback_box": "Sogar nachdem sie eine Antwort eingereicht haben (z.B. Feedback-Box)", + "everyone": "Jeder", + "fallback_missing": "Fehlender Fallback", + "fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.", + "field_name_eg_score_price": "Feldname z.B. Punktzahl, Preis", + "first_name": "Vorname", + "five_points_recommended": "5 Punkte (empfohlen)", + "follow_ups": "Follow-ups", + "follow_ups_delete_modal_text": "Bist du sicher, dass du dieses Follow-up löschen möchtest?", + "follow_ups_delete_modal_title": "Follow-up löschen?", + "follow_ups_empty_description": "Sende Nachrichten an Teilnehmer der Umfrage, dich selbst oder Teammitglieder.", + "follow_ups_empty_heading": "Automatische Follow-ups versenden", + "follow_ups_ending_card_delete_modal_text": "Dieser Abschluss wird in Follow-ups verwendet. Wenn Sie ihn löschen, wird er aus allen Follow-ups entfernt. Sind Sie sicher, dass Sie ihn löschen möchten?", + "follow_ups_ending_card_delete_modal_title": "Abschlusskarte löschen?", + "follow_ups_hidden_field_error": "Verstecktes Feld wird in einem Follow-up verwendet. Bitte entfernen Sie es zuerst aus dem Follow-up.", + "follow_ups_item_ending_tag": "Abschluss", + "follow_ups_item_issue_detected_tag": "Problem erkannt", + "follow_ups_item_response_tag": "Jede Antwort", + "follow_ups_item_send_email_tag": "E-Mail senden", + "follow_ups_modal_action_attach_response_data_description": "Füge die Daten der Umfrageantwort zur Nachverfolgung hinzu", + "follow_ups_modal_action_attach_response_data_label": "Antwortdaten anhängen", + "follow_ups_modal_action_body_label": "Inhalt", + "follow_ups_modal_action_body_placeholder": "Inhalt der E-Mail", + "follow_ups_modal_action_email_content": "E-Mail Inhalt", + "follow_ups_modal_action_email_settings": "E-Mail Einstellungen", + "follow_ups_modal_action_from_description": "Absender E-Mail", + "follow_ups_modal_action_from_label": "Von", + "follow_ups_modal_action_label": "Aktion", + "follow_ups_modal_action_replyTo_description": "Wenn der Empfänger antwortet, geht die Antwort an diese E-Mail-Adresse", + "follow_ups_modal_action_replyTo_label": "Antwort an", + "follow_ups_modal_action_subject": "Danke für deine Antworten!", + "follow_ups_modal_action_subject_label": "Betreff", + "follow_ups_modal_action_subject_placeholder": "Betreff der E-Mail", + "follow_ups_modal_action_to_description": "Empfänger-E-Mail-Adresse", + "follow_ups_modal_action_to_label": "An", + "follow_ups_modal_action_to_warning": "Kein E-Mail-Feld in der Umfrage gefunden.", + "follow_ups_modal_create_heading": "Neues Follow-up erstellen", + "follow_ups_modal_edit_heading": "Follow-up bearbeiten", + "follow_ups_modal_edit_no_id": "Keine Survey Follow-up-ID angegeben, das Survey-Follow-up kann nicht aktualisiert werden", + "follow_ups_modal_name_label": "Name des Follow-ups", + "follow_ups_modal_name_placeholder": "Benenne dein Follow-up", + "follow_ups_modal_subheading": "Sende Nachrichten an Teilnehmer, dich selbst oder Teammitglieder", + "follow_ups_modal_trigger_description": "Wann soll dieses Follow-up ausgelöst werden?", + "follow_ups_modal_trigger_label": "Auslöser", + "follow_ups_modal_trigger_type_ending": "Teilnehmer sieht einen bestimmten Abschluss", + "follow_ups_modal_trigger_type_ending_select": "Abschlüsse auswählen: ", + "follow_ups_modal_trigger_type_ending_warning": "Keine Abschlüsse in der Umfrage gefunden!", + "follow_ups_modal_trigger_type_response": "Teilnehmer schließt Umfrage ab", + "follow_ups_new": "Neues Follow-up", + "follow_ups_upgrade_button_text": "Upgrade, um Follow-ups zu aktivieren", + "form_styling": "Umfrage Styling", + "formbricks_ai_description": "Beschreibe deine Umfrage und lass Formbricks KI die Umfrage für Dich erstellen", + "formbricks_ai_generate": "erzeugen", + "formbricks_ai_prompt_placeholder": "Gib Umfrageinformationen ein (z.B. wichtige Themen, die abgedeckt werden sollen)", + "formbricks_sdk_is_not_connected": "Formbricks SDK ist nicht verbunden", + "four_points": "4 Punkte", + "heading": "Überschrift", + "hidden_field_added_successfully": "Verstecktes Feld erfolgreich hinzugefügt", + "hide_advanced_settings": "Erweiterte Einstellungen ausblenden", + "hide_back_button": "'Zurück'-Button ausblenden", + "hide_back_button_description": "Den Zurück-Button in der Umfrage nicht anzeigen", + "hide_logo": "Logo verstecken", + "hide_progress_bar": "Fortschrittsbalken ausblenden", + "hide_the_logo_in_this_specific_survey": "Logo in dieser speziellen Umfrage verstecken", + "hostname": "Hostname", + "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein", + "how_it_works": "Wie es funktioniert", + "if_you_need_more_please": "Wenn Du mehr brauchst, bitte", + "if_you_really_want_that_answer_ask_until_you_get_it": "Wenn Du diese Antwort brauchst, frag so lange, bis Du sie bekommst.", + "ignore_waiting_time_between_surveys": "Wartezeit zwischen Umfragen ignorieren", + "image": "Bild", + "includes_all_of": "Enthält alles von", + "includes_one_of": "Enthält eines von", + "initial_value": "Anfangswert", + "inner_text": "Innerer Text", + "input_border_color": "Randfarbe des Eingabefelds", + "input_color": "Farbe des Eingabefelds", + "invalid_targeting": "Ungültiges Targeting: Bitte überprüfe deine Zielgruppenfilter", + "invalid_video_url_warning": "Bitte gib eine gültige YouTube-, Vimeo- oder Loom-URL ein. Andere Video-Plattformen werden derzeit nicht unterstützt.", + "invalid_youtube_url": "Ungültige YouTube-URL", + "is_accepted": "Ist akzeptiert", + "is_after": "Ist nach", + "is_before": "Ist vor", + "is_booked": "Ist gebucht", + "is_clicked": "Wird geklickt", + "is_completely_submitted": "Vollständig eingereicht", + "is_not_set": "Ist nicht festgelegt", + "is_partially_submitted": "Teilweise eingereicht", + "is_set": "Ist festgelegt", + "is_skipped": "Wird übersprungen", + "is_submitted": "Wird eingereicht", + "jump_to_question": "Zur Frage springen", + "keep_current_order": "Bestehende Anordnung beibehalten", + "keep_showing_while_conditions_match": "Zeige weiter, solange die Bedingungen übereinstimmen", + "key": "Schlüssel", + "last_name": "Nachname", + "let_people_upload_up_to_25_files_at_the_same_time": "Erlaube bis zu 25 Dateien gleichzeitig hochzuladen.", + "limit_file_types": "Dateitypen einschränken", + "limit_the_maximum_file_size": "Maximale Dateigröße begrenzen", + "limit_upload_file_size_to": "Maximale Dateigröße für Uploads", + "link_survey_description": "Teile einen Link zu einer Umfrageseite oder bette ihn in eine Webseite oder E-Mail ein.", + "link_used_message": "Link verwendet", + "load_segment": "Segment laden", + "logic_error_warning": "Änderungen werden zu Logikfehlern führen", + "logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage", + "long_answer": "Lange Antwort", + "lower_label": "Unteres Label", + "manage_languages": "Sprachen verwalten", + "max_file_size": "Max. Dateigröße", + "max_file_size_limit_is": "Max. Dateigröße ist", + "multiply": "Multiplizieren *", + "needed_for_self_hosted_cal_com_instance": "Benötigt für eine selbstgehostete Cal.com-Instanz", + "next_button_label": "Weiter", + "next_question": "Nächste Frage", + "no_hidden_fields_yet_add_first_one_below": "Noch keine versteckten Felder. Füge das erste unten hinzu.", + "no_images_found_for": "Keine Bilder gefunden für ''{query}\"", + "no_languages_found_add_first_one_to_get_started": "Keine Sprachen gefunden. Füge die erste hinzu, um loszulegen.", + "no_variables_yet_add_first_one_below": "Noch keine Variablen. Füge die erste hinzu.", + "number": "Nummer", + "once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations": "Sobald die Standardsprache für diese Umfrage festgelegt ist, kann sie nur geändert werden, indem die Mehrsprachigkeitsoption deaktiviert und alle Übersetzungen gelöscht werden.", + "only_display_the_survey_to_a_subset_of_the_users": "Zeige die Umfrage nur einem Teil der Nutzer an", + "only_lower_case_letters_numbers_and_underscores_are_allowed": "nur Kleinbuchstaben, Zahlen und Unterstriche sind erlaubt.", + "only_people_who_match_your_targeting_can_be_surveyed": "Nur Personen, die zu deinem Zielgruppen-Targeting passen, können befragt werden.", + "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", + "override_theme_with_individual_styles_for_this_survey": "Styling für diese Umfrage überschreiben.", + "overwrite_placement": "Platzierung überschreiben", + "overwrite_the_global_placement_of_the_survey": "Platzierung für diese Umfrage überschreiben", + "overwrites_waiting_period_between_surveys_to_x_days": "Überschreibt die Wartezeit zwischen Umfragen auf {days} Tag(e).", + "pick_a_background_from_our_library_or_upload_your_own": "Wähle einen Hintergrund aus oder lade deinen eigenen hoch.", + "picture_idx": "Bild {idx}", + "pin_can_only_contain_numbers": "PIN darf nur Zahlen enthalten.", + "pin_must_be_a_four_digit_number": "Die PIN muss eine vierstellige Zahl sein.", + "please_enter_a_file_extension": "Bitte gib eine Dateierweiterung ein.", + "please_set_a_survey_trigger": "Bitte richte einen Umfrage-Trigger ein", + "please_specify": "Bitte angeben", + "prevent_double_submission": "Doppeltes Anbschicken verhindern", + "prevent_double_submission_description": "Nur eine Antwort pro E-Mail-Adresse zulassen (beta)", + "protect_survey_with_pin": "Umfrage mit einer PIN schützen", + "protect_survey_with_pin_description": "Nur Benutzer, die die PIN haben, können auf die Umfrage zugreifen.", + "publish": "Veröffentlichen", + "question": "Frage", + "question_color": "Fragefarbe", + "question_deleted": "Frage gelöscht.", + "question_duplicated": "Frage dupliziert.", + "question_id_updated": "Frage-ID aktualisiert", + "question_used_in_logic": "Diese Frage wird in der Logik der Frage {questionIndex} verwendet.", + "randomize_all": "Alle Optionen zufällig anordnen", + "randomize_all_except_last": "Alle Optionen zufällig anordnen außer der letzten", + "range": "Reichweite", + "recontact_options": "Optionen zur erneuten Kontaktaufnahme", + "redirect_thank_you_card": "Weiterleitung anlegen", + "redirect_to_url": "Zu URL weiterleiten", + "redirect_to_url_not_available_on_free_plan": "Umleitung zu URL ist im kostenlosen Plan nicht verfügbar", + "release_survey_on_date": "Umfrage an Datum veröffentlichen", + "remove_description": "Beschreibung entfernen", + "remove_translations": "Übersetzungen entfernen", + "require_answer": "Antwort erforderlich", + "required": "Erforderlich", + "reset_to_theme_styles": "Styling zurücksetzen", + "reset_to_theme_styles_main_text": "Bist Du sicher, dass Du das Styling auf die Themenstile zurücksetzen möchtest? Dadurch wird jegliches benutzerdefinierte Styling entfernt.", + "response_limit_can_t_be_set_to_0": "Das Antwortlimit kann nicht auf 0 gesetzt werden", + "response_limit_needs_to_exceed_number_of_received_responses": "Antwortlimit muss die Anzahl der erhaltenen Antworten ({responseCount}) überschreiten.", + "response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.", + "response_options": "Antwortoptionen", + "roundness": "Rundheit", + "rows": "Zeilen", + "save_and_close": "Speichern & Schließen", + "scale": "Scale", + "search_for_images": "Nach Bildern suchen", + "seconds_after_trigger_the_survey_will_be_closed_if_no_response": "Sekunden nach dem Auslösen wird die Umfrage geschlossen, wenn keine Antwort erfolgt.", + "seconds_before_showing_the_survey": "Sekunden, bevor die Umfrage angezeigt wird.", + "select_or_type_value": "Auswählen oder Wert eingeben", + "select_ordering": "Anordnung auswählen", + "select_saved_action": "Gespeicherte Aktion auswählen", + "select_type": "Typ auswählen", + "send_survey_to_audience_who_match": "Umfrage an das Publikum senden, das übereinstimmt...", + "send_your_respondents_to_a_page_of_your_choice": "Schicke deine Befragten auf eine Seite deiner Wahl.", + "set_the_global_placement_in_the_look_feel_settings": "Stelle die globale Platzierung in den Look & Feel-Einstellungen ein.", + "seven_points": "7 Punkte", + "show_advanced_settings": "Erweiterte Einstellungen anzeigen", + "show_button": "Button anzeigen", + "show_language_switch": "Sprachwechsel anzeigen", + "show_multiple_times": "Mehrfach anzeigen", + "show_only_once": "Nur einmal anzeigen", + "show_survey_maximum_of": "Umfrage maximal anzeigen von", + "show_survey_to_users": "Umfrage % der Nutzer anzeigen", + "show_to_x_percentage_of_targeted_users": "Zeige {percentage}% der Zielbenutzer", + "simple": "Einfach", + "single_use_survey_links": "Einmalige Umfragelinks", + "single_use_survey_links_description": "Erlaube nur eine Antwort pro Umfragelink.", + "skip_button_label": "Überspringen-Button-Beschriftung", + "smiley": "Smiley", + "star": "Stern", + "starts_with": "Fängt an mit", + "state": "Bundesland", + "straight": "Gerade", + "style_the_question_texts_descriptions_and_input_fields": "Styling für Fragetexte, Beschreibungen und Eingabefelder.", + "style_the_survey_card": "Styling für die Umfragekarte.", + "styling_set_to_theme_styles": "Styling auf Themenstile eingestellt", + "subheading": "Zwischenüberschrift", + "subtract": "Subtrahieren -", + "suggest_colors": "Farben vorschlagen", + "survey_already_answered_heading": "Die Umfrage wurde bereits beantwortet.", + "survey_already_answered_subheading": "Du kannst diesen Link nur einmal verwenden.", + "survey_completed_heading": "Umfrage abgeschlossen", + "survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen", + "survey_display_settings": "Einstellungen zur Anzeige der Umfrage", + "survey_placement": "Platzierung der Umfrage", + "survey_trigger": "Auslöser der Umfrage", + "switch_multi_lanugage_on_to_get_started": "Schalte Mehrsprachigkeit ein, um loszulegen \uD83D\uDC49", + "targeted": "Gezielt", + "ten_points": "10 Punkte", + "the_survey_will_be_shown_multiple_times_until_they_respond": "Die Umfrage wird mehrmals angezeigt, bis Du antwortest", + "the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Die Umfrage wird einmal angezeigt, auch wenn die Person nicht antwortet.", + "then": "dann", + "this_action_will_remove_all_the_translations_from_this_survey": "Diese Aktion entfernt alle Übersetzungen aus dieser Umfrage.", + "this_extension_is_already_added": "Diese Erweiterung ist bereits hinzugefügt.", + "this_file_type_is_not_supported": "Dieser Dateityp wird nicht unterstützt.", + "this_setting_overwrites_your": "Diese Einstellung überschreibt deine", + "three_points": "3 Punkte", + "times": "Zeiten", + "to_keep_the_placement_over_all_surveys_consistent_you_can": "Um die Platzierung über alle Umfragen hinweg konsistent zu halten, kannst du", + "trigger_survey_when_one_of_the_actions_is_fired": "Umfrage auslösen, wenn eine der Aktionen ausgeführt wird...", + "try_lollipop_or_mountain": "Versuch 'Lolli' oder 'Berge'...", + "type_field_id": "Feld-ID eingeben", + "unlock_targeting_description": "Spezifische Nutzergruppen basierend auf Attributen oder Geräteinformationen ansprechen", + "unlock_targeting_title": "Targeting mit einem höheren Plan freischalten", + "unsaved_changes_warning": "Du hast ungespeicherte Änderungen in deiner Umfrage. Möchtest Du sie speichern, bevor Du gehst?", + "until_they_submit_a_response": "Bis sie eine Antwort einreichen", + "upgrade_notice_description": "Erstelle mehrsprachige Umfragen und entdecke viele weitere Funktionen", + "upgrade_notice_title": "Schalte mehrsprachige Umfragen mit einem höheren Plan frei", + "upload": "Hochladen", + "upload_at_least_2_images": "Lade mindestens 2 Bilder hoch", + "upper_label": "Oberes Label", + "url_encryption": "URL-Verschlüsselung", + "url_filters": "URL-Filter", + "url_not_supported": "URL nicht unterstützt", + "use_with_caution": "Mit Vorsicht verwenden", + "variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.", + "variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.", + "variable_name_must_start_with_a_letter": "Variablenname muss mit einem Buchstaben beginnen.", + "verify_email_before_submission": "E-Mail vor dem Absenden überprüfen", + "verify_email_before_submission_description": "Lass nur Leute mit einer echten E-Mail antworten.", + "wait": "Warte", + "wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Warte ein paar Sekunden nach dem Auslöser, bevor Du die Umfrage anzeigst", + "waiting_period": "Wartezeit", + "welcome_message": "Willkommensnachricht", + "when": "Wenn", + "when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Wenn die Bedingungen übereinstimmen, wird die Wartezeit ignoriert und die Umfrage angezeigt.", + "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_project_to_work_with_translations": "Du musst zwei oder mehr Sprachen in deinem Projekt einrichten, 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", + "zip": "Postleitzahl" + }, + "error_deleting_survey": "Beim Löschen der Umfrage ist ein Fehler aufgetreten", + "failed_to_copy_link_to_results": "Kopieren des Links zu den Ergebnissen fehlgeschlagen", + "failed_to_copy_url": "Kopieren der URL fehlgeschlagen: nicht in einer Browserumgebung.", + "new_single_use_link_generated": "Neuer Einmal-Link erstellt", + "new_survey": "Neue Umfrage", + "no_surveys_created_yet": "Noch keine Umfragen erstellt", + "open_options": "Optionen öffnen", + "preview_survey_in_a_new_tab": "Vorschau der Umfrage in einem neuen Tab", + "read_only_user_not_allowed_to_create_survey_warning": "Als Benutzer mit Nur-Lese-Rechten dürfen Sie keine Umfragen erstellen. Bitten Sie einen Benutzer mit Schreibrechten, eine Umfrage zu erstellen, oder einen Manager, Ihre Rolle zu aktualisieren.", + "relevance": "Relevanz", + "responses": { + "address_line_1": "Adresszeile 1", + "address_line_2": "Adresszeile 2", + "an_error_occurred_creating_a_new_note": "Beim Erstellen einer neuen Notiz ist ein Fehler aufgetreten", + "an_error_occurred_deleting_the_tag": "Beim Löschen des Tags ist ein Fehler aufgetreten", + "an_error_occurred_resolving_a_note": "Beim Auflösen einer Notiz ist ein Fehler aufgetreten", + "an_error_occurred_updating_a_note": "Beim Aktualisieren einer Notiz ist ein Fehler aufgetreten", + "browser": "Browser", + "city": "Stadt", + "company": "Firma", + "completed": "Erledigt ✅", + "country": "Land", + "device": "Gerät", + "device_info": "Geräteinfo", + "email": "E-Mail", + "first_name": "Vorname", + "how_to_identify_users": "Wie man Benutzer identifiziert", + "last_name": "Nachname", + "not_completed": "Nicht abgeschlossen ⏳", + "os": "Betriebssystem", + "person_attributes": "Personenattribute", + "phone": "Telefon", + "resolve": "Lösen", + "respondent_skipped_questions": "Der Befragte hat diese Fragen übersprungen.", + "response_deleted_successfully": "Antwort erfolgreich gelöscht.", + "single_use_id": "Einmalige ID", + "source": "Quelle", + "state_region": "Bundesland / Region", + "survey_closed": "Umfrage geschlossen", + "tag_already_exists": "Tag existiert bereits", + "this_response_is_in_progress": "Diese Antwort ist in Bearbeitung.", + "zip_post_code": "PLZ / Postleitzahl" + }, + "results_unpublished_successfully": "Ergebnisse wurden nicht erfolgreich veröffentlicht.", + "search_by_survey_name": "Nach Umfragenamen suchen", + "summary": { + "added_filter_for_responses_where_answer_to_question": "Filter hinzugefügt für Antworten, bei denen die Antwort auf Frage {questionIdx} {filterComboBoxValue} - {filterValue} ist", + "added_filter_for_responses_where_answer_to_question_is_skipped": "Filter hinzugefügt für Antworten, bei denen die Frage {questionIdx} übersprungen wurde", + "all_responses_csv": "Alle Antworten (CSV)", + "all_responses_excel": "Alle Antworten (Excel)", + "all_time": "Gesamt", + "almost_there": "Fast geschafft! Installiere das Widget, um mit dem Empfang von Antworten zu beginnen.", + "average": "Durchschnittlich", + "completed": "Abgeschlossen", + "completed_tooltip": "Anzahl der abgeschlossenen Umfragen.", + "configure_alerts": "Benachrichtigungen konfigurieren", + "congrats": "Glückwunsch! Deine Umfrage ist jetzt live.", + "connect_your_website_or_app_with_formbricks_to_get_started": "Verbinde deine Website oder App mit Formbricks, um loszulegen.", + "copy_link_to_public_results": "Link zu öffentlichen Ergebnissen kopieren", + "create_single_use_links": "Single-Use Links erstellen", + "create_single_use_links_description": "Akzeptiere nur eine Antwort pro Link. So geht's.", + "custom_range": "Benutzerdefinierter Bereich...", + "data_prefilling": "Daten-Prefilling", + "data_prefilling_description": "Du möchtest einige Felder in der Umfrage vorausfüllen? So geht's.", + "define_when_and_where_the_survey_should_pop_up": "Definiere, wann und wo die Umfrage erscheinen soll", + "drop_offs": "Drop-Off Rate", + "drop_offs_tooltip": "So oft wurde die Umfrage gestartet, aber nicht abgeschlossen.", + "dynamic_popup": "Dynamisch (Pop-up)", + "email_sent": "E-Mail gesendet!", + "embed_code_copied_to_clipboard": "Einbettungscode in die Zwischenablage kopiert!", + "embed_in_an_email": "In eine E-Mail einbetten", + "embed_in_app": "In App einbetten", + "embed_mode": "Einbettungsmodus", + "embed_mode_description": "Bette deine Umfrage mit einem minimalistischen Design ein, ohne Karten und Hintergrund.", + "embed_on_website": "Auf Website einbetten", + "embed_pop_up_survey_title": "Wie man eine Pop-up-Umfrage auf seiner Website einbindet", + "embed_survey": "Umfrage einbetten", + "enable_ai_insights_banner_button": "Insights aktivieren", + "enable_ai_insights_banner_description": "Du kannst die neue Insights-Funktion für die Umfrage aktivieren, um KI-basierte Insights für deine Freitextantworten zu erhalten.", + "enable_ai_insights_banner_success": "Erzeuge Insights für diese Umfrage. Bitte in ein paar Minuten die Seite neu laden.", + "enable_ai_insights_banner_title": "Bereit, KI-Insights zu testen?", + "enable_ai_insights_banner_tooltip": "Das sind ganz schön viele Freitextantworten! Kontaktiere uns bitte unter hola@formbricks.com, um Insights für diese Umfrage zu erhalten.", + "failed_to_copy_link": "Kopieren des Links fehlgeschlagen", + "filter_added_successfully": "Filter erfolgreich hinzugefügt", + "filter_updated_successfully": "Filter erfolgreich aktualisiert", + "filtered_responses_csv": "Gefilterte Antworten (CSV)", + "filtered_responses_excel": "Gefilterte Antworten (Excel)", + "formbricks_email_survey_preview": "Formbricks E-Mail-Umfrage Vorschau", + "go_to_setup_checklist": "Gehe zur Einrichtungs-Checkliste \uD83D\uDC49", + "hide_embed_code": "Einbettungscode ausblenden", + "how_to_create_a_panel": "Wie man ein Panel erstellt", + "how_to_create_a_panel_step_1": "Schritt 1: Erstelle ein Konto bei Prolific", + "how_to_create_a_panel_step_1_description": "Erstelle ein Konto bei Prolific und bestätige deine E-Mail-Adresse.", + "how_to_create_a_panel_step_2": "Schritt 2: Eine Studie erstellen", + "how_to_create_a_panel_step_2_description": "Bei Prolific erstellst Du eine neue Studie, bei der Du dein bevorzugtes Publikum basierend auf Hunderten von Merkmalen auswählen kannst.", + "how_to_create_a_panel_step_3": "Schritt 3: Verbinde deine Umfrage", + "how_to_create_a_panel_step_3_description": "Richte in deiner Formbricks-Umfrage versteckte Felder ein, um nachzuverfolgen, welcher Teilnehmer welche Antwort gegeben hat.", + "how_to_create_a_panel_step_4": "Schritt 4: Starte deine Studie", + "how_to_create_a_panel_step_4_description": "Sobald alles eingerichtet ist, kannst Du deine Studie starten. Innerhalb weniger Stunden wirst Du die ersten Antworten erhalten.", + "impressions": "Eindrücke", + "impressions_tooltip": "Anzahl der Aufrufe der Umfrage.", + "includes_all": "Beinhaltet alles", + "includes_either": "Beinhaltet entweder", + "insights_disabled": "Insights deaktiviert", + "install_widget": "Formbricks Widget installieren", + "is_equal_to": "Ist gleich", + "is_less_than": "ist weniger als", + "last_30_days": "Letzte 30 Tage", + "last_6_months": "Letzte 6 Monate", + "last_7_days": "Letzte 7 Tage", + "last_month": "Letztes Monat", + "last_quarter": "Letztes Quartal", + "last_year": "Letztes Jahr", + "link_to_public_results_copied": "Link zu öffentlichen Ergebnissen kopiert", + "make_sure_the_survey_type_is_set_to": "Stelle sicher, dass der Umfragetyp richtig eingestellt ist", + "mobile_app": "Mobile App", + "no_response_matches_filter": "Keine Antwort entspricht deinem Filter", + "only_completed": "Nur vollständige Antworten", + "other_values_found": "Andere Werte gefunden", + "overall": "Insgesamt", + "publish_to_web": "Im Web veröffentlichen", + "publish_to_web_warning": "Du bist dabei, diese Umfrageergebnisse öffentlich zugänglich zu machen.", + "publish_to_web_warning_description": "Deine Umfrageergebnisse werden öffentlich sein. Jeder außerhalb deiner Organisation kann darauf zugreifen, wenn er den Link hat.", + "quickstart_mobile_apps": "Schnellstart: Mobile-Apps", + "quickstart_mobile_apps_description": "Um mit Umfragen in mobilen Apps zu beginnen, folge bitte der Schnellstartanleitung:", + "quickstart_web_apps": "Schnellstart: Web-Apps", + "quickstart_web_apps_description": "Bitte folge der Schnellstartanleitung, um loszulegen:", + "results_are_public": "Ergebnisse sind öffentlich", + "send_preview": "Vorschau senden", + "send_to_panel": "An das Panel senden", + "setup_instructions": "Einrichtung", + "setup_integrations": "Integrationen einrichten", + "share_results": "Ergebnisse teilen", + "share_the_link": "Teile den Link", + "share_the_link_to_get_responses": "Teile den Link, um Antworten einzusammeln", + "show_all_responses_that_match": "Zeige alle Antworten, die übereinstimmen", + "show_all_responses_where": "Zeige alle Antworten, bei denen...", + "single_use_links": "Single-Use Links", + "source_tracking": "Quellenverfolgung", + "source_tracking_description": "Führe DSGVO- und CCPA-konformes Quell-Tracking ohne zusätzliche Tools durch.", + "starts": "Startet", + "starts_tooltip": "So oft wurde die Umfrage gestartet.", + "static_iframe": "Statisch (iframe)", + "survey_results_are_public": "Deine Umfrageergebnisse sind öffentlich", + "survey_results_are_shared_with_anyone_who_has_the_link": "Deine Umfrageergebnisse stehen allen zur Verfügung, die den Link haben. Die Ergebnisse werden nicht von Suchmaschinen indexiert.", + "this_month": "Dieser Monat", + "this_quarter": "Dieses Quartal", + "this_year": "Dieses Jahr", + "time_to_complete": "Zeit zur Fertigstellung", + "to_connect_your_website_with_formbricks": "deine Website mit Formbricks zu verbinden", + "ttc_tooltip": "Durchschnittliche Zeit bis zum Abschluss der Umfrage.", + "unknown_question_type": "Unbekannter Fragetyp", + "unpublish_from_web": "Aus dem Web entfernen", + "unsupported_video_tag_warning": "Dein Browser unterstützt das Video-Tag nicht.", + "view_embed_code": "Einbettungscode anzeigen", + "view_embed_code_for_email": "Einbettungscode für E-Mail anzeigen", + "view_site": "Seite ansehen", + "waiting_for_response": "Warte auf eine Antwort \uD83E\uDDD8‍♂️", + "web_app": "Web-App", + "what_is_a_panel": "Was ist ein Panel?", + "what_is_a_panel_answer": "Ein Panel ist eine Gruppe von Teilnehmern, die basierend auf Merkmalen wie Alter, Beruf, Geschlecht usw. ausgewählt werden.", + "what_is_prolific": "Was ist Prolific?", + "what_is_prolific_answer": "Wir arbeiten mit Prolific zusammen, um dir Zugang zu einem Pool von über 200.000 geprüften Teilnehmern zu geben.", + "whats_next": "Was kommt als Nächstes?", + "when_do_i_need_it": "Wann brauche ich das?", + "when_do_i_need_it_answer": "Wenn Du keinen Zugang zu genügend Leuten hast, die deiner Zielgruppe entsprechen, macht es Sinn, für ein Panel zu bezahlen.", + "you_can_do_a_lot_more_with_links_surveys": "Mit Links-Umfragen kannst Du viel mehr machen \uD83D\uDCA1", + "your_survey_is_public": "Deine Umfrage ist öffentlich", + "youre_not_plugged_in_yet": "Du bist noch nicht verbunden!" + }, + "survey_deleted_successfully": "Umfrage erfolgreich gelöscht", + "survey_duplicated_successfully": "Umfrage erfolgreich dupliziert", + "survey_duplication_error": "Duplizieren der Umfrage fehlgeschlagen", + "survey_status_tooltip": "Um den Umfragestatus zu aktualisieren, aktualisiere den Zeitplan in den Umfrageoptionen.", + "templates": { + "all_channels": "Alle Kanäle", + "all_industries": "Alle Branchen", + "all_roles": "Alle Rollen", + "create_a_new_survey": "Erstelle eine neue Umfrage", + "multiple_industries": "Mehrere Branchen", + "use_this_template": "Vorlage verwenden", + "uses_branching_logic": "Diese Umfrage verwendet Logik." + } + }, + "xm-templates": { + "ces": "CES", + "ces_description": "Nutze jeden Berührungspunkt, um zu verstehen, wie Du einfach der Umgang mit Deinem Produkt ist.", + "csat": "CSAT", + "csat_description": "Bewährte Methoden zur Messung der Kundenzufriedenheit.", + "enps": "eNPS", + "enps_description": "Universelles Feedback, um das Engagement und die Zufriedenheit der Mitarbeiter zu verstehen.", + "five_star_rating": "5-Sterne-Bewertung", + "five_star_rating_description": "Universelle Feedback-Lösung zur Messung der allgemeinen Zufriedenheit.", + "headline": "Welche Art von Feedback möchtest Du bekommen?", + "nps": "NPS", + "nps_description": "Bewährte Best Practices umsetzen, um zu verstehen, WARUM Menschen kaufen.", + "smileys": "Smileys", + "smileys_description": "Verwende visuelle Indikatoren, um Feedback an allen Kundenkontaktpunkten zu erfassen." + } + }, + "organizations": { + "landing": { + "no_projects_warning_subtitle": "Wenden Sie sich an den Eigentümer Ihrer Organisation, um Zugriff auf Projekte zu erhalten. Oder erstellen Sie eine eigene Organisation, um loszulegen.", + "no_projects_warning_title": "Ihr Konto hat noch keinen Zugriff auf Projekte." + }, + "projects": { + "new": { + "channel": { + "channel_select_subtitle": "Teile einen Link oder zeige deine Umfrage in Apps oder auf Websites.", + "channel_select_title": "Welche Art von Umfrage brauchst du?", + "in_product_surveys": "In-Product-Umfragen", + "in_product_surveys_description": "Führe zielgerichtete Micro-Umfragen in deinen Apps durch.", + "link_and_email_surveys": "Link- und E-Mail-Umfragen", + "link_and_email_surveys_description": "Erreiche Menschen überall online." + }, + "mode": { + "formbricks_cx": "Formbricks CX", + "formbricks_cx_description": "Umfragen und Berichte, um zu verstehen, was deine Kunden brauchen.", + "formbricks_surveys": "Formbricks Umfragen", + "formbricks_surveys_description": "Multifunktionale Umfrageplattform für Web-, App- und E-Mail-Umfragen.", + "what_are_you_here_for": "Warum bist Du hier?" + }, + "settings": { + "brand_color": "Markenfarbe", + "brand_color_description": "Passe die Hauptfarbe der Umfragen an deine Marke an.", + "create_new_team": "Neues Team erstellen", + "project_creation_failed": "Projekterstellung fehlgeschlagen", + "project_name": "Produktname", + "project_name_description": "Wie heißt dein Produkt?", + "project_settings_subtitle": "Wenn Leute deine Marke erkennen, ist es viel wahrscheinlicher, dass sie Umfragen beantworten und abschließen.", + "project_settings_title": "Lass die Teilnehmenden wissen, dass du es bist", + "team_description": "Wer kann auf dieses Projekt zugreifen?" + } + } + } + }, + "s": { + "check_inbox_or_spam": "Bitte überprüfe auch deinen Spam-Ordner, falls Du die E-Mail nicht in deinem Posteingang siehst.", + "completed": "Diese kostenlose und quelloffene Umfrage wurde geschlossen.", + "create_your_own": "Erstelle deine eigene", + "enter_pin": "Diese Umfrage ist geschützt. Gib unten die PIN ein", + "just_curious": "Einfach neugierig?", + "link_invalid": "Diese Umfrage kann nur auf Einladung durchgeführt werden.", + "paused": "Diese Umfrage ist vorübergehend pausiert.", + "please_try_again_with_the_original_link": "Bitte versuche es nochmal mit dem ursprünglichen Link", + "preview_survey_questions": "Vorschau der Fragen.", + "question_preview": "Vorschau der Frage", + "response_already_received": "Wir haben bereits eine Antwort für diese E-Mail-Adresse erhalten.", + "response_submitted": "Eine Antwort, die mit dieser Umfrage und diesem Kontakt verknüpft ist, existiert bereits", + "survey_already_answered_heading": "Die Umfrage wurde bereits beantwortet.", + "survey_already_answered_subheading": "Du kannst diesen Link nur einmal verwenden.", + "survey_sent_to": "Umfrage an {email} gesendet", + "this_looks_fishy": "Das sieht verdächtig aus.", + "verify_email": "E-Mail bestätigen.", + "verify_email_before_submission": "Bestätige deine E-Mail, um zu antworten", + "verify_email_before_submission_button": "Überprüfen", + "verify_email_before_submission_description": "Um an dieser Umfrage teilzunehmen, bitte bestätige deine E-Mail", + "want_to_respond": "Möchtest Du antworten?" + }, + "setup": { + "intro": { + "get_started": "Leg los", + "made_with_love_in_kiel": "Gebaut mit \uD83E\uDD0D in Deutschland", + "paragraph_1": "Formbricks ist eine Experience Management Suite, die auf der am schnellsten wachsenden Open-Source-Umfrageplattform weltweit basiert.", + "paragraph_2": "Führe gezielte Umfragen auf Websites, in Apps oder überall online durch. Sammle wertvolle Insights, um unwiderstehliche Erlebnisse für Kunden, Nutzer und Mitarbeiter zu gestalten.", + "paragraph_3": "Wir schreiben DATENSCHUTZ groß (ha!). Hoste Formbricks selbst, um volle Kontrolle über deine Daten zu behalten.", + "welcome_to_formbricks": "Willkommen bei Formbricks!" + }, + "invite": { + "add_another_member": "Füge ein weiteres Mitglied hinzu", + "continue": "Weitermachen", + "failed_to_invite": "Einladung fehlgeschlagen", + "invitation_sent_to": "Einladung gesendet an", + "invite_your_organization_members": "Lade deine Organisationsmitglieder ein", + "life_s_no_fun_alone": "Allein macht das Leben keinen Spaß.", + "skip": "Überspringen", + "smtp_not_configured": "SMTP nicht konfiguriert", + "smtp_not_configured_description": "Einladungen können derzeit nicht gesendet werden, da der E-Mail-Dienst nicht konfiguriert ist. Du kannst den Einladungslink später in den Organisationseinstellungen kopieren." + }, + "organization": { + "create": { + "continue": "Weitermachen", + "delete_account": "Konto löschen", + "delete_account_description": "Wenn Du dein Konto löschen möchtest, kannst Du dies tun, indem Du auf den untenstehenden Button klickst.", + "description": "Passe es deiner Organisation an.", + "no_membership_found": "Keine Mitgliedschaft gefunden!", + "no_membership_found_description": "Du bist derzeit kein Mitglied einer Organisation. Wenn Du glaubst, dass dies ein Fehler ist, wende Dich bitte an den Eigentümer der Organisation.", + "title": "Organisation einrichten" + } + }, + "signup": { + "create_administrator": "Administrator erstellen", + "this_user_has_all_the_power": "Dieser Benutzer hat alle Rechte." + } + }, + "share": { + "back_to_home": "Zurück zur Startseite", + "page_not_found": "Seite nicht gefunden", + "page_not_found_description": "Entschuldigung, wir konnten die gesuchten Antworten mit der geteilten ID nicht finden." + }, + "templates": { + "address": "Adresse", + "address_description": "Frag nach einer Adresse", + "alignment_and_engagement_survey_description": "Bewerte die Ausrichtung der Mitarbeiter an der Unternehmensvision, Strategie und Kommunikation sowie die Teamzusammenarbeit.", + "alignment_and_engagement_survey_name": "Ausrichtung und Engagement mit der Unternehmensvision", + "alignment_and_engagement_survey_question_1_headline": "Ich verstehe, wie meine Rolle zur Gesamtstrategie des Unternehmens beiträgt.", + "alignment_and_engagement_survey_question_1_lower_label": "Kein Verständnis", + "alignment_and_engagement_survey_question_1_upper_label": "Vollständiges Verständnis", + "alignment_and_engagement_survey_question_2_headline": "Ich fühle, dass meine Werte mit der Mission und Kultur des Unternehmens übereinstimmen.", + "alignment_and_engagement_survey_question_2_lower_label": "Keine Übereinstimmung", + "alignment_and_engagement_survey_question_2_upper_label": "Vollständige Übereinstimmung", + "alignment_and_engagement_survey_question_3_headline": "Ich arbeite effektiv mit meinem Team zusammen, um unsere Ziele zu erreichen.", + "alignment_and_engagement_survey_question_3_lower_label": "Schlechte Zusammenarbeit", + "alignment_and_engagement_survey_question_3_upper_label": "Ausgezeichnete Zusammenarbeit", + "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", + "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", + "build_product_roadmap_name_with_project_name": "$[projectName] Roadmap Ideen", + "build_product_roadmap_question_1_headline": "Wie zufrieden bist Du mit den Funktionen und der Benutzerfreundlichkeit von $[projectName]?", + "build_product_roadmap_question_1_lower_label": "Überhaupt nicht zufrieden", + "build_product_roadmap_question_1_upper_label": "Extrem zufrieden", + "build_product_roadmap_question_2_headline": "Was ist EINE Änderung, die wir vornehmen könnten, um deine $[projectName]-Erfahrung am meisten zu verbessern?", + "build_product_roadmap_question_2_placeholder": "Tippe deine Antwort hier...", + "card_abandonment_survey": "Umfrage zum Warenkorbabbruch", + "card_abandonment_survey_description": "Verstehe die Gründe für Warenkorbabbrüche in deinem Webshop.", + "card_abandonment_survey_question_1_button_label": "Klar!", + "card_abandonment_survey_question_1_dismiss_button_label": "Nein, danke.", + "card_abandonment_survey_question_1_headline": "Haben Sie 2 Minuten Zeit, um uns bei der Verbesserung zu helfen?", + "card_abandonment_survey_question_1_html": "

Wir haben bemerkt, dass Du einige Artikel in deinem Warenkorb gelassen hast. Wir würden gerne verstehen, warum.

", + "card_abandonment_survey_question_2_choice_1": "Hohe Versandkosten", + "card_abandonment_survey_question_2_choice_2": "Woanders einen besseren Preis gefunden", + "card_abandonment_survey_question_2_choice_3": "Ich schaue mich nur um", + "card_abandonment_survey_question_2_choice_4": "Entschieden, nicht zu kaufen", + "card_abandonment_survey_question_2_choice_5": "Problem mit der Zahlung", + "card_abandonment_survey_question_2_choice_6": "Andere", + "card_abandonment_survey_question_2_headline": "Was war der Hauptgrund, warum Du deinen Kauf nicht abgeschlossen hast?", + "card_abandonment_survey_question_2_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "card_abandonment_survey_question_3_headline": "Bitte erläutere deinen Grund für den Abbruch des Kaufs:", + "card_abandonment_survey_question_4_headline": "Wie würdest Du dein gesamtes Einkaufserlebnis bewerten?", + "card_abandonment_survey_question_4_lower_label": "Sehr unzufrieden", + "card_abandonment_survey_question_4_upper_label": "Sehr zufrieden", + "card_abandonment_survey_question_5_choice_1": "Niedrigere Versandkosten", + "card_abandonment_survey_question_5_choice_2": "Rabatte oder Aktionen", + "card_abandonment_survey_question_5_choice_3": "Mehr Zahlungsmöglichkeiten", + "card_abandonment_survey_question_5_choice_4": "Bessere Produktbeschreibungen", + "card_abandonment_survey_question_5_choice_5": "Verbesserte Website-Navigation", + "card_abandonment_survey_question_5_choice_6": "Andere", + "card_abandonment_survey_question_5_headline": "Welche Faktoren würden Dich dazu ermutigen, deinen Kauf zukünftig abzuschließen?", + "card_abandonment_survey_question_5_subheader": "Bitte wähle alle zutreffenden Optionen aus:", + "card_abandonment_survey_question_6_headline": "Möchtest Du einen Rabattcode per E-Mail erhalten?", + "card_abandonment_survey_question_6_label": "Ja, sehr gerne.", + "card_abandonment_survey_question_7_headline": "Bitte teile deine E-Mail-Adresse:", + "card_abandonment_survey_question_8_headline": "Weitere Kommentare oder Vorschläge?", + "career_development_survey_description": "Bewerte die Mitarbeiterzufriedenheit anhand von Möglichkeiten der Weiterentwicklung.", + "career_development_survey_name": "Umfrage zur Karriereentwicklung", + "career_development_survey_question_1_headline": "Ich bin zufrieden mit den Möglichkeiten zur persönlichen und beruflichen Entwicklung bei $[projectName].", + "career_development_survey_question_1_lower_label": "Stimme überhaupt nicht zu", + "career_development_survey_question_1_upper_label": "Stimme voll und ganz zu", + "career_development_survey_question_2_headline": "Ich bin mit den mir zur Verfügung stehenden Karrieremöglichkeiten bei $[projectName] zufrieden.", + "career_development_survey_question_2_lower_label": "Stimme überhaupt nicht zu", + "career_development_survey_question_2_upper_label": "Stimme voll und ganz zu", + "career_development_survey_question_3_headline": "Ich bin mit den berufsbezogenen Schulungen zufrieden, die meine Organisation anbietet.", + "career_development_survey_question_3_lower_label": "Stimme überhaupt nicht zu", + "career_development_survey_question_3_upper_label": "Stimme voll und ganz zu", + "career_development_survey_question_4_headline": "Ich bin mit den Investitionen zufrieden, die meine Organisation in Aus- und Weiterbildung tätigt.", + "career_development_survey_question_4_lower_label": "Stimme überhaupt nicht zu", + "career_development_survey_question_4_upper_label": "Stimme voll und ganz zu", + "career_development_survey_question_5_choice_1": "Produktentwicklung", + "career_development_survey_question_5_choice_2": "Marketing", + "career_development_survey_question_5_choice_3": "Öffentlichkeitsarbeit", + "career_development_survey_question_5_choice_4": "Buchhaltung", + "career_development_survey_question_5_choice_5": "Betrieb", + "career_development_survey_question_5_choice_6": "Andere", + "career_development_survey_question_5_headline": "In welchem Bereich arbeitest du?", + "career_development_survey_question_5_subheader": "Bitte wähle eine der folgenden Optionen", + "career_development_survey_question_6_choice_1": "Einzelner Mitarbeiter", + "career_development_survey_question_6_choice_2": "Manager", + "career_development_survey_question_6_choice_3": "Senior Manager", + "career_development_survey_question_6_choice_4": "Vizepräsident", + "career_development_survey_question_6_choice_5": "Geschäftsführung", + "career_development_survey_question_6_choice_6": "Andere", + "career_development_survey_question_6_headline": "Was beschreibt deine aktuelle Position am besten?", + "career_development_survey_question_6_subheader": "Bitte wähle eine der folgenden Optionen", + "cess_survey_name": "CES-Umfrage", + "cess_survey_question_1_headline": "$[projectName] macht es mir leicht, [ZIEL HINZUFÜGEN] zu erreichen", + "cess_survey_question_1_lower_label": "Stimme nicht zu", + "cess_survey_question_1_upper_label": "Stimme zu", + "cess_survey_question_2_headline": "Danke! Wie könnten wir es dir leichter machen, [ZIEL HINZUFÜGEN] zu erreichen?", + "cess_survey_question_2_placeholder": "Tippe deine Antwort hier...", + "changing_subscription_experience_description": "Finde heraus, was den Leuten durch den Kopf geht, wenn sie ihre Abonnements ändern.", + "changing_subscription_experience_name": "Änderung des Abonnement-Erlebnisses", + "changing_subscription_experience_question_1_choice_1": "Extrem schwierig", + "changing_subscription_experience_question_1_choice_2": "Es hat eine Weile gedauert, aber ich hab's geschafft.", + "changing_subscription_experience_question_1_choice_3": "Es war in Ordnung", + "changing_subscription_experience_question_1_choice_4": "Recht einfach", + "changing_subscription_experience_question_1_choice_5": "Sehr einfach", + "changing_subscription_experience_question_1_headline": "Wie einfach war es, deinen Plan zu ändern?", + "changing_subscription_experience_question_2_choice_1": "Ja, sehr klar.", + "changing_subscription_experience_question_2_choice_2": "Ich war zuerst verwirrt, aber habe gefunden, was ich brauchte.", + "changing_subscription_experience_question_2_choice_3": "Ziemlich kompliziert.", + "changing_subscription_experience_question_2_headline": "Sind die Preisinformationen leicht verständlich?", + "churn_survey": "Umfrage zur Kündigung von Abos", + "churn_survey_description": "Finde heraus, warum Leute ihre Abos kündigen. Diese Einblicke sind pures Gold!", + "churn_survey_question_1_choice_1": "Schwer zu benutzen", + "churn_survey_question_1_choice_2": "Zu teuer", + "churn_survey_question_1_choice_3": "Mir fehlen Funktionen", + "churn_survey_question_1_choice_4": "Schlechter Kundenservice", + "churn_survey_question_1_choice_5": "Ich brauchte es einfach nicht mehr", + "churn_survey_question_1_headline": "Warum hast Du dein Abonnement gekündigt?", + "churn_survey_question_1_subheader": "Es tut uns leid, dass es mit uns nicht passt. Hilf dabei, uns zu verbessern:", + "churn_survey_question_2_button_label": "Senden", + "churn_survey_question_2_headline": "Was hätte $[projectName] benutzerfreundlicher gemacht?", + "churn_survey_question_3_button_label": "Erhalte 30% Rabatt", + "churn_survey_question_3_dismiss_button_label": "Überspringen", + "churn_survey_question_3_headline": "Erhalte 30% Rabatt für das nächste Jahr!", + "churn_survey_question_3_html": "Wir würden Dich gerne als Kunden behalten! Gerne bieten wir dir einen 30% Rabatt für das nächste Jahr an.", + "churn_survey_question_4_headline": "Welche Funktionen vermisst du?", + "churn_survey_question_5_button_label": "E-Mail an den CEO senden", + "churn_survey_question_5_dismiss_button_label": "Überspringen", + "churn_survey_question_5_headline": "Es tut mir leid zu hören \uD83D\uDE14 Sprich direkt mit unserem CEO!", + "churn_survey_question_5_html": "

Wir möchten den bestmöglichen Kundenservice bieten. Bitte sende eine E-Mail an unsere Geschäftsführerin, und sie wird sich persönlich um dein Anliegen kümmern.

", + "collect_feedback_description": "Sammle umfassendes Feedback zu deinem Produkt oder deiner Dienstleistung.", + "collect_feedback_name": "Feedback sammeln", + "collect_feedback_question_1_headline": "Wie bewertest Du deine Gesamterfahrung?", + "collect_feedback_question_1_lower_label": "Nicht gut", + "collect_feedback_question_1_subheader": "Keine Sorge, sei ehrlich.", + "collect_feedback_question_1_upper_label": "Sehr gut", + "collect_feedback_question_2_headline": "Schön! Was hat dir daran gefallen?", + "collect_feedback_question_2_placeholder": "Tippe deine Antwort hier...", + "collect_feedback_question_3_headline": "Danke fürs Teilen! Was hat dir nicht gefallen?", + "collect_feedback_question_3_placeholder": "Tippe deine Antwort hier...", + "collect_feedback_question_4_headline": "Wie bewertest Du unsere Kommunikation?", + "collect_feedback_question_4_lower_label": "Nicht gut", + "collect_feedback_question_4_upper_label": "Sehr gut", + "collect_feedback_question_5_headline": "Möchtest Du noch etwas mit unserem Team teilen?", + "collect_feedback_question_5_placeholder": "Tippe deine Antwort hier...", + "collect_feedback_question_6_choice_1": "Google", + "collect_feedback_question_6_choice_2": "Soziale Medien", + "collect_feedback_question_6_choice_3": "Freunde", + "collect_feedback_question_6_choice_4": "Podcast", + "collect_feedback_question_6_choice_5": "Andere", + "collect_feedback_question_6_headline": "Wie hast Du von uns gehört?", + "collect_feedback_question_7_headline": "Wir würden Dir gerne auf dein Feedback antworten. Bitte teile uns deine E-Mail-Adresse mit:", + "collect_feedback_question_7_placeholder": "beispiel@email.com", + "consent": "Zustimmung", + "consent_description": "Bitte um Zustimmung zu den Bedingungen, Konditionen oder der Datennutzung", + "contact_info": "Kontaktinfo", + "contact_info_description": "Bitte nach Name, Nachname, E-Mail, Telefonnummer und Firma gemeinsam fragen", + "csat_description": "Miss den Kundenzufriedenheitswert deines Produkts oder deiner Dienstleistung.", + "csat_name": "Kundenzufriedenheitswert (CSAT)", + "csat_question_10_headline": "Hast Du noch weitere Kommentare, Fragen oder Bedenken?", + "csat_question_10_placeholder": "Tippe deine Antwort hier...", + "csat_question_1_headline": "Wie wahrscheinlich ist es, dass Du dieses $[projectName] einem Freund oder Kollegen empfehlen würdest?", + "csat_question_1_lower_label": "Nicht wahrscheinlich", + "csat_question_1_upper_label": "Sehr wahrscheinlich", + "csat_question_2_choice_1": "Etwas zufrieden", + "csat_question_2_choice_2": "Sehr zufrieden", + "csat_question_2_choice_3": "Weder zufrieden noch unzufrieden", + "csat_question_2_choice_4": "Etwas unzufrieden", + "csat_question_2_choice_5": "Sehr unzufrieden", + "csat_question_2_headline": "Insgesamt, wie zufrieden oder unzufrieden bist Du mit unserem $[projectName]?", + "csat_question_2_subheader": "Bitte wähle eine aus:", + "csat_question_3_choice_1": "Unwirksam", + "csat_question_3_choice_10": "Einzigartig", + "csat_question_3_choice_2": "Nützlich", + "csat_question_3_choice_3": "Unpraktisch", + "csat_question_3_choice_4": "Überteuert", + "csat_question_3_choice_5": "Hochwertig", + "csat_question_3_choice_6": "Zuverlässig", + "csat_question_3_choice_7": "Gutes Preis-Leistungs-Verhältnis", + "csat_question_3_choice_8": "Schlechte Qualität", + "csat_question_3_choice_9": "Unzuverlässig", + "csat_question_3_headline": "Welches der folgenden Wörter würdest Du verwenden, um unser $[projectName] zu beschreiben?", + "csat_question_3_subheader": "Wähle alle zutreffenden aus:", + "csat_question_4_choice_1": "Extrem gut", + "csat_question_4_choice_2": "Sehr gut", + "csat_question_4_choice_3": "Ziemlich gut", + "csat_question_4_choice_4": "Nicht so gut", + "csat_question_4_choice_5": "Überhaupt nicht gut", + "csat_question_4_headline": "Wie gut erfüllt unser $[projectName] deine Bedürfnisse?", + "csat_question_4_subheader": "Wähle eine Option:", + "csat_question_5_choice_1": "Sehr hohe Qualität", + "csat_question_5_choice_2": "Hochwertig", + "csat_question_5_choice_3": "Niedrige Qualität", + "csat_question_5_choice_4": "Sehr niedrige Qualität", + "csat_question_5_choice_5": "Weder hoch noch niedrig", + "csat_question_5_headline": "Wie würdest Du die Qualität des $[projectName] bewerten?", + "csat_question_5_subheader": "Wähle eine Option:", + "csat_question_6_choice_1": "Ausgezeichnet", + "csat_question_6_choice_2": "Überdurchschnittlich", + "csat_question_6_choice_3": "Durchschnittlich", + "csat_question_6_choice_4": "Unterdurchschnittlich", + "csat_question_6_choice_5": "schlecht", + "csat_question_6_headline": "Wie würdest Du das Preis-Leistungs-Verhältnis des $[projectName] bewerten?", + "csat_question_6_subheader": "Bitte wähle eine aus:", + "csat_question_7_choice_1": "Extrem schnell", + "csat_question_7_choice_2": "Sehr schnell", + "csat_question_7_choice_3": "Etwas schnell", + "csat_question_7_choice_4": "Nicht so schnell", + "csat_question_7_choice_5": "Überhaupt nicht schnell", + "csat_question_7_choice_6": "Nicht zutreffend", + "csat_question_7_headline": "Wie schnell haben wir auf deine Fragen zu unseren Dienstleistungen reagiert?", + "csat_question_7_subheader": "Bitte wähle eine aus:", + "csat_question_8_choice_1": "Das ist mein erster Kauf", + "csat_question_8_choice_2": "Weniger als sechs Monate", + "csat_question_8_choice_3": "Sechs Monate bis ein Jahr", + "csat_question_8_choice_4": "1 - 2 Jahre", + "csat_question_8_choice_5": "3 oder mehr Jahre", + "csat_question_8_choice_6": "Ich habe noch keinen Kauf getätigt", + "csat_question_8_headline": "Wie lange bist Du schon Kunde von $[projectName]?", + "csat_question_8_subheader": "Bitte wähle eine aus:", + "csat_question_9_choice_1": "Sehr wahrscheinlich", + "csat_question_9_choice_2": "Sehr wahrscheinlich", + "csat_question_9_choice_3": "Eher wahrscheinlich", + "csat_question_9_choice_4": "Nicht so wahrscheinlich", + "csat_question_9_choice_5": "Überhaupt nicht wahrscheinlich", + "csat_question_9_headline": "Wie wahrscheinlich ist es, dass Du unser $[projectName] erneut kaufst?", + "csat_question_9_subheader": "Wähle eine Option:", + "csat_survey_name": "$[projectName] CSAT", + "csat_survey_question_1_headline": "Wie zufrieden bist Du mit deiner $[projectName] Erfahrung?", + "csat_survey_question_1_lower_label": "Extrem unzufrieden", + "csat_survey_question_1_upper_label": "Extrem zufrieden", + "csat_survey_question_2_headline": "Super! Gibt es etwas, das wir tun können, um deine Erfahrung zu verbessern?", + "csat_survey_question_2_placeholder": "Tippe deine Antwort hier...", + "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_description": "Erstelle eine Umfrage ohne Vorlage.", + "custom_survey_name": "Eigene Umfrage erstellen", + "custom_survey_question_1_headline": "Was möchtest Du wissen?", + "custom_survey_question_1_placeholder": "Tippe deine Antwort hier...", + "customer_effort_score_description": "Erfahre, wie einfach es ist, eine Funktion zu nutzen.", + "customer_effort_score_name": "Customer Effort Score (CES)", + "customer_effort_score_question_1_headline": "$[projectName] macht es mir leicht, [ZIEL HINZUFÜGEN] zu erreichen", + "customer_effort_score_question_1_lower_label": "Stimme nicht zu", + "customer_effort_score_question_1_upper_label": "Stimme zu", + "customer_effort_score_question_2_headline": "Danke! Wie könnten wir es dir leichter machen, [ZIEL HINZUFÜGEN] zu erreichen?", + "customer_effort_score_question_2_placeholder": "Tippe deine Antwort hier...", + "date": "Datum", + "date_description": "Frag nach einem Datum", + "default_ending_card_button_label": "Erstelle deine eigene Umfrage", + "default_ending_card_headline": "Danke!", + "default_ending_card_subheader": "Wir schätzen dein Feedback.", + "default_welcome_card_button_label": "Weiter", + "default_welcome_card_headline": "Willkommen!", + "default_welcome_card_html": "Danke für dein Feedback - los geht's!", + "docs_feedback_description": "Finde heraus, wie verständlich deine Entwicklerdokumentation ist.", + "docs_feedback_name": "Docs Feedback", + "docs_feedback_question_1_choice_1": "Ja \uD83D\uDC4D", + "docs_feedback_question_1_choice_2": "Nein \uD83D\uDC4E", + "docs_feedback_question_1_headline": "War diese Seite hilfreich?", + "docs_feedback_question_2_headline": "Bitte erläutere:", + "docs_feedback_question_3_headline": "Seiten-URL", + "earned_advocacy_score_description": "Der Earned Advocacy Score (EAS) ist eine Abwandlung des Net Promoter Score (NPS), bei der nach tatsächlichem vergangenen Verhalten anstatt nach hochtrabenden Absichten gefragt wird.", + "earned_advocacy_score_name": "Earned Advocacy Score (EAS)", + "earned_advocacy_score_question_1_choice_1": "Ja", + "earned_advocacy_score_question_1_choice_2": "Nein", + "earned_advocacy_score_question_1_headline": "Hast Du $[projectName] aktiv anderen empfohlen?", + "earned_advocacy_score_question_2_headline": "Warum hast Du uns empfohlen?", + "earned_advocacy_score_question_2_placeholder": "Tippe deine Antwort hier...", + "earned_advocacy_score_question_3_headline": "Schade! Warum nicht?", + "earned_advocacy_score_question_3_placeholder": "Tippe deine Antwort hier...", + "earned_advocacy_score_question_4_choice_1": "Ja", + "earned_advocacy_score_question_4_choice_2": "Nein", + "earned_advocacy_score_question_4_headline": "Hast Du aktiv andere davon abgehalten, $[projectName] zu wählen?", + "earned_advocacy_score_question_5_headline": "Was hat Dich dazu gebracht, sie zu entmutigen?", + "earned_advocacy_score_question_5_placeholder": "Tippe deine Antwort hier...", + "employee_satisfaction_description": "Die Zufriedenheit der Mitarbeiter messen und Bereiche zur Verbesserung identifizieren.", + "employee_satisfaction_name": "Mitarbeiterzufriedenheit", + "employee_satisfaction_question_1_headline": "Wie zufrieden bist Du mit deiner aktuellen Rolle?", + "employee_satisfaction_question_1_lower_label": "Nicht zufrieden", + "employee_satisfaction_question_1_upper_label": "Sehr zufrieden", + "employee_satisfaction_question_2_choice_1": "Extrem bedeutungsvoll", + "employee_satisfaction_question_2_choice_2": "Sehr bedeutungsvoll", + "employee_satisfaction_question_2_choice_3": "Mäßig bedeutungsvoll", + "employee_satisfaction_question_2_choice_4": "Leicht bedeutungsvoll", + "employee_satisfaction_question_2_choice_5": "Überhaupt nicht sinnvoll", + "employee_satisfaction_question_2_headline": "Wie sinnvoll findest Du deine Arbeit?", + "employee_satisfaction_question_3_headline": "Was gefällt dir am meisten daran, hier zu arbeiten?", + "employee_satisfaction_question_3_placeholder": "Tippe deine Antwort hier...", + "employee_satisfaction_question_5_headline": "Bewerte die Unterstützung, die Du von deinem Vorgesetzten erhältst.", + "employee_satisfaction_question_5_lower_label": "Schlecht", + "employee_satisfaction_question_5_upper_label": "Ausgezeichnet", + "employee_satisfaction_question_6_headline": "Welche Verbesserungen würdest Du für unseren Arbeitsplatz vorschlagen?", + "employee_satisfaction_question_6_placeholder": "Tippe deine Antwort hier...", + "employee_satisfaction_question_7_choice_1": "Sehr wahrscheinlich", + "employee_satisfaction_question_7_choice_2": "Sehr wahrscheinlich", + "employee_satisfaction_question_7_choice_3": "Ziemlich wahrscheinlich", + "employee_satisfaction_question_7_choice_4": "Eher wahrscheinlich", + "employee_satisfaction_question_7_choice_5": "Überhaupt nicht wahrscheinlich", + "employee_satisfaction_question_7_headline": "Wie wahrscheinlich ist es, dass Du unser Unternehmen einem Freund weiterempfiehlst?", + "employee_well_being_description": "Bewerte das Wohlbefinden deiner Mitarbeiter durch Work-Life-Balance, Arbeitsbelastung und Umfeld.", + "employee_well_being_name": "Mitarbeiterwohlbefinden", + "employee_well_being_question_1_headline": "Ich habe das Gefühl, dass ich eine gute Balance zwischen Arbeit und Privatleben habe.", + "employee_well_being_question_1_lower_label": "Sehr schlechte Balance", + "employee_well_being_question_1_upper_label": "Ausgezeichnetes Gleichgewicht", + "employee_well_being_question_2_headline": "Meine Arbeitsbelastung ist überschaubar, sodass ich produktiv bleiben kann, ohne mich überfordert zu fühlen.", + "employee_well_being_question_2_lower_label": "Überwältigende Arbeitsbelastung", + "employee_well_being_question_2_upper_label": "Perfekt machbar", + "employee_well_being_question_3_headline": "Die Arbeitsumgebung unterstützt mein körperliches und geistiges Wohlbefinden.", + "employee_well_being_question_3_lower_label": "Nicht unterstützend", + "employee_well_being_question_3_upper_label": "Sehr unterstützend", + "employee_well_being_question_4_headline": "Welche Veränderungen, wenn überhaupt, würden dein allgemeines Wohlbefinden bei der Arbeit verbessern?", + "employee_well_being_question_4_placeholder": "Tippe deine Antwort hier...", + "enps_survey_name": "eNPS-Umfrage", + "enps_survey_question_1_headline": "Wie wahrscheinlich ist es, dass Du einem Freund oder Kollegen empfehlen würdest, bei diesem Unternehmen zu arbeiten?", + "enps_survey_question_1_lower_label": "Überhaupt nicht wahrscheinlich", + "enps_survey_question_1_upper_label": "Sehr wahrscheinlich", + "enps_survey_question_2_headline": "Um uns zu helfen, uns zu verbessern, kannst Du die Gründe für deine Bewertung beschreiben?", + "enps_survey_question_3_headline": "Weitere Kommentare, Feedback oder Bedenken?", + "evaluate_a_product_idea_description": "Befrage Nutzer zu Produkt- oder Feature-Ideen. Erhalte schnell Feedback.", + "evaluate_a_product_idea_name": "Ein Produktidee bewerten", + "evaluate_a_product_idea_question_1_button_label": "Los geht's!", + "evaluate_a_product_idea_question_1_dismiss_button_label": "Überspringen", + "evaluate_a_product_idea_question_1_headline": "Uns gefällt, wie Du $[projectName] benutzt! Wir würden gerne deine Meinung zu einer Feature-Idee hören. Hast Du eine Minute?", + "evaluate_a_product_idea_question_1_html": "

Wir respektieren deine Zeit und haben es kurz gehalten \uD83E\uDD38

", + "evaluate_a_product_idea_question_2_headline": "Danke! Wie schwierig oder einfach ist es für Dich heute, [PROBLEM AREA] zu [erledigen]?", + "evaluate_a_product_idea_question_2_lower_label": "Sehr schwierig", + "evaluate_a_product_idea_question_2_upper_label": "Sehr einfach", + "evaluate_a_product_idea_question_3_headline": "Was fällt dir am schwersten, wenn es um [PROBLEM BEREICH] geht?", + "evaluate_a_product_idea_question_3_placeholder": "Tippe deine Antwort hier...", + "evaluate_a_product_idea_question_4_button_label": "Weiter", + "evaluate_a_product_idea_question_4_dismiss_button_label": "Überspringen", + "evaluate_a_product_idea_question_4_headline": "Wir arbeiten an einer Idee, um bei [PROBLEM BEREICH] zu helfen.", + "evaluate_a_product_idea_question_4_html": "

Füge hier die Konzeptbeschreibung ein. Füge notwendige Details hinzu, aber halte es kurz und leicht verständlich.

", + "evaluate_a_product_idea_question_5_headline": "Wie wertvoll wäre diese Funktion für dich?", + "evaluate_a_product_idea_question_5_lower_label": "Nicht wertvoll", + "evaluate_a_product_idea_question_5_upper_label": "Sehr wertvoll", + "evaluate_a_product_idea_question_6_headline": "Verstanden. Warum wäre diese Funktion für Dich nicht wertvoll?", + "evaluate_a_product_idea_question_6_placeholder": "Tippe deine Antwort hier...", + "evaluate_a_product_idea_question_7_headline": "Was ist dir an dieser Funktion am wichtigsten?", + "evaluate_a_product_idea_question_7_placeholder": "Tippe deine Antwort hier...", + "evaluate_a_product_idea_question_8_headline": "Gibt es sonst noch etwas, das wir beachten sollten?", + "evaluate_a_product_idea_question_8_placeholder": "Tippe deine Antwort hier...", + "evaluate_content_quality_description": "Finde heraus, wie gut dein Content-Marketing funktioniert.", + "evaluate_content_quality_name": "Bewerte die Inhaltsqualität", + "evaluate_content_quality_question_1_headline": "Wie gut hat dieser Artikel das behandelt, was Du zu lernen hofftest?", + "evaluate_content_quality_question_1_lower_label": "Überhaupt nicht gut", + "evaluate_content_quality_question_1_upper_label": "Extrem gut", + "evaluate_content_quality_question_2_headline": "Hmpft! Was hast Du dir erhofft?", + "evaluate_content_quality_question_2_placeholder": "Tippe deine Antwort hier...", + "evaluate_content_quality_question_3_headline": "Super! Gibt es noch etwas, das wir besprechen sollen?", + "evaluate_content_quality_question_3_placeholder": "Themen, Trends, Tutorials...", + "fake_door_follow_up_description": "Folge bei Nutzern nach, die auf eines deiner Fake-Door-Experimente gestoßen sind.", + "fake_door_follow_up_name": "Fake-Door Experiment", + "fake_door_follow_up_question_1_headline": "Wie wichtig ist dir diese Funktion?", + "fake_door_follow_up_question_1_lower_label": "Nicht wichtig", + "fake_door_follow_up_question_1_upper_label": "Sehr wichtig", + "fake_door_follow_up_question_2_choice_1": "Aspekt 1", + "fake_door_follow_up_question_2_choice_2": "Aspekt 2", + "fake_door_follow_up_question_2_choice_3": "Aspekt 3", + "fake_door_follow_up_question_2_choice_4": "Aspekt 4", + "fake_door_follow_up_question_2_headline": "Was sollte beim Bau davon unbedingt berücksichtigt werden?", + "feature_chaser_description": "Befrage Nutzer, die gerade eine bestimmte Funktion verwendet haben.", + "feature_chaser_name": "Feature Chaser", + "feature_chaser_question_1_headline": "Wie wichtig ist [FEATURE HINZUFÜGEN] für dich?", + "feature_chaser_question_1_lower_label": "Nicht wichtig", + "feature_chaser_question_1_upper_label": "Sehr wichtig", + "feature_chaser_question_2_choice_1": "Aspekt 1", + "feature_chaser_question_2_choice_2": "Aspekt 2", + "feature_chaser_question_2_choice_3": "Aspekt 3", + "feature_chaser_question_2_choice_4": "Aspekt 4", + "feature_chaser_question_2_headline": "Welcher Aspekt ist am wichtigsten?", + "feedback_box_description": "Gib deinen Nutzern die Möglichkeit, zu teilen, was ihnen durch den Kopf geht.", + "feedback_box_name": "Feedback-Box", + "feedback_box_question_1_choice_1": "Fehlermeldung \uD83D\uDC1E", + "feedback_box_question_1_choice_2": "Anfrage \uD83D\uDCA1", + "feedback_box_question_1_headline": "Was geht dir durch den Kopf, Chef?", + "feedback_box_question_1_subheader": "Danke fürs Teilen. Wir melden uns so schnell wie möglich bei dir.", + "feedback_box_question_2_headline": "Was ist kaputt?", + "feedback_box_question_2_subheader": "Je mehr Details, desto besser :)", + "feedback_box_question_3_button_label": "Ja, benachrichtige mich", + "feedback_box_question_3_dismiss_button_label": "Nein, danke", + "feedback_box_question_3_headline": "Möchtest Du auf dem Laufenden bleiben?", + "feedback_box_question_3_html": "Wir werden das so schnell wie möglich beheben. Möchtest Du benachrichtigt werden, wenn wir es getan haben?", + "feedback_box_question_4_button_label": "Feature anfordern", + "feedback_box_question_4_headline": "Erzähl uns mehr!", + "feedback_box_question_4_placeholder": "Tippe deine Antwort hier...", + "feedback_box_question_4_subheader": "Welches Problem sollten wir lösen?", + "file_upload": "Datei hochladen", + "file_upload_description": "Ermögliche es den Befragten, Dokumente, Bilder oder andere Dateien hochzuladen", + "finish": "Fertigstellen", + "follow_ups_modal_action_body": "

Hey \uD83D\uDC4B

Danke, dass du dir die Zeit genommen hast zu antworten. Wir melden uns bald bei dir.

Hab noch einen schönen Tag!

", + "free_text": "Freitext", + "free_text_description": "Sammle offenes Feedback", + "free_text_placeholder": "Tippe deine Antwort hier...", + "gauge_feature_satisfaction_description": "Bewerte die Zufriedenheit mit bestimmten Funktionen deines Produkts.", + "gauge_feature_satisfaction_name": "Zufriedenheit mit Funktionen messen", + "gauge_feature_satisfaction_question_1_headline": "Wie einfach war es, ... zu erreichen?", + "gauge_feature_satisfaction_question_1_lower_label": "Nicht einfach", + "gauge_feature_satisfaction_question_1_upper_label": "Sehr einfach", + "gauge_feature_satisfaction_question_2_headline": "Was könnten wir besser machen?", + "identify_customer_goals_description": "Besser verstehen, ob deine Botschaften die richtigen Erwartungen an dein Produkt schaffen.", + "identify_customer_goals_name": "Kundenziele identifizieren", + "identify_sign_up_barriers_description": "Biete einen Rabatt an, um Einblicke in Anmeldebarrieren zu gewinnen.", + "identify_sign_up_barriers_name": "Identifiziere Anmeldebarrieren", + "identify_sign_up_barriers_question_1_button_label": "Erhalte 10% Rabatt", + "identify_sign_up_barriers_question_1_dismiss_button_label": "Nein, danke", + "identify_sign_up_barriers_question_1_headline": "Beantworte diese kurze Umfrage, erhalte 10% Rabatt!", + "identify_sign_up_barriers_question_1_html": "Du scheinst darüber nachzudenken, Dich anzumelden. Beantworte vier Fragen und erhalte 10% Rabatt auf jeden Plan.", + "identify_sign_up_barriers_question_2_headline": "Wie wahrscheinlich ist es, dass Du Dich für $[projectName] anmeldest?", + "identify_sign_up_barriers_question_2_lower_label": "Überhaupt nicht wahrscheinlich", + "identify_sign_up_barriers_question_2_upper_label": "Sehr wahrscheinlich", + "identify_sign_up_barriers_question_3_choice_1_label": "Hat vielleicht nicht, wonach ich suche", + "identify_sign_up_barriers_question_3_choice_2_label": "Vergleiche noch Optionen", + "identify_sign_up_barriers_question_3_choice_3_label": "Scheint kompliziert zu sein", + "identify_sign_up_barriers_question_3_choice_4_label": "Preise sind ein Problem", + "identify_sign_up_barriers_question_3_choice_5_label": "Etwas anderes", + "identify_sign_up_barriers_question_3_headline": "Was hält Dich davon ab, $[projectName] auszuprobieren?", + "identify_sign_up_barriers_question_4_headline": "Was brauchst du, das $[projectName] nicht bietet?", + "identify_sign_up_barriers_question_4_placeholder": "Tippe deine Antwort hier...", + "identify_sign_up_barriers_question_5_headline": "Welche Optionen schaust Du dir an?", + "identify_sign_up_barriers_question_5_placeholder": "Tippe deine Antwort hier...", + "identify_sign_up_barriers_question_6_headline": "Was erscheint dir kompliziert?", + "identify_sign_up_barriers_question_6_placeholder": "Tippe deine Antwort hier...", + "identify_sign_up_barriers_question_7_headline": "Worüber machst Du dir bei dem Preis Sorgen?", + "identify_sign_up_barriers_question_7_placeholder": "Tippe deine Antwort hier...", + "identify_sign_up_barriers_question_8_headline": "Bitte erzähle uns mehr:", + "identify_sign_up_barriers_question_8_placeholder": "Tippe deine Antwort hier...", + "identify_sign_up_barriers_question_9_button_label": "Registrieren", + "identify_sign_up_barriers_question_9_dismiss_button_label": "Erstmal überspringen", + "identify_sign_up_barriers_question_9_headline": "Danke! Hier ist dein Code: SIGNUPNOW10", + "identify_sign_up_barriers_question_9_html": "Vielen Dank, dass Du dir die Zeit genommen hast, Feedback zu geben \uD83D\uDE4F", + "identify_sign_up_barriers_with_project_name": "Anmeldebarrieren für $[projectName]", + "identify_upsell_opportunities_description": "Finde heraus, wie viel Zeit dein Produkt deinem Nutzer spart. Nutze dies, um mehr zu verkaufen.", + "identify_upsell_opportunities_name": "Upsell-Möglichkeiten identifizieren", + "identify_upsell_opportunities_question_1_choice_1": "Weniger als 1 Stunde", + "identify_upsell_opportunities_question_1_choice_2": "1 bis 2 Stunden", + "identify_upsell_opportunities_question_1_choice_3": "3 bis 5 Stunden", + "identify_upsell_opportunities_question_1_choice_4": "5+ Stunden", + "identify_upsell_opportunities_question_1_headline": "Wie viele Stunden spart dein Team pro Woche durch die Nutzung von $[projectName]?", + "improve_activation_rate_description": "Schwachstellen in deinem Onboarding-Prozess identifizieren, um die Nutzeraktivierung zu steigern.", + "improve_activation_rate_name": "Aktivierungsrate verbessern", + "improve_activation_rate_question_1_choice_1": "Schien mir nicht nützlich.", + "improve_activation_rate_question_1_choice_2": "Schwierig einzurichten oder zu benutzen", + "improve_activation_rate_question_1_choice_3": "Fehlende Funktionen/Funktionalität", + "improve_activation_rate_question_1_choice_4": "Hatte einfach keine Zeit", + "improve_activation_rate_question_1_choice_5": "Etwas anderes", + "improve_activation_rate_question_1_headline": "Was ist der Hauptgrund, warum Du $[projectName] noch nicht eingerichtet hast?", + "improve_activation_rate_question_2_headline": "Warum denkst Du, dass $[projectName] nicht nützlich ist?", + "improve_activation_rate_question_2_placeholder": "Tippe deine Antwort hier...", + "improve_activation_rate_question_3_headline": "Was war schwierig bei der Einrichtung oder Nutzung von $[projectName]?", + "improve_activation_rate_question_3_placeholder": "Tippe deine Antwort hier...", + "improve_activation_rate_question_4_headline": "Welche Funktionen oder Eigenschaften haben gefehlt?", + "improve_activation_rate_question_4_placeholder": "Tippe deine Antwort hier...", + "improve_activation_rate_question_5_headline": "Wie könnten wir es dir leichter machen, anzufangen?", + "improve_activation_rate_question_5_placeholder": "Tippe deine Antwort hier...", + "improve_activation_rate_question_6_headline": "Was war das? Bitte erzähle uns mehr:", + "improve_activation_rate_question_6_placeholder": "Tippe deine Antwort hier...", + "improve_activation_rate_question_6_subheader": "Wir freuen uns, das so schnell wie möglich zu beheben.", + "improve_newsletter_content_description": "Finde heraus, wie deine Abonnenten deinen Newsletter finden.", + "improve_newsletter_content_name": "Newsletter verbessern", + "improve_newsletter_content_question_1_headline": "Wie bewertest Du den Newsletter dieser Woche?", + "improve_newsletter_content_question_1_lower_label": "Geht so", + "improve_newsletter_content_question_1_upper_label": "Toll", + "improve_newsletter_content_question_2_headline": "Was hätte den Newsletter dieser Woche hilfreicher gemacht?", + "improve_newsletter_content_question_2_placeholder": "Tippe deine Antwort hier...", + "improve_newsletter_content_question_3_button_label": "Freut mich, dir zu helfen!", + "improve_newsletter_content_question_3_dismiss_button_label": "Finde deine eigenen Freunde", + "improve_newsletter_content_question_3_headline": "Danke! ❤️ Teile den Newsletter mit einer Person, die Dir wichtig ist.", + "improve_newsletter_content_question_3_html": "Wer denkt wie du? Du würdest uns einen riesigen Gefallen tun, wenn Du diese Episode teilen würdest!", + "improve_trial_conversion_description": "Finde heraus, warum Leute ihre Testphase abgebrochen haben. Diese Erkenntnisse helfen dir, deinen Funnel zu verbessern.", + "improve_trial_conversion_name": "Trial Konvertierung verbessern", + "improve_trial_conversion_question_1_choice_1": "Ich habe nicht viel davon gehabt", + "improve_trial_conversion_question_1_choice_2": "Ich habe etwas anderes erwartet", + "improve_trial_conversion_question_1_choice_3": "Es ist zu teuer für das, was es leistet", + "improve_trial_conversion_question_1_choice_4": "Mir fehlt eine Funktion", + "improve_trial_conversion_question_1_choice_5": "Ich habe mich nur umgesehen", + "improve_trial_conversion_question_1_headline": "Warum hast Du deine Testversion beendet?", + "improve_trial_conversion_question_1_subheader": "Hilf uns, Dich besser zu verstehen:", + "improve_trial_conversion_question_2_button_label": "Weiter", + "improve_trial_conversion_question_2_headline": "Das tut mir leid zu hören. Was war das größte Problem bei der Nutzung von $[projectName]?", + "improve_trial_conversion_question_4_button_label": "Erhalte 20% Rabatt", + "improve_trial_conversion_question_4_dismiss_button_label": "Überspringen", + "improve_trial_conversion_question_4_headline": "Das tut mir leid zu hören! Erhalte 20% Rabatt im ersten Jahr.", + "improve_trial_conversion_question_4_html": "

Wir freuen uns, dir einen 20% Rabatt auf einen Jahresplan anzubieten.

", + "improve_trial_conversion_question_5_button_label": "Weiter", + "improve_trial_conversion_question_5_headline": "Was möchtest Du erreichen?", + "improve_trial_conversion_question_5_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "improve_trial_conversion_question_6_headline": "Wie löst Du dein Problem heutzutage?", + "improve_trial_conversion_question_6_subheader": "Bitte nenne alternative Lösungen:", + "integration_setup_survey_description": "Bewerte, wie einfach Nutzer Integrationen zu deinem Produkt hinzufügen können.", + "integration_setup_survey_name": "Umfrage zur Nutzung von Integrationen", + "integration_setup_survey_question_1_headline": "Wie einfach war es, diese Integration einzurichten?", + "integration_setup_survey_question_1_lower_label": "Nicht einfach", + "integration_setup_survey_question_1_upper_label": "Sehr einfach", + "integration_setup_survey_question_2_headline": "Warum war es schwer?", + "integration_setup_survey_question_2_placeholder": "Tippe deine Antwort hier...", + "integration_setup_survey_question_3_headline": "Welche anderen Tools würdest Du gerne mit $[projectName] verwenden?", + "integration_setup_survey_question_3_subheader": "Wir entwickeln ständig neue Integrationen, deine könnte die nächste sein:", + "interview_prompt_description": "Lade eine bestimmte Gruppe deiner Nutzer ein, ein Interview mit deinem Produktteam zu vereinbaren.", + "interview_prompt_name": "Interview Aufforderung", + "interview_prompt_question_1_button_label": "Zeitraum buchen", + "interview_prompt_question_1_headline": "Hast Du 15 Minuten Zeit, um mit uns zu sprechen? \uD83D\uDE4F", + "interview_prompt_question_1_html": "Du bist einer unserer Power-User. Wir würden Dich gerne kurz interviewen!", + "long_term_retention_check_in_description": "Langfristige Benutzerzufriedenheit, Loyalität und Verbesserungsbereiche messen, um loyale Benutzer zu halten.", + "long_term_retention_check_in_name": "Langzeitgedächtnis-Check-in", + "long_term_retention_check_in_question_10_headline": "Weitere Rückmeldungen oder Kommentare?", + "long_term_retention_check_in_question_10_placeholder": "Teile uns gerne deine Gedanken oder dein Feedback mit, das uns helfen könnte, uns zu verbessern...", + "long_term_retention_check_in_question_1_headline": "Wie zufrieden bist Du insgesamt mit $[projectName]?", + "long_term_retention_check_in_question_1_lower_label": "Nicht zufrieden", + "long_term_retention_check_in_question_1_upper_label": "Sehr zufrieden", + "long_term_retention_check_in_question_2_headline": "Was findest Du am wertvollsten an $[projectName]?", + "long_term_retention_check_in_question_2_placeholder": "Beschreibe das Merkmal oder den Vorteil, den Du am meisten schätzt...", + "long_term_retention_check_in_question_3_choice_1": "Funktionen", + "long_term_retention_check_in_question_3_choice_2": "Kundensupport", + "long_term_retention_check_in_question_3_choice_3": "Nutzererlebnis", + "long_term_retention_check_in_question_3_choice_4": "Preise", + "long_term_retention_check_in_question_3_choice_5": "Zuverlässigkeit und Betriebszeit", + "long_term_retention_check_in_question_3_headline": "Welchen Aspekt von $[projectName] findest Du für dein Erlebnis am wichtigsten?", + "long_term_retention_check_in_question_4_headline": "Wie gut erfüllt $[projectName] deine Erwartungen?", + "long_term_retention_check_in_question_4_lower_label": "Reicht nicht aus", + "long_term_retention_check_in_question_4_upper_label": "Übertrifft die Erwartungen", + "long_term_retention_check_in_question_5_headline": "Welche Herausforderungen oder Frustrationen hast Du bei der Nutzung von $[projectName] erlebt?", + "long_term_retention_check_in_question_5_placeholder": "Beschreibe alle Herausforderungen oder Verbesserungen, die Du gerne sehen würdest...", + "long_term_retention_check_in_question_6_headline": "Wie wahrscheinlich ist es, dass Du $[projectName] einem Freund oder Kollegen weiterempfiehlst?", + "long_term_retention_check_in_question_6_lower_label": "Nicht wahrscheinlich", + "long_term_retention_check_in_question_6_upper_label": "Sehr wahrscheinlich", + "long_term_retention_check_in_question_7_choice_1": "Neue Funktionen und Verbesserungen", + "long_term_retention_check_in_question_7_choice_2": "Verbesserter Kundensupport", + "long_term_retention_check_in_question_7_choice_3": "Bessere Preisoptionen", + "long_term_retention_check_in_question_7_choice_4": "Mehr Integrationen", + "long_term_retention_check_in_question_7_choice_5": "Verbesserungen der Benutzererfahrung", + "long_term_retention_check_in_question_7_headline": "Was würde Dich eher dazu bringen, langfristig Nutzer zu bleiben?", + "long_term_retention_check_in_question_8_headline": "Wenn Du eine Sache an $[projectName] ändern könntest, was wäre das?", + "long_term_retention_check_in_question_8_placeholder": "Teile uns Änderungen oder Funktionen mit, die Du dir wünschst.", + "long_term_retention_check_in_question_9_headline": "Wie zufrieden bist Du mit unseren Produktaktualisierungen und deren Häufigkeit?", + "long_term_retention_check_in_question_9_lower_label": "Nicht glücklich", + "long_term_retention_check_in_question_9_upper_label": "Sehr glücklich", + "market_attribution_description": "Lerne, wie Nutzer zuerst von deinem Produkt erfahren haben.", + "market_attribution_name": "Marketing-Attribution", + "market_attribution_question_1_choice_1": "Empfehlung", + "market_attribution_question_1_choice_2": "Soziale Medien", + "market_attribution_question_1_choice_3": "Werbung", + "market_attribution_question_1_choice_4": "Google-Suche", + "market_attribution_question_1_choice_5": "In einem Podcast", + "market_attribution_question_1_headline": "Wie hast Du zuerst von uns gehört?", + "market_attribution_question_1_subheader": "Bitte wähle eine der folgenden Optionen:", + "market_site_clarity_description": "Identifiziere Nutzer, die deine Marketingseite verlassen. Verbessere deine Botschaften.", + "market_site_clarity_name": "Klarheit von Marketingseiten", + "market_site_clarity_question_1_choice_1": "Ja, total", + "market_site_clarity_question_1_choice_2": "Sowas wie...", + "market_site_clarity_question_1_choice_3": "Nein, überhaupt nicht", + "market_site_clarity_question_1_headline": "Hast Du alle Infos, die Du brauchst, um $[projectName] auszuprobieren?", + "market_site_clarity_question_2_headline": "Was fehlt dir oder ist dir unklar an $[projectName]?", + "market_site_clarity_question_3_button_label": "Rabatt bekommen", + "market_site_clarity_question_3_headline": "Danke für deine Antwort! Erhalte 25% Rabatt auf deine ersten 6 Monate:", + "matrix": "Matrix", + "matrix_description": "Erstelle ein Raster, um mehrere Elemente anhand derselben Kriterien zu bewerten", + "measure_search_experience_description": "Miss die Relevanz deiner Suchergebnisse.", + "measure_search_experience_name": "Sucherfahrung messen", + "measure_search_experience_question_1_headline": "Wie relevant sind diese Suchergebnisse?", + "measure_search_experience_question_1_lower_label": "Überhaupt nicht relevant", + "measure_search_experience_question_1_upper_label": "Sehr relevant", + "measure_search_experience_question_2_headline": "Ugh! Was macht die Ergebnisse für Dich irrelevant?", + "measure_search_experience_question_2_placeholder": "Tippe deine Antwort hier...", + "measure_search_experience_question_3_headline": "Super! Gibt es etwas, das wir tun können, um deine Erfahrung zu verbessern?", + "measure_search_experience_question_3_placeholder": "Tippe deine Antwort hier...", + "measure_task_accomplishment_description": "Schau, ob die Leute ihren \"Job-to-be-done\" erledigen. Zufriedene Leute sind bessere Kunden.", + "measure_task_accomplishment_name": "Job-to-be-done messen", + "measure_task_accomplishment_question_1_headline": "Hast Du heute erreicht, was Du erreichen wolltest?", + "measure_task_accomplishment_question_1_option_1_label": "Ja", + "measure_task_accomplishment_question_1_option_2_label": "Bin dabei", + "measure_task_accomplishment_question_1_option_3_label": "Nein", + "measure_task_accomplishment_question_2_headline": "Wie einfach war es, dein Ziel zu erreichen?", + "measure_task_accomplishment_question_2_lower_label": "Sehr schwierig", + "measure_task_accomplishment_question_2_upper_label": "Sehr einfach", + "measure_task_accomplishment_question_3_headline": "Was hat es schwer gemacht?", + "measure_task_accomplishment_question_3_placeholder": "Tippe deine Antwort hier...", + "measure_task_accomplishment_question_4_button_label": "Senden", + "measure_task_accomplishment_question_4_headline": "Super! Was war dein Ziel?", + "measure_task_accomplishment_question_5_button_label": "Senden", + "measure_task_accomplishment_question_5_headline": "Was hat Dich aufgehalten?", + "measure_task_accomplishment_question_5_placeholder": "Tippe deine Antwort hier...", + "multi_select": "Mehrfachauswahl", + "multi_select_description": "Bitte die Befragten, eine oder mehrere Optionen auszuwählen", + "new_integration_survey_description": "Finde heraus, welche Integrationen deine Nutzer als nächstes sehen möchten.", + "new_integration_survey_name": "Umfrage zu neuen Integrationen", + "new_integration_survey_question_1_choice_1": "Salesforce", + "new_integration_survey_question_1_choice_2": "Segment", + "new_integration_survey_question_1_choice_3": "Hubspot", + "new_integration_survey_question_1_choice_4": "Twilio", + "new_integration_survey_question_1_choice_5": "Andere", + "new_integration_survey_question_1_headline": "Welche anderen Werkzeuge benutzt du?", + "next": "Weiter", + "nps": "Net Promoter Score (NPS)", + "nps_description": "Net-Promoter-Score messen (0-10)", + "nps_lower_label": "Überhaupt nicht wahrscheinlich", + "nps_name": "Net Promoter Score (NPS)", + "nps_question_1_headline": "Wie wahrscheinlich ist es, dass Du $[projectName] einem Freund oder Kollegen weiterempfiehlst?", + "nps_question_1_lower_label": "Nicht wahrscheinlich", + "nps_question_1_upper_label": "Sehr wahrscheinlich", + "nps_question_2_headline": "Was hat Dich dazu gebracht, diese Bewertung zu geben?", + "nps_survey_name": "NPS-Umfrage", + "nps_survey_question_1_headline": "Wie wahrscheinlich ist es, dass Du $[projectName] einem Freund oder Kollegen weiterempfiehlst?", + "nps_survey_question_1_lower_label": "Überhaupt nicht wahrscheinlich", + "nps_survey_question_1_upper_label": "Sehr wahrscheinlich", + "nps_survey_question_2_headline": "Hilf uns dabei, uns zu verbessern! Bitte erkläre uns, warum Du dieser Bewertung gibst:", + "nps_survey_question_3_headline": "Weitere Kommentare, Feedback oder Bedenken?", + "nps_upper_label": "Sehr wahrscheinlich", + "onboarding_segmentation": "Onboarding-Segmentierung", + "onboarding_segmentation_description": "Erfahre mehr darüber, wer sich für dein Produkt angemeldet hat und warum.", + "onboarding_segmentation_question_1_choice_1": "Gründer", + "onboarding_segmentation_question_1_choice_2": "Führungskraft", + "onboarding_segmentation_question_1_choice_3": "Produktmanager", + "onboarding_segmentation_question_1_choice_4": "People Manager", + "onboarding_segmentation_question_1_choice_5": "Softwareentwickler", + "onboarding_segmentation_question_1_headline": "Was ist deine Rolle?", + "onboarding_segmentation_question_1_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "onboarding_segmentation_question_2_choice_1": "nur ich", + "onboarding_segmentation_question_2_choice_2": "1-5 Mitarbeiter", + "onboarding_segmentation_question_2_choice_3": "6-10 Mitarbeiter", + "onboarding_segmentation_question_2_choice_4": "11-100 Mitarbeiter", + "onboarding_segmentation_question_2_choice_5": "über 100 Mitarbeiter", + "onboarding_segmentation_question_2_headline": "Wie groß ist deine Firma?", + "onboarding_segmentation_question_2_subheader": "Bitte wähle eine der folgenden Optionen:", + "onboarding_segmentation_question_3_choice_1": "Empfehlung", + "onboarding_segmentation_question_3_choice_2": "Soziale Medien", + "onboarding_segmentation_question_3_choice_3": "Werbung", + "onboarding_segmentation_question_3_choice_4": "Google", + "onboarding_segmentation_question_3_choice_5": "In einem Podcast", + "onboarding_segmentation_question_3_headline": "Wie hast Du zuerst von uns gehört?", + "onboarding_segmentation_question_3_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "picture_selection": "Bilderauswahl", + "picture_selection_description": "Bitte die Befragten, ein oder mehrere Bilder auszuwählen", + "preview_survey_ending_card_description": "Mach bitte mit deinem Onboarding weiter.", + "preview_survey_ending_card_headline": "Geschafft!", + "preview_survey_name": "Vorschau", + "preview_survey_question_1_headline": "Wie würdest Du {projectName} bewerten?", + "preview_survey_question_1_lower_label": "Nicht gut", + "preview_survey_question_1_subheader": "Das ist eine Vorschau der Umfrage.", + "preview_survey_question_1_upper_label": "Sehr gut", + "preview_survey_question_2_back_button_label": "Zurück", + "preview_survey_question_2_choice_1_label": "Ja, halte mich auf dem Laufenden.", + "preview_survey_question_2_choice_2_label": "Nein, danke!", + "preview_survey_question_2_headline": "Willst du auf dem Laufenden bleiben?", + "preview_survey_welcome_card_headline": "Willkommen!", + "preview_survey_welcome_card_html": "Danke für dein Feedback - los geht's!", + "prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.", + "prioritize_features_name": "Funktionen priorisieren", + "prioritize_features_question_1_choice_1": "Funktion 1", + "prioritize_features_question_1_choice_2": "Funktion 2", + "prioritize_features_question_1_choice_3": "Funktion 3", + "prioritize_features_question_1_choice_4": "Andere", + "prioritize_features_question_1_headline": "Welche dieser Funktionen wäre für Dich am wertvollsten?", + "prioritize_features_question_2_choice_1": "Funktion 1", + "prioritize_features_question_2_choice_2": "Funktion 2", + "prioritize_features_question_2_choice_3": "Funktion 3", + "prioritize_features_question_2_headline": "Welche dieser Funktionen wäre für Dich am wenigsten wertvoll?", + "prioritize_features_question_3_headline": "Wie könnten wir sonst noch deine Erfahrung mit $[projectName] verbessern?", + "prioritize_features_question_3_placeholder": "Tippe deine Antwort hier...", + "product_market_fit_short_description": "Miss den Product-Market-Fit, indem Du bewertest, wie enttäuscht die Nutzer wären, wenn es dein Produkt nicht mehr gäbe.", + "product_market_fit_short_name": "Product-Market-Fit (Kurz)", + "product_market_fit_short_question_1_choice_1": "Überhaupt nicht enttäuscht", + "product_market_fit_short_question_1_choice_2": "Etwas enttäuscht", + "product_market_fit_short_question_1_choice_3": "Sehr enttäuscht", + "product_market_fit_short_question_1_headline": "Wie enttäuscht wärst du, wenn Du $[projectName] nicht mehr nutzen könntest?", + "product_market_fit_short_question_1_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "product_market_fit_short_question_2_headline": "Wie können wir $[projectName] für Dich verbessern?", + "product_market_fit_short_question_2_subheader": "Bitte sei so genau wie möglich.", + "product_market_fit_superhuman": "Product-Market-Fit (Lang)", + "product_market_fit_superhuman_description": "Miss den Product-Market-Fit, indem Du bewertest, wie enttäuscht die Nutzer wären, wenn es dein Produkt nicht mehr gäbe.", + "product_market_fit_superhuman_question_1_button_label": "Freut mich, dir zu helfen!", + "product_market_fit_superhuman_question_1_dismiss_button_label": "Nein, danke.", + "product_market_fit_superhuman_question_1_headline": "Du bist einer unserer Power-User! Hast Du 5 Minuten?", + "product_market_fit_superhuman_question_1_html": "

Wir würden gerne besser verstehen, wie Dir $[projectName] gefällt! Deine Meinung hilft uns sehr!

", + "product_market_fit_superhuman_question_2_choice_1": "Überhaupt nicht enttäuscht", + "product_market_fit_superhuman_question_2_choice_2": "Etwas enttäuscht", + "product_market_fit_superhuman_question_2_choice_3": "Sehr enttäuscht", + "product_market_fit_superhuman_question_2_headline": "Wie enttäuscht wärst du, wenn Du $[projectName] nicht mehr nutzen könntest?", + "product_market_fit_superhuman_question_2_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "product_market_fit_superhuman_question_3_choice_1": "Gründer", + "product_market_fit_superhuman_question_3_choice_2": "Führungskraft", + "product_market_fit_superhuman_question_3_choice_3": "Produktmanager", + "product_market_fit_superhuman_question_3_choice_4": "People Manager", + "product_market_fit_superhuman_question_3_choice_5": "Softwareentwickler", + "product_market_fit_superhuman_question_3_headline": "Was ist deine Rolle?", + "product_market_fit_superhuman_question_3_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "product_market_fit_superhuman_question_4_headline": "Wer würde am ehesten von $[projectName] profitieren?", + "product_market_fit_superhuman_question_5_headline": "Welchen Mehrwert ziehst Du aus $[projectName]?", + "product_market_fit_superhuman_question_6_headline": "Wie können wir $[projectName] für Dich verbessern?", + "product_market_fit_superhuman_question_6_subheader": "Bitte sei so detalliert wie möglich.", + "professional_development_growth_survey_description": "Bewerte die Zufriedenheit der Mitarbeiter mit beruflichen Wachstums- und Entwicklungsmöglichkeiten.", + "professional_development_growth_survey_name": "Berufliche Entwicklung und Wachstum", + "professional_development_growth_survey_question_1_headline": "Ich habe das Gefühl, dass ich bei der Arbeit Möglichkeiten habe, meine Fähigkeiten weiterzuentwickeln.", + "professional_development_growth_survey_question_1_lower_label": "Keine Wachstumsmöglichkeiten", + "professional_development_growth_survey_question_1_upper_label": "Viele Wachstumsmöglichkeiten", + "professional_development_growth_survey_question_2_headline": "Ich habe genügend Autonomie, um Entscheidungen über meine Arbeitsweise zu treffen.", + "professional_development_growth_survey_question_2_lower_label": "Keine Autonomie", + "professional_development_growth_survey_question_2_upper_label": "Vollständige Autonomie", + "professional_development_growth_survey_question_3_headline": "Meine Ziele bei der Arbeit sind klar und auf meine Entwicklung abgestimmt.", + "professional_development_growth_survey_question_3_lower_label": "Unklare Ziele", + "professional_development_growth_survey_question_3_upper_label": "Klare und abgestimmte Ziele", + "professional_development_growth_survey_question_4_headline": "Was könnte verbessert werden, um deine berufliche Entwicklung zu unterstützen?", + "professional_development_growth_survey_question_4_placeholder": "Tippe deine Antwort hier...", + "professional_development_survey_description": "Bewerte die Zufriedenheit der Mitarbeiter mit beruflichen Entwicklungsmöglichkeiten.", + "professional_development_survey_name": "Berufliche Entwicklungsbewertung", + "professional_development_survey_question_1_choice_1": "Ja", + "professional_development_survey_question_1_choice_2": "Nein", + "professional_development_survey_question_1_headline": "Sind Sie an beruflichen Entwicklungsmöglichkeiten interessiert?", + "professional_development_survey_question_2_choice_1": "Networking-Veranstaltungen", + "professional_development_survey_question_2_choice_2": "Konferenzen oder Seminare", + "professional_development_survey_question_2_choice_3": "Kurse oder Workshops", + "professional_development_survey_question_2_choice_4": "Mentoring", + "professional_development_survey_question_2_choice_5": "Individuelle Forschung", + "professional_development_survey_question_2_choice_6": "Andere", + "professional_development_survey_question_2_headline": "Welche Arten von beruflichen Entwicklungsmöglichkeiten denken Sie, wären am wertvollsten für Ihre Weiterentwicklung?", + "professional_development_survey_question_2_subheader": "Bitte wählen Sie alle zutreffenden Optionen aus:", + "professional_development_survey_question_3_choice_1": "Ja", + "professional_development_survey_question_3_choice_2": "Nein", + "professional_development_survey_question_3_headline": "Haben Sie in der Vergangenheit Zeit für Ihre berufliche Weiterentwicklung eingeplant?", + "professional_development_survey_question_4_headline": "Wie unterstützt Sie Ihr Arbeitsumfeld bei der Verfolgung beruflicher Entwicklungsmöglichkeiten?", + "professional_development_survey_question_4_lower_label": "Überhaupt nicht unterstützt", + "professional_development_survey_question_4_upper_label": "Sehr unterstützt", + "professional_development_survey_question_5_choice_1": "Für mein eigenes Wissen", + "professional_development_survey_question_5_choice_2": "Mehr Verantwortung zu übernehmen", + "professional_development_survey_question_5_choice_3": "Meine Fähigkeiten zu verbessern", + "professional_development_survey_question_5_choice_4": "Meine Karriere weiter zu fördern", + "professional_development_survey_question_5_choice_5": "Eine neue Stelle zu suchen", + "professional_development_survey_question_5_choice_6": "Andere", + "professional_development_survey_question_5_headline": "Was sind Ihre Hauptgründe, um Zeit für berufliche Weiterentwicklung einzuplanen?", + "ranking": "Ranking", + "ranking_description": "Bitte die Befragten, die Elemente nach Präferenz oder Wichtigkeit zu ordnen.", + "rate_checkout_experience_description": "Lass Kunden den Checkout-Prozess bewerten, um die Conversion zu optimieren.", + "rate_checkout_experience_name": "Bewerte den Checkout-Prozess", + "rate_checkout_experience_question_1_headline": "Wie einfach oder schwierig war es, den Checkout abzuschließen?", + "rate_checkout_experience_question_1_lower_label": "Sehr schwierig", + "rate_checkout_experience_question_1_upper_label": "Sehr einfach", + "rate_checkout_experience_question_2_headline": "Oh das tut uns leid! Was hätte es dir leichter gemacht?", + "rate_checkout_experience_question_2_placeholder": "Tippe deine Antwort hier...", + "rate_checkout_experience_question_3_headline": "Super! Gibt es etwas, das wir tun können, um deine Erfahrung noch besser zu machen?", + "rate_checkout_experience_question_3_placeholder": "Tippe deine Antwort hier...", + "rating": "Bewertung", + "rating_description": "Bitte die Befragten um eine Bewertung (Sterne, Smileys, Zahlen)", + "rating_lower_label": "Nicht gut", + "rating_upper_label": "Sehr gut", + "recognition_and_reward_survey_description": "Bewerte die Mitarbeiterzufriedenheit in Bezug auf Anerkennung, Belohnungen, Führungsunterstützung und Meinungsfreiheit.", + "recognition_and_reward_survey_name": "Anerkennung und Belohnung", + "recognition_and_reward_survey_question_1_headline": "Wenn ich gute Leistungen erbringe, werden meine Beiträge von der Organisation anerkannt.", + "recognition_and_reward_survey_question_1_lower_label": "Gar nicht anerkannt", + "recognition_and_reward_survey_question_1_upper_label": "Sehr anerkannt", + "recognition_and_reward_survey_question_2_headline": "Ich fühle mich für meine Arbeit fair belohnt.", + "recognition_and_reward_survey_question_2_lower_label": "Nicht fair belohnt", + "recognition_and_reward_survey_question_2_upper_label": "Sehr fair belohnt", + "recognition_and_reward_survey_question_3_headline": "Ich fühle mich wohl dabei, meine Meinungen bei der Arbeit offen zu teilen.", + "recognition_and_reward_survey_question_3_lower_label": "Fühle mich nicht wohl", + "recognition_and_reward_survey_question_3_upper_label": "Fühle mich sehr wohl", + "recognition_and_reward_survey_question_4_headline": "Wie könnte die Organisation Anerkennung und Belohnungen verbessern?", + "recognition_and_reward_survey_question_4_placeholder": "Tippe deine Antwort hier...", + "review_prompt_description": "Lade Nutzer, die dein Produkt lieben, ein, es öffentlich zu bewerten.", + "review_prompt_name": "Bewertungsaufforderung", + "review_prompt_question_1_headline": "Wie gefällt dir $[projectName]?", + "review_prompt_question_1_lower_label": "Nicht gut", + "review_prompt_question_1_upper_label": "Sehr zufrieden", + "review_prompt_question_2_button_label": "Bewertung schreiben", + "review_prompt_question_2_headline": "Freut mich zu hören \uD83D\uDE4F Bitte schreibe eine Bewertung für uns!", + "review_prompt_question_2_html": "

Das hilft uns sehr.

", + "review_prompt_question_3_button_label": "Senden", + "review_prompt_question_3_headline": "Das tut mir leid zu hören! Was ist EINE Sache, die wir besser machen können?", + "review_prompt_question_3_placeholder": "Tippe deine Antwort hier...", + "review_prompt_question_3_subheader": "Hilf uns, deine Experience zu verbessern.", + "schedule_a_meeting": "Ein Meeting planen", + "schedule_a_meeting_description": "Bitte die Befragten, einen Termin für Meetings oder Anrufe zu buchen.", + "single_select": "Einzelauswahl", + "single_select_description": "Biete eine Liste von Optionen an (wähle eine aus)", + "site_abandonment_survey": "Umfrage zum Verlassen der Website", + "site_abandonment_survey_description": "Verstehe die Gründe für den Kaufabbruch in deinem Webshop.", + "site_abandonment_survey_question_1_html": "

Wir haben bemerkt, dass Du unsere Seite verlässt, ohne einen Kauf zu tätigen. Wir würden gerne verstehen, warum.

", + "site_abandonment_survey_question_2_button_label": "Klar!", + "site_abandonment_survey_question_2_dismiss_button_label": "Nein, danke.", + "site_abandonment_survey_question_2_headline": "Hast Du eine Minute?", + "site_abandonment_survey_question_3_choice_1": "Konnte nicht finden, wonach ich suche", + "site_abandonment_survey_question_3_choice_2": "Eine bessere Seite gefunden", + "site_abandonment_survey_question_3_choice_3": "Die Seite ist zu langsam", + "site_abandonment_survey_question_3_choice_4": "Ich schaue mich nur um", + "site_abandonment_survey_question_3_choice_5": "Woanders einen besseren Preis gefunden", + "site_abandonment_survey_question_3_choice_6": "Andere", + "site_abandonment_survey_question_3_headline": "Was ist der Hauptgrund, warum Du unsere Seite verlässt?", + "site_abandonment_survey_question_3_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "site_abandonment_survey_question_4_headline": "Bitte erläutere deinen Grund für das Verlassen der Seite:", + "site_abandonment_survey_question_5_headline": "Wie würdest Du deine Gesamterfahrung auf unserer Seite bewerten?", + "site_abandonment_survey_question_5_lower_label": "Sehr unzufrieden", + "site_abandonment_survey_question_5_upper_label": "Sehr zufrieden", + "site_abandonment_survey_question_6_choice_1": "Schnellere Ladezeiten", + "site_abandonment_survey_question_6_choice_2": "Bessere Produktsuchfunktion", + "site_abandonment_survey_question_6_choice_3": "Mehr Produktvielfalt", + "site_abandonment_survey_question_6_choice_4": "Verbesserte Seitengestaltung", + "site_abandonment_survey_question_6_choice_5": "Mehr Kundenbewertungen", + "site_abandonment_survey_question_6_choice_6": "Andere", + "site_abandonment_survey_question_6_headline": "Welche Verbesserungen würden Dich dazu ermutigen, länger auf unserer Seite zu bleiben?", + "site_abandonment_survey_question_6_subheader": "Bitte wähle alle zutreffenden Optionen aus:", + "site_abandonment_survey_question_7_headline": "Möchtest Du Updates über neue Produkte und Aktionen erhalten?", + "site_abandonment_survey_question_7_label": "Ja, bitte melde dich.", + "site_abandonment_survey_question_8_headline": "Bitte teile deine E-Mail-Adresse:", + "site_abandonment_survey_question_9_headline": "Weitere Kommentare oder Vorschläge?", + "skip": "Überspringen", + "smileys_survey_name": "Smileys-Umfrage", + "smileys_survey_question_1_headline": "Wie gefällt dir $[projectName]?", + "smileys_survey_question_1_lower_label": "Nicht gut", + "smileys_survey_question_1_upper_label": "Sehr zufrieden", + "smileys_survey_question_2_button_label": "Bewertung schreiben", + "smileys_survey_question_2_headline": "Freut mich zu hören \uD83D\uDE4F Bitte schreibe eine Bewertung für uns!", + "smileys_survey_question_2_html": "

Das hilft uns sehr.

", + "smileys_survey_question_3_button_label": "Senden", + "smileys_survey_question_3_headline": "Es tut mir leid, das zu hören! Was ist EINE Sache, die wir besser machen können?", + "smileys_survey_question_3_placeholder": "Tippe deine Antwort hier...", + "smileys_survey_question_3_subheader": "Hilf uns, dein Erlebnis zu verbessern.", + "star_rating_survey_name": "Bewertungsumfrage für $[projectName]", + "star_rating_survey_question_1_headline": "Wie gefällt dir $[projectName]?", + "star_rating_survey_question_1_lower_label": "Extrem unzufrieden", + "star_rating_survey_question_1_upper_label": "Extrem zufrieden", + "star_rating_survey_question_2_button_label": "Bewertung schreiben", + "star_rating_survey_question_2_headline": "Freut mich zu hören \uD83D\uDE4F Bitte schreibe eine Bewertung für uns!", + "star_rating_survey_question_2_html": "

Das hilft uns sehr.

", + "star_rating_survey_question_3_button_label": "Senden", + "star_rating_survey_question_3_headline": "Schade! Was können wir besser machen?", + "star_rating_survey_question_3_placeholder": "Schreib hier deine Antwort...", + "star_rating_survey_question_3_subheader": "Hilf uns, deine Erfahrung zu verbessern.", + "statement_call_to_action": "Aussage (Call-to-Action)", + "supportive_work_culture_survey_description": "Bewerte die Wahrnehmung der Mitarbeiter bezüglich Führungsunterstützung, Kommunikation und des gesamten Arbeitsumfelds.", + "supportive_work_culture_survey_name": "Unterstützende Arbeitskultur", + "supportive_work_culture_survey_question_1_headline": "Mein Vorgesetzter bietet mir die Unterstützung, die ich zur Erledigung meiner Arbeit benötige.", + "supportive_work_culture_survey_question_1_lower_label": "Keine Unterstützung", + "supportive_work_culture_survey_question_1_upper_label": "Sehr unterstützend", + "supportive_work_culture_survey_question_2_headline": "Die Kommunikation innerhalb der Organisation ist offen und effektiv.", + "supportive_work_culture_survey_question_2_lower_label": "Schlechte Kommunikation", + "supportive_work_culture_survey_question_2_upper_label": "Ausgezeichnete Kommunikation", + "supportive_work_culture_survey_question_3_headline": "Das Arbeitsumfeld ist positiv und unterstützt mein Wohlbefinden.", + "supportive_work_culture_survey_question_3_lower_label": "Nicht unterstützend", + "supportive_work_culture_survey_question_3_upper_label": "Sehr unterstützend", + "supportive_work_culture_survey_question_4_headline": "Wie könnte die Arbeitskultur verbessert werden, um Dich besser zu unterstützen?", + "supportive_work_culture_survey_question_4_placeholder": "Tippe deine Antwort hier...", + "uncover_strengths_and_weaknesses_description": "Finde heraus, was Nutzer an deinem Produkt oder Angebot mögen und nicht mögen.", + "uncover_strengths_and_weaknesses_name": "Stärken und Schwächen aufdecken", + "uncover_strengths_and_weaknesses_question_1_choice_1": "Benutzerfreundlichkeit", + "uncover_strengths_and_weaknesses_question_1_choice_2": "Gutes Preis-Leistungs-Verhältnis", + "uncover_strengths_and_weaknesses_question_1_choice_3": "Es ist Open-Source", + "uncover_strengths_and_weaknesses_question_1_choice_4": "Die Gründer sind süß", + "uncover_strengths_and_weaknesses_question_1_choice_5": "Andere", + "uncover_strengths_and_weaknesses_question_1_headline": "Was schätzt Du am meisten an $[projectName]?", + "uncover_strengths_and_weaknesses_question_2_choice_1": "Dokumentation", + "uncover_strengths_and_weaknesses_question_2_choice_2": "Anpassbarkeit", + "uncover_strengths_and_weaknesses_question_2_choice_3": "Preise", + "uncover_strengths_and_weaknesses_question_2_choice_4": "Andere", + "uncover_strengths_and_weaknesses_question_2_headline": "Was sollten wir verbessern?", + "uncover_strengths_and_weaknesses_question_2_subheader": "Bitte wähle eine der folgenden Optionen aus:", + "uncover_strengths_and_weaknesses_question_3_headline": "Möchtest Du etwas hinzufügen?", + "uncover_strengths_and_weaknesses_question_3_subheader": "Fühl Dich frei, deine Meinung zu sagen, das tun wir auch.", + "understand_low_engagement_description": "Gründe für geringe Beteiligung identifizieren, um die Nutzerakzeptanz zu verbessern.", + "understand_low_engagement_name": "Verstehe geringes Engagement", + "understand_low_engagement_question_1_choice_1": "Schwer zu benutzen", + "understand_low_engagement_question_1_choice_2": "Eine bessere Alternative gefunden", + "understand_low_engagement_question_1_choice_3": "Hatte einfach keine Zeit", + "understand_low_engagement_question_1_choice_4": "Es fehlten die Funktionen, die ich brauche", + "understand_low_engagement_question_1_choice_5": "Andere", + "understand_low_engagement_question_1_headline": "Was ist der Hauptgrund, warum Du in letzter Zeit nicht zu $[projectName] zurückgekehrt bist?", + "understand_low_engagement_question_2_headline": "Was ist schwierig an der Nutzung von $[projectName]?", + "understand_low_engagement_question_2_placeholder": "Tippe deine Antwort hier...", + "understand_low_engagement_question_3_headline": "Verstanden. Welche Alternative benutzt Du stattdessen?", + "understand_low_engagement_question_3_placeholder": "Tippe deine Antwort hier...", + "understand_low_engagement_question_4_headline": "Verstanden. Wie könnten wir es dir leichter machen, anzufangen?", + "understand_low_engagement_question_4_placeholder": "Tippe deine Antwort hier...", + "understand_low_engagement_question_5_headline": "Verstanden. Welche Funktionen oder Features haben gefehlt?", + "understand_low_engagement_question_5_placeholder": "Tippe deine Antwort hier...", + "understand_low_engagement_question_6_headline": "Bitte füge mehr Details hinzu:", + "understand_low_engagement_question_6_placeholder": "Tippe deine Antwort hier...", + "understand_purchase_intention_description": "Finde heraus, wie nah deine Besucher daran sind, zu kaufen oder zu abonnieren.", + "understand_purchase_intention_name": "Kaufabsicht verstehen", + "understand_purchase_intention_question_1_headline": "Wie wahrscheinlich ist es, dass Du heute bei uns einkaufst?", + "understand_purchase_intention_question_1_lower_label": "Überhaupt nicht wahrscheinlich", + "understand_purchase_intention_question_1_upper_label": "Sehr wahrscheinlich", + "understand_purchase_intention_question_2_headline": "Verstanden. Was ist dein Hauptgrund für den heutigen Besuch?", + "understand_purchase_intention_question_2_placeholder": "Tippe deine Antwort hier...", + "understand_purchase_intention_question_3_headline": "Was, wenn überhaupt, hält Dich heute davon ab, einen Kauf zu tätigen?", + "understand_purchase_intention_question_3_placeholder": "Tippe deine Antwort hier..." + } +} diff --git a/apps/web/lib/messages/en-US.json b/apps/web/lib/messages/en-US.json new file mode 100644 index 0000000000..ffeefe4544 --- /dev/null +++ b/apps/web/lib/messages/en-US.json @@ -0,0 +1,2845 @@ +{ + "auth": { + "continue_with_azure": "Continue with Azure", + "continue_with_email": "Continue with Email", + "continue_with_github": "Continue with GitHub", + "continue_with_google": "Continue with Google", + "continue_with_oidc": "Continue with {oidcDisplayName}", + "continue_with_openid": "Continue with OpenID", + "continue_with_saml": "Continue with SAML SSO", + "forgot-password": { + "back_to_login": "Back to login", + "email-sent": { + "heading": "Password reset successfully requested", + "text": "If an account with this email exists, you will receive password reset instructions shortly." + }, + "reset": { + "confirm_password": "Confirm password", + "new_password": "New password", + "no_token_provided": "No token provided", + "passwords_do_not_match": "Passwords do not match", + "success": { + "heading": "Password successfully reset", + "text": "You can now log in with your new password" + } + }, + "reset_password": "Reset password" + }, + "invite": { + "create_account": "Create an account", + "email_does_not_match": "Ooops! Wrong email \uD83E\uDD26", + "email_does_not_match_description": "The email in the invitation does not match yours.", + "go_to_app": "Go to app", + "happy_to_have_you": "Happy to have you \uD83E\uDD17", + "happy_to_have_you_description": "Please create an account or login.", + "invite_expired": "Invite expired \uD83D\uDE25", + "invite_expired_description": "Invites are valid for 7 days. Please request a new invite.", + "invite_not_found": "Invite not found \uD83D\uDE25", + "invite_not_found_description": "The invitation code cannot be found or has already been used.", + "login": "Login", + "welcome_to_organization": "You’re in \uD83C\uDF89", + "welcome_to_organization_description": "Welcome to the organization." + }, + "last_used": "Last Used", + "login": { + "backup_code": "Backup code", + "create_an_account": "Create an account", + "enter_your_backup_code": "Enter your backup code", + "enter_your_two_factor_authentication_code": "Enter your two-factor authentication code", + "forgot_your_password": "Forgot your password?", + "login_to_your_account": "Login to your account", + "login_with_email": "Login with Email", + "lost_access": "Lost Access?", + "new_to_formbricks": "New to Formbricks?", + "use_a_backup_code": "Use a backup code" + }, + "saml_connection_error": "Something went wrong. Please check your app console for more details.", + "signup": { + "captcha_failed": "Captcha failed", + "have_an_account": "Have an account?", + "log_in": "Log in", + "password_validation_contain_at_least_1_number": "Contain at least 1 number", + "password_validation_minimum_8_and_maximum_128_characters": "Minimum 8 & Maximum 128 characters", + "password_validation_uppercase_and_lowercase": "Mix of uppercase and lowercase", + "please_verify_captcha": "Please verify reCAPTCHA", + "privacy_policy": "Privacy Policy", + "terms_of_service": "Terms of Service", + "title": "Create your Formbricks account" + }, + "signup_without_verification_success": { + "user_successfully_created": "User successfully created", + "user_successfully_created_description": "Your new user has been created successfully. Please click the button below and sign in to your account." + }, + "testimonial_1": "We measure the clarity of our docs and learn from churn all on one platform. Great product, very responsive team!", + "testimonial_all_features_included": "All features included", + "testimonial_free_and_open_source": "Free and open-source", + "testimonial_no_credit_card_required": "No credit card required", + "testimonial_title": "Turn customer insights into irresistible experiences.", + "verification-requested": { + "invalid_email_address": "Invalid email address", + "invalid_token": "Invalid token ☹️", + "no_email_provided": "No email provided", + "please_click_the_link_in_the_email_to_activate_your_account": "Please click the link in the email to activate your account.", + "please_confirm_your_email_address": "Please confirm your email address", + "resend_verification_email": "Resend verification email", + "verification_email_successfully_sent": "Verification email successfully sent. Please check your inbox.", + "we_sent_an_email_to": "We sent an email to {email}. ", + "you_didnt_receive_an_email_or_your_link_expired": "You didn't receive an email or your link expired?" + }, + "verify": { + "no_token_provided": "No Token provided", + "verifying": "Verifying..." + } + }, + "billing_confirmation": { + "back_to_billing_overview": "Back to billing overview", + "thanks_for_upgrading": "Thanks a lot for upgrading your Formbricks subscription.", + "upgrade_successful": "Upgrade successful" + }, + "common": { + "accepted": "Accepted", + "account": "Account", + "account_settings": "Account settings", + "action": "Action", + "actions": "Actions", + "active_surveys": "Active surveys", + "activity": "Activity", + "add": "Add", + "add_action": "Add action", + "add_filter": "Add filter", + "add_logo": "Add logo", + "add_project": "Add project", + "add_to_team": "Add to team", + "all": "All", + "all_questions": "All questions", + "allow": "Allow", + "allow_users_to_exit_by_clicking_outside_the_survey": "Allow users to exit by clicking outside the survey", + "an_unknown_error_occurred_while_deleting_table_items": "An unknown error occurred while deleting {type}s", + "and": "And", + "and_response_limit_of": "and response limit of", + "anonymous": "Anonymous", + "api_keys": "API Keys", + "app": "App", + "app_survey": "App Survey", + "apply_filters": "Apply filters", + "are_you_sure": "Are you sure?", + "are_you_sure_this_action_cannot_be_undone": "Are you sure? This action cannot be undone.", + "attributes": "Attributes", + "avatar": "Avatar", + "back": "Back", + "billing": "Billing", + "booked": "Booked", + "bottom_left": "Bottom Left", + "bottom_right": "Bottom Right", + "cancel": "Cancel", + "centered_modal": "Centered Modal", + "choices": "Choices", + "clear_all": "Clear all", + "clear_filters": "Clear filters", + "clear_selection": "Clear selection", + "click": "Click", + "clicks": "Clicks", + "close": "Close", + "code": "Code", + "collapse_rows": "Collapse rows", + "completed": "Completed", + "configuration": "Configuration", + "confirm": "Confirm", + "connect": "Connect", + "connect_formbricks": "Connect Formbricks", + "connected": "Connected", + "contacts": "Contacts", + "copied_to_clipboard": "Copied to clipboard", + "copy": "Copy", + "copy_code": "Copy code", + "copy_link": "Copy Link", + "create_new_organization": "Create new organization", + "create_segment": "Create segment", + "create_survey": "Create survey", + "created": "Created", + "created_at": "Created at", + "created_by": "Created by", + "customer_success": "Customer Success", + "danger_zone": "Danger Zone", + "dark_overlay": "Dark overlay", + "date": "Date", + "default": "Default", + "delete": "Delete", + "description": "Description", + "dev_env": "Dev Environment", + "development_environment_banner": "You're in a development environment. Set it up to test surveys, actions and attributes.", + "disable": "Disable", + "disallow": "Don't allow", + "discard": "Discard", + "dismissed": "Dismissed", + "docs": "Documentation", + "documentation": "Documentation", + "download": "Download", + "draft": "Draft", + "duplicate": "Duplicate", + "e_commerce": "E-Commerce", + "edit": "Edit", + "email": "Email", + "embed": "Embed", + "enterprise_license": "Enterprise License", + "environment_not_found": "Environment not found", + "environment_notice": "You're currently in the {environment} environment.", + "error": "Error", + "error_component_description": "This resource doesn't exist or you don't have the necessary rights to access it.", + "error_component_title": "Error loading resources", + "expand_rows": "Expand rows", + "finish": "Finish", + "follow_these": "Follow these", + "formbricks_version": "Formbricks Version", + "full_name": "Full name", + "gathering_responses": "Gathering responses", + "general": "General", + "go_back": "Go Back", + "go_to_dashboard": "Go to Dashboard", + "hidden": "Hidden", + "hidden_field": "Hidden field", + "hidden_fields": "Hidden fields", + "hide": "Hide", + "hide_column": "Hide column", + "image": "Image", + "images": "Images", + "import": "Import", + "impressions": "Impressions", + "imprint": "Imprint", + "in_progress": "In Progress", + "inactive_surveys": "Inactive surveys", + "input_type": "Input type", + "insights": "Insights", + "integration": "integration", + "integrations": "Integrations", + "invalid_date": "Invalid date", + "invalid_file_type": "Invalid file type", + "invite": "Invite", + "invite_them": "Invite them", + "key": "Key", + "label": "Label", + "language": "Language", + "learn_more": "Learn more", + "license": "License", + "light_overlay": "Light overlay", + "limits_reached": "Limits Reached", + "link": "Link", + "link_and_email": "Link & Email", + "link_copied": "Link copied to clipboard!", + "link_survey": "Link Survey", + "link_surveys": "Link Surveys", + "load_more": "Load more", + "loading": "Loading", + "logo": "Logo", + "logout": "Logout", + "look_and_feel": "Look & Feel", + "manage": "Manage", + "marketing": "Marketing", + "maximum": "Maximum", + "member": "Member", + "members": "Members", + "membership_not_found": "Membership not found", + "metadata": "Metadata", + "minimum": "Minimum", + "mobile_overlay_text": "Formbricks is not available for devices with smaller resolutions.", + "move_down": "Move down", + "move_up": "Move up", + "multiple_languages": "Multiple languages", + "name": "Name", + "negative": "Negative", + "neutral": "Neutral", + "new": "New", + "new_survey": "New Survey", + "new_version_available": "Formbricks {version} is here. Upgrade now!", + "next": "Next", + "no_background_image_found": "No background image found.", + "no_code": "No code", + "no_files_uploaded": "No files were uploaded", + "no_result_found": "No result found", + "no_results": "No results", + "no_surveys_found": "No surveys found.", + "not_authenticated": "You are not authenticated to perform this action.", + "not_authorized": "Not authorized", + "not_connected": "Not Connected", + "note": "Note", + "notes": "Notes", + "notifications": "Notifications", + "number": "Number", + "off": "Off", + "on": "On", + "only_one_file_allowed": "Only one file is allowed", + "only_owners_managers_and_manage_access_members_can_perform_this_action": "Only owners and managers can perform this action.", + "or": "or", + "organization": "Organization", + "organization_id": "Organization ID", + "organization_not_found": "Organization not found", + "organization_teams_not_found": "Organization teams not found", + "other": "Other", + "others": "Others", + "overview": "Overview", + "password": "Password", + "paused": "Paused", + "pending_downgrade": "Pending Downgrade", + "people_manager": "People Manager", + "person": "Person", + "phone": "Phone", + "photo_by": "Photo by", + "pick_a_date": "Pick a date", + "placeholder": "Placeholder", + "please_select_at_least_one_survey": "Please select at least one survey", + "please_select_at_least_one_trigger": "Please select at least one trigger", + "please_upgrade_your_plan": "Please upgrade your plan.", + "positive": "Positive", + "preview": "Preview", + "preview_survey": "Preview Survey", + "privacy": "Privacy Policy", + "privacy_policy": "Privacy Policy", + "product_manager": "Product Manager", + "profile": "Profile", + "project": "Project", + "project_configuration": "Project's Configuration", + "project_id": "Project ID", + "project_name": "Project Name", + "project_not_found": "Project not found", + "project_permission_not_found": "Project permission not found", + "projects": "Projects", + "projects_limit_reached": "Projects limit reached", + "question": "Question", + "question_id": "Question ID", + "questions": "Questions", + "read_docs": "Read Docs", + "remove": "Remove", + "reorder_and_hide_columns": "Reorder and hide columns", + "report_survey": "Report Survey", + "request_trial_license": "Request trial license", + "reset_to_default": "Reset to default", + "response": "Response", + "responses": "Responses", + "restart": "Restart", + "role": "Role", + "role_organization": "Role (Organization)", + "saas": "SaaS", + "sales": "Sales", + "save": "Save", + "save_changes": "Save changes", + "scheduled": "Scheduled", + "search": "Search", + "security": "Security", + "segment": "Segment", + "segments": "Segments", + "select": "Select", + "select_all": "Select all", + "select_survey": "Select Survey", + "selected": "Selected", + "selected_questions": "Selected questions", + "selection": "Selection", + "selections": "Selections", + "send": "Send", + "send_test_email": "Send test email", + "session_not_found": "Session not found", + "settings": "Settings", + "share_feedback": "Share feedback", + "show": "Show", + "show_response_count": "Show response count", + "shown": "Shown", + "size": "Size", + "skipped": "Skipped", + "skips": "Skips", + "some_files_failed_to_upload": "Some files failed to upload", + "something_went_wrong_please_try_again": "Something went wrong. Please try again.", + "sort_by": "Sort by", + "start_free_trial": "Start Free Trial", + "status": "Status", + "step_by_step_manual": "Step by step manual", + "styling": "Styling", + "submit": "Submit", + "summary": "Summary", + "survey": "Survey", + "survey_completed": "Survey completed.", + "survey_id": "Survey ID", + "survey_languages": "Survey Languages", + "survey_live": "Survey live", + "survey_not_found": "Survey not found", + "survey_paused": "Survey paused.", + "survey_scheduled": "Survey scheduled.", + "survey_type": "Survey Type", + "surveys": "Surveys", + "switch_organization": "Switch organization", + "switch_to": "Switch to {environment}", + "table_items_deleted_successfully": "{type}s deleted successfully", + "table_settings": "Table settings", + "tags": "Tags", + "targeting": "Targeting", + "team": "Team", + "team_access": "Team Access", + "team_name": "Team name", + "teams": "Access Control", + "teams_not_found": "Teams not found", + "text": "Text", + "time": "Time", + "time_to_finish": "Time to finish", + "title": "Title", + "top_left": "Top Left", + "top_right": "Top Right", + "try_again": "Try again", + "type": "Type", + "unlock_more_projects_with_a_higher_plan": "Unlock more projects with a higher plan.", + "update": "Update", + "updated": "Updated", + "updated_at": "Updated at", + "upload": "Upload", + "upload_input_description": "Click or drag to upload files.", + "url": "URL", + "user": "User", + "user_id": "User ID", + "user_not_found": "User not found", + "variable": "Variable", + "variables": "Variables", + "verified_email": "Verified Email", + "video": "Video", + "warning": "Warning", + "we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "We were unable to verify your license because the license server is unreachable.", + "webhook": "Webhook", + "webhooks": "Webhooks", + "website_and_app_connection": "Website & App Connection", + "website_app_survey": "Website & App Survey", + "website_survey": "Website Survey", + "weekly_summary": "Weekly summary", + "welcome_card": "Welcome card", + "yes": "Yes", + "you": "You", + "you_are_downgraded_to_the_community_edition": "You are downgraded to the Community Edition.", + "you_are_not_authorised_to_perform_this_action": "You are not authorised to perform this action.", + "you_have_reached_your_limit_of_project_limit": "You have reached your limit of {projectLimit} projects.", + "you_have_reached_your_monthly_miu_limit_of": "You have reached your monthly MIU limit of", + "you_have_reached_your_monthly_response_limit_of": "You have reached your monthly response limit of", + "you_will_be_downgraded_to_the_community_edition_on_date": "You will be downgraded to the Community Edition on {date}." + }, + "emails": { + "accept": "Accept", + "click_or_drag_to_upload_files": "Click or drag to upload files.", + "email_customization_preview_email_heading": "Hey {userName}", + "email_customization_preview_email_subject": "Formbricks Email Customization Preview", + "email_customization_preview_email_text": "This is an email preview to show you which logo will be rendered in the emails.", + "email_footer_text_1": "Have a great day!", + "email_footer_text_2": "The Formbricks Team", + "email_template_text_1": "This email was sent via Formbricks.", + "embed_survey_preview_email_didnt_request": "Didn't request this?", + "embed_survey_preview_email_environment_id": "Environment ID", + "embed_survey_preview_email_fight_spam": "Help us fight spam and forward this mail to hola@formbricks.com", + "embed_survey_preview_email_heading": "Preview Email Embed", + "embed_survey_preview_email_subject": "Formbricks Email Survey Preview", + "embed_survey_preview_email_text": "This is how the code snippet looks embedded into an email:", + "forgot_password_email_change_password": "Change password", + "forgot_password_email_did_not_request": "If you didn't request this, please ignore this email.", + "forgot_password_email_heading": "Change password", + "forgot_password_email_link_valid_for_24_hours": "The link is valid for 24 hours.", + "forgot_password_email_subject": "Reset your Formbricks password", + "forgot_password_email_text": "You have requested a link to change your password. You can do this by clicking the link below:", + "imprint": "Imprint", + "invite_accepted_email_heading": "Hey", + "invite_accepted_email_subject": "You've got a new organization member!", + "invite_accepted_email_text_par1": "Just letting you know that", + "invite_accepted_email_text_par2": "accepted your invitation. Have fun collaborating!", + "invite_email_button_label": "Join organization", + "invite_email_heading": "Hey", + "invite_email_text_par1": "Your colleague", + "invite_email_text_par2": "invited you to join them at Formbricks. To accept the invitation, please click the link below:", + "invite_member_email_subject": "You're invited to collaborate on Formbricks!", + "live_survey_notification_completed": "Completed", + "live_survey_notification_draft": "Draft", + "live_survey_notification_in_progress": "In Progress", + "live_survey_notification_no_new_response": "No new response received this week \uD83D\uDD75️", + "live_survey_notification_no_responses_yet": "No Responses yet!", + "live_survey_notification_paused": "Paused", + "live_survey_notification_scheduled": "Scheduled", + "live_survey_notification_view_more_responses": "View {responseCount} more Responses", + "live_survey_notification_view_previous_responses": "View previous responses", + "live_survey_notification_view_response": "View Response", + "notification_footer_all_the_best": "All the best,", + "notification_footer_in_your_settings": "in your settings \uD83D\uDE4F", + "notification_footer_please_turn_them_off": "please turn them off", + "notification_footer_the_formbricks_team": "The Formbricks Team \uD83E\uDD0D", + "notification_footer_to_halt_weekly_updates": "To halt Weekly Updates,", + "notification_header_hey": "Hey \uD83D\uDC4B", + "notification_header_weekly_report_for": "Weekly Report for", + "notification_insight_completed": "Completed", + "notification_insight_completion_rate": "Completion %", + "notification_insight_displays": "Displays", + "notification_insight_responses": "Responses", + "notification_insight_surveys": "Surveys", + "onboarding_invite_email_button_label": "Join {inviterName}'s organization", + "onboarding_invite_email_connect_formbricks": "Connect Formbricks to your app or website via HTML Snippet or NPM in just a few minutes.", + "onboarding_invite_email_create_account": "Create an account to join {inviterName}'s organization.", + "onboarding_invite_email_done": "Done ✅", + "onboarding_invite_email_get_started_in_minutes": "Get Started in Minutes", + "onboarding_invite_email_heading": "Hey ", + "onboarding_invite_email_subject": "{inviterName} needs a hand setting up Formbricks. Can you help out?", + "password_changed_email_heading": "Password changed", + "password_changed_email_text": "Your password has been changed successfully.", + "password_reset_notify_email_subject": "Your Formbricks password has been changed", + "privacy_policy": "Privacy Policy", + "reject": "Reject", + "render_email_response_value_file_upload_response_link_not_included": "Link to uploaded file is not included for data privacy reasons", + "response_finished_email_subject": "A response for {surveyName} was completed ✅", + "response_finished_email_subject_with_email": "{personEmail} just completed your {surveyName} survey ✅", + "schedule_your_meeting": "Schedule your meeting", + "select_a_date": "Select a date", + "survey_response_finished_email_congrats": "Congrats, you received a new response to your survey! Someone just completed your survey: {surveyName}", + "survey_response_finished_email_dont_want_notifications": "Don't want to get these notifications?", + "survey_response_finished_email_hey": "Hey \uD83D\uDC4B", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Turn off notifications for all newly created forms", + "survey_response_finished_email_turn_off_notifications_for_this_form": "Turn off notifications for this form", + "survey_response_finished_email_view_more_responses": "View {responseCount} more responses", + "survey_response_finished_email_view_survey_summary": "View survey summary", + "verification_email_click_on_this_link": "You can also click on this link:", + "verification_email_heading": "Almost there!", + "verification_email_hey": "Hey \uD83D\uDC4B", + "verification_email_if_expired_request_new_token": "If it has expired please request a new token here:", + "verification_email_link_valid_for_24_hours": "The link is valid for 24 hours.", + "verification_email_request_new_verification": "Request new verification", + "verification_email_subject": "Please verify your email to use Formbricks", + "verification_email_survey_name": "Survey name", + "verification_email_take_survey": "Take survey", + "verification_email_text": "To start using Formbricks please verify your email below:", + "verification_email_thanks": "Thanks for validating your email!", + "verification_email_to_fill_survey": "To fill out the survey please click on the button below:", + "verification_email_verify_email": "Verify email", + "verified_link_survey_email_subject": "Your survey is ready to be filled out.", + "weekly_summary_create_reminder_notification_body_cal_slot": "Pick a 15-minute slot in our CEOs calendar", + "weekly_summary_create_reminder_notification_body_dont_let_a_week_pass": "Don't let a week pass without learning about your users:", + "weekly_summary_create_reminder_notification_body_need_help": "Need help finding the right survey for your product?", + "weekly_summary_create_reminder_notification_body_reply_email": "or reply to this email :)", + "weekly_summary_create_reminder_notification_body_setup_a_new_survey": "Setup a new survey", + "weekly_summary_create_reminder_notification_body_text": "We'd love to send you a Weekly Summary, but currently there are no surveys running for {projectName}.", + "weekly_summary_email_subject": "{projectName} User Insights - Last Week by Formbricks" + }, + "environments": { + "actions": { + "action_copied_successfully": "Action copied successfully", + "action_copy_failed": "Action copy failed", + "action_created_successfully": "Action created successfully", + "action_deleted_successfully": "Action deleted successfully", + "action_type": "Action Type", + "action_updated_successfully": "Action updated successfully", + "action_with_key_already_exists": "Action with key {key} already exists", + "action_with_name_already_exists": "Action with name {name} already exists", + "add_css_class_or_id": "Add CSS class or id", + "add_url": "Add URL", + "click": "Click", + "contains": "Contains", + "create_action": "Create action", + "css_selector": "CSS Selector", + "delete_action_text": "Are you sure you want to delete this action? This also removes this action as a trigger from all your surveys.", + "display_name": "Display name", + "does_not_contain": "Does not contain", + "does_not_exactly_match": "Does not exactly match", + "eg_clicked_download": "E.g. Clicked Download", + "eg_download_cta_click_on_home": "e.g. download_cta_click_on_home", + "eg_install_app": "E.g. Install App", + "eg_user_clicked_download_button": "E.g. User clicked Download Button", + "ends_with": "Ends with", + "enter_a_url_to_see_if_a_user_visiting_it_would_be_tracked": "Enter a URL to see if a user visiting it would be tracked.", + "exactly_matches": "Exactly matches", + "exit_intent": "Exit Intent", + "fifty_percent_scroll": "50% Scroll", + "how_do_code_actions_work": "How do Code Actions work?", + "if_a_user_clicks_a_button_with_a_specific_css_class_or_id": "If a user clicks a button with a specific CSS class or id", + "if_a_user_clicks_a_button_with_a_specific_text": "If a user clicks a button with a specific text", + "in_your_code_read_more_in_our": "in your code. Read more in our", + "inner_text": "Inner Text", + "invalid_css_selector": "Invalid CSS Selector", + "limit_the_pages_on_which_this_action_gets_captured": "Limit the pages on which this action gets captured", + "limit_to_specific_pages": "Limit to specific pages", + "on_all_pages": "On all pages", + "page_filter": "Page filter", + "page_view": "Page View", + "select_match_type": "Select match type", + "starts_with": "Starts with", + "test_match": "Test Match", + "test_your_url": "Test your URL", + "this_action_was_created_automatically_you_cannot_make_changes_to_it": "This action was created automatically. You cannot make changes to it.", + "this_action_will_be_triggered_when_the_page_is_loaded": "This action will be triggered when the page is loaded.", + "this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "This action will be triggered when the user scrolls 50% of the page.", + "this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "This action will be triggered when the user tries to leave the page.", + "this_is_a_code_action_please_make_changes_in_your_code_base": "This is a code action. Please make changes in your code base.", + "track_new_user_action": "Track New User Action", + "track_user_action_to_display_surveys_or_create_user_segment": "Track user action to display surveys or create user segment.", + "url": "URL", + "user_actions": "User Actions", + "user_clicked_download_button": "User clicked Download Button", + "what_did_your_user_do": "What did your user do?", + "what_is_the_user_doing": "What is the user doing?", + "you_can_track_code_action_anywhere_in_your_app_using": "You can track code action anywhere in your app using" + }, + "connect": { + "congrats": "Congrats!", + "connection_successful_message": "Well done! We're connected.", + "do_it_later": "I'll do it later", + "finish_onboarding": "Finish Onboarding", + "headline": "Connect your app or website", + "import_formbricks_and_initialize_the_widget_in_your_component": "Import Formbricks and initialize the widget in your Component (e.g. App.tsx):", + "insert_this_code_into_the_head_tag_of_your_website": "Insert this code into the head tag of your website:", + "subtitle": "It takes less than 4 minutes.", + "waiting_for_your_signal": "Waiting for your signal..." + }, + "contacts": { + "contact_deleted_successfully": "Contact deleted successfully", + "contact_not_found": "No such contact found", + "contacts_table_refresh": "Refresh contacts", + "contacts_table_refresh_error": "Something went wrong while refreshing contacts, please try again", + "contacts_table_refresh_success": "Contacts refreshed successfully", + "first_name": "First Name", + "last_name": "Last Name", + "no_responses_found": "No responses found", + "not_provided": "Not provided", + "search_contact": "Search contact", + "select_attribute": "Select Attribute", + "unlock_contacts_description": "Manage contacts and send out targeted surveys", + "unlock_contacts_title": "Unlock contacts with a higher plan", + "upload_contacts_modal_attributes_description": "Map the columns in your CSV to the attributes in Formbricks.", + "upload_contacts_modal_attributes_new": "New attribute", + "upload_contacts_modal_attributes_search_or_add": "Search or add attribute", + "upload_contacts_modal_attributes_should_be_mapped_to": "should be mapped to", + "upload_contacts_modal_attributes_title": "Attributes", + "upload_contacts_modal_description": "Upload a CSV to quickly import contacts with attributes", + "upload_contacts_modal_download_example_csv": "Download example CSV", + "upload_contacts_modal_duplicates_description": "How should we handle if a contact already exists in your contacts?", + "upload_contacts_modal_duplicates_overwrite_description": "Overwrites the existing contacts", + "upload_contacts_modal_duplicates_overwrite_title": "Overwrite", + "upload_contacts_modal_duplicates_skip_description": "Skips the duplicate contacts", + "upload_contacts_modal_duplicates_skip_title": "Skip", + "upload_contacts_modal_duplicates_title": "Duplicates", + "upload_contacts_modal_duplicates_update_description": "Updates the existing contacts", + "upload_contacts_modal_duplicates_update_title": "Update", + "upload_contacts_modal_pick_different_file": "Pick a different file", + "upload_contacts_modal_preview": "Here's a preview of your data.", + "upload_contacts_modal_upload_btn": "Upload contacts" + }, + "experience": { + "all": "All", + "all_time": "All time", + "analysed_feedbacks": "Analysed Free Text Answers", + "category": "Category", + "category_updated_successfully": "Category updated successfully!", + "complaint": "Complaint", + "did_you_find_this_insight_helpful": "Did you find this insight helpful?", + "failed_to_update_category": "Failed to update category", + "feature_request": "Request", + "good_afternoon": "\uD83C\uDF24️ Good afternoon", + "good_evening": "\uD83C\uDF19 Good evening", + "good_morning": "☀️ Good morning", + "insights_description": "All insights generated from responses across all your surveys", + "insights_for_project": "Insights for {projectName}", + "new_responses": "Responses", + "no_insights_for_this_filter": "No insights for this filter", + "no_insights_found": "No insights found. Collect more survey responses or enable insights for your existing surveys to get started.", + "praise": "Praise", + "sentiment_score": "Sentiment Score", + "templates_card_description": "Choose a template or start from scratch", + "templates_card_title": "Measure your customer experience", + "this_month": "This month", + "this_quarter": "This quarter", + "this_week": "This week", + "today": "Today" + }, + "formbricks_logo": "Formbricks Logo", + "integrations": { + "activepieces_integration_description": "Instantly connect Formbricks with popular apps to automate tasks without coding.", + "additional_settings": "Additional Settings", + "airtable": { + "airtable_base": "Airtable base", + "airtable_integration": "Airtable Integration", + "airtable_integration_description": "Sync responses directly with Airtable.", + "airtable_integration_is_not_configured": "Airtable Integration is not configured", + "connect_with_airtable": "Connect with Airtable", + "link_airtable_table": "Link Airtable Table", + "link_new_table": "Link new table", + "no_bases_found": "No Airtable bases found", + "no_integrations_yet": "Your airtable integrations will appear here as soon as you add them. ⏲️", + "please_create_a_base": "Please create a base on Airtable", + "please_select_a_base": "Please select a base", + "please_select_a_table": "Please select a table", + "sync_responses_with_airtable": "Sync responses with an Airtable", + "table_name": "Table Name" + }, + "airtable_integration_description": "Instantly populate your Airtable table with survey data", + "connected_with_email": "Connected with {email}", + "connecting_integration_failed_please_try_again": "Connecting integration failed. Please try again!", + "create_survey_warning": "You have to create a survey to be able to setup this integration", + "delete_integration": "Delete Integration", + "delete_integration_confirmation": "Are you sure you want to delete this integration?", + "google_sheet_integration_description": "Instantly populate your spreadsheets with survey data", + "google_sheets": { + "connect_with_google_sheets": "Connect with Google Sheets", + "enter_a_valid_spreadsheet_url_error": "Please enter a valid spreadsheet url", + "google_connection": "Google Connection", + "google_connection_deletion_description": "Sync responses directly with Google Sheets.", + "google_sheet_integration_is_not_configured": "Google Sheet Integration is not configured in your instance of Formbricks.", + "google_sheet_logo": "Google Sheet logo", + "google_sheet_name": "Google Sheet Name", + "google_sheets_integration": "Google Sheets Integration", + "google_sheets_integration_description": "Sync responses directly with Google Sheets.", + "link_google_sheet": "Link Google Sheet", + "link_new_sheet": "Link new Sheet", + "no_integrations_yet": "Your google sheet integrations will appear here as soon as you add them. ⏲️", + "spreadsheet_url": "Spreadsheet URL" + }, + "include_created_at": "Include Created At", + "include_hidden_fields": "Include Hidden Fields", + "include_metadata": "Include Metadata (Browser, Country, etc.)", + "include_variables": "Include Variables", + "integration_added_successfully": "Integration added successfully", + "integration_removed_successfully": "Integration removed successfully", + "integration_updated_successfully": "Integration updated successfully", + "make_integration_description": "Integrate Formbricks with 1000+ apps via Make", + "manage_webhooks": "Manage Webhooks", + "n8n_integration_description": "Integrate Formbricks with 350+ apps via n8n", + "notion": { + "col_name_of_type_is_not_supported": "{col_name} of type {type} is not supported by notion API. The data won't be reflected in your notion database.", + "connect_with_notion": "Connect with Notion", + "connected_with_workspace": "Connected with {workspace} workspace", + "create_at_least_one_database_to_setup_this_integration": "You have to create at least one database to be able to setup this integration", + "database_name": "Database Name", + "duplicate_connection_warning": "A connection with this database is live. Please make changes with caution.", + "link_database": "Link Database", + "link_new_database": "Link new database", + "link_notion_database": "Link Notion Database", + "map_formbricks_fields_to_notion_property": "Map Formbricks fields to Notion property", + "no_databases_found": "Your Notion integrations will appear here as soon as you add them. ⏲️", + "notion_integration": "Notion Integration", + "notion_integration_description": "Send responses directly to Notion.", + "notion_integration_is_not_configured": "Notion Integration is not configured in your instance of Formbricks.", + "notion_logo": "Notion logo", + "please_complete_mapping_fields_with_notion_property": "Please complete mapping fields with Notion property", + "please_resolve_mapping_errors": "Please resolve the mapping errors", + "please_select_a_database": "Please select a database", + "please_select_at_least_one_mapping": "Please select at least one mapping", + "que_name_of_type_cant_be_mapped_to": "{que_name} of type {question_label} can't be mapped to the column {col_name} of type {col_type}. Instead use column of type {mapped_type}.", + "select_a_database": "Select Database", + "select_a_field_to_map": "Select a field to map", + "select_a_survey_question": "Select a survey question", + "sync_responses_with_a_notion_database": "Sync responses with a Notion Database", + "update_connection": "Reconnect Notion", + "update_connection_tooltip": "Reconnect the integration to include newly added databases. Your existing integrations will remain intact." + }, + "notion_integration_description": "Send data to your Notion database", + "please_select_a_survey_error": "Please select a survey", + "select_at_least_one_question_error": "Please select at least one question", + "slack": { + "already_connected_another_survey": "You have already connected another survey to this channel.", + "channel_name": "Channel Name", + "connect_with_slack": "Connect with Slack", + "connect_your_first_slack_channel": "Connect your first Slack channel to get started.", + "connected_with_team": "Connected with {team}", + "create_at_least_one_channel_error": "You have to create at least one channel to be able to setup this integration", + "dont_see_your_channel": "Don't see your channel?", + "link_channel": "Link channel", + "link_slack_channel": "Link Slack Channel", + "please_select_a_channel": "Please select a channel", + "select_channel": "Select Channel", + "slack_integration": "Slack Integration", + "slack_integration_description": "Send responses directly to Slack.", + "slack_integration_is_not_configured": "Slack Integration is not configured in your instance of Formbricks.", + "slack_reconnect_button": "Reconnect", + "slack_reconnect_button_description": "Note: We recently changed our Slack integration to also support private channels. Please reconnect your Slack workspace." + }, + "slack_integration_description": "Instantly connect your Slack Workspace with Formbricks", + "to_configure_it": "to configure it.", + "webhook_integration_description": "Trigger Webhooks based on actions in your surveys", + "webhooks": { + "add_webhook": "Add Webhook", + "add_webhook_description": "Send survey response data to a custom endpoint", + "all_current_and_new_surveys": "All current and new surveys", + "created_by_third_party": "Created by a Third Party", + "discord_webhook_not_supported": "Discord webhooks are currently not supported.", + "empty_webhook_message": "Your webhooks will appear here as soon as you add them. ⏲️", + "endpoint_pinged": "Yay! We are able to ping the webhook!", + "endpoint_pinged_error": "Unable to ping the webhook!", + "please_check_console": "Please check the console for more details", + "please_enter_a_url": "Please enter a URL", + "response_created": "Response Created", + "response_finished": "Response Finished", + "response_updated": "Response Updated", + "source": "Source", + "test_endpoint": "Test Endpoint", + "triggers": "Triggers", + "webhook_added_successfully": "Webhook added successfully", + "webhook_delete_confirmation": "Are you sure you want to delete this Webhook? This will stop sending you any further notifications.", + "webhook_deleted_successfully": "Webhook deleted successfully", + "webhook_name_placeholder": "Optional: Label your webhook for easy identification", + "webhook_test_failed_due_to": "Webhook Test Failed due to", + "webhook_updated_successfully": "Webhook updated successfully.", + "webhook_url_placeholder": "Paste the URL you want the event to trigger on" + }, + "website_or_app_integration_description": "Integrate Formbricks into your Website or App", + "zapier_integration_description": "Integrate Formbricks with 5000+ apps via Zapier" + }, + "project": { + "api_keys": { + "add_api_key": "Add API Key", + "api_key": "API Key", + "api_key_copied_to_clipboard": "API key copied to clipboard", + "api_key_created": "API key created", + "api_key_deleted": "API Key deleted", + "api_key_label": "API Key Label", + "api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.", + "api_key_updated": "API Key updated", + "duplicate_access": "Duplicate project access not allowed", + "no_api_keys_yet": "You don't have any API keys yet", + "no_env_permissions_found": "No environment permissions found", + "organization_access": "Organization Access", + "permissions": "Permissions", + "project_access": "Project Access", + "secret": "Secret", + "unable_to_delete_api_key": "Unable to delete API Key" + }, + "app-connection": { + "api_host_description": "This is the URL of your Formbricks backend.", + "app_connection": "App Connection", + "app_connection_description": "Connect your app to Formbricks.", + "check_out_the_docs": "Check out the docs.", + "dive_into_the_docs": "Dive into the docs.", + "does_your_widget_work": "Does your widget work?", + "environment_id": "Your EnvironmentId", + "environment_id_description": "This id uniquely identifies this Formbricks environment.", + "environment_id_description_with_environment_id": "Used to identify the correct environment: {environmentId} is yours.", + "formbricks_sdk": "Formbricks SDK", + "formbricks_sdk_connected": "Formbricks SDK is connected", + "formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.", + "formbricks_sdk_not_connected_description": "Connect your website or app with Formbricks", + "have_a_problem": "Have a problem?", + "how_to_setup": "How to setup", + "how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.", + "identifying_your_users": "identifying your users", + "if_you_are_planning_to": "If you are planning to", + "insert_this_code_into_the": "Insert this code into the", + "need_a_more_detailed_setup_guide_for": "Need a more detailed setup guide for", + "not_working": "Not working?", + "open_an_issue_on_github": "Open an issue on GitHub", + "open_the_browser_console_to_see_the_logs": "Open the browser console to see the logs.", + "receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A", + "recheck": "Re-check", + "scroll_to_the_top": "Scroll to the top!", + "step_1": "Step 1: Install with pnpm, npm or yarn", + "step_2": "Step 2: Initialize widget", + "step_2_description": "Import Formbricks and initialize the widget in your Component (e.g. App.tsx):", + "step_3": "Step 3: Debug mode", + "switch_on_the_debug_mode_by_appending": "Switch on the debug mode by appending", + "tag_of_your_app": "tag of your app", + "to_the_url_where_you_load_the": "to the URL where you load the", + "want_to_learn_how_to_add_user_attributes": "Want to learn how to add user attributes, custom events and more?", + "you_are_done": "You're done \uD83C\uDF89", + "you_can_set_the_user_id_with": "you can set the user id with", + "your_app_now_communicates_with_formbricks": "Your app now communicates with Formbricks - sending events, and loading surveys automatically!" + }, + "general": { + "cannot_delete_only_project": "This is your only project, it cannot be deleted. Create a new project first.", + "delete_project": "Delete Project", + "delete_project_confirmation": "Are you sure you want to delete {projectName}? This action cannot be undone.", + "delete_project_name_includes_surveys_responses_people_and_more": "Delete {projectName} incl. all surveys, responses, people, actions and attributes.", + "delete_project_settings_description": "Delete project with all surveys, responses, people, actions and attributes. This cannot be undone.", + "error_saving_project_information": "Error saving project information", + "only_owners_or_managers_can_delete_projects": "Only owners or managers can delete projects", + "project_deleted_successfully": "Project deleted successfully", + "project_name_settings_description": "Change your projects name.", + "project_name_updated_successfully": "Project name updated successfully", + "recontact_waiting_time": "Recontact Waiting Time", + "recontact_waiting_time_settings_description": "Control how frequently users can be surveyed across all app surveys.", + "this_action_cannot_be_undone": "This action cannot be undone.", + "wait_x_days_before_showing_next_survey": "Wait X days before showing next survey:", + "waiting_period_updated_successfully": "Waiting period updated successfully", + "whats_your_project_called": "What's your project called?" + }, + "languages": { + "add_language": "Add language", + "alias": "Alias", + "alias_tooltip": "The alias is an alternate name to identify the language in link surveys and the SDK (optional)", + "cannot_remove_language_warning": "You cannot remove this language since it’s still used in these surveys:", + "conflict_between_identifier_and_alias": "There is a conflict between the identifier of an added language and one for your aliases. Aliases and identifiers cannot be identical.", + "conflict_between_selected_alias_and_another_language": "There is a conflict between the selected alias and another language that has this identifier. Please add the language with this identifier to your project instead to avoid inconsistencies.", + "delete_language_confirmation": "Are you sure you want to delete this language? This action cannot be undone.", + "duplicate_language_or_language_id": "Duplicate language or language ID", + "edit_languages": "Edit languages", + "identifier": "Identifier (ISO)", + "incomplete_translations": "Incomplete translations", + "language": "Language", + "language_deleted_successfully": "Language deleted successfully", + "languages_updated_successfully": "Languages updated successfully", + "multi_language_surveys": "Multi-Language Surveys", + "multi_language_surveys_description": "Add languages to create multi-language surveys.", + "no_language_found": "No language found. Add your first language below.", + "please_select_a_language": "Please select a language", + "remove_language": "Remove Language", + "remove_language_from_surveys_to_remove_it_from_project": "Please remove the language from these surveys in order to remove it from the project.", + "search_items": "Search items", + "translate": "Translate" + }, + "look": { + "add_background_color": "Add background color", + "add_background_color_description": "Add a background color to the logo container.", + "app_survey_placement": "App Survey Placement", + "app_survey_placement_settings_description": "Change where surveys will be shown in your web app or website.", + "centered_modal_overlay_color": "Centered modal overlay color", + "email_customization": "Email Customization", + "email_customization_description": "Change the look and feel of emails Formbricks sends out on your behalf.", + "enable_custom_styling": "Enable custom styling", + "enable_custom_styling_description": "Allow users to override this theme in the survey editor.", + "failed_to_remove_logo": "Failed to remove the logo", + "failed_to_update_logo": "Failed to update the logo", + "formbricks_branding": "Formbricks Branding", + "formbricks_branding_hidden": "Formbricks branding is hidden.", + "formbricks_branding_settings_description": "We love your support but understand if you toggle it off.", + "formbricks_branding_shown": "Formbricks branding is shown.", + "logo_removed_successfully": "Logo removed successfully", + "logo_settings_description": "Upload your company logo to brand surveys and link previews.", + "logo_updated_successfully": "Logo updated successfully", + "logo_upload_failed": "Logo upload failed. Please try again.", + "placement_updated_successfully": "Placement updated successfully", + "remove_branding_with_a_higher_plan": "Remove branding with a higher plan", + "remove_logo": "Remove Logo", + "remove_logo_confirmation": "Are you sure you want to remove the logo?", + "replace_logo": "Replace Logo", + "reset_styling": "Reset styling", + "reset_styling_confirmation": "Are you sure you want to reset the styling to default?", + "show_formbricks_branding_in": "Show Formbricks Branding in {type} surveys", + "show_powered_by_formbricks": "Show 'Powered by Formbricks' Signature", + "styling_updated_successfully": "Styling updated successfully", + "theme": "Theme", + "theme_settings_description": "Create a style theme for all surveys. You can enable custom styling for each survey." + }, + "tags": { + "add": "Add", + "add_tag": "Add Tag", + "count": "Count", + "delete_tag_confirmation": "Are you sure you want to delete this tag?", + "empty_message": "Tag a submission to find your list of tags here.", + "manage_tags": "Manage Tags", + "manage_tags_description": "Merge and remove response tags.", + "merge": "Merge", + "no_tag_found": "No tag found", + "search_tags": "Search Tags...", + "tag": "Tag", + "tag_already_exists": "Tag already exists", + "tag_deleted": "Tag deleted", + "tag_updated": "Tag updated", + "tags_merged": "Tags merged", + "unique_constraint_failed_on_the_fields": "Unique constraint failed on the fields" + }, + "teams": { + "manage_teams": "Manage teams", + "no_teams_found": "No teams found", + "only_organization_owners_and_managers_can_manage_teams": "Only organization owners and managers can manage teams.", + "permission": "Permission", + "team_name": "Team Name", + "team_settings_description": "See which teams can access this project." + } + }, + "projects_environments_organizations_not_found": "Projects, environments or organizations not found", + "segments": { + "add_filter_below": "Add filter below", + "add_your_first_filter_to_get_started": "Add your first filter to get started", + "cannot_delete_segment_used_in_surveys": "You cannot delete this segment since it’s still used in these surveys:", + "clone_and_edit_segment": "Clone & Edit Segment", + "create_group": "Create group", + "create_your_first_segment": "Create your first Segment to get started", + "delete_segment": "Delete Segment", + "desktop": "Desktop", + "devices": "Devices", + "edit_segment": "Edit Segment", + "error_resetting_filters": "Error resetting filters", + "error_saving_segment": "Error saving segment", + "ex_fully_activated_recurring_users": "Ex. Fully activated recurring users", + "ex_power_users": "Ex. Power Users", + "filters_reset_successfully": "Filters reset successfully", + "here": "here", + "hide_filters": "Hide filters", + "identifying_users": "identifying users", + "invalid_segment": "Invalid segment", + "invalid_segment_filters": "Invalid filters. Please check the filters and try again.", + "load_segment": "Load Segment", + "most_active_users_in_the_last_30_days": "Most active users in the last 30 days", + "no_attributes_yet": "No attributes yet!", + "no_filters_yet": "There are no filters yet!", + "no_segments_yet": "You currently have no saved segments.", + "person_and_attributes": "Person & Attributes", + "phone": "Phone", + "please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Please remove the segment from these surveys in order to delete it.", + "pre_segment_users": "Pre-segment your users with attributes filters.", + "remove_all_filters": "Remove all filters", + "reset_all_filters": "Reset all filters", + "save_as_new_segment": "Save as new segment", + "save_your_filters_as_a_segment_to_use_it_in_other_surveys": "Save your filters as a Segment to use it in other surveys", + "segment_created_successfully": "Segment created successfully!", + "segment_deleted_successfully": "Segment deleted successfully!", + "segment_id": "Segment ID", + "segment_saved_successfully": "Segment saved successfully", + "segment_updated_successfully": "Segment updated successfully!", + "segments_help_you_target_users_with_same_characteristics_easily": "Segments help you target users with the same characteristics easily", + "target_audience": "Target Audience", + "this_action_resets_all_filters_in_this_survey": "This action resets all filters in this survey.", + "this_segment_is_used_in_other_surveys": "This segment is used in other surveys. Make changes", + "title_is_required": "Title is required.", + "unknown_filter_type": "Unknown filter type", + "unlock_segments_description": "Organize contacts into segments to target specific user groups", + "unlock_segments_title": "Unlock segments with a higher plan", + "user_targeting_is_currently_only_available_when": "User targeting is currently only available when", + "value_cannot_be_empty": "Value cannot be empty.", + "value_must_be_a_number": "Value must be a number.", + "view_filters": "View filters", + "where": "Where", + "with_the_formbricks_sdk": "with the Formbricks SDK" + }, + "settings": { + "api_keys": { + "add_api_key": "Add API key", + "add_permission": "Add permission", + "api_keys_description": "Manage API keys to access Formbricks management APIs" + }, + "billing": { + "10000_monthly_responses": "10000 Monthly Responses", + "1500_monthly_responses": "1500 Monthly Responses", + "2000_monthly_identified_users": "2000 Monthly Identified Users", + "30000_monthly_identified_users": "30000 Monthly Identified Users", + "3_projects": "3 Projects", + "5000_monthly_responses": "5000 Monthly Responses", + "5_projects": "5 Projects", + "7500_monthly_identified_users": "7500 Monthly Identified Users", + "advanced_targeting": "Advanced Targeting", + "all_integrations": "All Integrations", + "all_surveying_features": "All surveying features", + "annually": "Annually", + "api_webhooks": "API & Webhooks", + "app_surveys": "App Surveys", + "contact_us": "Contact Us", + "current": "Current", + "current_plan": "Current Plan", + "current_tier_limit": "Current Tier Limit", + "custom_miu_limit": "Custom MIU limit", + "custom_project_limit": "Custom Project Limit", + "customer_success_manager": "Customer Success Manager", + "email_embedded_surveys": "Email Embedded Surveys", + "email_support": "Email Support", + "enterprise": "Enterprise", + "enterprise_description": "Premium support and custom limits.", + "everybody_has_the_free_plan_by_default": "Everybody has the free plan by default!", + "everything_in_free": "Everything in Free", + "everything_in_scale": "Everything in Scale", + "everything_in_startup": "Everything in Startup", + "free": "Free", + "free_description": "Unlimited Surveys, Team Members, and more.", + "get_2_months_free": "Get 2 months free", + "get_in_touch": "Get in touch", + "link_surveys": "Link Surveys (Shareable)", + "logic_jumps_hidden_fields_recurring_surveys": "Logic Jumps, Hidden Fields, Recurring Surveys, etc.", + "manage_card_details": "Manage Card Details", + "manage_subscription": "Manage Subscription", + "monthly": "Monthly", + "monthly_identified_users": "Monthly Identified Users", + "multi_language_surveys": "Multi-Language Surveys", + "per_month": "per month", + "per_year": "per year", + "plan_upgraded_successfully": "Plan upgraded successfully", + "premium_support_with_slas": "Premium support with SLAs", + "priority_support": "Priority Support", + "remove_branding": "Remove Branding", + "say_hi": "Say Hi!", + "scale": "Scale", + "scale_description": "Advanced features for scaling your business.", + "startup": "Startup", + "startup_description": "Everything in Free with additional features.", + "switch_plan": "Switch Plan", + "switch_plan_confirmation_text": "Are you sure you want to switch to the {plan} plan? You will be charged {price} {period}.", + "team_access_roles": "Team Access Roles", + "technical_onboarding": "Technical Onboarding", + "unable_to_upgrade_plan": "Unable to upgrade plan", + "unlimited_apps_websites": "Unlimited Apps & Websites", + "unlimited_miu": "Unlimited MIU", + "unlimited_projects": "Unlimited Projects", + "unlimited_responses": "Unlimited Responses", + "unlimited_surveys": "Unlimited Surveys", + "unlimited_team_members": "Unlimited Team Members", + "upgrade": "Upgrade", + "uptime_sla_99": "Uptime SLA (99%)", + "website_surveys": "Website Surveys" + }, + "enterprise": { + "ai": "AI Analysis", + "audit_logs": "Audit Logs", + "coming_soon": "Coming soon", + "contacts_and_segments": "Contact management & segments", + "enterprise_features": "Enterprise Features", + "get_an_enterprise_license_to_get_access_to_all_features": "Get an Enterprise license to get access to all features.", + "keep_full_control_over_your_data_privacy_and_security": "Keep full control over your data privacy and security.", + "no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "No call needed, no strings attached: Request a free 30-day trial license to test all features by filling out this form:", + "no_credit_card_no_sales_call_just_test_it": "No credit card. No sales call. Just test it :)", + "on_request": "On request", + "organization_roles": "Organization Roles (Admin, Editor, Developer, etc.)", + "questions_please_reach_out_to": "Questions? Please reach out to", + "request_30_day_trial_license": "Request 30-day Trial License", + "saml_sso": "SAML SSO", + "service_level_agreement": "Service Level Agreement", + "soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 Compliance check", + "sso": "SSO (Google, Microsoft, OpenID Connect)", + "teams": "Teams & Access Roles (Read, Read & Write, Manage)", + "unlock_the_full_power_of_formbricks_free_for_30_days": "Unlock the full power of Formbricks. Free for 30 days.", + "your_enterprise_license_is_active_all_features_unlocked": "Your Enterprise License is active. All features unlocked." + }, + "general": { + "bulk_invite_warning_description": "On the free plan, all organization members are always assigned the \"Owner\" role.", + "cannot_delete_only_organization": "This is your only organization, it cannot be deleted. Create a new organization first.", + "cannot_leave_only_organization": "You cannot leave this organization as it is your only organization. Create a new organization first.", + "copy_invite_link_to_clipboard": "Copy invite link to clipboard", + "create_new_organization": "Create new organization", + "create_new_organization_description": "Create a new organization to handle a different set of projects.", + "customize_email_with_a_higher_plan": "Customize email with a higher plan", + "delete_organization": "Delete Organization", + "delete_organization_description": "Delete organization with all its projects including all surveys, responses, people, actions and attributes", + "delete_organization_warning": "Before you proceed with deleting this organization, please be aware of the following consequences:", + "delete_organization_warning_1": "Permanent removal of all projects linked to this organization.", + "delete_organization_warning_2": "This action cannot be undone. If it's gone, it's gone.", + "delete_organization_warning_3": "Please enter {organizationName} in the following field to confirm the definitive deletion of this organization:", + "eliminate_branding_with_whitelabel": "Eliminate Formbricks branding and enable additional white-label customization options.", + "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.", + "enable_formbricks_ai": "Enable Formbricks AI", + "error_deleting_organization_please_try_again": "Error deleting organization. Please try again.", + "formbricks_ai": "Formbricks AI", + "formbricks_ai_description": "Get personalised insights from your survey responses with Formbricks AI", + "formbricks_ai_disable_success_message": "Formbricks AI disabled successfully.", + "formbricks_ai_enable_success_message": "Formbricks AI enabled successfully.", + "formbricks_ai_privacy_policy_text": "By activating Formbricks AI, you agree to the updated", + "from_your_organization": "from your organization", + "invitation_sent_once_more": "Invitation sent once more.", + "invite_deleted_successfully": "Invite deleted successfully", + "invited_on": "Invited on {date}", + "invites_failed": "Invites failed", + "leave_organization": "Leave organization", + "leave_organization_description": "You wil leave this organization and loose access to all surveys and responses. You can only rejoin if you are invited again.", + "leave_organization_ok_btn_text": "Yes, leave organization", + "leave_organization_title": "Are you sure?", + "logo_in_email_header": "Logo in email header", + "logo_removed_successfully": "Logo removed successfully", + "logo_saved_successfully": "Logo saved successfully", + "manage_members": "Manage members", + "manage_members_description": "Add or remove members in your organization.", + "member_deleted_successfully": "Member deleted successfully", + "member_invited_successfully": "Member invited successfully", + "once_its_gone_its_gone": "Once it's gone, it's gone.", + "only_org_owner_can_perform_action": "Only organization owners can access this setting.", + "organization_created_successfully": "Organization created successfully!", + "organization_deleted_successfully": "Organization deleted successfully.", + "organization_invite_link_ready": "Your organization invite link is ready!", + "organization_name": "Organization Name", + "organization_name_description": "Give your organization a descriptive name.", + "organization_name_placeholder": "e.g. Power Puff Girls", + "organization_name_updated_successfully": "Organization name updated successfully", + "organization_settings": "Organization Settings", + "please_add_a_logo": "Please add a logo", + "please_check_csv_file": "Please check the CSV file and make sure it is according to our format", + "please_save_logo_before_sending_test_email": "Please save the logo before sending a test email.", + "remove_logo": "Remove logo", + "replace_logo": "Replace logo", + "resend_invitation_email": "Resend Invitation Email", + "share_invite_link": "Share Invite Link", + "share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:", + "test_email_sent_successfully": "Test email sent successfully", + "use_multi_language_surveys_with_a_higher_plan": "Use multi-language surveys with a higher plan", + "use_multi_language_surveys_with_a_higher_plan_description": "Survey your users in different languages." + }, + "notifications": { + "auto_subscribe_to_new_surveys": "Auto-subscribe to new surveys", + "email_alerts_surveys": "Email alerts (Surveys)", + "every_response": "Every response", + "every_response_tooltip": "Sends complete responses, no partials.", + "need_slack_or_discord_notifications": "Need Slack or Discord notifications", + "notification_settings_updated": "Notification settings updated", + "set_up_an_alert_to_get_an_email_on_new_responses": "Set up an alert to get an email on new responses", + "stay_up_to_date_with_a_Weekly_every_Monday": "Stay up-to-date with a Weekly every Monday", + "use_the_integration": "Use the integration", + "want_to_loop_in_organization_mates": "Want to loop in organization mates", + "weekly_summary_projects": "Weekly summary (Projects)", + "you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore": "You will not be auto-subscribed to this organization's surveys anymore!", + "you_will_not_receive_any_more_emails_for_responses_on_this_survey": "You will not receive any more emails for responses on this survey!" + }, + "profile": { + "account_deletion_consequences_warning": "Account deletion consequences", + "avatar_update_failed": "Avatar update failed. Please try again.", + "backup_code": "Backup Code", + "change_image": "Change image", + "confirm_delete_account": "Delete your account with all of your personal information and data", + "confirm_delete_my_account": "Delete My Account", + "confirm_your_current_password_to_get_started": "Confirm your current password to get started.", + "delete_account": "Delete Account", + "disable_two_factor_authentication": "Disable two factor authentication", + "disable_two_factor_authentication_description": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.", + "each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Each backup code can be used exactly once to grant access without your authenticator.", + "enable_two_factor_authentication": "Enable two factor authentication", + "enter_the_code_from_your_authenticator_app_below": "Enter the code from your authenticator app below.", + "file_size_must_be_less_than_10mb": "File size must be less than 10MB.", + "invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.", + "lost_access": "Lost access", + "or_enter_the_following_code_manually": "Or enter the following code manually:", + "organization_identification": "Assist your organization in identifying you on Formbricks", + "organizations_delete_message": "You are the only owner of these organizations, so they will be deleted as well.", + "permanent_removal_of_all_of_your_personal_information_and_data": "Permanent removal of all of your personal information and data", + "personal_information": "Personal information", + "please_enter_email_to_confirm_account_deletion": "Please enter {email} in the following field to confirm the definitive deletion of your account:", + "profile_updated_successfully": "Your profile was updated successfully", + "remove_image": "Remove image", + "save_the_following_backup_codes_in_a_safe_place": "Save the following backup codes in a safe place.", + "scan_the_qr_code_below_with_your_authenticator_app": "Scan the QR code below with your authenticator app.", + "security_description": "Manage your password and other security settings like two-factor authentication (2FA).", + "two_factor_authentication": "Two factor authentication", + "two_factor_authentication_description": "Add an extra layer of security to your account in case your password is stolen.", + "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Two-factor authentication enabled. Please enter the six-digit code from your authenticator app.", + "two_factor_code": "Two-Factor Code", + "unlock_two_factor_authentication": "Unlock two-factor authentication with a higher plan", + "update_personal_info": "Update your personal information", + "upload_image": "Upload image", + "warning_cannot_delete_account": "You are the only owner of this organization. Please transfer ownership to another member first.", + "warning_cannot_undo": "This cannot be undone", + "you_must_select_a_file": "You must select a file." + }, + "teams": { + "add_members_description": "Add members to the team and determine their role.", + "add_projects_description": "Control which projects the team members can access.", + "all_members_added": "All members added to this team.", + "all_projects_added": "All projects added to this team.", + "are_you_sure_you_want_to_delete_this_team": "Are you sure you want to delete this team? This also removes the access to all the projects and surveys associated with this team.", + "billing_role_description": "Only have access to billing info.", + "bulk_invite": "Bulk Invite", + "contributor": "Contributor", + "create": "Create", + "create_first_team_message": "You need to create a team first.", + "create_new_team": "Create new team", + "delete_team": "Delete team", + "empty_teams_state": "Create your first team.", + "enter_team_name": "Enter team name", + "individual": "Individual", + "invite_member": "Invite member", + "invite_member_description": "Add your co-workers to this organization.", + "manage": "Manage", + "manage_team": "Manage team", + "manage_team_disabled": "Only organization owners, managers and team admins can manage teams.", + "manager_role_description": "Managers can access all projects and add and remove members.", + "member_role_description": "Members can work in selected projects.", + "member_role_info_message": "To give new members access to a project, please add them to a Team below. With Teams you can manage who has access to which project.", + "owner_role_description": "Owners have full control over the organization.", + "please_fill_all_member_fields": "Please fill all the fields to add a new member.", + "please_fill_all_project_fields": "Please fill all the fields to add a new project.", + "read": "Read", + "read_write": "Read & Write", + "team_admin": "Team Admin", + "team_created_successfully": "Team created successfully.", + "team_deleted_successfully": "Team deleted successfully.", + "team_deletion_not_allowed": "You are not allowed to delete this team.", + "team_name": "Team Name", + "team_name_settings_title": "{teamName} Settings", + "team_select_placeholder": "Search team name...", + "team_settings_description": "Manage team members, access rights, and more.", + "team_updated_successfully": "Team updated successfully", + "teams": "Teams", + "teams_description": "Assign members into teams and give teams access to projects.", + "unlock_teams_description": "Manage which organization members have access to specific projects and surveys.", + "unlock_teams_title": "Unlock Teams with a higher plan.", + "upgrade_plan_notice_message": "Unlock Organization Roles with a higher plan.", + "you_are_a_member": "You're a member" + } + }, + "surveys": { + "all_set_time_to_create_first_survey": "You're all set! Time to create your first survey", + "alphabetical": "Alphabetical", + "copy_survey": "Copy survey", + "copy_survey_description": "Copy this survey to another environment", + "copy_survey_error": "Failed to copy survey", + "copy_survey_link_to_clipboard": "Copy survey link to clipboard", + "copy_survey_success": "Survey copied successfully!", + "delete_survey_and_responses_warning": "Are you sure you want to delete this survey and all of its responses? This action cannot be undone.", + "edit": { + "1_choose_the_default_language_for_this_survey": "1. Choose the default language for this survey:", + "2_activate_translation_for_specific_languages": "2. Activate translation for specific languages:", + "add": "Add +", + "add_a_delay_or_auto_close_the_survey": "Add a delay or auto-close the survey", + "add_a_four_digit_pin": "Add a four digit PIN", + "add_a_new_question_to_your_survey": "Add a new question to your survey", + "add_a_variable_to_calculate": "Add a variable to calculate", + "add_action_below": "Add action below", + "add_choice_below": "Add choice below", + "add_color_coding": "Add color coding", + "add_color_coding_description": "Add red, orange and green color codes to the options.", + "add_column": "Add column", + "add_condition_below": "Add condition below", + "add_custom_styles": "Add custom styles", + "add_delay_before_showing_survey": "Add delay before showing survey", + "add_description": "Add description", + "add_ending": "Add ending", + "add_ending_below": "Add ending below", + "add_hidden_field_id": "Add hidden field ID", + "add_highlight_border": "Add highlight border", + "add_highlight_border_description": "Add an outer border to your survey card.", + "add_logic": "Add logic", + "add_option": "Add option", + "add_other": "Add \"Other\"", + "add_photo_or_video": "Add photo or video", + "add_pin": "Add PIN", + "add_question": "Add question", + "add_question_below": "Add question below", + "add_row": "Add row", + "add_variable": "Add variable", + "address_fields": "Address Fields", + "address_line_1": "Address Line 1", + "address_line_2": "Address Line 2", + "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_other_answers_will_continue_to": "All other answers will continue to", + "allow_file_type": "Allow file type", + "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", + "always_show_survey": "Always show survey", + "and_launch_surveys_in_your_website_or_app": "and launch surveys in your website or app.", + "animation": "Animation", + "app_survey_description": "Embed a survey in your web app or website to collect responses.", + "assign": "Assign =", + "audience": "Audience", + "auto_close_on_inactivity": "Auto close on inactivity", + "automatically_close_survey_after": "Automatically close survey after", + "automatically_close_the_survey_after_a_certain_number_of_responses": "Automatically close the survey after a certain number of responses.", + "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.", + "automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Automatically closes the survey at the beginning of the day (UTC).", + "automatically_mark_the_survey_as_complete_after": "Automatically mark the survey as complete after", + "automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Automatically release the survey at the beginning of the day (UTC).", + "back_button_label": "\"Back\" Button Label", + "background_styling": "Background Styling", + "blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "Blocks survey if a submission with the Single Use Id (suId) exists already.", + "blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "Blocks survey if the survey URL has no Single Use Id (suId).", + "brand_color": "Brand color", + "brightness": "Brightness", + "button_label": "Button Label", + "button_to_continue_in_survey": "Button to continue in survey", + "button_to_link_to_external_url": "Button to link to external URL", + "button_url": "Button URL", + "cal_username": "Cal.com username or username/event", + "calculate": "Calculate", + "capture_a_new_action_to_trigger_a_survey_on": "Capture a new action to trigger a survey on.", + "capture_new_action": "Capture new action", + "card_arrangement_for_survey_type_derived": "Card Arrangement for {surveyTypeDerived} Surveys", + "card_background_color": "Card background color", + "card_border_color": "Card border color", + "card_shadow_color": "Card shadow color", + "card_styling": "Card Styling", + "casual": "Casual", + "caution_text": "Changes will lead to inconsistencies", + "centered_modal_overlay_color": "Centered modal overlay color", + "change_anyway": "Change anyway", + "change_background": "Change background", + "change_question_type": "Change question type", + "change_the_background_color_of_the_card": "Change the background color of the card.", + "change_the_background_color_of_the_input_fields": "Change the background color of the input fields.", + "change_the_background_to_a_color_image_or_animation": "Change the background to a color, image or animation.", + "change_the_border_color_of_the_card": "Change the border color of the card.", + "change_the_border_color_of_the_input_fields": "Change the border color of the input fields.", + "change_the_border_radius_of_the_card_and_the_inputs": "Change the border radius of the card and the inputs.", + "change_the_brand_color_of_the_survey": "Change the brand color of the survey.", + "change_the_placement_of_this_survey": "Change the placement of this survey.", + "change_the_question_color_of_the_survey": "Change the question color of the survey.", + "change_the_shadow_color_of_the_card": "Change the shadow color of the card.", + "changes_saved": "Changes saved.", + "character_limit_toggle_description": "Limit how short or long an answer can be.", + "character_limit_toggle_title": "Add character limits", + "checkbox_label": "Checkbox Label", + "choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.", + "choose_where_to_run_the_survey": "Choose where to run the survey.", + "city": "City", + "close_survey_on_date": "Close survey on date", + "close_survey_on_response_limit": "Close survey on response limit", + "color": "Color", + "columns": "Columns", + "company": "Company", + "company_logo": "Company logo", + "completed_responses": "completed responses.", + "concat": "Concat +", + "conditional_logic": "Conditional Logic", + "confirm_default_language": "Confirm default language", + "confirm_survey_changes": "Confirm Survey Changes", + "contact_fields": "Contact Fields", + "contains": "Contains", + "continue_to_settings": "Continue to Settings", + "control_which_file_types_can_be_uploaded": "Control which file types can be uploaded.", + "convert_to_multiple_choice": "Convert to Multiple Choice", + "convert_to_single_choice": "Convert to Single Choice", + "country": "Country", + "create_group": "Create group", + "create_your_own_survey": "Create your own survey", + "css_selector": "CSS Selector", + "custom_hostname": "Custom hostname", + "darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.", + "date_format": "Date format", + "days_before_showing_this_survey_again": "days before showing this survey again.", + "decide_how_often_people_can_answer_this_survey": "Decide how often people can answer this survey.", + "delete_choice": "Delete choice", + "description": "Description", + "disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.", + "display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey", + "display_number_of_responses_for_survey": "Display number of responses for survey", + "divide": "Divide /", + "does_not_contain": "Does not contain", + "does_not_end_with": "Does not end with", + "does_not_equal": "Does not equal", + "does_not_include_all_of": "Does not include all of", + "does_not_include_one_of": "Does not include one of", + "does_not_start_with": "Does not start with", + "edit_recall": "Edit Recall", + "edit_translations": "Edit {lang} translations", + "enable_encryption_of_single_use_id_suid_in_survey_url": "Enable encryption of Single Use Id (suId) in survey URL.", + "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Enable participants to switch the survey language at any point during the survey.", + "end_screen_card": "End screen card", + "ending_card": "Ending card", + "ending_card_used_in_logic": "This ending card is used in logic of question {questionIndex}.", + "ends_with": "Ends with", + "equals": "Equals", + "equals_one_of": "Equals one of", + "error_publishing_survey": "An error occured while publishing the survey.", + "error_saving_changes": "Error saving changes", + "even_after_they_submitted_a_response_e_g_feedback_box": "Even after they submitted a response (e.g. Feedback Box)", + "everyone": "Everyone", + "fallback_missing": "Fallback missing", + "fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} is used in logic of question {questionIndex}. Please remove it from logic first.", + "field_name_eg_score_price": "Field name e.g, score, price", + "first_name": "First Name", + "five_points_recommended": "5 points (recommended)", + "follow_ups": "Follow-ups", + "follow_ups_delete_modal_text": "Are you sure you want to delete this follow-up?", + "follow_ups_delete_modal_title": "Delete follow-up?", + "follow_ups_empty_description": "Send messages to respondents, yourself or team mates.", + "follow_ups_empty_heading": "Send automatic follow-ups", + "follow_ups_ending_card_delete_modal_text": "This ending card is used in follow-ups. Deleting it will remove it from all follow-ups. Are you sure you want to delete it?", + "follow_ups_ending_card_delete_modal_title": "Delete ending card?", + "follow_ups_hidden_field_error": "Hidden field is used in a follow-up. Please remove it from follow-up first.", + "follow_ups_item_ending_tag": "Ending(s)", + "follow_ups_item_issue_detected_tag": "Issue detected", + "follow_ups_item_response_tag": "Any response", + "follow_ups_item_send_email_tag": "Send email", + "follow_ups_modal_action_attach_response_data_description": "Add the data of the survey response to the follow-up", + "follow_ups_modal_action_attach_response_data_label": "Attach response data", + "follow_ups_modal_action_body_label": "Body", + "follow_ups_modal_action_body_placeholder": "Body of the email", + "follow_ups_modal_action_email_content": "Email content", + "follow_ups_modal_action_email_settings": "Email settings", + "follow_ups_modal_action_from_description": "Email address to send the email from", + "follow_ups_modal_action_from_label": "From", + "follow_ups_modal_action_label": "Action", + "follow_ups_modal_action_replyTo_description": "If the recipient hits reply, the following email address will receive it", + "follow_ups_modal_action_replyTo_label": "Reply To", + "follow_ups_modal_action_subject": "Thanks for your answers!", + "follow_ups_modal_action_subject_label": "Subject", + "follow_ups_modal_action_subject_placeholder": "Subject of the email", + "follow_ups_modal_action_to_description": "Email address to send the email to", + "follow_ups_modal_action_to_label": "To", + "follow_ups_modal_action_to_warning": "No email field detected in the survey", + "follow_ups_modal_create_heading": "Create a new follow-up", + "follow_ups_modal_edit_heading": "Edit this follow-up", + "follow_ups_modal_edit_no_id": "No survey follow up id provided, can't update the survey follow up", + "follow_ups_modal_name_label": "Follow-up name", + "follow_ups_modal_name_placeholder": "Name your follow-up", + "follow_ups_modal_subheading": "Send messages to respondents, yourself or team mates", + "follow_ups_modal_trigger_description": "When should this follow-up be triggered?", + "follow_ups_modal_trigger_label": "Trigger", + "follow_ups_modal_trigger_type_ending": "Respondent sees a specific ending", + "follow_ups_modal_trigger_type_ending_select": "Select endings: ", + "follow_ups_modal_trigger_type_ending_warning": "No endings found in the survey!", + "follow_ups_modal_trigger_type_response": "Respondent completes survey", + "follow_ups_new": "New follow-up", + "follow_ups_upgrade_button_text": "Upgrade to enable follow-ups", + "form_styling": "Form styling", + "formbricks_ai_description": "Describe your survey and let Formbricks AI create the survey for you", + "formbricks_ai_generate": "Generate", + "formbricks_ai_prompt_placeholder": "Enter survey information (e.g. key topics to cover)", + "formbricks_sdk_is_not_connected": "Formbricks SDK is not connected", + "four_points": "4 points", + "heading": "Heading", + "hidden_field_added_successfully": "Hidden field added successfully", + "hide_advanced_settings": "Hide advanced settings", + "hide_back_button": "Hide 'Back' button", + "hide_back_button_description": "Do not display the back button in the survey", + "hide_logo": "Hide logo", + "hide_progress_bar": "Hide progress bar", + "hide_the_logo_in_this_specific_survey": "Hide the logo in this specific survey", + "hostname": "Hostname", + "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys", + "how_it_works": "How it works", + "if_you_need_more_please": "If you need more, please", + "if_you_really_want_that_answer_ask_until_you_get_it": "If you really want that answer, ask until you get it.", + "ignore_waiting_time_between_surveys": "Ignore waiting time between surveys", + "image": "Image", + "includes_all_of": "Includes all of", + "includes_one_of": "Includes one of", + "initial_value": "Initial value", + "inner_text": "Inner Text", + "input_border_color": "Input border color", + "input_color": "Input color", + "invalid_targeting": "Invalid targeting: Please check your audience filters", + "invalid_video_url_warning": "Please enter a valid YouTube, Vimeo, or Loom URL. We currently do not support other video hosting providers.", + "invalid_youtube_url": "Invalid YouTube URL", + "is_accepted": "Is accepted", + "is_after": "Is after", + "is_before": "Is before", + "is_booked": "Is booked", + "is_clicked": "Is clicked", + "is_completely_submitted": "Is completely submitted", + "is_not_set": "Is not set", + "is_partially_submitted": "Is partially submitted", + "is_set": "Is set", + "is_skipped": "Is skipped", + "is_submitted": "Is submitted", + "jump_to_question": "Jump to question", + "keep_current_order": "Keep current order", + "keep_showing_while_conditions_match": "Keep showing while conditions match", + "key": "Key", + "last_name": "Last Name", + "let_people_upload_up_to_25_files_at_the_same_time": "Let people upload up to 25 files at the same time.", + "limit_file_types": "Limit file types", + "limit_the_maximum_file_size": "Limit the maximum file size", + "limit_upload_file_size_to": "Limit upload file size to", + "link_survey_description": "Share a link to a survey page or embed it in a web page or email.", + "link_used_message": "Link Used", + "load_segment": "Load segment", + "logic_error_warning": "Changing will cause logic errors", + "logic_error_warning_text": "Changing the question type will remove the logic conditions from this question", + "long_answer": "Long answer", + "lower_label": "Lower Label", + "manage_languages": "Manage Languages", + "max_file_size": "Max file size", + "max_file_size_limit_is": "Max file size limit is", + "multiply": "Multiply *", + "needed_for_self_hosted_cal_com_instance": "Needed for a self-hosted Cal.com instance", + "next_button_label": "\"Next\" button label", + "next_question": "Next question", + "no_hidden_fields_yet_add_first_one_below": "No hidden fields yet. Add the first one below.", + "no_images_found_for": "No images found for ''{query}\"", + "no_languages_found_add_first_one_to_get_started": "No languages found. Add the first one to get started.", + "no_variables_yet_add_first_one_below": "No variables yet. Add the first one below.", + "number": "Number", + "once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations": "Once set, the default language for this survey can only be changed by disabling the multi-language option and deleting all translations.", + "only_display_the_survey_to_a_subset_of_the_users": "Only display the survey to a subset of the users", + "only_lower_case_letters_numbers_and_underscores_are_allowed": "Only lower case letters, numbers, and underscores are allowed.", + "only_people_who_match_your_targeting_can_be_surveyed": "Only people who match your targeting can be surveyed.", + "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", + "override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.", + "overwrite_placement": "Overwrite placement", + "overwrite_the_global_placement_of_the_survey": "Overwrite the global placement of the survey", + "overwrites_waiting_period_between_surveys_to_x_days": "Overwrites waiting period between surveys to {days} day(s).", + "pick_a_background_from_our_library_or_upload_your_own": "Pick a background from our library or upload your own.", + "picture_idx": "Picture {idx}", + "pin_can_only_contain_numbers": "PIN can only contain numbers.", + "pin_must_be_a_four_digit_number": "PIN must be a four digit number.", + "please_enter_a_file_extension": "Please enter a file extension.", + "please_set_a_survey_trigger": "Please set a survey trigger", + "please_specify": "Please specify", + "prevent_double_submission": "Prevent double submission", + "prevent_double_submission_description": "Only allow 1 response per email address", + "protect_survey_with_pin": "Protect survey with a PIN", + "protect_survey_with_pin_description": "Only users who have the PIN can access the survey.", + "publish": "Publish", + "question": "Question", + "question_color": "Question color", + "question_deleted": "Question deleted.", + "question_duplicated": "Question duplicated.", + "question_id_updated": "Question ID updated", + "question_used_in_logic": "This question is used in logic of question {questionIndex}.", + "randomize_all": "Randomize all", + "randomize_all_except_last": "Randomize all except last", + "range": "Range", + "recontact_options": "Recontact Options", + "redirect_thank_you_card": "Redirect thank you card", + "redirect_to_url": "Redirect to Url", + "redirect_to_url_not_available_on_free_plan": "Redirect To Url is not available on free plan", + "release_survey_on_date": "Release survey on date", + "remove_description": "Remove description", + "remove_translations": "Remove translations", + "require_answer": "Require Answer", + "required": "Required", + "reset_to_theme_styles": "Reset to theme styles", + "reset_to_theme_styles_main_text": "Are you sure you want to reset the styling to the theme styles? This will remove all custom styling.", + "response_limit_can_t_be_set_to_0": "Response limit can't be set to 0", + "response_limit_needs_to_exceed_number_of_received_responses": "Response limit needs to exceed number of received responses ({responseCount}).", + "response_limits_redirections_and_more": "Response limits, redirections and more.", + "response_options": "Response Options", + "roundness": "Roundness", + "rows": "Rows", + "save_and_close": "Save & Close", + "scale": "Scale", + "search_for_images": "Search for images", + "seconds_after_trigger_the_survey_will_be_closed_if_no_response": "seconds after trigger the survey will be closed if no response", + "seconds_before_showing_the_survey": "seconds before showing the survey.", + "select_or_type_value": "Select or type value", + "select_ordering": "Select ordering", + "select_saved_action": "Select saved action", + "select_type": "Select type", + "send_survey_to_audience_who_match": "Send survey to audience who match...", + "send_your_respondents_to_a_page_of_your_choice": "Send your respondents to a page of your choice.", + "set_the_global_placement_in_the_look_feel_settings": "Set the global placement in the Look & Feel settings.", + "seven_points": "7 points", + "show_advanced_settings": "Show Advanced settings", + "show_button": "Show Button", + "show_language_switch": "Show language switch", + "show_multiple_times": "Show multiple times", + "show_only_once": "Show only once", + "show_survey_maximum_of": "Show survey maximum of", + "show_survey_to_users": "Show survey to % of users", + "show_to_x_percentage_of_targeted_users": "Show to {percentage}% of targeted users", + "simple": "Simple", + "single_use_survey_links": "Single-use survey links", + "single_use_survey_links_description": "Allow only 1 response per survey link.", + "skip_button_label": "Skip Button Label", + "smiley": "Smiley", + "star": "Star", + "starts_with": "Starts with", + "state": "State", + "straight": "Straight", + "style_the_question_texts_descriptions_and_input_fields": "Style the question texts, descriptions and input fields.", + "style_the_survey_card": "Style the survey card.", + "styling_set_to_theme_styles": "Styling set to theme styles", + "subheading": "Subheading", + "subtract": "Subtract -", + "suggest_colors": "Suggest colors", + "survey_already_answered_heading": "The survey has already been answered.", + "survey_already_answered_subheading": "You can only use this link once.", + "survey_completed_heading": "Survey Completed", + "survey_completed_subheading": "This free & open-source survey has been closed", + "survey_display_settings": "Survey Display Settings", + "survey_placement": "Survey Placement", + "survey_trigger": "Survey Trigger", + "switch_multi_lanugage_on_to_get_started": "Switch multi-lanugage on to get started \uD83D\uDC49", + "targeted": "Targeted", + "ten_points": "10 points", + "the_survey_will_be_shown_multiple_times_until_they_respond": "The survey will be shown multiple times until they respond", + "the_survey_will_be_shown_once_even_if_person_doesnt_respond": "The survey will be shown once, even if person doesn't respond.", + "then": "Then", + "this_action_will_remove_all_the_translations_from_this_survey": "This action will remove all the translations from this survey.", + "this_extension_is_already_added": "This extension is already added.", + "this_file_type_is_not_supported": "This file type is not supported.", + "this_setting_overwrites_your": "This setting overwrites your", + "three_points": "3 points", + "times": "times", + "to_keep_the_placement_over_all_surveys_consistent_you_can": "To keep the placement over all surveys consistent, you can", + "trigger_survey_when_one_of_the_actions_is_fired": "Trigger survey when one of the actions is fired...", + "try_lollipop_or_mountain": "Try 'lollipop' or 'mountain'...", + "type_field_id": "Type field id", + "unlock_targeting_description": "Target specific user groups based on attributes or device information", + "unlock_targeting_title": "Unlock targeting with a higher plan", + "unsaved_changes_warning": "You have unsaved changes in your survey. Would you like to save them before leaving?", + "until_they_submit_a_response": "Until they submit a response", + "upgrade_notice_description": "Create multilingual surveys and unlock many more features", + "upgrade_notice_title": "Unlock multi-language surveys with a higher plan", + "upload": "Upload", + "upload_at_least_2_images": "Upload at least 2 images", + "upper_label": "Upper Label", + "url_encryption": "URL Encryption", + "url_filters": "URL Filters", + "url_not_supported": "URL not supported", + "use_with_caution": "Use with caution", + "variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} is used in logic of question {questionIndex}. Please remove it from logic first.", + "variable_name_is_already_taken_please_choose_another": "Variable name is already taken, please choose another.", + "variable_name_must_start_with_a_letter": "Variable name must start with a letter.", + "verify_email_before_submission": "Verify email before submission", + "verify_email_before_submission_description": "Only let people with a real email respond.", + "wait": "Wait", + "wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Wait a few seconds after the trigger before showing the survey", + "waiting_period": "waiting period", + "welcome_message": "Welcome message", + "when": "When", + "when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "When conditions match, waiting time will be ignored and survey shown.", + "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_project_to_work_with_translations": "You need to have two or more languages set up in your project 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", + "zip": "Zip" + }, + "error_deleting_survey": "An error occured while deleting survey", + "failed_to_copy_link_to_results": "Failed to copy link to results", + "failed_to_copy_url": "Failed to copy URL: not in a browser environment.", + "new_single_use_link_generated": "New single use link generated", + "new_survey": "New Survey", + "no_surveys_created_yet": "No surveys created yet", + "open_options": "Open options", + "preview_survey_in_a_new_tab": "Preview survey in a new tab", + "read_only_user_not_allowed_to_create_survey_warning": "As a Read-Only user you are not allowed to create surveys. Please ask a user with write access to create a survey or a manager to upgrade your role.", + "relevance": "Relevance", + "responses": { + "address_line_1": "Address Line 1", + "address_line_2": "Address Line 2", + "an_error_occurred_creating_a_new_note": "An error occurred creating a new note", + "an_error_occurred_deleting_the_tag": "An error occurred deleting the tag", + "an_error_occurred_resolving_a_note": "An error occurred resolving a note", + "an_error_occurred_updating_a_note": "An error occurred updating a note", + "browser": "Browser", + "city": "City", + "company": "Company", + "completed": "Completed ✅", + "country": "Country", + "device": "Device", + "device_info": "Device info", + "email": "Email", + "first_name": "First Name", + "how_to_identify_users": "How to identify users", + "last_name": "Last Name", + "not_completed": "Not Completed ⏳", + "os": "OS", + "person_attributes": "Person attributes", + "phone": "Phone", + "resolve": "Resolve", + "respondent_skipped_questions": "Respondent skipped these questions.", + "response_deleted_successfully": "Response deleted successfully.", + "single_use_id": "SingleUse ID", + "source": "Source", + "state_region": "State / Region", + "survey_closed": "Survey closed", + "tag_already_exists": "Tag already exists", + "this_response_is_in_progress": "This response is in progress.", + "zip_post_code": "ZIP / Post code" + }, + "results_unpublished_successfully": "Results unpublished successfully.", + "search_by_survey_name": "Search by survey name", + "summary": { + "added_filter_for_responses_where_answer_to_question": "Added filter for responses where answer to question {questionIdx} is {filterComboBoxValue} - {filterValue} ", + "added_filter_for_responses_where_answer_to_question_is_skipped": "Added filter for responses where answer to question {questionIdx} is skipped", + "all_responses_csv": "All responses (CSV)", + "all_responses_excel": "All responses (Excel)", + "all_time": "All time", + "almost_there": "Almost there! Install widget to start receiving responses.", + "average": "Average", + "completed": "Completed", + "completed_tooltip": "Number of times the survey has been completed.", + "configure_alerts": "Configure alerts", + "congrats": "Congrats! Your survey is live.", + "connect_your_website_or_app_with_formbricks_to_get_started": "Connect your website or app with Formbricks to get started.", + "copy_link_to_public_results": "Copy link to public results", + "create_single_use_links": "Create single-use links", + "create_single_use_links_description": "Accept only one submission per link. Here is how.", + "custom_range": "Custom range...", + "data_prefilling": "Data prefilling", + "data_prefilling_description": "You want to prefill some fields in the survey? Here is how.", + "define_when_and_where_the_survey_should_pop_up": "Define when and where the survey should pop up", + "drop_offs": "Drop-Offs", + "drop_offs_tooltip": "Number of times the survey has been started but not completed.", + "dynamic_popup": "Dynamic (Pop-up)", + "email_sent": "Email sent!", + "embed_code_copied_to_clipboard": "Embed code copied to clipboard!", + "embed_in_an_email": "Embed in an email", + "embed_in_app": "Embed in app", + "embed_mode": "Embed Mode", + "embed_mode_description": "Embed your survey with a minimalist design, discarding padding and background.", + "embed_on_website": "Embed on website", + "embed_pop_up_survey_title": "How to embed a pop-up survey on your website", + "embed_survey": "Embed survey", + "enable_ai_insights_banner_button": "Enable insights", + "enable_ai_insights_banner_description": "You can enable the new insights feature for the survey to get AI-based insights for your open-text responses.", + "enable_ai_insights_banner_success": "Generating insights for this survey. Please check back in a few minutes.", + "enable_ai_insights_banner_title": "Ready to test AI insights?", + "enable_ai_insights_banner_tooltip": "Kindly contact us at hola@formbricks.com to generate insights for this survey", + "failed_to_copy_link": "Failed to copy link", + "filter_added_successfully": "Filter added successfully", + "filter_updated_successfully": "Filter updated successfully", + "filtered_responses_csv": "Filtered responses (CSV)", + "filtered_responses_excel": "Filtered responses (Excel)", + "formbricks_email_survey_preview": "Formbricks Email Survey Preview", + "go_to_setup_checklist": "Go to Setup Checklist \uD83D\uDC49", + "hide_embed_code": "Hide embed code", + "how_to_create_a_panel": "How to create a panel", + "how_to_create_a_panel_step_1": "Step 1: Create an account with Prolific", + "how_to_create_a_panel_step_1_description": "Create an account with Prolific and verify your email address.", + "how_to_create_a_panel_step_2": "Step 2: Create a study", + "how_to_create_a_panel_step_2_description": "At Prolific, you create a new study where you can pick your preferred audience based on hundreds of characteristics.", + "how_to_create_a_panel_step_3": "Step 3: Connect your survey", + "how_to_create_a_panel_step_3_description": "Set up hidden fields in your Formbricks survey to track which participant provided which answer.", + "how_to_create_a_panel_step_4": "Step 4: Launch your study", + "how_to_create_a_panel_step_4_description": "Once everything is setup, you can launch your study. Within a few hours you’ll receive the first responses.", + "impressions": "Impressions", + "impressions_tooltip": "Number of times the survey has been viewed.", + "includes_all": "Includes all", + "includes_either": "Includes either", + "insights_disabled": "Insights disabled", + "install_widget": "Install Formbricks Widget", + "is_equal_to": "Is equal to", + "is_less_than": "Is less than", + "last_30_days": "Last 30 days", + "last_6_months": "Last 6 months", + "last_7_days": "Last 7 days", + "last_month": "Last month", + "last_quarter": "Last quarter", + "last_year": "Last year", + "link_to_public_results_copied": "Link to public results copied", + "make_sure_the_survey_type_is_set_to": "Make sure the survey type is set to", + "mobile_app": "Mobile app", + "no_response_matches_filter": "No response matches your filter", + "only_completed": "Only completed", + "other_values_found": "Other values found", + "overall": "Overall", + "publish_to_web": "Publish to web", + "publish_to_web_warning": "You are about to release these survey results to the public.", + "publish_to_web_warning_description": "Your survey results will be public. Anyone outside your organization can access them if they have the link.", + "quickstart_mobile_apps": "Quickstart: Mobile apps", + "quickstart_mobile_apps_description": "To get started with surveys in mobile apps, please follow the Quickstart guide:", + "quickstart_web_apps": "Quickstart: Web apps", + "quickstart_web_apps_description": "Please follow the Quickstart guide to get started:", + "results_are_public": "Results are public", + "send_preview": "Send preview", + "send_to_panel": "Send to panel", + "setup_instructions": "Setup instructions", + "setup_integrations": "Setup integrations", + "share_results": "Share results", + "share_the_link": "Share the link", + "share_the_link_to_get_responses": "Share the link to get responses", + "show_all_responses_that_match": "Show all responses that match", + "show_all_responses_where": "Show all responses where...", + "single_use_links": "Single use links", + "source_tracking": "Source tracking", + "source_tracking_description": "Run GDPR & CCPA compliant source tracking without extra tools.", + "starts": "Starts", + "starts_tooltip": "Number of times the survey has been started.", + "static_iframe": "Static (iframe)", + "survey_results_are_public": "Your survey results are public!", + "survey_results_are_shared_with_anyone_who_has_the_link": "Your survey results are shared with anyone who has the link. The results will not be indexed by search engines.", + "this_month": "This month", + "this_quarter": "This quarter", + "this_year": "This year", + "time_to_complete": "Time to Complete", + "to_connect_your_website_with_formbricks": "to connect your website with Formbricks", + "ttc_tooltip": "Average time to complete the survey.", + "unknown_question_type": "Unknown Question Type", + "unpublish_from_web": "Unpublish from web", + "unsupported_video_tag_warning": "Your browser does not support the video tag.", + "view_embed_code": "View embed code", + "view_embed_code_for_email": "View embed code for email", + "view_site": "View site", + "waiting_for_response": "Waiting for a response \uD83E\uDDD8‍♂️", + "web_app": "Web app", + "what_is_a_panel": "What is a panel?", + "what_is_a_panel_answer": "A panel is a group of participants selected based on characteristics such as age, profession, gender, etc.", + "what_is_prolific": "What is Prolific?", + "what_is_prolific_answer": "We're partnering with Prolific to give you access to a pool of over 200.000 vetted participants.", + "whats_next": "What's next?", + "when_do_i_need_it": "When do I need it?", + "when_do_i_need_it_answer": "If you don’t have access to enough people who match your target audience, it makes sense to pay for access to a panel.", + "you_can_do_a_lot_more_with_links_surveys": "You can do a lot more with links surveys \uD83D\uDCA1", + "your_survey_is_public": "Your survey is public", + "youre_not_plugged_in_yet": "You're not plugged in yet!" + }, + "survey_deleted_successfully": "Survey deleted successfully!", + "survey_duplicated_successfully": "Survey duplicated successfully.", + "survey_duplication_error": "Failed to duplicate the survey.", + "survey_status_tooltip": "To update the survey status, update the schedule and close setting in the survey response options.", + "templates": { + "all_channels": "All channels", + "all_industries": "All industries", + "all_roles": "All roles", + "create_a_new_survey": "Create a new survey", + "multiple_industries": "Multiple industries", + "use_this_template": "Use this template", + "uses_branching_logic": "This survey uses branching logic." + } + }, + "xm-templates": { + "ces": "CES", + "ces_description": "Leverage every touchpoint to understand ease of customer interaction.", + "csat": "CSAT", + "csat_description": "Implement best practices to measure customer satisfaction.", + "enps": "eNPS", + "enps_description": "Universal feedback to understand employee engagement and satisfaction.", + "five_star_rating": "5-Star Rating", + "five_star_rating_description": "Universal feedback solution to gauge overall satisfaction.", + "headline": "What kind of feedback would you like to get?", + "nps": "NPS", + "nps_description": "Implement proven best practices to understand WHY people buy.", + "smileys": "Smileys", + "smileys_description": "Use visual indicators to capture feedback across customer touchpoints." + } + }, + "organizations": { + "landing": { + "no_projects_warning_subtitle": "Reach out to your organization owner to get access to projects. Or create an own organization to get started.", + "no_projects_warning_title": "Your account doesn't have access to any projects yet." + }, + "projects": { + "new": { + "channel": { + "channel_select_subtitle": "Share a link or display your survey in apps or on websites.", + "channel_select_title": "What type of surveys do you need?", + "in_product_surveys": "In-product surveys", + "in_product_surveys_description": "Embedded in apps or websites.", + "link_and_email_surveys": "Link & email surveys", + "link_and_email_surveys_description": "Reach people anywhere online." + }, + "mode": { + "formbricks_cx": "Formbricks CX", + "formbricks_cx_description": "Surveys and reports to understand what your customers need.", + "formbricks_surveys": "Formbricks Surveys", + "formbricks_surveys_description": "Multi-purpose survey platform for web, app and email surveys.", + "what_are_you_here_for": "What are you here for?" + }, + "settings": { + "brand_color": "Brand color", + "brand_color_description": "Match the main color of surveys with your brand.", + "create_new_team": "Create new team", + "project_creation_failed": "Project creation failed", + "project_name": "Product name", + "project_name_description": "What is your product called?", + "project_settings_subtitle": "When people recognize your brand, they are much more likely to start and complete responses.", + "project_settings_title": "Let respondents know it's you", + "team_description": "Who all can access this project?" + } + } + } + }, + "s": { + "check_inbox_or_spam": "Please also check your spam folder if you don't see the email in your inbox.", + "completed": "This free & open-source survey has been closed.", + "create_your_own": "Create your own", + "enter_pin": "This survey is protected. Enter the PIN below", + "just_curious": "Just curious?", + "link_invalid": "This survey can only be taken by invitation.", + "paused": "This free & open-source survey is temporarily paused.", + "please_try_again_with_the_original_link": "Please try again with the original link", + "preview_survey_questions": "Preview survey questions.", + "question_preview": "Question Preview", + "response_already_received": "We already received a response for this email address.", + "response_submitted": "A response linked to this survey and contact already exists", + "survey_already_answered_heading": "The survey has already been answered.", + "survey_already_answered_subheading": "You can only use this link once.", + "survey_sent_to": "Survey sent to {email}", + "this_looks_fishy": "This looks fishy.", + "verify_email": "Verify email.", + "verify_email_before_submission": "Verify your email to respond", + "verify_email_before_submission_button": "Verify", + "verify_email_before_submission_description": "To respond to this survey, please verify your email", + "want_to_respond": "Want to respond?" + }, + "setup": { + "intro": { + "get_started": "Get started", + "made_with_love_in_kiel": "Made with \uD83E\uDD0D in Germany", + "paragraph_1": "Formbricks is an Experience Management Suite built of the fastest growing open source survey platform worldwide.", + "paragraph_2": "Run targeted surveys on websites, in apps or anywhere online. Gather valuable insights to craft irresistible experiences for customers, users and employees.", + "paragraph_3": "We're commited to highest degree of data privacy. Self-host to keep full control over your data.", + "welcome_to_formbricks": "Welcome to Formbricks!" + }, + "invite": { + "add_another_member": "Add another member", + "continue": "Continue", + "failed_to_invite": "Failed to invite", + "invitation_sent_to": "Invitation sent to", + "invite_your_organization_members": "Invite your Organization members", + "life_s_no_fun_alone": "Life's no fun alone.", + "skip": "Skip", + "smtp_not_configured": "SMTP not configured", + "smtp_not_configured_description": "Invitations cannot be sent at this time because the email service is not configured. You can copy the invite link in the organization settings later." + }, + "organization": { + "create": { + "continue": "Continue", + "delete_account": "Delete account", + "delete_account_description": "If you want to delete your account, you can do so by clicking the button below.", + "description": "Make it yours.", + "no_membership_found": "No membership found!", + "no_membership_found_description": "You are not a member of any organization at this time. If you believe this is a mistake, please reach out to the organization owner.", + "title": "Setup your organization" + } + }, + "signup": { + "create_administrator": "Create Administrator", + "this_user_has_all_the_power": "This user has all the power." + } + }, + "share": { + "back_to_home": "Back to home", + "page_not_found": "Page not found", + "page_not_found_description": "Sorry, we couldn't find the responses sharing ID you're looking for." + }, + "templates": { + "address": "Address", + "address_description": "Ask for a mailing address", + "alignment_and_engagement_survey_description": "Gauge employee alignment with the company's vision, strategy, and communication, as well as team collaboration.", + "alignment_and_engagement_survey_name": "Alignment and Engagement with Company Vision", + "alignment_and_engagement_survey_question_1_headline": "I understand how my role contributes to the company’s overall strategy.", + "alignment_and_engagement_survey_question_1_lower_label": "No understanding", + "alignment_and_engagement_survey_question_1_upper_label": "Complete understanding", + "alignment_and_engagement_survey_question_2_headline": "I feel that my values align with the company’s mission and culture.", + "alignment_and_engagement_survey_question_2_lower_label": "Not aligned", + "alignment_and_engagement_survey_question_2_upper_label": "Completely aligned", + "alignment_and_engagement_survey_question_3_headline": "I collaborate effectively with my team to achieve our goals.", + "alignment_and_engagement_survey_question_3_lower_label": "Poor collaboration", + "alignment_and_engagement_survey_question_3_upper_label": "Excellent collaboration", + "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", + "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", + "build_product_roadmap_name_with_project_name": "$[projectName] Roadmap Input", + "build_product_roadmap_question_1_headline": "How satisfied are you with the features and functionality of $[projectName]?", + "build_product_roadmap_question_1_lower_label": "Not at all satisfied", + "build_product_roadmap_question_1_upper_label": "Extremely satisfied", + "build_product_roadmap_question_2_headline": "What's ONE change we could make to improve your $[projectName] experience most?", + "build_product_roadmap_question_2_placeholder": "Type your answer here...", + "card_abandonment_survey": "Cart Abandonment Survey", + "card_abandonment_survey_description": "Understand the reasons behind cart abandonment in your web shop.", + "card_abandonment_survey_question_1_button_label": "Sure!", + "card_abandonment_survey_question_1_dismiss_button_label": "No, thanks.", + "card_abandonment_survey_question_1_headline": "Do you have 2 minutes to help us improve?", + "card_abandonment_survey_question_1_html": "

We noticed you left some items in your cart. We would love to understand why.

", + "card_abandonment_survey_question_2_choice_1": "High shipping costs", + "card_abandonment_survey_question_2_choice_2": "Found a better price elsewhere", + "card_abandonment_survey_question_2_choice_3": "Just browsing", + "card_abandonment_survey_question_2_choice_4": "Decided not to buy", + "card_abandonment_survey_question_2_choice_5": "Payment issues", + "card_abandonment_survey_question_2_choice_6": "Other", + "card_abandonment_survey_question_2_headline": "What was the primary reason you didn't complete your purchase?", + "card_abandonment_survey_question_2_subheader": "Please select one of the following options:", + "card_abandonment_survey_question_3_headline": "Please elaborate on your reason for not completing the purchase:", + "card_abandonment_survey_question_4_headline": "How would you rate your overall shopping experience?", + "card_abandonment_survey_question_4_lower_label": "Very dissatisfied", + "card_abandonment_survey_question_4_upper_label": "Very satisfied", + "card_abandonment_survey_question_5_choice_1": "Lower shipping costs", + "card_abandonment_survey_question_5_choice_2": "Discounts or promotions", + "card_abandonment_survey_question_5_choice_3": "More payment options", + "card_abandonment_survey_question_5_choice_4": "Better product descriptions", + "card_abandonment_survey_question_5_choice_5": "Improved website navigation", + "card_abandonment_survey_question_5_choice_6": "Other", + "card_abandonment_survey_question_5_headline": "What factors would encourage you to complete your purchase in the future?", + "card_abandonment_survey_question_5_subheader": "Please select all that apply:", + "card_abandonment_survey_question_6_headline": "Would you like to receive a discount code via email?", + "card_abandonment_survey_question_6_label": "Yes, please reach out.", + "card_abandonment_survey_question_7_headline": "Please share your email address:", + "card_abandonment_survey_question_8_headline": "Any additional comments or suggestions?", + "career_development_survey_description": "Assess employee satisfaction with career growth and development opportunities.", + "career_development_survey_name": "Career Development Survey", + "career_development_survey_question_1_headline": "I am satisfied with the opportunities for personal and professional growth at $[projectName].", + "career_development_survey_question_1_lower_label": "Strongly disagree", + "career_development_survey_question_1_upper_label": "Strongly agree", + "career_development_survey_question_2_headline": "I am pleased with the career advancement opportunities available to me at $[projectName].", + "career_development_survey_question_2_lower_label": "Strongly disagree", + "career_development_survey_question_2_upper_label": "Strongly agree", + "career_development_survey_question_3_headline": "I am satisfied with the job-related training my organization offers.", + "career_development_survey_question_3_lower_label": "Strongly disagree", + "career_development_survey_question_3_upper_label": "Strongly agree", + "career_development_survey_question_4_headline": "I am satisfied with the investment my organization makes in training and education.", + "career_development_survey_question_4_lower_label": "Strongly disagree", + "career_development_survey_question_4_upper_label": "Strongly agree", + "career_development_survey_question_5_choice_1": "Product Development", + "career_development_survey_question_5_choice_2": "Marketing", + "career_development_survey_question_5_choice_3": "Public Relations", + "career_development_survey_question_5_choice_4": "Accounting", + "career_development_survey_question_5_choice_5": "Operations", + "career_development_survey_question_5_choice_6": "Other", + "career_development_survey_question_5_headline": "Which function do you work in?", + "career_development_survey_question_5_subheader": "Please select one of the following", + "career_development_survey_question_6_choice_1": "Individual Contributor", + "career_development_survey_question_6_choice_2": "Manager", + "career_development_survey_question_6_choice_3": "Senior Manager", + "career_development_survey_question_6_choice_4": "Vice President", + "career_development_survey_question_6_choice_5": "Executive", + "career_development_survey_question_6_choice_6": "Other", + "career_development_survey_question_6_headline": "Which of the following best describes your current job level?", + "career_development_survey_question_6_subheader": "Please select one of the following", + "cess_survey_name": "CES Survey", + "cess_survey_question_1_headline": "$[projectName] makes it easy for me to [ADD GOAL]", + "cess_survey_question_1_lower_label": "Disagree strongly", + "cess_survey_question_1_upper_label": "Agree strongly", + "cess_survey_question_2_headline": "Thanks! How could we make it easier for you to [ADD GOAL]?", + "cess_survey_question_2_placeholder": "Type your answer here...", + "changing_subscription_experience_description": "Find out what goes through peoples minds when changing their subscriptions.", + "changing_subscription_experience_name": "Changing Subscription Experience", + "changing_subscription_experience_question_1_choice_1": "Extremely difficult", + "changing_subscription_experience_question_1_choice_2": "It took a while, but I got it", + "changing_subscription_experience_question_1_choice_3": "It was alright", + "changing_subscription_experience_question_1_choice_4": "Quite easy", + "changing_subscription_experience_question_1_choice_5": "Very easy, love it!", + "changing_subscription_experience_question_1_headline": "How easy was it to change your plan?", + "changing_subscription_experience_question_2_choice_1": "Yes, very clear.", + "changing_subscription_experience_question_2_choice_2": "I was confused at first, but found what I needed.", + "changing_subscription_experience_question_2_choice_3": "Quite complicated.", + "changing_subscription_experience_question_2_headline": "Is the pricing information easy to understand?", + "churn_survey": "Churn Survey", + "churn_survey_description": "Find out why people cancel their subscriptions. These insights are pure gold!", + "churn_survey_question_1_choice_1": "Difficult to use", + "churn_survey_question_1_choice_2": "It's too expensive", + "churn_survey_question_1_choice_3": "I am missing features", + "churn_survey_question_1_choice_4": "Poor customer service", + "churn_survey_question_1_choice_5": "I just didn't need it anymore", + "churn_survey_question_1_headline": "Why did you cancel your subscription?", + "churn_survey_question_1_subheader": "We're sorry to see you leave. Help us do better:", + "churn_survey_question_2_button_label": "Send", + "churn_survey_question_2_headline": "What would have made $[projectName] easier to use?", + "churn_survey_question_3_button_label": "Get 30% off", + "churn_survey_question_3_dismiss_button_label": "Skip", + "churn_survey_question_3_headline": "Get 30% off for the next year!", + "churn_survey_question_3_html": "

We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.

", + "churn_survey_question_4_headline": "What features are you missing?", + "churn_survey_question_5_button_label": "Send email to CEO", + "churn_survey_question_5_dismiss_button_label": "Skip", + "churn_survey_question_5_headline": "So sorry to hear \uD83D\uDE14 Talk to our CEO directly!", + "churn_survey_question_5_html": "

We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.

", + "collect_feedback_description": "Gather comprehensive feedback on your product or service.", + "collect_feedback_name": "Collect Feedback", + "collect_feedback_question_1_headline": "How do you rate your overall experience?", + "collect_feedback_question_1_lower_label": "Not good", + "collect_feedback_question_1_subheader": "Don't worry, be honest.", + "collect_feedback_question_1_upper_label": "Very good", + "collect_feedback_question_2_headline": "Lovely! What did you like about it?", + "collect_feedback_question_2_placeholder": "Type your answer here...", + "collect_feedback_question_3_headline": "Thanks for sharing! What did you not like?", + "collect_feedback_question_3_placeholder": "Type your answer here...", + "collect_feedback_question_4_headline": "How do you rate our communication?", + "collect_feedback_question_4_lower_label": "Not good", + "collect_feedback_question_4_upper_label": "Very good", + "collect_feedback_question_5_headline": "Anything else you'd like to share with our team?", + "collect_feedback_question_5_placeholder": "Type your answer here...", + "collect_feedback_question_6_choice_1": "Google", + "collect_feedback_question_6_choice_2": "Social Media", + "collect_feedback_question_6_choice_3": "Friends", + "collect_feedback_question_6_choice_4": "Podcast", + "collect_feedback_question_6_choice_5": "Other", + "collect_feedback_question_6_headline": "How did you hear about us?", + "collect_feedback_question_7_headline": "Lastly, we'd love to respond to your feedback. Please share your email:", + "collect_feedback_question_7_placeholder": "example@email.com", + "consent": "Consent", + "consent_description": "Ask to agree to terms, conditions, or data usage", + "contact_info": "Contact Info", + "contact_info_description": "Ask for name, surname, email, phone number and company jointly", + "csat_description": "Measure the Customer Satisfaction Score of your product or service.", + "csat_name": "Customer Satisfaction Score (CSAT)", + "csat_question_10_headline": "Do you have any other comments, questions or concerns?", + "csat_question_10_placeholder": "Type your answer here...", + "csat_question_1_headline": "How likely is it that you would recommend this $[projectName] to a friend or colleague?", + "csat_question_1_lower_label": "Not likely", + "csat_question_1_upper_label": "Very likely", + "csat_question_2_choice_1": "Somewhat satisfied", + "csat_question_2_choice_2": "Very satisfied", + "csat_question_2_choice_3": "Neither satisfied nor dissatisfied", + "csat_question_2_choice_4": "Somewhat dissatisfied", + "csat_question_2_choice_5": "Very dissatisfied", + "csat_question_2_headline": "Overall, how satisfied or dissatisfied are you with our $[projectName]", + "csat_question_2_subheader": "Please select one:", + "csat_question_3_choice_1": "Ineffective", + "csat_question_3_choice_10": "Unique", + "csat_question_3_choice_2": "Useful", + "csat_question_3_choice_3": "Impractical", + "csat_question_3_choice_4": "Overpriced", + "csat_question_3_choice_5": "High quality", + "csat_question_3_choice_6": "Reliable", + "csat_question_3_choice_7": "Good value for money", + "csat_question_3_choice_8": "Poor quality", + "csat_question_3_choice_9": "Unreliable", + "csat_question_3_headline": "Which of the following words would you use to describe our $[projectName]?", + "csat_question_3_subheader": "Select all that apply:", + "csat_question_4_choice_1": "Extremely well", + "csat_question_4_choice_2": "Very well", + "csat_question_4_choice_3": "Somewhat well", + "csat_question_4_choice_4": "Not so well", + "csat_question_4_choice_5": "Not at all well", + "csat_question_4_headline": "How well do our $[projectName] meet your needs?", + "csat_question_4_subheader": "Select one option:", + "csat_question_5_choice_1": "Very high quality", + "csat_question_5_choice_2": "High quality", + "csat_question_5_choice_3": "Low quality", + "csat_question_5_choice_4": "Very low quality", + "csat_question_5_choice_5": "Neither high nor low", + "csat_question_5_headline": "How would you rate the quality of the $[projectName]?", + "csat_question_5_subheader": "Select one option:", + "csat_question_6_choice_1": "Excellent", + "csat_question_6_choice_2": "Above average", + "csat_question_6_choice_3": "Average", + "csat_question_6_choice_4": "Below average", + "csat_question_6_choice_5": "Poor", + "csat_question_6_headline": "How would you rate the value for money of the $[projectName]?", + "csat_question_6_subheader": "Please select one:", + "csat_question_7_choice_1": "Extremely responsive", + "csat_question_7_choice_2": "Very responsive", + "csat_question_7_choice_3": "Somewhat responsive", + "csat_question_7_choice_4": "Not so responsive", + "csat_question_7_choice_5": "Not at all responsive", + "csat_question_7_choice_6": "Not applicable", + "csat_question_7_headline": "How responsive have we been to your questions about our services?", + "csat_question_7_subheader": "Please select one:", + "csat_question_8_choice_1": "This is my first purchase", + "csat_question_8_choice_2": "Less than six months", + "csat_question_8_choice_3": "Six months to a year", + "csat_question_8_choice_4": "1 - 2 years", + "csat_question_8_choice_5": "3 or more years", + "csat_question_8_choice_6": "I haven't made a purchase yet", + "csat_question_8_headline": "How long have you been a customer of $[projectName]?", + "csat_question_8_subheader": "Please select one:", + "csat_question_9_choice_1": "Extremely likely", + "csat_question_9_choice_2": "Very likely", + "csat_question_9_choice_3": "Somewhat likely", + "csat_question_9_choice_4": "Not so likely", + "csat_question_9_choice_5": "Not at all likely", + "csat_question_9_headline": "How likely are you to purchase any of our $[projectName] again?", + "csat_question_9_subheader": "Select one option:", + "csat_survey_name": "$[projectName] CSAT", + "csat_survey_question_1_headline": "How satisfied are you with your $[projectName] experience?", + "csat_survey_question_1_lower_label": "Extremely dissatisfied", + "csat_survey_question_1_upper_label": "Extremely satisfied", + "csat_survey_question_2_headline": "Lovely! Is there anything we can do to improve your experience?", + "csat_survey_question_2_placeholder": "Type your answer here...", + "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_description": "Create a survey without template.", + "custom_survey_name": "Start from scratch", + "custom_survey_question_1_headline": "What would you like to know?", + "custom_survey_question_1_placeholder": "Type your answer here...", + "customer_effort_score_description": "Determine how easy it is to use a feature.", + "customer_effort_score_name": "Customer Effort Score (CES)", + "customer_effort_score_question_1_headline": "$[projectName] makes it easy for me to [ADD GOAL]", + "customer_effort_score_question_1_lower_label": "Disagree strongly", + "customer_effort_score_question_1_upper_label": "Agree strongly", + "customer_effort_score_question_2_headline": "Thanks! How could we make it easier for you to [ADD GOAL]?", + "customer_effort_score_question_2_placeholder": "Type your answer here...", + "date": "Date", + "date_description": "Ask for a date selection", + "default_ending_card_button_label": "Create your own Survey", + "default_ending_card_headline": "Thank you!", + "default_ending_card_subheader": "We appreciate your feedback.", + "default_welcome_card_button_label": "Next", + "default_welcome_card_headline": "Welcome!", + "default_welcome_card_html": "Thanks for providing your feedback - let's go!", + "docs_feedback_description": "Measure how clear each page of your developer documentation is.", + "docs_feedback_name": "Docs Feedback", + "docs_feedback_question_1_choice_1": "Yes \uD83D\uDC4D", + "docs_feedback_question_1_choice_2": "No \uD83D\uDC4E", + "docs_feedback_question_1_headline": "Was this page helpful?", + "docs_feedback_question_2_headline": "Please elaborate:", + "docs_feedback_question_3_headline": "Page URL", + "earned_advocacy_score_description": "The EAS is a riff off the NPS but asking for actual past behaviour instead of lofty intentions.", + "earned_advocacy_score_name": "Earned Advocacy Score (EAS)", + "earned_advocacy_score_question_1_choice_1": "Yes", + "earned_advocacy_score_question_1_choice_2": "No", + "earned_advocacy_score_question_1_headline": "Have you actively recommended $[projectName] to others?", + "earned_advocacy_score_question_2_headline": "Why did you recommend us?", + "earned_advocacy_score_question_2_placeholder": "Type your answer here...", + "earned_advocacy_score_question_3_headline": "So sad. Why not?", + "earned_advocacy_score_question_3_placeholder": "Type your answer here...", + "earned_advocacy_score_question_4_choice_1": "Yes", + "earned_advocacy_score_question_4_choice_2": "No", + "earned_advocacy_score_question_4_headline": "Have you actively discouraged others from choosing $[projectName]?", + "earned_advocacy_score_question_5_headline": "What made you discourage them?", + "earned_advocacy_score_question_5_placeholder": "Type your answer here...", + "employee_satisfaction_description": "Gauge employee satisfaction and identify areas for improvement.", + "employee_satisfaction_name": "Employee Satisfaction", + "employee_satisfaction_question_1_headline": "How satisfied are you with your current role?", + "employee_satisfaction_question_1_lower_label": "Not satisfied", + "employee_satisfaction_question_1_upper_label": "Very satisfied", + "employee_satisfaction_question_2_choice_1": "Extremely meaningful", + "employee_satisfaction_question_2_choice_2": "Very meaningful", + "employee_satisfaction_question_2_choice_3": "Moderately meaningful", + "employee_satisfaction_question_2_choice_4": "Slightly meaningful", + "employee_satisfaction_question_2_choice_5": "Not at all meaningful", + "employee_satisfaction_question_2_headline": "How meaningful do you find your work?", + "employee_satisfaction_question_3_headline": "What do you enjoy most about working here?", + "employee_satisfaction_question_3_placeholder": "Type your answer here...", + "employee_satisfaction_question_5_headline": "Rate the support you receive from your manager.", + "employee_satisfaction_question_5_lower_label": "Poor", + "employee_satisfaction_question_5_upper_label": "Excellent", + "employee_satisfaction_question_6_headline": "What improvements would you suggest for our workplace?", + "employee_satisfaction_question_6_placeholder": "Type your answer here...", + "employee_satisfaction_question_7_choice_1": "Extremely likely", + "employee_satisfaction_question_7_choice_2": "Very likely", + "employee_satisfaction_question_7_choice_3": "Moderately likely", + "employee_satisfaction_question_7_choice_4": "Slightly likely", + "employee_satisfaction_question_7_choice_5": "Not at all likely", + "employee_satisfaction_question_7_headline": "How likely are you to recommend our company to a friend?", + "employee_well_being_description": "Assess your employee well-being through work-life balance, workload, and environment.", + "employee_well_being_name": "Employee Well-Being", + "employee_well_being_question_1_headline": "I feel that I have a good balance between my work and personal life.", + "employee_well_being_question_1_lower_label": "Very poor balance", + "employee_well_being_question_1_upper_label": "Excellent balance", + "employee_well_being_question_2_headline": "My workload is manageable, allowing me to stay productive without feeling overwhelmed.", + "employee_well_being_question_2_lower_label": "Overwhelming workload", + "employee_well_being_question_2_upper_label": "Perfectly manageable", + "employee_well_being_question_3_headline": "The work environment supports my physical and mental well-being.", + "employee_well_being_question_3_lower_label": "Not supportive", + "employee_well_being_question_3_upper_label": "Highly supportive", + "employee_well_being_question_4_headline": "What changes, if any, would improve your overall well-being at work?", + "employee_well_being_question_4_placeholder": "Type your answer here...", + "enps_survey_name": "eNPS Survey", + "enps_survey_question_1_headline": "How likely are you to recommend working at this company to a friend or colleague?", + "enps_survey_question_1_lower_label": "Not at all likely", + "enps_survey_question_1_upper_label": "Extremely likely", + "enps_survey_question_2_headline": "To help us improve, can you describe the reason(s) for your rating?", + "enps_survey_question_3_headline": "Any other comments, feedback, or concerns?", + "evaluate_a_product_idea_description": "Survey users about product or feature ideas. Get feedback rapidly.", + "evaluate_a_product_idea_name": "Evaluate a Product Idea", + "evaluate_a_product_idea_question_1_button_label": "Let's do it!", + "evaluate_a_product_idea_question_1_dismiss_button_label": "Skip", + "evaluate_a_product_idea_question_1_headline": "We love how you use $[projectName]! We'd love to pick your brain on a feature idea. Got a minute?", + "evaluate_a_product_idea_question_1_html": "

We respect your time and kept it short \uD83E\uDD38

", + "evaluate_a_product_idea_question_2_headline": "Thanks! How difficult or easy is it for you to [PROBLEM AREA] today?", + "evaluate_a_product_idea_question_2_lower_label": "Very difficult", + "evaluate_a_product_idea_question_2_upper_label": "Very easy", + "evaluate_a_product_idea_question_3_headline": "What's most difficult for you when it comes to [PROBLEM AREA]?", + "evaluate_a_product_idea_question_3_placeholder": "Type your answer here...", + "evaluate_a_product_idea_question_4_button_label": "Next", + "evaluate_a_product_idea_question_4_dismiss_button_label": "Skip", + "evaluate_a_product_idea_question_4_headline": "We're working on an idea to help with [PROBLEM AREA].", + "evaluate_a_product_idea_question_4_html": "

Insert concept brief here. Add necessary details but keep it concise and easy to understand.

", + "evaluate_a_product_idea_question_5_headline": "How valuable would this feature be to you?", + "evaluate_a_product_idea_question_5_lower_label": "Not valuable", + "evaluate_a_product_idea_question_5_upper_label": "Very valuable", + "evaluate_a_product_idea_question_6_headline": "Got it. Why wouldn't this feature be valuable to you?", + "evaluate_a_product_idea_question_6_placeholder": "Type your answer here...", + "evaluate_a_product_idea_question_7_headline": "What would be most valuable to you in this feature?", + "evaluate_a_product_idea_question_7_placeholder": "Type your answer here...", + "evaluate_a_product_idea_question_8_headline": "Anything else we should keep in mind?", + "evaluate_a_product_idea_question_8_placeholder": "Type your answer here...", + "evaluate_content_quality_description": "Measure if your content marketing pieces hit right.", + "evaluate_content_quality_name": "Evaluate Content Quality", + "evaluate_content_quality_question_1_headline": "How well did this article address what you were hoping to learn?", + "evaluate_content_quality_question_1_lower_label": "Not at all well", + "evaluate_content_quality_question_1_upper_label": "Extremely well", + "evaluate_content_quality_question_2_headline": "Hmpft! What were you hoping for?", + "evaluate_content_quality_question_2_placeholder": "Type your answer here...", + "evaluate_content_quality_question_3_headline": "Lovely! Is there anything else you would like us to cover?", + "evaluate_content_quality_question_3_placeholder": "Topics, trends, tutorials...", + "fake_door_follow_up_description": "Follow up with users who ran into one of your Fake Door experiments.", + "fake_door_follow_up_name": "Fake Door Follow-Up", + "fake_door_follow_up_question_1_headline": "How important is this feature for you?", + "fake_door_follow_up_question_1_lower_label": "Not important", + "fake_door_follow_up_question_1_upper_label": "Very important", + "fake_door_follow_up_question_2_choice_1": "Aspect 1", + "fake_door_follow_up_question_2_choice_2": "Aspect 2", + "fake_door_follow_up_question_2_choice_3": "Aspect 3", + "fake_door_follow_up_question_2_choice_4": "Aspect 4", + "fake_door_follow_up_question_2_headline": "What should be definitely include building this?", + "feature_chaser_description": "Follow up with users who just used a specific feature.", + "feature_chaser_name": "Feature Chaser", + "feature_chaser_question_1_headline": "How important is [ADD FEATURE] for you?", + "feature_chaser_question_1_lower_label": "Not important", + "feature_chaser_question_1_upper_label": "Very important", + "feature_chaser_question_2_choice_1": "Aspect 1", + "feature_chaser_question_2_choice_2": "Aspect 2", + "feature_chaser_question_2_choice_3": "Aspect 3", + "feature_chaser_question_2_choice_4": "Aspect 4", + "feature_chaser_question_2_headline": "Which aspect is most important?", + "feedback_box_description": "Give your users the chance to seamlessly share what's on their minds.", + "feedback_box_name": "Feedback Box", + "feedback_box_question_1_choice_1": "Bug report \uD83D\uDC1E", + "feedback_box_question_1_choice_2": "Feature Request \uD83D\uDCA1", + "feedback_box_question_1_headline": "What's on your mind, boss?", + "feedback_box_question_1_subheader": "Thanks for sharing. We'll get back to you asap.", + "feedback_box_question_2_headline": "What's broken?", + "feedback_box_question_2_subheader": "The more detail, the better :)", + "feedback_box_question_3_button_label": "Yes, notify me", + "feedback_box_question_3_dismiss_button_label": "No, thanks", + "feedback_box_question_3_headline": "Want to stay in the loop?", + "feedback_box_question_3_html": "

We will fix this as soon as possible. Do you want to be notified when we did?

", + "feedback_box_question_4_button_label": "Request feature", + "feedback_box_question_4_headline": "Lovely, tell us more!", + "feedback_box_question_4_placeholder": "Type your answer here...", + "feedback_box_question_4_subheader": "What problem do you want us to solve?", + "file_upload": "File Upload", + "file_upload_description": "Enable respondents to upload documents, images, or other files", + "finish": "Finish", + "follow_ups_modal_action_body": "

Hey \uD83D\uDC4B

Thanks for taking the time to respond, we will be in touch shortly.

Have a great day!

", + "free_text": "Free text", + "free_text_description": "Collect open-ended feedback", + "free_text_placeholder": "Type your answer here...", + "gauge_feature_satisfaction_description": "Evaluate the satisfaction of specific features of your product.", + "gauge_feature_satisfaction_name": "Gauge Feature Satisfaction", + "gauge_feature_satisfaction_question_1_headline": "How easy was it to achieve ... ?", + "gauge_feature_satisfaction_question_1_lower_label": "Not easy", + "gauge_feature_satisfaction_question_1_upper_label": "Very easy", + "gauge_feature_satisfaction_question_2_headline": "What is one thing we could do better?", + "identify_customer_goals_description": "Better understand if your messaging creates the right expectations of the value your product provides.", + "identify_customer_goals_name": "Identify Customer Goals", + "identify_sign_up_barriers_description": "Offer a discount to gather insights about sign up barriers.", + "identify_sign_up_barriers_name": "Identify Sign Up Barriers", + "identify_sign_up_barriers_question_1_button_label": "Get 10% discount", + "identify_sign_up_barriers_question_1_dismiss_button_label": "No, thanks", + "identify_sign_up_barriers_question_1_headline": "Answer this short survey, get 10% off!", + "identify_sign_up_barriers_question_1_html": "You seem to be considering signing up. Answer four questions and get 10% on any plan.", + "identify_sign_up_barriers_question_2_headline": "How likely are you to sign up for $[projectName]?", + "identify_sign_up_barriers_question_2_lower_label": "Not at all likely", + "identify_sign_up_barriers_question_2_upper_label": "Very likely", + "identify_sign_up_barriers_question_3_choice_1_label": "May not have what I'm looking for", + "identify_sign_up_barriers_question_3_choice_2_label": "Still comparing options", + "identify_sign_up_barriers_question_3_choice_3_label": "Seems complicated", + "identify_sign_up_barriers_question_3_choice_4_label": "Pricing is a concern", + "identify_sign_up_barriers_question_3_choice_5_label": "Something else", + "identify_sign_up_barriers_question_3_headline": "What is holding you back from trying $[projectName]?", + "identify_sign_up_barriers_question_4_headline": "What do you need but $[projectName] does not offer?", + "identify_sign_up_barriers_question_4_placeholder": "Type your answer here...", + "identify_sign_up_barriers_question_5_headline": "What options are you looking at?", + "identify_sign_up_barriers_question_5_placeholder": "Type your answer here...", + "identify_sign_up_barriers_question_6_headline": "What seems complicated to you?", + "identify_sign_up_barriers_question_6_placeholder": "Type your answer here...", + "identify_sign_up_barriers_question_7_headline": "What are you concerned about regarding pricing?", + "identify_sign_up_barriers_question_7_placeholder": "Type your answer here...", + "identify_sign_up_barriers_question_8_headline": "Please explain:", + "identify_sign_up_barriers_question_8_placeholder": "Type your answer here...", + "identify_sign_up_barriers_question_9_button_label": "Sign Up", + "identify_sign_up_barriers_question_9_dismiss_button_label": "Skip for now", + "identify_sign_up_barriers_question_9_headline": "Thanks! Here is your code: SIGNUPNOW10", + "identify_sign_up_barriers_question_9_html": "

Thanks a lot for taking the time to share feedback \uD83D\uDE4F

", + "identify_sign_up_barriers_with_project_name": "$[projectName] Sign Up Barriers", + "identify_upsell_opportunities_description": "Find out how much time your product saves your user. Use it to upsell.", + "identify_upsell_opportunities_name": "Identify Upsell Opportunities", + "identify_upsell_opportunities_question_1_choice_1": "Less than 1 hour", + "identify_upsell_opportunities_question_1_choice_2": "1 to 2 hours", + "identify_upsell_opportunities_question_1_choice_3": "3 to 5 hours", + "identify_upsell_opportunities_question_1_choice_4": "5+ hours", + "identify_upsell_opportunities_question_1_headline": "How many hours does your team save per week by using $[projectName]?", + "improve_activation_rate_description": "Identify weaknesses in your onboarding flow to increase user activation.", + "improve_activation_rate_name": "Improve Activation Rate", + "improve_activation_rate_question_1_choice_1": "Didn't seem useful to me", + "improve_activation_rate_question_1_choice_2": "Difficult to set up or use", + "improve_activation_rate_question_1_choice_3": "Lacked features/functionality", + "improve_activation_rate_question_1_choice_4": "Just haven't had the time", + "improve_activation_rate_question_1_choice_5": "Something else", + "improve_activation_rate_question_1_headline": "What's the main reason why you haven't finished setting up $[projectName]?", + "improve_activation_rate_question_2_headline": "What made you think $[projectName] wouldn't be useful?", + "improve_activation_rate_question_2_placeholder": "Type your answer here...", + "improve_activation_rate_question_3_headline": "What was difficult about setting up or using $[projectName]?", + "improve_activation_rate_question_3_placeholder": "Type your answer here...", + "improve_activation_rate_question_4_headline": "What features or functionality were missing?", + "improve_activation_rate_question_4_placeholder": "Type your answer here...", + "improve_activation_rate_question_5_headline": "How could we make it easier for you to get started?", + "improve_activation_rate_question_5_placeholder": "Type your answer here...", + "improve_activation_rate_question_6_headline": "What was it? Please explain:", + "improve_activation_rate_question_6_placeholder": "Type your answer here...", + "improve_activation_rate_question_6_subheader": "We're eager to fix it asap.", + "improve_newsletter_content_description": "Find out how your subscribers like your newsletter content.", + "improve_newsletter_content_name": "Improve Newsletter Content", + "improve_newsletter_content_question_1_headline": "How would you rate this weeks newsletter?", + "improve_newsletter_content_question_1_lower_label": "Meh", + "improve_newsletter_content_question_1_upper_label": "Great", + "improve_newsletter_content_question_2_headline": "What would have made this weeks newsletter more helpful?", + "improve_newsletter_content_question_2_placeholder": "Type your answer here...", + "improve_newsletter_content_question_3_button_label": "Happy to help!", + "improve_newsletter_content_question_3_dismiss_button_label": "Find your own friends", + "improve_newsletter_content_question_3_headline": "Thanks! ❤️ Spread the love with ONE friend.", + "improve_newsletter_content_question_3_html": "

Who thinks like you? You'd do us a huge favor if you'd share this weeks episode with your brain friend!

", + "improve_trial_conversion_description": "Find out why people stopped their trial. These insights help you improve your funnel.", + "improve_trial_conversion_name": "Improve Trial Conversion", + "improve_trial_conversion_question_1_choice_1": "I didn't get much value out of it", + "improve_trial_conversion_question_1_choice_2": "I expected something else", + "improve_trial_conversion_question_1_choice_3": "It's too expensive for what it does", + "improve_trial_conversion_question_1_choice_4": "I am missing a feature", + "improve_trial_conversion_question_1_choice_5": "I was just looking around", + "improve_trial_conversion_question_1_headline": "Why did you stop your trial?", + "improve_trial_conversion_question_1_subheader": "Help us understand you better:", + "improve_trial_conversion_question_2_button_label": "Next", + "improve_trial_conversion_question_2_headline": "Sorry to hear. What was the biggest problem using $[projectName]?", + "improve_trial_conversion_question_4_button_label": "Get 20% off", + "improve_trial_conversion_question_4_dismiss_button_label": "Skip", + "improve_trial_conversion_question_4_headline": "Sorry to hear! Get 20% off the first year.", + "improve_trial_conversion_question_4_html": "

We're happy to offer you a 20% discount on a yearly plan.

", + "improve_trial_conversion_question_5_button_label": "Next", + "improve_trial_conversion_question_5_headline": "What would you like to achieve?", + "improve_trial_conversion_question_5_subheader": "Please select one of the following options:", + "improve_trial_conversion_question_6_headline": "How are you solving your problem now?", + "improve_trial_conversion_question_6_subheader": "Please name alternative solutions:", + "integration_setup_survey_description": "Evaluate how easily users can add integrations to your product. Find blind spots.", + "integration_setup_survey_name": "Integration Usage Survey", + "integration_setup_survey_question_1_headline": "How easy was it to set this integration up?", + "integration_setup_survey_question_1_lower_label": "Not easy", + "integration_setup_survey_question_1_upper_label": "Very easy", + "integration_setup_survey_question_2_headline": "Why was it hard?", + "integration_setup_survey_question_2_placeholder": "Type your answer here...", + "integration_setup_survey_question_3_headline": "What other tools would you like to use with $[projectName]?", + "integration_setup_survey_question_3_subheader": "We keep building integrations, yours can be next:", + "interview_prompt_description": "Invite a specific subset of your users to schedule an interview with your product team.", + "interview_prompt_name": "Interview Prompt", + "interview_prompt_question_1_button_label": "Book slot", + "interview_prompt_question_1_headline": "Do you have 15 min to talk to us? \uD83D\uDE4F", + "interview_prompt_question_1_html": "You're one of our power users. We would love to interview you briefly!", + "long_term_retention_check_in_description": "Gauge long-term user satisfaction, loyalty, and areas for improvement to retain loyal users.", + "long_term_retention_check_in_name": "Long-Term Retention Check-In", + "long_term_retention_check_in_question_10_headline": "Any additional feedback or comments?", + "long_term_retention_check_in_question_10_placeholder": "Share any thoughts or feedback that might help us improve...", + "long_term_retention_check_in_question_1_headline": "How satisfied are you with $[projectName] overall?", + "long_term_retention_check_in_question_1_lower_label": "Not satisfied", + "long_term_retention_check_in_question_1_upper_label": "Very satisfied", + "long_term_retention_check_in_question_2_headline": "What do you find most valuable about $[projectName]?", + "long_term_retention_check_in_question_2_placeholder": "Describe the feature or benefit you value most...", + "long_term_retention_check_in_question_3_choice_1": "Features", + "long_term_retention_check_in_question_3_choice_2": "Customer support", + "long_term_retention_check_in_question_3_choice_3": "User experience", + "long_term_retention_check_in_question_3_choice_4": "Pricing", + "long_term_retention_check_in_question_3_choice_5": "Reliability and uptime", + "long_term_retention_check_in_question_3_headline": "Which aspect of $[projectName] do you find most essential to your experience?", + "long_term_retention_check_in_question_4_headline": "How well does $[projectName] meet your expectations?", + "long_term_retention_check_in_question_4_lower_label": "Falls short", + "long_term_retention_check_in_question_4_upper_label": "Exceeds expectations", + "long_term_retention_check_in_question_5_headline": "What challenges or frustrations have you faced while using $[projectName]?", + "long_term_retention_check_in_question_5_placeholder": "Describe any challenges or improvements you’d like to see...", + "long_term_retention_check_in_question_6_headline": "How likely are you to recommend $[projectName] to a friend or colleague?", + "long_term_retention_check_in_question_6_lower_label": "Not likely", + "long_term_retention_check_in_question_6_upper_label": "Very likely", + "long_term_retention_check_in_question_7_choice_1": "New features and improvements", + "long_term_retention_check_in_question_7_choice_2": "Enhanced customer support", + "long_term_retention_check_in_question_7_choice_3": "Better pricing options", + "long_term_retention_check_in_question_7_choice_4": "More integrations", + "long_term_retention_check_in_question_7_choice_5": "User experience refinements", + "long_term_retention_check_in_question_7_headline": "What would make you more likely to remain a long-term user?", + "long_term_retention_check_in_question_8_headline": "If you could change one thing about $[projectName], what would it be?", + "long_term_retention_check_in_question_8_placeholder": "Share any changes or features you wish we’d consider...", + "long_term_retention_check_in_question_9_headline": "How happy are you with our product updates and frequency?", + "long_term_retention_check_in_question_9_lower_label": "Not happy", + "long_term_retention_check_in_question_9_upper_label": "Very happy", + "market_attribution_description": "Learn how users first heard about your product.", + "market_attribution_name": "Marketing Attribution", + "market_attribution_question_1_choice_1": "Recommendation", + "market_attribution_question_1_choice_2": "Social Media", + "market_attribution_question_1_choice_3": "Ads", + "market_attribution_question_1_choice_4": "Google Search", + "market_attribution_question_1_choice_5": "In a Podcast", + "market_attribution_question_1_headline": "How did you hear about us first?", + "market_attribution_question_1_subheader": "Please select one of the following options:", + "market_site_clarity_description": "Identify users dropping off your marketing site. Improve your messaging.", + "market_site_clarity_name": "Marketing Site Clarity", + "market_site_clarity_question_1_choice_1": "Yes, totally", + "market_site_clarity_question_1_choice_2": "Kind of...", + "market_site_clarity_question_1_choice_3": "No, not at all", + "market_site_clarity_question_1_headline": "Do you have all the info you need to give $[projectName] a try?", + "market_site_clarity_question_2_headline": "What's missing or unclear to you about $[projectName]?", + "market_site_clarity_question_3_button_label": "Get discount", + "market_site_clarity_question_3_headline": "Thanks for your answer! Get 25% off your first 6 months:", + "matrix": "Matrix", + "matrix_description": "Create a grid to rate multiple items on the same set of criteria", + "measure_search_experience_description": "Measure how relevant your search results are.", + "measure_search_experience_name": "Measure Search Experience", + "measure_search_experience_question_1_headline": "How relevant are these search results?", + "measure_search_experience_question_1_lower_label": "Not at all relevant", + "measure_search_experience_question_1_upper_label": "Very relevant", + "measure_search_experience_question_2_headline": "Ugh! What makes the results irrelevant for you?", + "measure_search_experience_question_2_placeholder": "Type your answer here...", + "measure_search_experience_question_3_headline": "Lovely! Is there anything we can do to improve your experience?", + "measure_search_experience_question_3_placeholder": "Type your answer here...", + "measure_task_accomplishment_description": "See if people get their 'Job To Be Done' done. Successful people are better customers.", + "measure_task_accomplishment_name": "Measure Task Accomplishment", + "measure_task_accomplishment_question_1_headline": "Were you able to accomplish what you came here to do today?", + "measure_task_accomplishment_question_1_option_1_label": "Yes", + "measure_task_accomplishment_question_1_option_2_label": "Working on it, boss", + "measure_task_accomplishment_question_1_option_3_label": "No", + "measure_task_accomplishment_question_2_headline": "How easy was it to achieve your goal?", + "measure_task_accomplishment_question_2_lower_label": "Very difficult", + "measure_task_accomplishment_question_2_upper_label": "Very easy", + "measure_task_accomplishment_question_3_headline": "What made it hard?", + "measure_task_accomplishment_question_3_placeholder": "Type your answer here...", + "measure_task_accomplishment_question_4_button_label": "Send", + "measure_task_accomplishment_question_4_headline": "Great! What did you come here to do today?", + "measure_task_accomplishment_question_5_button_label": "Send", + "measure_task_accomplishment_question_5_headline": "What stopped you?", + "measure_task_accomplishment_question_5_placeholder": "Type your answer here...", + "multi_select": "Multi-Select", + "multi_select_description": "Ask respondents to choose one or more options", + "new_integration_survey_description": "Find out which integrations your users would like to see next.", + "new_integration_survey_name": "New Integration Survey", + "new_integration_survey_question_1_choice_1": "PostHog", + "new_integration_survey_question_1_choice_2": "Segment", + "new_integration_survey_question_1_choice_3": "Hubspot", + "new_integration_survey_question_1_choice_4": "Twilio", + "new_integration_survey_question_1_choice_5": "Other", + "new_integration_survey_question_1_headline": "Which other tools are you using?", + "next": "Next", + "nps": "Net Promoter Score (NPS)", + "nps_description": "Measure Net-Promoter-Score (0-10)", + "nps_lower_label": "Not at all likely", + "nps_name": "Net Promoter Score (NPS)", + "nps_question_1_headline": "How likely are you to recommend $[projectName] to a friend or colleague?", + "nps_question_1_lower_label": "Not likely", + "nps_question_1_upper_label": "Very likely", + "nps_question_2_headline": "What made you give that rating?", + "nps_survey_name": "NPS Survey", + "nps_survey_question_1_headline": "How likely are you to recommend $[projectName] to a friend or colleague?", + "nps_survey_question_1_lower_label": "Not at all likely", + "nps_survey_question_1_upper_label": "Extremely likely", + "nps_survey_question_2_headline": "To help us improve, can you describe the reason(s) for your rating?", + "nps_survey_question_3_headline": "Any other comments, feedback, or concerns?", + "nps_upper_label": "Extremely likely", + "onboarding_segmentation": "Onboarding Segmentation", + "onboarding_segmentation_description": "Learn more about who signed up to your product and why.", + "onboarding_segmentation_question_1_choice_1": "Founder", + "onboarding_segmentation_question_1_choice_2": "Executive", + "onboarding_segmentation_question_1_choice_3": "Product Manager", + "onboarding_segmentation_question_1_choice_4": "Product Owner", + "onboarding_segmentation_question_1_choice_5": "Software Engineer", + "onboarding_segmentation_question_1_headline": "What is your role?", + "onboarding_segmentation_question_1_subheader": "Please select one of the following options:", + "onboarding_segmentation_question_2_choice_1": "only me", + "onboarding_segmentation_question_2_choice_2": "1-5 employees", + "onboarding_segmentation_question_2_choice_3": "6-10 employees", + "onboarding_segmentation_question_2_choice_4": "11-100 employees", + "onboarding_segmentation_question_2_choice_5": "over 100 employees", + "onboarding_segmentation_question_2_headline": "What's your company size?", + "onboarding_segmentation_question_2_subheader": "Please select one of the following options:", + "onboarding_segmentation_question_3_choice_1": "Recommendation", + "onboarding_segmentation_question_3_choice_2": "Social Media", + "onboarding_segmentation_question_3_choice_3": "Ads", + "onboarding_segmentation_question_3_choice_4": "Google Search", + "onboarding_segmentation_question_3_choice_5": "In a Podcast", + "onboarding_segmentation_question_3_headline": "How did you hear about us first?", + "onboarding_segmentation_question_3_subheader": "Please select one of the following options:", + "picture_selection": "Picture Selection", + "picture_selection_description": "Ask respondents to choose one or more images", + "preview_survey_ending_card_description": "Please continue your onboarding.", + "preview_survey_ending_card_headline": "You did it!", + "preview_survey_name": "New Survey", + "preview_survey_question_1_headline": "How would you rate {projectName}?", + "preview_survey_question_1_lower_label": "Not good", + "preview_survey_question_1_subheader": "This is a survey preview.", + "preview_survey_question_1_upper_label": "Very good", + "preview_survey_question_2_back_button_label": "Back", + "preview_survey_question_2_choice_1_label": "Yes, keep me informed.", + "preview_survey_question_2_choice_2_label": "No, thank you!", + "preview_survey_question_2_headline": "What to stay in the loop?", + "preview_survey_welcome_card_headline": "Welcome!", + "preview_survey_welcome_card_html": "Thanks for providing your feedback - let's go!", + "prioritize_features_description": "Identify features your users need most and least.", + "prioritize_features_name": "Prioritize Features", + "prioritize_features_question_1_choice_1": "Feature 1", + "prioritize_features_question_1_choice_2": "Feature 2", + "prioritize_features_question_1_choice_3": "Feature 3", + "prioritize_features_question_1_choice_4": "Other", + "prioritize_features_question_1_headline": "Which of these features would be MOST valuable to you?", + "prioritize_features_question_2_choice_1": "Feature 1", + "prioritize_features_question_2_choice_2": "Feature 2", + "prioritize_features_question_2_choice_3": "Feature 3", + "prioritize_features_question_2_headline": "Which of these features would be LEAST valuable to you?", + "prioritize_features_question_3_headline": "How else could we improve you experience with $[projectName]?", + "prioritize_features_question_3_placeholder": "Type your answer here...", + "product_market_fit_short_description": "Measure PMF by assessing how disappointed users would be if your product disappeared.", + "product_market_fit_short_name": "Product Market Fit Survey (Short)", + "product_market_fit_short_question_1_choice_1": "Not at all disappointed", + "product_market_fit_short_question_1_choice_2": "Somewhat disappointed", + "product_market_fit_short_question_1_choice_3": "Very disappointed", + "product_market_fit_short_question_1_headline": "How disappointed would you be if you could no longer use $[projectName]?", + "product_market_fit_short_question_1_subheader": "Please select one of the following options:", + "product_market_fit_short_question_2_headline": "How can we improve $[projectName] for you?", + "product_market_fit_short_question_2_subheader": "Please be as specific as possible.", + "product_market_fit_superhuman": "Product Market Fit (Superhuman)", + "product_market_fit_superhuman_description": "Measure PMF by assessing how disappointed users would be if your product disappeared.", + "product_market_fit_superhuman_question_1_button_label": "Happy to help!", + "product_market_fit_superhuman_question_1_dismiss_button_label": "No, thanks.", + "product_market_fit_superhuman_question_1_headline": "You are one of our power users! Do you have 5 minutes?", + "product_market_fit_superhuman_question_1_html": "

We would love to understand your user experience better. Sharing your insight helps a lot.

", + "product_market_fit_superhuman_question_2_choice_1": "Not at all disappointed", + "product_market_fit_superhuman_question_2_choice_2": "Somewhat disappointed", + "product_market_fit_superhuman_question_2_choice_3": "Very disappointed", + "product_market_fit_superhuman_question_2_headline": "How disappointed would you be if you could no longer use $[projectName]?", + "product_market_fit_superhuman_question_2_subheader": "Please select one of the following options:", + "product_market_fit_superhuman_question_3_choice_1": "Founder", + "product_market_fit_superhuman_question_3_choice_2": "Executive", + "product_market_fit_superhuman_question_3_choice_3": "Product Manager", + "product_market_fit_superhuman_question_3_choice_4": "Product Owner", + "product_market_fit_superhuman_question_3_choice_5": "Software Engineer", + "product_market_fit_superhuman_question_3_headline": "What is your role?", + "product_market_fit_superhuman_question_3_subheader": "Please select one of the following options:", + "product_market_fit_superhuman_question_4_headline": "What type of people do you think would most benefit from $[projectName]?", + "product_market_fit_superhuman_question_5_headline": "What is the main benefit you receive from $[projectName]?", + "product_market_fit_superhuman_question_6_headline": "How can we improve $[projectName] for you?", + "product_market_fit_superhuman_question_6_subheader": "Please be as specific as possible.", + "professional_development_growth_survey_description": "Assess employee satisfaction with professional growth and development opportunities.", + "professional_development_growth_survey_name": "Professional Development Growth Survey", + "professional_development_growth_survey_question_1_headline": "I feel that I have opportunities to grow and develop my skills at work.", + "professional_development_growth_survey_question_1_lower_label": "No growth opportunities", + "professional_development_growth_survey_question_1_upper_label": "Many growth opportunities", + "professional_development_growth_survey_question_2_headline": "I have enough autonomy to make decisions about how I do my job.", + "professional_development_growth_survey_question_2_lower_label": "No autonomy", + "professional_development_growth_survey_question_2_upper_label": "Complete autonomy", + "professional_development_growth_survey_question_3_headline": "My goals at work are clear and aligned with my development.", + "professional_development_growth_survey_question_3_lower_label": "Unclear goals", + "professional_development_growth_survey_question_3_upper_label": "Clear and aligned goals", + "professional_development_growth_survey_question_4_headline": "What could be improved to support your professional growth?", + "professional_development_growth_survey_question_4_placeholder": "Type your answer here...", + "professional_development_survey_description": "Assess employee satisfaction with professional growth and development opportunities.", + "professional_development_survey_name": "Professional Development Survey", + "professional_development_survey_question_1_choice_1": "Yes", + "professional_development_survey_question_1_choice_2": "No", + "professional_development_survey_question_1_headline": "Are you interested in professional development activities?", + "professional_development_survey_question_2_choice_1": "Networking events", + "professional_development_survey_question_2_choice_2": "Conferences or seminars", + "professional_development_survey_question_2_choice_3": "Courses or workshops", + "professional_development_survey_question_2_choice_4": "Mentoring", + "professional_development_survey_question_2_choice_5": "Individual research", + "professional_development_survey_question_2_choice_6": "Other", + "professional_development_survey_question_2_headline": "What types of professional development activities do you think would be most valuable for your growth?", + "professional_development_survey_question_2_subheader": "Select all that apply", + "professional_development_survey_question_3_choice_1": "Yes", + "professional_development_survey_question_3_choice_2": "No", + "professional_development_survey_question_3_headline": "Have you dedicated time to your professional development in the past?", + "professional_development_survey_question_4_headline": "How supported do you feel in your workplace when it comes to pursuing professional development?", + "professional_development_survey_question_4_lower_label": "Not at all supported", + "professional_development_survey_question_4_upper_label": "Extremely supported", + "professional_development_survey_question_5_choice_1": "For my own knowledge", + "professional_development_survey_question_5_choice_2": "To gain more responsibilities", + "professional_development_survey_question_5_choice_3": "Improving my skills", + "professional_development_survey_question_5_choice_4": "Advancing on my current career path", + "professional_development_survey_question_5_choice_5": "Looking for a new job", + "professional_development_survey_question_5_choice_6": "Other", + "professional_development_survey_question_5_headline": "What are your main reasons for wanting to spend time on professional development?", + "ranking": "Ranking", + "ranking_description": "Ask respondents to order items by preference or importance", + "rate_checkout_experience_description": "Let customers rate the checkout experience to tweak conversion.", + "rate_checkout_experience_name": "Rate Checkout Experience", + "rate_checkout_experience_question_1_headline": "How easy or difficult was it to complete the checkout?", + "rate_checkout_experience_question_1_lower_label": "Very difficult", + "rate_checkout_experience_question_1_upper_label": "Very easy", + "rate_checkout_experience_question_2_headline": "Sorry about that! What would have made it easier for you?", + "rate_checkout_experience_question_2_placeholder": "Type your answer here...", + "rate_checkout_experience_question_3_headline": "Lovely! Is there anything we can do to improve your experience?", + "rate_checkout_experience_question_3_placeholder": "Type your answer here...", + "rating": "Rating", + "rating_description": "Ask respondents for a rating (stars, smileys, numbers)", + "rating_lower_label": "Not good", + "rating_upper_label": "Very good", + "recognition_and_reward_survey_description": "Evaluate employee satisfaction with recognition, rewards, leadership support, and freedom of expression.", + "recognition_and_reward_survey_name": "Recognition and Reward", + "recognition_and_reward_survey_question_1_headline": "When I perform well, my contributions are recognized by the organization.", + "recognition_and_reward_survey_question_1_lower_label": "Not recognized at all", + "recognition_and_reward_survey_question_1_upper_label": "Highly recognized", + "recognition_and_reward_survey_question_2_headline": "I feel fairly rewarded for the work I do.", + "recognition_and_reward_survey_question_2_lower_label": "Not fairly rewarded", + "recognition_and_reward_survey_question_2_upper_label": "Very fairly rewarded", + "recognition_and_reward_survey_question_3_headline": "I feel comfortable sharing my opinions openly at work.", + "recognition_and_reward_survey_question_3_lower_label": "Not comfortable", + "recognition_and_reward_survey_question_3_upper_label": "Very comfortable", + "recognition_and_reward_survey_question_4_headline": "How could the organization improve recognition and rewards?", + "recognition_and_reward_survey_question_4_placeholder": "Type your answer here...", + "review_prompt_description": "Invite users who love your product to review it publicly.", + "review_prompt_name": "Review Prompt", + "review_prompt_question_1_headline": "How do you like $[projectName]?", + "review_prompt_question_1_lower_label": "Not good", + "review_prompt_question_1_upper_label": "Very satisfied", + "review_prompt_question_2_button_label": "Write review", + "review_prompt_question_2_headline": "Happy to hear \uD83D\uDE4F Please write a review for us!", + "review_prompt_question_2_html": "

This helps us a lot.

", + "review_prompt_question_3_button_label": "Send", + "review_prompt_question_3_headline": "Sorry to hear! What is ONE thing we can do better?", + "review_prompt_question_3_placeholder": "Type your answer here...", + "review_prompt_question_3_subheader": "Help us improve your experience.", + "schedule_a_meeting": "Schedule a meeting", + "schedule_a_meeting_description": "Ask respondents to book a time slot for meetings or calls", + "single_select": "Single-Select", + "single_select_description": "Offer a list of options (choose one)", + "site_abandonment_survey": "Site Abandonment Survey", + "site_abandonment_survey_description": "Understand the reasons behind site abandonment in your web shop.", + "site_abandonment_survey_question_1_html": "

We noticed you're leaving our site without making a purchase. We would love to understand why.

", + "site_abandonment_survey_question_2_button_label": "Sure!", + "site_abandonment_survey_question_2_dismiss_button_label": "No, thanks.", + "site_abandonment_survey_question_2_headline": "Do you have a minute?", + "site_abandonment_survey_question_3_choice_1": "Can't find what I am looking for", + "site_abandonment_survey_question_3_choice_2": "Found a better site", + "site_abandonment_survey_question_3_choice_3": "Site is too slow", + "site_abandonment_survey_question_3_choice_4": "Just browsing", + "site_abandonment_survey_question_3_choice_5": "Found a better price elsewhere", + "site_abandonment_survey_question_3_choice_6": "Other", + "site_abandonment_survey_question_3_headline": "What's the primary reason you're leaving our site?", + "site_abandonment_survey_question_3_subheader": "Please select one of the following options:", + "site_abandonment_survey_question_4_headline": "Please elaborate on your reason for leaving the site:", + "site_abandonment_survey_question_5_headline": "How would you rate your overall experience on our site?", + "site_abandonment_survey_question_5_lower_label": "Very dissatisfied", + "site_abandonment_survey_question_5_upper_label": "Very satisfied", + "site_abandonment_survey_question_6_choice_1": "Faster loading times", + "site_abandonment_survey_question_6_choice_2": "Better product search functionality", + "site_abandonment_survey_question_6_choice_3": "More product variety", + "site_abandonment_survey_question_6_choice_4": "Improved site design", + "site_abandonment_survey_question_6_choice_5": "More customer reviews", + "site_abandonment_survey_question_6_choice_6": "Other", + "site_abandonment_survey_question_6_headline": "What improvements would encourage you to stay longer on our site?", + "site_abandonment_survey_question_6_subheader": "Please select all that apply:", + "site_abandonment_survey_question_7_headline": "Would you like to receive updates about new products and promotions?", + "site_abandonment_survey_question_7_label": "Yes, please reach out.", + "site_abandonment_survey_question_8_headline": "Please share your email address:", + "site_abandonment_survey_question_9_headline": "Any additional comments or suggestions?", + "skip": "Skip", + "smileys_survey_name": "Smileys Survey", + "smileys_survey_question_1_headline": "How do you like $[projectName]?", + "smileys_survey_question_1_lower_label": "Not good", + "smileys_survey_question_1_upper_label": "Very satisfied", + "smileys_survey_question_2_button_label": "Write review", + "smileys_survey_question_2_headline": "Happy to hear \uD83D\uDE4F Please write a review for us!", + "smileys_survey_question_2_html": "

This helps us a lot.

", + "smileys_survey_question_3_button_label": "Send", + "smileys_survey_question_3_headline": "Sorry to hear! What is ONE thing we can do better?", + "smileys_survey_question_3_placeholder": "Type your answer here...", + "smileys_survey_question_3_subheader": "Help us improve your experience.", + "star_rating_survey_name": "$[projectName]'s Rating Survey", + "star_rating_survey_question_1_headline": "How do you like $[projectName]?", + "star_rating_survey_question_1_lower_label": "Extremely dissatisfied", + "star_rating_survey_question_1_upper_label": "Extremely satisfied", + "star_rating_survey_question_2_button_label": "Write review", + "star_rating_survey_question_2_headline": "Happy to hear \uD83D\uDE4F Please write a review for us!", + "star_rating_survey_question_2_html": "

This helps us a lot.

", + "star_rating_survey_question_3_button_label": "Send", + "star_rating_survey_question_3_headline": "Sorry to hear! What is ONE thing we can do better?", + "star_rating_survey_question_3_placeholder": "Type your answer here...", + "star_rating_survey_question_3_subheader": "Help us improve your experience.", + "statement_call_to_action": "Statement (Call to Action)", + "supportive_work_culture_survey_description": "Assess employee perceptions of leadership support, communication, and the overall work environment.", + "supportive_work_culture_survey_name": "Supportive Work Culture", + "supportive_work_culture_survey_question_1_headline": "My manager provides me with the support I need to complete my work.", + "supportive_work_culture_survey_question_1_lower_label": "Not supported", + "supportive_work_culture_survey_question_1_upper_label": "Highly supported", + "supportive_work_culture_survey_question_2_headline": "Communication within the organization is open and effective.", + "supportive_work_culture_survey_question_2_lower_label": "Poor communication", + "supportive_work_culture_survey_question_2_upper_label": "Excellent communication", + "supportive_work_culture_survey_question_3_headline": "The work environment is positive and supports my well-being.", + "supportive_work_culture_survey_question_3_lower_label": "Not supportive", + "supportive_work_culture_survey_question_3_upper_label": "Very supportive", + "supportive_work_culture_survey_question_4_headline": "How could the work culture be improved to better support you?", + "supportive_work_culture_survey_question_4_placeholder": "Type your answer here...", + "uncover_strengths_and_weaknesses_description": "Find out what users like and don't like about your product or offering.", + "uncover_strengths_and_weaknesses_name": "Uncover Strengths & Weaknesses", + "uncover_strengths_and_weaknesses_question_1_choice_1": "Ease of use", + "uncover_strengths_and_weaknesses_question_1_choice_2": "Good value for money", + "uncover_strengths_and_weaknesses_question_1_choice_3": "It's open-source", + "uncover_strengths_and_weaknesses_question_1_choice_4": "The founders are cute", + "uncover_strengths_and_weaknesses_question_1_choice_5": "Other", + "uncover_strengths_and_weaknesses_question_1_headline": "What do you value most about $[projectName]?", + "uncover_strengths_and_weaknesses_question_2_choice_1": "Documentation", + "uncover_strengths_and_weaknesses_question_2_choice_2": "Customizability", + "uncover_strengths_and_weaknesses_question_2_choice_3": "Pricing", + "uncover_strengths_and_weaknesses_question_2_choice_4": "Other", + "uncover_strengths_and_weaknesses_question_2_headline": "What should we improve on?", + "uncover_strengths_and_weaknesses_question_2_subheader": "Please select one of the following options:", + "uncover_strengths_and_weaknesses_question_3_headline": "Would you like to add something?", + "uncover_strengths_and_weaknesses_question_3_subheader": "Feel free to speak your mind, we do too.", + "understand_low_engagement_description": "Identify reasons for low engagement to improve user adoption.", + "understand_low_engagement_name": "Understand Low Engagement", + "understand_low_engagement_question_1_choice_1": "Difficult to use", + "understand_low_engagement_question_1_choice_2": "Found a better alternative", + "understand_low_engagement_question_1_choice_3": "Just haven't had the time", + "understand_low_engagement_question_1_choice_4": "Lacked features I need", + "understand_low_engagement_question_1_choice_5": "Other", + "understand_low_engagement_question_1_headline": "What's the main reason you haven't been back to $[projectName] recently?", + "understand_low_engagement_question_2_headline": "What's difficult about using $[projectName]?", + "understand_low_engagement_question_2_placeholder": "Type your answer here...", + "understand_low_engagement_question_3_headline": "Got it. Which alternative are you using instead?", + "understand_low_engagement_question_3_placeholder": "Type your answer here...", + "understand_low_engagement_question_4_headline": "Got it. How could we make it easier for you to get started?", + "understand_low_engagement_question_4_placeholder": "Type your answer here...", + "understand_low_engagement_question_5_headline": "Got it. What features or functionality were missing?", + "understand_low_engagement_question_5_placeholder": "Type your answer here...", + "understand_low_engagement_question_6_headline": "Please add more details:", + "understand_low_engagement_question_6_placeholder": "Type your answer here...", + "understand_purchase_intention_description": "Find out how close your visitors are to buy or subscribe.", + "understand_purchase_intention_name": "Understand Purchase Intention", + "understand_purchase_intention_question_1_headline": "How likely are you to shop from us today?", + "understand_purchase_intention_question_1_lower_label": "Not at all likely", + "understand_purchase_intention_question_1_upper_label": "Extremely likely", + "understand_purchase_intention_question_2_headline": "Got it. What's your primary reason for visiting today?", + "understand_purchase_intention_question_2_placeholder": "Type your answer here...", + "understand_purchase_intention_question_3_headline": "What, if anything, is holding you back from making a purchase today?", + "understand_purchase_intention_question_3_placeholder": "Type your answer here..." + } +} diff --git a/apps/web/lib/messages/fr-FR.json b/apps/web/lib/messages/fr-FR.json new file mode 100644 index 0000000000..a7b915892d --- /dev/null +++ b/apps/web/lib/messages/fr-FR.json @@ -0,0 +1,2845 @@ +{ + "auth": { + "continue_with_azure": "Continuer avec Azure", + "continue_with_email": "Continuer avec l'e-mail", + "continue_with_github": "Continuer avec GitHub", + "continue_with_google": "Continuer avec Google", + "continue_with_oidc": "Continuer avec {oidcDisplayName}", + "continue_with_openid": "Continuer avec OpenID", + "continue_with_saml": "Continuer avec SAML SSO", + "forgot-password": { + "back_to_login": "Retour à la connexion", + "email-sent": { + "heading": "Demande de réinitialisation de mot de passe réussie.", + "text": "Si un compte avec cet e-mail existe, vous recevrez bientôt des instructions pour réinitialiser votre mot de passe." + }, + "reset": { + "confirm_password": "Confirmer le mot de passe", + "new_password": "Nouveau mot de passe", + "no_token_provided": "Aucun jeton fourni", + "passwords_do_not_match": "Les mots de passe ne correspondent pas", + "success": { + "heading": "Mot de passe réinitialisé avec succès", + "text": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe." + } + }, + "reset_password": "Réinitialiser le mot de passe" + }, + "invite": { + "create_account": "Créer un compte", + "email_does_not_match": "Oups ! Mauvais email \uD83E\uDD26", + "email_does_not_match_description": "L'email dans l'invitation ne correspond pas au vôtre.", + "go_to_app": "Aller à l'application", + "happy_to_have_you": "Ravi de t'avoir \uD83E\uDD17", + "happy_to_have_you_description": "Veuillez créer un compte ou vous connecter.", + "invite_expired": "Invitation expirée \uD83D\uDE25", + "invite_expired_description": "Les invitations sont valables pendant 7 jours. Veuillez demander une nouvelle invitation.", + "invite_not_found": "Invitation non trouvée \uD83D\uDE25", + "invite_not_found_description": "Le code d'invitation ne peut pas être trouvé ou a déjà été utilisé.", + "login": "Connexion", + "welcome_to_organization": "Vous êtes dedans \uD83C\uDF89", + "welcome_to_organization_description": "Bienvenue dans l'organisation." + }, + "last_used": "Dernière utilisation", + "login": { + "backup_code": "Code de sauvegarde", + "create_an_account": "Créer un compte", + "enter_your_backup_code": "Entrez votre code de sauvegarde", + "enter_your_two_factor_authentication_code": "Entrez votre code d'authentification à deux facteurs.", + "forgot_your_password": "Mot de passe oublié ?", + "login_to_your_account": "Connectez-vous à votre compte", + "login_with_email": "Se connecter avec un e-mail", + "lost_access": "Accès perdu ?", + "new_to_formbricks": "Nouveau sur Formbricks ?", + "use_a_backup_code": "Utiliser un code de secours" + }, + "saml_connection_error": "Quelque chose s'est mal passé. Veuillez vérifier la console de votre application pour plus de détails.", + "signup": { + "captcha_failed": "Captcha échoué", + "have_an_account": "Avez-vous un compte ?", + "log_in": "Se connecter", + "password_validation_contain_at_least_1_number": "Contenir au moins 1 chiffre", + "password_validation_minimum_8_and_maximum_128_characters": "Minimum 8 et Maximum 128 caractères", + "password_validation_uppercase_and_lowercase": "Mélange de majuscules et de minuscules", + "please_verify_captcha": "Veuillez vérifier reCAPTCHA", + "privacy_policy": "Politique de confidentialité", + "terms_of_service": "Conditions d'utilisation", + "title": "Créez votre compte Formbricks" + }, + "signup_without_verification_success": { + "user_successfully_created": "Utilisateur créé avec succès", + "user_successfully_created_description": "Votre nouvel utilisateur a été créé avec succès. Veuillez cliquer sur le bouton ci-dessous et vous connecter à votre compte." + }, + "testimonial_1": "Nous mesurons la clarté de nos documents et apprenons des abandons, le tout sur une seule plateforme. Excellent produit, équipe très réactive !", + "testimonial_all_features_included": "Toutes les fonctionnalités incluses", + "testimonial_free_and_open_source": "Libre et open-source", + "testimonial_no_credit_card_required": "Aucune carte de crédit requise", + "testimonial_title": "Transformez les insights clients en expériences irrésistibles.", + "verification-requested": { + "invalid_email_address": "Adresse e-mail invalide", + "invalid_token": "Jeton non valide ☹️", + "no_email_provided": "Aucun e-mail fourni", + "please_click_the_link_in_the_email_to_activate_your_account": "Veuillez cliquer sur le lien dans l'e-mail pour activer votre compte.", + "please_confirm_your_email_address": "Veuillez confirmer votre adresse e-mail.", + "resend_verification_email": "Renvoyer l'email de vérification", + "verification_email_successfully_sent": "Email de vérification envoyé avec succès. Veuillez vérifier votre boîte de réception.", + "we_sent_an_email_to": "Nous avons envoyé un email à {email}", + "you_didnt_receive_an_email_or_your_link_expired": "Vous n'avez pas reçu d'email ou votre lien a expiré ?" + }, + "verify": { + "no_token_provided": "Aucun jeton fourni", + "verifying": "Vérification..." + } + }, + "billing_confirmation": { + "back_to_billing_overview": "Retour à l'aperçu de la facturation", + "thanks_for_upgrading": "Merci beaucoup d'avoir mis à niveau votre abonnement Formbricks.", + "upgrade_successful": "Mise à niveau réussie" + }, + "common": { + "accepted": "Accepté", + "account": "Compte", + "account_settings": "Paramètres du compte", + "action": "Action", + "actions": "Actions", + "active_surveys": "Sondages actifs", + "activity": "Activité", + "add": "Ajouter", + "add_action": "Ajouter une action", + "add_filter": "Ajouter un filtre", + "add_logo": "Ajouter un logo", + "add_project": "Ajouter un projet", + "add_to_team": "Ajouter à l'équipe", + "all": "Tout", + "all_questions": " toutes les questions", + "allow": "Autoriser", + "allow_users_to_exit_by_clicking_outside_the_survey": "Permettre aux utilisateurs de sortir en cliquant en dehors de l'enquête", + "an_unknown_error_occurred_while_deleting_table_items": "Une erreur inconnue est survenue lors de la suppression des {type}s", + "and": "Et", + "and_response_limit_of": "et limite de réponse de", + "anonymous": "Anonyme", + "api_keys": "Clés API", + "app": "Application", + "app_survey": "Sondage d'application", + "apply_filters": "Appliquer des filtres", + "are_you_sure": "Es-tu sûr ?", + "are_you_sure_this_action_cannot_be_undone": "Êtes-vous sûr ? Cette action ne peut pas être annulée.", + "attributes": "Attributs", + "avatar": "Avatar", + "back": "Retour", + "billing": "Facturation", + "booked": "Réservé", + "bottom_left": "En bas à gauche", + "bottom_right": "En bas à droite", + "cancel": "Annuler", + "centered_modal": "Modal centré", + "choices": "Choix", + "clear_all": "Tout effacer", + "clear_filters": "Effacer les filtres", + "clear_selection": "Effacer la sélection", + "click": "Cliquez", + "clicks": "Clics", + "close": "Fermer", + "code": "Code", + "collapse_rows": "Réduire les lignes", + "completed": "Terminé", + "configuration": "Configuration", + "confirm": "Confirmer", + "connect": "Connecter", + "connect_formbricks": "Connecter Formbricks", + "connected": "Connecté", + "contacts": "Contacts", + "copied_to_clipboard": "Copié dans le presse-papiers", + "copy": "Copier", + "copy_code": "Copier le code", + "copy_link": "Copier le lien", + "create_new_organization": "Créer une nouvelle organisation", + "create_segment": "Créer un segment", + "create_survey": "Créer un sondage", + "created": "Créé", + "created_at": "Créé le", + "created_by": "Créé par", + "customer_success": "Succès Client", + "danger_zone": "Zone de danger", + "dark_overlay": "Superposition sombre", + "date": "Date", + "default": "Par défaut", + "delete": "Supprimer", + "description": "Description", + "dev_env": "Environnement de développement", + "development_environment_banner": "Vous êtes dans un environnement de développement. Configurez-le pour tester des enquêtes, des actions et des attributs.", + "disable": "Désactiver", + "disallow": "Ne pas permettre", + "discard": "Annuler", + "dismissed": "Rejeté", + "docs": "Documentation", + "documentation": "Documentation", + "download": "Télécharger", + "draft": "Brouillon", + "duplicate": "Dupliquer", + "e_commerce": "E-commerce", + "edit": "Modifier", + "email": "Email", + "embed": "Intégrer", + "enterprise_license": "Licence d'entreprise", + "environment_not_found": "Environnement non trouvé", + "environment_notice": "Vous êtes actuellement dans l'environnement {environment}.", + "error": "Erreur", + "error_component_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.", + "error_component_title": "Erreur de chargement des ressources", + "expand_rows": "Développer les lignes", + "finish": "Terminer", + "follow_these": "Suivez ceci", + "formbricks_version": "Version de Formbricks", + "full_name": "Nom complet", + "gathering_responses": "Collecte des réponses", + "general": "Général", + "go_back": "Retourner", + "go_to_dashboard": "Aller au tableau de bord", + "hidden": "Caché", + "hidden_field": "Champ caché", + "hidden_fields": "Champs cachés", + "hide": "Cacher", + "hide_column": "Cacher la colonne", + "image": "Image", + "images": "Images", + "import": "Importer", + "impressions": "Impressions", + "imprint": "Empreinte", + "in_progress": "En cours", + "inactive_surveys": "Sondages inactifs", + "input_type": "Type d'entrée", + "insights": "Perspectives", + "integration": "intégration", + "integrations": "Intégrations", + "invalid_date": "Date invalide", + "invalid_file_type": "Type de fichier invalide", + "invite": "Inviter", + "invite_them": "Invitez-les", + "key": "Clé", + "label": "Étiquette", + "language": "Langue", + "learn_more": "En savoir plus", + "license": "Licence", + "light_overlay": "Superposition légère", + "limits_reached": "Limites atteints", + "link": "Lien", + "link_and_email": "Liens et e-mail", + "link_copied": " lien copié dans le presse-papiers !", + "link_survey": "Enquête de lien", + "link_surveys": "Sondages de lien", + "load_more": "Charger plus", + "loading": "Chargement", + "logo": "Logo", + "logout": "Déconnexion", + "look_and_feel": "Apparence et sensation", + "manage": "Gérer", + "marketing": "Marketing", + "maximum": "Max", + "member": "Membre", + "members": "Membres", + "membership_not_found": "Abonnement non trouvé", + "metadata": "Métadonnées", + "minimum": "Min", + "mobile_overlay_text": "Formbricks n'est pas disponible pour les appareils avec des résolutions plus petites.", + "move_down": "Déplacer vers le bas", + "move_up": "Déplacer vers le haut", + "multiple_languages": "Plusieurs langues", + "name": "Nom", + "negative": "Négatif", + "neutral": "Neutre", + "new": "Nouveau", + "new_survey": "Nouveau Sondage", + "new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !", + "next": "Suivant", + "no_background_image_found": "Aucune image de fond trouvée.", + "no_code": "Pas de code", + "no_files_uploaded": "Aucun fichier n'a été téléchargé.", + "no_result_found": "Aucun résultat trouvé", + "no_results": "Aucun résultat", + "no_surveys_found": "Aucun sondage trouvé.", + "not_authenticated": "Vous n'êtes pas authentifié pour effectuer cette action.", + "not_authorized": "Non autorisé", + "not_connected": "Non connecté", + "note": "Remarque", + "notes": "Notes", + "notifications": "Notifications", + "number": "Numéro", + "off": "Éteint", + "on": "Sur", + "only_one_file_allowed": "Un seul fichier est autorisé", + "only_owners_managers_and_manage_access_members_can_perform_this_action": "Seules les propriétaires, les gestionnaires et les membres ayant accès à la gestion peuvent effectuer cette action.", + "or": "ou", + "organization": "Organisation", + "organization_id": "ID de l'organisation", + "organization_not_found": "Organisation non trouvée", + "organization_teams_not_found": "Équipes d'organisation non trouvées", + "other": "Autre", + "others": "Autres", + "overview": "Aperçu", + "password": "Mot de passe", + "paused": "En pause", + "pending_downgrade": "Downgrade en attente", + "people_manager": "Responsable des personnes", + "person": "Personne", + "phone": "Téléphone", + "photo_by": "Photo par", + "pick_a_date": "Choisissez une date", + "placeholder": "Remplaçant", + "please_select_at_least_one_survey": "Veuillez sélectionner au moins une enquête.", + "please_select_at_least_one_trigger": "Veuillez sélectionner au moins un déclencheur.", + "please_upgrade_your_plan": "Veuillez mettre à niveau votre plan.", + "positive": "Positif", + "preview": "Aperçu", + "preview_survey": "Aperçu de l'enquête", + "privacy": "Politique de confidentialité", + "privacy_policy": "Politique de confidentialité", + "product_manager": "Chef de produit", + "profile": "Profil", + "project": "Projet", + "project_configuration": "Configuration du projet", + "project_id": "ID de projet", + "project_name": "Nom du projet", + "project_not_found": "Projet non trouvé", + "project_permission_not_found": "Autorisation de projet non trouvée", + "projects": "Projets", + "projects_limit_reached": "Limite de projets atteinte", + "question": "Question", + "question_id": "ID de la question", + "questions": "Questions", + "read_docs": "Lire les documents", + "remove": "Retirer", + "reorder_and_hide_columns": "Réorganiser et masquer des colonnes", + "report_survey": "Rapport d'enquête", + "request_trial_license": "Demander une licence d'essai", + "reset_to_default": "Réinitialiser par défaut", + "response": "Réponse", + "responses": "Réponses", + "restart": "Redémarrer", + "role": "Rôle", + "role_organization": "Rôle (Organisation)", + "saas": "SaaS", + "sales": "Ventes", + "save": "Enregistrer", + "save_changes": "Enregistrer les modifications", + "scheduled": "Programmé", + "search": "Recherche", + "security": "Sécurité", + "segment": "Segmenter", + "segments": "Segments", + "select": "Sélectionner", + "select_all": "Sélectionner tout", + "select_survey": "Sélectionner l'enquête", + "selected": "Sélectionné", + "selected_questions": "Questions sélectionnées", + "selection": "Sélection", + "selections": "Sélections", + "send": "Envoyer", + "send_test_email": "Envoyer un e-mail de test", + "session_not_found": "Session non trouvée", + "settings": "Paramètres", + "share_feedback": "Partager des retours", + "show": "Montrer", + "show_response_count": "Afficher le nombre de réponses", + "shown": "Montré", + "size": "Taille", + "skipped": "Passé", + "skips": "Sauter", + "some_files_failed_to_upload": "Certains fichiers n'ont pas pu être téléchargés", + "something_went_wrong_please_try_again": "Une erreur s'est produite. Veuillez réessayer.", + "sort_by": "Trier par", + "start_free_trial": "Commencer l'essai gratuit", + "status": "Statut", + "step_by_step_manual": "Manuel étape par étape", + "styling": "Style", + "submit": "Soumettre", + "summary": "Résumé", + "survey": "Enquête", + "survey_completed": "Enquête terminée.", + "survey_id": "ID de l'enquête", + "survey_languages": "Langues de l'enquête", + "survey_live": "Sondage en direct", + "survey_not_found": "Sondage non trouvé", + "survey_paused": "Sondage en pause.", + "survey_scheduled": "Sondage programmé.", + "survey_type": "Type de sondage", + "surveys": "Enquêtes", + "switch_organization": "Changer d'organisation", + "switch_to": "Passer à {environment}", + "table_items_deleted_successfully": "{type}s supprimés avec succès", + "table_settings": "Réglages de table", + "tags": "Étiquettes", + "targeting": "Ciblage", + "team": "Équipe", + "team_access": "Accès Équipe", + "team_name": "Nom de l'équipe", + "teams": "Contrôle d'accès", + "teams_not_found": "Équipes non trouvées", + "text": "Texte", + "time": "Temps", + "time_to_finish": "Temps de finir", + "title": "Titre", + "top_left": "Haut Gauche", + "top_right": "Haut Droit", + "try_again": "Réessayer", + "type": "Type", + "unlock_more_projects_with_a_higher_plan": "Débloquez plus de projets avec un plan supérieur.", + "update": "Mise à jour", + "updated": "Mise à jour", + "updated_at": "Mis à jour à", + "upload": "Télécharger", + "upload_input_description": "Cliquez ou faites glisser pour télécharger des fichiers.", + "url": "URL", + "user": "Utilisateur", + "user_id": "Identifiant d'utilisateur", + "user_not_found": "Utilisateur non trouvé", + "variable": "Variable", + "variables": "Variables", + "verified_email": "Email vérifié", + "video": "Vidéo", + "warning": "Avertissement", + "we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Nous n'avons pas pu vérifier votre licence car le serveur de licence est inaccessible.", + "webhook": "Webhook", + "webhooks": "Webhooks", + "website_and_app_connection": "Connexion Site Web & Application", + "website_app_survey": "Sondage sur le site Web et l'application", + "website_survey": "Sondage de site web", + "weekly_summary": "Résumé hebdomadaire", + "welcome_card": "Carte de bienvenue", + "yes": "Oui", + "you": "Vous", + "you_are_downgraded_to_the_community_edition": "Vous êtes rétrogradé à l'édition communautaire.", + "you_are_not_authorised_to_perform_this_action": "Vous n'êtes pas autorisé à effectuer cette action.", + "you_have_reached_your_limit_of_project_limit": "Vous avez atteint votre limite de {projectLimit} projets.", + "you_have_reached_your_monthly_miu_limit_of": "Vous avez atteint votre limite mensuelle de MIU de", + "you_have_reached_your_monthly_response_limit_of": "Vous avez atteint votre limite de réponses mensuelle de", + "you_will_be_downgraded_to_the_community_edition_on_date": "Vous serez rétrogradé à l'édition communautaire le {date}." + }, + "emails": { + "accept": "Accepter", + "click_or_drag_to_upload_files": "Cliquez ou faites glisser pour télécharger des fichiers.", + "email_customization_preview_email_heading": "Salut {userName}", + "email_customization_preview_email_subject": "Aperçu de la personnalisation des e-mails Formbricks", + "email_customization_preview_email_text": "C'est une prévisualisation d'e-mail pour vous montrer quel logo sera rendu dans les e-mails.", + "email_footer_text_1": "Passe une belle journée !", + "email_footer_text_2": "L'équipe Formbricks", + "email_template_text_1": "Cet e-mail a été envoyé via Formbricks.", + "embed_survey_preview_email_didnt_request": "Vous n'avez pas demandé cela ?", + "embed_survey_preview_email_environment_id": "ID d'environnement", + "embed_survey_preview_email_fight_spam": "Aidez-nous à lutter contre le spam et transférez ce mail à hola@formbricks.com.", + "embed_survey_preview_email_heading": "Aperçu de l'email intégré", + "embed_survey_preview_email_subject": "Aperçu du sondage par e-mail Formbricks", + "embed_survey_preview_email_text": "C'est ainsi que le code s'affiche intégré dans un e-mail :", + "forgot_password_email_change_password": "Changer le mot de passe", + "forgot_password_email_did_not_request": "Si vous n'avez pas demandé cela, veuillez ignorer cet e-mail.", + "forgot_password_email_heading": "Changer le mot de passe", + "forgot_password_email_link_valid_for_24_hours": "Le lien est valable pendant 24 heures.", + "forgot_password_email_subject": "Réinitialise ton mot de passe Formbricks", + "forgot_password_email_text": "Vous avez demandé un lien pour changer votre mot de passe. Vous pouvez le faire en cliquant sur le lien ci-dessous :", + "imprint": "Impressum", + "invite_accepted_email_heading": "Salut", + "invite_accepted_email_subject": "Vous avez un nouveau membre dans votre organisation !", + "invite_accepted_email_text_par1": "Je te fais savoir que", + "invite_accepted_email_text_par2": "accepté votre invitation. Amusez-vous bien à collaborer !", + "invite_email_button_label": "Rejoindre l'organisation", + "invite_email_heading": "Salut", + "invite_email_text_par1": "Votre collègue", + "invite_email_text_par2": "vous a invité à les rejoindre sur Formbricks. Pour accepter l'invitation, veuillez cliquer sur le lien ci-dessous :", + "invite_member_email_subject": "Vous avez été invité à collaborer sur Formbricks !", + "live_survey_notification_completed": "Terminé", + "live_survey_notification_draft": "Brouillon", + "live_survey_notification_in_progress": "En cours", + "live_survey_notification_no_new_response": "Aucune nouvelle réponse reçue cette semaine \uD83D\uDD75️", + "live_survey_notification_no_responses_yet": "Aucune réponse pour le moment !", + "live_survey_notification_paused": "En pause", + "live_survey_notification_scheduled": "Programmé", + "live_survey_notification_view_more_responses": "Voir {responseCount} réponses supplémentaires", + "live_survey_notification_view_previous_responses": "Voir les réponses précédentes", + "live_survey_notification_view_response": "Voir la réponse", + "notification_footer_all_the_best": "Tous mes vœux,", + "notification_footer_in_your_settings": "dans vos paramètres \uD83D\uDE4F", + "notification_footer_please_turn_them_off": "veuillez les éteindre", + "notification_footer_the_formbricks_team": "L'équipe Formbricks \uD83E\uDD0D", + "notification_footer_to_halt_weekly_updates": "Pour arrêter les mises à jour hebdomadaires,", + "notification_header_hey": "Salut \uD83D\uDC4B", + "notification_header_weekly_report_for": "Rapport hebdomadaire pour", + "notification_insight_completed": "Terminé", + "notification_insight_completion_rate": "Pourcentage d'achèvement", + "notification_insight_displays": "Affichages", + "notification_insight_responses": "Réponses", + "notification_insight_surveys": "Enquêtes", + "onboarding_invite_email_button_label": "Rejoins l'organisation de {inviterName}", + "onboarding_invite_email_connect_formbricks": "Connectez Formbricks à votre application ou site web via un extrait HTML ou NPM en quelques minutes seulement.", + "onboarding_invite_email_create_account": "Créez un compte pour rejoindre l'organisation de {inviterName}.", + "onboarding_invite_email_done": "Fait ✅", + "onboarding_invite_email_get_started_in_minutes": "Commencez en quelques minutes", + "onboarding_invite_email_heading": "Salut ", + "onboarding_invite_email_subject": "{inviterName} a besoin d'aide pour configurer Formbricks. Peux-tu l'aider ?", + "password_changed_email_heading": "Mot de passe changé", + "password_changed_email_text": "Votre mot de passe a été changé avec succès.", + "password_reset_notify_email_subject": "Ton mot de passe Formbricks a été changé", + "privacy_policy": "Politique de confidentialité", + "reject": "Rejeter", + "render_email_response_value_file_upload_response_link_not_included": "Le lien vers le fichier téléchargé n'est pas inclus pour des raisons de confidentialité des données", + "response_finished_email_subject": "Une réponse pour {surveyName} a été complétée ✅", + "response_finished_email_subject_with_email": "{personEmail} vient de compléter votre enquête {surveyName} ✅", + "schedule_your_meeting": "Planifier votre rendez-vous", + "select_a_date": "Sélectionner une date", + "survey_response_finished_email_congrats": "Félicitations, vous avez reçu une nouvelle réponse à votre enquête ! Quelqu'un vient de compléter votre enquête : {surveyName}", + "survey_response_finished_email_dont_want_notifications": "Vous ne voulez pas recevoir ces notifications ?", + "survey_response_finished_email_hey": "Salut \uD83D\uDC4B", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Désactiver les notifications pour tous les formulaires nouvellement créés", + "survey_response_finished_email_turn_off_notifications_for_this_form": "Désactiver les notifications pour ce formulaire", + "survey_response_finished_email_view_more_responses": "Voir {responseCount} réponses supplémentaires", + "survey_response_finished_email_view_survey_summary": "Voir le résumé de l'enquête", + "verification_email_click_on_this_link": "Vous pouvez également cliquer sur ce lien :", + "verification_email_heading": "Presque là !", + "verification_email_hey": "Salut \uD83D\uDC4B", + "verification_email_if_expired_request_new_token": "Si cela a expiré, veuillez demander un nouveau jeton ici :", + "verification_email_link_valid_for_24_hours": "Le lien est valide pendant 24 heures.", + "verification_email_request_new_verification": "Demander une nouvelle vérification", + "verification_email_subject": "Veuillez vérifier votre e-mail pour utiliser Formbricks.", + "verification_email_survey_name": "Nom de l'enquête", + "verification_email_take_survey": "Participer à l'enquête", + "verification_email_text": "Pour commencer à utiliser Formbricks, veuillez vérifier votre e-mail ci-dessous :", + "verification_email_thanks": "Merci de valider votre email !", + "verification_email_to_fill_survey": "Pour remplir le questionnaire, veuillez cliquer sur le bouton ci-dessous :", + "verification_email_verify_email": "Vérifier l'email", + "verified_link_survey_email_subject": "Votre enquête est prête à être remplie.", + "weekly_summary_create_reminder_notification_body_cal_slot": "Choisissez un créneau de 15 minutes dans le calendrier de notre PDG.", + "weekly_summary_create_reminder_notification_body_dont_let_a_week_pass": "Ne laissez pas une semaine passer sans en apprendre davantage sur vos utilisateurs :", + "weekly_summary_create_reminder_notification_body_need_help": "Besoin d'aide pour trouver le bon sondage pour votre produit ?", + "weekly_summary_create_reminder_notification_body_reply_email": "ou répondez à cet e-mail :)", + "weekly_summary_create_reminder_notification_body_setup_a_new_survey": "Configurer une nouvelle enquête", + "weekly_summary_create_reminder_notification_body_text": "Nous aimerions vous envoyer un résumé hebdomadaire, mais actuellement, il n'y a pas d'enquêtes en cours pour {projectName}.", + "weekly_summary_email_subject": "Aperçu des utilisateurs de {projectName} – La semaine dernière par Formbricks" + }, + "environments": { + "actions": { + "action_copied_successfully": "Action copiée avec succès", + "action_copy_failed": "Échec de la copie de l'action", + "action_created_successfully": "Action créée avec succès", + "action_deleted_successfully": "Action supprimée avec succès", + "action_type": "Type d'action", + "action_updated_successfully": "Action mise à jour avec succès", + "action_with_key_already_exists": "L'action avec la clé '{'key'}' existe déjà", + "action_with_name_already_exists": "L'action avec le nom '{'name'}' existe déjà", + "add_css_class_or_id": "Ajouter une classe ou un identifiant CSS", + "add_url": "Ajouter une URL", + "click": "Cliquez", + "contains": "Contient", + "create_action": "Créer une action", + "css_selector": "Sélecteur CSS", + "delete_action_text": "Êtes-vous sûr de vouloir supprimer cette action ? Cela supprime également cette action en tant que déclencheur de toutes vos enquêtes.", + "display_name": "Nom d'affichage", + "does_not_contain": "Ne contient pas", + "does_not_exactly_match": "Ne correspond pas exactement", + "eg_clicked_download": "Par exemple, cliqué sur Télécharger", + "eg_download_cta_click_on_home": "Par exemple, cliquez sur le CTA de téléchargement sur la page d'accueil", + "eg_install_app": "Par exemple, installer l'application", + "eg_user_clicked_download_button": "Par exemple, l'utilisateur a cliqué sur le bouton de téléchargement.", + "ends_with": "Se termine par", + "enter_a_url_to_see_if_a_user_visiting_it_would_be_tracked": "Saisissez une URL pour voir si un utilisateur la visitant serait suivi.", + "exactly_matches": "Correspondance exacte", + "exit_intent": "Intention de sortie", + "fifty_percent_scroll": "50% Défilement", + "how_do_code_actions_work": "Comment fonctionnent les actions de code ?", + "if_a_user_clicks_a_button_with_a_specific_css_class_or_id": "Si un utilisateur clique sur un bouton avec une classe ou un identifiant CSS spécifique", + "if_a_user_clicks_a_button_with_a_specific_text": "Si un utilisateur clique sur un bouton avec un texte spécifique", + "in_your_code_read_more_in_our": "dans votre code. En savoir plus dans notre", + "inner_text": "Texte interne", + "invalid_css_selector": "Sélecteur CSS invalide", + "limit_the_pages_on_which_this_action_gets_captured": "Limiter les pages sur lesquelles cette action est capturée", + "limit_to_specific_pages": "Limiter à des pages spécifiques", + "on_all_pages": "Sur toutes les pages", + "page_filter": "Filtre de page", + "page_view": "Vue de page", + "select_match_type": "Sélectionner le type de match", + "starts_with": "Commence par", + "test_match": "Match de test", + "test_your_url": "Testez votre URL", + "this_action_was_created_automatically_you_cannot_make_changes_to_it": "Cette action a été créée automatiquement. Vous ne pouvez pas y apporter de modifications.", + "this_action_will_be_triggered_when_the_page_is_loaded": "Cette action sera déclenchée lorsque la page sera chargée.", + "this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Cette action sera déclenchée lorsque l'utilisateur fera défiler 50 % de la page.", + "this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Cette action sera déclenchée lorsque l'utilisateur essaiera de quitter la page.", + "this_is_a_code_action_please_make_changes_in_your_code_base": "Ceci est une action de code. Veuillez apporter des modifications à votre base de code.", + "track_new_user_action": "Suivre l'action des nouveaux utilisateurs", + "track_user_action_to_display_surveys_or_create_user_segment": "Suivre l'action de l'utilisateur pour afficher des enquêtes ou créer un segment d'utilisateur.", + "url": "URL", + "user_actions": "Actions de l'utilisateur", + "user_clicked_download_button": "L'utilisateur a cliqué sur le bouton de téléchargement", + "what_did_your_user_do": "Que fait votre utilisateur ?", + "what_is_the_user_doing": "Que fait l'utilisateur ?", + "you_can_track_code_action_anywhere_in_your_app_using": "Vous pouvez suivre l'action du code partout dans votre application en utilisant" + }, + "connect": { + "congrats": "Félicitations !", + "connection_successful_message": "Bien joué ! Nous sommes connectés.", + "do_it_later": "Je le ferai plus tard", + "finish_onboarding": "Terminer l'intégration", + "headline": "Connecte ton appli ou site web", + "import_formbricks_and_initialize_the_widget_in_your_component": "Importez Formbricks et initialisez le widget dans votre composant (par exemple, App.tsx) :", + "insert_this_code_into_the_head_tag_of_your_website": "Insérez ce code dans la balise head de votre site web :", + "subtitle": "Ça prend moins de 4 minutes.", + "waiting_for_your_signal": "En attente de votre signal..." + }, + "contacts": { + "contact_deleted_successfully": "Contact supprimé avec succès", + "contact_not_found": "Aucun contact trouvé", + "contacts_table_refresh": "Rafraîchir les contacts", + "contacts_table_refresh_error": "Une erreur s'est produite lors de la mise à jour des contacts. Veuillez réessayer.", + "contacts_table_refresh_success": "Contacts rafraîchis avec succès", + "first_name": "Prénom", + "last_name": "Nom de famille", + "no_responses_found": "Aucune réponse trouvée", + "not_provided": "Non fourni", + "search_contact": "Rechercher un contact", + "select_attribute": "Sélectionner un attribut", + "unlock_contacts_description": "Gérer les contacts et envoyer des enquêtes ciblées", + "unlock_contacts_title": "Débloquez des contacts avec un plan supérieur.", + "upload_contacts_modal_attributes_description": "Mappez les colonnes de votre CSV aux attributs dans Formbricks.", + "upload_contacts_modal_attributes_new": "Nouvel attribut", + "upload_contacts_modal_attributes_search_or_add": "Rechercher ou ajouter un attribut", + "upload_contacts_modal_attributes_should_be_mapped_to": "devrait être mappé à", + "upload_contacts_modal_attributes_title": "Attributs", + "upload_contacts_modal_description": "Téléchargez un CSV pour importer rapidement des contacts avec des attributs.", + "upload_contacts_modal_download_example_csv": "Télécharger un exemple de CSV", + "upload_contacts_modal_duplicates_description": "Comment devrions-nous procéder si un contact existe déjà dans vos contacts ?", + "upload_contacts_modal_duplicates_overwrite_description": "Écrase les contacts existants", + "upload_contacts_modal_duplicates_overwrite_title": "Sélectionner", + "upload_contacts_modal_duplicates_skip_description": "Ignore les contacts en double", + "upload_contacts_modal_duplicates_skip_title": "Sauter", + "upload_contacts_modal_duplicates_title": "Doublons", + "upload_contacts_modal_duplicates_update_description": "Mise à jour des contacts existants", + "upload_contacts_modal_duplicates_update_title": "Mise à jour", + "upload_contacts_modal_pick_different_file": "Choisissez un fichier différent", + "upload_contacts_modal_preview": "Voici un aperçu de vos données.", + "upload_contacts_modal_upload_btn": "Importer des contacts" + }, + "experience": { + "all": "Tout", + "all_time": "Tout le temps", + "analysed_feedbacks": "Réponses en texte libre analysées", + "category": "Catégorie", + "category_updated_successfully": "Catégorie mise à jour avec succès !", + "complaint": "Plainte", + "did_you_find_this_insight_helpful": "Avez-vous trouvé cette information utile ?", + "failed_to_update_category": "Échec de la mise à jour de la catégorie", + "feature_request": "Demande", + "good_afternoon": "\uD83C\uDF24️ Bon après-midi", + "good_evening": "\uD83C\uDF19 Bonsoir", + "good_morning": "☀️ Bonjour", + "insights_description": "Toutes les informations générées à partir des réponses de toutes vos enquêtes", + "insights_for_project": "Aperçus pour {projectName}", + "new_responses": "Réponses", + "no_insights_for_this_filter": "Aucune information pour ce filtre", + "no_insights_found": "Aucune information trouvée. Collectez plus de réponses à l'enquête ou activez les insights pour vos enquêtes existantes pour commencer.", + "praise": "Éloge", + "sentiment_score": "Score de sentiment", + "templates_card_description": "Choisissez un modèle ou commencez à partir de zéro", + "templates_card_title": "Mesurez l'expérience de vos clients", + "this_month": "Ce mois-ci", + "this_quarter": "Ce trimestre", + "this_week": "Cette semaine", + "today": "Aujourd'hui" + }, + "formbricks_logo": "Logo Formbricks", + "integrations": { + "activepieces_integration_description": "Connectez instantanément Formbricks avec des applications populaires pour automatiser les tâches sans coder.", + "additional_settings": "Paramètres supplémentaires", + "airtable": { + "airtable_base": "Base Airtable", + "airtable_integration": "Intégration Airtable", + "airtable_integration_description": "Synchronisez les réponses directement avec Airtable.", + "airtable_integration_is_not_configured": "L'intégration Airtable n'est pas configurée", + "connect_with_airtable": "Se connecter à Airtable", + "link_airtable_table": "Lier la table Airtable", + "link_new_table": "Lier nouvelle table", + "no_bases_found": "Aucune base Airtable trouvée", + "no_integrations_yet": "Vos intégrations Airtable apparaîtront ici dès que vous les ajouterez. ⏲️", + "please_create_a_base": "Veuillez créer une base sur Airtable.", + "please_select_a_base": "Veuillez sélectionner une base.", + "please_select_a_table": "Veuillez sélectionner une table.", + "sync_responses_with_airtable": "Synchroniser les réponses avec un Airtable", + "table_name": "Nom de la table" + }, + "airtable_integration_description": "Remplissez instantanément votre table Airtable avec des données d'enquête", + "connected_with_email": "Connecté avec {email}", + "connecting_integration_failed_please_try_again": "Échec de la connexion d'intégration. Veuillez réessayer !", + "create_survey_warning": "Vous devez créer une enquête pour pouvoir configurer cette intégration.", + "delete_integration": "Supprimer l'intégration", + "delete_integration_confirmation": "Êtes-vous sûr de vouloir supprimer cette intégration ?", + "google_sheet_integration_description": "Remplissez instantanément vos feuilles de calcul avec des données d'enquête", + "google_sheets": { + "connect_with_google_sheets": "Se connecter à Google Sheets", + "enter_a_valid_spreadsheet_url_error": "Veuillez entrer une URL de feuille de calcul valide.", + "google_connection": "Connexion Google", + "google_connection_deletion_description": "Synchronisez les réponses directement avec Google Sheets.", + "google_sheet_integration_is_not_configured": "L'intégration de Google Sheet n'est pas configurée dans votre instance de Formbricks.", + "google_sheet_logo": "logo de Google Sheets", + "google_sheet_name": "Nom de la feuille Google", + "google_sheets_integration": "Intégration de Google Sheets", + "google_sheets_integration_description": "Synchronisez les réponses directement avec Google Sheets.", + "link_google_sheet": "Lien Google Sheet", + "link_new_sheet": "Lier une nouvelle feuille", + "no_integrations_yet": "Vos intégrations Google Sheets apparaîtront ici dès que vous les ajouterez. ⏲️", + "spreadsheet_url": "URL de la feuille de calcul" + }, + "include_created_at": "Inclure la date de création", + "include_hidden_fields": "Inclure les champs cachés", + "include_metadata": "Inclure des métadonnées (navigateur, pays, etc.)", + "include_variables": "Inclure des variables", + "integration_added_successfully": "Intégration ajoutée avec succès", + "integration_removed_successfully": "Intégration supprimée avec succès", + "integration_updated_successfully": "Intégration mise à jour avec succès", + "make_integration_description": "Intégrez Formbricks avec plus de 1000 applications via Make", + "manage_webhooks": "Gérer les Webhooks", + "n8n_integration_description": "Intégrez Formbricks avec plus de 350 applications via n8n.", + "notion": { + "col_name_of_type_is_not_supported": "{col_name} de type {type} n'est pas pris en charge par l'API de Notion. Les données ne seront pas reflétées dans votre base de données Notion.", + "connect_with_notion": "Se connecter avec Notion", + "connected_with_workspace": "Connecté avec l'espace de travail '{'workspace'}'", + "create_at_least_one_database_to_setup_this_integration": "Vous devez créer au moins une base de données pour pouvoir configurer cette intégration.", + "database_name": "Nom de la base de données", + "duplicate_connection_warning": "Une connexion avec cette base de données est active. Veuillez apporter des modifications avec prudence.", + "link_database": "Base de données de liens", + "link_new_database": "Lier nouvelle base de données", + "link_notion_database": "Lier la base de données Notion", + "map_formbricks_fields_to_notion_property": "Mapper les champs Formbricks à la propriété Notion", + "no_databases_found": "Vos intégrations Notion apparaîtront ici dès que vous les ajouterez. ⏲️", + "notion_integration": "Intégration Notion", + "notion_integration_description": "Envoyez les réponses directement à Notion.", + "notion_integration_is_not_configured": "L'intégration Notion n'est pas configurée dans votre instance de Formbricks.", + "notion_logo": "Logo Notion", + "please_complete_mapping_fields_with_notion_property": "Veuillez compléter le mappage des champs avec la propriété Notion.", + "please_resolve_mapping_errors": "Veuillez résoudre les erreurs de mappage.", + "please_select_a_database": "Veuillez sélectionner une base de données.", + "please_select_at_least_one_mapping": "Veuillez sélectionner au moins un mappage.", + "que_name_of_type_cant_be_mapped_to": "{que_name} de type {question_label} ne peut pas être mappé à la colonne {col_name} de type {col_type}. Utilisez plutôt une colonne de type {mapped_type}.", + "select_a_database": "Sélectionner la base de données", + "select_a_field_to_map": "Sélectionnez un champ à mapper", + "select_a_survey_question": "Sélectionnez une question d'enquête", + "sync_responses_with_a_notion_database": "Synchroniser les réponses avec une base de données Notion", + "update_connection": "Reconnecter Notion", + "update_connection_tooltip": "Reconnectez l'intégration pour inclure les nouvelles bases de données ajoutées. Vos intégrations existantes resteront intactes." + }, + "notion_integration_description": "Envoyer des données à votre base de données Notion", + "please_select_a_survey_error": "Veuillez sélectionner une enquête.", + "select_at_least_one_question_error": "Veuillez sélectionner au moins une question.", + "slack": { + "already_connected_another_survey": "Vous avez déjà connecté une autre enquête à ce canal.", + "channel_name": "Nom de la chaîne", + "connect_with_slack": "Se connecter avec Slack", + "connect_your_first_slack_channel": "Connectez votre premier canal Slack pour commencer.", + "connected_with_team": "Connecté avec {team}", + "create_at_least_one_channel_error": "Vous devez créer au moins un canal pour pouvoir configurer cette intégration.", + "dont_see_your_channel": "Tu ne vois pas ton canal?", + "link_channel": "Canal de lien", + "link_slack_channel": "Lier le canal Slack", + "please_select_a_channel": "Veuillez sélectionner une chaîne.", + "select_channel": "Choisir le canal", + "slack_integration": "Intégration Slack", + "slack_integration_description": "Envoyez les réponses directement sur Slack.", + "slack_integration_is_not_configured": "L'intégration Slack n'est pas configurée dans votre instance de Formbricks.", + "slack_reconnect_button": "Reconnecter", + "slack_reconnect_button_description": "Remarque : Nous avons récemment modifié notre intégration Slack pour prendre en charge les canaux privés. Veuillez reconnecter votre espace de travail Slack." + }, + "slack_integration_description": "Connectez instantanément votre espace de travail Slack avec Formbricks", + "to_configure_it": "pour le configurer.", + "webhook_integration_description": "Déclenchez des Webhooks en fonction des actions dans vos enquêtes", + "webhooks": { + "add_webhook": "Ajouter un Webhook", + "add_webhook_description": "Envoyer les données de réponse à l'enquête à un point de terminaison personnalisé", + "all_current_and_new_surveys": "Tous les sondages actuels et nouveaux", + "created_by_third_party": "Créé par un tiers", + "discord_webhook_not_supported": "Les webhooks Discord ne sont actuellement pas pris en charge.", + "empty_webhook_message": "Vos webhooks apparaîtront ici dès que vous les ajouterez. ⏲️", + "endpoint_pinged": "Yay ! Nous pouvons pinger le webhook !", + "endpoint_pinged_error": "Impossible de pinger le webhook !", + "please_check_console": "Veuillez vérifier la console pour plus de détails.", + "please_enter_a_url": "Veuillez entrer une URL.", + "response_created": "Réponse créée", + "response_finished": "Réponse terminée", + "response_updated": "Réponse mise à jour", + "source": "Source", + "test_endpoint": "Point de test", + "triggers": "Déclencheurs", + "webhook_added_successfully": "Webhook ajouté avec succès", + "webhook_delete_confirmation": "Êtes-vous sûr de vouloir supprimer ce Webhook ? Cela arrêtera l'envoi de toute notification future.", + "webhook_deleted_successfully": "Webhook supprimé avec succès", + "webhook_name_placeholder": "Optionnel : Étiquetez votre webhook pour une identification facile", + "webhook_test_failed_due_to": "Échec du test de Webhook en raison de", + "webhook_updated_successfully": "Webhook mis à jour avec succès.", + "webhook_url_placeholder": "Collez l'URL sur laquelle vous souhaitez que l'événement se déclenche" + }, + "website_or_app_integration_description": "Intégrez Formbricks dans votre site Web ou votre application.", + "zapier_integration_description": "Intégrez Formbricks avec plus de 5000 applications via Zapier." + }, + "project": { + "api_keys": { + "add_api_key": "Ajouter une clé API", + "api_key": "Clé API", + "api_key_copied_to_clipboard": "Clé API copiée dans le presse-papiers", + "api_key_created": "Clé API créée", + "api_key_deleted": "Clé API supprimée", + "api_key_label": "Étiquette de clé API", + "api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.", + "api_key_updated": "Clé API mise à jour", + "duplicate_access": "L'accès en double au projet n'est pas autorisé", + "no_api_keys_yet": "Vous n'avez pas encore de clés API.", + "no_env_permissions_found": "Aucune autorisation d'environnement trouvée", + "organization_access": "Accès à l'organisation", + "permissions": "Permissions", + "project_access": "Accès au projet", + "secret": "Secret", + "unable_to_delete_api_key": "Impossible de supprimer la clé API" + }, + "app-connection": { + "api_host_description": "Ceci est l'URL de votre backend Formbricks.", + "app_connection": "Connexion d'application", + "app_connection_description": "Connectez votre application à Formbricks.", + "check_out_the_docs": "Consultez la documentation.", + "dive_into_the_docs": "Plongez dans la documentation.", + "does_your_widget_work": "Votre widget fonctionne-t-il ?", + "environment_id": "Votre identifiant d'environnement", + "environment_id_description": "Cet identifiant identifie de manière unique cet environnement Formbricks.", + "environment_id_description_with_environment_id": "Utilisé pour identifier l'environnement correct : {environmentId} est le vôtre.", + "formbricks_sdk": "SDK Formbricks", + "formbricks_sdk_connected": "Le SDK Formbricks est connecté", + "formbricks_sdk_not_connected": "Le SDK Formbricks n'est pas encore connecté.", + "formbricks_sdk_not_connected_description": "Connectez votre site web ou votre application à Formbricks.", + "have_a_problem": "Vous avez un problème ?", + "how_to_setup": "Comment configurer", + "how_to_setup_description": "Suivez ces étapes pour configurer le widget Formbricks dans votre application.", + "identifying_your_users": "identifier vos utilisateurs", + "if_you_are_planning_to": "Si vous prévoyez de", + "insert_this_code_into_the": "Insérez ce code dans le", + "need_a_more_detailed_setup_guide_for": "Besoin d'un guide d'installation plus détaillé pour", + "not_working": "Ça ne fonctionne pas ?", + "open_an_issue_on_github": "Ouvrir un problème sur GitHub", + "open_the_browser_console_to_see_the_logs": "Ouvrez la console du navigateur pour voir les journaux.", + "receiving_data": "Réception des données \uD83D\uDC83\uD83D\uDD7A", + "recheck": "Re-vérifier", + "scroll_to_the_top": "Faites défiler vers le haut !", + "step_1": "Étape 1 : Installer avec pnpm, npm ou yarn", + "step_2": "Étape 2 : Initialiser le widget", + "step_2_description": "Importez Formbricks et initialisez le widget dans votre composant (par exemple, App.tsx) :", + "step_3": "Étape 3 : Mode débogage", + "switch_on_the_debug_mode_by_appending": "Activez le mode débogage en ajoutant", + "tag_of_your_app": "étiquette de votre application", + "to_the_url_where_you_load_the": "vers l'URL où vous chargez le", + "want_to_learn_how_to_add_user_attributes": "Vous voulez apprendre à ajouter des attributs utilisateur, des événements personnalisés et plus encore ?", + "you_are_done": "Vous avez terminé \uD83C\uDF89", + "you_can_set_the_user_id_with": "vous pouvez définir l'ID utilisateur avec", + "your_app_now_communicates_with_formbricks": "Votre application communique désormais avec Formbricks - envoyant des événements et chargeant des enquêtes automatiquement !" + }, + "general": { + "cannot_delete_only_project": "Ceci est votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.", + "delete_project": "Supprimer le projet", + "delete_project_confirmation": "Êtes-vous sûr de vouloir supprimer {projectName} ? Cette action ne peut pas être annulée.", + "delete_project_name_includes_surveys_responses_people_and_more": "Supprimer {projectName} y compris toutes les enquêtes, réponses, personnes, actions et attributs.", + "delete_project_settings_description": "Supprimer le projet avec toutes les enquêtes, réponses, personnes, actions et attributs. Cela ne peut pas être annulé.", + "error_saving_project_information": "Erreur lors de l'enregistrement des informations du projet", + "only_owners_or_managers_can_delete_projects": "Seuls les propriétaires ou les gestionnaires peuvent supprimer des projets.", + "project_deleted_successfully": "Projet supprimé avec succès", + "project_name_settings_description": "Changez le nom de votre projet.", + "project_name_updated_successfully": "Le nom du projet a été mis à jour avec succès.", + "recontact_waiting_time": "Temps d'attente pour le recontact", + "recontact_waiting_time_settings_description": "Contrôlez la fréquence à laquelle les utilisateurs peuvent être sondés dans toutes les enquêtes de l'application.", + "this_action_cannot_be_undone": "Cette action ne peut pas être annulée.", + "wait_x_days_before_showing_next_survey": "Attendre X jours avant de montrer la prochaine enquête :", + "waiting_period_updated_successfully": "Le délai d'attente a été mis à jour avec succès", + "whats_your_project_called": "Comment s'appelle votre projet ?" + }, + "languages": { + "add_language": "Ajouter une langue", + "alias": "Alias", + "alias_tooltip": "L'alias est un nom alternatif pour identifier la langue dans les enquêtes de lien et le SDK (facultatif)", + "cannot_remove_language_warning": "Vous ne pouvez pas supprimer cette langue car elle est encore utilisée dans ces enquêtes :", + "conflict_between_identifier_and_alias": "Il y a un conflit entre l'identifiant d'une langue ajoutée et celui de vos alias. Les alias et les identifiants ne peuvent pas être identiques.", + "conflict_between_selected_alias_and_another_language": "Il y a un conflit entre l'alias sélectionné et une autre langue qui a cet identifiant. Veuillez ajouter la langue avec cet identifiant à votre projet à la place pour éviter les incohérences.", + "delete_language_confirmation": "Êtes-vous sûr de vouloir supprimer cette langue ? Cette action ne peut pas être annulée.", + "duplicate_language_or_language_id": "Langue ou identifiant de langue en double", + "edit_languages": "Modifier les langues", + "identifier": "Identifiant (ISO)", + "incomplete_translations": "Traductions incomplètes", + "language": "Langue", + "language_deleted_successfully": "Langue supprimée avec succès", + "languages_updated_successfully": "Langues mises à jour avec succès", + "multi_language_surveys": "Sondages multilingues", + "multi_language_surveys_description": "Ajoutez des langues pour créer des enquêtes multilingues.", + "no_language_found": "Aucune langue trouvée. Ajoutez votre première langue ci-dessous.", + "please_select_a_language": "Veuillez sélectionner une langue.", + "remove_language": "Supprimer la langue", + "remove_language_from_surveys_to_remove_it_from_project": "Veuillez retirer la langue de ces enquêtes afin de l'éliminer du projet.", + "search_items": "Rechercher des articles", + "translate": "Traduire" + }, + "look": { + "add_background_color": "Ajouter une couleur de fond", + "add_background_color_description": "Ajoutez une couleur de fond au conteneur du logo.", + "app_survey_placement": "Placement de l'enquête dans l'application", + "app_survey_placement_settings_description": "Changez l'emplacement où les enquêtes seront affichées dans votre application web ou votre site web.", + "centered_modal_overlay_color": "Couleur de superposition modale centrée", + "email_customization": "Personnalisation des e-mails", + "email_customization_description": "Modifiez l'apparence des e-mails envoyés par Formbricks en votre nom.", + "enable_custom_styling": "Activer le style personnalisé", + "enable_custom_styling_description": "Permettre aux utilisateurs de remplacer ce thème dans l'éditeur d'enquête.", + "failed_to_remove_logo": "Échec de la suppression du logo", + "failed_to_update_logo": "Échec de la mise à jour du logo", + "formbricks_branding": "Marque Formbricks", + "formbricks_branding_hidden": "La marque Formbricks est cachée.", + "formbricks_branding_settings_description": "Nous apprécions votre soutien mais comprenons si vous le désactivez.", + "formbricks_branding_shown": "La marque Formbricks est affichée.", + "logo_removed_successfully": "Logo supprimé avec succès", + "logo_settings_description": "Téléchargez le logo de votre entreprise pour personnaliser les enquêtes et les aperçus de lien.", + "logo_updated_successfully": "Logo mis à jour avec succès", + "logo_upload_failed": "Échec du téléchargement du logo. Veuillez réessayer.", + "placement_updated_successfully": "Placement mis à jour avec succès", + "remove_branding_with_a_higher_plan": "Supprimer la marque avec un plan supérieur", + "remove_logo": "Supprimer le logo", + "remove_logo_confirmation": "Êtes-vous sûr de vouloir supprimer le logo ?", + "replace_logo": "Remplacer le logo", + "reset_styling": "Réinitialiser le style", + "reset_styling_confirmation": "Êtes-vous sûr de vouloir réinitialiser le style par défaut ?", + "show_formbricks_branding_in": "Afficher la marque Formbricks dans les enquêtes {type}", + "show_powered_by_formbricks": "Afficher la signature \"Propulsé par Formbricks", + "styling_updated_successfully": "Style mis à jour avec succès", + "theme": "Thème", + "theme_settings_description": "Créez un thème de style pour toutes les enquêtes. Vous pouvez activer le style personnalisé pour chaque enquête." + }, + "tags": { + "add": "Ajouter", + "add_tag": "Ajouter une étiquette", + "count": "Compter", + "delete_tag_confirmation": "Êtes-vous sûr de vouloir supprimer cette étiquette ?", + "empty_message": "Taguez une soumission pour trouver votre liste de tags ici.", + "manage_tags": "Gérer les étiquettes", + "manage_tags_description": "Fusionner et supprimer les balises de réponse.", + "merge": "Fusionner", + "no_tag_found": "Aucun tag trouvé", + "search_tags": "Tags de recherche...", + "tag": "Étiquette", + "tag_already_exists": "Le tag existe déjà", + "tag_deleted": "Tag supprimé", + "tag_updated": "Étiquette mise à jour", + "tags_merged": "Étiquettes fusionnées", + "unique_constraint_failed_on_the_fields": "Échec de la contrainte unique sur les champs" + }, + "teams": { + "manage_teams": "Gérer les équipes", + "no_teams_found": "Aucune équipe trouvée", + "only_organization_owners_and_managers_can_manage_teams": "Seuls les propriétaires et les gestionnaires de l'organisation peuvent gérer les équipes.", + "permission": "Permission", + "team_name": "Nom de l'équipe", + "team_settings_description": "Les équipes et leurs membres peuvent accéder à ce projet et à ses enquêtes. Les propriétaires et les gestionnaires de l'organisation peuvent accorder cet accès." + } + }, + "projects_environments_organizations_not_found": "Projets, environnements ou organisations non trouvés", + "segments": { + "add_filter_below": "Ajouter un filtre ci-dessous", + "add_your_first_filter_to_get_started": "Ajoutez votre premier filtre pour commencer", + "cannot_delete_segment_used_in_surveys": "Vous ne pouvez pas supprimer ce segment car il est encore utilisé dans ces enquêtes :", + "clone_and_edit_segment": "Cloner et modifier le segment", + "create_group": "Créer un groupe", + "create_your_first_segment": "Créez votre premier segment pour commencer", + "delete_segment": "Supprimer le segment", + "desktop": "Bureau", + "devices": "Appareils", + "edit_segment": "Modifier le segment", + "error_resetting_filters": "Erreur lors de la réinitialisation des filtres", + "error_saving_segment": "Erreur lors de l'enregistrement du segment", + "ex_fully_activated_recurring_users": "Ex. Utilisateurs récurrents entièrement activés", + "ex_power_users": "Ex. Utilisateurs avancés", + "filters_reset_successfully": "Filtres réinitialisés avec succès", + "here": "ici", + "hide_filters": "Cacher les filtres", + "identifying_users": "identification des utilisateurs", + "invalid_segment": "Segment invalide", + "invalid_segment_filters": "Filtres invalides. Veuillez vérifier les filtres et réessayer.", + "load_segment": "Charger le segment", + "most_active_users_in_the_last_30_days": "Utilisateurs les plus actifs au cours des 30 derniers jours", + "no_attributes_yet": "Aucun attribut pour le moment !", + "no_filters_yet": "Il n'y a pas encore de filtres !", + "no_segments_yet": "Vous n'avez actuellement aucun segment enregistré.", + "person_and_attributes": "Personne et Attributs", + "phone": "Téléphone", + "please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Veuillez supprimer le segment de ces enquêtes afin de le supprimer.", + "pre_segment_users": "Précisez vos utilisateurs à l'avance avec des filtres d'attributs.", + "remove_all_filters": "Supprimer tous les filtres", + "reset_all_filters": "Réinitialiser tous les filtres", + "save_as_new_segment": "Enregistrer comme nouveau segment", + "save_your_filters_as_a_segment_to_use_it_in_other_surveys": "Enregistrez vos filtres en tant que segment pour les utiliser dans d'autres enquêtes.", + "segment_created_successfully": "Segment créé avec succès !", + "segment_deleted_successfully": "Segment supprimé avec succès !", + "segment_id": "ID de segment", + "segment_saved_successfully": "Segment enregistré avec succès", + "segment_updated_successfully": "Segment mis à jour avec succès !", + "segments_help_you_target_users_with_same_characteristics_easily": "Les segments vous aident à cibler facilement les utilisateurs ayant les mêmes caractéristiques.", + "target_audience": "Public cible", + "this_action_resets_all_filters_in_this_survey": "Cette action réinitialise tous les filtres de cette enquête.", + "this_segment_is_used_in_other_surveys": "Ce segment est utilisé dans d'autres enquêtes. Apportez des modifications.", + "title_is_required": "Le titre est requis.", + "unknown_filter_type": "Type de filtre inconnu", + "unlock_segments_description": "Organisez les contacts en segments pour cibler des groupes d'utilisateurs spécifiques", + "unlock_segments_title": "Débloquez des segments avec un plan supérieur.", + "user_targeting_is_currently_only_available_when": "La ciblage des utilisateurs est actuellement disponible uniquement lorsque", + "value_cannot_be_empty": "La valeur ne peut pas être vide.", + "value_must_be_a_number": "La valeur doit être un nombre.", + "view_filters": "Filtres de vue", + "where": "Où", + "with_the_formbricks_sdk": "avec le SDK Formbricks" + }, + "settings": { + "api_keys": { + "add_api_key": "Ajouter une clé API", + "add_permission": "Ajouter une permission", + "api_keys_description": "Gérer les clés API pour accéder aux API de gestion de Formbricks" + }, + "billing": { + "10000_monthly_responses": "10000 Réponses Mensuelles", + "1500_monthly_responses": "1500 Réponses Mensuelles", + "2000_monthly_identified_users": "2000 Utilisateurs Identifiés Mensuels", + "30000_monthly_identified_users": "30000 Utilisateurs Identifiés Mensuels", + "3_projects": "3 Projets", + "5000_monthly_responses": "5000 Réponses Mensuelles", + "5_projects": "5 Projets", + "7500_monthly_identified_users": "7500 Utilisateurs Identifiés Mensuels", + "advanced_targeting": "Ciblage Avancé", + "all_integrations": "Toutes les intégrations", + "all_surveying_features": "Tous les outils d'arpentage", + "annually": "Annuellement", + "api_webhooks": "API et Webhooks", + "app_surveys": "Sondages d'application", + "contact_us": "Contactez-nous", + "current": "Actuel", + "current_plan": "Plan actuel", + "current_tier_limit": "Limite de niveau actuel", + "custom_miu_limit": "Limite MIU personnalisé", + "custom_project_limit": "Limite de projet personnalisé", + "customer_success_manager": "Responsable de la réussite client", + "email_embedded_surveys": "Sondages intégrés par e-mail", + "email_support": "Support par e-mail", + "enterprise": "Entreprise", + "enterprise_description": "Soutien premium et limites personnalisées.", + "everybody_has_the_free_plan_by_default": "Tout le monde a le plan gratuit par défaut !", + "everything_in_free": "Tout est gratuit", + "everything_in_scale": "Tout à l'échelle", + "everything_in_startup": "Tout dans le Startup", + "free": "Gratuit", + "free_description": "Sondages illimités, membres d'équipe, et plus encore.", + "get_2_months_free": "Obtenez 2 mois gratuits", + "get_in_touch": "Prenez contact", + "link_surveys": "Sondages par lien (partageables)", + "logic_jumps_hidden_fields_recurring_surveys": "Sauts logiques, champs cachés, enquêtes récurrentes, etc.", + "manage_card_details": "Gérer les détails de la carte", + "manage_subscription": "Gérer l'abonnement", + "monthly": "Mensuel", + "monthly_identified_users": "Utilisateurs Identifiés Mensuels", + "multi_language_surveys": "Sondages multilingues", + "per_month": "par mois", + "per_year": "par an", + "plan_upgraded_successfully": "Plan mis à jour avec succès", + "premium_support_with_slas": "Soutien premium avec SLA", + "priority_support": "Soutien Prioritaire", + "remove_branding": "Supprimer la marque", + "say_hi": "Dis bonjour !", + "scale": "Échelle", + "scale_description": "Fonctionnalités avancées pour développer votre entreprise.", + "startup": "Startup", + "startup_description": "Tout est gratuit avec des fonctionnalités supplémentaires.", + "switch_plan": "Changer de plan", + "switch_plan_confirmation_text": "Êtes-vous sûr de vouloir passer au plan {plan} ? Vous serez facturé {price} {period}.", + "team_access_roles": "Rôles d'accès d'équipe", + "technical_onboarding": "Intégration technique", + "unable_to_upgrade_plan": "Impossible de mettre à niveau le plan", + "unlimited_apps_websites": "Applications et sites Web illimités", + "unlimited_miu": "MIU Illimité", + "unlimited_projects": "Projets illimités", + "unlimited_responses": "Réponses illimitées", + "unlimited_surveys": "Sondages illimités", + "unlimited_team_members": "Membres d'équipe illimités", + "upgrade": "Mise à niveau", + "uptime_sla_99": "SLA de disponibilité (99%)", + "website_surveys": "Sondages de site web" + }, + "enterprise": { + "ai": "Analyse IA", + "audit_logs": "Journaux d'audit", + "coming_soon": "À venir bientôt", + "contacts_and_segments": "Gestion des contacts et des segments", + "enterprise_features": "Fonctionnalités d'entreprise", + "get_an_enterprise_license_to_get_access_to_all_features": "Obtenez une licence Entreprise pour accéder à toutes les fonctionnalités.", + "keep_full_control_over_your_data_privacy_and_security": "Gardez un contrôle total sur la confidentialité et la sécurité de vos données.", + "no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Aucun appel nécessaire, aucune obligation : Demandez une licence d'essai gratuite de 30 jours pour tester toutes les fonctionnalités en remplissant ce formulaire :", + "no_credit_card_no_sales_call_just_test_it": "Aucune carte de crédit. Aucun appel de vente. Testez-le simplement :)", + "on_request": "Sur demande", + "organization_roles": "Rôles d'organisation (Administrateur, Éditeur, Développeur, etc.)", + "questions_please_reach_out_to": "Des questions ? Veuillez contacter", + "request_30_day_trial_license": "Demander une licence d'essai de 30 jours", + "saml_sso": "SAML SSO", + "service_level_agreement": "Accord de niveau de service", + "soc2_hipaa_iso_27001_compliance_check": "Vérification de conformité SOC2, HIPAA, ISO 27001", + "sso": "SSO (Google, Microsoft, OpenID Connect)", + "teams": "Équipes et Rôles d'Accès (Lire, Lire et Écrire, Gérer)", + "unlock_the_full_power_of_formbricks_free_for_30_days": "Débloquez tout le potentiel de Formbricks. Gratuit pendant 30 jours.", + "your_enterprise_license_is_active_all_features_unlocked": "Votre licence d'entreprise est active. Toutes les fonctionnalités sont déverrouillées." + }, + "general": { + "bulk_invite_warning_description": "Dans le plan gratuit, tous les membres de l'organisation se voient toujours attribuer le rôle \"Owner\".", + "cannot_delete_only_organization": "C'est votre seule organisation, elle ne peut pas être supprimée. Créez d'abord une nouvelle organisation.", + "cannot_leave_only_organization": "Vous ne pouvez pas quitter cette organisation car c'est votre seule organisation. Créez d'abord une nouvelle organisation.", + "copy_invite_link_to_clipboard": "Copier le lien d'invitation dans le presse-papiers", + "create_new_organization": "Créer une nouvelle organisation", + "create_new_organization_description": "Créer une nouvelle organisation pour gérer un ensemble différent de projets.", + "customize_email_with_a_higher_plan": "Personnalisez l'e-mail avec un plan supérieur", + "delete_organization": "Supprimer l'organisation", + "delete_organization_description": "Supprimer l'organisation avec tous ses projets, y compris toutes les enquêtes, réponses, personnes, actions et attributs.", + "delete_organization_warning": "Avant de procéder à la suppression de cette organisation, veuillez prendre connaissance des conséquences suivantes :", + "delete_organization_warning_1": "Suppression permanente de tous les projets liés à cette organisation.", + "delete_organization_warning_2": "Cette action ne peut pas être annulée. Si c'est parti, c'est parti.", + "delete_organization_warning_3": "Veuillez entrer {organizationName} dans le champ suivant pour confirmer la suppression définitive de cette organisation :", + "eliminate_branding_with_whitelabel": "Éliminez la marque Formbricks et activez des options de personnalisation supplémentaires.", + "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.", + "enable_formbricks_ai": "Activer Formbricks IA", + "error_deleting_organization_please_try_again": "Erreur lors de la suppression de l'organisation. Veuillez réessayer.", + "formbricks_ai": "Formbricks IA", + "formbricks_ai_description": "Obtenez des insights personnalisés à partir de vos réponses au sondage avec Formbricks AI.", + "formbricks_ai_disable_success_message": "Formbricks AI désactivé avec succès.", + "formbricks_ai_enable_success_message": "Formbricks AI activé avec succès.", + "formbricks_ai_privacy_policy_text": "En activant Formbricks AI, vous acceptez les mises à jour", + "from_your_organization": "de votre organisation", + "invitation_sent_once_more": "Invitation envoyée une fois de plus.", + "invite_deleted_successfully": "Invitation supprimée avec succès", + "invited_on": "Invité le {date}", + "invites_failed": "Invitations échouées", + "leave_organization": "Quitter l'organisation", + "leave_organization_description": "Vous quitterez cette organisation et perdrez l'accès à toutes les enquêtes et réponses. Vous ne pourrez revenir que si vous êtes de nouveau invité.", + "leave_organization_ok_btn_text": "Oui, quitter l'organisation", + "leave_organization_title": "Es-tu sûr ?", + "logo_in_email_header": "Logo dans l'en-tête de l'e-mail", + "logo_removed_successfully": "Logo supprimé avec succès", + "logo_saved_successfully": "Logo enregistré avec succès", + "manage_members": "Gérer les membres", + "manage_members_description": "Ajouter ou supprimer des membres dans votre organisation.", + "member_deleted_successfully": "Membre supprimé avec succès", + "member_invited_successfully": "Membre invité avec succès", + "once_its_gone_its_gone": "Une fois que c'est parti, c'est parti.", + "only_org_owner_can_perform_action": "Seules les personnes ayant un rôle d'administrateur dans l'organisation peuvent accéder à ce paramètre.", + "organization_created_successfully": "Organisation créée avec succès !", + "organization_deleted_successfully": "Organisation supprimée avec succès.", + "organization_invite_link_ready": "Le lien d'invitation de votre organisation est prêt !", + "organization_name": "Nom de l'organisation", + "organization_name_description": "Donnez à votre organisation un nom descriptif.", + "organization_name_placeholder": "e.g. Power Puff Girls", + "organization_name_updated_successfully": "Nom de l'organisation mis à jour avec succès", + "organization_settings": "Paramètres de l'organisation", + "please_add_a_logo": "Veuillez ajouter un logo", + "please_check_csv_file": "Veuillez vérifier le fichier CSV et vous assurer qu'il est conforme à notre format.", + "please_save_logo_before_sending_test_email": "Veuillez enregistrer le logo avant d'envoyer un e-mail de test.", + "remove_logo": "Supprimer le logo", + "replace_logo": "Remplacer le logo", + "resend_invitation_email": "Renvoyer l'e-mail d'invitation", + "share_invite_link": "Partager le lien d'invitation", + "share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :", + "test_email_sent_successfully": "E-mail de test envoyé avec succès", + "use_multi_language_surveys_with_a_higher_plan": "Utilisez des sondages multilingues avec un plan supérieur", + "use_multi_language_surveys_with_a_higher_plan_description": "Interrogez vos utilisateurs dans différentes langues." + }, + "notifications": { + "auto_subscribe_to_new_surveys": "S'abonner automatiquement aux nouveaux sondages", + "email_alerts_surveys": "Alertes par e-mail (Enquêtes)", + "every_response": "Chaque réponse", + "every_response_tooltip": "Envoie des réponses complètes, pas de réponses partielles.", + "need_slack_or_discord_notifications": "Besoin de notifications Slack ou Discord", + "notification_settings_updated": "Paramètres de notification mis à jour", + "set_up_an_alert_to_get_an_email_on_new_responses": "Configurez une alerte pour recevoir un e-mail lors de nouvelles réponses.", + "stay_up_to_date_with_a_Weekly_every_Monday": "Restez à jour avec un hebdomadaire chaque lundi.", + "use_the_integration": "Utilisez l'intégration", + "want_to_loop_in_organization_mates": "Voulez-vous inclure des collègues de l'organisation ?", + "weekly_summary_projects": "Résumé hebdomadaire (Projets)", + "you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore": "Vous ne serez plus automatiquement abonné aux enquêtes de cette organisation !", + "you_will_not_receive_any_more_emails_for_responses_on_this_survey": "Vous ne recevrez plus d'e-mails concernant les réponses à cette enquête !" + }, + "profile": { + "account_deletion_consequences_warning": "Conséquences de la suppression de compte", + "avatar_update_failed": "La mise à jour de l'avatar a échoué. Veuillez réessayer.", + "backup_code": "Code de sauvegarde", + "change_image": "Changer l'image", + "confirm_delete_account": "Supprimez votre compte avec toutes vos informations personnelles et données.", + "confirm_delete_my_account": "Supprimer mon compte", + "confirm_your_current_password_to_get_started": "Confirmez votre mot de passe actuel pour commencer.", + "delete_account": "Supprimer le compte", + "disable_two_factor_authentication": "Désactiver l'authentification à deux facteurs", + "disable_two_factor_authentication_description": "Si vous devez désactiver l'authentification à deux facteurs, nous vous recommandons de la réactiver dès que possible.", + "each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Chaque code de sauvegarde peut être utilisé exactement une fois pour accorder l'accès sans votre authentificateur.", + "enable_two_factor_authentication": "Activer l'authentification à deux facteurs", + "enter_the_code_from_your_authenticator_app_below": "Entrez le code de votre application d'authentification ci-dessous.", + "file_size_must_be_less_than_10mb": "La taille du fichier doit être inférieure à 10 Mo.", + "invalid_file_type": "Type de fichier invalide. Seuls les fichiers JPEG, PNG et WEBP sont autorisés.", + "lost_access": "Accès perdu", + "or_enter_the_following_code_manually": "Ou entrez le code suivant manuellement :", + "organization_identification": "Aidez votre organisation à vous identifier sur Formbricks", + "organizations_delete_message": "Tu es le seul propriétaire de ces organisations, elles seront aussi supprimées.", + "permanent_removal_of_all_of_your_personal_information_and_data": "Suppression permanente de toutes vos informations et données personnelles.", + "personal_information": "Informations personnelles", + "please_enter_email_to_confirm_account_deletion": "Veuillez entrer {email} dans le champ suivant pour confirmer la suppression définitive de votre compte :", + "profile_updated_successfully": "Votre profil a été mis à jour avec succès.", + "remove_image": "Supprimer l'image", + "save_the_following_backup_codes_in_a_safe_place": "Enregistrez les codes de sauvegarde suivants dans un endroit sûr.", + "scan_the_qr_code_below_with_your_authenticator_app": "Scannez le code QR ci-dessous avec votre application d'authentification.", + "security_description": "Gérez votre mot de passe et d'autres paramètres de sécurité comme l'authentification à deux facteurs (2FA).", + "two_factor_authentication": "Authentification à deux facteurs", + "two_factor_authentication_description": "Ajoutez une couche de sécurité supplémentaire à votre compte au cas où votre mot de passe serait volé.", + "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Authentification à deux facteurs activée. Veuillez entrer le code à six chiffres de votre application d'authentification.", + "two_factor_code": "Code à deux facteurs", + "unlock_two_factor_authentication": "Débloquez l'authentification à deux facteurs avec une offre supérieure", + "update_personal_info": "Mettez à jour vos informations personnelles", + "upload_image": "Télécharger l'image", + "warning_cannot_delete_account": "Tu es le seul propriétaire de cette organisation. Transfère la propriété à un autre membre d'abord.", + "warning_cannot_undo": "Ceci ne peut pas être annulé", + "you_must_select_a_file": "Vous devez sélectionner un fichier." + }, + "teams": { + "add_members_description": "Ajoutez des membres à l'équipe et déterminez leur rôle.", + "add_projects_description": "Contrôlez les projets auxquels les membres de l'équipe peuvent accéder.", + "all_members_added": "Tous les membres ajoutés à cette équipe.", + "all_projects_added": "Tous les projets ajoutés à cette équipe.", + "are_you_sure_you_want_to_delete_this_team": "Êtes-vous sûr de vouloir supprimer cette équipe ? Cela supprimera également l'accès à tous les projets et enquêtes associés à cette équipe.", + "billing_role_description": "N'ont accès qu'aux informations de facturation.", + "bulk_invite": "Invitation en masse", + "contributor": "Contributeur", + "create": "Créer", + "create_first_team_message": "Vous devez d'abord créer une équipe.", + "create_new_team": "Créer une nouvelle équipe", + "delete_team": "Supprimer l'équipe", + "empty_teams_state": "Créez votre première équipe.", + "enter_team_name": "Entrez le nom de l'équipe", + "individual": "individuelle", + "invite_member": "Inviter un membre", + "invite_member_description": "Ajoutez vos collègues à cette organisation.", + "manage": "Gérer", + "manage_team": "Gérer l'équipe", + "manage_team_disabled": "Seuls les propriétaires de l'organisation, les gestionnaires et les administrateurs d'équipe peuvent gérer les équipes.", + "manager_role_description": "Les gestionnaires peuvent accéder à tous les projets et ajouter et supprimer des membres.", + "member_role_description": "Les membres peuvent travailler sur des projets sélectionnés.", + "member_role_info_message": "Pour donner accès à un projet aux nouveaux membres, veuillez les ajouter à une équipe ci-dessous. Avec les équipes, vous pouvez gérer qui a accès à quel projet.", + "owner_role_description": "Les propriétaires ont un contrôle total sur l'organisation.", + "please_fill_all_member_fields": "Veuillez remplir tous les champs pour ajouter un nouveau membre.", + "please_fill_all_project_fields": "Veuillez remplir tous les champs pour ajouter un nouveau projet.", + "read": "Lire", + "read_write": "Lire et Écrire", + "team_admin": "Administrateur d'équipe", + "team_created_successfully": "Équipe créée avec succès.", + "team_deleted_successfully": "Équipe supprimée avec succès.", + "team_deletion_not_allowed": "Vous n'êtes pas autorisé à supprimer cette équipe.", + "team_name": "Nom de l'équipe", + "team_name_settings_title": "Paramètres de {teamName}", + "team_select_placeholder": "Rechercher le nom de l'équipe...", + "team_settings_description": "Gérez les membres de l'équipe, les droits d'accès et plus encore.", + "team_updated_successfully": "Équipe mise à jour avec succès", + "teams": "Équipes", + "teams_description": "Attribuez des membres à des équipes et donnez aux équipes accès aux projets.", + "unlock_teams_description": "Gérez les membres de l'organisation qui ont accès à des projets et enquêtes spécifiques.", + "unlock_teams_title": "Débloquez Teams avec un forfait supérieur.", + "upgrade_plan_notice_message": "Débloquez les rôles d'organisation avec un plan supérieur.", + "you_are_a_member": "Vous êtes un membre" + } + }, + "surveys": { + "all_set_time_to_create_first_survey": "Vous êtes prêt ! Il est temps de créer votre première enquête.", + "alphabetical": "Alphabétique", + "copy_survey": "Copier l'enquête", + "copy_survey_description": "Copier cette enquête dans un autre environnement", + "copy_survey_error": "Échec de la copie du sondage", + "copy_survey_link_to_clipboard": "Copier le lien du sondage dans le presse-papiers", + "copy_survey_success": "Enquête copiée avec succès !", + "delete_survey_and_responses_warning": "Êtes-vous sûr de vouloir supprimer cette enquête et toutes ses réponses ? Cette action ne peut pas être annulée.", + "edit": { + "1_choose_the_default_language_for_this_survey": "1. Choisissez la langue par défaut pour ce sondage :", + "2_activate_translation_for_specific_languages": "2. Activer la traduction pour des langues spécifiques :", + "add": "Ajouter +", + "add_a_delay_or_auto_close_the_survey": "Ajouter un délai ou fermer automatiquement l'enquête", + "add_a_four_digit_pin": "Ajoutez un code PIN à quatre chiffres.", + "add_a_new_question_to_your_survey": "Ajouter une nouvelle question à votre enquête", + "add_a_variable_to_calculate": "Ajouter une variable à calculer", + "add_action_below": "Ajouter une action ci-dessous", + "add_choice_below": "Ajouter une option ci-dessous", + "add_color_coding": "Ajouter un code couleur", + "add_color_coding_description": "Ajoutez des codes de couleur rouge, orange et vert aux options.", + "add_column": "Ajouter une colonne", + "add_condition_below": "Ajouter une condition ci-dessous", + "add_custom_styles": "Ajouter des styles personnalisés", + "add_delay_before_showing_survey": "Ajouter un délai avant d'afficher l'enquête", + "add_description": "Ajouter une description", + "add_ending": "Ajouter une fin", + "add_ending_below": "Ajouter une fin ci-dessous", + "add_hidden_field_id": "Ajouter un champ caché ID", + "add_highlight_border": "Ajouter une bordure de surlignage", + "add_highlight_border_description": "Ajoutez une bordure extérieure à votre carte d'enquête.", + "add_logic": "Ajouter de la logique", + "add_option": "Ajouter une option", + "add_other": "Ajouter \"Autre", + "add_photo_or_video": "Ajouter une photo ou une vidéo", + "add_pin": "Ajouter un code PIN", + "add_question": "Ajouter une question", + "add_question_below": "Ajouter une question ci-dessous", + "add_row": "Ajouter une ligne", + "add_variable": "Ajouter une variable", + "address_fields": "Champs d'adresse", + "address_line_1": "Ligne d'adresse 1", + "address_line_2": "Ligne d'adresse 2", + "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_other_answers_will_continue_to": "Toutes les autres réponses continueront à", + "allow_file_type": "Autoriser le type de fichier", + "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", + "always_show_survey": "Afficher toujours l'enquête", + "and_launch_surveys_in_your_website_or_app": "et lancez des enquêtes sur votre site web ou votre application.", + "animation": "Animation", + "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", + "auto_close_on_inactivity": "Fermeture automatique en cas d'inactivité", + "automatically_close_survey_after": "Fermer automatiquement l'enquête après", + "automatically_close_the_survey_after_a_certain_number_of_responses": "Fermer automatiquement l'enquête après un certain nombre de réponses.", + "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fermer automatiquement l'enquête si l'utilisateur ne répond pas après un certain nombre de secondes.", + "automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Ferme automatiquement l'enquête au début de la journée (UTC).", + "automatically_mark_the_survey_as_complete_after": "Marquer automatiquement l'enquête comme terminée après", + "automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Libérer automatiquement l'enquête au début de la journée (UTC).", + "back_button_label": "Label du bouton \"Retour''", + "background_styling": "Style de fond", + "blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "Bloque les enquêtes si une soumission avec l'Identifiant à Usage Unique (suId) existe déjà.", + "blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "Bloque les enquêtes si l'URL de l'enquête n'a pas d'Identifiant d'Utilisation Unique (suId).", + "brand_color": "Couleur de marque", + "brightness": "Luminosité", + "button_label": "Label du bouton", + "button_to_continue_in_survey": "Bouton pour continuer dans l'enquête", + "button_to_link_to_external_url": "Bouton pour lier à une URL externe", + "button_url": "URL du bouton", + "cal_username": "Nom d'utilisateur Cal.com ou nom d'utilisateur/événement", + "calculate": "Calculer", + "capture_a_new_action_to_trigger_a_survey_on": "Capturez une nouvelle action pour déclencher une enquête.", + "capture_new_action": "Capturer une nouvelle action", + "card_arrangement_for_survey_type_derived": "Disposition des cartes pour les enquêtes {surveyTypeDerived}", + "card_background_color": "Couleur de fond de la carte", + "card_border_color": "Couleur de la bordure de la carte", + "card_shadow_color": "Couleur de l'ombre de la carte", + "card_styling": "Style de carte", + "casual": "Décontracté", + "caution_text": "Les changements entraîneront des incohérences.", + "centered_modal_overlay_color": "Couleur de superposition modale centrée", + "change_anyway": "Changer de toute façon", + "change_background": "Changer l'arrière-plan", + "change_question_type": "Changer le type de question", + "change_the_background_color_of_the_card": "Changez la couleur de fond de la carte.", + "change_the_background_color_of_the_input_fields": "Changez la couleur de fond des champs de saisie.", + "change_the_background_to_a_color_image_or_animation": "Changez l'arrière-plan en une couleur, une image ou une animation.", + "change_the_border_color_of_the_card": "Changez la couleur de la bordure de la carte.", + "change_the_border_color_of_the_input_fields": "Changez la couleur de la bordure des champs de saisie.", + "change_the_border_radius_of_the_card_and_the_inputs": "Changez le rayon de bordure de la carte et des champs de saisie.", + "change_the_brand_color_of_the_survey": "Changez la couleur de la marque du sondage.", + "change_the_placement_of_this_survey": "Changez le placement de cette enquête.", + "change_the_question_color_of_the_survey": "Changez la couleur des questions du sondage.", + "change_the_shadow_color_of_the_card": "Changez la couleur de l'ombre de la carte.", + "changes_saved": "Modifications enregistrées.", + "character_limit_toggle_description": "Limitez la longueur des réponses.", + "character_limit_toggle_title": "Ajouter des limites de caractères", + "checkbox_label": "Étiquette de case à cocher", + "choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.", + "choose_where_to_run_the_survey": "Choisissez où réaliser l'enquête.", + "city": "Ville", + "close_survey_on_date": "Clôturer l'enquête à la date", + "close_survey_on_response_limit": "Fermer l'enquête sur la limite de réponse", + "color": "Couleur", + "columns": "Colonnes", + "company": "Société", + "company_logo": "Logo de l'entreprise", + "completed_responses": "réponses complètes.", + "concat": "Concat +", + "conditional_logic": "Logique conditionnelle", + "confirm_default_language": "Confirmer la langue par défaut", + "confirm_survey_changes": "Confirmer les modifications de l'enquête", + "contact_fields": "Champs de contact", + "contains": "Contient", + "continue_to_settings": "Continuer vers les paramètres", + "control_which_file_types_can_be_uploaded": "Contrôlez quels types de fichiers peuvent être téléchargés.", + "convert_to_multiple_choice": "Convertir en choix multiples", + "convert_to_single_choice": "Convertir en choix unique", + "country": "Pays", + "create_group": "Créer un groupe", + "create_your_own_survey": "Créez votre propre enquête", + "css_selector": "Sélecteur CSS", + "custom_hostname": "Nom d'hôte personnalisé", + "darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.", + "date_format": "Format de date", + "days_before_showing_this_survey_again": "jours avant de montrer à nouveau cette enquête.", + "decide_how_often_people_can_answer_this_survey": "Décidez à quelle fréquence les gens peuvent répondre à cette enquête.", + "delete_choice": "Supprimer l'option", + "description": "Description", + "disable_the_visibility_of_survey_progress": "Désactiver la visibilité de la progression du sondage.", + "display_an_estimate_of_completion_time_for_survey": "Afficher une estimation du temps de complétion pour l'enquête.", + "display_number_of_responses_for_survey": "Afficher le nombre de réponses pour l'enquête", + "divide": "Diviser /", + "does_not_contain": "Ne contient pas", + "does_not_end_with": "Ne se termine pas par", + "does_not_equal": "n'est pas égal à", + "does_not_include_all_of": "n'inclut pas tout", + "does_not_include_one_of": "n'inclut pas un de", + "does_not_start_with": "Ne commence pas par", + "edit_recall": "Modifier le rappel", + "edit_translations": "Modifier les traductions {lang}", + "enable_encryption_of_single_use_id_suid_in_survey_url": "Activer le chiffrement de l'identifiant à usage unique (suId) dans l'URL de l'enquête.", + "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permettre aux participants de changer la langue de l'enquête à tout moment pendant celle-ci.", + "end_screen_card": "Carte de fin d'écran", + "ending_card": "Carte de fin", + "ending_card_used_in_logic": "Cette carte de fin est utilisée dans la logique de la question '{'questionIndex'}'.", + "ends_with": "Se termine par", + "equals": "Égal", + "equals_one_of": "Égal à l'un de", + "error_publishing_survey": "Une erreur est survenue lors de la publication de l'enquête.", + "error_saving_changes": "Erreur lors de l'enregistrement des modifications", + "even_after_they_submitted_a_response_e_g_feedback_box": "Même après avoir soumis une réponse (par exemple, la boîte de feedback)", + "everyone": "Tout le monde", + "fallback_missing": "Fallback manquant", + "fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.", + "field_name_eg_score_price": "Nom du champ par exemple, score, prix", + "first_name": "Prénom", + "five_points_recommended": "5 points (recommandé)", + "follow_ups": "Relances", + "follow_ups_delete_modal_text": "Êtes-vous sûr de vouloir supprimer ce suivi ?", + "follow_ups_delete_modal_title": "Supprimer le suivi ?", + "follow_ups_empty_description": "Envoyez des messages aux répondants, à vous-même ou à vos coéquipiers.", + "follow_ups_empty_heading": "Envoyer des relances automatiques", + "follow_ups_ending_card_delete_modal_text": "Cette carte de fin est utilisée dans les suivis. La supprimer la retirera de tous les suivis. Êtes-vous sûr de vouloir la supprimer ?", + "follow_ups_ending_card_delete_modal_title": "Supprimer la carte de fin ?", + "follow_ups_hidden_field_error": "Le champ caché est utilisé dans un suivi. Veuillez d'abord le supprimer du suivi.", + "follow_ups_item_ending_tag": "Fin(s)", + "follow_ups_item_issue_detected_tag": "Problème détecté", + "follow_ups_item_response_tag": "Une réponse quelconque", + "follow_ups_item_send_email_tag": "Envoyer un e-mail", + "follow_ups_modal_action_attach_response_data_description": "Ajouter les données de la réponse à l'enquête au suivi", + "follow_ups_modal_action_attach_response_data_label": "Joindre les données de réponse", + "follow_ups_modal_action_body_label": "Corps", + "follow_ups_modal_action_body_placeholder": "Corps de l'email", + "follow_ups_modal_action_email_content": "Contenu de l'email", + "follow_ups_modal_action_email_settings": "Paramètres de messagerie", + "follow_ups_modal_action_from_description": "Adresse e-mail à partir de laquelle envoyer l'e-mail", + "follow_ups_modal_action_from_label": "De", + "follow_ups_modal_action_label": "Action", + "follow_ups_modal_action_replyTo_description": "Si le destinataire clique sur répondre, l'adresse e-mail suivante le recevra.", + "follow_ups_modal_action_replyTo_label": "Répondre à", + "follow_ups_modal_action_subject": "Merci pour vos réponses !", + "follow_ups_modal_action_subject_label": "Sujet", + "follow_ups_modal_action_subject_placeholder": "Objet de l'email", + "follow_ups_modal_action_to_description": "Adresse e-mail à laquelle envoyer l'e-mail", + "follow_ups_modal_action_to_label": "à", + "follow_ups_modal_action_to_warning": "Aucun champ d'email détecté dans l'enquête", + "follow_ups_modal_create_heading": "Créer un nouveau suivi", + "follow_ups_modal_edit_heading": "Modifier ce suivi", + "follow_ups_modal_edit_no_id": "Aucun identifiant de suivi d'enquête fourni, impossible de mettre à jour le suivi de l'enquête.", + "follow_ups_modal_name_label": "Nom de suivi", + "follow_ups_modal_name_placeholder": "Nommez votre suivi", + "follow_ups_modal_subheading": "Envoyez des messages aux répondants, à vous-même ou à vos coéquipiers", + "follow_ups_modal_trigger_description": "Quand ce suivi devrait-il être déclenché ?", + "follow_ups_modal_trigger_label": "Déclencheur", + "follow_ups_modal_trigger_type_ending": "Le répondant voit une fin spécifique", + "follow_ups_modal_trigger_type_ending_select": "Choisir des fins :", + "follow_ups_modal_trigger_type_ending_warning": "Aucune fin trouvée dans l'enquête !", + "follow_ups_modal_trigger_type_response": "Le répondant complète l'enquête", + "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_ai_description": "Décrivez votre enquête et laissez l'IA de Formbricks créer l'enquête pour vous.", + "formbricks_ai_generate": "Générer", + "formbricks_ai_prompt_placeholder": "Saisissez les informations de l'enquête (par exemple, les sujets clés à aborder)", + "formbricks_sdk_is_not_connected": "Le SDK Formbricks n'est pas connecté", + "four_points": "4 points", + "heading": "En-tête", + "hidden_field_added_successfully": "Champ caché ajouté avec succès", + "hide_advanced_settings": "Cacher les paramètres avancés", + "hide_back_button": "Masquer le bouton 'Retour'", + "hide_back_button_description": "Ne pas afficher le bouton retour dans l'enquête", + "hide_logo": "Cacher le logo", + "hide_progress_bar": "Cacher la barre de progression", + "hide_the_logo_in_this_specific_survey": "Cacher le logo dans cette enquête spécifique", + "hostname": "Nom d'hôte", + "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}", + "how_it_works": "Comment ça fonctionne", + "if_you_need_more_please": "Si vous en avez besoin de plus, s'il vous plaît", + "if_you_really_want_that_answer_ask_until_you_get_it": "Si tu veux vraiment cette réponse, demande jusqu'à ce que tu l'obtiennes.", + "ignore_waiting_time_between_surveys": "Ignorer le temps d'attente entre les enquêtes", + "image": "Image", + "includes_all_of": "Comprend tous les", + "includes_one_of": "Comprend un de", + "initial_value": "Valeur initiale", + "inner_text": "Texte interne", + "input_border_color": "Couleur de bordure d'entrée", + "input_color": "Couleur d'entrée", + "invalid_targeting": "Ciblage invalide : Veuillez vérifier vos filtres d'audience", + "invalid_video_url_warning": "Merci d'entrer une URL YouTube, Vimeo ou Loom valide. Les autres plateformes vidéo ne sont pas encore supportées.", + "invalid_youtube_url": "URL YouTube invalide", + "is_accepted": "C'est accepté", + "is_after": "est après", + "is_before": "Est avant", + "is_booked": "Est réservé", + "is_clicked": "Est cliqué", + "is_completely_submitted": "Est complètement soumis", + "is_not_set": "N'est pas défini", + "is_partially_submitted": "Est partiellement soumis", + "is_set": "Est défini", + "is_skipped": "Est ignoré", + "is_submitted": "Est soumis", + "jump_to_question": "Passer à la question", + "keep_current_order": "Conserver la commande actuelle", + "keep_showing_while_conditions_match": "Continuer à afficher tant que les conditions correspondent", + "key": "Clé", + "last_name": "Nom de famille", + "let_people_upload_up_to_25_files_at_the_same_time": "Permettre aux utilisateurs de télécharger jusqu'à 25 fichiers en même temps.", + "limit_file_types": "Limiter les types de fichiers", + "limit_the_maximum_file_size": "Limiter la taille maximale du fichier", + "limit_upload_file_size_to": "Limiter la taille des fichiers téléchargés à", + "link_survey_description": "Partagez un lien vers une page d'enquête ou intégrez-le dans une page web ou un e-mail.", + "link_used_message": "Lien utilisé", + "load_segment": "Segment de chargement", + "logic_error_warning": "Changer causera des erreurs logiques", + "logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.", + "long_answer": "Longue réponse", + "lower_label": "Étiquette inférieure", + "manage_languages": "Gérer les langues", + "max_file_size": "Taille maximale du fichier", + "max_file_size_limit_is": "La taille maximale du fichier est", + "multiply": "Multiplier *", + "needed_for_self_hosted_cal_com_instance": "Nécessaire pour une instance Cal.com auto-hébergée", + "next_button_label": "Label du bouton \"Suivant\"", + "next_question": "Question suivante", + "no_hidden_fields_yet_add_first_one_below": "Aucun champ caché pour le moment. Ajoutez le premier ci-dessous.", + "no_images_found_for": "Aucune image trouvée pour ''{query}\"", + "no_languages_found_add_first_one_to_get_started": "Aucune langue trouvée. Ajoutez la première pour commencer.", + "no_variables_yet_add_first_one_below": "Aucune variable pour le moment. Ajoutez la première ci-dessous.", + "number": "Numéro", + "once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations": "Une fois défini, la langue par défaut de cette enquête ne peut être changée qu'en désactivant l'option multilingue et en supprimant toutes les traductions.", + "only_display_the_survey_to_a_subset_of_the_users": "Afficher l'enquête uniquement à un sous-ensemble des utilisateurs", + "only_lower_case_letters_numbers_and_underscores_are_allowed": "Seules les lettres minuscules, les chiffres et les underscores sont autorisés.", + "only_people_who_match_your_targeting_can_be_surveyed": "Seules les personnes correspondant à votre ciblage peuvent être sondées.", + "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", + "override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.", + "overwrite_placement": "Surcharge de placement", + "overwrite_the_global_placement_of_the_survey": "Surcharger le placement global de l'enquête", + "overwrites_waiting_period_between_surveys_to_x_days": "Remplace la période d'attente entre les enquêtes par {days} jour(s).", + "pick_a_background_from_our_library_or_upload_your_own": "Choisissez un arrière-plan dans notre bibliothèque ou téléchargez le vôtre.", + "picture_idx": "Image {idx}", + "pin_can_only_contain_numbers": "Le code PIN ne peut contenir que des chiffres.", + "pin_must_be_a_four_digit_number": "Le code PIN doit être un numéro à quatre chiffres.", + "please_enter_a_file_extension": "Veuillez entrer une extension de fichier.", + "please_set_a_survey_trigger": "Veuillez définir un déclencheur d'enquête.", + "please_specify": "Veuillez préciser", + "prevent_double_submission": "Empêcher la double soumission", + "prevent_double_submission_description": "Autoriser uniquement 1 réponse par adresse e-mail", + "protect_survey_with_pin": "Protéger l'enquête par un code PIN", + "protect_survey_with_pin_description": "Seules les personnes ayant le code PIN peuvent accéder à l'enquête.", + "publish": "Publier", + "question": "Question", + "question_color": "Couleur de la question", + "question_deleted": "Question supprimée.", + "question_duplicated": "Question dupliquée.", + "question_id_updated": "ID de la question mis à jour", + "question_used_in_logic": "Cette question est utilisée dans la logique de la question '{'questionIndex'}'.", + "randomize_all": "Randomiser tout", + "randomize_all_except_last": "Randomiser tout sauf le dernier", + "range": "Plage", + "recontact_options": "Options de recontact", + "redirect_thank_you_card": "Carte de remerciement de redirection", + "redirect_to_url": "Rediriger vers l'URL", + "redirect_to_url_not_available_on_free_plan": "La redirection vers l'URL n'est pas disponible sur le plan gratuit.", + "release_survey_on_date": "Publier l'enquête à la date", + "remove_description": "Supprimer la description", + "remove_translations": "Supprimer les traductions", + "require_answer": "Réponse requise", + "required": "Requis", + "reset_to_theme_styles": "Réinitialiser aux styles de thème", + "reset_to_theme_styles_main_text": "Êtes-vous sûr de vouloir réinitialiser le style aux styles du thème ? Cela supprimera tous les styles personnalisés.", + "response_limit_can_t_be_set_to_0": "La limite de réponse ne peut pas être fixée à 0.", + "response_limit_needs_to_exceed_number_of_received_responses": "La limite de réponses doit dépasser le nombre de réponses reçues ({responseCount}).", + "response_limits_redirections_and_more": "Limites de réponse, redirections et plus.", + "response_options": "Options de réponse", + "roundness": "Rondité", + "rows": "Lignes", + "save_and_close": "Enregistrer et fermer", + "scale": "Échelle", + "search_for_images": "Rechercher des images", + "seconds_after_trigger_the_survey_will_be_closed_if_no_response": "Les secondes après le déclenchement, l'enquête sera fermée si aucune réponse n'est donnée.", + "seconds_before_showing_the_survey": "secondes avant de montrer l'enquête.", + "select_or_type_value": "Sélectionnez ou saisissez une valeur", + "select_ordering": "Choisir l'ordre", + "select_saved_action": "Sélectionner une action enregistrée", + "select_type": "Choisir le type", + "send_survey_to_audience_who_match": "Envoyer l'enquête au public qui correspond...", + "send_your_respondents_to_a_page_of_your_choice": "Envoyez vos répondants vers une page de votre choix.", + "set_the_global_placement_in_the_look_feel_settings": "Définissez le placement global dans les paramètres d'apparence.", + "seven_points": "7 points", + "show_advanced_settings": "Afficher les paramètres avancés", + "show_button": "Afficher le bouton", + "show_language_switch": "Afficher le changement de langue", + "show_multiple_times": "Afficher plusieurs fois", + "show_only_once": "Afficher une seule fois", + "show_survey_maximum_of": "Afficher le maximum du sondage de", + "show_survey_to_users": "Afficher l'enquête à % des utilisateurs", + "show_to_x_percentage_of_targeted_users": "Afficher à {percentage}% des utilisateurs ciblés", + "simple": "Simple", + "single_use_survey_links": "Liens d'enquête à usage unique", + "single_use_survey_links_description": "Autoriser uniquement 1 réponse par lien d'enquête.", + "skip_button_label": "Étiquette du bouton Ignorer", + "smiley": "Sourire", + "star": "Étoile", + "starts_with": "Commence par", + "state": "État", + "straight": "Droit", + "style_the_question_texts_descriptions_and_input_fields": "Stylisez les textes des questions, les descriptions et les champs de saisie.", + "style_the_survey_card": "Styliser la carte d'enquête.", + "styling_set_to_theme_styles": "Style défini sur les styles du thème", + "subheading": "Sous-titre", + "subtract": "Soustraire -", + "suggest_colors": "Suggérer des couleurs", + "survey_already_answered_heading": "L'enquête a déjà été répondue.", + "survey_already_answered_subheading": "Vous ne pouvez utiliser ce lien qu'une seule fois.", + "survey_completed_heading": "Enquête terminée", + "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_trigger": "Déclencheur d'enquête", + "switch_multi_lanugage_on_to_get_started": "Activez le multilingue pour commencer \uD83D\uDC49", + "targeted": "Ciblé", + "ten_points": "10 points", + "the_survey_will_be_shown_multiple_times_until_they_respond": "L'enquête sera affichée plusieurs fois jusqu'à ce qu'ils répondent.", + "the_survey_will_be_shown_once_even_if_person_doesnt_respond": "L'enquête sera affichée une fois, même si la personne ne répond pas.", + "then": "Alors", + "this_action_will_remove_all_the_translations_from_this_survey": "Cette action supprimera toutes les traductions de cette enquête.", + "this_extension_is_already_added": "Cette extension est déjà ajoutée.", + "this_file_type_is_not_supported": "Ce type de fichier n'est pas pris en charge.", + "this_setting_overwrites_your": "Ce paramètre écrase votre", + "three_points": "3 points", + "times": "fois", + "to_keep_the_placement_over_all_surveys_consistent_you_can": "Pour maintenir la cohérence du placement sur tous les sondages, vous pouvez", + "trigger_survey_when_one_of_the_actions_is_fired": "Déclencher l'enquête lorsqu'une des actions est déclenchée...", + "try_lollipop_or_mountain": "Essayez 'sucette' ou 'montagne'...", + "type_field_id": "Identifiant de champ de type", + "unlock_targeting_description": "Cibler des groupes d'utilisateurs spécifiques en fonction des attributs ou des informations sur l'appareil", + "unlock_targeting_title": "Débloquez le ciblage avec un plan supérieur.", + "unsaved_changes_warning": "Vous avez des modifications non enregistrées dans votre enquête. Souhaitez-vous les enregistrer avant de partir ?", + "until_they_submit_a_response": "Jusqu'à ce qu'ils soumettent une réponse", + "upgrade_notice_description": "Créez des sondages multilingues et débloquez de nombreuses autres fonctionnalités", + "upgrade_notice_title": "Débloquez les sondages multilingues avec un plan supérieur", + "upload": "Télécharger", + "upload_at_least_2_images": "Téléchargez au moins 2 images", + "upper_label": "Étiquette supérieure", + "url_encryption": "Chiffrement d'URL", + "url_filters": "Filtres d'URL", + "url_not_supported": "URL non supportée", + "use_with_caution": "À utiliser avec précaution", + "variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.", + "variable_name_is_already_taken_please_choose_another": "Le nom de la variable est déjà pris, veuillez en choisir un autre.", + "variable_name_must_start_with_a_letter": "Le nom de la variable doit commencer par une lettre.", + "verify_email_before_submission": "Vérifiez l'email avant la soumission", + "verify_email_before_submission_description": "Ne laissez répondre que les personnes ayant une véritable adresse e-mail.", + "wait": "Attendre", + "wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Attendez quelques secondes après le déclencheur avant de montrer l'enquête.", + "waiting_period": "période d'attente", + "welcome_message": "Message de bienvenue", + "when": "Quand", + "when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Lorsque les conditions correspondent, le temps d'attente sera ignoré et l'enquête sera affichée.", + "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_project_to_work_with_translations": "Vous devez avoir deux langues ou plus configurées dans votre projet pour travailler avec des 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", + "zip": "Zip" + }, + "error_deleting_survey": "Une erreur est survenue lors de la suppression de l'enquête.", + "failed_to_copy_link_to_results": "Échec de la copie du lien vers les résultats", + "failed_to_copy_url": "Échec de la copie de l'URL : pas dans un environnement de navigateur.", + "new_single_use_link_generated": "Nouveau lien à usage unique généré", + "new_survey": "Nouveau Sondage", + "no_surveys_created_yet": "Aucun sondage créé pour le moment", + "open_options": "Ouvrir les options", + "preview_survey_in_a_new_tab": "Aperçu de l'enquête dans un nouvel onglet", + "read_only_user_not_allowed_to_create_survey_warning": "En tant qu'utilisateur en lecture seule, vous n'êtes pas autorisé à créer des enquêtes. Veuillez demander à un utilisateur ayant un accès en écriture de créer une enquête ou à un responsable de mettre à jour votre rôle.", + "relevance": "Pertinence", + "responses": { + "address_line_1": "Ligne d'adresse 1", + "address_line_2": "Ligne d'adresse 2", + "an_error_occurred_creating_a_new_note": "Une erreur est survenue lors de la création d'une nouvelle note.", + "an_error_occurred_deleting_the_tag": "Une erreur est survenue lors de la suppression de l'étiquette.", + "an_error_occurred_resolving_a_note": "Une erreur est survenue lors de la résolution d'une note.", + "an_error_occurred_updating_a_note": "Une erreur est survenue lors de la mise à jour d'une note.", + "browser": "Navigateur", + "city": "Ville", + "company": "Société", + "completed": "Terminé ✅", + "country": "Pays", + "device": "Dispositif", + "device_info": "Informations sur l'appareil", + "email": "Email", + "first_name": "Prénom", + "how_to_identify_users": "Comment identifier les utilisateurs", + "last_name": "Nom de famille", + "not_completed": "Non terminé ⏳", + "os": "Système d'exploitation", + "person_attributes": "Attributs de la personne", + "phone": "Téléphone", + "resolve": "Résoudre", + "respondent_skipped_questions": "Le répondant a sauté ces questions.", + "response_deleted_successfully": "Réponse supprimée avec succès.", + "single_use_id": "Identifiant à usage unique", + "source": "Source", + "state_region": "État / Région", + "survey_closed": "Sondage fermé", + "tag_already_exists": "Le tag existe déjà", + "this_response_is_in_progress": "Cette réponse est en cours.", + "zip_post_code": "Code postal" + }, + "results_unpublished_successfully": "Résultats publiés avec succès.", + "search_by_survey_name": "Recherche par nom d'enquête", + "summary": { + "added_filter_for_responses_where_answer_to_question": "Filtre ajouté pour les réponses où la réponse à la question '{'questionIdx'}' est '{'filterComboBoxValue'}' - '{'filterValue'}' ", + "added_filter_for_responses_where_answer_to_question_is_skipped": "Filtre ajouté pour les réponses où la réponse à la question '{'questionIdx'}' est ignorée", + "all_responses_csv": "Tous les réponses (CSV)", + "all_responses_excel": "Tous les réponses (Excel)", + "all_time": "Tout le temps", + "almost_there": "Presque là ! Installez le widget pour commencer à recevoir des réponses.", + "average": "Moyenne", + "completed": "Terminé", + "completed_tooltip": "Nombre de fois que l'enquête a été complétée.", + "configure_alerts": "Configurer les alertes", + "congrats": "Félicitations ! Votre enquête est en ligne.", + "connect_your_website_or_app_with_formbricks_to_get_started": "Connectez votre site web ou votre application à Formbricks pour commencer.", + "copy_link_to_public_results": "Copier le lien vers les résultats publics", + "create_single_use_links": "Créer des liens à usage unique", + "create_single_use_links_description": "Acceptez uniquement une soumission par lien. Voici comment.", + "custom_range": "Plage personnalisée...", + "data_prefilling": "Préremplissage des données", + "data_prefilling_description": "Vous souhaitez préremplir certains champs dans l'enquête ? Voici comment faire.", + "define_when_and_where_the_survey_should_pop_up": "Définissez quand et où le sondage doit apparaître.", + "drop_offs": "Dépôts", + "drop_offs_tooltip": "Nombre de fois que l'enquête a été commencée mais non terminée.", + "dynamic_popup": "Dynamique (Pop-up)", + "email_sent": "Email envoyé !", + "embed_code_copied_to_clipboard": "Code d'intégration copié dans le presse-papiers !", + "embed_in_an_email": "Inclure dans un e-mail", + "embed_in_app": "Intégrer dans l'application", + "embed_mode": "Mode d'intégration", + "embed_mode_description": "Intégrez votre enquête avec un design minimaliste, en supprimant les marges et l'arrière-plan.", + "embed_on_website": "Incorporer sur le site web", + "embed_pop_up_survey_title": "Comment intégrer une enquête pop-up sur votre site web", + "embed_survey": "Intégrer l'enquête", + "enable_ai_insights_banner_button": "Activer les insights", + "enable_ai_insights_banner_description": "Vous pouvez activer la nouvelle fonctionnalité d'aperçus pour l'enquête afin d'obtenir des aperçus basés sur l'IA pour vos réponses en texte libre.", + "enable_ai_insights_banner_success": "Génération d'analyses pour cette enquête. Veuillez revenir dans quelques minutes.", + "enable_ai_insights_banner_title": "Prêt à tester les insights de l'IA ?", + "enable_ai_insights_banner_tooltip": "Veuillez nous contacter à hola@formbricks.com pour générer des insights pour cette enquête.", + "failed_to_copy_link": "Échec de la copie du lien", + "filter_added_successfully": "Filtre ajouté avec succès", + "filter_updated_successfully": "Filtre mis à jour avec succès", + "filtered_responses_csv": "Réponses filtrées (CSV)", + "filtered_responses_excel": "Réponses filtrées (Excel)", + "formbricks_email_survey_preview": "Aperçu de l'enquête par e-mail Formbricks", + "go_to_setup_checklist": "Allez à la liste de contrôle de configuration \uD83D\uDC49", + "hide_embed_code": "Cacher le code d'intégration", + "how_to_create_a_panel": "Comment créer un panneau", + "how_to_create_a_panel_step_1": "Étape 1 : Créez un compte avec Prolific", + "how_to_create_a_panel_step_1_description": "Créez un compte avec Prolific et vérifiez votre adresse e-mail.", + "how_to_create_a_panel_step_2": "Étape 2 : Créer une étude", + "how_to_create_a_panel_step_2_description": "Chez Prolific, vous créez une nouvelle étude où vous pouvez choisir votre audience préférée en fonction de centaines de caractéristiques.", + "how_to_create_a_panel_step_3": "Étape 3 : Connectez votre enquête", + "how_to_create_a_panel_step_3_description": "Configurez des champs cachés dans votre enquête Formbricks pour suivre quel participant a fourni quelle réponse.", + "how_to_create_a_panel_step_4": "Étape 4 : Lancez votre étude", + "how_to_create_a_panel_step_4_description": "Une fois que tout est configuré, vous pouvez lancer votre étude. Dans quelques heures, vous recevrez les premières réponses.", + "impressions": "Impressions", + "impressions_tooltip": "Nombre de fois que l'enquête a été consultée.", + "includes_all": "Comprend tous", + "includes_either": "Comprend soit", + "insights_disabled": "Insights désactivés", + "install_widget": "Installer le widget Formbricks", + "is_equal_to": "Est égal à", + "is_less_than": "est inférieur à", + "last_30_days": "30 derniers jours", + "last_6_months": "6 derniers mois", + "last_7_days": "7 derniers jours", + "last_month": "Le mois dernier", + "last_quarter": "dernier trimestre", + "last_year": "l'année dernière", + "link_to_public_results_copied": "Lien vers les résultats publics copié", + "make_sure_the_survey_type_is_set_to": "Assurez-vous que le type d'enquête est défini sur", + "mobile_app": "Application mobile", + "no_response_matches_filter": "Aucune réponse ne correspond à votre filtre", + "only_completed": "Uniquement terminé", + "other_values_found": "D'autres valeurs trouvées", + "overall": "Globalement", + "publish_to_web": "Publier sur le web", + "publish_to_web_warning": "Vous êtes sur le point de rendre ces résultats d'enquête publics.", + "publish_to_web_warning_description": "Les résultats de votre enquête seront publics. Toute personne en dehors de votre organisation pourra y accéder si elle a le lien.", + "quickstart_mobile_apps": "Démarrage rapide : Applications mobiles", + "quickstart_mobile_apps_description": "Pour commencer avec les enquêtes dans les applications mobiles, veuillez suivre le guide de démarrage rapide :", + "quickstart_web_apps": "Démarrage rapide : Applications web", + "quickstart_web_apps_description": "Veuillez suivre le guide de démarrage rapide pour commencer :", + "results_are_public": "Les résultats sont publics.", + "send_preview": "Envoyer un aperçu", + "send_to_panel": "Envoyer au panneau", + "setup_instructions": "Instructions d'installation", + "setup_integrations": "Configurer les intégrations", + "share_results": "Partager les résultats", + "share_the_link": "Partager le lien", + "share_the_link_to_get_responses": "Partagez le lien pour obtenir des réponses", + "show_all_responses_that_match": "Afficher toutes les réponses correspondantes", + "show_all_responses_where": "Afficher toutes les réponses où...", + "single_use_links": "Liens à usage unique", + "source_tracking": "Suivi des sources", + "source_tracking_description": "Exécutez un suivi des sources conforme au RGPD et au CCPA sans outils supplémentaires.", + "starts": "Commence", + "starts_tooltip": "Nombre de fois que l'enquête a été commencée.", + "static_iframe": "Statique (iframe)", + "survey_results_are_public": "Les résultats de votre enquête sont publics !", + "survey_results_are_shared_with_anyone_who_has_the_link": "Les résultats de votre enquête sont partagés avec quiconque possède le lien. Les résultats ne seront pas indexés par les moteurs de recherche.", + "this_month": "Ce mois-ci", + "this_quarter": "Ce trimestre", + "this_year": "Cette année", + "time_to_complete": "Temps à compléter", + "to_connect_your_website_with_formbricks": "connecter votre site web à Formbricks", + "ttc_tooltip": "Temps moyen pour compléter l'enquête.", + "unknown_question_type": "Type de question inconnu", + "unpublish_from_web": "Désactiver la publication sur le web", + "unsupported_video_tag_warning": "Votre navigateur ne prend pas en charge la balise vidéo.", + "view_embed_code": "Voir le code d'intégration", + "view_embed_code_for_email": "Voir le code d'intégration pour l'email", + "view_site": "Voir le site", + "waiting_for_response": "En attente d'une réponse \uD83E\uDDD8‍♂️", + "web_app": "application web", + "what_is_a_panel": "Qu'est-ce qu'un panneau ?", + "what_is_a_panel_answer": "Un panel est un groupe de participants sélectionnés en fonction de caractéristiques telles que l'âge, la profession, le sexe, etc.", + "what_is_prolific": "Qu'est-ce que Prolific ?", + "what_is_prolific_answer": "Nous nous associons à Prolific pour vous donner accès à un panel de plus de 200 000 participants vérifiés.", + "whats_next": "Qu'est-ce qui vient ensuite ?", + "when_do_i_need_it": "Quand en ai-je besoin ?", + "when_do_i_need_it_answer": "Si vous n'avez pas accès à suffisamment de personnes correspondant à votre public cible, il est logique de payer pour accéder à un panel.", + "you_can_do_a_lot_more_with_links_surveys": "Vous pouvez faire beaucoup plus avec des sondages par lien \uD83D\uDCA1", + "your_survey_is_public": "Votre enquête est publique.", + "youre_not_plugged_in_yet": "Vous n'êtes pas encore branché !" + }, + "survey_deleted_successfully": "Enquête supprimée avec succès !", + "survey_duplicated_successfully": "Enquête dupliquée avec succès.", + "survey_duplication_error": "Échec de la duplication de l'enquête.", + "survey_status_tooltip": "Pour mettre à jour le statut de l'enquête, mettez à jour le calendrier et fermez les paramètres dans les options de réponse à l'enquête.", + "templates": { + "all_channels": "Tous les canaux", + "all_industries": "Tous les secteurs", + "all_roles": "Tous les rôles", + "create_a_new_survey": "Créer une nouvelle enquête", + "multiple_industries": "Plusieurs secteurs", + "use_this_template": "Utilisez ce modèle", + "uses_branching_logic": "Cette enquête utilise une logique de branchement." + } + }, + "xm-templates": { + "ces": "CES", + "ces_description": "Tirez parti de chaque point de contact pour comprendre la facilité d'interaction avec le client.", + "csat": "CSAT", + "csat_description": "Mettez en œuvre les meilleures pratiques pour mesurer la satisfaction client.", + "enps": "eNPS", + "enps_description": "Retour d'information universel pour comprendre l'engagement et la satisfaction des employés.", + "five_star_rating": "Évaluation 5 étoiles", + "five_star_rating_description": "Solution de feedback universelle pour évaluer la satisfaction globale.", + "headline": "Quel type de retour aimeriez-vous recevoir ?", + "nps": "NPS", + "nps_description": "Mettez en œuvre des pratiques éprouvées pour comprendre POURQUOI les gens achètent.", + "smileys": "Émoticônes", + "smileys_description": "Utilisez des indicateurs visuels pour recueillir des retours d'expérience à travers les points de contact avec les clients." + } + }, + "organizations": { + "landing": { + "no_projects_warning_subtitle": "Contactez le propriétaire de votre organisation pour obtenir l'accès aux projets. Ou créez votre propre organisation pour commencer.", + "no_projects_warning_title": "Votre compte n'a pas encore accès à des projets." + }, + "projects": { + "new": { + "channel": { + "channel_select_subtitle": "Partage un lien ou affiche ton sondage dans des applis ou sur des sites web.", + "channel_select_title": "Quel type de sondage te faut-il ?", + "in_product_surveys": "Enquêtes dans les applications", + "in_product_surveys_description": "Réalisez des enquêtes micro-ciblées dans vos applications.", + "link_and_email_surveys": "Liens et enquêtes par e-mail", + "link_and_email_surveys_description": "Atteignez les gens partout en ligne." + }, + "mode": { + "formbricks_cx": "Formbricks CX", + "formbricks_cx_description": "Enquêtes et rapports pour comprendre ce dont vos clients ont besoin.", + "formbricks_surveys": "Enquêtes Formbricks", + "formbricks_surveys_description": "Plateforme d'enquête multi-usage pour des enquêtes sur le web, les applications et par email.", + "what_are_you_here_for": "Pourquoi êtes-vous ici ?" + }, + "settings": { + "brand_color": "Couleur de marque", + "brand_color_description": "Faites correspondre la couleur principale des enquêtes avec votre marque.", + "create_new_team": "Créer une nouvelle équipe", + "project_creation_failed": "Échec de la création du projet", + "project_name": "Nom du produit", + "project_name_description": "Comment s'appelle votre produit ?", + "project_settings_subtitle": "Lorsque les gens reconnaissent votre marque, ils sont beaucoup plus susceptibles de commencer et de compléter des réponses.", + "project_settings_title": "Fais savoir aux répondants que c'est toi", + "team_description": "Qui peut accéder à ce projet ?" + } + } + } + }, + "s": { + "check_inbox_or_spam": "Veuillez également vérifier votre dossier de spam si vous ne voyez pas l'e-mail dans votre boîte de réception.", + "completed": "Cette enquête gratuite et open-source a été fermée.", + "create_your_own": "Créez le vôtre", + "enter_pin": "Ce sondage est protégé. Entrez le code PIN ci-dessous", + "just_curious": "Juste curieux ?", + "link_invalid": "Cette enquête ne peut être réalisée que sur invitation.", + "paused": "Cette enquête gratuite et open-source est temporairement suspendue.", + "please_try_again_with_the_original_link": "Veuillez réessayer avec le lien d'origine.", + "preview_survey_questions": "Aperçu des questions de l'enquête.", + "question_preview": "Aperçu de la question", + "response_already_received": "Nous avons déjà reçu une réponse pour cette adresse e-mail.", + "response_submitted": "Une réponse liée à cette enquête et à ce contact existe déjà", + "survey_already_answered_heading": "L'enquête a déjà été répondue.", + "survey_already_answered_subheading": "Vous ne pouvez utiliser ce lien qu'une seule fois.", + "survey_sent_to": "Enquête envoyée à {email}", + "this_looks_fishy": "Cela semble louche.", + "verify_email": "Vérifiez l'email.", + "verify_email_before_submission": "Vérifiez votre email pour répondre.", + "verify_email_before_submission_button": "Vérifier", + "verify_email_before_submission_description": "Pour répondre à cette enquête, veuillez vérifier votre e-mail.", + "want_to_respond": "Voulez-vous répondre ?" + }, + "setup": { + "intro": { + "get_started": "Commencer", + "made_with_love_in_kiel": "Fabriqué avec \uD83E\uDD0D en Allemagne", + "paragraph_1": "Formbricks est une suite de gestion de l'expérience construite sur la plateforme d'enquête open source à la croissance la plus rapide au monde.", + "paragraph_2": "Réalisez des enquêtes ciblées sur des sites web, dans des applications ou partout en ligne. Collectez des informations précieuses pour créer des expériences irrésistibles pour les clients, les utilisateurs et les employés.", + "paragraph_3": "Nous sommes engagés à garantir le plus haut niveau de confidentialité des données. Auto-hébergez pour garder le contrôle total sur vos données. Toujours.", + "welcome_to_formbricks": "Bienvenue sur Formbricks !" + }, + "invite": { + "add_another_member": "Ajouter un autre membre", + "continue": "Continuer", + "failed_to_invite": "Échec de l'invitation", + "invitation_sent_to": "Invitation envoyée à", + "invite_your_organization_members": "Invitez les membres de votre organisation", + "life_s_no_fun_alone": "La vie n'est pas amusante seule.", + "skip": "Sauter", + "smtp_not_configured": "SMTP non configuré", + "smtp_not_configured_description": "Les invitations ne peuvent pas être envoyées pour le moment car le service de messagerie n'est pas configuré. Vous pourrez copier le lien d'invitation dans les paramètres de l'organisation plus tard." + }, + "organization": { + "create": { + "continue": "Continuer", + "delete_account": "Supprimer le compte", + "delete_account_description": "Si vous souhaitez supprimer votre compte, vous pouvez le faire en cliquant sur le bouton ci-dessous.", + "description": "Faites-en le vôtre.", + "no_membership_found": "Aucun abonnement trouvé !", + "no_membership_found_description": "Vous n'êtes membre d'aucune organisation pour le moment. Si vous pensez qu'il s'agit d'une erreur, veuillez contacter le propriétaire de l'organisation.", + "title": "Configurez votre organisation" + } + }, + "signup": { + "create_administrator": "Créer un administrateur", + "this_user_has_all_the_power": "Cet utilisateur a tout le pouvoir." + } + }, + "share": { + "back_to_home": "Retour à l'accueil", + "page_not_found": "Page non trouvée", + "page_not_found_description": "Désolé, nous n'avons pas pu trouver l'ID de partage des réponses que vous recherchez." + }, + "templates": { + "address": "Adresse", + "address_description": "Demander une adresse postale", + "alignment_and_engagement_survey_description": "Évaluer l'alignement des employés avec la vision, la stratégie et la communication de l'entreprise, ainsi que la collaboration au sein de l'équipe.", + "alignment_and_engagement_survey_name": "Alignement et engagement avec la vision de l'entreprise", + "alignment_and_engagement_survey_question_1_headline": "Je comprends comment mon rôle contribue à la stratégie globale de l'entreprise.", + "alignment_and_engagement_survey_question_1_lower_label": "Aucune compréhension", + "alignment_and_engagement_survey_question_1_upper_label": "Compréhension complète", + "alignment_and_engagement_survey_question_2_headline": "Je sens que mes valeurs s'alignent avec la mission et la culture de l'entreprise.", + "alignment_and_engagement_survey_question_2_lower_label": "Non aligné", + "alignment_and_engagement_survey_question_2_upper_label": "Complètement aligné", + "alignment_and_engagement_survey_question_3_headline": "Je collabore efficacement avec mon équipe pour atteindre nos objectifs.", + "alignment_and_engagement_survey_question_3_lower_label": "Mauvaise collaboration", + "alignment_and_engagement_survey_question_3_upper_label": "Excellente collaboration", + "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", + "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", + "build_product_roadmap_name_with_project_name": "Entrée de feuille de route $[projectName]", + "build_product_roadmap_question_1_headline": "Dans quelle mesure êtes-vous satisfait des fonctionnalités et de l'ergonomie de $[projectName] ?", + "build_product_roadmap_question_1_lower_label": "Pas du tout satisfait", + "build_product_roadmap_question_1_upper_label": "Extrêmement satisfait", + "build_product_roadmap_question_2_headline": "Quel est UN changement que nous pourrions apporter pour améliorer le plus votre expérience $[projectName] ?", + "build_product_roadmap_question_2_placeholder": "Entrez votre réponse ici...", + "card_abandonment_survey": "Sondage sur l'abandon de panier", + "card_abandonment_survey_description": "Comprendre les raisons derrière l'abandon de panier dans votre boutique en ligne.", + "card_abandonment_survey_question_1_button_label": "Bien sûr !", + "card_abandonment_survey_question_1_dismiss_button_label": "Non, merci.", + "card_abandonment_survey_question_1_headline": "Avez-vous 2 minutes pour nous aider à nous améliorer ?", + "card_abandonment_survey_question_1_html": "

Nous avons remarqué que vous avez laissé des articles dans votre panier. Nous aimerions comprendre pourquoi.

", + "card_abandonment_survey_question_2_choice_1": "Frais d'expédition élevés", + "card_abandonment_survey_question_2_choice_2": "J'ai trouvé un meilleur prix ailleurs", + "card_abandonment_survey_question_2_choice_3": "Juste en train de naviguer", + "card_abandonment_survey_question_2_choice_4": "Décidé de ne pas acheter", + "card_abandonment_survey_question_2_choice_5": "Problèmes de paiement", + "card_abandonment_survey_question_2_choice_6": "Autre", + "card_abandonment_survey_question_2_headline": "Quelle était la principale raison pour laquelle vous n'avez pas finalisé votre achat ?", + "card_abandonment_survey_question_2_subheader": "Veuillez sélectionner l'une des options suivantes :", + "card_abandonment_survey_question_3_headline": "Veuillez expliquer votre raison pour ne pas avoir finalisé l'achat :", + "card_abandonment_survey_question_4_headline": "Comment évalueriez-vous votre expérience d'achat globale ?", + "card_abandonment_survey_question_4_lower_label": "Très insatisfait", + "card_abandonment_survey_question_4_upper_label": "Très satisfait", + "card_abandonment_survey_question_5_choice_1": "Réduire les coûts d'expédition", + "card_abandonment_survey_question_5_choice_2": "Remises ou promotions", + "card_abandonment_survey_question_5_choice_3": "Plus d'options de paiement", + "card_abandonment_survey_question_5_choice_4": "Meilleures descriptions de produits", + "card_abandonment_survey_question_5_choice_5": "Navigation du site améliorée", + "card_abandonment_survey_question_5_choice_6": "Autre", + "card_abandonment_survey_question_5_headline": "Quels facteurs vous inciteraient à finaliser votre achat à l'avenir ?", + "card_abandonment_survey_question_5_subheader": "Veuillez sélectionner tout ce qui s'applique :", + "card_abandonment_survey_question_6_headline": "Souhaitez-vous recevoir un code de réduction par e-mail ?", + "card_abandonment_survey_question_6_label": "Oui, veuillez me contacter.", + "card_abandonment_survey_question_7_headline": "Veuillez partager votre adresse e-mail :", + "card_abandonment_survey_question_8_headline": "Des commentaires ou suggestions supplémentaires ?", + "career_development_survey_description": "Évaluer la satisfaction des employés concernant les opportunités de croissance et de développement de carrière.", + "career_development_survey_name": "Sondage sur le développement de carrière", + "career_development_survey_question_1_headline": "Je suis satisfait des opportunités de croissance personnelle et professionnelle chez $[projectName].", + "career_development_survey_question_1_lower_label": "Fortement en désaccord", + "career_development_survey_question_1_upper_label": "Tout à fait d'accord", + "career_development_survey_question_2_headline": "Je suis satisfait des opportunités d'avancement professionnel qui s'offrent à moi chez $[projectName].", + "career_development_survey_question_2_lower_label": "Fortement en désaccord", + "career_development_survey_question_2_upper_label": "Tout à fait d'accord", + "career_development_survey_question_3_headline": "Je suis satisfait de la formation liée au travail que mon organisation propose.", + "career_development_survey_question_3_lower_label": "Fortement en désaccord", + "career_development_survey_question_3_upper_label": "Tout à fait d'accord", + "career_development_survey_question_4_headline": "Je suis satisfait de l'investissement que mon organisation fait dans la formation et l'éducation.", + "career_development_survey_question_4_lower_label": "Fortement en désaccord", + "career_development_survey_question_4_upper_label": "Tout à fait d'accord", + "career_development_survey_question_5_choice_1": "Développement de produit", + "career_development_survey_question_5_choice_2": "Marketing", + "career_development_survey_question_5_choice_3": "Relations publiques", + "career_development_survey_question_5_choice_4": "Comptabilité", + "career_development_survey_question_5_choice_5": "Opérations", + "career_development_survey_question_5_choice_6": "Autre", + "career_development_survey_question_5_headline": "Dans quelle fonction travaillez-vous ?", + "career_development_survey_question_5_subheader": "Veuillez sélectionner l'une des options suivantes.", + "career_development_survey_question_6_choice_1": "Contributeur individuel", + "career_development_survey_question_6_choice_2": "Gestionnaire", + "career_development_survey_question_6_choice_3": "Directeur Senior", + "career_development_survey_question_6_choice_4": "Vice-président", + "career_development_survey_question_6_choice_5": "Exécutif", + "career_development_survey_question_6_choice_6": "Autre", + "career_development_survey_question_6_headline": "Lequel des éléments suivants décrit le mieux votre niveau de poste actuel ?", + "career_development_survey_question_6_subheader": "Veuillez sélectionner l'une des options suivantes.", + "cess_survey_name": "Sondage CES", + "cess_survey_question_1_headline": "$[projectName] facilite l'ajout de mes objectifs.", + "cess_survey_question_1_lower_label": "Pas du tout d'accord", + "cess_survey_question_1_upper_label": "Tout à fait d'accord", + "cess_survey_question_2_headline": "Merci ! Comment pourrions-nous vous faciliter la tâche pour [AJOUTER UN OBJECTIF] ?", + "cess_survey_question_2_placeholder": "Entrez votre réponse ici...", + "changing_subscription_experience_description": "Découvrez ce qui traverse l'esprit des gens lorsqu'ils changent d'abonnement.", + "changing_subscription_experience_name": "Changement de l'expérience d'abonnement", + "changing_subscription_experience_question_1_choice_1": "Extrêmement difficile", + "changing_subscription_experience_question_1_choice_2": "Ça a pris un certain temps, mais j'y suis arrivé.", + "changing_subscription_experience_question_1_choice_3": "C'était bien", + "changing_subscription_experience_question_1_choice_4": "Assez facile", + "changing_subscription_experience_question_1_choice_5": "Très facile, j'adore !", + "changing_subscription_experience_question_1_headline": "À quel point était-il facile de changer votre plan ?", + "changing_subscription_experience_question_2_choice_1": "Oui, très clair.", + "changing_subscription_experience_question_2_choice_2": "J'étais confus au début, mais j'ai trouvé ce dont j'avais besoin.", + "changing_subscription_experience_question_2_choice_3": "Assez compliqué.", + "changing_subscription_experience_question_2_headline": "Les informations tarifaires sont-elles faciles à comprendre ?", + "churn_survey": "Enquête de désabonnement", + "churn_survey_description": "Découvrez pourquoi les gens annulent leurs abonnements. Ces informations sont de l'or en barre !", + "churn_survey_question_1_choice_1": "Difficile à utiliser", + "churn_survey_question_1_choice_2": "C'est trop cher", + "churn_survey_question_1_choice_3": "Il me manque des fonctionnalités", + "churn_survey_question_1_choice_4": "Mauvais service client", + "churn_survey_question_1_choice_5": "Je n'en avais tout simplement plus besoin.", + "churn_survey_question_1_headline": "Pourquoi avez-vous annulé votre abonnement ?", + "churn_survey_question_1_subheader": "Nous sommes désolés de vous voir partir. Aidez-nous à nous améliorer :", + "churn_survey_question_2_button_label": "Envoyer", + "churn_survey_question_2_headline": "Qu'est-ce qui aurait rendu $[projectName] plus facile à utiliser ?", + "churn_survey_question_3_button_label": "Obtenez 30 % de réduction", + "churn_survey_question_3_dismiss_button_label": "Sauter", + "churn_survey_question_3_headline": "Obtenez 30 % de réduction pour l'année prochaine !", + "churn_survey_question_3_html": "

Nous aimerions vous garder comme client. Nous sommes heureux de vous offrir une remise de 30 % pour l'année prochaine.

", + "churn_survey_question_4_headline": "Quelles fonctionnalités vous manquent ?", + "churn_survey_question_5_button_label": "Envoyer un e-mail au PDG", + "churn_survey_question_5_dismiss_button_label": "Sauter", + "churn_survey_question_5_headline": "Je suis désolé d'apprendre cela \uD83D\uDE14 Parlez directement à notre PDG !", + "churn_survey_question_5_html": "

Nous visons à fournir le meilleur service client possible. Veuillez envoyer un e-mail à notre PDG et elle s'occupera personnellement de votre problème.

", + "collect_feedback_description": "Rassemblez des retours d'expérience complets sur votre produit ou service.", + "collect_feedback_name": "Collecter des retours", + "collect_feedback_question_1_headline": "Comment évaluez-vous votre expérience globale ?", + "collect_feedback_question_1_lower_label": "Pas bon", + "collect_feedback_question_1_subheader": "Ne t'inquiète pas, sois honnête.", + "collect_feedback_question_1_upper_label": "Très bien", + "collect_feedback_question_2_headline": "Charmant ! Qu'est-ce que tu as aimé à ce sujet ?", + "collect_feedback_question_2_placeholder": "Entrez votre réponse ici...", + "collect_feedback_question_3_headline": "Merci de partager ! Qu'est-ce que tu n'as pas aimé ?", + "collect_feedback_question_3_placeholder": "Entrez votre réponse ici...", + "collect_feedback_question_4_headline": "Comment évaluez-vous notre communication ?", + "collect_feedback_question_4_lower_label": "Pas bon", + "collect_feedback_question_4_upper_label": "Très bien", + "collect_feedback_question_5_headline": "Y a-t-il autre chose que vous aimeriez partager avec notre équipe ?", + "collect_feedback_question_5_placeholder": "Entrez votre réponse ici...", + "collect_feedback_question_6_choice_1": "Google", + "collect_feedback_question_6_choice_2": "Médias sociaux", + "collect_feedback_question_6_choice_3": "Amis", + "collect_feedback_question_6_choice_4": "Podcast", + "collect_feedback_question_6_choice_5": "Autre", + "collect_feedback_question_6_headline": "Comment avez-vous entendu parler de nous ?", + "collect_feedback_question_7_headline": "Enfin, nous aimerions répondre à vos commentaires. Veuillez partager votre e-mail :", + "collect_feedback_question_7_placeholder": "exemple@email.com", + "consent": "Consentement", + "consent_description": "Demander d'accepter les termes, conditions ou l'utilisation des données", + "contact_info": "Informations de contact", + "contact_info_description": "Demandez le nom, le prénom, l'email, le numéro de téléphone et l'entreprise ensemble.", + "csat_description": "Mesurez le score de satisfaction client de votre produit ou service.", + "csat_name": "Score de Satisfaction Client (CSAT)", + "csat_question_10_headline": "Avez-vous d'autres commentaires, questions ou préoccupations ?", + "csat_question_10_placeholder": "Entrez votre réponse ici...", + "csat_question_1_headline": "Quelle est la probabilité que vous recommandiez ce $[projectName] à un ami ou un collègue ?", + "csat_question_1_lower_label": "Peu probable", + "csat_question_1_upper_label": "Très probable", + "csat_question_2_choice_1": "Un peu satisfait", + "csat_question_2_choice_2": "Très satisfait", + "csat_question_2_choice_3": "Ni satisfait ni insatisfait", + "csat_question_2_choice_4": "Un peu insatisfait", + "csat_question_2_choice_5": "Très insatisfait", + "csat_question_2_headline": "Dans l'ensemble, dans quelle mesure êtes-vous satisfait ou insatisfait de notre $[projectName] ?", + "csat_question_2_subheader": "Veuillez en sélectionner un :", + "csat_question_3_choice_1": "Inefficace", + "csat_question_3_choice_10": "Unique", + "csat_question_3_choice_2": "Utile", + "csat_question_3_choice_3": "Impraticable", + "csat_question_3_choice_4": "Trop cher", + "csat_question_3_choice_5": "Haute qualité", + "csat_question_3_choice_6": "Fiable", + "csat_question_3_choice_7": "Bon rapport qualité-prix", + "csat_question_3_choice_8": "Qualité médiocre", + "csat_question_3_choice_9": "Peu fiable", + "csat_question_3_headline": "Lequel des mots suivants utiliseriez-vous pour décrire notre $[projectName] ?", + "csat_question_3_subheader": "Sélectionnez tout ce qui s'applique :", + "csat_question_4_choice_1": "Extrêmement bien", + "csat_question_4_choice_2": "Très bien", + "csat_question_4_choice_3": "Plutôt bien", + "csat_question_4_choice_4": "Pas très bien", + "csat_question_4_choice_5": "Pas du tout bien", + "csat_question_4_headline": "Dans quelle mesure nos $[projectName] répondent-elles à vos besoins ?", + "csat_question_4_subheader": "Veuillez sélectionner une option :", + "csat_question_5_choice_1": "Très haute qualité", + "csat_question_5_choice_2": "Haute qualité", + "csat_question_5_choice_3": "De mauvaise qualité", + "csat_question_5_choice_4": "Qualité très faible", + "csat_question_5_choice_5": "Ni haut ni bas", + "csat_question_5_headline": "Comment évalueriez-vous la qualité de $[projectName] ?", + "csat_question_5_subheader": "Veuillez sélectionner une option :", + "csat_question_6_choice_1": "Excellent", + "csat_question_6_choice_2": "Au-dessus de la moyenne", + "csat_question_6_choice_3": "Moyenne", + "csat_question_6_choice_4": "En dessous de la moyenne", + "csat_question_6_choice_5": "Pauvre", + "csat_question_6_headline": "Comment évalueriez-vous le rapport qualité-prix de $[projectName] ?", + "csat_question_6_subheader": "Veuillez en sélectionner un :", + "csat_question_7_choice_1": "Extrêmement réactif", + "csat_question_7_choice_2": "Très réactif", + "csat_question_7_choice_3": "Quelque peu réactif", + "csat_question_7_choice_4": "Pas si réactif", + "csat_question_7_choice_5": "Pas du tout réactif", + "csat_question_7_choice_6": "Non applicable", + "csat_question_7_headline": "Dans quelle mesure avons-nous été réactifs à vos questions concernant nos services ?", + "csat_question_7_subheader": "Veuillez en sélectionner un :", + "csat_question_8_choice_1": "Ceci est mon premier achat", + "csat_question_8_choice_2": "Moins de six mois", + "csat_question_8_choice_3": "Six mois à un an", + "csat_question_8_choice_4": "1 - 2 ans", + "csat_question_8_choice_5": "3 ans ou plus", + "csat_question_8_choice_6": "Je n'ai pas encore effectué d'achat.", + "csat_question_8_headline": "Depuis combien de temps êtes-vous client de $[projectName] ?", + "csat_question_8_subheader": "Veuillez en sélectionner un :", + "csat_question_9_choice_1": "Extrêmement probable", + "csat_question_9_choice_2": "Très probable", + "csat_question_9_choice_3": "Un peu probable", + "csat_question_9_choice_4": "Pas si probable", + "csat_question_9_choice_5": "Pas du tout probable", + "csat_question_9_headline": "Quelle est la probabilité que vous achetiez à nouveau l'un de nos $[projectName] ?", + "csat_question_9_subheader": "Veuillez sélectionner une option :", + "csat_survey_name": "CSAT $[projectName]", + "csat_survey_question_1_headline": "À quel point êtes-vous satisfait de votre expérience avec $[projectName] ?", + "csat_survey_question_1_lower_label": "Extrêmement insatisfait", + "csat_survey_question_1_upper_label": "Extrêmement satisfait", + "csat_survey_question_2_headline": "Charmant ! Y a-t-il quelque chose que nous puissions faire pour améliorer votre expérience ?", + "csat_survey_question_2_placeholder": "Entrez votre réponse ici...", + "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_description": "Créer une enquête sans modèle.", + "custom_survey_name": "Commencer à zéro", + "custom_survey_question_1_headline": "Que voudriez-vous savoir ?", + "custom_survey_question_1_placeholder": "Entrez votre réponse ici...", + "customer_effort_score_description": "Déterminez à quel point il est facile d'utiliser une fonctionnalité.", + "customer_effort_score_name": "Score d'Effort Client (SEC)", + "customer_effort_score_question_1_headline": "$[projectName] me facilite l'ajout d'un objectif.", + "customer_effort_score_question_1_lower_label": "Pas du tout d'accord", + "customer_effort_score_question_1_upper_label": "Tout à fait d'accord", + "customer_effort_score_question_2_headline": "Merci ! Comment pourrions-nous vous faciliter la tâche pour [AJOUTER UN OBJECTIF] ?", + "customer_effort_score_question_2_placeholder": "Entrez votre réponse ici...", + "date": "Date", + "date_description": "Demander une sélection de date", + "default_ending_card_button_label": "Créez votre propre enquête", + "default_ending_card_headline": "Merci !", + "default_ending_card_subheader": "Nous apprécions vos retours.", + "default_welcome_card_button_label": "Suivant", + "default_welcome_card_headline": "Bienvenue !", + "default_welcome_card_html": "Merci pour vos retours - allons-y !", + "docs_feedback_description": "Mesurez la clarté de chaque page de votre documentation pour développeurs.", + "docs_feedback_name": "Commentaires sur les documents", + "docs_feedback_question_1_choice_1": "Oui \uD83D\uDC4D", + "docs_feedback_question_1_choice_2": "Non \uD83D\uDC4E", + "docs_feedback_question_1_headline": "Cette page vous a-t-elle été utile ?", + "docs_feedback_question_2_headline": "Veuillez élaborer :", + "docs_feedback_question_3_headline": "URL de la page", + "earned_advocacy_score_description": "L'EAS est une variation du NPS mais demande des comportements passés réels au lieu d'intentions élevées.", + "earned_advocacy_score_name": "Score de Plaidoyer Gagné (SPG)", + "earned_advocacy_score_question_1_choice_1": "Oui", + "earned_advocacy_score_question_1_choice_2": "Non", + "earned_advocacy_score_question_1_headline": "Avez-vous activement recommandé $[projectName] à d'autres ?", + "earned_advocacy_score_question_2_headline": "Pourquoi nous avez-vous recommandé ?", + "earned_advocacy_score_question_2_placeholder": "Entrez votre réponse ici...", + "earned_advocacy_score_question_3_headline": "Si triste. Pourquoi pas ?", + "earned_advocacy_score_question_3_placeholder": "Entrez votre réponse ici...", + "earned_advocacy_score_question_4_choice_1": "Oui", + "earned_advocacy_score_question_4_choice_2": "Non", + "earned_advocacy_score_question_4_headline": "Avez-vous activement découragé d'autres personnes de choisir $[projectName] ?", + "earned_advocacy_score_question_5_headline": "Qu'est-ce qui t'a poussé à les décourager ?", + "earned_advocacy_score_question_5_placeholder": "Entrez votre réponse ici...", + "employee_satisfaction_description": "Évaluer la satisfaction des employés et identifier les domaines à améliorer.", + "employee_satisfaction_name": "Satisfaction des employés", + "employee_satisfaction_question_1_headline": "À quel point êtes-vous satisfait de votre rôle actuel ?", + "employee_satisfaction_question_1_lower_label": "Pas satisfait", + "employee_satisfaction_question_1_upper_label": "Très satisfait", + "employee_satisfaction_question_2_choice_1": "Extrêmement significatif", + "employee_satisfaction_question_2_choice_2": "Très significatif", + "employee_satisfaction_question_2_choice_3": "Modérément significatif", + "employee_satisfaction_question_2_choice_4": "Légèrement significatif", + "employee_satisfaction_question_2_choice_5": "Pas du tout significatif", + "employee_satisfaction_question_2_headline": "À quel point trouvez-vous votre travail significatif ?", + "employee_satisfaction_question_3_headline": "Qu'est-ce que vous appréciez le plus dans votre travail ici ?", + "employee_satisfaction_question_3_placeholder": "Entrez votre réponse ici...", + "employee_satisfaction_question_5_headline": "Évaluez le soutien que vous recevez de votre manager.", + "employee_satisfaction_question_5_lower_label": "Pauvre", + "employee_satisfaction_question_5_upper_label": "Excellent", + "employee_satisfaction_question_6_headline": "Quelles améliorations suggéreriez-vous pour notre lieu de travail ?", + "employee_satisfaction_question_6_placeholder": "Entrez votre réponse ici...", + "employee_satisfaction_question_7_choice_1": "Extrêmement probable", + "employee_satisfaction_question_7_choice_2": "Très probable", + "employee_satisfaction_question_7_choice_3": "Modérément probable", + "employee_satisfaction_question_7_choice_4": "Légèrement probable", + "employee_satisfaction_question_7_choice_5": "Pas du tout probable", + "employee_satisfaction_question_7_headline": "Quelle est la probabilité que vous recommandiez notre entreprise à un ami ?", + "employee_well_being_description": "Évaluez le bien-être de vos employés à travers l'équilibre travail-vie personnelle, la charge de travail et l'environnement.", + "employee_well_being_name": "Bien-être des employés", + "employee_well_being_question_1_headline": "Je sens que j'ai un bon équilibre entre ma vie professionnelle et ma vie personnelle.", + "employee_well_being_question_1_lower_label": "Équilibre très pauvre", + "employee_well_being_question_1_upper_label": "Équilibre excellent", + "employee_well_being_question_2_headline": "Ma charge de travail est gérable, ce qui me permet de rester productif sans me sentir submergé.", + "employee_well_being_question_2_lower_label": "Charge de travail écrasante", + "employee_well_being_question_2_upper_label": "Parfaitement gérable", + "employee_well_being_question_3_headline": "L'environnement de travail soutient mon bien-être physique et mental.", + "employee_well_being_question_3_lower_label": "Pas solidaire", + "employee_well_being_question_3_upper_label": "Très soutenant", + "employee_well_being_question_4_headline": "Quels changements, le cas échéant, amélioreraient votre bien-être général au travail ?", + "employee_well_being_question_4_placeholder": "Entrez votre réponse ici...", + "enps_survey_name": "Sondage eNPS", + "enps_survey_question_1_headline": "Quelle est la probabilité que vous recommandiez de travailler dans cette entreprise à un ami ou un collègue ?", + "enps_survey_question_1_lower_label": "Pas du tout probable", + "enps_survey_question_1_upper_label": "Extrêmement probable", + "enps_survey_question_2_headline": "Pour nous aider à nous améliorer, pouvez-vous décrire la ou les raisons de votre évaluation ?", + "enps_survey_question_3_headline": "D'autres commentaires, retours ou préoccupations ?", + "evaluate_a_product_idea_description": "Interrogez les utilisateurs sur des idées de produits ou de fonctionnalités. Obtenez des retours rapidement.", + "evaluate_a_product_idea_name": "Évaluer une idée de produit", + "evaluate_a_product_idea_question_1_button_label": "Faisons-le !", + "evaluate_a_product_idea_question_1_dismiss_button_label": "Sauter", + "evaluate_a_product_idea_question_1_headline": "Nous adorons la façon dont vous utilisez $[projectName] ! Nous aimerions avoir votre avis sur une idée de fonctionnalité. Avez-vous une minute ?", + "evaluate_a_product_idea_question_1_html": "

Nous respectons votre temps et nous avons fait court \uD83E\uDD38

", + "evaluate_a_product_idea_question_2_headline": "Merci ! À quel point est-il difficile ou facile pour vous de [ZONE DE PROBLÈME] aujourd'hui ?", + "evaluate_a_product_idea_question_2_lower_label": "Très difficile", + "evaluate_a_product_idea_question_2_upper_label": "Très facile", + "evaluate_a_product_idea_question_3_headline": "Qu'est-ce qui est le plus difficile pour vous en ce qui concerne [DOMAIN DE PROBLÈME] ?", + "evaluate_a_product_idea_question_3_placeholder": "Entrez votre réponse ici...", + "evaluate_a_product_idea_question_4_button_label": "Suivant", + "evaluate_a_product_idea_question_4_dismiss_button_label": "Sauter", + "evaluate_a_product_idea_question_4_headline": "Nous travaillons sur une idée pour aider avec [DOMAINES DE PROBLÈME].", + "evaluate_a_product_idea_question_4_html": "

Insérez le résumé du concept ici. Ajoutez les détails nécessaires tout en restant concis et facile à comprendre.

", + "evaluate_a_product_idea_question_5_headline": "Quelle valeur cette fonctionnalité aurait-elle pour vous ?", + "evaluate_a_product_idea_question_5_lower_label": "Pas précieux", + "evaluate_a_product_idea_question_5_upper_label": "Très précieux", + "evaluate_a_product_idea_question_6_headline": "Compris. Pourquoi cette fonctionnalité ne serait-elle pas précieuse pour vous ?", + "evaluate_a_product_idea_question_6_placeholder": "Entrez votre réponse ici...", + "evaluate_a_product_idea_question_7_headline": "Qu'est-ce qui serait le plus précieux pour vous dans cette fonctionnalité ?", + "evaluate_a_product_idea_question_7_placeholder": "Entrez votre réponse ici...", + "evaluate_a_product_idea_question_8_headline": "Y a-t-il autre chose à garder à l'esprit ?", + "evaluate_a_product_idea_question_8_placeholder": "Entrez votre réponse ici...", + "evaluate_content_quality_description": "Mesurez si vos contenus marketing atteignent leur cible.", + "evaluate_content_quality_name": "Évaluer la qualité du contenu", + "evaluate_content_quality_question_1_headline": "Dans quelle mesure cet article a-t-il répondu à ce que vous espériez apprendre ?", + "evaluate_content_quality_question_1_lower_label": "Pas du tout bien", + "evaluate_content_quality_question_1_upper_label": "Extrêmement bien", + "evaluate_content_quality_question_2_headline": "Hmpft ! Qu'espériez-vous ?", + "evaluate_content_quality_question_2_placeholder": "Entrez votre réponse ici...", + "evaluate_content_quality_question_3_headline": "Superbe ! Y a-t-il autre chose que vous aimeriez que nous abordions ?", + "evaluate_content_quality_question_3_placeholder": "Sujets, tendances, tutoriels...", + "fake_door_follow_up_description": "Faites un suivi avec les utilisateurs qui ont rencontré l'un de vos expériences de Faux Portes.", + "fake_door_follow_up_name": "Suivi de la porte fictive", + "fake_door_follow_up_question_1_headline": "Quelle importance cette fonctionnalité a-t-elle pour vous ?", + "fake_door_follow_up_question_1_lower_label": "Pas important", + "fake_door_follow_up_question_1_upper_label": "Très important", + "fake_door_follow_up_question_2_choice_1": "Aspecte 1", + "fake_door_follow_up_question_2_choice_2": "Aspecte 2", + "fake_door_follow_up_question_2_choice_3": "Aspect 3", + "fake_door_follow_up_question_2_choice_4": "Aspecte 4", + "fake_door_follow_up_question_2_headline": "Qu'est-ce qui devrait absolument être inclus dans la construction de cela ?", + "feature_chaser_description": "Faire un suivi avec les utilisateurs qui viennent d'utiliser une fonctionnalité spécifique.", + "feature_chaser_name": "Chasseur de fonctionnalités", + "feature_chaser_question_1_headline": "Quelle est l'importance de [AJOUTER UNE FONCTION] pour vous ?", + "feature_chaser_question_1_lower_label": "Pas important", + "feature_chaser_question_1_upper_label": "Très important", + "feature_chaser_question_2_choice_1": "Aspecte 1", + "feature_chaser_question_2_choice_2": "Aspecte 2", + "feature_chaser_question_2_choice_3": "Aspect 3", + "feature_chaser_question_2_choice_4": "Aspecte 4", + "feature_chaser_question_2_headline": "Quel aspect est le plus important ?", + "feedback_box_description": "Offrez à vos utilisateurs la possibilité de partager sans effort ce qu'ils ont en tête.", + "feedback_box_name": "Boîte de retour d'information", + "feedback_box_question_1_choice_1": "Rapport de bogue \uD83D\uDC1E", + "feedback_box_question_1_choice_2": "Demande de fonctionnalité \uD83D\uDCA1", + "feedback_box_question_1_headline": "Qu'est-ce qui vous préoccupe, patron ?", + "feedback_box_question_1_subheader": "Merci de partager. Nous reviendrons vers vous dès que possible.", + "feedback_box_question_2_headline": "Qu'est-ce qui est cassé ?", + "feedback_box_question_2_subheader": "Plus il y a de détails, mieux c'est :)", + "feedback_box_question_3_button_label": "Oui, prévenez-moi", + "feedback_box_question_3_dismiss_button_label": "Non, merci", + "feedback_box_question_3_headline": "Vous voulez rester informé ?", + "feedback_box_question_3_html": "

Nous allons régler cela dès que possible. Voulez-vous être informé lorsque ce sera fait ?

", + "feedback_box_question_4_button_label": "Demander une fonctionnalité", + "feedback_box_question_4_headline": "Charmant, dis-nous en plus !", + "feedback_box_question_4_placeholder": "Entrez votre réponse ici...", + "feedback_box_question_4_subheader": "Quel problème souhaitez-vous que nous résolvions ?", + "file_upload": "Téléversement de fichier", + "file_upload_description": "Permettre aux répondants de télécharger des documents, des images ou d'autres fichiers", + "finish": "Terminer", + "follow_ups_modal_action_body": "

Salut \uD83D\uDC4B

Merci d'avoir pris le temps de répondre, nous vous contacterons sous peu.

Passez une excellente journée !

", + "free_text": "Texte libre", + "free_text_description": "Collecter des retours ouverts", + "free_text_placeholder": "Entrez votre réponse ici...", + "gauge_feature_satisfaction_description": "Évaluez la satisfaction des fonctionnalités spécifiques de votre produit.", + "gauge_feature_satisfaction_name": "Évaluer la satisfaction des fonctionnalités", + "gauge_feature_satisfaction_question_1_headline": "À quel point était-il facile d'atteindre ... ?", + "gauge_feature_satisfaction_question_1_lower_label": "Pas facile", + "gauge_feature_satisfaction_question_1_upper_label": "Très facile", + "gauge_feature_satisfaction_question_2_headline": "Quelle est une chose que nous pourrions améliorer ?", + "identify_customer_goals_description": "Mieux comprendre si votre message crée les bonnes attentes quant à la valeur que votre produit apporte.", + "identify_customer_goals_name": "Identifier les objectifs des clients", + "identify_sign_up_barriers_description": "Offrir une remise pour recueillir des informations sur les obstacles à l'inscription.", + "identify_sign_up_barriers_name": "Identifier les obstacles à l'inscription", + "identify_sign_up_barriers_question_1_button_label": "Obtenez 10 % de réduction", + "identify_sign_up_barriers_question_1_dismiss_button_label": "Non, merci", + "identify_sign_up_barriers_question_1_headline": "Répondez à ce court sondage, obtenez 10 % de réduction !", + "identify_sign_up_barriers_question_1_html": "Vous semblez envisager de vous inscrire. Répondez à quatre questions et obtenez 10 % sur n'importe quel plan.", + "identify_sign_up_barriers_question_2_headline": "Quelle est la probabilité que vous vous inscriviez à $[projectName] ?", + "identify_sign_up_barriers_question_2_lower_label": "Pas du tout probable", + "identify_sign_up_barriers_question_2_upper_label": "Très probable", + "identify_sign_up_barriers_question_3_choice_1_label": "Peut ne pas avoir ce que je cherche", + "identify_sign_up_barriers_question_3_choice_2_label": "Comparer encore les options", + "identify_sign_up_barriers_question_3_choice_3_label": "Ça semble compliqué", + "identify_sign_up_barriers_question_3_choice_4_label": "Le prix est une préoccupation", + "identify_sign_up_barriers_question_3_choice_5_label": "Quelque chose d'autre", + "identify_sign_up_barriers_question_3_headline": "Qu'est-ce qui vous empêche d'essayer $[projectName] ?", + "identify_sign_up_barriers_question_4_headline": "De quoi avez-vous besoin que $[projectName] n'offre pas ?", + "identify_sign_up_barriers_question_4_placeholder": "Entrez votre réponse ici...", + "identify_sign_up_barriers_question_5_headline": "Quelles options envisagez-vous ?", + "identify_sign_up_barriers_question_5_placeholder": "Entrez votre réponse ici...", + "identify_sign_up_barriers_question_6_headline": "Qu'est-ce qui vous semble compliqué ?", + "identify_sign_up_barriers_question_6_placeholder": "Entrez votre réponse ici...", + "identify_sign_up_barriers_question_7_headline": "Qu'est-ce qui vous préoccupe concernant les prix ?", + "identify_sign_up_barriers_question_7_placeholder": "Entrez votre réponse ici...", + "identify_sign_up_barriers_question_8_headline": "Veuillez expliquer :", + "identify_sign_up_barriers_question_8_placeholder": "Entrez votre réponse ici...", + "identify_sign_up_barriers_question_9_button_label": "S'inscrire", + "identify_sign_up_barriers_question_9_dismiss_button_label": "Passer pour l'instant", + "identify_sign_up_barriers_question_9_headline": "Merci ! Voici votre code : SIGNUPNOW10", + "identify_sign_up_barriers_question_9_html": "

Merci beaucoup d'avoir pris le temps de partager vos retours \uD83D\uDE4F

", + "identify_sign_up_barriers_with_project_name": "Barrières d'inscription $[projectName]", + "identify_upsell_opportunities_description": "Découvrez combien de temps votre produit fait gagner à vos utilisateurs. Utilisez-le pour vendre davantage.", + "identify_upsell_opportunities_name": "Identifier les opportunités de vente additionnelle", + "identify_upsell_opportunities_question_1_choice_1": "Moins d'une heure", + "identify_upsell_opportunities_question_1_choice_2": "1 à 2 heures", + "identify_upsell_opportunities_question_1_choice_3": "3 à 5 heures", + "identify_upsell_opportunities_question_1_choice_4": "5+ heures", + "identify_upsell_opportunities_question_1_headline": "Combien d'heures votre équipe économise-t-elle par semaine en utilisant $[projectName] ?", + "improve_activation_rate_description": "Identifiez les faiblesses de votre processus d'intégration pour augmenter l'activation des utilisateurs.", + "improve_activation_rate_name": "Améliorer le taux d'activation", + "improve_activation_rate_question_1_choice_1": "Ne me semblait pas utile.", + "improve_activation_rate_question_1_choice_2": "Difficile à configurer ou à utiliser", + "improve_activation_rate_question_1_choice_3": "Manque de fonctionnalités", + "improve_activation_rate_question_1_choice_4": "Je n'ai tout simplement pas eu le temps.", + "improve_activation_rate_question_1_choice_5": "Quelque chose d'autre", + "improve_activation_rate_question_1_headline": "Quelle est la principale raison pour laquelle vous n'avez pas terminé de configurer $[projectName] ?", + "improve_activation_rate_question_2_headline": "Qu'est-ce qui vous a fait penser que $[projectName] ne serait pas utile ?", + "improve_activation_rate_question_2_placeholder": "Entrez votre réponse ici...", + "improve_activation_rate_question_3_headline": "Qu'est-ce qui a été difficile lors de la configuration ou de l'utilisation de $[projectName] ?", + "improve_activation_rate_question_3_placeholder": "Entrez votre réponse ici...", + "improve_activation_rate_question_4_headline": "Quelles fonctionnalités ou caractéristiques manquaient ?", + "improve_activation_rate_question_4_placeholder": "Entrez votre réponse ici...", + "improve_activation_rate_question_5_headline": "Comment pourrions-nous vous faciliter la tâche pour commencer ?", + "improve_activation_rate_question_5_placeholder": "Entrez votre réponse ici...", + "improve_activation_rate_question_6_headline": "Qu'est-ce que c'était ? Veuillez expliquer :", + "improve_activation_rate_question_6_placeholder": "Entrez votre réponse ici...", + "improve_activation_rate_question_6_subheader": "Nous sommes impatients de le réparer dès que possible.", + "improve_newsletter_content_description": "Découvrez comment vos abonnés apprécient le contenu de votre newsletter.", + "improve_newsletter_content_name": "Améliorer le contenu de la newsletter", + "improve_newsletter_content_question_1_headline": "Comment évalueriez-vous la newsletter de cette semaine ?", + "improve_newsletter_content_question_1_lower_label": "Meh", + "improve_newsletter_content_question_1_upper_label": "Génial", + "improve_newsletter_content_question_2_headline": "Qu'est-ce qui aurait rendu la newsletter de cette semaine plus utile ?", + "improve_newsletter_content_question_2_placeholder": "Entrez votre réponse ici...", + "improve_newsletter_content_question_3_button_label": "Ravi d'aider !", + "improve_newsletter_content_question_3_dismiss_button_label": "Trouve tes propres amis", + "improve_newsletter_content_question_3_headline": "Merci ! ❤️ Partage l'amour avec UN ami.", + "improve_newsletter_content_question_3_html": "

Qui pense comme vous ? Vous nous rendriez un grand service en partageant l'épisode de cette semaine avec votre ami cérébral !

", + "improve_trial_conversion_description": "Découvrez pourquoi les gens ont arrêté leur essai. Ces informations vous aident à améliorer votre entonnoir.", + "improve_trial_conversion_name": "Améliorer la conversion des essais", + "improve_trial_conversion_question_1_choice_1": "Je n'en ai pas tiré beaucoup de valeur.", + "improve_trial_conversion_question_1_choice_2": "Je m'attendais à autre chose", + "improve_trial_conversion_question_1_choice_3": "C'est trop cher pour ce qu'il fait.", + "improve_trial_conversion_question_1_choice_4": "Il me manque une fonctionnalité", + "improve_trial_conversion_question_1_choice_5": "Je regardais juste autour.", + "improve_trial_conversion_question_1_headline": "Pourquoi avez-vous arrêté votre essai ?", + "improve_trial_conversion_question_1_subheader": "Aidez-nous à mieux vous comprendre :", + "improve_trial_conversion_question_2_button_label": "Suivant", + "improve_trial_conversion_question_2_headline": "Désolé d'apprendre cela. Quel était le plus gros problème rencontré avec $[projectName] ?", + "improve_trial_conversion_question_4_button_label": "Obtenez 20 % de réduction", + "improve_trial_conversion_question_4_dismiss_button_label": "Sauter", + "improve_trial_conversion_question_4_headline": "Désolé d'apprendre cela ! Bénéficiez de 20 % de réduction sur la première année.", + "improve_trial_conversion_question_4_html": "

Nous sommes heureux de vous offrir une remise de 20 % sur un plan annuel.

", + "improve_trial_conversion_question_5_button_label": "Suivant", + "improve_trial_conversion_question_5_headline": "Que souhaitez-vous accomplir ?", + "improve_trial_conversion_question_5_subheader": "Veuillez sélectionner l'une des options suivantes :", + "improve_trial_conversion_question_6_headline": "Comment résolvez-vous votre problème maintenant ?", + "improve_trial_conversion_question_6_subheader": "Veuillez nommer des solutions alternatives :", + "integration_setup_survey_description": "Évaluez la facilité avec laquelle les utilisateurs peuvent ajouter des intégrations à votre produit. Identifiez les points aveugles.", + "integration_setup_survey_name": "Sondage sur l'utilisation de l'intégration", + "integration_setup_survey_question_1_headline": "À quel point était-il facile de configurer cette intégration ?", + "integration_setup_survey_question_1_lower_label": "Pas facile", + "integration_setup_survey_question_1_upper_label": "Très facile", + "integration_setup_survey_question_2_headline": "Pourquoi c'était difficile ?", + "integration_setup_survey_question_2_placeholder": "Entrez votre réponse ici...", + "integration_setup_survey_question_3_headline": "Quels autres outils aimeriez-vous utiliser avec $[projectName] ?", + "integration_setup_survey_question_3_subheader": "Nous continuons à développer des intégrations, la vôtre peut être la prochaine :", + "interview_prompt_description": "Invitez un sous-ensemble spécifique de vos utilisateurs à planifier un entretien avec votre équipe produit.", + "interview_prompt_name": "Invite à l'entretien", + "interview_prompt_question_1_button_label": "Réserver un créneau", + "interview_prompt_question_1_headline": "Avez-vous 15 minutes pour nous parler ? \uD83D\uDE4F", + "interview_prompt_question_1_html": "Vous êtes l'un de nos utilisateurs avancés. Nous aimerions vous interviewer brièvement !", + "long_term_retention_check_in_description": "Évaluer la satisfaction des utilisateurs à long terme, la fidélité et les domaines à améliorer pour conserver les utilisateurs fidèles.", + "long_term_retention_check_in_name": "Vérification de la rétention à long terme", + "long_term_retention_check_in_question_10_headline": "Avez-vous des commentaires ou des retours supplémentaires ?", + "long_term_retention_check_in_question_10_placeholder": "Partagez vos réflexions ou commentaires qui pourraient nous aider à nous améliorer...", + "long_term_retention_check_in_question_1_headline": "Quel est votre niveau de satisfaction global concernant $[projectName] ?", + "long_term_retention_check_in_question_1_lower_label": "Pas satisfait", + "long_term_retention_check_in_question_1_upper_label": "Très satisfait", + "long_term_retention_check_in_question_2_headline": "Qu'est-ce que vous trouvez le plus précieux dans $[projectName] ?", + "long_term_retention_check_in_question_2_placeholder": "Décrivez la fonctionnalité ou le bénéfice que vous appréciez le plus...", + "long_term_retention_check_in_question_3_choice_1": "Fonctionnalités", + "long_term_retention_check_in_question_3_choice_2": "Soutien client", + "long_term_retention_check_in_question_3_choice_3": "expérience utilisateur", + "long_term_retention_check_in_question_3_choice_4": "Tarification", + "long_term_retention_check_in_question_3_choice_5": "Fiabilité et disponibilité", + "long_term_retention_check_in_question_3_headline": "Quel aspect de $[projectName] trouvez-vous le plus essentiel à votre expérience ?", + "long_term_retention_check_in_question_4_headline": "Dans quelle mesure $[projectName] répond-il à vos attentes ?", + "long_term_retention_check_in_question_4_lower_label": "Ne répond pas", + "long_term_retention_check_in_question_4_upper_label": "Dépasse les attentes", + "long_term_retention_check_in_question_5_headline": "Quels défis ou frustrations avez-vous rencontrés en utilisant $[projectName] ?", + "long_term_retention_check_in_question_5_placeholder": "Décrivez les défis ou les améliorations que vous aimeriez voir...", + "long_term_retention_check_in_question_6_headline": "Quelle est la probabilité que vous recommandiez $[projectName] à un ami ou un collègue ?", + "long_term_retention_check_in_question_6_lower_label": "Peu probable", + "long_term_retention_check_in_question_6_upper_label": "Très probable", + "long_term_retention_check_in_question_7_choice_1": "Nouvelles fonctionnalités et améliorations", + "long_term_retention_check_in_question_7_choice_2": "Soutien client amélioré", + "long_term_retention_check_in_question_7_choice_3": "Meilleures options de tarification", + "long_term_retention_check_in_question_7_choice_4": "Plus d'intégrations", + "long_term_retention_check_in_question_7_choice_5": "Améliorations de l'expérience utilisateur", + "long_term_retention_check_in_question_7_headline": "Qu'est-ce qui vous inciterait à rester un utilisateur à long terme ?", + "long_term_retention_check_in_question_8_headline": "Si vous pouviez changer une chose à propos de $[projectName], qu'est-ce que ce serait ?", + "long_term_retention_check_in_question_8_placeholder": "Partagez les modifications ou les fonctionnalités que vous aimeriez que nous prenions en compte...", + "long_term_retention_check_in_question_9_headline": "À quel point êtes-vous satisfait de nos mises à jour de produits et de leur fréquence ?", + "long_term_retention_check_in_question_9_lower_label": "Pas content", + "long_term_retention_check_in_question_9_upper_label": "Très heureux", + "market_attribution_description": "Découvrez comment les utilisateurs ont d'abord entendu parler de votre produit.", + "market_attribution_name": "Attribution marketing", + "market_attribution_question_1_choice_1": "Recommandation", + "market_attribution_question_1_choice_2": "Médias sociaux", + "market_attribution_question_1_choice_3": "Annonces", + "market_attribution_question_1_choice_4": "Recherche Google", + "market_attribution_question_1_choice_5": "Dans un podcast", + "market_attribution_question_1_headline": "Comment avez-vous entendu parler de nous pour la première fois ?", + "market_attribution_question_1_subheader": "Veuillez sélectionner l'une des options suivantes :", + "market_site_clarity_description": "Identifiez les utilisateurs qui quittent votre site marketing. Améliorez votre message.", + "market_site_clarity_name": "Clarté du site marketing", + "market_site_clarity_question_1_choice_1": "Oui, totalement", + "market_site_clarity_question_1_choice_2": "Un peu...", + "market_site_clarity_question_1_choice_3": "Non, pas du tout", + "market_site_clarity_question_1_headline": "Avez-vous toutes les informations nécessaires pour essayer $[projectName] ?", + "market_site_clarity_question_2_headline": "Qu'est-ce qui vous manque ou n'est pas clair concernant $[projectName] ?", + "market_site_clarity_question_3_button_label": "Obtenir une réduction", + "market_site_clarity_question_3_headline": "Merci pour votre réponse ! Obtenez 25 % de réduction sur vos 6 premiers mois :", + "matrix": "Matrice", + "matrix_description": "Créer une grille pour évaluer plusieurs éléments selon le même ensemble de critères.", + "measure_search_experience_description": "Mesurez la pertinence de vos résultats de recherche.", + "measure_search_experience_name": "Mesurer l'expérience de recherche", + "measure_search_experience_question_1_headline": "Quelle est la pertinence de ces résultats de recherche ?", + "measure_search_experience_question_1_lower_label": "Pas du tout pertinent", + "measure_search_experience_question_1_upper_label": "Très pertinent", + "measure_search_experience_question_2_headline": "Beurk ! Qu'est-ce qui rend les résultats sans intérêt pour vous ?", + "measure_search_experience_question_2_placeholder": "Entrez votre réponse ici...", + "measure_search_experience_question_3_headline": "Superbe ! Y a-t-il quelque chose que nous puissions faire pour améliorer votre expérience ?", + "measure_search_experience_question_3_placeholder": "Entrez votre réponse ici...", + "measure_task_accomplishment_description": "Vérifiez si les gens accomplissent leur 'travail à réaliser'. Les personnes qui réussissent sont de meilleurs clients.", + "measure_task_accomplishment_name": "Mesurer l'accomplissement des tâches", + "measure_task_accomplishment_question_1_headline": "Avez-vous pu accomplir ce que vous êtes venu faire ici aujourd'hui ?", + "measure_task_accomplishment_question_1_option_1_label": "Oui", + "measure_task_accomplishment_question_1_option_2_label": "Je m'en occupe, patron.", + "measure_task_accomplishment_question_1_option_3_label": "Non", + "measure_task_accomplishment_question_2_headline": "À quel point était-il facile d'atteindre votre objectif ?", + "measure_task_accomplishment_question_2_lower_label": "Très difficile", + "measure_task_accomplishment_question_2_upper_label": "Très facile", + "measure_task_accomplishment_question_3_headline": "Qu'est-ce qui a rendu cela difficile ?", + "measure_task_accomplishment_question_3_placeholder": "Entrez votre réponse ici...", + "measure_task_accomplishment_question_4_button_label": "Envoyer", + "measure_task_accomplishment_question_4_headline": "Génial ! Qu'est-ce que tu es venu faire ici aujourd'hui ?", + "measure_task_accomplishment_question_5_button_label": "Envoyer", + "measure_task_accomplishment_question_5_headline": "Qu'est-ce qui t'a arrêté ?", + "measure_task_accomplishment_question_5_placeholder": "Entrez votre réponse ici...", + "multi_select": "Sélection multiple", + "multi_select_description": "Demandez aux répondants de choisir une ou plusieurs options", + "new_integration_survey_description": "Découvrez quelles intégrations vos utilisateurs aimeraient voir ensuite.", + "new_integration_survey_name": "Nouveau sondage d'intégration", + "new_integration_survey_question_1_choice_1": "PostHog", + "new_integration_survey_question_1_choice_2": "Segmenter", + "new_integration_survey_question_1_choice_3": "Hubspot", + "new_integration_survey_question_1_choice_4": "Twilio", + "new_integration_survey_question_1_choice_5": "Autre", + "new_integration_survey_question_1_headline": "Quels autres outils utilisez-vous ?", + "next": "Suivant", + "nps": "Score de Promoteur Net (NPS)", + "nps_description": "Mesurer le Net Promoter Score (0-10)", + "nps_lower_label": "Pas du tout probable", + "nps_name": "Score de Promoteur Net (NPS)", + "nps_question_1_headline": "Quelle est la probabilité que vous recommandiez $[projectName] à un ami ou un collègue ?", + "nps_question_1_lower_label": "Peu probable", + "nps_question_1_upper_label": "Très probable", + "nps_question_2_headline": "Qu'est-ce qui vous a poussé à donner cette note ?", + "nps_survey_name": "Sondage NPS", + "nps_survey_question_1_headline": "Quelle est la probabilité que vous recommandiez $[projectName] à un ami ou un collègue ?", + "nps_survey_question_1_lower_label": "Pas du tout probable", + "nps_survey_question_1_upper_label": "Extrêmement probable", + "nps_survey_question_2_headline": "Pour nous aider à nous améliorer, pouvez-vous décrire la ou les raisons de votre évaluation ?", + "nps_survey_question_3_headline": "D'autres commentaires, retours ou préoccupations ?", + "nps_upper_label": "Extrêmement probable", + "onboarding_segmentation": "Segmentation d'intégration", + "onboarding_segmentation_description": "Découvrez-en plus sur les personnes qui se sont inscrites à votre produit et pourquoi.", + "onboarding_segmentation_question_1_choice_1": "Fondateur", + "onboarding_segmentation_question_1_choice_2": "Exécutif", + "onboarding_segmentation_question_1_choice_3": "Chef de produit", + "onboarding_segmentation_question_1_choice_4": "Propriétaire de produit", + "onboarding_segmentation_question_1_choice_5": "Ingénieur logiciel", + "onboarding_segmentation_question_1_headline": "Quel est votre rôle ?", + "onboarding_segmentation_question_1_subheader": "Veuillez sélectionner l'une des options suivantes :", + "onboarding_segmentation_question_2_choice_1": "seulement moi", + "onboarding_segmentation_question_2_choice_2": "1-5 employés", + "onboarding_segmentation_question_2_choice_3": "6-10 employés", + "onboarding_segmentation_question_2_choice_4": "11-100 employés", + "onboarding_segmentation_question_2_choice_5": "plus de 100 employés", + "onboarding_segmentation_question_2_headline": "Quelle est la taille de votre entreprise ?", + "onboarding_segmentation_question_2_subheader": "Veuillez sélectionner l'une des options suivantes :", + "onboarding_segmentation_question_3_choice_1": "Recommandation", + "onboarding_segmentation_question_3_choice_2": "Médias sociaux", + "onboarding_segmentation_question_3_choice_3": "Annonces", + "onboarding_segmentation_question_3_choice_4": "Recherche Google", + "onboarding_segmentation_question_3_choice_5": "Dans un podcast", + "onboarding_segmentation_question_3_headline": "Comment avez-vous entendu parler de nous pour la première fois ?", + "onboarding_segmentation_question_3_subheader": "Veuillez sélectionner l'une des options suivantes :", + "picture_selection": "Sélection d'images", + "picture_selection_description": "Demandez aux répondants de choisir une ou plusieurs images", + "preview_survey_ending_card_description": "Continue ton onboarding, s'il te plaît.", + "preview_survey_ending_card_headline": "C'est fait !", + "preview_survey_name": "Nouveau Sondage", + "preview_survey_question_1_headline": "Comment évalueriez-vous {projectName} ?", + "preview_survey_question_1_lower_label": "Pas bon", + "preview_survey_question_1_subheader": "Ceci est un aperçu du sondage.", + "preview_survey_question_1_upper_label": "Très bien", + "preview_survey_question_2_back_button_label": "Retour", + "preview_survey_question_2_choice_1_label": "Oui, tiens-moi au courant.", + "preview_survey_question_2_choice_2_label": "Non, merci !", + "preview_survey_question_2_headline": "Tu veux rester dans la boucle ?", + "preview_survey_welcome_card_headline": "Bienvenue !", + "preview_survey_welcome_card_html": "Merci pour vos retours - allons-y !", + "prioritize_features_description": "Identifiez les fonctionnalités dont vos utilisateurs ont le plus et le moins besoin.", + "prioritize_features_name": "Prioriser les fonctionnalités", + "prioritize_features_question_1_choice_1": "Fonctionnalité 1", + "prioritize_features_question_1_choice_2": "Fonctionnalité 2", + "prioritize_features_question_1_choice_3": "Fonctionnalité 3", + "prioritize_features_question_1_choice_4": "Autre", + "prioritize_features_question_1_headline": "Lequel de ces fonctionnalités vous serait le plus précieux ?", + "prioritize_features_question_2_choice_1": "Fonctionnalité 1", + "prioritize_features_question_2_choice_2": "Fonctionnalité 2", + "prioritize_features_question_2_choice_3": "Fonctionnalité 3", + "prioritize_features_question_2_headline": "Lequelle de ces fonctionnalités serait la moins précieuse pour vous ?", + "prioritize_features_question_3_headline": "Comment pourrions-nous améliorer votre expérience avec $[projectName] ?", + "prioritize_features_question_3_placeholder": "Entrez votre réponse ici...", + "product_market_fit_short_description": "Mesurez le PMF en évaluant à quel point les utilisateurs seraient déçus si votre produit disparaissait.", + "product_market_fit_short_name": "Enquête sur l'adéquation produit-marché (courte)", + "product_market_fit_short_question_1_choice_1": "Pas du tout déçu", + "product_market_fit_short_question_1_choice_2": "Un peu déçu", + "product_market_fit_short_question_1_choice_3": "Très déçu", + "product_market_fit_short_question_1_headline": "À quel point seriez-vous déçu si vous ne pouviez plus utiliser $[projectName] ?", + "product_market_fit_short_question_1_subheader": "Veuillez sélectionner l'une des options suivantes :", + "product_market_fit_short_question_2_headline": "Comment pouvons-nous améliorer $[projectName] pour vous ?", + "product_market_fit_short_question_2_subheader": "Veuillez être aussi précis que possible.", + "product_market_fit_superhuman": "Adéquation produit-marché (Superhuman)", + "product_market_fit_superhuman_description": "Mesurez le PMF en évaluant à quel point les utilisateurs seraient déçus si votre produit disparaissait.", + "product_market_fit_superhuman_question_1_button_label": "Ravi d'aider !", + "product_market_fit_superhuman_question_1_dismiss_button_label": "Non, merci.", + "product_market_fit_superhuman_question_1_headline": "Vous êtes l'un de nos utilisateurs avancés ! Avez-vous 5 minutes ?", + "product_market_fit_superhuman_question_1_html": "

Nous aimerions mieux comprendre votre expérience utilisateur. Partager vos idées nous aide beaucoup.

", + "product_market_fit_superhuman_question_2_choice_1": "Pas du tout déçu", + "product_market_fit_superhuman_question_2_choice_2": "Un peu déçu", + "product_market_fit_superhuman_question_2_choice_3": "Très déçu", + "product_market_fit_superhuman_question_2_headline": "À quel point seriez-vous déçu si vous ne pouviez plus utiliser $[projectName] ?", + "product_market_fit_superhuman_question_2_subheader": "Veuillez sélectionner l'une des options suivantes :", + "product_market_fit_superhuman_question_3_choice_1": "Fondateur", + "product_market_fit_superhuman_question_3_choice_2": "Exécutif", + "product_market_fit_superhuman_question_3_choice_3": "Chef de produit", + "product_market_fit_superhuman_question_3_choice_4": "Propriétaire de produit", + "product_market_fit_superhuman_question_3_choice_5": "Ingénieur logiciel", + "product_market_fit_superhuman_question_3_headline": "Quel est votre rôle ?", + "product_market_fit_superhuman_question_3_subheader": "Veuillez sélectionner l'une des options suivantes :", + "product_market_fit_superhuman_question_4_headline": "Quel type de personnes pensez-vous bénéficierait le plus de $[projectName] ?", + "product_market_fit_superhuman_question_5_headline": "Quel est le principal avantage que vous tirez de $[projectName] ?", + "product_market_fit_superhuman_question_6_headline": "Comment pouvons-nous améliorer $[projectName] pour vous ?", + "product_market_fit_superhuman_question_6_subheader": "Veuillez être aussi précis que possible.", + "professional_development_growth_survey_description": "Évaluer la satisfaction des employés concernant les opportunités de croissance et de développement professionnel.", + "professional_development_growth_survey_name": "Sondage sur le développement professionnel", + "professional_development_growth_survey_question_1_headline": "Je sens que j'ai des opportunités de grandir et de développer mes compétences au travail.", + "professional_development_growth_survey_question_1_lower_label": "Aucune opportunité de croissance", + "professional_development_growth_survey_question_1_upper_label": "De nombreuses opportunités de croissance", + "professional_development_growth_survey_question_2_headline": "J'ai suffisamment d'autonomie pour prendre des décisions sur la façon dont je fais mon travail.", + "professional_development_growth_survey_question_2_lower_label": "Pas d'autonomie", + "professional_development_growth_survey_question_2_upper_label": "Autonomie complète", + "professional_development_growth_survey_question_3_headline": "Mes objectifs au travail sont clairs et alignés avec mon développement.", + "professional_development_growth_survey_question_3_lower_label": "Objectifs flous", + "professional_development_growth_survey_question_3_upper_label": "Objectifs clairs et alignés", + "professional_development_growth_survey_question_4_headline": "Qu'est-ce qui pourrait être amélioré pour soutenir votre développement professionnel ?", + "professional_development_growth_survey_question_4_placeholder": "Entrez votre réponse ici...", + "professional_development_survey_description": "Évaluer la satisfaction des employés concernant les opportunités de croissance et de développement professionnel.", + "professional_development_survey_name": "Sondage sur le développement professionnel", + "professional_development_survey_question_1_choice_1": "Oui", + "professional_development_survey_question_1_choice_2": "Non", + "professional_development_survey_question_1_headline": "Êtes-vous intéressé par des activités de développement professionnel ?", + "professional_development_survey_question_2_choice_1": "Événements de réseautage", + "professional_development_survey_question_2_choice_2": "Conférences ou séminaires", + "professional_development_survey_question_2_choice_3": "Cours ou ateliers", + "professional_development_survey_question_2_choice_4": "Mentorat", + "professional_development_survey_question_2_choice_5": "Recherche individuelle", + "professional_development_survey_question_2_choice_6": "Autre", + "professional_development_survey_question_2_headline": "Quels types d'activités de développement professionnel pensez-vous être les plus précieuses pour votre croissance ?", + "professional_development_survey_question_2_subheader": "Sélectionnez tout ce qui s'applique", + "professional_development_survey_question_3_choice_1": "Oui", + "professional_development_survey_question_3_choice_2": "Non", + "professional_development_survey_question_3_headline": "Avez-vous consacré du temps à votre développement professionnel dans le passé ?", + "professional_development_survey_question_4_headline": "Dans quelle mesure vous sentez-vous soutenu dans votre lieu de travail en ce qui concerne le développement professionnel ?", + "professional_development_survey_question_4_lower_label": "Pas du tout pris en charge", + "professional_development_survey_question_4_upper_label": "Extrêmement soutenu", + "professional_development_survey_question_5_choice_1": "Pour ma propre connaissance", + "professional_development_survey_question_5_choice_2": "Pour obtenir plus de responsabilités", + "professional_development_survey_question_5_choice_3": "Améliorer mes compétences", + "professional_development_survey_question_5_choice_4": "Avancer sur mon parcours professionnel actuel", + "professional_development_survey_question_5_choice_5": "À la recherche d'un nouvel emploi", + "professional_development_survey_question_5_choice_6": "Autre", + "professional_development_survey_question_5_headline": "Quelles sont vos principales raisons de vouloir consacrer du temps au développement professionnel ?", + "ranking": "Classement", + "ranking_description": "Demandez aux répondants de classer les éléments par préférence ou par importance.", + "rate_checkout_experience_description": "Permettez aux clients d'évaluer l'expérience de paiement pour ajuster la conversion.", + "rate_checkout_experience_name": "Évaluer l'expérience de paiement", + "rate_checkout_experience_question_1_headline": "À quel point était-il facile ou difficile de finaliser le paiement ?", + "rate_checkout_experience_question_1_lower_label": "Très difficile", + "rate_checkout_experience_question_1_upper_label": "Très facile", + "rate_checkout_experience_question_2_headline": "Désolé pour cela ! Qu'est-ce qui aurait pu vous faciliter la tâche ?", + "rate_checkout_experience_question_2_placeholder": "Entrez votre réponse ici...", + "rate_checkout_experience_question_3_headline": "Superbe ! Y a-t-il quelque chose que nous puissions faire pour améliorer votre expérience ?", + "rate_checkout_experience_question_3_placeholder": "Entrez votre réponse ici...", + "rating": "Évaluation", + "rating_description": "Demandez aux répondants de donner une note (étoiles, smileys, chiffres)", + "rating_lower_label": "Pas bon", + "rating_upper_label": "Très bien", + "recognition_and_reward_survey_description": "Évaluer la satisfaction des employés en ce qui concerne la reconnaissance, les récompenses, le soutien des dirigeants et la liberté d'expression.", + "recognition_and_reward_survey_name": "Reconnaissance et Récompense", + "recognition_and_reward_survey_question_1_headline": "Lorsque je performe bien, mes contributions sont reconnues par l'organisation.", + "recognition_and_reward_survey_question_1_lower_label": "Pas du tout reconnu", + "recognition_and_reward_survey_question_1_upper_label": "Fortement reconnu", + "recognition_and_reward_survey_question_2_headline": "Je me sens assez récompensé pour le travail que je fais.", + "recognition_and_reward_survey_question_2_lower_label": "Pas récompensé équitablement", + "recognition_and_reward_survey_question_2_upper_label": "Très justement récompensé", + "recognition_and_reward_survey_question_3_headline": "Je me sens à l'aise de partager mes opinions ouvertement au travail.", + "recognition_and_reward_survey_question_3_lower_label": "Pas à l'aise", + "recognition_and_reward_survey_question_3_upper_label": "Très confortable", + "recognition_and_reward_survey_question_4_headline": "Comment l'organisation pourrait-elle améliorer la reconnaissance et les récompenses ?", + "recognition_and_reward_survey_question_4_placeholder": "Entrez votre réponse ici...", + "review_prompt_description": "Invitez les utilisateurs qui aiment votre produit à le commenter publiquement.", + "review_prompt_name": "Demande d'évaluation", + "review_prompt_question_1_headline": "Que pensez-vous de $[projectName] ?", + "review_prompt_question_1_lower_label": "Pas bon", + "review_prompt_question_1_upper_label": "Très satisfait", + "review_prompt_question_2_button_label": "Écrire un avis", + "review_prompt_question_2_headline": "Heureux d'entendre cela \uD83D\uDE4F Veuillez écrire un avis pour nous !", + "review_prompt_question_2_html": "

Cela nous aide beaucoup.

", + "review_prompt_question_3_button_label": "Envoyer", + "review_prompt_question_3_headline": "Désolé d'apprendre cela ! Quelle est UNE chose que nous pouvons améliorer ?", + "review_prompt_question_3_placeholder": "Entrez votre réponse ici...", + "review_prompt_question_3_subheader": "Aidez-nous à améliorer votre expérience.", + "schedule_a_meeting": "Planifier une réunion", + "schedule_a_meeting_description": "Demandez aux répondants de réserver un créneau horaire pour des réunions ou des appels.", + "single_select": "Choix unique", + "single_select_description": "Propose une liste d'options (choisissez-en une)", + "site_abandonment_survey": "Enquête sur l'abandon de site", + "site_abandonment_survey_description": "Comprendre les raisons de l'abandon de site dans votre boutique en ligne.", + "site_abandonment_survey_question_1_html": "

Nous avons remarqué que vous quittez notre site sans effectuer d'achat. Nous aimerions comprendre pourquoi.

", + "site_abandonment_survey_question_2_button_label": "Bien sûr !", + "site_abandonment_survey_question_2_dismiss_button_label": "Non, merci.", + "site_abandonment_survey_question_2_headline": "Avez-vous une minute ?", + "site_abandonment_survey_question_3_choice_1": "Je ne trouve pas ce que je cherche", + "site_abandonment_survey_question_3_choice_2": "Trouvé un meilleur site", + "site_abandonment_survey_question_3_choice_3": "Le site est trop lent", + "site_abandonment_survey_question_3_choice_4": "Juste en train de naviguer", + "site_abandonment_survey_question_3_choice_5": "J'ai trouvé un meilleur prix ailleurs", + "site_abandonment_survey_question_3_choice_6": "Autre", + "site_abandonment_survey_question_3_headline": "Quelle est la principale raison pour laquelle vous quittez notre site ?", + "site_abandonment_survey_question_3_subheader": "Veuillez sélectionner l'une des options suivantes :", + "site_abandonment_survey_question_4_headline": "Veuillez expliquer votre raison de quitter le site :", + "site_abandonment_survey_question_5_headline": "Comment évalueriez-vous votre expérience globale sur notre site ?", + "site_abandonment_survey_question_5_lower_label": "Très insatisfait", + "site_abandonment_survey_question_5_upper_label": "Très satisfait", + "site_abandonment_survey_question_6_choice_1": "Temps de chargement plus rapides", + "site_abandonment_survey_question_6_choice_2": "Meilleure fonctionnalité de recherche de produits", + "site_abandonment_survey_question_6_choice_3": "Plus de variété de produits", + "site_abandonment_survey_question_6_choice_4": "Conception de site améliorée", + "site_abandonment_survey_question_6_choice_5": "Plus d'avis clients", + "site_abandonment_survey_question_6_choice_6": "Autre", + "site_abandonment_survey_question_6_headline": "Quelles améliorations vous inciteraient à rester plus longtemps sur notre site ?", + "site_abandonment_survey_question_6_subheader": "Veuillez sélectionner tout ce qui s'applique :", + "site_abandonment_survey_question_7_headline": "Souhaitez-vous recevoir des mises à jour sur les nouveaux produits et les promotions ?", + "site_abandonment_survey_question_7_label": "Oui, veuillez me contacter.", + "site_abandonment_survey_question_8_headline": "Veuillez partager votre adresse e-mail :", + "site_abandonment_survey_question_9_headline": "Avez-vous des commentaires ou des suggestions supplémentaires ?", + "skip": "Sauter", + "smileys_survey_name": "Sondage des Émoticônes", + "smileys_survey_question_1_headline": "Que pensez-vous de $[projectName] ?", + "smileys_survey_question_1_lower_label": "Pas bon", + "smileys_survey_question_1_upper_label": "Très satisfait", + "smileys_survey_question_2_button_label": "Écrire un avis", + "smileys_survey_question_2_headline": "Heureux d'entendre cela \uD83D\uDE4F Veuillez écrire un avis pour nous !", + "smileys_survey_question_2_html": "

Cela nous aide beaucoup.

", + "smileys_survey_question_3_button_label": "Envoyer", + "smileys_survey_question_3_headline": "Désolé d'apprendre cela ! Quelle est UNE chose que nous pouvons améliorer ?", + "smileys_survey_question_3_placeholder": "Entrez votre réponse ici...", + "smileys_survey_question_3_subheader": "Aidez-nous à améliorer votre expérience.", + "star_rating_survey_name": "Sondage de notation de $[projectName]", + "star_rating_survey_question_1_headline": "Que pensez-vous de $[projectName] ?", + "star_rating_survey_question_1_lower_label": "Extrêmement insatisfait", + "star_rating_survey_question_1_upper_label": "Extrêmement satisfait", + "star_rating_survey_question_2_button_label": "Écrire un avis", + "star_rating_survey_question_2_headline": "Heureux d'entendre cela \uD83D\uDE4F Veuillez écrire un avis pour nous !", + "star_rating_survey_question_2_html": "

Cela nous aide beaucoup.

", + "star_rating_survey_question_3_button_label": "Envoyer", + "star_rating_survey_question_3_headline": "Dommage! Que pouvons-nous améliorer?", + "star_rating_survey_question_3_placeholder": "Tapez votre réponse ici...", + "star_rating_survey_question_3_subheader": "Aidez-nous à améliorer votre expérience.", + "statement_call_to_action": "Déclaration (Appel à l'action)", + "supportive_work_culture_survey_description": "Évaluer les perceptions des employés concernant le soutien des dirigeants, la communication et l'environnement de travail global.", + "supportive_work_culture_survey_name": "Culture de travail bienveillante", + "supportive_work_culture_survey_question_1_headline": "Mon manager me fournit le soutien dont j'ai besoin pour accomplir mon travail.", + "supportive_work_culture_survey_question_1_lower_label": "Non pris en charge", + "supportive_work_culture_survey_question_1_upper_label": "Fortement soutenu", + "supportive_work_culture_survey_question_2_headline": "La communication au sein de l'organisation est ouverte et efficace.", + "supportive_work_culture_survey_question_2_lower_label": "Mauvaise communication", + "supportive_work_culture_survey_question_2_upper_label": "Excellente communication", + "supportive_work_culture_survey_question_3_headline": "L'environnement de travail est positif et soutient mon bien-être.", + "supportive_work_culture_survey_question_3_lower_label": "Pas solidaire", + "supportive_work_culture_survey_question_3_upper_label": "Très soutenant", + "supportive_work_culture_survey_question_4_headline": "Comment la culture de travail pourrait-elle être améliorée pour mieux vous soutenir ?", + "supportive_work_culture_survey_question_4_placeholder": "Entrez votre réponse ici...", + "uncover_strengths_and_weaknesses_description": "Découvrez ce que les utilisateurs aiment et n'aiment pas dans votre produit ou votre offre.", + "uncover_strengths_and_weaknesses_name": "Découvrir les forces et les faiblesses", + "uncover_strengths_and_weaknesses_question_1_choice_1": "Facilité d'utilisation", + "uncover_strengths_and_weaknesses_question_1_choice_2": "Bon rapport qualité-prix", + "uncover_strengths_and_weaknesses_question_1_choice_3": "C'est open-source", + "uncover_strengths_and_weaknesses_question_1_choice_4": "Les fondateurs sont mignons", + "uncover_strengths_and_weaknesses_question_1_choice_5": "Autre", + "uncover_strengths_and_weaknesses_question_1_headline": "Qu'est-ce que vous appréciez le plus dans $[projectName] ?", + "uncover_strengths_and_weaknesses_question_2_choice_1": "Documentation", + "uncover_strengths_and_weaknesses_question_2_choice_2": "Personnalisabilité", + "uncover_strengths_and_weaknesses_question_2_choice_3": "Tarification", + "uncover_strengths_and_weaknesses_question_2_choice_4": "Autre", + "uncover_strengths_and_weaknesses_question_2_headline": "Que devrions-nous améliorer ?", + "uncover_strengths_and_weaknesses_question_2_subheader": "Veuillez sélectionner l'une des options suivantes :", + "uncover_strengths_and_weaknesses_question_3_headline": "Souhaitez-vous ajouter quelque chose ?", + "uncover_strengths_and_weaknesses_question_3_subheader": "N'hésitez pas à exprimer vos pensées, nous le faisons aussi.", + "understand_low_engagement_description": "Identifier les raisons d'un faible engagement pour améliorer l'adoption des utilisateurs.", + "understand_low_engagement_name": "Comprendre le faible engagement", + "understand_low_engagement_question_1_choice_1": "Difficile à utiliser", + "understand_low_engagement_question_1_choice_2": "Trouvé une meilleure alternative", + "understand_low_engagement_question_1_choice_3": "Je n'ai tout simplement pas eu le temps.", + "understand_low_engagement_question_1_choice_4": "Manquait des fonctionnalités dont j'ai besoin", + "understand_low_engagement_question_1_choice_5": "Autre", + "understand_low_engagement_question_1_headline": "Quelle est la principale raison pour laquelle vous n'êtes pas revenu à $[projectName] récemment ?", + "understand_low_engagement_question_2_headline": "Qu'est-ce qui est difficile avec l'utilisation de $[projectName] ?", + "understand_low_engagement_question_2_placeholder": "Entrez votre réponse ici...", + "understand_low_engagement_question_3_headline": "D'accord. Quelle alternative utilisez-vous à la place ?", + "understand_low_engagement_question_3_placeholder": "Entrez votre réponse ici...", + "understand_low_engagement_question_4_headline": "Compris. Comment pourrions-nous faciliter votre démarrage ?", + "understand_low_engagement_question_4_placeholder": "Entrez votre réponse ici...", + "understand_low_engagement_question_5_headline": "Compris. Quelles fonctionnalités ou options manquaient ?", + "understand_low_engagement_question_5_placeholder": "Entrez votre réponse ici...", + "understand_low_engagement_question_6_headline": "Veuillez ajouter plus de détails :", + "understand_low_engagement_question_6_placeholder": "Entrez votre réponse ici...", + "understand_purchase_intention_description": "Découvrez à quel point vos visiteurs sont proches d'acheter ou de s'abonner.", + "understand_purchase_intention_name": "Comprendre l'intention d'achat", + "understand_purchase_intention_question_1_headline": "Quelle est la probabilité que vous fassiez vos achats chez nous aujourd'hui ?", + "understand_purchase_intention_question_1_lower_label": "Pas du tout probable", + "understand_purchase_intention_question_1_upper_label": "Extrêmement probable", + "understand_purchase_intention_question_2_headline": "Compris. Quelle est votre raison principale de visite aujourd'hui ?", + "understand_purchase_intention_question_2_placeholder": "Entrez votre réponse ici...", + "understand_purchase_intention_question_3_headline": "Qu'est-ce qui vous empêche de faire un achat aujourd'hui, s'il y a quelque chose ?", + "understand_purchase_intention_question_3_placeholder": "Entrez votre réponse ici..." + } +} diff --git a/apps/web/lib/messages/pt-BR.json b/apps/web/lib/messages/pt-BR.json new file mode 100644 index 0000000000..ecc6728bf5 --- /dev/null +++ b/apps/web/lib/messages/pt-BR.json @@ -0,0 +1,2845 @@ +{ + "auth": { + "continue_with_azure": "Continuar com Azure", + "continue_with_email": "Continuar com o Email", + "continue_with_github": "Continuar com o GitHub", + "continue_with_google": "Continuar com o Google", + "continue_with_oidc": "Continuar com {oidcDisplayName}", + "continue_with_openid": "Continuar com OpenID", + "continue_with_saml": "Continuar com SAML SSO", + "forgot-password": { + "back_to_login": "Voltar para o login", + "email-sent": { + "heading": "Pedido de redefinição de senha feito com sucesso", + "text": "Se existir uma conta com esse e-mail, você vai receber em breve as instruções pra redefinir sua senha." + }, + "reset": { + "confirm_password": "Confirmar senha", + "new_password": "Nova senha", + "no_token_provided": "Token não fornecido", + "passwords_do_not_match": "As senhas não coincidem", + "success": { + "heading": "Senha redefinida com sucesso", + "text": "Agora você pode fazer login com sua nova senha" + } + }, + "reset_password": "Redefinir senha" + }, + "invite": { + "create_account": "Cria uma conta", + "email_does_not_match": "Opa! Email errado \uD83E\uDD26", + "email_does_not_match_description": "O email no convite não bate com o seu.", + "go_to_app": "Ir para o app", + "happy_to_have_you": "Feliz em ter você aqui \uD83E\uDD17", + "happy_to_have_you_description": "Por favor, crie uma conta ou faça login.", + "invite_expired": "Convite expirado \uD83D\uDE25", + "invite_expired_description": "Convites são válidos por 7 dias. Por favor, peça um novo convite.", + "invite_not_found": "Convite não encontrado \uD83D\uDE25", + "invite_not_found_description": "O código de convite não pode ser encontrado ou já foi usado.", + "login": "Entrar", + "welcome_to_organization": "Você tá dentro \uD83C\uDF89", + "welcome_to_organization_description": "Bem-vindo à organização." + }, + "last_used": "Usado por último", + "login": { + "backup_code": "Código de backup", + "create_an_account": "Cria uma conta", + "enter_your_backup_code": "Digite seu código de backup", + "enter_your_two_factor_authentication_code": "Digite seu código de autenticação de dois fatores", + "forgot_your_password": "Esqueceu sua senha?", + "login_to_your_account": "Faça login na sua conta", + "login_with_email": "Entrar com Email", + "lost_access": "Perdeu o acesso?", + "new_to_formbricks": "Novo no Formbricks?", + "use_a_backup_code": "Usar um código de backup" + }, + "saml_connection_error": "Algo deu errado. Por favor, verifica o console do app para mais detalhes.", + "signup": { + "captcha_failed": "reCAPTCHA falhou", + "have_an_account": "Já tem uma conta?", + "log_in": "Fazer login", + "password_validation_contain_at_least_1_number": "Conter pelo menos 1 número", + "password_validation_minimum_8_and_maximum_128_characters": "Mínimo 8 e Máximo 128 caracteres", + "password_validation_uppercase_and_lowercase": "mistura de maiúsculas e minúsculas", + "please_verify_captcha": "Por favor, verifique o reCAPTCHA", + "privacy_policy": "Política de Privacidade", + "terms_of_service": "Termos de Serviço", + "title": "Crie sua conta no Formbricks" + }, + "signup_without_verification_success": { + "user_successfully_created": "Usuário criado com sucesso", + "user_successfully_created_description": "Seu novo usuário foi criado com sucesso. Por favor, clique no botão abaixo e faça login na sua conta." + }, + "testimonial_1": "Mediamos a clareza dos nossos documentos e aprendemos com a rotatividade tudo em uma única plataforma. Ótimo produto, equipe muito atenciosa!", + "testimonial_all_features_included": "Todas as funcionalidades incluídas", + "testimonial_free_and_open_source": "Grátis e de código aberto", + "testimonial_no_credit_card_required": "Sem necessidade de cartão de crédito", + "testimonial_title": "Transforme insights dos clientes em experiências irresistíveis.", + "verification-requested": { + "invalid_email_address": "Endereço de email inválido", + "invalid_token": "Token inválido ☹️", + "no_email_provided": "Nenhum e-mail fornecido", + "please_click_the_link_in_the_email_to_activate_your_account": "Por favor, clica no link do e-mail pra ativar sua conta.", + "please_confirm_your_email_address": "Por favor, confirme seu endereço de e-mail", + "resend_verification_email": "Reenviar e-mail de verificação", + "verification_email_successfully_sent": "Email de verificação enviado com sucesso. Por favor, verifique sua caixa de entrada.", + "we_sent_an_email_to": "Enviamos um email para {email}", + "you_didnt_receive_an_email_or_your_link_expired": "Você não recebeu um e-mail ou seu link expirou?" + }, + "verify": { + "no_token_provided": "Token não fornecido", + "verifying": "Verificando..." + } + }, + "billing_confirmation": { + "back_to_billing_overview": "Voltar para visão geral de cobrança", + "thanks_for_upgrading": "Valeu demais por atualizar sua assinatura do Formbricks.", + "upgrade_successful": "Atualização bem-sucedida" + }, + "common": { + "accepted": "Aceito", + "account": "conta", + "account_settings": "Configurações da conta", + "action": "Ação", + "actions": "Ações", + "active_surveys": "Pesquisas ativas", + "activity": "Atividade", + "add": "Adicionar", + "add_action": "Adicionar ação", + "add_filter": "Adicionar filtro", + "add_logo": "Adicionar logo", + "add_project": "Adicionar projeto", + "add_to_team": "Adicionar à equipe", + "all": "Todos", + "all_questions": "Todas as perguntas", + "allow": "permitir", + "allow_users_to_exit_by_clicking_outside_the_survey": "Permitir que os usuários saiam clicando fora da pesquisa", + "an_unknown_error_occurred_while_deleting_table_items": "Ocorreu um erro desconhecido ao deletar {type}s", + "and": "E", + "and_response_limit_of": "e limite de resposta de", + "anonymous": "Anônimo", + "api_keys": "Chaves de API", + "app": "app", + "app_survey": "Pesquisa de App", + "apply_filters": "Aplicar filtros", + "are_you_sure": "Certeza?", + "are_you_sure_this_action_cannot_be_undone": "Tem certeza? Essa ação não pode ser desfeita.", + "attributes": "atributos", + "avatar": "Avatar", + "back": "Voltar", + "billing": "Faturamento", + "booked": "Reservado", + "bottom_left": "canto inferior esquerdo", + "bottom_right": "Canto Inferior Direito", + "cancel": "Cancelar", + "centered_modal": "Modal Centralizado", + "choices": "Escolhas", + "clear_all": "Limpar tudo", + "clear_filters": "Limpar filtros", + "clear_selection": "Limpar seleção", + "click": "Clica", + "clicks": "cliques", + "close": "Fechar", + "code": "Código", + "collapse_rows": "Recolher linhas", + "completed": "Concluído", + "configuration": "Configuração", + "confirm": "Confirmar", + "connect": "Conectar", + "connect_formbricks": "Conectar Formbricks", + "connected": "conectado", + "contacts": "Contatos", + "copied_to_clipboard": "Copiado para a área de transferência", + "copy": "Copiar", + "copy_code": "Copiar código", + "copy_link": "Copiar Link", + "create_new_organization": "Criar nova organização", + "create_segment": "Criar segmento", + "create_survey": "Criar pesquisa", + "created": "Criado", + "created_at": "Data de criação", + "created_by": "Criado por", + "customer_success": "Sucesso do Cliente", + "danger_zone": "Zona de Perigo", + "dark_overlay": "sobreposição escura", + "date": "Encontro", + "default": "Padrão", + "delete": "Apagar", + "description": "Descrição", + "dev_env": "Ambiente de Desenvolvimento", + "development_environment_banner": "Você está em um ambiente de desenvolvimento. Configure-o para testar pesquisas, ações e atributos.", + "disable": "desativar", + "disallow": "Não permita", + "discard": "Descartar", + "dismissed": "Dispensado", + "docs": "Documentação", + "documentation": "Documentação", + "download": "baixar", + "draft": "Rascunho", + "duplicate": "Duplicar", + "e_commerce": "comércio eletrônico", + "edit": "Editar", + "email": "Email", + "embed": "incorporar", + "enterprise_license": "Licença Empresarial", + "environment_not_found": "Ambiente não encontrado", + "environment_notice": "Você está atualmente no ambiente {environment}.", + "error": "Erro", + "error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.", + "error_component_title": "Erro ao carregar recursos", + "expand_rows": "Expandir linhas", + "finish": "Terminar", + "follow_these": "Siga esses", + "formbricks_version": "Versão do Formbricks", + "full_name": "Nome completo", + "gathering_responses": "Recolhendo respostas", + "general": "geral", + "go_back": "Voltar", + "go_to_dashboard": "Ir para o Painel", + "hidden": "Escondido", + "hidden_field": "Campo oculto", + "hidden_fields": "Campos ocultos", + "hide": "esconder", + "hide_column": "Ocultar coluna", + "image": "imagem", + "images": "Imagens", + "import": "importar", + "impressions": "Impressões", + "imprint": "impressão", + "in_progress": "Em andamento", + "inactive_surveys": "Pesquisas inativas", + "input_type": "Tipo de entrada", + "insights": "Percepções", + "integration": "integração", + "integrations": "Integrações", + "invalid_date": "Data inválida", + "invalid_file_type": "Tipo de arquivo inválido", + "invite": "convidar", + "invite_them": "Convida eles", + "key": "Chave", + "label": "Etiqueta", + "language": "Língua", + "learn_more": "Saiba mais", + "license": "Licença", + "light_overlay": "sobreposição leve", + "limits_reached": "Limites Atingidos", + "link": "link", + "link_and_email": "Link & E-mail", + "link_copied": "Link copiado para a área de transferência!", + "link_survey": "Pesquisa de Link", + "link_surveys": "Link de Pesquisas", + "load_more": "Carregar mais", + "loading": "Carregando", + "logo": "Em breve", + "logout": "Sair", + "look_and_feel": "Aparência e Experiência", + "manage": "gerenciar", + "marketing": "marketing", + "maximum": "Máximo", + "member": "Membros", + "members": "Membros", + "membership_not_found": "Assinatura não encontrada", + "metadata": "metadados", + "minimum": "Mínimo", + "mobile_overlay_text": "O Formbricks não está disponível para dispositivos com resoluções menores.", + "move_down": "Descer", + "move_up": "Subir", + "multiple_languages": "Vários idiomas", + "name": "Nome", + "negative": "Negativo", + "neutral": "Neutro", + "new": "Novo", + "new_survey": "Nova Pesquisa", + "new_version_available": "Formbricks {version} chegou. Atualize agora!", + "next": "Próximo", + "no_background_image_found": "Imagem de fundo não encontrada.", + "no_code": "Sem código", + "no_files_uploaded": "Nenhum arquivo foi enviado", + "no_result_found": "Nenhum resultado encontrado", + "no_results": "Nenhum resultado", + "no_surveys_found": "Não foram encontradas pesquisas.", + "not_authenticated": "Você não está autenticado para realizar essa ação.", + "not_authorized": "Não autorizado", + "not_connected": "Desconectado", + "note": "Nota", + "notes": "Anotações", + "notifications": "Notificações", + "number": "Número", + "off": "desligado", + "on": "ligado", + "only_one_file_allowed": "É permitido apenas um arquivo", + "only_owners_managers_and_manage_access_members_can_perform_this_action": "Apenas proprietários, gerentes e membros com acesso de gerenciamento podem realizar essa ação.", + "or": "ou", + "organization": "organização", + "organization_id": "ID da Organização", + "organization_not_found": "Organização não encontrada", + "organization_teams_not_found": "Equipes da organização não encontradas", + "other": "outro", + "others": "Outros", + "overview": "Visão Geral", + "password": "Senha", + "paused": "Pausado", + "pending_downgrade": "Rebaixamento Pendente", + "people_manager": "Gerente de Pessoas", + "person": "Pessoa", + "phone": "Celular", + "photo_by": "Foto por", + "pick_a_date": "Escolhe uma data", + "placeholder": "Espaço reservado", + "please_select_at_least_one_survey": "Por favor, selecione pelo menos uma pesquisa", + "please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho", + "please_upgrade_your_plan": "Por favor, atualize seu plano.", + "positive": "Positivo", + "preview": "Prévia", + "preview_survey": "Prévia da Pesquisa", + "privacy": "Política de Privacidade", + "privacy_policy": "Política de Privacidade", + "product_manager": "Gerente de Produto", + "profile": "Perfil", + "project": "Projeto", + "project_configuration": "Configuração do Projeto", + "project_id": "ID do Projeto", + "project_name": "Nome do Projeto", + "project_not_found": "Projeto não encontrado", + "project_permission_not_found": "Permissão do projeto não encontrada", + "projects": "Projetos", + "projects_limit_reached": "Limites de projetos atingidos", + "question": "Pergunta", + "question_id": "ID da Pergunta", + "questions": "Perguntas", + "read_docs": "Ler Documentos", + "remove": "remover", + "reorder_and_hide_columns": "Reordenar e ocultar colunas", + "report_survey": "Relatório de Pesquisa", + "request_trial_license": "Pedir licença de teste", + "reset_to_default": "Restaurar para o padrão", + "response": "Resposta", + "responses": "Respostas", + "restart": "Reiniciar", + "role": "Rolê", + "role_organization": "Função (Organização)", + "saas": "SaaS", + "sales": "vendas", + "save": "Salvar", + "save_changes": "Salvar alterações", + "scheduled": "agendado", + "search": "Buscar", + "security": "Segurança", + "segment": "segmento", + "segments": "Segmentos", + "select": "Selecionar", + "select_all": "Selecionar tudo", + "select_survey": "Selecionar Pesquisa", + "selected": "Selecionado", + "selected_questions": "Perguntas selecionadas", + "selection": "seleção", + "selections": "seleções", + "send": "Enviar", + "send_test_email": "Enviar e-mail de teste", + "session_not_found": "Sessão não encontrada", + "settings": "Configurações", + "share_feedback": "Compartilhar feedback", + "show": "Legal", + "show_response_count": "Mostrar contagem de respostas", + "shown": "mostrado", + "size": "Tamanho", + "skipped": "Pulou", + "skips": "Pula", + "some_files_failed_to_upload": "Alguns arquivos falharam ao enviar", + "something_went_wrong_please_try_again": "Algo deu errado. Tente novamente.", + "sort_by": "Ordenar por", + "start_free_trial": "Iniciar Teste Grátis", + "status": "status", + "step_by_step_manual": "Manual passo a passo", + "styling": "estilização", + "submit": "Enviar", + "summary": "Resumo", + "survey": "Pesquisa", + "survey_completed": "Pesquisa concluída.", + "survey_id": "ID da Pesquisa", + "survey_languages": "Idiomas da Pesquisa", + "survey_live": "Pesquisa ao vivo", + "survey_not_found": "Pesquisa não encontrada", + "survey_paused": "Pesquisa pausada.", + "survey_scheduled": "Pesquisa agendada.", + "survey_type": "Tipo de Pesquisa", + "surveys": "pesquisas", + "switch_organization": "Mudar organização", + "switch_to": "Mudar para {environment}", + "table_items_deleted_successfully": "{type}s deletados com sucesso", + "table_settings": "Arrumação da mesa", + "tags": "etiquetas", + "targeting": "mirando", + "team": "Time", + "team_access": "Acesso da equipe", + "team_name": "Nome da equipe", + "teams": "Controle de Acesso", + "teams_not_found": "Equipes não encontradas", + "text": "Texto", + "time": "tempo", + "time_to_finish": "Hora de terminar", + "title": "Título", + "top_left": "Canto superior esquerdo", + "top_right": "Canto Superior Direito", + "try_again": "Tenta de novo", + "type": "Tipo", + "unlock_more_projects_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.", + "update": "atualizar", + "updated": "atualizado", + "updated_at": "Atualizado em", + "upload": "Enviar", + "upload_input_description": "Clique ou arraste para fazer o upload de arquivos.", + "url": "URL", + "user": "Usuário", + "user_id": "ID do usuário", + "user_not_found": "Usuário não encontrado", + "variable": "variável", + "variables": "Variáveis", + "verified_email": "Email Verificado", + "video": "vídeo", + "warning": "Aviso", + "we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Não conseguimos verificar sua licença porque o servidor de licenças está inacessível.", + "webhook": "webhook", + "webhooks": "webhooks", + "website_and_app_connection": "Conexão de Site e App", + "website_app_survey": "Pesquisa de Site e App", + "website_survey": "Pesquisa de Site", + "weekly_summary": "Resumo semanal", + "welcome_card": "Cartão de boas-vindas", + "yes": "Sim", + "you": "Você", + "you_are_downgraded_to_the_community_edition": "Você foi rebaixado para a Edição Comunitária.", + "you_are_not_authorised_to_perform_this_action": "Você não tem autorização para fazer isso.", + "you_have_reached_your_limit_of_project_limit": "Você atingiu o seu limite de {projectLimit} projetos.", + "you_have_reached_your_monthly_miu_limit_of": "Você atingiu o seu limite mensal de MIU de", + "you_have_reached_your_monthly_response_limit_of": "Você atingiu o limite mensal de respostas de", + "you_will_be_downgraded_to_the_community_edition_on_date": "Você será rebaixado para a Edição Comunitária em {date}." + }, + "emails": { + "accept": "Aceitar", + "click_or_drag_to_upload_files": "Clique ou arraste para fazer o upload de arquivos.", + "email_customization_preview_email_heading": "Oi {userName}", + "email_customization_preview_email_subject": "Prévia da personalização de e-mails do Formbricks", + "email_customization_preview_email_text": "Esta é uma pré-visualização de e-mail para mostrar qual logo será renderizado nos e-mails.", + "email_footer_text_1": "Tenha um ótimo dia!", + "email_footer_text_2": "O time Formbricks", + "email_template_text_1": "Este e-mail foi enviado através do Formbricks.", + "embed_survey_preview_email_didnt_request": "Não pediu isso?", + "embed_survey_preview_email_environment_id": "ID do Ambiente", + "embed_survey_preview_email_fight_spam": "Ajude a gente a combater spam e encaminhe este e-mail para hola@formbricks.com", + "embed_survey_preview_email_heading": "Pré-visualizar Incorporação de Email", + "embed_survey_preview_email_subject": "Prévia da pesquisa por e-mail do Formbricks", + "embed_survey_preview_email_text": "É assim que o trecho de código fica embutido em um e-mail:", + "forgot_password_email_change_password": "Mudar senha", + "forgot_password_email_did_not_request": "Se você não solicitou isso, por favor ignore este e-mail.", + "forgot_password_email_heading": "Mudar senha", + "forgot_password_email_link_valid_for_24_hours": "O link é válido por 24 horas.", + "forgot_password_email_subject": "Redefinir sua senha Formbricks", + "forgot_password_email_text": "Você pediu um link pra trocar sua senha. Você pode fazer isso clicando no link abaixo:", + "imprint": "Impressum", + "invite_accepted_email_heading": "E aí", + "invite_accepted_email_subject": "Você tem um novo membro na sua organização!", + "invite_accepted_email_text_par1": "Só pra te avisar que", + "invite_accepted_email_text_par2": "aceitou seu convite. Divirta-se colaborando!", + "invite_email_button_label": "Entrar na organização", + "invite_email_heading": "E aí", + "invite_email_text_par1": "Seu colega", + "invite_email_text_par2": "te convidou para se juntar a eles na Formbricks. Para aceitar o convite, por favor clique no link abaixo:", + "invite_member_email_subject": "Você foi convidado a colaborar no Formbricks!", + "live_survey_notification_completed": "Concluído", + "live_survey_notification_draft": "Rascunho", + "live_survey_notification_in_progress": "Em andamento", + "live_survey_notification_no_new_response": "Nenhuma resposta nova recebida essa semana \uD83D\uDD75️", + "live_survey_notification_no_responses_yet": "Ainda sem respostas!", + "live_survey_notification_paused": "Pausado", + "live_survey_notification_scheduled": "agendado", + "live_survey_notification_view_more_responses": "Ver mais {responseCount} respostas", + "live_survey_notification_view_previous_responses": "Ver respostas anteriores", + "live_survey_notification_view_response": "Ver Resposta", + "notification_footer_all_the_best": "Tudo de bom,", + "notification_footer_in_your_settings": "nas suas configurações \uD83D\uDE4F", + "notification_footer_please_turn_them_off": "por favor, desliga eles", + "notification_footer_the_formbricks_team": "A Equipe Formbricks \uD83E\uDD0D", + "notification_footer_to_halt_weekly_updates": "Para parar as Atualizações Semanais,", + "notification_header_hey": "Oi \uD83D\uDC4B", + "notification_header_weekly_report_for": "Relatório Semanal de", + "notification_insight_completed": "Concluído", + "notification_insight_completion_rate": "Conclusão %", + "notification_insight_displays": "telas", + "notification_insight_responses": "Respostas", + "notification_insight_surveys": "pesquisas", + "onboarding_invite_email_button_label": "Entre na organização de {inviterName}", + "onboarding_invite_email_connect_formbricks": "Conecte o Formbricks ao seu app ou site via HTML Snippet ou NPM em apenas alguns minutos.", + "onboarding_invite_email_create_account": "Crie uma conta para entrar na organização de {inviterName}.", + "onboarding_invite_email_done": "Feito ✅", + "onboarding_invite_email_get_started_in_minutes": "Comece em Minutos", + "onboarding_invite_email_heading": "Oi ", + "onboarding_invite_email_subject": "{inviterName} precisa de ajuda para configurar o Formbricks. Você pode ajudar?", + "password_changed_email_heading": "Senha alterada", + "password_changed_email_text": "Sua senha foi alterada com sucesso.", + "password_reset_notify_email_subject": "Sua senha Formbricks foi alterada", + "privacy_policy": "Política de Privacidade", + "reject": "Rejeitar", + "render_email_response_value_file_upload_response_link_not_included": "O link para o arquivo enviado não está incluído por motivos de privacidade de dados", + "response_finished_email_subject": "Uma resposta para {surveyName} foi concluída ✅", + "response_finished_email_subject_with_email": "{personEmail} acabou de completar sua pesquisa {surveyName} ✅", + "schedule_your_meeting": "Agendar sua reunião", + "select_a_date": "Selecione uma data", + "survey_response_finished_email_congrats": "Parabéns, você recebeu uma nova resposta na sua pesquisa! Alguém acabou de completar sua pesquisa: {surveyName}", + "survey_response_finished_email_dont_want_notifications": "Não quer receber essas notificações?", + "survey_response_finished_email_hey": "E aí \uD83D\uDC4B", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Desativar notificações para todos os formulários recém-criados", + "survey_response_finished_email_turn_off_notifications_for_this_form": "Desativar notificações para este formulário", + "survey_response_finished_email_view_more_responses": "Ver mais {responseCount} respostas", + "survey_response_finished_email_view_survey_summary": "Ver resumo da pesquisa", + "verification_email_click_on_this_link": "Você também pode clicar neste link:", + "verification_email_heading": "Quase lá!", + "verification_email_hey": "Oi \uD83D\uDC4B", + "verification_email_if_expired_request_new_token": "Se expirou, por favor solicite um novo token aqui:", + "verification_email_link_valid_for_24_hours": "O link é válido por 24 horas.", + "verification_email_request_new_verification": "Pedir nova verificação", + "verification_email_subject": "Por favor, verifique seu email para usar o Formbricks", + "verification_email_survey_name": "Nome da pesquisa", + "verification_email_take_survey": "Responder pesquisa", + "verification_email_text": "Para começar a usar o Formbricks, por favor verifique seu e-mail abaixo:", + "verification_email_thanks": "Valeu por validar seu e-mail!", + "verification_email_to_fill_survey": "Para preencher a pesquisa, por favor clique no botão abaixo:", + "verification_email_verify_email": "Verificar e-mail", + "verified_link_survey_email_subject": "Sua pesquisa está pronta para ser preenchida.", + "weekly_summary_create_reminder_notification_body_cal_slot": "Escolha um horário de 15 minutos na agenda do nosso CEO", + "weekly_summary_create_reminder_notification_body_dont_let_a_week_pass": "Não deixe uma semana passar sem aprender sobre seus usuários:", + "weekly_summary_create_reminder_notification_body_need_help": "Precisa de ajuda pra encontrar a pesquisa certa pro seu produto?", + "weekly_summary_create_reminder_notification_body_reply_email": "ou responde a esse e-mail :)", + "weekly_summary_create_reminder_notification_body_setup_a_new_survey": "Configurar uma nova pesquisa", + "weekly_summary_create_reminder_notification_body_text": "Adoraríamos te enviar um Resumo Semanal, mas no momento não há pesquisas em andamento para {projectName}.", + "weekly_summary_email_subject": "Insights de usuários do {projectName} – Semana passada por Formbricks" + }, + "environments": { + "actions": { + "action_copied_successfully": "Ação copiada com sucesso", + "action_copy_failed": "Falha ao copiar a ação", + "action_created_successfully": "Ação criada com sucesso", + "action_deleted_successfully": "Ação deletada com sucesso", + "action_type": "Tipo de Ação", + "action_updated_successfully": "Ação atualizada com sucesso", + "action_with_key_already_exists": "Ação com a chave {key} já existe", + "action_with_name_already_exists": "Ação com o nome {name} já existe", + "add_css_class_or_id": "Adicionar classe ou id CSS", + "add_url": "Adicionar URL", + "click": "Clica", + "contains": "contém", + "create_action": "criar ação", + "css_selector": "Seletor CSS", + "delete_action_text": "Tem certeza de que quer deletar essa ação? Isso também vai remover essa ação como gatilho de todas as suas pesquisas.", + "display_name": "Nome de exibição", + "does_not_contain": "não contém", + "does_not_exactly_match": "Não bate exatamente", + "eg_clicked_download": "Por exemplo, clicou em baixar", + "eg_download_cta_click_on_home": "e.g. download_cta_click_on_home", + "eg_install_app": "Ex: Instalar App", + "eg_user_clicked_download_button": "Por exemplo, usuário clicou no botão de download", + "ends_with": "Termina com", + "enter_a_url_to_see_if_a_user_visiting_it_would_be_tracked": "Digite uma URL para ver se um usuário que a visita seria rastreado.", + "exactly_matches": "Combina exatamente", + "exit_intent": "Intenção de Saída", + "fifty_percent_scroll": "Rolar 50%", + "how_do_code_actions_work": "Como as Ações de Código funcionam?", + "if_a_user_clicks_a_button_with_a_specific_css_class_or_id": "Se um usuário clicar em um botão com uma classe CSS ou id específico", + "if_a_user_clicks_a_button_with_a_specific_text": "Se um usuário clicar em um botão com um texto específico", + "in_your_code_read_more_in_our": "no seu código. Leia mais em nosso", + "inner_text": "Texto Interno", + "invalid_css_selector": "Seletor CSS Inválido", + "limit_the_pages_on_which_this_action_gets_captured": "Limite as páginas nas quais essa ação é capturada", + "limit_to_specific_pages": "Limitar a páginas específicas", + "on_all_pages": "Em todas as páginas", + "page_filter": "filtro de página", + "page_view": "Visualização de Página", + "select_match_type": "Selecionar tipo de partida", + "starts_with": "Começa com", + "test_match": "Partida de Teste", + "test_your_url": "Teste sua URL", + "this_action_was_created_automatically_you_cannot_make_changes_to_it": "Essa ação foi criada automaticamente. Você não pode fazer alterações nela.", + "this_action_will_be_triggered_when_the_page_is_loaded": "Essa ação vai ser disparada quando a página carregar.", + "this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Essa ação vai ser acionada quando o usuário rolar 50% da página.", + "this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Essa ação será acionada quando o usuário tentar sair da página.", + "this_is_a_code_action_please_make_changes_in_your_code_base": "Esta é uma ação de código. Por favor, faça alterações na sua base de código.", + "track_new_user_action": "Rastrear Ação de Novo Usuário", + "track_user_action_to_display_surveys_or_create_user_segment": "Rastrear ações do usuário para exibir pesquisas ou criar segmento de usuários.", + "url": "URL", + "user_actions": "Ações do Usuário", + "user_clicked_download_button": "Usuário clicou no botão de download", + "what_did_your_user_do": "O que seu usuário fez?", + "what_is_the_user_doing": "O que o usuário tá fazendo?", + "you_can_track_code_action_anywhere_in_your_app_using": "Você pode rastrear ações de código em qualquer lugar do seu app usando" + }, + "connect": { + "congrats": "Parabéns!", + "connection_successful_message": "Mandou bem! Estamos conectados.", + "do_it_later": "Vou fazer isso mais tarde", + "finish_onboarding": "Concluir Integração", + "headline": "Conecte seu app ou site", + "import_formbricks_and_initialize_the_widget_in_your_component": "Importe o Formbricks e inicialize o widget no seu Componente (por exemplo, App.tsx):", + "insert_this_code_into_the_head_tag_of_your_website": "Insira esse código na tag head do seu site:", + "subtitle": "Leva menos de 4 minutos.", + "waiting_for_your_signal": "Esperando seu sinal..." + }, + "contacts": { + "contact_deleted_successfully": "Contato excluído com sucesso", + "contact_not_found": "Nenhum contato encontrado", + "contacts_table_refresh": "Atualizar contatos", + "contacts_table_refresh_error": "Ocorreu um erro ao atualizar os contatos. Por favor, tente novamente.", + "contacts_table_refresh_success": "Contatos atualizados com sucesso", + "first_name": "Primeiro Nome", + "last_name": "Sobrenome", + "no_responses_found": "Nenhuma resposta encontrada", + "not_provided": "Não fornecido", + "search_contact": "Buscar contato", + "select_attribute": "Selecionar Atributo", + "unlock_contacts_description": "Gerencie contatos e envie pesquisas direcionadas", + "unlock_contacts_title": "Desbloqueie contatos com um plano superior", + "upload_contacts_modal_attributes_description": "Mapeie as colunas do seu CSV para os atributos no Formbricks.", + "upload_contacts_modal_attributes_new": "Novo atributo", + "upload_contacts_modal_attributes_search_or_add": "Buscar ou adicionar atributo", + "upload_contacts_modal_attributes_should_be_mapped_to": "deve ser mapeado para", + "upload_contacts_modal_attributes_title": "Atributos", + "upload_contacts_modal_description": "Faça upload de um CSV para importar contatos com atributos rapidamente", + "upload_contacts_modal_download_example_csv": "Baixar exemplo de CSV", + "upload_contacts_modal_duplicates_description": "O que devemos fazer se um contato já existir nos seus contatos?", + "upload_contacts_modal_duplicates_overwrite_description": "Sobrescreve os contatos existentes", + "upload_contacts_modal_duplicates_overwrite_title": "Sobrescrever", + "upload_contacts_modal_duplicates_skip_description": "Ignora os contatos duplicados", + "upload_contacts_modal_duplicates_skip_title": "Ignorar", + "upload_contacts_modal_duplicates_title": "Duplicados", + "upload_contacts_modal_duplicates_update_description": "Atualiza os contatos existentes", + "upload_contacts_modal_duplicates_update_title": "Atualizar", + "upload_contacts_modal_pick_different_file": "Escolha um arquivo diferente", + "upload_contacts_modal_preview": "Aqui está uma prévia dos seus dados.", + "upload_contacts_modal_upload_btn": "Fazer upload de contatos" + }, + "experience": { + "all": "tudo", + "all_time": "Todo o tempo", + "analysed_feedbacks": "Feedbacks Analisados", + "category": "Categoria", + "category_updated_successfully": "Categoria atualizada com sucesso!", + "complaint": "Reclamação", + "did_you_find_this_insight_helpful": "Você achou essa dica útil?", + "failed_to_update_category": "Falha ao atualizar categoria", + "feature_request": "Pedido de Recurso", + "good_afternoon": "\uD83C\uDF24️ Boa tarde", + "good_evening": "\uD83C\uDF19 Boa noite", + "good_morning": "☀️ Bom dia", + "insights_description": "Todos os insights gerados a partir das respostas de todas as suas pesquisas", + "insights_for_project": "Insights para {projectName}", + "new_responses": "Novas Respostas", + "no_insights_for_this_filter": "Sem insights para este filtro", + "no_insights_found": "Não foram encontrados insights. Colete mais respostas de pesquisa ou ative insights para suas pesquisas existentes para começar.", + "praise": "elogio", + "sentiment_score": "Pontuação de Sentimento", + "templates_card_description": "Escolha um template ou comece do zero", + "templates_card_title": "Meça a experiência do seu cliente", + "this_month": "Este mês", + "this_quarter": "Esse trimestre", + "this_week": "Essa semana", + "today": "Hoje" + }, + "formbricks_logo": "Logo da Formbricks", + "integrations": { + "activepieces_integration_description": "Conecte o Formbricks instantaneamente com aplicativos populares para automatizar tarefas sem codificação.", + "additional_settings": "Configurações Adicionais", + "airtable": { + "airtable_base": "base do Airtable", + "airtable_integration": "Integração com Airtable", + "airtable_integration_description": "Sincronize respostas diretamente com o Airtable.", + "airtable_integration_is_not_configured": "A integração com o Airtable não está configurada", + "connect_with_airtable": "Conectar com o Airtable", + "link_airtable_table": "Vincular Tabela do Airtable", + "link_new_table": "Vincular nova tabela", + "no_bases_found": "Não foram encontradas bases no Airtable", + "no_integrations_yet": "Suas integrações do Airtable vão aparecer aqui assim que você adicioná-las. ⏲️", + "please_create_a_base": "Por favor, crie uma base no Airtable", + "please_select_a_base": "Por favor, escolha uma base", + "please_select_a_table": "Por favor, escolha uma mesa", + "sync_responses_with_airtable": "Sincronizar respostas com um Airtable", + "table_name": "Nome da Tabela" + }, + "airtable_integration_description": "Preencha sua tabela do Airtable com dados de pesquisa instantaneamente", + "connected_with_email": "Conectado com {email}", + "connecting_integration_failed_please_try_again": "Falha na conexão da integração. Tente novamente!", + "create_survey_warning": "Você tem que criar uma pesquisa para poder configurar essa integração", + "delete_integration": "Excluir Integração", + "delete_integration_confirmation": "Tem certeza de que quer deletar essa integração?", + "google_sheet_integration_description": "Preencha suas planilhas com dados de pesquisa instantaneamente", + "google_sheets": { + "connect_with_google_sheets": "Conectar com o Google Sheets", + "enter_a_valid_spreadsheet_url_error": "Por favor, insira uma URL de planilha válida", + "google_connection": "Conexão Google", + "google_connection_deletion_description": "Sincronize respostas diretamente com o Google Sheets.", + "google_sheet_integration_is_not_configured": "A integração do Google Sheets não está configurada na sua instância do Formbricks.", + "google_sheet_logo": "logo do Google Planilhas", + "google_sheet_name": "Nome da Planilha do Google", + "google_sheets_integration": "Integração com Google Sheets", + "google_sheets_integration_description": "Sincronize respostas diretamente com o Google Sheets.", + "link_google_sheet": "Link da Planilha do Google", + "link_new_sheet": "Vincular nova planilha", + "no_integrations_yet": "Suas integrações do Google Sheets vão aparecer aqui assim que você adicioná-las. ⏲️", + "spreadsheet_url": "URL da planilha" + }, + "include_created_at": "Incluir Data de Criação", + "include_hidden_fields": "Incluir Campos Ocultos", + "include_metadata": "Incluir Metadados (Navegador, País, etc.)", + "include_variables": "Incluir Variáveis", + "integration_added_successfully": "Integração adicionada com sucesso", + "integration_removed_successfully": "Integração removida com sucesso", + "integration_updated_successfully": "Integração atualizada com sucesso", + "make_integration_description": "Integrar Formbricks com mais de 1000 apps via Make", + "manage_webhooks": "Gerenciar Webhooks", + "n8n_integration_description": "Integrar Formbricks com mais de 350 apps via n8n", + "notion": { + "col_name_of_type_is_not_supported": "{col_name} do tipo {type} não é suportado pela API do Notion. Os dados não serão refletidos no seu banco de dados do Notion.", + "connect_with_notion": "Conectar com o Notion", + "connected_with_workspace": "Conectado com o espaço de trabalho {workspace}", + "create_at_least_one_database_to_setup_this_integration": "Você tem que criar pelo menos um banco de dados para poder configurar essa integração", + "database_name": "Nome do Banco de Dados", + "duplicate_connection_warning": "A conexão com esse banco de dados está ativa. Faça alterações com cuidado.", + "link_database": "Banco de Dados de Links", + "link_new_database": "Vincular novo banco de dados", + "link_notion_database": "Vincular Banco de Dados do Notion", + "map_formbricks_fields_to_notion_property": "Mapear campos do Formbricks para propriedade do Notion", + "no_databases_found": "Suas integrações do Notion vão aparecer aqui assim que você adicioná-las. ⏲️", + "notion_integration": "Integração com o Notion", + "notion_integration_description": "Envie respostas diretamente para o Notion.", + "notion_integration_is_not_configured": "A integração do Notion não está configurada na sua instância do Formbricks.", + "notion_logo": "logo do Notion", + "please_complete_mapping_fields_with_notion_property": "Por favor, complete o mapeamento dos campos com a propriedade do Notion", + "please_resolve_mapping_errors": "Por favor, resolva os erros de mapeamento", + "please_select_a_database": "Por favor, selecione um banco de dados", + "please_select_at_least_one_mapping": "Por favor, selecione pelo menos um mapeamento", + "que_name_of_type_cant_be_mapped_to": "{que_name} do tipo {question_label} não pode ser mapeado para a coluna {col_name} do tipo {col_type}. Em vez disso, use uma coluna do tipo {mapped_type}.", + "select_a_database": "Selecionar Banco de Dados", + "select_a_field_to_map": "Selecione um campo para mapear", + "select_a_survey_question": "Escolha uma pergunta da pesquisa", + "sync_responses_with_a_notion_database": "Sincronizar respostas com um banco de dados do Notion", + "update_connection": "Reconectar Notion", + "update_connection_tooltip": "Reconecte a integração para incluir os novos bancos de dados adicionados. Suas integrações existentes permanecerão intactas." + }, + "notion_integration_description": "Enviar dados para seu banco de dados do Notion", + "please_select_a_survey_error": "Por favor, escolha uma pesquisa", + "select_at_least_one_question_error": "Por favor, selecione pelo menos uma pergunta", + "slack": { + "already_connected_another_survey": "Você já conectou outra pesquisa a este canal.", + "channel_name": "Nome do Canal", + "connect_with_slack": "Conectar com o Slack", + "connect_your_first_slack_channel": "Conecte seu primeiro canal do Slack para começar.", + "connected_with_team": "Conectado com {team}", + "create_at_least_one_channel_error": "Você tem que criar pelo menos um canal para poder configurar essa integração", + "dont_see_your_channel": "Não está vendo seu canal?", + "link_channel": "Canal de link", + "link_slack_channel": "Link do Canal do Slack", + "please_select_a_channel": "Por favor, escolha um canal", + "select_channel": "Selecionar Canal", + "slack_integration": "Integração com o Slack", + "slack_integration_description": "Manda as respostas direto pro Slack.", + "slack_integration_is_not_configured": "A integração do Slack não está configurada na sua instância do Formbricks.", + "slack_reconnect_button": "Reconectar", + "slack_reconnect_button_description": "Observação: Recentemente, alteramos nossa integração com o Slack para também suportar canais privados. Por favor, reconecte seu workspace do Slack." + }, + "slack_integration_description": "Conecte instantaneamente seu Workspace do Slack com o Formbricks", + "to_configure_it": "configurar isso.", + "webhook_integration_description": "Dispare Webhooks com base nas ações nas suas pesquisas", + "webhooks": { + "add_webhook": "Adicionar Webhook", + "add_webhook_description": "Enviar dados das respostas da pesquisa para um endpoint personalizado", + "all_current_and_new_surveys": "Todas as pesquisas atuais e novas", + "created_by_third_party": "Criado por um Terceiro", + "discord_webhook_not_supported": "Webhooks do Discord não são suportados no momento.", + "empty_webhook_message": "Seus webhooks vão aparecer aqui assim que você adicioná-los. ⏲️", + "endpoint_pinged": "Uhul! Conseguimos pingar o webhook!", + "endpoint_pinged_error": "Não consegui pingar o webhook!", + "please_check_console": "Por favor, verifica o console para mais detalhes", + "please_enter_a_url": "Por favor, insira uma URL", + "response_created": "Resposta Criada", + "response_finished": "Resposta Finalizada", + "response_updated": "Resposta Atualizada", + "source": "fonte", + "test_endpoint": "Testar Ponto de Extremidade", + "triggers": "gatilhos", + "webhook_added_successfully": "Webhook adicionado com sucesso", + "webhook_delete_confirmation": "Tem certeza de que quer deletar esse Webhook? Isso vai parar de te enviar qualquer notificação.", + "webhook_deleted_successfully": "Webhook deletado com sucesso", + "webhook_name_placeholder": "Opcional: Dê um nome ao seu webhook para facilitar a identificação", + "webhook_test_failed_due_to": "Teste de Webhook Falhou devido a", + "webhook_updated_successfully": "Webhook atualizado com sucesso.", + "webhook_url_placeholder": "Cola a URL na qual você quer que o evento seja acionado" + }, + "website_or_app_integration_description": "Integrar o Formbricks no seu site ou app", + "zapier_integration_description": "Integrar o Formbricks com mais de 5000 apps via Zapier" + }, + "project": { + "api_keys": { + "add_api_key": "Adicionar Chave API", + "api_key": "Chave de API", + "api_key_copied_to_clipboard": "Chave da API copiada para a área de transferência", + "api_key_created": "Chave da API criada", + "api_key_deleted": "Chave da API deletada", + "api_key_label": "Rótulo da Chave API", + "api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", + "api_key_updated": "Chave de API atualizada", + "duplicate_access": "Acesso duplicado ao projeto não permitido", + "no_api_keys_yet": "Você ainda não tem nenhuma chave de API", + "no_env_permissions_found": "Nenhuma permissão de ambiente encontrada", + "organization_access": "Acesso à Organização", + "permissions": "Permissões", + "project_access": "Acesso ao Projeto", + "secret": "Segredo", + "unable_to_delete_api_key": "Não foi possível deletar a Chave API" + }, + "app-connection": { + "api_host_description": "Essa é a URL do seu backend do Formbricks.", + "app_connection": "Conexão do App", + "app_connection_description": "Conecte seu app ao Formbricks.", + "check_out_the_docs": "Confere a documentação.", + "dive_into_the_docs": "Mergulha na documentação.", + "does_your_widget_work": "Seu widget funciona?", + "environment_id": "Seu Id do Ambiente", + "environment_id_description": "Este ID identifica exclusivamente este ambiente do Formbricks.", + "environment_id_description_with_environment_id": "Usado para identificar o ambiente correto: {environmentId} é o seu.", + "formbricks_sdk": "SDK do Formbricks", + "formbricks_sdk_connected": "O SDK do Formbricks está conectado", + "formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.", + "formbricks_sdk_not_connected_description": "Conecte seu site ou app com o Formbricks", + "have_a_problem": "Tá com problema?", + "how_to_setup": "Como configurar", + "how_to_setup_description": "Siga esses passos para configurar o widget do Formbricks no seu app.", + "identifying_your_users": "identificando seus usuários", + "if_you_are_planning_to": "Se você está planejando", + "insert_this_code_into_the": "Insere esse código no", + "need_a_more_detailed_setup_guide_for": "Preciso de um guia de configuração mais detalhado para", + "not_working": "Não tá funcionando?", + "open_an_issue_on_github": "Abre uma issue no GitHub", + "open_the_browser_console_to_see_the_logs": "Abre o console do navegador pra ver os logs.", + "receiving_data": "Recebendo dados \uD83D\uDC83\uD83D\uDD7A", + "recheck": "Verificar novamente", + "scroll_to_the_top": "Rola pra cima!", + "step_1": "Passo 1: Instale com pnpm, npm ou yarn", + "step_2": "Passo 2: Iniciar widget", + "step_2_description": "Importe o Formbricks e inicialize o widget no seu Componente (por exemplo, App.tsx):", + "step_3": "Passo 3: Modo de depuração", + "switch_on_the_debug_mode_by_appending": "Ative o modo de depuração adicionando", + "tag_of_your_app": "etiqueta do seu app", + "to_the_url_where_you_load_the": "para a URL onde você carrega o", + "want_to_learn_how_to_add_user_attributes": "Quer aprender como adicionar atributos de usuário, eventos personalizados e mais?", + "you_are_done": "Você terminou \uD83C\uDF89", + "you_can_set_the_user_id_with": "você pode definir o id do usuário com", + "your_app_now_communicates_with_formbricks": "Seu app agora se comunica com o Formbricks - enviando eventos e carregando pesquisas automaticamente!" + }, + "general": { + "cannot_delete_only_project": "Esse é seu único projeto, não pode ser deletado. Crie um novo projeto primeiro.", + "delete_project": "Excluir Projeto", + "delete_project_confirmation": "Tem certeza de que quer deletar {projectName}? Essa ação não pode ser desfeita.", + "delete_project_name_includes_surveys_responses_people_and_more": "Excluir {projectName} incl. todas as pesquisas, respostas, pessoas, ações e atributos.", + "delete_project_settings_description": "Excluir projeto com todas as pesquisas, respostas, pessoas, ações e atributos. Isso não pode ser desfeito.", + "error_saving_project_information": "Erro ao salvar informações do projeto", + "only_owners_or_managers_can_delete_projects": "Apenas proprietários ou gerentes podem excluir projetos", + "project_deleted_successfully": "Projeto deletado com sucesso", + "project_name_settings_description": "Mude o nome do seu projeto.", + "project_name_updated_successfully": "Nome do projeto atualizado com sucesso", + "recontact_waiting_time": "Tempo de Espera para Recontato", + "recontact_waiting_time_settings_description": "Controle com que frequência os usuários podem ser pesquisados em todas as pesquisas do app.", + "this_action_cannot_be_undone": "Essa ação não pode ser desfeita.", + "wait_x_days_before_showing_next_survey": "Espere X dias antes de mostrar a próxima pesquisa:", + "waiting_period_updated_successfully": "Período de espera atualizado com sucesso", + "whats_your_project_called": "Como é o nome do seu projeto?" + }, + "languages": { + "add_language": "Adicionar idioma", + "alias": "Apelido", + "alias_tooltip": "O apelido é um nome alternativo para identificar o idioma em pesquisas de link e no SDK (opcional)", + "cannot_remove_language_warning": "Você não pode remover esse idioma porque ele ainda é usado nessas pesquisas:", + "conflict_between_identifier_and_alias": "Tem um conflito entre o identificador de um idioma adicionado e um dos seus aliases. Aliases e identificadores não podem ser iguais.", + "conflict_between_selected_alias_and_another_language": "Tem um conflito entre o alias selecionado e outra linguagem que tem esse identificador. Adiciona a linguagem com esse identificador ao seu projeto pra evitar inconsistências.", + "delete_language_confirmation": "Tem certeza de que quer deletar esse idioma? Essa ação não pode ser desfeita.", + "duplicate_language_or_language_id": "Linguagem ou ID de linguagem duplicado", + "edit_languages": "Editar idiomas", + "identifier": "Identificador (ISO)", + "incomplete_translations": "Traduções incompletas", + "language": "Língua", + "language_deleted_successfully": "Linguagem deletada com sucesso", + "languages_updated_successfully": "Idiomas atualizados com sucesso", + "multi_language_surveys": "Pesquisas Multilíngues", + "multi_language_surveys_description": "Adicione idiomas para criar pesquisas multilíngues.", + "no_language_found": "Nenhum idioma encontrado. Adicione seu primeiro idioma abaixo.", + "please_select_a_language": "Por favor, escolha um idioma", + "remove_language": "Remover Idioma", + "remove_language_from_surveys_to_remove_it_from_project": "Por favor, remova o idioma dessas pesquisas para tirá-lo do projeto.", + "search_items": "Buscar itens", + "translate": "traduzir" + }, + "look": { + "add_background_color": "Adicionar cor de fundo", + "add_background_color_description": "Adicione uma cor de fundo ao contêiner do logo.", + "app_survey_placement": "Posicionamento da Pesquisa no App", + "app_survey_placement_settings_description": "Mude onde as pesquisas serão exibidas no seu app ou site.", + "centered_modal_overlay_color": "Cor de sobreposição modal centralizada", + "email_customization": "Personalização de Email", + "email_customization_description": "Mude a aparência e o estilo dos e-mails que o Formbricks envia em seu nome.", + "enable_custom_styling": "Ativar estilo personalizado", + "enable_custom_styling_description": "Permitir que os usuários alterem esse tema no editor de pesquisa.", + "failed_to_remove_logo": "Falha ao remover o logo", + "failed_to_update_logo": "Falha ao atualizar o logo", + "formbricks_branding": "Marca Formbricks", + "formbricks_branding_hidden": "A marca da Formbricks está oculta.", + "formbricks_branding_settings_description": "A gente adora seu apoio, mas entende se você quiser desativar.", + "formbricks_branding_shown": "A marca da Formbricks é exibida.", + "logo_removed_successfully": "Logo removido com sucesso", + "logo_settings_description": "Faça o upload do logo da sua empresa para personalizar pesquisas e pré-visualizações de links.", + "logo_updated_successfully": "Logo atualizado com sucesso", + "logo_upload_failed": "Falha no upload do logo. Tente novamente.", + "placement_updated_successfully": "Posicionamento atualizado com sucesso", + "remove_branding_with_a_higher_plan": "Remover marca com um plano superior", + "remove_logo": "Remover Logo", + "remove_logo_confirmation": "Tem certeza que quer remover o logo?", + "replace_logo": "Trocar Logo", + "reset_styling": "Redefinir estilo", + "reset_styling_confirmation": "Tem certeza que quer resetar o estilo para o padrão?", + "show_formbricks_branding_in": "Mostrar a marca Formbricks em pesquisas {type}", + "show_powered_by_formbricks": "Mostrar assinatura \"Powered by Formbricks", + "styling_updated_successfully": "Estilo atualizado com sucesso", + "theme": "Tema", + "theme_settings_description": "Crie um tema de estilo para todas as pesquisas. Você pode habilitar a personalização de estilo para cada pesquisa." + }, + "tags": { + "add": "Adicionar", + "add_tag": "Adicionar Tag", + "count": "Contar", + "delete_tag_confirmation": "Tem certeza de que quer deletar essa tag?", + "empty_message": "Marque uma submissão para encontrar sua lista de tags aqui.", + "manage_tags": "Gerenciar Tags", + "manage_tags_description": "Mesclar e remover tags de resposta.", + "merge": "mesclar", + "no_tag_found": "Tag não encontrada", + "search_tags": "Buscar Tags...", + "tag": "etiqueta", + "tag_already_exists": "Tag já existe", + "tag_deleted": "Tag apagada", + "tag_updated": "Tag atualizada", + "tags_merged": "Tags mescladas", + "unique_constraint_failed_on_the_fields": "Falha na restrição única nos campos" + }, + "teams": { + "manage_teams": "Gerenciar Equipes", + "no_teams_found": "Nenhuma equipe encontrada", + "only_organization_owners_and_managers_can_manage_teams": "Apenas proprietários e gerentes da organização podem gerenciar equipes.", + "permission": "Permissão", + "team_name": "Nome da equipe", + "team_settings_description": "As equipes e seus membros podem acessar este projeto e suas pesquisas. Proprietários e gerentes da organização podem conceder esse acesso." + } + }, + "projects_environments_organizations_not_found": "Projetos, ambientes ou organizações não encontrados", + "segments": { + "add_filter_below": "Adicionar filtro abaixo", + "add_your_first_filter_to_get_started": "Adicione seu primeiro filtro para começar", + "cannot_delete_segment_used_in_surveys": "Você não pode deletar esse segmento porque ele ainda é usado nessas pesquisas:", + "clone_and_edit_segment": "Clonar e Editar Segmento", + "create_group": "Criar grupo", + "create_your_first_segment": "Crie seu primeiro Segmento para começar", + "delete_segment": "Excluir Segmento", + "desktop": "Área de trabalho", + "devices": "Dispositivos", + "edit_segment": "Editar Segmento", + "error_resetting_filters": "Erro ao redefinir filtros", + "error_saving_segment": "Erro ao salvar segmento", + "ex_fully_activated_recurring_users": "Ex. Usuários recorrentes totalmente ativados", + "ex_power_users": "Usuários Avançados", + "filters_reset_successfully": "Filtros redefinidos com sucesso", + "here": "aqui", + "hide_filters": "Esconder filtros", + "identifying_users": "identificando usuários", + "invalid_segment": "Segmento inválido", + "invalid_segment_filters": "Filtros inválidos. Por favor, verifique os filtros e tente novamente.", + "load_segment": "Segmento de Carga", + "most_active_users_in_the_last_30_days": "Usuários mais ativos nos últimos 30 dias", + "no_attributes_yet": "Ainda não tem atributos!", + "no_filters_yet": "Ainda não tem filtros!", + "no_segments_yet": "Você não tem segmentos salvos no momento.", + "person_and_attributes": "Pessoa & Atributos", + "phone": "Celular", + "please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Por favor, remova o segmento dessas pesquisas para deletá-lo.", + "pre_segment_users": "Pré-segmente seus usuários com filtros de atributos.", + "remove_all_filters": "Remover todos os filtros", + "reset_all_filters": "Redefinir todos os filtros", + "save_as_new_segment": "Salvar como novo segmento", + "save_your_filters_as_a_segment_to_use_it_in_other_surveys": "Salve seus filtros como um Segmento para usar em outras pesquisas", + "segment_created_successfully": "Segmento criado com sucesso!", + "segment_deleted_successfully": "Segmento deletado com sucesso!", + "segment_id": "ID do segmento", + "segment_saved_successfully": "Segmento salvo com sucesso", + "segment_updated_successfully": "Segmento atualizado com sucesso!", + "segments_help_you_target_users_with_same_characteristics_easily": "Segmentos ajudam você a direcionar usuários com as mesmas características facilmente", + "target_audience": "Público-alvo", + "this_action_resets_all_filters_in_this_survey": "Essa ação reseta todos os filtros dessa pesquisa.", + "this_segment_is_used_in_other_surveys": "Esse segmento é usado em outras pesquisas. Faça alterações", + "title_is_required": "É necessário um título.", + "unknown_filter_type": "Tipo de filtro desconhecido", + "unlock_segments_description": "Organize contatos em segmentos para direcionar grupos específicos de usuários", + "unlock_segments_title": "Desbloqueie segmentos com um plano superior", + "user_targeting_is_currently_only_available_when": "A segmentação de usuários está disponível apenas quando", + "value_cannot_be_empty": "O valor não pode estar vazio.", + "value_must_be_a_number": "O valor deve ser um número.", + "view_filters": "Ver filtros", + "where": "Onde", + "with_the_formbricks_sdk": "com o SDK do Formbricks." + }, + "settings": { + "api_keys": { + "add_api_key": "Adicionar chave de API", + "add_permission": "Adicionar permissão", + "api_keys_description": "Gerencie chaves de API para acessar as APIs de gerenciamento do Formbricks" + }, + "billing": { + "10000_monthly_responses": "10000 Respostas Mensais", + "1500_monthly_responses": "1500 Respostas Mensais", + "2000_monthly_identified_users": "2000 Usuários Identificados Mensalmente", + "30000_monthly_identified_users": "30000 Usuários Identificados Mensalmente", + "3_projects": "3 Projetos", + "5000_monthly_responses": "5000 Respostas Mensais", + "5_projects": "5 Projetos", + "7500_monthly_identified_users": "7500 Usuários Identificados Mensalmente", + "advanced_targeting": "Mira Avançada", + "all_integrations": "Todas as Integrações", + "all_surveying_features": "Todos os recursos de levantamento", + "annually": "anualmente", + "api_webhooks": "API e Webhooks", + "app_surveys": "Pesquisas de App", + "contact_us": "Fale Conosco", + "current": "atual", + "current_plan": "Plano Atual", + "current_tier_limit": "Limite Atual de Nível", + "custom_miu_limit": "Limite MIU personalizado", + "custom_project_limit": "Limite de Projeto Personalizado", + "customer_success_manager": "Gerente de Sucesso do Cliente", + "email_embedded_surveys": "Pesquisas Incorporadas no Email", + "email_support": "Suporte por Email", + "enterprise": "Empresa", + "enterprise_description": "Suporte premium e limites personalizados.", + "everybody_has_the_free_plan_by_default": "Todo mundo tem o plano gratuito por padrão!", + "everything_in_free": "Tudo de graça", + "everything_in_scale": "Tudo em Escala", + "everything_in_startup": "Tudo em Startup", + "free": "grátis", + "free_description": "Pesquisas ilimitadas, membros da equipe e mais.", + "get_2_months_free": "Ganhe 2 meses grátis", + "get_in_touch": "Entre em contato", + "link_surveys": "Link de Pesquisas (Compartilhável)", + "logic_jumps_hidden_fields_recurring_surveys": "Pulos Lógicos, Campos Ocultos, Pesquisas Recorrentes, etc.", + "manage_card_details": "Gerenciar Detalhes do Cartão", + "manage_subscription": "Gerenciar Assinatura", + "monthly": "mensal", + "monthly_identified_users": "Usuários Identificados Mensalmente", + "multi_language_surveys": "Pesquisas Multilíngues", + "per_month": "por mês", + "per_year": "por ano", + "plan_upgraded_successfully": "Plano atualizado com sucesso", + "premium_support_with_slas": "Suporte premium com SLAs", + "priority_support": "Suporte Prioritário", + "remove_branding": "Remover Marca", + "say_hi": "Diz oi!", + "scale": "escala", + "scale_description": "Recursos avançados pra escalar seu negócio.", + "startup": "startup", + "startup_description": "Tudo no Grátis com recursos adicionais.", + "switch_plan": "Mudar Plano", + "switch_plan_confirmation_text": "Tem certeza de que deseja mudar para o plano {plan}? Você será cobrado {price} {period}.", + "team_access_roles": "Funções de Acesso da Equipe", + "technical_onboarding": "Integração Técnica", + "unable_to_upgrade_plan": "Não foi possível atualizar o plano", + "unlimited_apps_websites": "Apps e Sites Ilimitados", + "unlimited_miu": "MIU Ilimitado", + "unlimited_projects": "Projetos Ilimitados", + "unlimited_responses": "Respostas Ilimitadas", + "unlimited_surveys": "Pesquisas Ilimitadas", + "unlimited_team_members": "Membros Ilimitados na Equipe", + "upgrade": "Atualizar", + "uptime_sla_99": "Tempo de atividade SLA (99%)", + "website_surveys": "Pesquisas de Site" + }, + "enterprise": { + "ai": "Análise de IA", + "audit_logs": "Registros de Auditoria", + "coming_soon": "Em breve", + "contacts_and_segments": "Gerenciamento de contatos e segmentos", + "enterprise_features": "Recursos Empresariais", + "get_an_enterprise_license_to_get_access_to_all_features": "Adquira uma licença Enterprise para ter acesso a todos os recursos.", + "keep_full_control_over_your_data_privacy_and_security": "Mantenha controle total sobre a privacidade e segurança dos seus dados.", + "no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sem necessidade de ligação, sem compromisso: Solicite uma licença de teste gratuita de 30 dias para testar todas as funcionalidades preenchendo este formulário:", + "no_credit_card_no_sales_call_just_test_it": "Sem cartão de crédito. Sem ligação de vendas. Só teste :)", + "on_request": "Quando solicitado", + "organization_roles": "Funções na Organização (Admin, Editor, Desenvolvedor, etc.)", + "questions_please_reach_out_to": "Perguntas? Entre em contato com", + "request_30_day_trial_license": "Pedir Licença de Teste de 30 Dias", + "saml_sso": "SSO SAML", + "service_level_agreement": "Acordo de Nível de Serviço", + "soc2_hipaa_iso_27001_compliance_check": "Verificação de conformidade SOC2, HIPAA, ISO 27001", + "sso": "SSO (Google, Microsoft, OpenID Connect)", + "teams": "Equipes e Funções de Acesso (Ler, Ler e Escrever, Gerenciar)", + "unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias.", + "your_enterprise_license_is_active_all_features_unlocked": "Sua licença empresarial está ativa. Todos os recursos estão desbloqueados." + }, + "general": { + "bulk_invite_warning_description": "Por favor, note que no Plano Gratuito, todos os membros da organização são automaticamente atribuídos ao papel de 'Owner', independentemente do papel especificado no arquivo CSV.", + "cannot_delete_only_organization": "Essa é sua única organização, não pode ser deletada. Crie uma nova organização primeiro.", + "cannot_leave_only_organization": "Você não pode sair dessa organização porque é a sua única. Crie uma nova organização primeiro.", + "copy_invite_link_to_clipboard": "Copiar link do convite para a área de transferência", + "create_new_organization": "Criar nova organização", + "create_new_organization_description": "Criar uma nova organização para lidar com um conjunto diferente de projetos.", + "customize_email_with_a_higher_plan": "Personalize o email com um plano superior", + "delete_organization": "Excluir Organização", + "delete_organization_description": "Excluir organização com todos os seus projetos, incluindo todas as pesquisas, respostas, pessoas, ações e atributos", + "delete_organization_warning": "Antes de continuar com a exclusão desta organização, esteja ciente das seguintes consequências:", + "delete_organization_warning_1": "Remoção permanente de todos os projetos ligados a essa organização.", + "delete_organization_warning_2": "Essa ação não pode ser desfeita. Se foi, foi.", + "delete_organization_warning_3": "Por favor, insira {organizationName} no campo abaixo para confirmar a exclusão definitiva desta organização:", + "eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.", + "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.", + "enable_formbricks_ai": "Ativar Formbricks IA", + "error_deleting_organization_please_try_again": "Erro ao deletar a organização. Por favor, tente novamente.", + "formbricks_ai": "Formbricks IA", + "formbricks_ai_description": "Obtenha insights personalizados das suas respostas de pesquisa com o Formbricks AI", + "formbricks_ai_disable_success_message": "Formbricks AI desativado com sucesso.", + "formbricks_ai_enable_success_message": "Formbricks AI ativado com sucesso.", + "formbricks_ai_privacy_policy_text": "Ao ativar o Formbricks AI, você concorda com a versão atualizada", + "from_your_organization": "da sua organização", + "invitation_sent_once_more": "Convite enviado de novo.", + "invite_deleted_successfully": "Convite deletado com sucesso", + "invited_on": "Convidado em {date}", + "invites_failed": "Convites falharam", + "leave_organization": "Sair da organização", + "leave_organization_description": "Você vai sair dessa organização e perder acesso a todas as pesquisas e respostas. Você só pode voltar se for convidado de novo.", + "leave_organization_ok_btn_text": "Sim, sair da organização", + "leave_organization_title": "Você tem certeza?", + "logo_in_email_header": "Logo na cabeçalho do e-mail", + "logo_removed_successfully": "Logo removido com sucesso", + "logo_saved_successfully": "Logo salvo com sucesso", + "manage_members": "Gerenciar membros", + "manage_members_description": "Adicionar ou remover membros na sua organização.", + "member_deleted_successfully": "Membro deletado com sucesso", + "member_invited_successfully": "Membro convidado com sucesso", + "once_its_gone_its_gone": "Uma vez que se foi, se foi.", + "only_org_owner_can_perform_action": "Somente o Dono pode deletar a organização.", + "organization_created_successfully": "Organização criada com sucesso!", + "organization_deleted_successfully": "Organização deletada com sucesso.", + "organization_invite_link_ready": "O link de convite da sua organização está pronto!", + "organization_name": "Nome da Organização", + "organization_name_description": "Dê um nome descritivo pra sua organização.", + "organization_name_placeholder": "por exemplo, Meninas Superpoderosas", + "organization_name_updated_successfully": "Nome da organização atualizado com sucesso", + "organization_settings": "Configurações da Organização", + "please_add_a_logo": "Por favor, adicione um logo", + "please_check_csv_file": "Por favor, verifique o arquivo CSV e certifique-se de que está de acordo com o nosso formato", + "please_save_logo_before_sending_test_email": "Por favor, salve o logo antes de enviar um e-mail de teste.", + "remove_logo": "Remover logo", + "replace_logo": "Substituir logo", + "resend_invitation_email": "Reenviar E-mail de Convite", + "share_invite_link": "Compartilhar Link de Convite", + "share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:", + "test_email_sent_successfully": "E-mail de teste enviado com sucesso", + "use_multi_language_surveys_with_a_higher_plan": "Use pesquisas multilíngues com um plano superior", + "use_multi_language_surveys_with_a_higher_plan_description": "Pesquise seus usuários em diferentes idiomas." + }, + "notifications": { + "auto_subscribe_to_new_surveys": "Inscrever-se automaticamente em novas pesquisas", + "email_alerts_surveys": "Alertas de email (Pesquisas)", + "every_response": "Cada resposta", + "every_response_tooltip": "Envia respostas completas, nada de parciais.", + "need_slack_or_discord_notifications": "Preciso de notificações no Slack ou Discord", + "notification_settings_updated": "Configurações de notificação atualizadas", + "set_up_an_alert_to_get_an_email_on_new_responses": "Configura um alerta pra receber um e-mail com novas respostas", + "stay_up_to_date_with_a_Weekly_every_Monday": "Fique por dentro com um resumo semanal toda segunda-feira", + "use_the_integration": "Use a integração", + "want_to_loop_in_organization_mates": "Quero incluir os colegas da organização", + "weekly_summary_projects": "Resumo semanal (Projetos)", + "you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore": "Você não vai ser mais inscrito automaticamente nas pesquisas dessa organização!", + "you_will_not_receive_any_more_emails_for_responses_on_this_survey": "Você não vai receber mais e-mails sobre respostas dessa pesquisa!" + }, + "profile": { + "account_deletion_consequences_warning": "Consequências da exclusão da conta", + "avatar_update_failed": "Falha ao atualizar o avatar. Por favor, tente novamente.", + "backup_code": "Código de Backup", + "change_image": "Mudar imagem", + "confirm_delete_account": "Apague sua conta com todas as suas informações pessoais e dados", + "confirm_delete_my_account": "Excluir Minha Conta", + "confirm_your_current_password_to_get_started": "Confirme sua senha atual para começar.", + "delete_account": "Excluir Conta", + "disable_two_factor_authentication": "Desativar a autenticação de dois fatores", + "disable_two_factor_authentication_description": "Se você precisar desativar a 2FA, recomendamos reativá-la o mais rápido possível.", + "each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Cada código de backup pode ser usado exatamente uma vez para conceder acesso sem o seu autenticador.", + "enable_two_factor_authentication": "Ativar autenticação de dois fatores", + "enter_the_code_from_your_authenticator_app_below": "Digite o código do seu app autenticador abaixo.", + "file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.", + "invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.", + "lost_access": "Perdi o acesso", + "or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:", + "organization_identification": "Ajude sua organização a te identificar no Formbricks", + "organizations_delete_message": "Você é o único dono dessas organizações, então elas também serão apagadas.", + "permanent_removal_of_all_of_your_personal_information_and_data": "Remoção permanente de todas as suas informações e dados pessoais", + "personal_information": "Informações pessoais", + "please_enter_email_to_confirm_account_deletion": "Por favor, insira {email} no campo abaixo para confirmar a exclusão definitiva da sua conta:", + "profile_updated_successfully": "Seu perfil foi atualizado com sucesso", + "remove_image": "Remover imagem", + "save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup em um lugar seguro.", + "scan_the_qr_code_below_with_your_authenticator_app": "Escaneie o código QR abaixo com seu app autenticador.", + "security_description": "Gerencie sua senha e outras configurações de segurança como a autenticação de dois fatores (2FA).", + "two_factor_authentication": "Autenticação de dois fatores", + "two_factor_authentication_description": "Adicione uma camada extra de segurança à sua conta caso sua senha seja roubada.", + "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autenticação de dois fatores ativada. Por favor, insira o código de seis dígitos do seu app autenticador.", + "two_factor_code": "Código de Dois Fatores", + "unlock_two_factor_authentication": "Desbloqueia a autenticação de dois fatores com um plano melhor", + "update_personal_info": "Atualize suas informações pessoais", + "upload_image": "Enviar imagem", + "warning_cannot_delete_account": "Você é o único dono desta organização. Transfere a propriedade para outra pessoa primeiro.", + "warning_cannot_undo": "Isso não pode ser desfeito", + "you_must_select_a_file": "Você tem que selecionar um arquivo." + }, + "teams": { + "add_members_description": "Adicione membros à equipe e determine sua função.", + "add_projects_description": "Controle quais projetos os membros da equipe podem acessar.", + "all_members_added": "Todos os membros adicionados a esta equipe.", + "all_projects_added": "Todos os projetos adicionados a esta equipe.", + "are_you_sure_you_want_to_delete_this_team": "Tem certeza de que deseja excluir esta equipe? Isso também remove o acesso a todos os projetos e pesquisas associados a esta equipe.", + "billing_role_description": "Apenas têm acesso às informações de faturamento.", + "bulk_invite": "Convite em Massa", + "contributor": "Contribuinte", + "create": "Criar", + "create_first_team_message": "Você precisa criar uma equipe primeiro.", + "create_new_team": "Criar nova equipe", + "delete_team": "Excluir equipe", + "empty_teams_state": "Crie sua primeira equipe.", + "enter_team_name": "Insira o nome da equipe", + "individual": "Individual", + "invite_member": "Convidar membro", + "invite_member_description": "Adicione seus colegas a esta organização.", + "manage": "Gerenciar", + "manage_team": "Gerenciar equipe", + "manage_team_disabled": "Apenas proprietários da organização, gerentes e administradores da equipe podem gerenciar equipes.", + "manager_role_description": "Os gerentes podem acessar todos os projetos e adicionar e remover membros.", + "member_role_description": "Os membros podem trabalhar em projetos selecionados.", + "member_role_info_message": "Para dar acesso a novos membros a um projeto, por favor, adicione-os a uma equipe abaixo. Com equipes, você pode gerenciar quem tem acesso a qual projeto.", + "owner_role_description": "Os proprietários têm controle total sobre a organização.", + "please_fill_all_member_fields": "Por favor, preencha todos os campos para adicionar um novo membro.", + "please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.", + "read": "Leitura", + "read_write": "Leitura & Escrita", + "team_admin": "Administrador da equipe", + "team_created_successfully": "Equipe criada com sucesso.", + "team_deleted_successfully": "Equipe excluída com sucesso.", + "team_deletion_not_allowed": "Você não tem permissão para excluir esta equipe.", + "team_name": "Nome da equipe", + "team_name_settings_title": "Configurações de {teamName}", + "team_select_placeholder": "Pesquisar nome da equipe...", + "team_settings_description": "Gerencie membros da equipe, direitos de acesso e muito mais.", + "team_updated_successfully": "Equipe atualizada com sucesso", + "teams": "Equipes", + "teams_description": "Atribua membros a equipes e dê acesso a projetos.", + "unlock_teams_description": "Gerencie quais membros da organização têm acesso a projetos e pesquisas específicos.", + "unlock_teams_title": "Desbloqueie equipes com um plano superior.", + "upgrade_plan_notice_message": "Desbloqueie Funções de Organização com um plano superior.", + "you_are_a_member": "Você é um membro" + } + }, + "surveys": { + "all_set_time_to_create_first_survey": "Tá tudo pronto! Hora de criar sua primeira pesquisa", + "alphabetical": "alfabético", + "copy_survey": "Copiar pesquisa", + "copy_survey_description": "Copiar essa pesquisa para outro ambiente", + "copy_survey_error": "Falha ao copiar pesquisa", + "copy_survey_link_to_clipboard": "Copiar link da pesquisa para a área de transferência", + "copy_survey_success": "Pesquisa copiada com sucesso!", + "delete_survey_and_responses_warning": "Você tem certeza de que quer deletar essa pesquisa e todas as suas respostas? Essa ação não pode ser desfeita.", + "edit": { + "1_choose_the_default_language_for_this_survey": "1. Escolha o idioma padrão para essa pesquisa:", + "2_activate_translation_for_specific_languages": "2. Ativar tradução para idiomas específicos:", + "add": "Adicionar +", + "add_a_delay_or_auto_close_the_survey": "Adicione um atraso ou feche a pesquisa automaticamente", + "add_a_four_digit_pin": "Adicione um PIN de quatro dígitos", + "add_a_new_question_to_your_survey": "Adicionar uma nova pergunta à sua pesquisa", + "add_a_variable_to_calculate": "Adicione uma variável para calcular", + "add_action_below": "Adicionar ação abaixo", + "add_choice_below": "Adicionar opção abaixo", + "add_color_coding": "Adicionar codificação por cores", + "add_color_coding_description": "Adicione os códigos de cores vermelho, laranja e verde às opções.", + "add_column": "Adicionar coluna", + "add_condition_below": "Adicionar condição abaixo", + "add_custom_styles": "Adicionar estilos personalizados", + "add_delay_before_showing_survey": "Adicionar atraso antes de mostrar a pesquisa", + "add_description": "Adicionar Descrição", + "add_ending": "Adicionar final", + "add_ending_below": "Adicione o final abaixo", + "add_hidden_field_id": "Adicionar campo oculto ID", + "add_highlight_border": "Adicionar borda de destaque", + "add_highlight_border_description": "Adicione uma borda externa ao seu cartão de pesquisa.", + "add_logic": "Adicionar lógica", + "add_option": "Adicionar opção", + "add_other": "Adicionar \"Outro", + "add_photo_or_video": "Adicionar foto ou video", + "add_pin": "Adicionar PIN", + "add_question": "Adicionar pergunta", + "add_question_below": "Adicione a pergunta abaixo", + "add_row": "Adicionar linha", + "add_variable": "Adicionar variável", + "address_fields": "Campos de Endereço", + "address_line_1": "Endereço Linha 1", + "address_line_2": "Complemento", + "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_other_answers_will_continue_to": "Todas as outras respostas continuarão a", + "allow_file_type": "Permitir tipo de arquivo", + "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", + "always_show_survey": "Mostrar pesquisa sempre", + "and_launch_surveys_in_your_website_or_app": "e lançar pesquisas no seu site ou app.", + "animation": "animação", + "app_survey_description": "Embuta uma pesquisa no seu app ou site para coletar respostas.", + "assign": "atribuir =", + "audience": "Público", + "auto_close_on_inactivity": "Fechar automaticamente por inatividade", + "automatically_close_survey_after": "Fechar pesquisa automaticamente após", + "automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente a pesquisa depois de um certo número de respostas.", + "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Feche automaticamente a pesquisa se o usuário não responder depois de alguns segundos.", + "automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Fecha automaticamente a pesquisa no começo do dia (UTC).", + "automatically_mark_the_survey_as_complete_after": "Marcar automaticamente a pesquisa como concluída após", + "automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Liberar automaticamente a pesquisa no começo do dia (UTC).", + "back_button_label": "Voltar", + "background_styling": "Estilo de Fundo", + "blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "Bloqueia a pesquisa se já existir uma submissão com o Id de Uso Único (suId).", + "blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "Bloqueia a pesquisa se a URL da pesquisa não tiver um Id de Uso Único (suId).", + "brand_color": "Cor da marca", + "brightness": "brilho", + "button_label": "Rótulo do Botão", + "button_to_continue_in_survey": "Botão para continuar na pesquisa", + "button_to_link_to_external_url": "Botão para link externo", + "button_url": "URL do Botão", + "cal_username": "Nome de usuário do Cal.com ou nome de usuário/evento", + "calculate": "Calcular", + "capture_a_new_action_to_trigger_a_survey_on": "Captura uma nova ação pra disparar uma pesquisa.", + "capture_new_action": "Capturar nova ação", + "card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Pesquisas {surveyTypeDerived}", + "card_background_color": "Cor de fundo do cartão", + "card_border_color": "Cor da borda do cartão", + "card_shadow_color": "cor da sombra do cartão", + "card_styling": "Estilização de Cartão", + "casual": "Casual", + "caution_text": "Mudanças vão levar a inconsistências", + "centered_modal_overlay_color": "cor de sobreposição modal centralizada", + "change_anyway": "Mudar mesmo assim", + "change_background": "Mudar fundo", + "change_question_type": "Mudar tipo de pergunta", + "change_the_background_color_of_the_card": "Muda a cor de fundo do cartão.", + "change_the_background_color_of_the_input_fields": "Mude a cor de fundo dos campos de entrada.", + "change_the_background_to_a_color_image_or_animation": "Mude o fundo para uma cor, imagem ou animação.", + "change_the_border_color_of_the_card": "Muda a cor da borda do cartão.", + "change_the_border_color_of_the_input_fields": "Mude a cor da borda dos campos de entrada.", + "change_the_border_radius_of_the_card_and_the_inputs": "Muda o raio da borda do card e dos inputs.", + "change_the_brand_color_of_the_survey": "Muda a cor da marca da pesquisa.", + "change_the_placement_of_this_survey": "Muda a posição dessa pesquisa.", + "change_the_question_color_of_the_survey": "Muda a cor da pergunta da pesquisa.", + "change_the_shadow_color_of_the_card": "Muda a cor da sombra do cartão.", + "changes_saved": "Mudanças salvas.", + "character_limit_toggle_description": "Limite o quão curta ou longa uma resposta pode ser.", + "character_limit_toggle_title": "Adicionar limites de caracteres", + "checkbox_label": "Rótulo da Caixa de Seleção", + "choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.", + "choose_where_to_run_the_survey": "Escolha onde realizar a pesquisa.", + "city": "cidade", + "close_survey_on_date": "Fechar pesquisa na data", + "close_survey_on_response_limit": "Fechar pesquisa ao atingir limite de respostas", + "color": "cor", + "columns": "colunas", + "company": "empresa", + "company_logo": "Logo da empresa", + "completed_responses": "respostas completas", + "concat": "Concatenar +", + "conditional_logic": "Lógica Condicional", + "confirm_default_language": "Confirmar idioma padrão", + "confirm_survey_changes": "Confirmar Alterações na Pesquisa", + "contact_fields": "Campos de Contato", + "contains": "contém", + "continue_to_settings": "Continuar para Configurações", + "control_which_file_types_can_be_uploaded": "Controlar quais tipos de arquivos podem ser enviados.", + "convert_to_multiple_choice": "Converter para Múltipla Escolha", + "convert_to_single_choice": "Converter para Escolha Única", + "country": "país", + "create_group": "Criar grupo", + "create_your_own_survey": "Crie sua própria pesquisa", + "css_selector": "Seletor CSS", + "custom_hostname": "Hostname personalizado", + "darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.", + "date_format": "Formato de data", + "days_before_showing_this_survey_again": "dias antes de mostrar essa pesquisa de novo.", + "decide_how_often_people_can_answer_this_survey": "Decida com que frequência as pessoas podem responder a essa pesquisa.", + "delete_choice": "Deletar opção", + "description": "Descrição", + "disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.", + "display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa de tempo de conclusão da pesquisa", + "display_number_of_responses_for_survey": "Mostrar número de respostas da pesquisa", + "divide": "Divida /", + "does_not_contain": "não contém", + "does_not_end_with": "Não termina com", + "does_not_equal": "não é igual", + "does_not_include_all_of": "Não inclui todos de", + "does_not_include_one_of": "Não inclui um de", + "does_not_start_with": "Não começa com", + "edit_recall": "Editar Lembrete", + "edit_translations": "Editar traduções de {lang}", + "enable_encryption_of_single_use_id_suid_in_survey_url": "Habilitar criptografia do Id de Uso Único (suId) na URL da pesquisa.", + "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os participantes mudem o idioma da pesquisa a qualquer momento durante a pesquisa.", + "end_screen_card": "cartão de tela final", + "ending_card": "Cartão de encerramento", + "ending_card_used_in_logic": "Esse cartão de encerramento é usado na lógica da pergunta {questionIndex}.", + "ends_with": "Termina com", + "equals": "Igual", + "equals_one_of": "É igual a um de", + "error_publishing_survey": "Ocorreu um erro ao publicar a pesquisa.", + "error_saving_changes": "Erro ao salvar alterações", + "even_after_they_submitted_a_response_e_g_feedback_box": "Mesmo depois de eles enviarem uma resposta (por exemplo, Caixa de Feedback)", + "everyone": "Todo mundo", + "fallback_missing": "Faltando alternativa", + "fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} é usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.", + "field_name_eg_score_price": "Nome do campo, por exemplo, pontuação, preço", + "first_name": "Primeiro Nome", + "five_points_recommended": "5 pontos (recomendado)", + "follow_ups": "Acompanhamentos", + "follow_ups_delete_modal_text": "Tem certeza de que deseja excluir este acompanhamento?", + "follow_ups_delete_modal_title": "Excluir acompanhamento?", + "follow_ups_empty_description": "Envie mensagens para os entrevistados, para você mesmo ou para os colegas de equipe.", + "follow_ups_empty_heading": "Enviar acompanhamentos automáticos", + "follow_ups_ending_card_delete_modal_text": "Este final é usado em acompanhamentos. Excluí-lo o removerá de todos os acompanhamentos. Tem certeza de que deseja excluí-lo?", + "follow_ups_ending_card_delete_modal_title": "Excluir cartão de final?", + "follow_ups_hidden_field_error": "O campo oculto está sendo usado em um acompanhamento. Por favor, remova-o do acompanhamento primeiro.", + "follow_ups_item_ending_tag": "Final(is)", + "follow_ups_item_issue_detected_tag": "Problema detectado", + "follow_ups_item_response_tag": "Qualquer resposta", + "follow_ups_item_send_email_tag": "Enviar e-mail", + "follow_ups_modal_action_attach_response_data_description": "Adicionar os dados da resposta da pesquisa ao acompanhamento", + "follow_ups_modal_action_attach_response_data_label": "Anexar dados da resposta", + "follow_ups_modal_action_body_label": "Corpo", + "follow_ups_modal_action_body_placeholder": "Corpo do e-mail", + "follow_ups_modal_action_email_content": "Conteúdo do e-mail", + "follow_ups_modal_action_email_settings": "Configuração de e-mail", + "follow_ups_modal_action_from_description": "Endereço de e-mail de onde o e-mail será enviado", + "follow_ups_modal_action_from_label": "De", + "follow_ups_modal_action_label": "Ação", + "follow_ups_modal_action_replyTo_description": "Se o destinatário responder, o seguinte endereço de e-mail receberá a resposta", + "follow_ups_modal_action_replyTo_label": "Responder para", + "follow_ups_modal_action_subject": "Valeu pelas respostas!", + "follow_ups_modal_action_subject_label": "Assunto", + "follow_ups_modal_action_subject_placeholder": "Assunto do e-mail", + "follow_ups_modal_action_to_description": "Endereço de e-mail para enviar o e-mail para", + "follow_ups_modal_action_to_label": "Para", + "follow_ups_modal_action_to_warning": "Nenhum campo de e-mail detectado na pesquisa", + "follow_ups_modal_create_heading": "Criar um novo acompanhamento", + "follow_ups_modal_edit_heading": "Editar este acompanhamento", + "follow_ups_modal_edit_no_id": "Nenhum ID de acompanhamento da pesquisa fornecido, não é possível atualizar o acompanhamento da pesquisa", + "follow_ups_modal_name_label": "Nome do acompanhamento", + "follow_ups_modal_name_placeholder": "Nomeie seu acompanhamento", + "follow_ups_modal_subheading": "Envie mensagens para os entrevistados, para você mesmo ou para os colegas de equipe", + "follow_ups_modal_trigger_description": "Quando este acompanhamento deve ser acionado?", + "follow_ups_modal_trigger_label": "Gatilho", + "follow_ups_modal_trigger_type_ending": "Respondente vê um final específico", + "follow_ups_modal_trigger_type_ending_select": "Selecione os finais: ", + "follow_ups_modal_trigger_type_ending_warning": "Nenhum final encontrado na pesquisa!", + "follow_ups_modal_trigger_type_response": "Respondente completa 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_ai_description": "Descreva sua pesquisa e deixe a Formbricks AI criar a pesquisa pra você", + "formbricks_ai_generate": "gerar", + "formbricks_ai_prompt_placeholder": "Insira as informações da pesquisa (ex.: tópicos principais a serem abordados)", + "formbricks_sdk_is_not_connected": "O SDK do Formbricks não está conectado", + "four_points": "4 pontos", + "heading": "Título", + "hidden_field_added_successfully": "Campo oculto adicionado com sucesso", + "hide_advanced_settings": "Ocultar configurações avançadas", + "hide_back_button": "Ocultar botão 'Voltar'", + "hide_back_button_description": "Não exibir o botão de voltar na pesquisa", + "hide_logo": "Esconder logo", + "hide_progress_bar": "Esconder barra de progresso", + "hide_the_logo_in_this_specific_survey": "Esconder o logo nessa pesquisa específica", + "hostname": "nome do host", + "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}", + "how_it_works": "Como funciona", + "if_you_need_more_please": "Se você precisar de mais, por favor", + "if_you_really_want_that_answer_ask_until_you_get_it": "Se você realmente quer essa resposta, pergunte até conseguir.", + "ignore_waiting_time_between_surveys": "Ignorar tempo de espera entre pesquisas", + "image": "imagem", + "includes_all_of": "Inclui tudo de", + "includes_one_of": "Inclui um de", + "initial_value": "Valor inicial", + "inner_text": "Texto Interno", + "input_border_color": "Cor da borda de entrada", + "input_color": "Cor de entrada", + "invalid_targeting": "Segmentação inválida: Por favor, verifique os filtros do seu público", + "invalid_video_url_warning": "Por favor, insira uma URL válida do YouTube, Vimeo ou Loom. No momento, não suportamos outros provedores de vídeo.", + "invalid_youtube_url": "URL do YouTube inválida", + "is_accepted": "Está aceito", + "is_after": "é depois", + "is_before": "é antes", + "is_booked": "Tá reservado", + "is_clicked": "É clicado", + "is_completely_submitted": "Está completamente submetido", + "is_not_set": "Não está definido", + "is_partially_submitted": "Parcialmente enviado", + "is_set": "Está definido", + "is_skipped": "é pulado", + "is_submitted": "é submetido", + "jump_to_question": "Pular para a pergunta", + "keep_current_order": "Manter pedido atual", + "keep_showing_while_conditions_match": "Continue mostrando enquanto as condições corresponderem", + "key": "chave", + "last_name": "Sobrenome", + "let_people_upload_up_to_25_files_at_the_same_time": "Deixe as pessoas fazerem upload de até 25 arquivos ao mesmo tempo.", + "limit_file_types": "Limitar tipos de arquivos", + "limit_the_maximum_file_size": "Limitar o tamanho máximo do arquivo", + "limit_upload_file_size_to": "Limitar tamanho do arquivo de upload para", + "link_survey_description": "Compartilhe um link para a página da pesquisa ou incorpore-a em uma página da web ou e-mail.", + "link_used_message": "Link Usado", + "load_segment": "segmento de carga", + "logic_error_warning": "Mudar vai causar erros de lógica", + "logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta", + "long_answer": "resposta longa", + "lower_label": "Etiqueta Inferior", + "manage_languages": "Gerenciar Idiomas", + "max_file_size": "Tamanho máximo do arquivo", + "max_file_size_limit_is": "Tamanho máximo do arquivo é", + "multiply": "Multiplicar *", + "needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com", + "next_button_label": "Próximo", + "next_question": "próxima pergunta", + "no_hidden_fields_yet_add_first_one_below": "Ainda não há campos ocultos. Adicione o primeiro abaixo.", + "no_images_found_for": "Nenhuma imagem encontrada para ''{query}\"", + "no_languages_found_add_first_one_to_get_started": "Nenhum idioma encontrado. Adicione o primeiro para começar.", + "no_variables_yet_add_first_one_below": "Ainda não há variáveis. Adicione a primeira abaixo.", + "number": "Número", + "once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations": "Depois de definido, o idioma padrão desta pesquisa só pode ser alterado desativando a opção de vários idiomas e excluindo todas as traduções.", + "only_display_the_survey_to_a_subset_of_the_users": "Mostrar a pesquisa apenas para um subconjunto dos usuários", + "only_lower_case_letters_numbers_and_underscores_are_allowed": "só letras minúsculas, números e underscores são permitidos.", + "only_people_who_match_your_targeting_can_be_surveyed": "Somente pessoas que correspondem ao seu público-alvo podem ser pesquisadas.", + "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", + "override_theme_with_individual_styles_for_this_survey": "Substitua o tema com estilos individuais para essa pesquisa.", + "overwrite_placement": "Substituir posicionamento", + "overwrite_the_global_placement_of_the_survey": "Substituir a posição global da pesquisa", + "overwrites_waiting_period_between_surveys_to_x_days": "Substitui o período de espera entre as pesquisas para {days} dia(s).", + "pick_a_background_from_our_library_or_upload_your_own": "Escolha um fundo da nossa biblioteca ou faça upload do seu próprio.", + "picture_idx": "Imagem {idx}", + "pin_can_only_contain_numbers": "O PIN só pode conter números.", + "pin_must_be_a_four_digit_number": "O PIN deve ser um número de quatro dígitos.", + "please_enter_a_file_extension": "Por favor, insira uma extensão de arquivo.", + "please_set_a_survey_trigger": "Por favor, configure um gatilho para a pesquisa", + "please_specify": "Por favor, especifique", + "prevent_double_submission": "Evitar envio duplicado", + "prevent_double_submission_description": "Permitir apenas 1 resposta por endereço de email", + "protect_survey_with_pin": "Proteger pesquisa com um PIN", + "protect_survey_with_pin_description": "Somente usuários que têm o PIN podem acessar a pesquisa.", + "publish": "Publicar", + "question": "Pergunta", + "question_color": "Cor da pergunta", + "question_deleted": "Pergunta deletada.", + "question_duplicated": "Pergunta duplicada.", + "question_id_updated": "ID da pergunta atualizado", + "question_used_in_logic": "Essa pergunta é usada na lógica da pergunta {questionIndex}.", + "randomize_all": "Randomizar tudo", + "randomize_all_except_last": "Randomizar tudo, exceto o último", + "range": "alcance", + "recontact_options": "Opções de Recontato", + "redirect_thank_you_card": "Redirecionar cartão de agradecimento", + "redirect_to_url": "Redirecionar para URL", + "redirect_to_url_not_available_on_free_plan": "Redirecionar para URL não está disponível no plano gratuito", + "release_survey_on_date": "Lançar pesquisa na data", + "remove_description": "Remover descrição", + "remove_translations": "Remover traduções", + "require_answer": "Preciso de Resposta", + "required": "Obrigatório", + "reset_to_theme_styles": "Redefinir para estilos do tema", + "reset_to_theme_styles_main_text": "Tem certeza de que quer redefinir o estilo para o tema padrão? Isso vai remover todas as personalizações.", + "response_limit_can_t_be_set_to_0": "Limite de resposta não pode ser 0", + "response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).", + "response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.", + "response_options": "Opções de Resposta", + "roundness": "redondeza", + "rows": "linhas", + "save_and_close": "Salvar e Fechar", + "scale": "escala", + "search_for_images": "Buscar imagens", + "seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos após acionar, a pesquisa será encerrada se não houver resposta", + "seconds_before_showing_the_survey": "segundos antes de mostrar a pesquisa.", + "select_or_type_value": "Selecionar ou digitar valor", + "select_ordering": "Selecionar pedido", + "select_saved_action": "Selecionar ação salva", + "select_type": "Selecionar tipo", + "send_survey_to_audience_who_match": "Enviar pesquisa para o público que corresponde...", + "send_your_respondents_to_a_page_of_your_choice": "Envie seus respondentes para uma página de sua escolha.", + "set_the_global_placement_in_the_look_feel_settings": "Defina o posicionamento global nas configurações de Aparência.", + "seven_points": "7 pontos", + "show_advanced_settings": "Mostrar configurações avançadas", + "show_button": "Mostrar Botão", + "show_language_switch": "Mostrar troca de idioma", + "show_multiple_times": "Mostrar várias vezes", + "show_only_once": "Mostrar só uma vez", + "show_survey_maximum_of": "Mostrar no máximo", + "show_survey_to_users": "Mostrar pesquisa para % dos usuários", + "show_to_x_percentage_of_targeted_users": "Mostrar para {percentage}% dos usuários segmentados", + "simple": "Simples", + "single_use_survey_links": "Links de pesquisa de uso único", + "single_use_survey_links_description": "Permitir apenas 1 resposta por link da pesquisa.", + "skip_button_label": "Botão de Pular", + "smiley": "Sorridente", + "star": "Estrela", + "starts_with": "Começa com", + "state": "Estado", + "straight": "hétero", + "style_the_question_texts_descriptions_and_input_fields": "Estilize os textos das perguntas, descrições e campos de entrada.", + "style_the_survey_card": "Estilize o cartão da pesquisa.", + "styling_set_to_theme_styles": "Estilo definido para os estilos do tema", + "subheading": "Subtítulo", + "subtract": "Subtrair -", + "suggest_colors": "Sugerir cores", + "survey_already_answered_heading": "A pesquisa já foi respondida.", + "survey_already_answered_subheading": "Você só pode usar esse link uma vez.", + "survey_completed_heading": "Pesquisa Concluída", + "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_trigger": "Gatilho de Pesquisa", + "switch_multi_lanugage_on_to_get_started": "Ative o modo multilíngue para começar \uD83D\uDC49", + "targeted": "direcionado", + "ten_points": "10 pontos", + "the_survey_will_be_shown_multiple_times_until_they_respond": "A pesquisa vai ser mostrada várias vezes até eles responderem", + "the_survey_will_be_shown_once_even_if_person_doesnt_respond": "A pesquisa será mostrada uma vez, mesmo se a pessoa não responder.", + "then": "Então", + "this_action_will_remove_all_the_translations_from_this_survey": "Essa ação vai remover todas as traduções dessa pesquisa.", + "this_extension_is_already_added": "Essa extensão já foi adicionada.", + "this_file_type_is_not_supported": "Esse tipo de arquivo não é suportado.", + "this_setting_overwrites_your": "Essa configuração sobrescreve seu", + "three_points": "3 pontos", + "times": "times", + "to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todas as pesquisas, você pode", + "trigger_survey_when_one_of_the_actions_is_fired": "Disparar pesquisa quando uma das ações for executada...", + "try_lollipop_or_mountain": "Tenta 'pirulito' ou 'montanha'...", + "type_field_id": "Digite o id do campo", + "unlock_targeting_description": "Direcione grupos específicos de usuários com base em atributos ou informações do dispositivo", + "unlock_targeting_title": "Desbloqueie o direcionamento com um plano superior", + "unsaved_changes_warning": "Você tem alterações não salvas na sua pesquisa. Quer salvar antes de sair?", + "until_they_submit_a_response": "Até eles enviarem uma resposta", + "upgrade_notice_description": "Crie pesquisas multilíngues e desbloqueie muitas outras funcionalidades", + "upgrade_notice_title": "Desbloqueie pesquisas multilíngues com um plano superior", + "upload": "Enviar", + "upload_at_least_2_images": "Faz o upload de pelo menos 2 imagens", + "upper_label": "Etiqueta Superior", + "url_encryption": "Criptografia de URL", + "url_filters": "Filtros de URL", + "url_not_supported": "URL não suportada", + "use_with_caution": "Use com cuidado", + "variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} está sendo usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.", + "variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.", + "variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.", + "verify_email_before_submission": "Verifique o e-mail antes de enviar", + "verify_email_before_submission_description": "Deixe só quem tem um email real responder.", + "wait": "Espera", + "wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Espera alguns segundos depois do gatilho antes de mostrar a pesquisa", + "waiting_period": "período de espera", + "welcome_message": "Mensagem de boas-vindas", + "when": "Quando", + "when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Quando as condições forem atendidas, o tempo de espera será ignorado e a pesquisa será exibida.", + "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_project_to_work_with_translations": "Você precisa ter dois ou mais idiomas configurados no seu projeto 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", + "zip": "Fecho éclair" + }, + "error_deleting_survey": "Ocorreu um erro ao deletar a pesquisa", + "failed_to_copy_link_to_results": "Falha ao copiar link dos resultados", + "failed_to_copy_url": "Falha ao copiar URL: não está em um ambiente de navegador.", + "new_single_use_link_generated": "Novo link de uso único gerado", + "new_survey": "Nova Pesquisa", + "no_surveys_created_yet": "Ainda não foram criadas pesquisas", + "open_options": "Abre opções", + "preview_survey_in_a_new_tab": "Visualizar pesquisa em uma nova aba", + "read_only_user_not_allowed_to_create_survey_warning": "Como um usuário somente leitura, você não tem permissão para criar pesquisas. Peça a um usuário com acesso de gravação para criar uma pesquisa ou a um gerente para atualizar sua função.", + "relevance": "Relevância", + "responses": { + "address_line_1": "Endereço Linha 1", + "address_line_2": "Complemento", + "an_error_occurred_creating_a_new_note": "Deu erro ao criar uma nova nota", + "an_error_occurred_deleting_the_tag": "Ocorreu um erro ao deletar a tag", + "an_error_occurred_resolving_a_note": "Ocorreu um erro ao resolver uma nota", + "an_error_occurred_updating_a_note": "Ocorreu um erro ao atualizar uma nota", + "browser": "navegador", + "city": "Cidade", + "company": "empresa", + "completed": "Concluído ✅", + "country": "País", + "device": "dispositivo", + "device_info": "Informações do dispositivo", + "email": "Email", + "first_name": "Primeiro Nome", + "how_to_identify_users": "Como identificar usuários", + "last_name": "Sobrenome", + "not_completed": "Não Concluído ⏳", + "os": "sistema operacional", + "person_attributes": "Atributos da pessoa", + "phone": "Celular", + "resolve": "resolver", + "respondent_skipped_questions": "Respondente pulou essas perguntas.", + "response_deleted_successfully": "Resposta deletada com sucesso.", + "single_use_id": "ID de Uso Único", + "source": "fonte", + "state_region": "Estado / Região", + "survey_closed": "Pesquisa encerrada", + "tag_already_exists": "Tag já existe", + "this_response_is_in_progress": "Essa resposta está em andamento.", + "zip_post_code": "CEP / Código postal" + }, + "results_unpublished_successfully": "Resultados não publicados com sucesso.", + "search_by_survey_name": "Buscar pelo nome da pesquisa", + "summary": { + "added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ", + "added_filter_for_responses_where_answer_to_question_is_skipped": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} foi pulada", + "all_responses_csv": "Todas as respostas (CSV)", + "all_responses_excel": "Todas as respostas (Excel)", + "all_time": "Todo o tempo", + "almost_there": "Quase lá! Instale o widget para começar a receber respostas.", + "average": "média", + "completed": "Concluído", + "completed_tooltip": "Número de vezes que a pesquisa foi completada.", + "configure_alerts": "Configurar alertas", + "congrats": "Parabéns! Sua pesquisa está no ar.", + "connect_your_website_or_app_with_formbricks_to_get_started": "Conecte seu site ou app com o Formbricks para começar.", + "copy_link_to_public_results": "Copiar link para resultados públicos", + "create_single_use_links": "Crie links de uso único", + "create_single_use_links_description": "Aceite apenas uma submissão por link. Aqui está como.", + "custom_range": "Intervalo personalizado...", + "data_prefilling": "preenchimento automático de dados", + "data_prefilling_description": "Quer preencher alguns campos da pesquisa? Aqui está como fazer.", + "define_when_and_where_the_survey_should_pop_up": "Defina quando e onde a pesquisa deve aparecer", + "drop_offs": "Pontos de Entrega", + "drop_offs_tooltip": "Número de vezes que a pesquisa foi iniciada mas não concluída.", + "dynamic_popup": "Dinâmico (Pop-up)", + "email_sent": "Email enviado!", + "embed_code_copied_to_clipboard": "Código incorporado copiado para a área de transferência!", + "embed_in_an_email": "Incorporar em um e-mail", + "embed_in_app": "Integrar no app", + "embed_mode": "Modo Embutido", + "embed_mode_description": "Incorpore sua pesquisa com um design minimalista, sem preenchimento e fundo.", + "embed_on_website": "Incorporar no site", + "embed_pop_up_survey_title": "Como incorporar uma pesquisa pop-up no seu site", + "embed_survey": "Incorporar pesquisa", + "enable_ai_insights_banner_button": "Ativar insights", + "enable_ai_insights_banner_description": "Você pode ativar o novo recurso de insights para a pesquisa e obter insights baseados em IA para suas respostas em texto aberto.", + "enable_ai_insights_banner_success": "Gerando insights para essa pesquisa. Por favor, volte em alguns minutos.", + "enable_ai_insights_banner_title": "Pronto pra testar as ideias da IA?", + "enable_ai_insights_banner_tooltip": "Por favor, entre em contato conosco pelo e-mail hola@formbricks.com para gerar insights para esta pesquisa", + "failed_to_copy_link": "Falha ao copiar link", + "filter_added_successfully": "Filtro adicionado com sucesso", + "filter_updated_successfully": "Filtro atualizado com sucesso", + "filtered_responses_csv": "Respostas filtradas (CSV)", + "filtered_responses_excel": "Respostas filtradas (Excel)", + "formbricks_email_survey_preview": "Prévia da Pesquisa por E-mail do Formbricks", + "go_to_setup_checklist": "Vai para a Lista de Configuração \uD83D\uDC49", + "hide_embed_code": "Esconder código de incorporação", + "how_to_create_a_panel": "Como criar um painel", + "how_to_create_a_panel_step_1": "Passo 1: Crie uma conta no Prolific", + "how_to_create_a_panel_step_1_description": "Cria uma conta no Prolific e verifica teu e-mail.", + "how_to_create_a_panel_step_2": "Passo 2: Crie um estudo", + "how_to_create_a_panel_step_2_description": "Na Prolific, você cria um novo estudo onde pode escolher seu público preferido com base em centenas de características.", + "how_to_create_a_panel_step_3": "Passo 3: Conecte sua pesquisa", + "how_to_create_a_panel_step_3_description": "Configure campos ocultos na sua pesquisa do Formbricks para rastrear qual participante forneceu qual resposta.", + "how_to_create_a_panel_step_4": "Passo 4: Lançar seu estudo", + "how_to_create_a_panel_step_4_description": "Depois que tudo estiver configurado, você pode iniciar seu estudo. Em algumas horas, você vai receber as primeiras respostas.", + "impressions": "Impressões", + "impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.", + "includes_all": "Inclui tudo", + "includes_either": "Inclui ou", + "insights_disabled": "Insights desativados", + "install_widget": "Instalar Widget do Formbricks", + "is_equal_to": "É igual a", + "is_less_than": "É menor que", + "last_30_days": "Últimos 30 dias", + "last_6_months": "Últimos 6 meses", + "last_7_days": "Últimos 7 dias", + "last_month": "Último mês", + "last_quarter": "Último trimestre", + "last_year": "Último ano", + "link_to_public_results_copied": "Link pros resultados públicos copiado", + "make_sure_the_survey_type_is_set_to": "Certifique-se de que o tipo de pesquisa esteja definido como", + "mobile_app": "app de celular", + "no_response_matches_filter": "Nenhuma resposta corresponde ao seu filtro", + "only_completed": "Somente concluído", + "other_values_found": "Outros valores encontrados", + "overall": "No geral", + "publish_to_web": "Publicar na web", + "publish_to_web_warning": "Você está prestes a divulgar esses resultados da pesquisa para o público.", + "publish_to_web_warning_description": "Os resultados da sua pesquisa serão públicos. Qualquer pessoa fora da sua organização pode acessá-los se tiver o link.", + "quickstart_mobile_apps": "Início rápido: Aplicativos móveis", + "quickstart_mobile_apps_description": "Para começar com pesquisas em aplicativos móveis, por favor, siga o guia de início rápido:", + "quickstart_web_apps": "Início rápido: Aplicativos web", + "quickstart_web_apps_description": "Por favor, siga o guia de início rápido para começar:", + "results_are_public": "Os resultados são públicos", + "send_preview": "Enviar prévia", + "send_to_panel": "Enviar para o painel", + "setup_instructions": "Instruções de configuração", + "setup_integrations": "Configurar integrações", + "share_results": "Compartilhar resultados", + "share_the_link": "Compartilha o link", + "share_the_link_to_get_responses": "Compartilha o link pra receber respostas", + "show_all_responses_that_match": "Mostrar todas as respostas que correspondem", + "show_all_responses_where": "Mostre todas as respostas onde...", + "single_use_links": "Links de uso único", + "source_tracking": "rastreamento de origem", + "source_tracking_description": "Rastreie a origem de forma compatível com GDPR e CCPA sem ferramentas extras.", + "starts": "começa", + "starts_tooltip": "Número de vezes que a pesquisa foi iniciada.", + "static_iframe": "Estático (iframe)", + "survey_results_are_public": "Os resultados da sua pesquisa são públicos!", + "survey_results_are_shared_with_anyone_who_has_the_link": "Os resultados da sua pesquisa são compartilhados com quem tiver o link. Os resultados não serão indexados por motores de busca.", + "this_month": "Este mês", + "this_quarter": "Este trimestre", + "this_year": "Este ano", + "time_to_complete": "Tempo para Concluir", + "to_connect_your_website_with_formbricks": "conectar seu site com o Formbricks", + "ttc_tooltip": "Tempo médio para completar a pesquisa.", + "unknown_question_type": "Tipo de pergunta desconhecido", + "unpublish_from_web": "Despublicar da web", + "unsupported_video_tag_warning": "Seu navegador não suporta a tag de vídeo.", + "view_embed_code": "Ver código incorporado", + "view_embed_code_for_email": "Ver código incorporado para e-mail", + "view_site": "Ver site", + "waiting_for_response": "Aguardando uma resposta \uD83E\uDDD8‍♂️", + "web_app": "aplicativo web", + "what_is_a_panel": "O que é um painel?", + "what_is_a_panel_answer": "Um painel é um grupo de participantes selecionados com base em características como idade, profissão, gênero, etc.", + "what_is_prolific": "O que é Prolific?", + "what_is_prolific_answer": "Estamos fazendo parceria com a Prolific pra te dar acesso a um grupo de mais de 200.000 participantes verificados.", + "whats_next": "E agora?", + "when_do_i_need_it": "Quando eu preciso disso?", + "when_do_i_need_it_answer": "Se você não tem acesso a pessoas suficientes que correspondam ao seu público-alvo, faz sentido pagar por acesso a um painel.", + "you_can_do_a_lot_more_with_links_surveys": "Você pode fazer muito mais com pesquisas de links \uD83D\uDCA1", + "your_survey_is_public": "Sua pesquisa é pública", + "youre_not_plugged_in_yet": "Você ainda não tá conectado!" + }, + "survey_deleted_successfully": "Pesquisa deletada com sucesso!", + "survey_duplicated_successfully": "Pesquisa duplicada com sucesso.", + "survey_duplication_error": "Falha ao duplicar a pesquisa.", + "survey_status_tooltip": "Para atualizar o status da pesquisa, atualize o cronograma e feche a configuração nas opções de resposta da pesquisa.", + "templates": { + "all_channels": "Todos os canais", + "all_industries": "Todas as indústrias", + "all_roles": "Todos os papéis", + "create_a_new_survey": "Cria uma nova pesquisa", + "multiple_industries": "várias indústrias", + "use_this_template": "Use esse modelo", + "uses_branching_logic": "Essa pesquisa usa lógica de ramificação." + } + }, + "xm-templates": { + "ces": "CES", + "ces_description": "Aproveite cada ponto de contato para entender a facilidade de interação do cliente.", + "csat": "Satisfação do Cliente", + "csat_description": "Implementar as melhores práticas para medir a satisfação do cliente.", + "enps": "eNPS", + "enps_description": "Feedback universal pra entender o engajamento e a satisfação dos funcionários.", + "five_star_rating": "Avaliação de 5 Estrelas", + "five_star_rating_description": "Solução universal de feedback para medir a satisfação geral.", + "headline": "Que tipo de feedback você gostaria de receber?", + "nps": "NPS", + "nps_description": "Implementar práticas recomendadas comprovadas para entender POR QUE as pessoas compram.", + "smileys": "Emoticons", + "smileys_description": "Use indicadores visuais pra captar feedback em todos os pontos de contato com o cliente." + } + }, + "organizations": { + "landing": { + "no_projects_warning_subtitle": "Entre em contato com o proprietário da sua organização para obter acesso aos projetos. Ou crie uma organização própria para começar.", + "no_projects_warning_title": "Sua conta ainda não tem acesso a nenhum projeto." + }, + "projects": { + "new": { + "channel": { + "channel_select_subtitle": "Compartilha um link ou mostra sua pesquisa em apps ou sites.", + "channel_select_title": "Que tipo de pesquisa você quer?", + "in_product_surveys": "Pesquisas em produto", + "in_product_surveys_description": "Fazer pesquisas micro-direcionadas em apps.", + "link_and_email_surveys": "Pesquisas por link e email", + "link_and_email_surveys_description": "Alcance pessoas em qualquer lugar online." + }, + "mode": { + "formbricks_cx": "Formbricks CX", + "formbricks_cx_description": "Pesquisas e relatórios pra entender o que seus clientes precisam.", + "formbricks_surveys": "Pesquisas Formbricks", + "formbricks_surveys_description": "Plataforma de pesquisa multiuso para pesquisas na web, app e email.", + "what_are_you_here_for": "O que você tá fazendo aqui?" + }, + "settings": { + "brand_color": "cor da marca", + "brand_color_description": "Combine a cor principal das pesquisas com a sua marca.", + "create_new_team": "Criar nova equipe", + "project_creation_failed": "Falha ao criar o projeto", + "project_name": "Nome do produto", + "project_name_description": "Como se chama o seu produto?", + "project_settings_subtitle": "Quando as pessoas reconhecem sua marca, é muito mais provável que comecem e concluam respostas.", + "project_settings_title": "Deixe os respondentes saberem que é você", + "team_description": "Quem pode acessar este projeto?" + } + } + } + }, + "s": { + "check_inbox_or_spam": "Por favor, dá uma olhada na sua pasta de spam se você não encontrar o e-mail na sua caixa de entrada.", + "completed": "Essa pesquisa gratuita e de código aberto foi encerrada.", + "create_your_own": "Crie o seu próprio", + "enter_pin": "Essa pesquisa está protegida. Insira o PIN abaixo", + "just_curious": "Só curioso?", + "link_invalid": "Essa pesquisa só pode ser respondida por convite.", + "paused": "Essa pesquisa gratuita e de código aberto está temporariamente pausada.", + "please_try_again_with_the_original_link": "Por favor, tente novamente com o link original", + "preview_survey_questions": "Visualizar perguntas da pesquisa.", + "question_preview": "Prévia da Pergunta", + "response_already_received": "Já recebemos uma resposta para este endereço de email.", + "response_submitted": "Já existe uma resposta vinculada a esta pesquisa e contato", + "survey_already_answered_heading": "A pesquisa já foi respondida.", + "survey_already_answered_subheading": "Você só pode usar esse link uma vez.", + "survey_sent_to": "Pesquisa enviada para {email}", + "this_looks_fishy": "Isso parece suspeito.", + "verify_email": "Verifica o e-mail.", + "verify_email_before_submission": "Verifique seu e-mail para responder", + "verify_email_before_submission_button": "Verificar", + "verify_email_before_submission_description": "Para responder a esta pesquisa, confirme seu e-mail", + "want_to_respond": "Quer responder?" + }, + "setup": { + "intro": { + "get_started": "Começar", + "made_with_love_in_kiel": "Feito com \uD83E\uDD0D em Alemanha", + "paragraph_1": "Formbricks é uma suíte de gerenciamento de experiência construída na plataforma de pesquisa open source que mais cresce no mundo.", + "paragraph_2": "Faça pesquisas direcionadas em sites, apps ou em qualquer lugar online. Recolha insights valiosos para criar experiências irresistíveis para clientes, usuários e funcionários.", + "paragraph_3": "Estamos comprometidos com o mais alto nível de privacidade de dados. Hospede você mesmo para manter controle total sobre seus dados. Sempre", + "welcome_to_formbricks": "Bem-vindo ao Formbricks!" + }, + "invite": { + "add_another_member": "Adicionar mais um membro", + "continue": "Continuar", + "failed_to_invite": "Falha ao convidar", + "invitation_sent_to": "Convite enviado para", + "invite_your_organization_members": "Convide os membros da sua organização", + "life_s_no_fun_alone": "A vida não tem graça sozinho.", + "skip": "Pular", + "smtp_not_configured": "SMTP não configurado", + "smtp_not_configured_description": "Convites não podem ser enviados agora porque o serviço de e-mail não está configurado. Você pode copiar o link do convite nas configurações da organização mais tarde." + }, + "organization": { + "create": { + "continue": "Continuar", + "delete_account": "Excluir conta", + "delete_account_description": "Se você quiser deletar sua conta, pode fazer isso clicando no botão abaixo.", + "description": "Faça do seu jeito.", + "no_membership_found": "Não encontramos nenhuma assinatura!", + "no_membership_found_description": "Você não é membro de nenhuma organização no momento. Se você acha que isso é um erro, por favor, entre em contato com o dono da organização.", + "title": "Configurar sua organização" + } + }, + "signup": { + "create_administrator": "Criar Administrador", + "this_user_has_all_the_power": "Esse usuário tem todo o poder." + } + }, + "share": { + "back_to_home": "Voltar pra casa", + "page_not_found": "Página não encontrada", + "page_not_found_description": "Desculpa, não conseguimos encontrar as respostas com o ID que você está procurando." + }, + "templates": { + "address": "endereço", + "address_description": "Pede um endereço pra correspondência", + "alignment_and_engagement_survey_description": "Avalie o alinhamento dos funcionários com a visão, estratégia e comunicação da empresa, bem como a colaboração em equipe.", + "alignment_and_engagement_survey_name": "Alinhamento e Engajamento com a Visão da Empresa", + "alignment_and_engagement_survey_question_1_headline": "Eu entendo como meu papel contribui para a estratégia geral da empresa.", + "alignment_and_engagement_survey_question_1_lower_label": "Nenhum entendimento", + "alignment_and_engagement_survey_question_1_upper_label": "Entendimento completo", + "alignment_and_engagement_survey_question_2_headline": "Sinto que meus valores estão alinhados com a missão e cultura da empresa.", + "alignment_and_engagement_survey_question_2_lower_label": "Nenhum alinhamento", + "alignment_and_engagement_survey_question_2_upper_label": "Totalmente alinhado", + "alignment_and_engagement_survey_question_3_headline": "Eu trabalho efetivamente com minha equipe para atingir nossos objetivos.", + "alignment_and_engagement_survey_question_3_lower_label": "Colaboração ruim", + "alignment_and_engagement_survey_question_3_upper_label": "Colaboração excelente", + "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", + "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", + "build_product_roadmap_name_with_project_name": "Entrada do Roadmap do $[projectName]", + "build_product_roadmap_question_1_headline": "Quão satisfeito(a) você está com os recursos e funcionalidades do $[projectName]?", + "build_product_roadmap_question_1_lower_label": "Nada satisfeito", + "build_product_roadmap_question_1_upper_label": "Super satisfeito", + "build_product_roadmap_question_2_headline": "Qual é a ÚNICA mudança que poderíamos fazer para melhorar mais a sua experiência com o $[projectName]?", + "build_product_roadmap_question_2_placeholder": "Digite sua resposta aqui...", + "card_abandonment_survey": "Pesquisa de Abandono de Carrinho", + "card_abandonment_survey_description": "Entenda os motivos por trás do abandono de carrinho na sua loja online.", + "card_abandonment_survey_question_1_button_label": "Claro!", + "card_abandonment_survey_question_1_dismiss_button_label": "Não, valeu.", + "card_abandonment_survey_question_1_headline": "Você tem 2 minutos para nos ajudar a melhorar?", + "card_abandonment_survey_question_1_html": "Percebemos que você deixou alguns itens no seu carrinho. Adoraríamos entender o motivo.", + "card_abandonment_survey_question_2_choice_1": "Custos de frete altos", + "card_abandonment_survey_question_2_choice_2": "Encontrei um preço melhor em outro lugar", + "card_abandonment_survey_question_2_choice_3": "Só dando uma olhada", + "card_abandonment_survey_question_2_choice_4": "Decidi não comprar", + "card_abandonment_survey_question_2_choice_5": "Problemas com pagamento", + "card_abandonment_survey_question_2_choice_6": "outro", + "card_abandonment_survey_question_2_headline": "Qual foi o principal motivo pra você não ter finalizado a compra?", + "card_abandonment_survey_question_2_subheader": "Por favor, escolha uma das opções a seguir:", + "card_abandonment_survey_question_3_headline": "Por favor, explique o motivo de não ter concluído a compra:", + "card_abandonment_survey_question_4_headline": "Como você avaliaria sua experiência geral de compra?", + "card_abandonment_survey_question_4_lower_label": "Muito insatisfeito", + "card_abandonment_survey_question_4_upper_label": "Muito satisfeito", + "card_abandonment_survey_question_5_choice_1": "Reduzir os custos de envio", + "card_abandonment_survey_question_5_choice_2": "Descontos ou promoções", + "card_abandonment_survey_question_5_choice_3": "Mais opções de pagamento", + "card_abandonment_survey_question_5_choice_4": "Melhores descrições de produtos", + "card_abandonment_survey_question_5_choice_5": "Navegação do site melhorada", + "card_abandonment_survey_question_5_choice_6": "outro", + "card_abandonment_survey_question_5_headline": "O que te incentivaria a finalizar sua compra no futuro?", + "card_abandonment_survey_question_5_subheader": "Por favor, selecione todas as opções que se aplicam:", + "card_abandonment_survey_question_6_headline": "Você gostaria de receber um código de desconto por e-mail?", + "card_abandonment_survey_question_6_label": "Sim, por favor entre em contato.", + "card_abandonment_survey_question_7_headline": "Por favor, compartilha seu e-mail:", + "card_abandonment_survey_question_8_headline": "Algum comentário ou sugestão a mais?", + "career_development_survey_description": "Avalie a satisfação dos funcionários com o crescimento na carreira e oportunidades de desenvolvimento.", + "career_development_survey_name": "Pesquisa de Desenvolvimento de Carreira", + "career_development_survey_question_1_headline": "Estou satisfeito(a) com as oportunidades de desenvolvimento pessoal e profissional no $[projectName].", + "career_development_survey_question_1_lower_label": "Discordo totalmente", + "career_development_survey_question_1_upper_label": "Concordo totalmente", + "career_development_survey_question_2_headline": "Estou satisfeito(a) com as oportunidades de carreira disponíveis para mim no $[projectName].", + "career_development_survey_question_2_lower_label": "Discordo totalmente", + "career_development_survey_question_2_upper_label": "Concordo totalmente", + "career_development_survey_question_3_headline": "Estou satisfeito(a) com os treinamentos profissionais oferecidos pela minha organização.", + "career_development_survey_question_3_lower_label": "Discordo totalmente", + "career_development_survey_question_3_upper_label": "Concordo totalmente", + "career_development_survey_question_4_headline": "Estou satisfeito(a) com os investimentos que minha organização faz em treinamento e desenvolvimento.", + "career_development_survey_question_4_lower_label": "Discordo totalmente", + "career_development_survey_question_4_upper_label": "Concordo totalmente", + "career_development_survey_question_5_choice_1": "Desenvolvimento de Produto", + "career_development_survey_question_5_choice_2": "Marketing", + "career_development_survey_question_5_choice_3": "Relações Públicas", + "career_development_survey_question_5_choice_4": "Contabilidade", + "career_development_survey_question_5_choice_5": "Operações", + "career_development_survey_question_5_choice_6": "Outro", + "career_development_survey_question_5_headline": "Em qual área você trabalha?", + "career_development_survey_question_5_subheader": "Por favor, escolha uma das opções a seguir", + "career_development_survey_question_6_choice_1": "Colaborador Individual", + "career_development_survey_question_6_choice_2": "Gerente", + "career_development_survey_question_6_choice_3": "Gerente Sênior", + "career_development_survey_question_6_choice_4": "Vice-presidente", + "career_development_survey_question_6_choice_5": "Diretoria Executiva", + "career_development_survey_question_6_choice_6": "Outro", + "career_development_survey_question_6_headline": "O que melhor descreve seu cargo atual?", + "career_development_survey_question_6_subheader": "Por favor, escolha uma das opções a seguir", + "cess_survey_name": "Pesquisa CES", + "cess_survey_question_1_headline": "$[projectName] facilita pra mim [ADICIONAR OBJETIVO]", + "cess_survey_question_1_lower_label": "Discordar veementemente", + "cess_survey_question_1_upper_label": "Concordo totalmente", + "cess_survey_question_2_headline": "Valeu! Como a gente pode facilitar pra você [ADICIONAR OBJETIVO]?", + "cess_survey_question_2_placeholder": "Digite sua resposta aqui...", + "changing_subscription_experience_description": "Descubra o que passa pela cabeça das pessoas quando mudam suas assinaturas.", + "changing_subscription_experience_name": "Mudando a Experiência de Assinatura", + "changing_subscription_experience_question_1_choice_1": "Extremamente difícil", + "changing_subscription_experience_question_1_choice_2": "Demorou, mas consegui", + "changing_subscription_experience_question_1_choice_3": "Foi de boa", + "changing_subscription_experience_question_1_choice_4": "Molezinha", + "changing_subscription_experience_question_1_choice_5": "Muito fácil, adorei!", + "changing_subscription_experience_question_1_headline": "Quão fácil foi mudar seu plano?", + "changing_subscription_experience_question_2_choice_1": "Sim, muito claro.", + "changing_subscription_experience_question_2_choice_2": "No começo eu fiquei confuso, mas achei o que precisava.", + "changing_subscription_experience_question_2_choice_3": "Bastante complicado.", + "changing_subscription_experience_question_2_headline": "A informação de preços é fácil de entender?", + "churn_survey": "Pesquisa de Cancelamento", + "churn_survey_description": "Descubra por que as pessoas cancelam suas assinaturas. Essas informações são ouro puro!", + "churn_survey_question_1_choice_1": "Difícil de usar", + "churn_survey_question_1_choice_2": "Tá caro demais", + "churn_survey_question_1_choice_3": "Estou sentindo falta de recursos", + "churn_survey_question_1_choice_4": "Atendimento ao cliente ruim", + "churn_survey_question_1_choice_5": "Eu só não precisava mais disso", + "churn_survey_question_1_headline": "Por que você cancelou sua assinatura?", + "churn_survey_question_1_subheader": "Lamentamos ver você partir. Ajude-nos a melhorar:", + "churn_survey_question_2_button_label": "Enviar", + "churn_survey_question_2_headline": "O que teria feito o $[projectName] mais fácil de usar?", + "churn_survey_question_3_button_label": "Ganhe 30% de desconto", + "churn_survey_question_3_dismiss_button_label": "Pular", + "churn_survey_question_3_headline": "Ganhe 30% de desconto pelo próximo ano!", + "churn_survey_question_3_html": "A gente adoraria te manter como cliente. Feliz em oferecer um desconto de 30% pro próximo ano.", + "churn_survey_question_4_headline": "Quais recursos você está sentindo falta?", + "churn_survey_question_5_button_label": "Enviar e-mail para o CEO", + "churn_survey_question_5_dismiss_button_label": "Pular", + "churn_survey_question_5_headline": "Que pena ouvir isso \uD83D\uDE14 Fala direto com nosso CEO!", + "churn_survey_question_5_html": "Nosso objetivo é oferecer o melhor atendimento ao cliente possível. Por favor, envie um e-mail para nossa CEO e ela vai cuidar pessoalmente do seu problema.", + "collect_feedback_description": "Recolha feedback completo sobre seu produto ou serviço.", + "collect_feedback_name": "Coletar Feedback", + "collect_feedback_question_1_headline": "Como você avalia sua experiência geral?", + "collect_feedback_question_1_lower_label": "Não tá bom", + "collect_feedback_question_1_subheader": "Não se preocupe, seja honesto.", + "collect_feedback_question_1_upper_label": "Muito bom", + "collect_feedback_question_2_headline": "Adorei! O que você gostou nisso?", + "collect_feedback_question_2_placeholder": "Digite sua resposta aqui...", + "collect_feedback_question_3_headline": "Valeu por compartilhar! O que você não curtiu?", + "collect_feedback_question_3_placeholder": "Digite sua resposta aqui...", + "collect_feedback_question_4_headline": "Como você avalia nossa comunicação?", + "collect_feedback_question_4_lower_label": "Não tá bom", + "collect_feedback_question_4_upper_label": "Muito bom", + "collect_feedback_question_5_headline": "Tem mais alguma coisa que você gostaria de compartilhar com a nossa equipe?", + "collect_feedback_question_5_placeholder": "Digite sua resposta aqui...", + "collect_feedback_question_6_choice_1": "Google", + "collect_feedback_question_6_choice_2": "Redes Sociais", + "collect_feedback_question_6_choice_3": "Amigos", + "collect_feedback_question_6_choice_4": "podcast", + "collect_feedback_question_6_choice_5": "Outro", + "collect_feedback_question_6_headline": "Como você ficou sabendo sobre a gente?", + "collect_feedback_question_7_headline": "Por fim, adoraríamos responder ao seu feedback. Por favor, compartilhe seu e-mail:", + "collect_feedback_question_7_placeholder": "exemplo@email.com", + "consent": "consentimento", + "consent_description": "Pedir para concordar com os termos, condições ou uso de dados", + "contact_info": "Informações de Contato", + "contact_info_description": "Peça nome, sobrenome, e-mail, telefone e empresa juntos", + "csat_description": "Mede o Índice de Satisfação do Cliente do seu produto ou serviço.", + "csat_name": "Pontuação de Satisfação do Cliente (CSAT)", + "csat_question_10_headline": "Você tem mais algum comentário, pergunta ou preocupação?", + "csat_question_10_placeholder": "Digite sua resposta aqui...", + "csat_question_1_headline": "Qual a probabilidade de você recomendar este $[projectName] para um amigo ou colega?", + "csat_question_1_lower_label": "Pouco provável", + "csat_question_1_upper_label": "Muito provável", + "csat_question_2_choice_1": "Meio satisfeito", + "csat_question_2_choice_2": "Muito satisfeito", + "csat_question_2_choice_3": "Nem satisfeito nem insatisfeito", + "csat_question_2_choice_4": "Um pouco insatisfeito", + "csat_question_2_choice_5": "Muito insatisfeito", + "csat_question_2_headline": "No geral, quão satisfeito ou insatisfeito você está com o nosso $[projectName]?", + "csat_question_2_subheader": "Por favor, escolha uma:", + "csat_question_3_choice_1": "Ineficaz", + "csat_question_3_choice_10": "único", + "csat_question_3_choice_2": "Útil", + "csat_question_3_choice_3": "impraticável", + "csat_question_3_choice_4": "caro demais", + "csat_question_3_choice_5": "alta qualidade", + "csat_question_3_choice_6": "confiável", + "csat_question_3_choice_7": "Bom custo-benefício", + "csat_question_3_choice_8": "Qualidade ruim", + "csat_question_3_choice_9": "pouco confiável", + "csat_question_3_headline": "Qual dessas palavras você usaria para descrever nosso $[projectName]?", + "csat_question_3_subheader": "Selecione todas as opções que se aplicam:", + "csat_question_4_choice_1": "Muito bem mesmo", + "csat_question_4_choice_2": "Muito bem", + "csat_question_4_choice_3": "Mais ou menos bem", + "csat_question_4_choice_4": "Não muito bem", + "csat_question_4_choice_5": "Nada bem", + "csat_question_4_headline": "Quão bem o nosso $[projectName] atende às suas necessidades?", + "csat_question_4_subheader": "Escolha uma opção:", + "csat_question_5_choice_1": "Qualidade muito alta", + "csat_question_5_choice_2": "Alta qualidade", + "csat_question_5_choice_3": "baixa qualidade", + "csat_question_5_choice_4": "Qualidade muito baixa", + "csat_question_5_choice_5": "Nem alto nem baixo", + "csat_question_5_headline": "Como você avaliaria a qualidade do $[projectName]?", + "csat_question_5_subheader": "Escolha uma opção:", + "csat_question_6_choice_1": "Excelente", + "csat_question_6_choice_2": "Acima da média", + "csat_question_6_choice_3": "média", + "csat_question_6_choice_4": "Abaixo da média", + "csat_question_6_choice_5": "pobre", + "csat_question_6_headline": "Como você avaliaria o custo-benefício do $[projectName]?", + "csat_question_6_subheader": "Por favor, escolha uma:", + "csat_question_7_choice_1": "Super ágil", + "csat_question_7_choice_2": "Muito ágil", + "csat_question_7_choice_3": "Meio responsivo", + "csat_question_7_choice_4": "Não tão responsivo", + "csat_question_7_choice_5": "Nada responsivo", + "csat_question_7_choice_6": "Não se aplica", + "csat_question_7_headline": "Quão rápido temos respondido suas perguntas sobre nossos serviços?", + "csat_question_7_subheader": "Por favor, escolha uma:", + "csat_question_8_choice_1": "Essa é minha primeira compra", + "csat_question_8_choice_2": "Menos de seis meses", + "csat_question_8_choice_3": "De seis meses a um ano", + "csat_question_8_choice_4": "1 - 2 anos", + "csat_question_8_choice_5": "3 ou mais anos", + "csat_question_8_choice_6": "Ainda não fiz uma compra", + "csat_question_8_headline": "Há quanto tempo você é cliente do $[projectName]?", + "csat_question_8_subheader": "Por favor, escolha uma:", + "csat_question_9_choice_1": "Muito provável", + "csat_question_9_choice_2": "Muito provável", + "csat_question_9_choice_3": "Meio provável", + "csat_question_9_choice_4": "Pouco provável", + "csat_question_9_choice_5": "Nem um pouco provável", + "csat_question_9_headline": "Qual a chance de você comprar nosso $[projectName] de novo?", + "csat_question_9_subheader": "Escolha uma opção:", + "csat_survey_name": "$[projectName] Satisfação do Cliente", + "csat_survey_question_1_headline": "Quão satisfeito(a) você está com a sua experiência com o $[projectName]?", + "csat_survey_question_1_lower_label": "Extremamente insatisfeito", + "csat_survey_question_1_upper_label": "Super satisfeito", + "csat_survey_question_2_headline": "Que legal! Tem algo que a gente possa fazer pra melhorar sua experiência?", + "csat_survey_question_2_placeholder": "Digite sua resposta aqui...", + "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_description": "Crie uma pesquisa sem modelo.", + "custom_survey_name": "Começar do zero", + "custom_survey_question_1_headline": "O que você gostaria de saber?", + "custom_survey_question_1_placeholder": "Digite sua resposta aqui...", + "customer_effort_score_description": "Descubra quão fácil é usar uma funcionalidade.", + "customer_effort_score_name": "Pontuação de Esforço do Cliente (CES)", + "customer_effort_score_question_1_headline": "$[projectName] facilita pra mim [ADICIONAR OBJETIVO]", + "customer_effort_score_question_1_lower_label": "Discordar fortemente", + "customer_effort_score_question_1_upper_label": "Concordo totalmente", + "customer_effort_score_question_2_headline": "Valeu! Como a gente pode facilitar pra você [ADICIONAR OBJETIVO]?", + "customer_effort_score_question_2_placeholder": "Digite sua resposta aqui...", + "date": "Encontro", + "date_description": "Pede pra escolher uma data", + "default_ending_card_button_label": "Crie sua própria pesquisa", + "default_ending_card_headline": "Valeu!", + "default_ending_card_subheader": "Agradecemos seu feedback.", + "default_welcome_card_button_label": "Próximo", + "default_welcome_card_headline": "Bem-vindo!", + "default_welcome_card_html": "Valeu pelo feedback - bora lá!", + "docs_feedback_description": "Meça a clareza de cada página da sua documentação de desenvolvedor.", + "docs_feedback_name": "Feedback dos Docs", + "docs_feedback_question_1_choice_1": "Sim \uD83D\uDC4D", + "docs_feedback_question_1_choice_2": "Não \uD83D\uDC4E", + "docs_feedback_question_1_headline": "Essa página foi útil?", + "docs_feedback_question_2_headline": "Por favor, explica melhor:", + "docs_feedback_question_3_headline": "URL da página", + "earned_advocacy_score_description": "O EAS é uma variação do NPS, mas perguntando sobre comportamentos passados reais em vez de intenções elevadas.", + "earned_advocacy_score_name": "Pontuação de Advocacia Conquistada (PAC)", + "earned_advocacy_score_question_1_choice_1": "Sim", + "earned_advocacy_score_question_1_choice_2": "Não", + "earned_advocacy_score_question_1_headline": "Você tem recomendado ativamente $[projectName] para outras pessoas?", + "earned_advocacy_score_question_2_headline": "Por que você nos recomendou?", + "earned_advocacy_score_question_2_placeholder": "Digite sua resposta aqui...", + "earned_advocacy_score_question_3_headline": "Tão triste. Por quê não?", + "earned_advocacy_score_question_3_placeholder": "Digite sua resposta aqui...", + "earned_advocacy_score_question_4_choice_1": "Sim", + "earned_advocacy_score_question_4_choice_2": "Não", + "earned_advocacy_score_question_4_headline": "Você já desencorajou ativamente outras pessoas de escolherem $[projectName]?", + "earned_advocacy_score_question_5_headline": "O que te fez desanimar eles?", + "earned_advocacy_score_question_5_placeholder": "Digite sua resposta aqui...", + "employee_satisfaction_description": "Medir a satisfação dos funcionários e identificar áreas para melhorar.", + "employee_satisfaction_name": "Satisfação dos Funcionários", + "employee_satisfaction_question_1_headline": "Quão satisfeito você está com seu papel atual?", + "employee_satisfaction_question_1_lower_label": "Não satisfeito", + "employee_satisfaction_question_1_upper_label": "Muito satisfeito", + "employee_satisfaction_question_2_choice_1": "Extremamente significativo", + "employee_satisfaction_question_2_choice_2": "Muito significativo", + "employee_satisfaction_question_2_choice_3": "Moderadamente significativo", + "employee_satisfaction_question_2_choice_4": "Levemente significativo", + "employee_satisfaction_question_2_choice_5": "Nada significativo", + "employee_satisfaction_question_2_headline": "Quão significativo você acha que é o seu trabalho?", + "employee_satisfaction_question_3_headline": "O que você mais gosta de trabalhar aqui?", + "employee_satisfaction_question_3_placeholder": "Digite sua resposta aqui...", + "employee_satisfaction_question_5_headline": "Avalie o suporte que você recebe do seu gerente.", + "employee_satisfaction_question_5_lower_label": "pobre", + "employee_satisfaction_question_5_upper_label": "Excelente", + "employee_satisfaction_question_6_headline": "Quais melhorias você sugeriria para o nosso local de trabalho?", + "employee_satisfaction_question_6_placeholder": "Digite sua resposta aqui...", + "employee_satisfaction_question_7_choice_1": "Muito provável", + "employee_satisfaction_question_7_choice_2": "Muito provável", + "employee_satisfaction_question_7_choice_3": "Moderadamente provável", + "employee_satisfaction_question_7_choice_4": "Pouco provável", + "employee_satisfaction_question_7_choice_5": "Nem um pouco provável", + "employee_satisfaction_question_7_headline": "Qual a probabilidade de você recomendar nossa empresa para um amigo?", + "employee_well_being_description": "Avalie o bem-estar dos seus funcionários através do equilíbrio entre vida pessoal e trabalho, carga de trabalho e ambiente.", + "employee_well_being_name": "Bem-estar dos funcionários", + "employee_well_being_question_1_headline": "Sinto que tenho um bom equilíbrio entre meu trabalho e minha vida pessoal.", + "employee_well_being_question_1_lower_label": "Equilíbrio muito ruim", + "employee_well_being_question_1_upper_label": "Equilíbrio excelente", + "employee_well_being_question_2_headline": "Minha carga de trabalho é tranquila, me permitindo ser produtivo sem me sentir sobrecarregado.", + "employee_well_being_question_2_lower_label": "Trampo esmagador", + "employee_well_being_question_2_upper_label": "Perfeitamente gerenciável", + "employee_well_being_question_3_headline": "O ambiente de trabalho apoia meu bem-estar físico e mental.", + "employee_well_being_question_3_lower_label": "Não apoia", + "employee_well_being_question_3_upper_label": "Super apoiador", + "employee_well_being_question_4_headline": "Quais mudanças, se houver, melhorariam seu bem-estar geral no trabalho?", + "employee_well_being_question_4_placeholder": "Digite sua resposta aqui...", + "enps_survey_name": "Pesquisa eNPS", + "enps_survey_question_1_headline": "Qual a probabilidade de você recomendar trabalhar nessa empresa pra um amigo ou colega?", + "enps_survey_question_1_lower_label": "Nem um pouco provável", + "enps_survey_question_1_upper_label": "Muito provável", + "enps_survey_question_2_headline": "Pra ajudar a gente a melhorar, você pode descrever o(s) motivo(s) da sua avaliação?", + "enps_survey_question_3_headline": "Mais algum comentário, feedback ou preocupação?", + "evaluate_a_product_idea_description": "Pesquise os usuários sobre ideias de produtos ou recursos. Obtenha feedback rapidamente.", + "evaluate_a_product_idea_name": "Avaliar uma Ideia de Produto", + "evaluate_a_product_idea_question_1_button_label": "Bora fazer isso!", + "evaluate_a_product_idea_question_1_dismiss_button_label": "Pular", + "evaluate_a_product_idea_question_1_headline": "A gente adora como você usa o $[projectName]! Queremos muito saber sua opinião sobre uma ideia de recurso. Tem um minutinho?", + "evaluate_a_product_idea_question_1_html": "Respeitamos seu tempo e mantivemos curto \uD83E\uDD38", + "evaluate_a_product_idea_question_2_headline": "Valeu! Quão difícil ou fácil é pra você [ÁREA DO PROBLEMA] hoje?", + "evaluate_a_product_idea_question_2_lower_label": "Muito difícil", + "evaluate_a_product_idea_question_2_upper_label": "Muito fácil", + "evaluate_a_product_idea_question_3_headline": "O que é mais difícil pra você quando se trata de [ÁREA DO PROBLEMA]?", + "evaluate_a_product_idea_question_3_placeholder": "Digite sua resposta aqui...", + "evaluate_a_product_idea_question_4_button_label": "Próximo", + "evaluate_a_product_idea_question_4_dismiss_button_label": "Pular", + "evaluate_a_product_idea_question_4_headline": "Estamos trabalhando em uma ideia para ajudar com [ÁREA DO PROBLEMA].", + "evaluate_a_product_idea_question_4_html": "Insira um breve conceito aqui. Adicione os detalhes necessários, mas mantenha conciso e fácil de entender.", + "evaluate_a_product_idea_question_5_headline": "Quão valiosa essa funcionalidade seria pra você?", + "evaluate_a_product_idea_question_5_lower_label": "Sem valor", + "evaluate_a_product_idea_question_5_upper_label": "Muito valioso", + "evaluate_a_product_idea_question_6_headline": "Entendi. Por que essa função não seria valiosa pra você?", + "evaluate_a_product_idea_question_6_placeholder": "Digite sua resposta aqui...", + "evaluate_a_product_idea_question_7_headline": "O que seria mais valioso pra você nessa funcionalidade?", + "evaluate_a_product_idea_question_7_placeholder": "Digite sua resposta aqui...", + "evaluate_a_product_idea_question_8_headline": "Mais alguma coisa que a gente deve lembrar?", + "evaluate_a_product_idea_question_8_placeholder": "Digite sua resposta aqui...", + "evaluate_content_quality_description": "Meça se suas peças de marketing de conteúdo estão acertando em cheio.", + "evaluate_content_quality_name": "Avaliar Qualidade do Conteúdo", + "evaluate_content_quality_question_1_headline": "Quão bem esse artigo abordou o que você esperava aprender?", + "evaluate_content_quality_question_1_lower_label": "Nada bem", + "evaluate_content_quality_question_1_upper_label": "Muito bem mesmo", + "evaluate_content_quality_question_2_headline": "Hmpft! O que você esperava?", + "evaluate_content_quality_question_2_placeholder": "Digite sua resposta aqui...", + "evaluate_content_quality_question_3_headline": "Que legal! Tem mais alguma coisa que você gostaria que a gente cobrisse?", + "evaluate_content_quality_question_3_placeholder": "Tópicos, tendências, tutoriais...", + "fake_door_follow_up_description": "Acompanhe os usuários que encontraram um dos seus experimentos de Fake Door.", + "fake_door_follow_up_name": "Acompanhamento de Porta Falsa", + "fake_door_follow_up_question_1_headline": "Quão importante é essa funcionalidade pra você?", + "fake_door_follow_up_question_1_lower_label": "Não é importante", + "fake_door_follow_up_question_1_upper_label": "Muito importante", + "fake_door_follow_up_question_2_choice_1": "Aspecto 1", + "fake_door_follow_up_question_2_choice_2": "Aspecto 2", + "fake_door_follow_up_question_2_choice_3": "Aspecto 3", + "fake_door_follow_up_question_2_choice_4": "Aspecto 4", + "fake_door_follow_up_question_2_headline": "O que definitivamente deve ser incluído ao construir isso?", + "feature_chaser_description": "Acompanhe os usuários que acabaram de usar uma funcionalidade específica.", + "feature_chaser_name": "Caçador de Recursos", + "feature_chaser_question_1_headline": "Quão importante é [ADICIONAR RECURSO] pra você?", + "feature_chaser_question_1_lower_label": "Não é importante", + "feature_chaser_question_1_upper_label": "Muito importante", + "feature_chaser_question_2_choice_1": "Aspecto 1", + "feature_chaser_question_2_choice_2": "Aspecto 2", + "feature_chaser_question_2_choice_3": "Aspecto 3", + "feature_chaser_question_2_choice_4": "Aspecto 4", + "feature_chaser_question_2_headline": "Qual aspecto é mais importante?", + "feedback_box_description": "Dê aos seus usuários a chance de compartilhar o que estão pensando sem complicação.", + "feedback_box_name": "Caixa de Feedback", + "feedback_box_question_1_choice_1": "Relatório de bug \uD83D\uDC1E", + "feedback_box_question_1_choice_2": "Pedido de Recurso \uD83D\uDCA1", + "feedback_box_question_1_headline": "O que tá rolando na sua cabeça, chefe?", + "feedback_box_question_1_subheader": "Valeu por compartilhar. A gente te responde o mais rápido possível.", + "feedback_box_question_2_headline": "O que tá quebrado?", + "feedback_box_question_2_subheader": "Quanto mais detalhes, melhor :)", + "feedback_box_question_3_button_label": "Sim, me avise", + "feedback_box_question_3_dismiss_button_label": "Não, valeu", + "feedback_box_question_3_headline": "Quer ficar por dentro?", + "feedback_box_question_3_html": "Vamos consertar isso o mais rápido possível. Você quer ser avisado quando fizermos?", + "feedback_box_question_4_button_label": "Solicitar recurso", + "feedback_box_question_4_headline": "Querida, conta mais pra gente!", + "feedback_box_question_4_placeholder": "Digite sua resposta aqui...", + "feedback_box_question_4_subheader": "Qual problema você quer que a gente resolva?", + "file_upload": "Enviar Arquivo", + "file_upload_description": "Permitir que os respondentes façam upload de documentos, imagens ou outros arquivos", + "finish": "Terminar", + "follow_ups_modal_action_body": "

Oi \uD83D\uDC4B

Valeu por tirar um tempinho pra responder. A gente vai entrar em contato em breve.

Tenha um ótimo dia!

", + "free_text": "Texto livre", + "free_text_description": "Coletar feedback aberto", + "free_text_placeholder": "Digite sua resposta aqui...", + "gauge_feature_satisfaction_description": "Avalie a satisfação com recursos específicos do seu produto.", + "gauge_feature_satisfaction_name": "Medir a Satisfação com o Recurso", + "gauge_feature_satisfaction_question_1_headline": "Quão fácil foi alcançar ... ?", + "gauge_feature_satisfaction_question_1_lower_label": "Não é fácil", + "gauge_feature_satisfaction_question_1_upper_label": "Muito fácil", + "gauge_feature_satisfaction_question_2_headline": "O que a gente poderia melhorar?", + "identify_customer_goals_description": "Entenda melhor se sua mensagem cria as expectativas certas sobre o valor que seu produto oferece.", + "identify_customer_goals_name": "Identificar Objetivos do Cliente", + "identify_sign_up_barriers_description": "Ofereça um desconto pra entender melhor as barreiras de cadastro.", + "identify_sign_up_barriers_name": "Identificar Barreiras de Cadastro", + "identify_sign_up_barriers_question_1_button_label": "Ganhe 10% de desconto", + "identify_sign_up_barriers_question_1_dismiss_button_label": "Não, valeu", + "identify_sign_up_barriers_question_1_headline": "Responda essa pesquisa rápida e ganhe 10% de desconto!", + "identify_sign_up_barriers_question_1_html": "Você parece estar pensando em se inscrever. Responda quatro perguntas e ganhe 10% de desconto em qualquer plano.", + "identify_sign_up_barriers_question_2_headline": "Qual a chance de você se inscrever no $[projectName]?", + "identify_sign_up_barriers_question_2_lower_label": "Nem um pouco provável", + "identify_sign_up_barriers_question_2_upper_label": "Muito provável", + "identify_sign_up_barriers_question_3_choice_1_label": "Pode ser que não tenha o que eu tô procurando", + "identify_sign_up_barriers_question_3_choice_2_label": "Ainda comparando opções", + "identify_sign_up_barriers_question_3_choice_3_label": "Parece complicado", + "identify_sign_up_barriers_question_3_choice_4_label": "Preço é uma preocupação", + "identify_sign_up_barriers_question_3_choice_5_label": "Outra coisa", + "identify_sign_up_barriers_question_3_headline": "O que tá te segurando de experimentar o $[projectName]?", + "identify_sign_up_barriers_question_4_headline": "O que você precisa, mas $[projectName] não oferece?", + "identify_sign_up_barriers_question_4_placeholder": "Digite sua resposta aqui...", + "identify_sign_up_barriers_question_5_headline": "Quais opções você tá considerando?", + "identify_sign_up_barriers_question_5_placeholder": "Digite sua resposta aqui...", + "identify_sign_up_barriers_question_6_headline": "O que parece complicado pra você?", + "identify_sign_up_barriers_question_6_placeholder": "Digite sua resposta aqui...", + "identify_sign_up_barriers_question_7_headline": "O que te preocupa em relação aos preços?", + "identify_sign_up_barriers_question_7_placeholder": "Digite sua resposta aqui...", + "identify_sign_up_barriers_question_8_headline": "Por favor, explica:", + "identify_sign_up_barriers_question_8_placeholder": "Digite sua resposta aqui...", + "identify_sign_up_barriers_question_9_button_label": "Cadastre-se", + "identify_sign_up_barriers_question_9_dismiss_button_label": "Pular por enquanto", + "identify_sign_up_barriers_question_9_headline": "Valeu! Aqui está seu código: SIGNUPNOW10", + "identify_sign_up_barriers_question_9_html": "Valeu demais por tirar um tempinho pra compartilhar seu feedback \uD83D\uDE4F", + "identify_sign_up_barriers_with_project_name": "Barreiras de Cadastro do $[projectName]", + "identify_upsell_opportunities_description": "Descubra quanto tempo seu produto economiza para o usuário. Use isso para fazer upsell.", + "identify_upsell_opportunities_name": "Identificar Oportunidades de Upsell", + "identify_upsell_opportunities_question_1_choice_1": "Menos de 1 hora", + "identify_upsell_opportunities_question_1_choice_2": "1 a 2 horas", + "identify_upsell_opportunities_question_1_choice_3": "3 a 5 horas", + "identify_upsell_opportunities_question_1_choice_4": "mais de 5 horas", + "identify_upsell_opportunities_question_1_headline": "Quantas horas sua equipe economiza por semana usando $[projectName]?", + "improve_activation_rate_description": "Identifique pontos fracos no seu processo de onboarding para aumentar a ativação dos usuários.", + "improve_activation_rate_name": "Melhorar a Taxa de Ativação", + "improve_activation_rate_question_1_choice_1": "Não me pareceu útil", + "improve_activation_rate_question_1_choice_2": "Difícil de configurar ou usar", + "improve_activation_rate_question_1_choice_3": "Faltaram recursos/funcionalidades", + "improve_activation_rate_question_1_choice_4": "Só não tive tempo", + "improve_activation_rate_question_1_choice_5": "Outra coisa", + "improve_activation_rate_question_1_headline": "Qual é o principal motivo pelo qual você ainda não terminou de configurar o $[projectName]?", + "improve_activation_rate_question_2_headline": "O que te fez pensar que $[projectName] não seria útil?", + "improve_activation_rate_question_2_placeholder": "Digite sua resposta aqui...", + "improve_activation_rate_question_3_headline": "O que foi difícil ao configurar ou usar o $[projectName]?", + "improve_activation_rate_question_3_placeholder": "Digite sua resposta aqui...", + "improve_activation_rate_question_4_headline": "Quais recursos ou funcionalidades estavam faltando?", + "improve_activation_rate_question_4_placeholder": "Digite sua resposta aqui...", + "improve_activation_rate_question_5_headline": "Como a gente pode facilitar pra você começar?", + "improve_activation_rate_question_5_placeholder": "Digite sua resposta aqui...", + "improve_activation_rate_question_6_headline": "O que foi? Por favor, explica:", + "improve_activation_rate_question_6_placeholder": "Digite sua resposta aqui...", + "improve_activation_rate_question_6_subheader": "Estamos ansiosos pra consertar isso o mais rápido possível.", + "improve_newsletter_content_description": "Descubra como seus inscritos gostam do conteúdo da sua newsletter.", + "improve_newsletter_content_name": "Melhorar o Conteúdo do Boletim Informativo", + "improve_newsletter_content_question_1_headline": "Como você avaliaria o boletim informativo desta semana?", + "improve_newsletter_content_question_1_lower_label": "Tanto faz", + "improve_newsletter_content_question_1_upper_label": "Ótimo", + "improve_newsletter_content_question_2_headline": "O que teria feito o boletim desta semana mais útil?", + "improve_newsletter_content_question_2_placeholder": "Digite sua resposta aqui...", + "improve_newsletter_content_question_3_button_label": "Feliz em ajudar!", + "improve_newsletter_content_question_3_dismiss_button_label": "Encontre seus próprios amigos", + "improve_newsletter_content_question_3_headline": "Valeu! ❤️ Espalhe o amor com UM amigo.", + "improve_newsletter_content_question_3_html": "Quem pensa como você? Você faria um favorzão pra gente se compartilhasse o episódio dessa semana com seu amigo cérebro!", + "improve_trial_conversion_description": "Descubra por que as pessoas pararam o teste. Esses insights ajudam a melhorar seu funil.", + "improve_trial_conversion_name": "Melhorar a Conversão de Testes", + "improve_trial_conversion_question_1_choice_1": "Não tirei muito proveito disso", + "improve_trial_conversion_question_1_choice_2": "Eu esperava outra coisa", + "improve_trial_conversion_question_1_choice_3": "É caro demais pelo que faz", + "improve_trial_conversion_question_1_choice_4": "Tá faltando uma função", + "improve_trial_conversion_question_1_choice_5": "Eu só estava dando uma olhada", + "improve_trial_conversion_question_1_headline": "Por que você parou seu teste?", + "improve_trial_conversion_question_1_subheader": "Ajuda a gente a te entender melhor:", + "improve_trial_conversion_question_2_button_label": "Próximo", + "improve_trial_conversion_question_2_headline": "Que chato ouvir isso. Qual foi o maior problema ao usar $[projectName]?", + "improve_trial_conversion_question_4_button_label": "Ganhe 20% de desconto", + "improve_trial_conversion_question_4_dismiss_button_label": "Pular", + "improve_trial_conversion_question_4_headline": "Que pena ouvir isso! Ganhe 20% de desconto no primeiro ano.", + "improve_trial_conversion_question_4_html": "Estamos felizes em te oferecer um desconto de 20% no plano anual.", + "improve_trial_conversion_question_5_button_label": "Próximo", + "improve_trial_conversion_question_5_headline": "O que você gostaria de alcançar?", + "improve_trial_conversion_question_5_subheader": "Por favor, escolha uma das opções a seguir:", + "improve_trial_conversion_question_6_headline": "Como você tá resolvendo seu problema agora?", + "improve_trial_conversion_question_6_subheader": "Por favor, nomeie soluções alternativas:", + "integration_setup_survey_description": "Avalie quão fácil é para os usuários adicionarem integrações ao seu produto. Encontre pontos cegos.", + "integration_setup_survey_name": "Pesquisa de Uso de Integração", + "integration_setup_survey_question_1_headline": "Quão fácil foi configurar essa integração?", + "integration_setup_survey_question_1_lower_label": "Não é fácil", + "integration_setup_survey_question_1_upper_label": "Muito fácil", + "integration_setup_survey_question_2_headline": "Por que foi difícil?", + "integration_setup_survey_question_2_placeholder": "Digite sua resposta aqui...", + "integration_setup_survey_question_3_headline": "Quais outras ferramentas você gostaria de usar com $[projectName]?", + "integration_setup_survey_question_3_subheader": "Continuamos criando integrações, a sua pode ser a próxima:", + "interview_prompt_description": "Convide um grupo específico dos seus usuários para agendar uma entrevista com o seu time de produto.", + "interview_prompt_name": "Pergunta de Entrevista", + "interview_prompt_question_1_button_label": "Reservar horário", + "interview_prompt_question_1_headline": "Você tem 15 min pra conversar com a gente? \uD83D\uDE4F", + "interview_prompt_question_1_html": "Você é um dos nossos usuários top. Adoraríamos te entrevistar rapidinho!", + "long_term_retention_check_in_description": "Avalie a satisfação dos usuários a longo prazo, a lealdade e áreas para melhorar pra manter os usuários fiéis.", + "long_term_retention_check_in_name": "Verificação de Retenção a Longo Prazo", + "long_term_retention_check_in_question_10_headline": "Algum feedback ou comentário adicional?", + "long_term_retention_check_in_question_10_placeholder": "Compartilha qualquer ideia ou feedback que possa nos ajudar a melhorar...", + "long_term_retention_check_in_question_1_headline": "Quão satisfeito(a) você está com $[projectName] no geral?", + "long_term_retention_check_in_question_1_lower_label": "Não tô satisfeito", + "long_term_retention_check_in_question_1_upper_label": "Muito satisfeito", + "long_term_retention_check_in_question_2_headline": "O que você acha mais valioso no $[projectName]?", + "long_term_retention_check_in_question_2_placeholder": "Descreva a característica ou benefício que você mais valoriza...", + "long_term_retention_check_in_question_3_choice_1": "Recursos", + "long_term_retention_check_in_question_3_choice_2": "atendimento ao cliente", + "long_term_retention_check_in_question_3_choice_3": "Experiência do usuário", + "long_term_retention_check_in_question_3_choice_4": "preços", + "long_term_retention_check_in_question_3_choice_5": "Confiabilidade e tempo de atividade", + "long_term_retention_check_in_question_3_headline": "Qual aspecto do $[projectName] você acha mais essencial pra sua experiência?", + "long_term_retention_check_in_question_4_headline": "$[projectName] atendeu bem às suas expectativas?", + "long_term_retention_check_in_question_4_lower_label": "Fica aquém", + "long_term_retention_check_in_question_4_upper_label": "Supera as expectativas", + "long_term_retention_check_in_question_5_headline": "Quais desafios ou frustrações você enfrentou ao usar $[projectName]?", + "long_term_retention_check_in_question_5_placeholder": "Descreva qualquer desafio ou melhoria que você gostaria de ver...", + "long_term_retention_check_in_question_6_headline": "Qual a chance de você recomendar $[projectName] para um amigo ou colega?", + "long_term_retention_check_in_question_6_lower_label": "Pouco provável", + "long_term_retention_check_in_question_6_upper_label": "Muito provável", + "long_term_retention_check_in_question_7_choice_1": "Novas funcionalidades e melhorias", + "long_term_retention_check_in_question_7_choice_2": "Suporte ao cliente melhorado", + "long_term_retention_check_in_question_7_choice_3": "Opções de preços melhores", + "long_term_retention_check_in_question_7_choice_4": "Mais integrações", + "long_term_retention_check_in_question_7_choice_5": "Melhorias na experiência do usuário", + "long_term_retention_check_in_question_7_headline": "O que faria você ficar mais tempo como usuário?", + "long_term_retention_check_in_question_8_headline": "Se você pudesse mudar uma coisa no $[projectName], o que seria?", + "long_term_retention_check_in_question_8_placeholder": "Compartilhe quaisquer mudanças ou recursos que você gostaria que considerássemos...", + "long_term_retention_check_in_question_9_headline": "Quão feliz você está com nossas atualizações de produto e a frequência delas?", + "long_term_retention_check_in_question_9_lower_label": "Não tô feliz", + "long_term_retention_check_in_question_9_upper_label": "Muito feliz", + "market_attribution_description": "Descubra como os usuários ouviram falar do seu produto pela primeira vez.", + "market_attribution_name": "Atribuição de Marketing", + "market_attribution_question_1_choice_1": "Recomendação", + "market_attribution_question_1_choice_2": "Mídia Social", + "market_attribution_question_1_choice_3": "anúncios", + "market_attribution_question_1_choice_4": "Pesquisa Google", + "market_attribution_question_1_choice_5": "Num Podcast", + "market_attribution_question_1_headline": "Como você ficou sabendo da gente pela primeira vez?", + "market_attribution_question_1_subheader": "Por favor, selecione uma das opções a seguir:", + "market_site_clarity_description": "Identifique usuários que estão saindo do seu site de marketing. Melhore sua mensagem.", + "market_site_clarity_name": "Clareza no Site de Marketing", + "market_site_clarity_question_1_choice_1": "Sim, totalmente", + "market_site_clarity_question_1_choice_2": "Meio que...", + "market_site_clarity_question_1_choice_3": "Não, de jeito nenhum", + "market_site_clarity_question_1_headline": "Você tem todas as informações que precisa para experimentar o $[projectName]?", + "market_site_clarity_question_2_headline": "O que está faltando ou não está claro pra você sobre $[projectName]?", + "market_site_clarity_question_3_button_label": "Conseguir desconto", + "market_site_clarity_question_3_headline": "Valeu pela resposta! Ganhe 25% de desconto nos primeiros 6 meses:", + "matrix": "Matrix", + "matrix_description": "Crie uma grade para avaliar vários itens com os mesmos critérios", + "measure_search_experience_description": "Meça o quão relevantes são os seus resultados de busca.", + "measure_search_experience_name": "Medir Experiência de Busca", + "measure_search_experience_question_1_headline": "Quão relevantes são esses resultados de busca?", + "measure_search_experience_question_1_lower_label": "Não é nem um pouco relevante", + "measure_search_experience_question_1_upper_label": "Muito relevante", + "measure_search_experience_question_2_headline": "Aff! O que faz os resultados serem irrelevantes pra você?", + "measure_search_experience_question_2_placeholder": "Digite sua resposta aqui...", + "measure_search_experience_question_3_headline": "Que legal! Tem algo que a gente possa fazer pra melhorar sua experiência?", + "measure_search_experience_question_3_placeholder": "Digite sua resposta aqui...", + "measure_task_accomplishment_description": "Veja se as pessoas conseguem fazer o 'Trabalho a Ser Feito'. Pessoas bem-sucedidas são clientes melhores.", + "measure_task_accomplishment_name": "Medir Realização de Tarefas", + "measure_task_accomplishment_question_1_headline": "Você conseguiu fazer o que veio fazer hoje?", + "measure_task_accomplishment_question_1_option_1_label": "Sim", + "measure_task_accomplishment_question_1_option_2_label": "Tô trabalhando nisso, chefe", + "measure_task_accomplishment_question_1_option_3_label": "Não", + "measure_task_accomplishment_question_2_headline": "Quão fácil foi alcançar seu objetivo?", + "measure_task_accomplishment_question_2_lower_label": "Muito difícil", + "measure_task_accomplishment_question_2_upper_label": "Muito fácil", + "measure_task_accomplishment_question_3_headline": "O que dificultou?", + "measure_task_accomplishment_question_3_placeholder": "Digite sua resposta aqui...", + "measure_task_accomplishment_question_4_button_label": "Enviar", + "measure_task_accomplishment_question_4_headline": "Legal! O que você veio fazer aqui hoje?", + "measure_task_accomplishment_question_5_button_label": "Enviar", + "measure_task_accomplishment_question_5_headline": "O que te impediu?", + "measure_task_accomplishment_question_5_placeholder": "Digite sua resposta aqui...", + "multi_select": "Seleção Múltipla", + "multi_select_description": "Peça aos respondentes para escolher uma ou mais opções", + "new_integration_survey_description": "Descubra quais integrações seus usuários gostariam de ver a seguir.", + "new_integration_survey_name": "Nova Pesquisa de Integração", + "new_integration_survey_question_1_choice_1": "PostHog", + "new_integration_survey_question_1_choice_2": "segmento", + "new_integration_survey_question_1_choice_3": "Hubspot", + "new_integration_survey_question_1_choice_4": "Twilio", + "new_integration_survey_question_1_choice_5": "outro", + "new_integration_survey_question_1_headline": "Quais outras ferramentas você está usando?", + "next": "Próximo", + "nps": "Pontuação de Promotores Líquidos (NPS)", + "nps_description": "Medir o Net-Promoter-Score (0-10)", + "nps_lower_label": "Nem um pouco provável", + "nps_name": "Pontuação de Promotores Líquidos (NPS)", + "nps_question_1_headline": "Qual a probabilidade de você recomendar $[projectName] para um amigo ou colega?", + "nps_question_1_lower_label": "Pouco provável", + "nps_question_1_upper_label": "Muito provável", + "nps_question_2_headline": "O que te fez dar essa nota?", + "nps_survey_name": "Pesquisa de NPS", + "nps_survey_question_1_headline": "Qual a probabilidade de você recomendar $[projectName] para um amigo ou colega?", + "nps_survey_question_1_lower_label": "Nada provável", + "nps_survey_question_1_upper_label": "Muito provável", + "nps_survey_question_2_headline": "Pra ajudar a gente a melhorar, você pode descrever o(s) motivo(s) da sua avaliação?", + "nps_survey_question_3_headline": "Mais algum comentário, feedback ou preocupação?", + "nps_upper_label": "Muito provável", + "onboarding_segmentation": "Segmentação de Onboarding", + "onboarding_segmentation_description": "Saiba mais sobre quem se inscreveu no seu produto e por quê.", + "onboarding_segmentation_question_1_choice_1": "fundador", + "onboarding_segmentation_question_1_choice_2": "Executivo", + "onboarding_segmentation_question_1_choice_3": "Gerente de Produto", + "onboarding_segmentation_question_1_choice_4": "Dono do Produto", + "onboarding_segmentation_question_1_choice_5": "Engenheiro de Software", + "onboarding_segmentation_question_1_headline": "Qual é a sua função?", + "onboarding_segmentation_question_1_subheader": "Por favor, selecione uma das opções a seguir:", + "onboarding_segmentation_question_2_choice_1": "só eu", + "onboarding_segmentation_question_2_choice_2": "1-5 funcionários", + "onboarding_segmentation_question_2_choice_3": "6-10 funcionários", + "onboarding_segmentation_question_2_choice_4": "11-100 funcionários", + "onboarding_segmentation_question_2_choice_5": "mais de 100 funcionários", + "onboarding_segmentation_question_2_headline": "Qual é o tamanho da sua empresa?", + "onboarding_segmentation_question_2_subheader": "Por favor, escolha uma das opções a seguir:", + "onboarding_segmentation_question_3_choice_1": "Recomendação", + "onboarding_segmentation_question_3_choice_2": "Redes Sociais", + "onboarding_segmentation_question_3_choice_3": "anúncios", + "onboarding_segmentation_question_3_choice_4": "Pesquisa do Google", + "onboarding_segmentation_question_3_choice_5": "Num Podcast", + "onboarding_segmentation_question_3_headline": "Como você ficou sabendo da gente pela primeira vez?", + "onboarding_segmentation_question_3_subheader": "Por favor, escolha uma das opções a seguir:", + "picture_selection": "Seleção de Imagem", + "picture_selection_description": "Peça aos respondentes para escolherem uma ou mais imagens", + "preview_survey_ending_card_description": "Por favor, continue seu onboarding.", + "preview_survey_ending_card_headline": "Você conseguiu!", + "preview_survey_name": "Nova pesquisa", + "preview_survey_question_1_headline": "Como você avaliaria {projectName}?", + "preview_survey_question_1_lower_label": "Não tá bom", + "preview_survey_question_1_subheader": "Esta é uma prévia da pesquisa.", + "preview_survey_question_1_upper_label": "Muito bom", + "preview_survey_question_2_back_button_label": "Voltar", + "preview_survey_question_2_choice_1_label": "Sim, me mantenha informado.", + "preview_survey_question_2_choice_2_label": "Não, obrigado!", + "preview_survey_question_2_headline": "Quer ficar por dentro?", + "preview_survey_welcome_card_headline": "Bem-vindo!", + "preview_survey_welcome_card_html": "Valeu pelo feedback - bora lá!", + "prioritize_features_description": "Identifique os recursos que seus usuários mais e menos precisam.", + "prioritize_features_name": "Priorizar Funcionalidades", + "prioritize_features_question_1_choice_1": "Recurso 1", + "prioritize_features_question_1_choice_2": "Recurso 2", + "prioritize_features_question_1_choice_3": "Recurso 3", + "prioritize_features_question_1_choice_4": "outro", + "prioritize_features_question_1_headline": "Qual dessas funcionalidades seria MAIS valiosa pra você?", + "prioritize_features_question_2_choice_1": "Recurso 1", + "prioritize_features_question_2_choice_2": "Recurso 2", + "prioritize_features_question_2_choice_3": "Recurso 3", + "prioritize_features_question_2_headline": "Qual dessas funcionalidades seria a MENOS valiosa pra você?", + "prioritize_features_question_3_headline": "De que outra forma poderíamos melhorar sua experiência com $[projectName]?", + "prioritize_features_question_3_placeholder": "Digite sua resposta aqui...", + "product_market_fit_short_description": "Mede o PMF avaliando o quão desapontados os usuários ficariam se seu produto desaparecesse.", + "product_market_fit_short_name": "Pesquisa de Adequação ao Mercado do Produto (Curta)", + "product_market_fit_short_question_1_choice_1": "Nem um pouco decepcionado", + "product_market_fit_short_question_1_choice_2": "Meio desapontado", + "product_market_fit_short_question_1_choice_3": "Muito decepcionado", + "product_market_fit_short_question_1_headline": "Quão decepcionado você ficaria se não pudesse mais usar $[projectName]?", + "product_market_fit_short_question_1_subheader": "Por favor, escolha uma das opções a seguir:", + "product_market_fit_short_question_2_headline": "Como podemos melhorar $[projectName] pra você?", + "product_market_fit_short_question_2_subheader": "Por favor, seja o mais específico possível.", + "product_market_fit_superhuman": "Ajuste do Produto ao Mercado (Superhuman)", + "product_market_fit_superhuman_description": "Meça o PMF avaliando o quão desapontados os usuários ficariam se seu produto desaparecesse.", + "product_market_fit_superhuman_question_1_button_label": "Feliz em ajudar!", + "product_market_fit_superhuman_question_1_dismiss_button_label": "Não, valeu.", + "product_market_fit_superhuman_question_1_headline": "Você é um dos nossos usuários top! Tem 5 minutinhos?", + "product_market_fit_superhuman_question_1_html": "Adoraríamos entender melhor sua experiência como usuário. Compartilhar sua opinião ajuda muito.", + "product_market_fit_superhuman_question_2_choice_1": "Nem um pouco decepcionado", + "product_market_fit_superhuman_question_2_choice_2": "Meio desapontado", + "product_market_fit_superhuman_question_2_choice_3": "Muito decepcionado", + "product_market_fit_superhuman_question_2_headline": "Quão decepcionado você ficaria se não pudesse mais usar $[projectName]?", + "product_market_fit_superhuman_question_2_subheader": "Por favor, escolha uma das opções a seguir:", + "product_market_fit_superhuman_question_3_choice_1": "fundador", + "product_market_fit_superhuman_question_3_choice_2": "Executivo", + "product_market_fit_superhuman_question_3_choice_3": "Gerente de Produto", + "product_market_fit_superhuman_question_3_choice_4": "Dono do Produto", + "product_market_fit_superhuman_question_3_choice_5": "Engenheiro de Software", + "product_market_fit_superhuman_question_3_headline": "Qual é a sua função?", + "product_market_fit_superhuman_question_3_subheader": "Por favor, escolha uma das opções a seguir:", + "product_market_fit_superhuman_question_4_headline": "Que tipo de pessoas você acha que mais se beneficiariam do $[projectName]?", + "product_market_fit_superhuman_question_5_headline": "Qual é o principal benefício que você recebe do $[projectName]?", + "product_market_fit_superhuman_question_6_headline": "Como podemos melhorar $[projectName] pra você?", + "product_market_fit_superhuman_question_6_subheader": "Por favor, seja o mais específico possível.", + "professional_development_growth_survey_description": "Avalie a satisfação dos funcionários com oportunidades de crescimento e desenvolvimento profissional.", + "professional_development_growth_survey_name": "Pesquisa de Crescimento e Desenvolvimento Profissional", + "professional_development_growth_survey_question_1_headline": "Sinto que tenho oportunidades para crescer e desenvolver minhas habilidades no trabalho.", + "professional_development_growth_survey_question_1_lower_label": "Sem oportunidades de crescimento", + "professional_development_growth_survey_question_1_upper_label": "Muitas oportunidades de crescimento", + "professional_development_growth_survey_question_2_headline": "Tenho autonomia suficiente para tomar decisões sobre como faço meu trabalho.", + "professional_development_growth_survey_question_2_lower_label": "Sem autonomia", + "professional_development_growth_survey_question_2_upper_label": "Autonomia total", + "professional_development_growth_survey_question_3_headline": "Meus objetivos no trabalho são claros e alinhados com meu desenvolvimento.", + "professional_development_growth_survey_question_3_lower_label": "Objetivos pouco claros", + "professional_development_growth_survey_question_3_upper_label": "Objetivos claros e alinhados", + "professional_development_growth_survey_question_4_headline": "O que poderia ser melhorado para apoiar seu crescimento profissional?", + "professional_development_growth_survey_question_4_placeholder": "Digite sua resposta aqui...", + "professional_development_survey_description": "Avalie a satisfação dos funcionários com oportunidades de desenvolvimento profissional.", + "professional_development_survey_name": "Avaliação de Desenvolvimento Profissional", + "professional_development_survey_question_1_choice_1": "Sim", + "professional_development_survey_question_1_choice_2": "Não", + "professional_development_survey_question_1_headline": "Você está interessado em atividades de desenvolvimento profissional?", + "professional_development_survey_question_2_choice_1": "Eventos de networking", + "professional_development_survey_question_2_choice_2": "Conferencias ou seminários", + "professional_development_survey_question_2_choice_3": "Cursos ou workshops", + "professional_development_survey_question_2_choice_4": "Mentoria", + "professional_development_survey_question_2_choice_5": "Pesquisa individual", + "professional_development_survey_question_2_choice_6": "Outro", + "professional_development_survey_question_2_headline": "Quais tipos de atividades de desenvolvimento profissional você acha que seriam mais valiosas para o seu crescimento?", + "professional_development_survey_question_2_subheader": "Selecione todas as que se aplicam", + "professional_development_survey_question_3_choice_1": "Sim", + "professional_development_survey_question_3_choice_2": "Não", + "professional_development_survey_question_3_headline": "Você dedicou tempo para o seu desenvolvimento profissional no passado?", + "professional_development_survey_question_4_headline": "Como você se sente em seu ambiente de trabalho quando se trata de buscar oportunidades de desenvolvimento profissional?", + "professional_development_survey_question_4_lower_label": "Não é suportado em absoluto", + "professional_development_survey_question_4_upper_label": "Extremamente suportado", + "professional_development_survey_question_5_choice_1": "Para meu próprio conhecimento", + "professional_development_survey_question_5_choice_2": "Para ganhar mais responsabilidades", + "professional_development_survey_question_5_choice_3": "Para melhorar minhas habilidades", + "professional_development_survey_question_5_choice_4": "Para avançar em minha carreira atual", + "professional_development_survey_question_5_choice_5": "Procurando uma nova vaga", + "professional_development_survey_question_5_choice_6": "Outro", + "professional_development_survey_question_5_headline": "Quais são seus principais motivos para querer dedicar tempo para o desenvolvimento profissional?", + "ranking": "classificação", + "ranking_description": "Peça aos respondentes para ordenar os itens por preferência ou importância", + "rate_checkout_experience_description": "Deixe os clientes avaliarem a experiência de checkout para ajustar a conversão.", + "rate_checkout_experience_name": "Avaliar Experiência de Checkout", + "rate_checkout_experience_question_1_headline": "Quão fácil ou difícil foi finalizar a compra?", + "rate_checkout_experience_question_1_lower_label": "Muito difícil", + "rate_checkout_experience_question_1_upper_label": "Muito fácil", + "rate_checkout_experience_question_2_headline": "Desculpa por isso! O que teria facilitado pra você?", + "rate_checkout_experience_question_2_placeholder": "Digite sua resposta aqui...", + "rate_checkout_experience_question_3_headline": "Que legal! Tem algo que a gente possa fazer pra melhorar sua experiência?", + "rate_checkout_experience_question_3_placeholder": "Digite sua resposta aqui...", + "rating": "avaliação", + "rating_description": "Peça aos respondentes uma avaliação (estrelas, carinhas, números)", + "rating_lower_label": "Não tá bom", + "rating_upper_label": "Muito bom", + "recognition_and_reward_survey_description": "Avalie a satisfação dos funcionários com reconhecimento, recompensas, suporte da liderança e liberdade de expressão.", + "recognition_and_reward_survey_name": "Reconhecimento e Recompensa", + "recognition_and_reward_survey_question_1_headline": "Quando me desempenho bem, minhas contribuições são reconhecidas pela organização.", + "recognition_and_reward_survey_question_1_lower_label": "Nada reconhecido", + "recognition_and_reward_survey_question_1_upper_label": "Muito reconhecido", + "recognition_and_reward_survey_question_2_headline": "Sinto que sou recompensado(a) de forma justa pelo trabalho que faço.", + "recognition_and_reward_survey_question_2_lower_label": "Não recompensado justamente", + "recognition_and_reward_survey_question_2_upper_label": "Muito bem recompensado", + "recognition_and_reward_survey_question_3_headline": "Me sinto à vontade para compartilhar minhas opiniões abertamente no trabalho.", + "recognition_and_reward_survey_question_3_lower_label": "Não me sinto à vontade", + "recognition_and_reward_survey_question_3_upper_label": "Muito à vontade", + "recognition_and_reward_survey_question_4_headline": "Como a organização poderia melhorar o reconhecimento e as recompensas?", + "recognition_and_reward_survey_question_4_placeholder": "Digite sua resposta aqui...", + "review_prompt_description": "Convida os usuários que amam seu produto a fazer uma avaliação pública.", + "review_prompt_name": "Solicitação de Avaliação", + "review_prompt_question_1_headline": "O que você achou do $[projectName]?", + "review_prompt_question_1_lower_label": "Não tá bom", + "review_prompt_question_1_upper_label": "Muito satisfeito", + "review_prompt_question_2_button_label": "Escrever avaliação", + "review_prompt_question_2_headline": "Feliz em saber \uD83D\uDE4F Por favor, escreva uma avaliação pra gente!", + "review_prompt_question_2_html": "Isso ajuda a gente muito.", + "review_prompt_question_3_button_label": "Enviar", + "review_prompt_question_3_headline": "Que pena ouvir isso! O que é UMA coisa que podemos melhorar?", + "review_prompt_question_3_placeholder": "Digite sua resposta aqui...", + "review_prompt_question_3_subheader": "Ajude a gente a melhorar sua experiência.", + "schedule_a_meeting": "Marcar uma reunião", + "schedule_a_meeting_description": "Peça aos respondentes para agendarem um horário para reuniões ou chamadas", + "single_select": "Seleção Única", + "single_select_description": "Ofereça uma lista de opções (escolha uma)", + "site_abandonment_survey": "Pesquisa de Abandono de Site", + "site_abandonment_survey_description": "Entenda os motivos por trás do abandono de carrinho na sua loja online.", + "site_abandonment_survey_question_1_html": "Percebemos que você está saindo do nosso site sem fazer uma compra. Adoraríamos entender o motivo.", + "site_abandonment_survey_question_2_button_label": "Claro!", + "site_abandonment_survey_question_2_dismiss_button_label": "Não, valeu.", + "site_abandonment_survey_question_2_headline": "Você tem um minuto?", + "site_abandonment_survey_question_3_choice_1": "Não consigo encontrar o que estou procurando", + "site_abandonment_survey_question_3_choice_2": "Encontrei um site melhor", + "site_abandonment_survey_question_3_choice_3": "O site tá muito lento", + "site_abandonment_survey_question_3_choice_4": "Só dando uma olhada", + "site_abandonment_survey_question_3_choice_5": "Encontrei um preço melhor em outro lugar", + "site_abandonment_survey_question_3_choice_6": "outro", + "site_abandonment_survey_question_3_headline": "Qual o principal motivo pra você estar saindo do nosso site?", + "site_abandonment_survey_question_3_subheader": "Por favor, escolha uma das opções a seguir:", + "site_abandonment_survey_question_4_headline": "Por favor, explique o motivo de estar saindo do site:", + "site_abandonment_survey_question_5_headline": "Como você avaliaria sua experiência geral no nosso site?", + "site_abandonment_survey_question_5_lower_label": "Muito insatisfeito", + "site_abandonment_survey_question_5_upper_label": "Muito satisfeito", + "site_abandonment_survey_question_6_choice_1": "Tempos de carregamento mais rápidos", + "site_abandonment_survey_question_6_choice_2": "Funcionalidade melhor de busca de produtos", + "site_abandonment_survey_question_6_choice_3": "Mais variedade de produtos", + "site_abandonment_survey_question_6_choice_4": "Design do site melhorado", + "site_abandonment_survey_question_6_choice_5": "Mais avaliações de clientes", + "site_abandonment_survey_question_6_choice_6": "outro", + "site_abandonment_survey_question_6_headline": "Quais melhorias fariam você ficar mais tempo no nosso site?", + "site_abandonment_survey_question_6_subheader": "Por favor, selecione todas as opções que se aplicam:", + "site_abandonment_survey_question_7_headline": "Você gostaria de receber atualizações sobre novos produtos e promoções?", + "site_abandonment_survey_question_7_label": "Sim, por favor entre em contato.", + "site_abandonment_survey_question_8_headline": "Por favor, compartilha seu e-mail:", + "site_abandonment_survey_question_9_headline": "Algum comentário ou sugestão a mais?", + "skip": "Pular", + "smileys_survey_name": "Pesquisa de Smileys", + "smileys_survey_question_1_headline": "O que você tá achando do $[projectName]?", + "smileys_survey_question_1_lower_label": "Não tá bom", + "smileys_survey_question_1_upper_label": "Muito satisfeito", + "smileys_survey_question_2_button_label": "Escrever avaliação", + "smileys_survey_question_2_headline": "Feliz em saber \uD83D\uDE4F Por favor, escreva uma avaliação pra gente!", + "smileys_survey_question_2_html": "Isso nos ajuda muito.", + "smileys_survey_question_3_button_label": "Enviar", + "smileys_survey_question_3_headline": "Que pena ouvir isso! O que é UMA coisa que podemos melhorar?", + "smileys_survey_question_3_placeholder": "Digite sua resposta aqui...", + "smileys_survey_question_3_subheader": "Ajude a gente a melhorar sua experiência.", + "star_rating_survey_name": "Pesquisa de Avaliação do $[projectName]", + "star_rating_survey_question_1_headline": "O que você tá achando do $[projectName]?", + "star_rating_survey_question_1_lower_label": "Extremamente insatisfeito", + "star_rating_survey_question_1_upper_label": "Super satisfeito", + "star_rating_survey_question_2_button_label": "Escrever avaliação", + "star_rating_survey_question_2_headline": "Feliz em saber \uD83D\uDE4F Por favor, escreva uma avaliação pra gente!", + "star_rating_survey_question_2_html": "Isso ajuda a gente muito.", + "star_rating_survey_question_3_button_label": "Enviar", + "star_rating_survey_question_3_headline": "Que pena! O que podemos melhorar?", + "star_rating_survey_question_3_placeholder": "Digite sua resposta aqui...", + "star_rating_survey_question_3_subheader": "Ajude-nos a melhorar sua experiência.", + "statement_call_to_action": "Declaração (Chamada para Ação)", + "supportive_work_culture_survey_description": "Avalie a percepção dos funcionários sobre o suporte da liderança, comunicação e ambiente geral de trabalho.", + "supportive_work_culture_survey_name": "Cultura de Trabalho de Apoio", + "supportive_work_culture_survey_question_1_headline": "Meu gestor me oferece o suporte necessário para realizar meu trabalho.", + "supportive_work_culture_survey_question_1_lower_label": "Sem suporte", + "supportive_work_culture_survey_question_1_upper_label": "Muito apoiador", + "supportive_work_culture_survey_question_2_headline": "A comunicação dentro da organização é aberta e eficaz.", + "supportive_work_culture_survey_question_2_lower_label": "Comunicação ruim", + "supportive_work_culture_survey_question_2_upper_label": "Comunicação excelente", + "supportive_work_culture_survey_question_3_headline": "O ambiente de trabalho é positivo e apoia meu bem-estar.", + "supportive_work_culture_survey_question_3_lower_label": "Não apoiador", + "supportive_work_culture_survey_question_3_upper_label": "Muito apoiador", + "supportive_work_culture_survey_question_4_headline": "Como a cultura de trabalho poderia ser melhorada para te apoiar melhor?", + "supportive_work_culture_survey_question_4_placeholder": "Digite sua resposta aqui...", + "uncover_strengths_and_weaknesses_description": "Descubra o que os usuários gostam e não gostam sobre seu produto ou serviço.", + "uncover_strengths_and_weaknesses_name": "Descubra Pontos Fortes e Fracos", + "uncover_strengths_and_weaknesses_question_1_choice_1": "Facilidade de uso", + "uncover_strengths_and_weaknesses_question_1_choice_2": "Bom custo-benefício", + "uncover_strengths_and_weaknesses_question_1_choice_3": "É código aberto", + "uncover_strengths_and_weaknesses_question_1_choice_4": "Os fundadores são fofos", + "uncover_strengths_and_weaknesses_question_1_choice_5": "Outro", + "uncover_strengths_and_weaknesses_question_1_headline": "O que você mais valoriza no $[projectName]?", + "uncover_strengths_and_weaknesses_question_2_choice_1": "Documentação", + "uncover_strengths_and_weaknesses_question_2_choice_2": "Personalização", + "uncover_strengths_and_weaknesses_question_2_choice_3": "Preços", + "uncover_strengths_and_weaknesses_question_2_choice_4": "Outro", + "uncover_strengths_and_weaknesses_question_2_headline": "O que a gente deve melhorar?", + "uncover_strengths_and_weaknesses_question_2_subheader": "Por favor, escolha uma das opções a seguir:", + "uncover_strengths_and_weaknesses_question_3_headline": "Você gostaria de acrescentar algo?", + "uncover_strengths_and_weaknesses_question_3_subheader": "Sinta-se à vontade para falar o que pensa, nós também fazemos isso.", + "understand_low_engagement_description": "Identificar razões para o baixo engajamento para melhorar a adoção dos usuários.", + "understand_low_engagement_name": "Entender Baixo Engajamento", + "understand_low_engagement_question_1_choice_1": "Difícil de usar", + "understand_low_engagement_question_1_choice_2": "Encontrei uma alternativa melhor", + "understand_low_engagement_question_1_choice_3": "Só não tive tempo", + "understand_low_engagement_question_1_choice_4": "Faltaram recursos que eu preciso", + "understand_low_engagement_question_1_choice_5": "outro", + "understand_low_engagement_question_1_headline": "Qual é o principal motivo de você não ter voltado para $[projectName] recentemente?", + "understand_low_engagement_question_2_headline": "O que é difícil em usar $[projectName]?", + "understand_low_engagement_question_2_placeholder": "Digite sua resposta aqui...", + "understand_low_engagement_question_3_headline": "Entendi. Qual alternativa você tá usando então?", + "understand_low_engagement_question_3_placeholder": "Digite sua resposta aqui...", + "understand_low_engagement_question_4_headline": "Entendi. Como a gente pode facilitar pra você começar?", + "understand_low_engagement_question_4_placeholder": "Digite sua resposta aqui...", + "understand_low_engagement_question_5_headline": "Entendi. Quais recursos ou funcionalidades estavam faltando?", + "understand_low_engagement_question_5_placeholder": "Digite sua resposta aqui...", + "understand_low_engagement_question_6_headline": "Por favor, adiciona mais detalhes:", + "understand_low_engagement_question_6_placeholder": "Digite sua resposta aqui...", + "understand_purchase_intention_description": "Descubra quão perto seus visitantes estão de comprar ou assinar.", + "understand_purchase_intention_name": "Entender a Intenção de Compra", + "understand_purchase_intention_question_1_headline": "Qual a chance de você comprar com a gente hoje?", + "understand_purchase_intention_question_1_lower_label": "Nem um pouco provável", + "understand_purchase_intention_question_1_upper_label": "Muito provável", + "understand_purchase_intention_question_2_headline": "Entendi. Qual é o principal motivo da sua visita hoje?", + "understand_purchase_intention_question_2_placeholder": "Digite sua resposta aqui...", + "understand_purchase_intention_question_3_headline": "O que, se é que tem algo, está te impedindo de fazer a compra hoje?", + "understand_purchase_intention_question_3_placeholder": "Digite sua resposta aqui..." + } +} diff --git a/apps/web/lib/messages/pt-PT.json b/apps/web/lib/messages/pt-PT.json new file mode 100644 index 0000000000..9930e9de90 --- /dev/null +++ b/apps/web/lib/messages/pt-PT.json @@ -0,0 +1,2845 @@ +{ + "auth": { + "continue_with_azure": "Continuar com Azure", + "continue_with_email": "Continuar com Email", + "continue_with_github": "Continuar com GitHub", + "continue_with_google": "Continuar com Google", + "continue_with_oidc": "Continuar com {oidcDisplayName}", + "continue_with_openid": "Continuar com OpenID", + "continue_with_saml": "Continuar com SAML SSO", + "forgot-password": { + "back_to_login": "Voltar ao login", + "email-sent": { + "heading": "Pedido de redefinição de palavra-passe efetuado com sucesso", + "text": "Se existir uma conta com este email, receberá instruções para redefinir a palavra-passe em breve." + }, + "reset": { + "confirm_password": "Confirmar palavra-passe", + "new_password": "Nova palavra-passe", + "no_token_provided": "Nenhum token fornecido", + "passwords_do_not_match": "As palavras-passe não coincidem", + "success": { + "heading": "Palavra-passe redefinida com sucesso", + "text": "Pode agora iniciar sessão com a sua nova palavra-passe" + } + }, + "reset_password": "Redefinir palavra-passe" + }, + "invite": { + "create_account": "Criar uma conta", + "email_does_not_match": "Ooops! Email errada \uD83E\uDD26", + "email_does_not_match_description": "O email no convite não corresponde ao seu.", + "go_to_app": "Ir para a aplicação", + "happy_to_have_you": "Feliz por ter-te aqui \uD83E\uDD17", + "happy_to_have_you_description": "Por favor, crie uma conta ou inicie sessão.", + "invite_expired": "Convite expirado \uD83D\uDE25", + "invite_expired_description": "Os convites são válidos por 7 dias. Por favor, solicite um novo convite.", + "invite_not_found": "Convite não encontrado \uD83D\uDE25", + "invite_not_found_description": "O código de convite não pode ser encontrado ou já foi utilizado.", + "login": "Iniciar sessão", + "welcome_to_organization": "Estás dentro \uD83C\uDF89", + "welcome_to_organization_description": "Bem-vindo à organização." + }, + "last_used": "Última Utilização", + "login": { + "backup_code": "Código de backup", + "create_an_account": "Criar uma conta", + "enter_your_backup_code": "Introduza o seu código de backup", + "enter_your_two_factor_authentication_code": "Introduza o seu código de autenticação de dois fatores", + "forgot_your_password": "Esqueceu a sua palavra-passe?", + "login_to_your_account": "Iniciar sessão na sua conta", + "login_with_email": "Iniciar sessão com Email", + "lost_access": "Perdeu o acesso?", + "new_to_formbricks": "Novo no Formbricks?", + "use_a_backup_code": "Use um código de backup" + }, + "saml_connection_error": "Algo correu mal. Por favor, verifique a consola da aplicação para mais detalhes.", + "signup": { + "captcha_failed": "Captcha falhou", + "have_an_account": "Tem uma conta?", + "log_in": "Iniciar sessão", + "password_validation_contain_at_least_1_number": "Conter pelo menos 1 número", + "password_validation_minimum_8_and_maximum_128_characters": "Mínimo 8 e Máximo 128 caracteres", + "password_validation_uppercase_and_lowercase": "Mistura de maiúsculas e minúsculas", + "please_verify_captcha": "Por favor, verifique o reCAPTCHA", + "privacy_policy": "Política de Privacidade", + "terms_of_service": "Termos de Serviço", + "title": "Crie a sua conta Formbricks" + }, + "signup_without_verification_success": { + "user_successfully_created": "Utilizador criado com sucesso", + "user_successfully_created_description": "O seu novo utilizador foi criado com sucesso. Por favor, clique no botão abaixo e inicie sessão na sua conta." + }, + "testimonial_1": "Medimos a clareza dos nossos documentos e aprendemos com a rotatividade, tudo numa só plataforma. Ótimo produto, equipa muito responsiva!", + "testimonial_all_features_included": "Todas as funcionalidades incluídas", + "testimonial_free_and_open_source": "Gratuito e de código aberto", + "testimonial_no_credit_card_required": "Não é necessário cartão de crédito", + "testimonial_title": "Transforme as perceções dos clientes em experiências irresistíveis.", + "verification-requested": { + "invalid_email_address": "Endereço de email inválido", + "invalid_token": "Token inválido ☹️", + "no_email_provided": "Nenhum email fornecido", + "please_click_the_link_in_the_email_to_activate_your_account": "Por favor, clique no link no email para ativar a sua conta.", + "please_confirm_your_email_address": "Por favor, confirme o seu endereço de email", + "resend_verification_email": "Reenviar email de verificação", + "verification_email_successfully_sent": "Email de verificação enviado com sucesso. Por favor, verifique a sua caixa de entrada.", + "we_sent_an_email_to": "Enviámos um email para {email}. ", + "you_didnt_receive_an_email_or_your_link_expired": "Não recebeu um email ou o seu link expirou?" + }, + "verify": { + "no_token_provided": "Nenhum token fornecido", + "verifying": "A verificar..." + } + }, + "billing_confirmation": { + "back_to_billing_overview": "Voltar à visão geral de faturação", + "thanks_for_upgrading": "Muito obrigado por atualizar a sua subscrição do Formbricks.", + "upgrade_successful": "Atualização bem-sucedida" + }, + "common": { + "accepted": "Aceite", + "account": "Conta", + "account_settings": "Configurações da conta", + "action": "Ação", + "actions": "Ações", + "active_surveys": "Inquéritos ativos", + "activity": "Atividade", + "add": "Adicionar", + "add_action": "Adicionar ação", + "add_filter": "Adicionar filtro", + "add_logo": "Adicionar logótipo", + "add_project": "Adicionar projeto", + "add_to_team": "Adicionar à equipa", + "all": "Todos", + "all_questions": "Todas as perguntas", + "allow": "Permitir", + "allow_users_to_exit_by_clicking_outside_the_survey": "Permitir que os utilizadores saiam ao clicar fora do questionário", + "an_unknown_error_occurred_while_deleting_table_items": "Ocorreu um erro desconhecido ao eliminar {type}s", + "and": "E", + "and_response_limit_of": "e limite de resposta de", + "anonymous": "Anónimo", + "api_keys": "Chaves API", + "app": "Aplicação", + "app_survey": "Inquérito da Aplicação", + "apply_filters": "Aplicar filtros", + "are_you_sure": "Tem a certeza?", + "are_you_sure_this_action_cannot_be_undone": "Tem a certeza? Esta ação não pode ser desfeita.", + "attributes": "Atributos", + "avatar": "Avatar", + "back": "Voltar", + "billing": "Faturação", + "booked": "Reservado", + "bottom_left": "Inferior Esquerdo", + "bottom_right": "Inferior Direito", + "cancel": "Cancelar", + "centered_modal": "Modal Centralizado", + "choices": "Escolhas", + "clear_all": "Limpar tudo", + "clear_filters": "Limpar filtros", + "clear_selection": "Limpar seleção", + "click": "Clique", + "clicks": "Cliques", + "close": "Fechar", + "code": "Código", + "collapse_rows": "Recolher linhas", + "completed": "Concluído", + "configuration": "Configuração", + "confirm": "Confirmar", + "connect": "Conectar", + "connect_formbricks": "Ligar Formbricks", + "connected": "Conectado", + "contacts": "Contactos", + "copied_to_clipboard": "Copiado para a área de transferência", + "copy": "Copiar", + "copy_code": "Copiar código", + "copy_link": "Copiar Link", + "create_new_organization": "Criar nova organização", + "create_segment": "Criar segmento", + "create_survey": "Criar inquérito", + "created": "Criado", + "created_at": "Criado em", + "created_by": "Criado por", + "customer_success": "Sucesso do Cliente", + "danger_zone": "Zona de Perigo", + "dark_overlay": "Sobreposição escura", + "date": "Data", + "default": "Padrão", + "delete": "Eliminar", + "description": "Descrição", + "dev_env": "Ambiente de Desenvolvimento", + "development_environment_banner": "Está num ambiente de desenvolvimento. Configure-o para testar inquéritos, ações e atributos.", + "disable": "Desativar", + "disallow": "Não permitir", + "discard": "Descartar", + "dismissed": "Dispensado", + "docs": "Documentação", + "documentation": "Documentação", + "download": "Transferir", + "draft": "Rascunho", + "duplicate": "Duplicar", + "e_commerce": "Comércio Eletrónico", + "edit": "Editar", + "email": "Email", + "embed": "Incorporar", + "enterprise_license": "Licença Enterprise", + "environment_not_found": "Ambiente não encontrado", + "environment_notice": "Está atualmente no ambiente {environment}.", + "error": "Erro", + "error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.", + "error_component_title": "Erro ao carregar recursos", + "expand_rows": "Expandir linhas", + "finish": "Concluir", + "follow_these": "Siga estes", + "formbricks_version": "Versão do Formbricks", + "full_name": "Nome completo", + "gathering_responses": "A recolher respostas", + "general": "Geral", + "go_back": "Voltar", + "go_to_dashboard": "Ir para o Painel", + "hidden": "Oculto", + "hidden_field": "Campo oculto", + "hidden_fields": "Campos ocultos", + "hide": "Esconder", + "hide_column": "Ocultar coluna", + "image": "Imagem", + "images": "Imagens", + "import": "Importar", + "impressions": "Impressões", + "imprint": "Impressão", + "in_progress": "Em Progresso", + "inactive_surveys": "Inquéritos inativos", + "input_type": "Tipo de entrada", + "insights": "Informações", + "integration": "integração", + "integrations": "Integrações", + "invalid_date": "Data inválida", + "invalid_file_type": "Tipo de ficheiro inválido", + "invite": "Convidar", + "invite_them": "Convide-os", + "key": "Chave", + "label": "Etiqueta", + "language": "Idioma", + "learn_more": "Saiba mais", + "license": "Licença", + "light_overlay": "Sobreposição leve", + "limits_reached": "Limites Atingidos", + "link": "Link", + "link_and_email": "Link e Email", + "link_copied": "Link copiado para a área de transferência!", + "link_survey": "Ligar Inquérito", + "link_surveys": "Ligar Inquéritos", + "load_more": "Carregar mais", + "loading": "A carregar", + "logo": "Logótipo", + "logout": "Terminar sessão", + "look_and_feel": "Aparência e Sensação", + "manage": "Gerir", + "marketing": "Marketing", + "maximum": "Máximo", + "member": "Membro", + "members": "Membros", + "membership_not_found": "Associação não encontrada", + "metadata": "Metadados", + "minimum": "Mínimo", + "mobile_overlay_text": "O Formbricks não está disponível para dispositivos com resoluções menores.", + "move_down": "Mover para baixo", + "move_up": "Mover para cima", + "multiple_languages": "Várias línguas", + "name": "Nome", + "negative": "Negativo", + "neutral": "Neutro", + "new": "Novo", + "new_survey": "Novo inquérito", + "new_version_available": "Formbricks {version} está aqui. Atualize agora!", + "next": "Seguinte", + "no_background_image_found": "Nenhuma imagem de fundo encontrada.", + "no_code": "Sem código", + "no_files_uploaded": "Nenhum ficheiro foi carregado", + "no_result_found": "Nenhum resultado encontrado", + "no_results": "Nenhum resultado", + "no_surveys_found": "Nenhum inquérito encontrado.", + "not_authenticated": "Não está autenticado para realizar esta ação.", + "not_authorized": "Não autorizado", + "not_connected": "Não Conectado", + "note": "Nota", + "notes": "Notas", + "notifications": "Notificações", + "number": "Número", + "off": "Desligado", + "on": "Ligado", + "only_one_file_allowed": "Apenas um ficheiro é permitido", + "only_owners_managers_and_manage_access_members_can_perform_this_action": "Apenas proprietários e gestores podem realizar esta ação.", + "or": "ou", + "organization": "Organização", + "organization_id": "ID da Organização", + "organization_not_found": "Organização não encontrada", + "organization_teams_not_found": "Equipas da organização não encontradas", + "other": "Outro", + "others": "Outros", + "overview": "Visão geral", + "password": "Palavra-passe", + "paused": "Pausado", + "pending_downgrade": "Rebaixamento Pendente", + "people_manager": "Gestor de Pessoas", + "person": "Pessoa", + "phone": "Telefone", + "photo_by": "Foto de", + "pick_a_date": "Escolha uma data", + "placeholder": "Espaço reservado", + "please_select_at_least_one_survey": "Por favor, selecione pelo menos um inquérito", + "please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho", + "please_upgrade_your_plan": "Por favor, atualize o seu plano.", + "positive": "Positivo", + "preview": "Pré-visualização", + "preview_survey": "Pré-visualização do inquérito", + "privacy": "Política de Privacidade", + "privacy_policy": "Política de Privacidade", + "product_manager": "Gestor de Produto", + "profile": "Perfil", + "project": "Projeto", + "project_configuration": "Configuração do Projeto", + "project_id": "ID do Projeto", + "project_name": "Nome do Projeto", + "project_not_found": "Projeto não encontrado", + "project_permission_not_found": "Permissão do projeto não encontrada", + "projects": "Projetos", + "projects_limit_reached": "Limite de projetos atingido", + "question": "Pergunta", + "question_id": "ID da pergunta", + "questions": "Perguntas", + "read_docs": "Ler Documentos", + "remove": "Remover", + "reorder_and_hide_columns": "Reordenar e ocultar colunas", + "report_survey": "Relatório de Inquérito", + "request_trial_license": "Solicitar licença de teste", + "reset_to_default": "Repor para o padrão", + "response": "Resposta", + "responses": "Respostas", + "restart": "Reiniciar", + "role": "Função", + "role_organization": "Função (Organização)", + "saas": "SaaS", + "sales": "Vendas", + "save": "Guardar", + "save_changes": "Guardar alterações", + "scheduled": "Agendado", + "search": "Procurar", + "security": "Segurança", + "segment": "Segmento", + "segments": "Segmentos", + "select": "Selecionar", + "select_all": "Selecionar tudo", + "select_survey": "Selecionar Inquérito", + "selected": "Selecionado", + "selected_questions": "Perguntas selecionadas", + "selection": "Seleção", + "selections": "Seleções", + "send": "Enviar", + "send_test_email": "Enviar email de teste", + "session_not_found": "Sessão não encontrada", + "settings": "Configurações", + "share_feedback": "Partilhar feedback", + "show": "Mostrar", + "show_response_count": "Mostrar contagem de respostas", + "shown": "Mostrado", + "size": "Tamanho", + "skipped": "Ignorado", + "skips": "Saltos", + "some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar", + "something_went_wrong_please_try_again": "Algo correu mal. Por favor, tente novamente.", + "sort_by": "Ordenar por", + "start_free_trial": "Iniciar Teste Grátis", + "status": "Estado", + "step_by_step_manual": "Manual passo a passo", + "styling": "Estilo", + "submit": "Submeter", + "summary": "Resumo", + "survey": "Inquérito", + "survey_completed": "Inquérito concluído.", + "survey_id": "ID do Inquérito", + "survey_languages": "Idiomas da Pesquisa", + "survey_live": "Inquérito ao vivo", + "survey_not_found": "Inquérito não encontrado", + "survey_paused": "Inquérito pausado.", + "survey_scheduled": "Inquérito agendado.", + "survey_type": "Tipo de Inquérito", + "surveys": "Inquéritos", + "switch_organization": "Mudar de organização", + "switch_to": "Mudar para {environment}", + "table_items_deleted_successfully": "{type}s eliminados com sucesso", + "table_settings": "Configurações da tabela", + "tags": "Etiquetas", + "targeting": "Segmentação", + "team": "Equipa", + "team_access": "Acesso da Equipa", + "team_name": "Nome da equipa", + "teams": "Controlo de Acesso", + "teams_not_found": "Equipas não encontradas", + "text": "Texto", + "time": "Tempo", + "time_to_finish": "Tempo para concluir", + "title": "Título", + "top_left": "Superior Esquerdo", + "top_right": "Superior Direito", + "try_again": "Tente novamente", + "type": "Tipo", + "unlock_more_projects_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior", + "update": "Atualizar", + "updated": "Atualizado", + "updated_at": "Atualizado em", + "upload": "Carregar", + "upload_input_description": "Clique ou arraste para carregar ficheiros.", + "url": "URL", + "user": "Utilizador", + "user_id": "ID do Utilizador", + "user_not_found": "Utilizador não encontrado", + "variable": "Variável", + "variables": "Variáveis", + "verified_email": "Email verificado", + "video": "Vídeo", + "warning": "Aviso", + "we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "Não foi possível verificar a sua licença porque o servidor de licenças está inacessível.", + "webhook": "Webhook", + "webhooks": "Webhooks", + "website_and_app_connection": "Ligação de Website e Aplicação", + "website_app_survey": "Inquérito do Website e da Aplicação", + "website_survey": "Inquérito do Website", + "weekly_summary": "Resumo semanal", + "welcome_card": "Cartão de boas-vindas", + "yes": "Sim", + "you": "Você", + "you_are_downgraded_to_the_community_edition": "Foi rebaixado para a Edição Comunitária.", + "you_are_not_authorised_to_perform_this_action": "Não está autorizado para realizar esta ação.", + "you_have_reached_your_limit_of_project_limit": "Atingiu o seu limite de {projectLimit} projetos.", + "you_have_reached_your_monthly_miu_limit_of": "Atingiu o seu limite mensal de MIU de", + "you_have_reached_your_monthly_response_limit_of": "Atingiu o seu limite mensal de respostas de", + "you_will_be_downgraded_to_the_community_edition_on_date": "Será rebaixado para a Edição Comunitária em {date}." + }, + "emails": { + "accept": "Aceitar", + "click_or_drag_to_upload_files": "Clique ou arraste para carregar ficheiros.", + "email_customization_preview_email_heading": "Olá {userName}", + "email_customization_preview_email_subject": "Pré-visualização da Personalização de E-mail do Formbricks", + "email_customization_preview_email_text": "Esta é uma pré-visualização de email para mostrar qual logotipo será exibido nos emails.", + "email_footer_text_1": "Tenha um ótimo dia!", + "email_footer_text_2": "A Equipa Formbricks", + "email_template_text_1": "Este email foi enviado via Formbricks.", + "embed_survey_preview_email_didnt_request": "Não pediu isto?", + "embed_survey_preview_email_environment_id": "ID do Ambiente", + "embed_survey_preview_email_fight_spam": "Ajude-nos a combater o spam e encaminhe este e-mail para hola@formbricks.com", + "embed_survey_preview_email_heading": "Pré-visualizar Incorporação de Email", + "embed_survey_preview_email_subject": "Pré-visualização da Pesquisa de E-mail do Formbricks", + "embed_survey_preview_email_text": "É assim que o trecho de código aparece incorporado num email:", + "forgot_password_email_change_password": "Alterar palavra-passe", + "forgot_password_email_did_not_request": "Se não solicitou isto, por favor ignore este email.", + "forgot_password_email_heading": "Alterar palavra-passe", + "forgot_password_email_link_valid_for_24_hours": "O link é válido por 24 horas.", + "forgot_password_email_subject": "Redefina a sua palavra-passe do Formbricks", + "forgot_password_email_text": "Solicitou um link para alterar a sua palavra-passe. Pode fazê-lo clicando no link abaixo:", + "imprint": "Impressão", + "invite_accepted_email_heading": "Olá", + "invite_accepted_email_subject": "Tem um novo membro na organização!", + "invite_accepted_email_text_par1": "Só para te informar que", + "invite_accepted_email_text_par2": "aceitou o seu convite. Divirta-se a colaborar!", + "invite_email_button_label": "Junte-se à organização", + "invite_email_heading": "Olá", + "invite_email_text_par1": "O seu colega", + "invite_email_text_par2": "convidou-o a juntar-se a eles no Formbricks. Para aceitar o convite, por favor clique no link abaixo:", + "invite_member_email_subject": "Está convidado a colaborar no Formbricks!", + "live_survey_notification_completed": "Concluído", + "live_survey_notification_draft": "Rascunho", + "live_survey_notification_in_progress": "Em Progresso", + "live_survey_notification_no_new_response": "Nenhuma nova resposta recebida esta semana \uD83D\uDD75️", + "live_survey_notification_no_responses_yet": "Ainda sem respostas!", + "live_survey_notification_paused": "Pausado", + "live_survey_notification_scheduled": "Agendado", + "live_survey_notification_view_more_responses": "Ver mais {responseCount} respostas", + "live_survey_notification_view_previous_responses": "Ver respostas anteriores", + "live_survey_notification_view_response": "Ver Resposta", + "notification_footer_all_the_best": "Tudo de bom,", + "notification_footer_in_your_settings": "nas suas definições \uD83D\uDE4F", + "notification_footer_please_turn_them_off": "por favor, desative-os", + "notification_footer_the_formbricks_team": "A Equipa Formbricks \uD83E\uDD0D", + "notification_footer_to_halt_weekly_updates": "Para parar as Atualizações Semanais,", + "notification_header_hey": "Olá \uD83D\uDC4B", + "notification_header_weekly_report_for": "Relatório Semanal para", + "notification_insight_completed": "Concluído", + "notification_insight_completion_rate": "Conclusão %", + "notification_insight_displays": "Ecrãs", + "notification_insight_responses": "Respostas", + "notification_insight_surveys": "Inquéritos", + "onboarding_invite_email_button_label": "Junte-se à organização de {inviterName}", + "onboarding_invite_email_connect_formbricks": "Conecte o Formbricks à sua aplicação ou website através de um Snippet HTML ou NPM em apenas alguns minutos.", + "onboarding_invite_email_create_account": "Crie uma conta para se juntar à organização de {inviterName}.", + "onboarding_invite_email_done": "Concluído ✅", + "onboarding_invite_email_get_started_in_minutes": "Começar em Minutos", + "onboarding_invite_email_heading": "Olá ", + "onboarding_invite_email_subject": "{inviterName} precisa de ajuda para configurar o Formbricks. Podes ajudar?", + "password_changed_email_heading": "Palavra-passe alterada", + "password_changed_email_text": "A sua palavra-passe foi alterada com sucesso.", + "password_reset_notify_email_subject": "A sua palavra-passe do Formbricks foi alterada", + "privacy_policy": "Política de Privacidade", + "reject": "Rejeitar", + "render_email_response_value_file_upload_response_link_not_included": "O link para o ficheiro carregado não está incluído por razões de privacidade de dados", + "response_finished_email_subject": "Uma resposta para {surveyName} foi concluída ✅", + "response_finished_email_subject_with_email": "{personEmail} acabou de completar o seu inquérito {surveyName} ✅", + "schedule_your_meeting": "Agende a sua reunião", + "select_a_date": "Selecionar uma data", + "survey_response_finished_email_congrats": "Parabéns, recebeu uma nova resposta ao seu inquérito! Alguém acabou de completar o seu inquérito: {surveyName}", + "survey_response_finished_email_dont_want_notifications": "Não quer receber estas notificações?", + "survey_response_finished_email_hey": "Olá \uD83D\uDC4B", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Desativar notificações para todos os formulários recém-criados", + "survey_response_finished_email_turn_off_notifications_for_this_form": "Desativar notificações para este formulário", + "survey_response_finished_email_view_more_responses": "Ver mais {responseCount} respostas", + "survey_response_finished_email_view_survey_summary": "Ver resumo do inquérito", + "verification_email_click_on_this_link": "Também pode clicar neste link:", + "verification_email_heading": "Quase lá!", + "verification_email_hey": "Olá \uD83D\uDC4B", + "verification_email_if_expired_request_new_token": "Se tiver expirado, solicite um novo token aqui:", + "verification_email_link_valid_for_24_hours": "O link é válido por 24 horas.", + "verification_email_request_new_verification": "Pedir nova verificação", + "verification_email_subject": "Por favor, verifique o seu email para usar o Formbricks", + "verification_email_survey_name": "Nome do inquérito", + "verification_email_take_survey": "Responder ao inquérito", + "verification_email_text": "Para começar a usar o Formbricks, por favor verifique o seu email abaixo:", + "verification_email_thanks": "Obrigado por validar o seu email!", + "verification_email_to_fill_survey": "Para preencher o questionário, clique no botão abaixo:", + "verification_email_verify_email": "Verificar email", + "verified_link_survey_email_subject": "O seu inquérito está pronto para ser preenchido.", + "weekly_summary_create_reminder_notification_body_cal_slot": "Escolha um intervalo de 15 minutos no calendário do nosso CEO", + "weekly_summary_create_reminder_notification_body_dont_let_a_week_pass": "Não deixe passar uma semana sem aprender sobre os seus utilizadores:", + "weekly_summary_create_reminder_notification_body_need_help": "Precisa de ajuda para encontrar o inquérito certo para o seu produto?", + "weekly_summary_create_reminder_notification_body_reply_email": "ou responda a este email :)", + "weekly_summary_create_reminder_notification_body_setup_a_new_survey": "Configurar um novo inquérito", + "weekly_summary_create_reminder_notification_body_text": "Gostaríamos de lhe enviar um Resumo Semanal, mas de momento não há inquéritos a decorrer para {projectName}.", + "weekly_summary_email_subject": "{projectName} Informações do Utilizador - Última Semana por Formbricks" + }, + "environments": { + "actions": { + "action_copied_successfully": "Ação copiada com sucesso", + "action_copy_failed": "Falha na cópia da ação", + "action_created_successfully": "Ação criada com sucesso", + "action_deleted_successfully": "Ação eliminada com sucesso", + "action_type": "Tipo de Ação", + "action_updated_successfully": "Ação atualizada com sucesso", + "action_with_key_already_exists": "Ação com a chave {key} já existe", + "action_with_name_already_exists": "Ação com o nome {name} já existe", + "add_css_class_or_id": "Adicionar classe ou id CSS", + "add_url": "Adicionar URL", + "click": "Clique", + "contains": "Contém", + "create_action": "Criar ação", + "css_selector": "Seletor CSS", + "delete_action_text": "Tem a certeza de que deseja eliminar esta ação? Isto também remove esta ação como um gatilho de todos os seus inquéritos.", + "display_name": "Nome de exibição", + "does_not_contain": "Não contém", + "does_not_exactly_match": "Não corresponde exatamente", + "eg_clicked_download": "Por exemplo, Clicou em Descarregar", + "eg_download_cta_click_on_home": "por exemplo, descarregar_cta_clicar_em_home", + "eg_install_app": "Ex. Instalar App", + "eg_user_clicked_download_button": "Por exemplo, Utilizador clicou no Botão Descarregar", + "ends_with": "Termina com", + "enter_a_url_to_see_if_a_user_visiting_it_would_be_tracked": "Introduza um URL para ver se um utilizador que o visita seria rastreado.", + "exactly_matches": "Corresponde exatamente", + "exit_intent": "Intenção de Saída", + "fifty_percent_scroll": "Rolar 50%", + "how_do_code_actions_work": "Como funcionam as Ações de Código?", + "if_a_user_clicks_a_button_with_a_specific_css_class_or_id": "Se um utilizador clicar num botão com uma classe ou id CSS específica", + "if_a_user_clicks_a_button_with_a_specific_text": "Se um utilizador clicar num botão com um texto específico", + "in_your_code_read_more_in_our": "no seu código. Leia mais no nosso", + "inner_text": "Texto Interno", + "invalid_css_selector": "Seletor CSS inválido", + "limit_the_pages_on_which_this_action_gets_captured": "Limitar as páginas nas quais esta ação é capturada", + "limit_to_specific_pages": "Limitar a páginas específicas", + "on_all_pages": "Em todas as páginas", + "page_filter": "Filtro de página", + "page_view": "Visualização de Página", + "select_match_type": "Selecionar tipo de correspondência", + "starts_with": "Começa com", + "test_match": "Testar correspondência", + "test_your_url": "Testar o seu URL", + "this_action_was_created_automatically_you_cannot_make_changes_to_it": "Esta ação foi criada automaticamente. Não pode fazer alterações a esta ação.", + "this_action_will_be_triggered_when_the_page_is_loaded": "Esta ação será desencadeada quando a página for carregada.", + "this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Esta ação será desencadeada quando o utilizador rolar 50% da página.", + "this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Esta ação será desencadeada quando o utilizador tentar sair da página.", + "this_is_a_code_action_please_make_changes_in_your_code_base": "Esta é uma ação de código. Por favor, faça alterações na sua base de código.", + "track_new_user_action": "Rastrear Nova Ação do Utilizador", + "track_user_action_to_display_surveys_or_create_user_segment": "Rastrear ação do utilizador para exibir inquéritos ou criar segmento de utilizador.", + "url": "URL", + "user_actions": "Ações do Utilizador", + "user_clicked_download_button": "Utilizador clicou no Botão Descarregar", + "what_did_your_user_do": "O que fez o seu utilizador?", + "what_is_the_user_doing": "O que está o utilizador a fazer?", + "you_can_track_code_action_anywhere_in_your_app_using": "Pode rastrear a ação do código em qualquer lugar na sua aplicação usando" + }, + "connect": { + "congrats": "Parabéns!", + "connection_successful_message": "Muito bem! Estamos ligados.", + "do_it_later": "Farei isso mais tarde", + "finish_onboarding": "Concluir Integração", + "headline": "Ligue a sua aplicação ou website", + "import_formbricks_and_initialize_the_widget_in_your_component": "Importar Formbricks e inicializar o widget no seu Componente (por exemplo, App.tsx):", + "insert_this_code_into_the_head_tag_of_your_website": "Insira este código na tag head do seu website:", + "subtitle": "Demora menos de 4 minutos.", + "waiting_for_your_signal": "À espera do seu sinal..." + }, + "contacts": { + "contact_deleted_successfully": "Contacto eliminado com sucesso", + "contact_not_found": "Nenhum contacto encontrado", + "contacts_table_refresh": "Atualizar contactos", + "contacts_table_refresh_error": "Algo correu mal ao atualizar os contactos, por favor, tente novamente", + "contacts_table_refresh_success": "Contactos atualizados com sucesso", + "first_name": "Primeiro Nome", + "last_name": "Apelido", + "no_responses_found": "Nenhuma resposta encontrada", + "not_provided": "Não fornecido", + "search_contact": "Procurar contacto", + "select_attribute": "Selecionar Atributo", + "unlock_contacts_description": "Gerir contactos e enviar inquéritos direcionados", + "unlock_contacts_title": "Desbloqueie os contactos com um plano superior", + "upload_contacts_modal_attributes_description": "Mapeie as colunas no seu CSV para os atributos no Formbricks.", + "upload_contacts_modal_attributes_new": "Novo atributo", + "upload_contacts_modal_attributes_search_or_add": "Pesquisar ou adicionar atributo", + "upload_contacts_modal_attributes_should_be_mapped_to": "deve ser mapeado para", + "upload_contacts_modal_attributes_title": "Atributos", + "upload_contacts_modal_description": "Carregue um ficheiro CSV para importar rapidamente contactos com atributos", + "upload_contacts_modal_download_example_csv": "Descarregar exemplo de CSV", + "upload_contacts_modal_duplicates_description": "Como devemos proceder se um contacto já existir nos seus contactos?", + "upload_contacts_modal_duplicates_overwrite_description": "Sobrescreve os contactos existentes", + "upload_contacts_modal_duplicates_overwrite_title": "Sobrescrever", + "upload_contacts_modal_duplicates_skip_description": "Ignora os contactos duplicados", + "upload_contacts_modal_duplicates_skip_title": "Saltar", + "upload_contacts_modal_duplicates_title": "Duplicados", + "upload_contacts_modal_duplicates_update_description": "Atualiza os contactos existentes", + "upload_contacts_modal_duplicates_update_title": "Atualizar", + "upload_contacts_modal_pick_different_file": "Escolher um ficheiro diferente", + "upload_contacts_modal_preview": "Aqui está uma pré-visualização dos seus dados.", + "upload_contacts_modal_upload_btn": "Carregar contactos" + }, + "experience": { + "all": "Todos", + "all_time": "Todo o tempo", + "analysed_feedbacks": "Respostas de Texto Livre Analisadas", + "category": "Categoria", + "category_updated_successfully": "Categoria atualizada com sucesso!", + "complaint": "Queixa", + "did_you_find_this_insight_helpful": "Achou esta informação útil?", + "failed_to_update_category": "Falha ao atualizar a categoria", + "feature_request": "Pedido", + "good_afternoon": "\uD83C\uDF24️ Boa tarde", + "good_evening": "\uD83C\uDF19 Boa noite", + "good_morning": "☀️ Bom dia", + "insights_description": "Todos os insights gerados a partir das respostas de todos os seus inquéritos", + "insights_for_project": "Informações sobre {projectName}", + "new_responses": "Respostas", + "no_insights_for_this_filter": "Sem informações para este filtro", + "no_insights_found": "Não foram encontradas informações. Recolha mais respostas ao inquérito ou ative informações para os seus inquéritos existentes para começar.", + "praise": "Elogio", + "sentiment_score": "Pontuação de Sentimento", + "templates_card_description": "Escolha um modelo ou comece do zero", + "templates_card_title": "Meça a experiência do seu cliente", + "this_month": "Este mês", + "this_quarter": "Este trimestre", + "this_week": "Esta semana", + "today": "Hoje" + }, + "formbricks_logo": "Logotipo do Formbricks", + "integrations": { + "activepieces_integration_description": "Conecte instantaneamente o Formbricks com apps populares para automatizar tarefas sem codificação.", + "additional_settings": "Configurações Adicionais", + "airtable": { + "airtable_base": "Base do Airtable", + "airtable_integration": "Integração com o Airtable", + "airtable_integration_description": "Sincronize respostas diretamente com o Airtable.", + "airtable_integration_is_not_configured": "A integração com o Airtable não está configurada", + "connect_with_airtable": "Ligar ao Airtable", + "link_airtable_table": "Ligar Tabela Airtable", + "link_new_table": "Ligar nova tabela", + "no_bases_found": "Nenhuma base do Airtable encontrada", + "no_integrations_yet": "As suas integrações com o Airtable aparecerão aqui assim que as adicionar. ⏲️", + "please_create_a_base": "Por favor, crie uma base no Airtable", + "please_select_a_base": "Por favor, selecione uma base", + "please_select_a_table": "Por favor, selecione uma tabela", + "sync_responses_with_airtable": "Sincronizar respostas com um Airtable", + "table_name": "Nome da Tabela" + }, + "airtable_integration_description": "Preencha instantaneamente a sua tabela Airtable com dados de inquéritos", + "connected_with_email": "Ligado com {email}", + "connecting_integration_failed_please_try_again": "Falha ao conectar a integração. Por favor, tente novamente!", + "create_survey_warning": "Tem de criar um inquérito para poder configurar esta integração", + "delete_integration": "Eliminar Integração", + "delete_integration_confirmation": "Tem a certeza de que deseja eliminar esta integração?", + "google_sheet_integration_description": "Preencha instantaneamente as suas folhas de cálculo com dados de inquéritos", + "google_sheets": { + "connect_with_google_sheets": "Conectar com o Google Sheets", + "enter_a_valid_spreadsheet_url_error": "Por favor, insira um URL de folha de cálculo válido", + "google_connection": "Ligação Google", + "google_connection_deletion_description": "Sincronize respostas diretamente com o Google Sheets.", + "google_sheet_integration_is_not_configured": "A integração com o Google Sheets não está configurada na sua instância do Formbricks.", + "google_sheet_logo": "Logótipo da Folha do Google", + "google_sheet_name": "Nome da Folha do Google", + "google_sheets_integration": "Integração com o Google Sheets", + "google_sheets_integration_description": "Sincronize respostas diretamente com o Google Sheets.", + "link_google_sheet": "Ligar Folha do Google", + "link_new_sheet": "Ligar nova Folha", + "no_integrations_yet": "As suas integrações com o Google Sheets aparecerão aqui assim que as adicionar. ⏲️", + "spreadsheet_url": "URL da folha de cálculo" + }, + "include_created_at": "Incluir Criado Em", + "include_hidden_fields": "Incluir Campos Ocultos", + "include_metadata": "Incluir Metadados (Navegador, País, etc.)", + "include_variables": "Incluir Variáveis", + "integration_added_successfully": "Integração adicionada com sucesso", + "integration_removed_successfully": "Integração removida com sucesso", + "integration_updated_successfully": "Integração atualizada com sucesso", + "make_integration_description": "Integre o Formbricks com mais de 1000 apps via Make", + "manage_webhooks": "Gerir Webhooks", + "n8n_integration_description": "Integre o Formbricks com mais de 350 apps via n8n", + "notion": { + "col_name_of_type_is_not_supported": "{col_name} do tipo {type} não é suportado pela API do Notion. Os dados não serão refletidos na sua base de dados do Notion.", + "connect_with_notion": "Ligar ao Notion", + "connected_with_workspace": "Ligado com o espaço de trabalho {workspace}", + "create_at_least_one_database_to_setup_this_integration": "Tem de criar pelo menos uma base de dados para poder configurar esta integração", + "database_name": "Nome da Base de Dados", + "duplicate_connection_warning": "Uma ligação com esta base de dados está ativa. Por favor, faça alterações com cautela.", + "link_database": "Ligar Base de Dados", + "link_new_database": "Ligar nova base de dados", + "link_notion_database": "Ligar Base de Dados do Notion", + "map_formbricks_fields_to_notion_property": "Mapear campos do Formbricks para propriedade do Notion", + "no_databases_found": "As suas integrações com o Notion aparecerão aqui assim que as adicionar. ⏲️", + "notion_integration": "Integração com Notion", + "notion_integration_description": "Enviar respostas diretamente para o Notion.", + "notion_integration_is_not_configured": "A integração com o Notion não está configurada na sua instância do Formbricks.", + "notion_logo": "Logotipo do Notion", + "please_complete_mapping_fields_with_notion_property": "Por favor, complete os campos de mapeamento com a propriedade do Notion", + "please_resolve_mapping_errors": "Por favor, resolva os erros de mapeamento", + "please_select_a_database": "Por favor, selecione uma base de dados", + "please_select_at_least_one_mapping": "Por favor, selecione pelo menos um mapeamento", + "que_name_of_type_cant_be_mapped_to": "{que_name} do tipo {question_label} não pode ser mapeado para a coluna {col_name} do tipo {col_type}. Em vez disso, use a coluna do tipo {mapped_type}.", + "select_a_database": "Selecionar Base de Dados", + "select_a_field_to_map": "Selecione um campo para mapear", + "select_a_survey_question": "Selecione uma pergunta do inquérito", + "sync_responses_with_a_notion_database": "Sincronizar respostas com uma Base de Dados do Notion", + "update_connection": "Reconectar Notion", + "update_connection_tooltip": "Restabeleça a integração para incluir as bases de dados recentemente adicionadas. As suas integrações existentes permanecerão intactas." + }, + "notion_integration_description": "Enviar dados para a sua base de dados do Notion", + "please_select_a_survey_error": "Por favor, selecione um inquérito", + "select_at_least_one_question_error": "Por favor, selecione pelo menos uma pergunta", + "slack": { + "already_connected_another_survey": "Já ligou outro inquérito a este canal.", + "channel_name": "Nome do Canal", + "connect_with_slack": "Ligar ao Slack", + "connect_your_first_slack_channel": "Ligue o seu primeiro canal Slack para começar.", + "connected_with_team": "Ligado com {team}", + "create_at_least_one_channel_error": "Tem de criar pelo menos um canal para poder configurar esta integração", + "dont_see_your_channel": "Não vê o seu canal?", + "link_channel": "Ligar canal", + "link_slack_channel": "Ligar Canal Slack", + "please_select_a_channel": "Por favor, selecione um canal", + "select_channel": "Selecione Canal", + "slack_integration": "Integração com Slack", + "slack_integration_description": "Enviar respostas diretamente para o Slack.", + "slack_integration_is_not_configured": "A integração com o Slack não está configurada na sua instância do Formbricks.", + "slack_reconnect_button": "Reconectar", + "slack_reconnect_button_description": "Nota: Recentemente alterámos a nossa integração com o Slack para também suportar canais privados. Por favor, reconecte o seu espaço de trabalho do Slack." + }, + "slack_integration_description": "Conecte instantaneamente o seu Workspace do Slack com o Formbricks", + "to_configure_it": "para configurá-lo.", + "webhook_integration_description": "Acione Webhooks com base em ações nos seus inquéritos", + "webhooks": { + "add_webhook": "Adicionar Webhook", + "add_webhook_description": "Enviar dados de resposta do inquérito para um endpoint personalizado", + "all_current_and_new_surveys": "Todos os inquéritos atuais e novos", + "created_by_third_party": "Criado por um Terceiro", + "discord_webhook_not_supported": "Os webhooks do Discord não são atualmente suportados.", + "empty_webhook_message": "Os seus webhooks aparecerão aqui assim que os adicionar. ⏲️", + "endpoint_pinged": "Yay! Conseguimos aceder ao webhook!", + "endpoint_pinged_error": "Não foi possível aceder ao webhook!", + "please_check_console": "Por favor, verifique a consola para mais detalhes", + "please_enter_a_url": "Por favor, insira um URL", + "response_created": "Resposta Criada", + "response_finished": "Resposta Concluída", + "response_updated": "Resposta Atualizada", + "source": "Fonte", + "test_endpoint": "Testar Endpoint", + "triggers": "Disparadores", + "webhook_added_successfully": "Webhook adicionado com sucesso", + "webhook_delete_confirmation": "Tem a certeza de que deseja eliminar este Webhook? Isto irá parar de lhe enviar quaisquer notificações futuras.", + "webhook_deleted_successfully": "Webhook eliminado com sucesso", + "webhook_name_placeholder": "Opcional: Rotule o seu webhook para fácil identificação", + "webhook_test_failed_due_to": "Teste de Webhook Falhou devido a", + "webhook_updated_successfully": "Webhook atualizado com sucesso.", + "webhook_url_placeholder": "Cole o URL no qual deseja que o evento seja acionado" + }, + "website_or_app_integration_description": "Integre o Formbricks no seu Website ou Aplicação", + "zapier_integration_description": "Integre o Formbricks com mais de 5000 apps via Zapier" + }, + "project": { + "api_keys": { + "add_api_key": "Adicionar Chave API", + "api_key": "Chave API", + "api_key_copied_to_clipboard": "Chave API copiada para a área de transferência", + "api_key_created": "Chave API criada", + "api_key_deleted": "Chave API eliminada", + "api_key_label": "Etiqueta da Chave API", + "api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", + "api_key_updated": "Chave API atualizada", + "duplicate_access": "Acesso duplicado ao projeto não permitido", + "no_api_keys_yet": "Ainda não tem nenhuma chave API", + "no_env_permissions_found": "Nenhuma permissão de ambiente encontrada", + "organization_access": "Acesso à Organização", + "permissions": "Permissões", + "project_access": "Acesso ao Projeto", + "secret": "Segredo", + "unable_to_delete_api_key": "Não é possível eliminar a chave API" + }, + "app-connection": { + "api_host_description": "Este é o URL do seu backend Formbricks.", + "app_connection": "Ligação de Aplicação", + "app_connection_description": "Ligue a sua aplicação ao Formbricks", + "check_out_the_docs": "Consulte a documentação.", + "dive_into_the_docs": "Mergulhe na documentação.", + "does_your_widget_work": "O seu widget funciona?", + "environment_id": "O seu EnvironmentId", + "environment_id_description": "Este id identifica de forma única este ambiente Formbricks.", + "environment_id_description_with_environment_id": "Usado para identificar o ambiente correto: {environmentId} é o seu.", + "formbricks_sdk": "SDK Formbricks", + "formbricks_sdk_connected": "O SDK do Formbricks está conectado", + "formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado", + "formbricks_sdk_not_connected_description": "Ligue o seu website ou aplicação ao Formbricks", + "have_a_problem": "Tem um problema?", + "how_to_setup": "Como configurar", + "how_to_setup_description": "Siga estes passos para configurar o widget Formbricks na sua aplicação.", + "identifying_your_users": "identificar os seus utilizadores", + "if_you_are_planning_to": "Se está a planear", + "insert_this_code_into_the": "Insira este código no", + "need_a_more_detailed_setup_guide_for": "Precisa de um guia de configuração mais detalhado para", + "not_working": "Não está a funcionar?", + "open_an_issue_on_github": "Abrir um problema no GitHub", + "open_the_browser_console_to_see_the_logs": "Abra a consola do navegador para ver os registos.", + "receiving_data": "A receber dados \uD83D\uDC83\uD83D\uDD7A", + "recheck": "Verificar novamente", + "scroll_to_the_top": "Rolar para o topo!", + "step_1": "Passo 1: Instalar com pnpm, npm ou yarn", + "step_2": "Passo 2: Inicializar widget", + "step_2_description": "Importar Formbricks e inicializar o widget no seu Componente (por exemplo, App.tsx):", + "step_3": "Passo 3: Modo de depuração", + "switch_on_the_debug_mode_by_appending": "Ativar o modo de depuração adicionando", + "tag_of_your_app": "tag da sua aplicação", + "to_the_url_where_you_load_the": "para o URL onde carrega o", + "want_to_learn_how_to_add_user_attributes": "Quer aprender a adicionar atributos de utilizador, eventos personalizados e mais?", + "you_are_done": "Está concluído \uD83C\uDF89", + "you_can_set_the_user_id_with": "pode definir o ID do utilizador com", + "your_app_now_communicates_with_formbricks": "A sua aplicação agora comunica com o Formbricks - enviando eventos e carregando inquéritos automaticamente!" + }, + "general": { + "cannot_delete_only_project": "Este é o seu único projeto, não pode ser eliminado. Crie um novo projeto primeiro.", + "delete_project": "Eliminar Projeto", + "delete_project_confirmation": "Tem a certeza de que deseja eliminar {projectName}? Esta ação não pode ser desfeita.", + "delete_project_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incl. todos os inquéritos, respostas, pessoas, ações e atributos.", + "delete_project_settings_description": "Eliminar projeto com todos os inquéritos, respostas, pessoas, ações e atributos. Isto não pode ser desfeito.", + "error_saving_project_information": "Erro ao guardar informações do projeto", + "only_owners_or_managers_can_delete_projects": "Apenas os proprietários ou gestores podem eliminar projetos", + "project_deleted_successfully": "Projeto eliminado com sucesso", + "project_name_settings_description": "Altere o nome dos seus projetos.", + "project_name_updated_successfully": "Nome do projeto atualizado com sucesso", + "recontact_waiting_time": "Tempo de Espera para Recontacto", + "recontact_waiting_time_settings_description": "Controlar a frequência com que os utilizadores podem ser inquiridos em todos os inquéritos da aplicação.", + "this_action_cannot_be_undone": "Esta ação não pode ser desfeita.", + "wait_x_days_before_showing_next_survey": "Aguarde X dias antes de mostrar o próximo inquérito:", + "waiting_period_updated_successfully": "Período de espera atualizado com sucesso", + "whats_your_project_called": "Como se chama o seu projeto?" + }, + "languages": { + "add_language": "Adicionar idioma", + "alias": "Pseudónimo", + "alias_tooltip": "O alias é um nome alternativo para identificar a língua em inquéritos de ligação e no SDK (opcional)", + "cannot_remove_language_warning": "Não pode remover este idioma, pois ainda é utilizado nestes questionários:", + "conflict_between_identifier_and_alias": "Existe um conflito entre o identificador de uma língua adicionada e um dos seus aliases. Aliases e identificadores não podem ser idênticos.", + "conflict_between_selected_alias_and_another_language": "Existe um conflito entre o alias selecionado e outra língua que tem este identificador. Por favor, adicione a língua com este identificador ao seu projeto para evitar inconsistências.", + "delete_language_confirmation": "Tem a certeza de que deseja eliminar este idioma? Esta ação não pode ser desfeita.", + "duplicate_language_or_language_id": "Idioma ou ID de idioma duplicado", + "edit_languages": "Editar idiomas", + "identifier": "Identificador (ISO)", + "incomplete_translations": "Traduções incompletas", + "language": "Idioma", + "language_deleted_successfully": "Idioma eliminado com sucesso", + "languages_updated_successfully": "Idiomas atualizados com sucesso", + "multi_language_surveys": "Inquéritos Multilingues", + "multi_language_surveys_description": "Adicione idiomas para criar inquéritos multilingues.", + "no_language_found": "Nenhuma língua encontrada. Adicione a sua primeira língua abaixo.", + "please_select_a_language": "Por favor, selecione um idioma", + "remove_language": "Remover Idioma", + "remove_language_from_surveys_to_remove_it_from_project": "Por favor, remova o idioma destes questionários para o remover do projeto.", + "search_items": "Procurar itens", + "translate": "Traduzir" + }, + "look": { + "add_background_color": "Adicionar cor de fundo", + "add_background_color_description": "Adicionar uma cor de fundo ao contentor do logótipo.", + "app_survey_placement": "Colocação do Inquérito da Aplicação", + "app_survey_placement_settings_description": "Altere onde os inquéritos serão exibidos na sua aplicação web ou site.", + "centered_modal_overlay_color": "Cor da sobreposição modal centralizada", + "email_customization": "Personalização de E-mail", + "email_customization_description": "Altere a aparência e a sensação dos e-mails que o Formbricks envia em seu nome.", + "enable_custom_styling": "Ativar estilo personalizado", + "enable_custom_styling_description": "Permitir que os utilizadores substituam este tema no editor de inquéritos.", + "failed_to_remove_logo": "Falha ao remover o logótipo", + "failed_to_update_logo": "Falha ao atualizar o logótipo", + "formbricks_branding": "Marca Formbricks", + "formbricks_branding_hidden": "Marca Formbricks está oculta.", + "formbricks_branding_settings_description": "Adoramos o seu apoio, mas compreendemos se o desativar.", + "formbricks_branding_shown": "Marca Formbricks está visível.", + "logo_removed_successfully": "Logótipo removido com sucesso", + "logo_settings_description": "Carregue o logótipo da sua empresa para personalizar inquéritos e pré-visualizações de links.", + "logo_updated_successfully": "Logótipo atualizado com sucesso", + "logo_upload_failed": "Falha no carregamento do logótipo. Por favor, tente novamente.", + "placement_updated_successfully": "Posicionamento atualizado com sucesso", + "remove_branding_with_a_higher_plan": "Remova a marca com um plano superior", + "remove_logo": "Remover Logótipo", + "remove_logo_confirmation": "Tem a certeza de que quer remover o logótipo?", + "replace_logo": "Substituir Logotipo", + "reset_styling": "Repor estilo", + "reset_styling_confirmation": "Tem a certeza de que deseja repor o estilo para o padrão?", + "show_formbricks_branding_in": "Mostrar Marca Formbricks em inquéritos {type}", + "show_powered_by_formbricks": "Mostrar assinatura 'Desenvolvido por Formbricks'", + "styling_updated_successfully": "Estilo atualizado com sucesso", + "theme": "Tema", + "theme_settings_description": "Criar um tema de estilo para todos os inquéritos. Pode ativar o estilo personalizado para cada inquérito." + }, + "tags": { + "add": "Adicionar", + "add_tag": "Adicionar Etiqueta", + "count": "Contagem", + "delete_tag_confirmation": "Tem a certeza de que deseja eliminar esta etiqueta?", + "empty_message": "Etiqueta uma submissão para encontrar a tua lista de etiquetas aqui.", + "manage_tags": "Gerir Etiquetas", + "manage_tags_description": "Fundir e remover etiquetas de resposta.", + "merge": "Fundir", + "no_tag_found": "Nenhuma etiqueta encontrada", + "search_tags": "Procurar Etiquetas...", + "tag": "Etiqueta", + "tag_already_exists": "A etiqueta já existe", + "tag_deleted": "Etiqueta eliminada", + "tag_updated": "Etiqueta atualizada", + "tags_merged": "Etiquetas fundidas", + "unique_constraint_failed_on_the_fields": "A restrição de unicidade falhou nos campos" + }, + "teams": { + "manage_teams": "Gerir equipas", + "no_teams_found": "Nenhuma equipa encontrada", + "only_organization_owners_and_managers_can_manage_teams": "Apenas os proprietários e gestores da organização podem gerir equipas.", + "permission": "Permissão", + "team_name": "Nome da Equipa", + "team_settings_description": "Veja quais equipas podem aceder a este projeto." + } + }, + "projects_environments_organizations_not_found": "Projetos, ambientes ou organizações não encontrados", + "segments": { + "add_filter_below": "Adicionar filtro abaixo", + "add_your_first_filter_to_get_started": "Adicione o seu primeiro filtro para começar", + "cannot_delete_segment_used_in_surveys": "Não pode eliminar este segmento, pois ainda é utilizado nestes questionários:", + "clone_and_edit_segment": "Clonar e Editar Segmento", + "create_group": "Criar grupo", + "create_your_first_segment": "Crie o seu primeiro segmento para começar", + "delete_segment": "Eliminar Segmento", + "desktop": "Ambiente de Trabalho", + "devices": "Dispositivos", + "edit_segment": "Editar Segmento", + "error_resetting_filters": "Erro ao repor os filtros", + "error_saving_segment": "Erro ao guardar segmento", + "ex_fully_activated_recurring_users": "Ex. Utilizadores recorrentes totalmente ativados", + "ex_power_users": "Ex. Utilizadores avançados", + "filters_reset_successfully": "Filtros redefinidos com sucesso", + "here": "aqui", + "hide_filters": "Ocultar filtros", + "identifying_users": "identificar utilizadores", + "invalid_segment": "Segmento inválido", + "invalid_segment_filters": "Filtros inválidos. Por favor, verifique os filtros e tente novamente.", + "load_segment": "Carregar Segmento", + "most_active_users_in_the_last_30_days": "Utilizadores mais ativos nos últimos 30 dias", + "no_attributes_yet": "Ainda não há atributos!", + "no_filters_yet": "Ainda não há filtros!", + "no_segments_yet": "Atualmente, não tem segmentos guardados.", + "person_and_attributes": "Pessoa e Atributos", + "phone": "Telefone", + "please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Por favor, remova o segmento destes questionários para o eliminar.", + "pre_segment_users": "Pré-segmentar os seus utilizadores com filtros de atributos.", + "remove_all_filters": "Remover todos os filtros", + "reset_all_filters": "Repor todos os filtros", + "save_as_new_segment": "Guardar como novo segmento", + "save_your_filters_as_a_segment_to_use_it_in_other_surveys": "Guarde os seus filtros como um Segmento para usá-los noutros questionários", + "segment_created_successfully": "Segmento criado com sucesso!", + "segment_deleted_successfully": "Segmento eliminado com sucesso!", + "segment_id": "ID do Segmento", + "segment_saved_successfully": "Segmento guardado com sucesso", + "segment_updated_successfully": "Segmento atualizado com sucesso!", + "segments_help_you_target_users_with_same_characteristics_easily": "Os segmentos ajudam-no a direcionar utilizadores com as mesmas características facilmente", + "target_audience": "Público-alvo", + "this_action_resets_all_filters_in_this_survey": "Esta ação redefine todos os filtros nesta pesquisa.", + "this_segment_is_used_in_other_surveys": "Este segmento é utilizado noutros questionários. Faça alterações", + "title_is_required": "Título é obrigatório.", + "unknown_filter_type": "Tipo de filtro desconhecido", + "unlock_segments_description": "Organize contactos em segmentos para direcionar grupos de utilizadores específicos", + "unlock_segments_title": "Desbloqueie os segmentos com um plano superior", + "user_targeting_is_currently_only_available_when": "A segmentação de utilizadores está atualmente disponível apenas quando", + "value_cannot_be_empty": "O valor não pode estar vazio.", + "value_must_be_a_number": "O valor deve ser um número.", + "view_filters": "Ver filtros", + "where": "Onde", + "with_the_formbricks_sdk": "com o SDK Formbricks" + }, + "settings": { + "api_keys": { + "add_api_key": "Adicionar chave API", + "add_permission": "Adicionar permissão", + "api_keys_description": "Gerir chaves API para aceder às APIs de gestão do Formbricks" + }, + "billing": { + "10000_monthly_responses": "10000 Respostas Mensais", + "1500_monthly_responses": "1500 Respostas Mensais", + "2000_monthly_identified_users": "2000 Utilizadores Identificados Mensalmente", + "30000_monthly_identified_users": "30000 Utilizadores Identificados Mensalmente", + "3_projects": "3 Projetos", + "5000_monthly_responses": "5000 Respostas Mensais", + "5_projects": "5 Projetos", + "7500_monthly_identified_users": "7500 Utilizadores Identificados Mensalmente", + "advanced_targeting": "Segmentação Avançada", + "all_integrations": "Todas as Integrações", + "all_surveying_features": "Todas as funcionalidades de inquérito", + "annually": "Anualmente", + "api_webhooks": "API e Webhooks", + "app_surveys": "Inquéritos da Aplicação", + "contact_us": "Contacte-nos", + "current": "Atual", + "current_plan": "Plano Atual", + "current_tier_limit": "Limite Atual do Nível", + "custom_miu_limit": "Limite MIU Personalizado", + "custom_project_limit": "Limite de Projeto Personalizado", + "customer_success_manager": "Gestor de Sucesso do Cliente", + "email_embedded_surveys": "Inquéritos Incorporados no Email", + "email_support": "Suporte por Email", + "enterprise": "Empresa", + "enterprise_description": "Suporte premium e limites personalizados.", + "everybody_has_the_free_plan_by_default": "Todos têm o plano gratuito por defeito!", + "everything_in_free": "Tudo em Gratuito", + "everything_in_scale": "Tudo em Escala", + "everything_in_startup": "Tudo em Startup", + "free": "Grátis", + "free_description": "Inquéritos ilimitados, membros da equipa e mais.", + "get_2_months_free": "Obtenha 2 meses grátis", + "get_in_touch": "Entre em contacto", + "link_surveys": "Ligar Inquéritos (Partilhável)", + "logic_jumps_hidden_fields_recurring_surveys": "Saltos Lógicos, Campos Ocultos, Inquéritos Recorrentes, etc.", + "manage_card_details": "Gerir Detalhes do Cartão", + "manage_subscription": "Gerir Subscrição", + "monthly": "Mensal", + "monthly_identified_users": "Utilizadores Identificados Mensalmente", + "multi_language_surveys": "Inquéritos Multilingues", + "per_month": "por mês", + "per_year": "por ano", + "plan_upgraded_successfully": "Plano atualizado com sucesso", + "premium_support_with_slas": "Suporte premium com SLAs", + "priority_support": "Suporte Prioritário", + "remove_branding": "Remover Marca", + "say_hi": "Diga Olá!", + "scale": "Escala", + "scale_description": "Funcionalidades avançadas para escalar o seu negócio.", + "startup": "Inicialização", + "startup_description": "Tudo no plano Gratuito com funcionalidades adicionais.", + "switch_plan": "Mudar Plano", + "switch_plan_confirmation_text": "Tem a certeza de que deseja mudar para o plano {plan}? Ser-lhe-á cobrado {price} {period}.", + "team_access_roles": "Funções de Acesso da Equipa", + "technical_onboarding": "Integração Técnica", + "unable_to_upgrade_plan": "Não é possível atualizar o plano", + "unlimited_apps_websites": "Aplicações e Websites Ilimitados", + "unlimited_miu": "MIU Ilimitado", + "unlimited_projects": "Projetos Ilimitados", + "unlimited_responses": "Respostas Ilimitadas", + "unlimited_surveys": "Inquéritos Ilimitados", + "unlimited_team_members": "Membros da Equipa Ilimitados", + "upgrade": "Atualizar", + "uptime_sla_99": "SLA de Tempo de Atividade (99%)", + "website_surveys": "Inquéritos do Website" + }, + "enterprise": { + "ai": "Análise de IA", + "audit_logs": "Registos de Auditoria", + "coming_soon": "Em breve", + "contacts_and_segments": "Gestão de contactos e segmentos", + "enterprise_features": "Funcionalidades da Empresa", + "get_an_enterprise_license_to_get_access_to_all_features": "Obtenha uma licença Enterprise para ter acesso a todas as funcionalidades.", + "keep_full_control_over_your_data_privacy_and_security": "Mantenha controlo total sobre a privacidade e segurança dos seus dados.", + "no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sem necessidade de chamada, sem compromissos: Solicite uma licença de teste gratuita de 30 dias para testar todas as funcionalidades preenchendo este formulário:", + "no_credit_card_no_sales_call_just_test_it": "Sem cartão de crédito. Sem chamada de vendas. Apenas teste :)", + "on_request": "A pedido", + "organization_roles": "Funções da Organização (Administrador, Editor, Programador, etc.)", + "questions_please_reach_out_to": "Questões? Por favor entre em contacto com", + "request_30_day_trial_license": "Solicitar Licença de Teste de 30 Dias", + "saml_sso": "SSO SAML", + "service_level_agreement": "Acordo de Nível de Serviço", + "soc2_hipaa_iso_27001_compliance_check": "Verificação de conformidade SOC2, HIPAA, ISO 27001", + "sso": "SSO (Google, Microsoft, OpenID Connect)", + "teams": "Equipas e Funções de Acesso (Ler, Ler e Escrever, Gerir)", + "unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias.", + "your_enterprise_license_is_active_all_features_unlocked": "A sua licença Enterprise está ativa. Todas as funcionalidades desbloqueadas." + }, + "general": { + "bulk_invite_warning_description": "No plano gratuito, todos os membros da organização são sempre atribuídos ao papel de \"Proprietário\".", + "cannot_delete_only_organization": "Esta é a sua única organização, não pode ser eliminada. Crie uma nova organização primeiro.", + "cannot_leave_only_organization": "Não pode sair desta organização, pois é a sua única organização. Crie uma nova organização primeiro.", + "copy_invite_link_to_clipboard": "Copiar link de convite para a área de transferência", + "create_new_organization": "Criar nova organização", + "create_new_organization_description": "Crie uma nova organização para gerir um conjunto diferente de projetos.", + "customize_email_with_a_higher_plan": "Personalize o e-mail com um plano superior", + "delete_organization": "Eliminar Organização", + "delete_organization_description": "Eliminar organização com todos os seus projetos, incluindo todos os inquéritos, respostas, pessoas, ações e atributos", + "delete_organization_warning": "Antes de prosseguir com a eliminação desta organização, esteja ciente das seguintes consequências:", + "delete_organization_warning_1": "Remoção permanente de todos os projetos ligados a esta organização.", + "delete_organization_warning_2": "Esta ação não pode ser desfeita. Se for eliminada, está eliminada.", + "delete_organization_warning_3": "Por favor, insira {organizationName} no campo seguinte para confirmar a eliminação definitiva desta organização:", + "eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.", + "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.", + "enable_formbricks_ai": "Ativar Formbricks IA", + "error_deleting_organization_please_try_again": "Erro ao eliminar a organização. Por favor, tente novamente.", + "formbricks_ai": "Formbricks IA", + "formbricks_ai_description": "Obtenha informações personalizadas das suas respostas aos inquéritos com o Formbricks IA", + "formbricks_ai_disable_success_message": "Formbricks AI desativado com sucesso.", + "formbricks_ai_enable_success_message": "Formbricks IA ativado com sucesso.", + "formbricks_ai_privacy_policy_text": "Ao ativar o Formbricks AI, você concorda com a atualização", + "from_your_organization": "da sua organização", + "invitation_sent_once_more": "Convite enviado mais uma vez.", + "invite_deleted_successfully": "Convite eliminado com sucesso", + "invited_on": "Convidado em {date}", + "invites_failed": "Convites falharam", + "leave_organization": "Sair da organização", + "leave_organization_description": "Vai sair desta organização e perder o acesso a todos os inquéritos e respostas. Só pode voltar a juntar-se se for convidado novamente.", + "leave_organization_ok_btn_text": "Sim, sair da organização", + "leave_organization_title": "Tem a certeza?", + "logo_in_email_header": "Logotipo no cabeçalho do e-mail", + "logo_removed_successfully": "Logótipo removido com sucesso", + "logo_saved_successfully": "Logótipo guardado com sucesso", + "manage_members": "Gerir membros", + "manage_members_description": "Adicionar ou remover membros na sua organização.", + "member_deleted_successfully": "Membro eliminado com sucesso", + "member_invited_successfully": "Membro convidado com sucesso", + "once_its_gone_its_gone": "Uma vez que se vai, já era.", + "only_org_owner_can_perform_action": "Apenas os proprietários da organização podem aceder a esta configuração.", + "organization_created_successfully": "Organização criada com sucesso!", + "organization_deleted_successfully": "Organização eliminada com sucesso.", + "organization_invite_link_ready": "O link de convite da sua organização está pronto!", + "organization_name": "Nome da Organização", + "organization_name_description": "Dê à sua organização um nome descritivo.", + "organization_name_placeholder": "por exemplo, Power Puff Girls", + "organization_name_updated_successfully": "Nome da organização atualizado com sucesso", + "organization_settings": "Configurações da organização", + "please_add_a_logo": "Por favor, adicione um logótipo", + "please_check_csv_file": "Por favor, verifique o ficheiro CSV e certifique-se de que está de acordo com o nosso formato", + "please_save_logo_before_sending_test_email": "Por favor, guarde o logótipo antes de enviar um email de teste.", + "remove_logo": "Remover logótipo", + "replace_logo": "Substituir logotipo", + "resend_invitation_email": "Reenviar Email de Convite", + "share_invite_link": "Partilhar Link de Convite", + "share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:", + "test_email_sent_successfully": "Email de teste enviado com sucesso", + "use_multi_language_surveys_with_a_higher_plan": "Use inquéritos multilingues com um plano superior", + "use_multi_language_surveys_with_a_higher_plan_description": "Inquira os seus utilizadores em diferentes idiomas." + }, + "notifications": { + "auto_subscribe_to_new_surveys": "Subscrever automaticamente a novos inquéritos", + "email_alerts_surveys": "Alertas por email (Inquéritos)", + "every_response": "Cada resposta", + "every_response_tooltip": "Envia respostas completas, sem parciais.", + "need_slack_or_discord_notifications": "Precisa de notificações do Slack ou Discord", + "notification_settings_updated": "Definições de notificações atualizadas", + "set_up_an_alert_to_get_an_email_on_new_responses": "Configurar um alerta para receber um e-mail sobre novas respostas", + "stay_up_to_date_with_a_Weekly_every_Monday": "Mantenha-se atualizado com um Resumo semanal todas as segundas-feiras", + "use_the_integration": "Use a integração", + "want_to_loop_in_organization_mates": "Quer incluir colegas da organização", + "weekly_summary_projects": "Resumo semanal (Projetos)", + "you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore": "Já não será automaticamente subscrito aos inquéritos desta organização!", + "you_will_not_receive_any_more_emails_for_responses_on_this_survey": "Não receberá mais emails para respostas a este inquérito!" + }, + "profile": { + "account_deletion_consequences_warning": "Consequências da eliminação da conta", + "avatar_update_failed": "Falha na atualização do avatar. Por favor, tente novamente.", + "backup_code": "Código de Backup", + "change_image": "Alterar imagem", + "confirm_delete_account": "Eliminar a sua conta com todas as suas informações e dados pessoais", + "confirm_delete_my_account": "Eliminar a Minha Conta", + "confirm_your_current_password_to_get_started": "Confirme a sua palavra-passe atual para começar.", + "delete_account": "Eliminar Conta", + "disable_two_factor_authentication": "Desativar autenticação de dois fatores", + "disable_two_factor_authentication_description": "Se precisar de desativar a autenticação de dois fatores, recomendamos que a reative o mais rapidamente possível.", + "each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Cada código de backup pode ser usado exatamente uma vez para conceder acesso sem o seu autenticador.", + "enable_two_factor_authentication": "Ativar autenticação de dois fatores", + "enter_the_code_from_your_authenticator_app_below": "Introduza o código da sua aplicação de autenticação abaixo.", + "file_size_must_be_less_than_10mb": "O tamanho do ficheiro deve ser inferior a 10MB.", + "invalid_file_type": "Tipo de ficheiro inválido. Apenas são permitidos ficheiros JPEG, PNG e WEBP.", + "lost_access": "Perdeu o acesso", + "or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:", + "organization_identification": "Ajude a sua organização a identificá-lo no Formbricks", + "organizations_delete_message": "É o único proprietário destas organizações, por isso também serão eliminadas.", + "permanent_removal_of_all_of_your_personal_information_and_data": "Remoção permanente de todas as suas informações e dados pessoais", + "personal_information": "Informações pessoais", + "please_enter_email_to_confirm_account_deletion": "Por favor, insira {email} no campo seguinte para confirmar a eliminação definitiva da sua conta:", + "profile_updated_successfully": "O seu perfil foi atualizado com sucesso", + "remove_image": "Remover imagem", + "save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup num local seguro.", + "scan_the_qr_code_below_with_your_authenticator_app": "Digitalize o código QR abaixo com a sua aplicação de autenticação.", + "security_description": "Gerir a sua palavra-passe e outras definições de segurança, como a autenticação de dois fatores (2FA).", + "two_factor_authentication": "Autenticação de dois fatores", + "two_factor_authentication_description": "Adicione uma camada extra de segurança à sua conta caso a sua palavra-passe seja roubada.", + "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autenticação de dois fatores ativada. Introduza o código de seis dígitos da sua aplicação de autenticação.", + "two_factor_code": "Código de Dois Fatores", + "unlock_two_factor_authentication": "Desbloqueie a autenticação de dois fatores com um plano superior", + "update_personal_info": "Atualize as suas informações pessoais", + "upload_image": "Carregar imagem", + "warning_cannot_delete_account": "É o único proprietário desta organização. Transfira a propriedade para outro membro primeiro.", + "warning_cannot_undo": "Isto não pode ser desfeito", + "you_must_select_a_file": "Deve selecionar um ficheiro." + }, + "teams": { + "add_members_description": "Adicionar membros à equipa e determinar o seu papel.", + "add_projects_description": "Controla a que projetos os membros da equipa podem aceder.", + "all_members_added": "Todos os membros adicionados a esta equipa.", + "all_projects_added": "Todos os projetos adicionados a esta equipa.", + "are_you_sure_you_want_to_delete_this_team": "Tem a certeza de que deseja eliminar esta equipa? Isto também remove o acesso a todos os projetos e inquéritos associados a esta equipa.", + "billing_role_description": "Apenas tem acesso a informações de faturação.", + "bulk_invite": "Convite em Massa", + "contributor": "Contribuidor", + "create": "Criar", + "create_first_team_message": "Primeiro, precisa de criar uma equipa.", + "create_new_team": "Criar nova equipa", + "delete_team": "Eliminar equipa", + "empty_teams_state": "Crie a sua primeira equipa.", + "enter_team_name": "Introduza o nome da equipa", + "individual": "Individual", + "invite_member": "Convidar membro", + "invite_member_description": "Adicione os seus colegas a esta organização.", + "manage": "Gerir", + "manage_team": "Gerir equipa", + "manage_team_disabled": "Apenas os proprietários da organização, gestores e administradores de equipa podem gerir equipas.", + "manager_role_description": "Os gestores podem aceder a todos os projetos e adicionar e remover membros.", + "member_role_description": "Os membros podem trabalhar em projetos selecionados.", + "member_role_info_message": "Para dar acesso a novos membros a um projeto, por favor adicione-os a uma Equipa abaixo. Com Equipas pode gerir quem tem acesso a que projeto.", + "owner_role_description": "Os proprietários têm controlo total sobre a organização.", + "please_fill_all_member_fields": "Por favor, preencha todos os campos para adicionar um novo membro.", + "please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.", + "read": "Ler", + "read_write": "Ler e Escrever", + "team_admin": "Administrador da Equipa", + "team_created_successfully": "Equipa criada com sucesso.", + "team_deleted_successfully": "Equipa eliminada com sucesso.", + "team_deletion_not_allowed": "Não tem permissão para eliminar esta equipa.", + "team_name": "Nome da Equipa", + "team_name_settings_title": "Definições de {teamName}", + "team_select_placeholder": "Pesquisar nome da equipa...", + "team_settings_description": "Gerir membros da equipa, direitos de acesso e mais.", + "team_updated_successfully": "Equipa atualizada com sucesso", + "teams": "Equipas", + "teams_description": "Atribua membros às equipas e dê acesso a projetos às equipas.", + "unlock_teams_description": "Gerir quais os membros da organização que têm acesso a projetos e inquéritos específicos.", + "unlock_teams_title": "Desbloqueie as Equipas com um plano superior", + "upgrade_plan_notice_message": "Desbloqueie as Funções da Organização com um plano superior", + "you_are_a_member": "És um membro" + } + }, + "surveys": { + "all_set_time_to_create_first_survey": "Está tudo pronto! Hora de criar o seu primeiro inquérito", + "alphabetical": "Alfabética", + "copy_survey": "Copiar inquérito", + "copy_survey_description": "Copiar este questionário para outro ambiente", + "copy_survey_error": "Falha ao copiar inquérito", + "copy_survey_link_to_clipboard": "Copiar link do inquérito para a área de transferência", + "copy_survey_success": "Inquérito copiado com sucesso!", + "delete_survey_and_responses_warning": "Tem a certeza de que deseja eliminar este inquérito e todas as suas respostas? Esta ação não pode ser desfeita.", + "edit": { + "1_choose_the_default_language_for_this_survey": "1. Escolha o idioma padrão para este inquérito:", + "2_activate_translation_for_specific_languages": "2. Ativar tradução para idiomas específicos:", + "add": "Adicionar +", + "add_a_delay_or_auto_close_the_survey": "Adicionar um atraso ou fechar automaticamente o inquérito", + "add_a_four_digit_pin": "Adicione um PIN de quatro dígitos", + "add_a_new_question_to_your_survey": "Adicionar uma nova pergunta ao seu inquérito", + "add_a_variable_to_calculate": "Adicionar uma variável para calcular", + "add_action_below": "Adicionar ação abaixo", + "add_choice_below": "Adicionar escolha abaixo", + "add_color_coding": "Adicionar codificação de cores", + "add_color_coding_description": "Adicionar códigos de cores vermelho, laranja e verde às opções.", + "add_column": "Adicionar coluna", + "add_condition_below": "Adicionar condição abaixo", + "add_custom_styles": "Adicionar estilos personalizados", + "add_delay_before_showing_survey": "Adicionar atraso antes de mostrar o inquérito", + "add_description": "Adicionar descrição", + "add_ending": "Adicionar encerramento", + "add_ending_below": "Adicionar encerramento abaixo", + "add_hidden_field_id": "Adicionar ID do campo oculto", + "add_highlight_border": "Adicionar borda de destaque", + "add_highlight_border_description": "Adicione uma borda externa ao seu cartão de inquérito.", + "add_logic": "Adicionar lógica", + "add_option": "Adicionar opção", + "add_other": "Adicionar \"Outro\"", + "add_photo_or_video": "Adicionar foto ou vídeo", + "add_pin": "Adicionar PIN", + "add_question": "Adicionar pergunta", + "add_question_below": "Adicionar pergunta abaixo", + "add_row": "Adicionar linha", + "add_variable": "Adicionar variável", + "address_fields": "Campos de Endereço", + "address_line_1": "Endereço Linha 1", + "address_line_2": "Endereço Linha 2", + "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_other_answers_will_continue_to": "Todas as outras respostas continuarão a", + "allow_file_type": "Permitir tipo de ficheiro", + "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", + "always_show_survey": "Mostrar sempre o inquérito", + "and_launch_surveys_in_your_website_or_app": "e lance inquéritos no seu site ou aplicação.", + "animation": "Animação", + "app_survey_description": "Incorpore um inquérito na sua aplicação web ou site para recolher respostas.", + "assign": "Atribuir =", + "audience": "Público", + "auto_close_on_inactivity": "Fechar automaticamente por inatividade", + "automatically_close_survey_after": "Fechar automaticamente o inquérito após", + "automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente o inquérito após um certo número de respostas", + "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fechar automaticamente o inquérito se o utilizador não responder após um certo número de segundos.", + "automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Encerrar automaticamente o inquérito no início do dia (UTC).", + "automatically_mark_the_survey_as_complete_after": "Marcar automaticamente o inquérito como concluído após", + "automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Lançar automaticamente o inquérito no início do dia (UTC).", + "back_button_label": "Rótulo do botão \"Voltar\"", + "background_styling": "Estilo de Fundo", + "blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "Bloqueia o inquérito se já existir uma submissão com o Id de Uso Único (suId).", + "blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "Bloqueia o inquérito se o URL do inquérito não tiver um Id de Uso Único (suId).", + "brand_color": "Cor da marca", + "brightness": "Brilho", + "button_label": "Rótulo do botão", + "button_to_continue_in_survey": "Botão para continuar na pesquisa", + "button_to_link_to_external_url": "Botão para ligar a URL externa", + "button_url": "URL do botão", + "cal_username": "Nome de utilizador do Cal.com ou nome de utilizador/evento", + "calculate": "Calcular", + "capture_a_new_action_to_trigger_a_survey_on": "Capturar uma nova ação para desencadear um inquérito.", + "capture_new_action": "Capturar nova ação", + "card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Inquéritos {surveyTypeDerived}", + "card_background_color": "Cor de fundo do cartão", + "card_border_color": "Cor da borda do cartão", + "card_shadow_color": "Cor da sombra do cartão", + "card_styling": "Estilo do cartão", + "casual": "Casual", + "caution_text": "As alterações levarão a inconsistências", + "centered_modal_overlay_color": "Cor da sobreposição modal centralizada", + "change_anyway": "Alterar mesmo assim", + "change_background": "Alterar fundo", + "change_question_type": "Alterar tipo de pergunta", + "change_the_background_color_of_the_card": "Alterar a cor de fundo do cartão", + "change_the_background_color_of_the_input_fields": "Alterar a cor de fundo dos campos de entrada", + "change_the_background_to_a_color_image_or_animation": "Altere o fundo para uma cor, imagem ou animação", + "change_the_border_color_of_the_card": "Alterar a cor da borda do cartão.", + "change_the_border_color_of_the_input_fields": "Alterar a cor da borda dos campos de entrada", + "change_the_border_radius_of_the_card_and_the_inputs": "Alterar o raio da borda do cartão e dos campos de entrada", + "change_the_brand_color_of_the_survey": "Alterar a cor da marca do inquérito", + "change_the_placement_of_this_survey": "Alterar a colocação deste inquérito.", + "change_the_question_color_of_the_survey": "Alterar a cor da pergunta do inquérito", + "change_the_shadow_color_of_the_card": "Alterar a cor da sombra do cartão.", + "changes_saved": "Alterações guardadas.", + "character_limit_toggle_description": "Limitar o quão curta ou longa uma resposta pode ser.", + "character_limit_toggle_title": "Adicionar limites de caracteres", + "checkbox_label": "Rótulo da Caixa de Seleção", + "choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.", + "choose_where_to_run_the_survey": "Escolha onde realizar o inquérito.", + "city": "Cidade", + "close_survey_on_date": "Encerrar inquérito na data", + "close_survey_on_response_limit": "Fechar inquérito no limite de respostas", + "color": "Cor", + "columns": "Colunas", + "company": "Empresa", + "company_logo": "Logotipo da empresa", + "completed_responses": "respostas concluídas", + "concat": "Concatenar +", + "conditional_logic": "Lógica Condicional", + "confirm_default_language": "Confirmar idioma padrão", + "confirm_survey_changes": "Confirmar Alterações do Inquérito", + "contact_fields": "Campos de Contacto", + "contains": "Contém", + "continue_to_settings": "Continuar para Definições", + "control_which_file_types_can_be_uploaded": "Controlar quais tipos de ficheiros podem ser carregados.", + "convert_to_multiple_choice": "Converter para Escolha Múltipla", + "convert_to_single_choice": "Converter para Escolha Única", + "country": "País", + "create_group": "Criar grupo", + "create_your_own_survey": "Crie o seu próprio inquérito", + "css_selector": "Seletor CSS", + "custom_hostname": "Nome do host personalizado", + "darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.", + "date_format": "Formato da data", + "days_before_showing_this_survey_again": "dias antes de mostrar este inquérito novamente.", + "decide_how_often_people_can_answer_this_survey": "Decida com que frequência as pessoas podem responder a este inquérito.", + "delete_choice": "Eliminar escolha", + "description": "Descrição", + "disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.", + "display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa do tempo de conclusão do inquérito", + "display_number_of_responses_for_survey": "Mostrar número de respostas do inquérito", + "divide": "Dividir /", + "does_not_contain": "Não contém", + "does_not_end_with": "Não termina com", + "does_not_equal": "Não é igual", + "does_not_include_all_of": "Não inclui todos de", + "does_not_include_one_of": "Não inclui um de", + "does_not_start_with": "Não começa com", + "edit_recall": "Editar Lembrete", + "edit_translations": "Editar traduções {lang}", + "enable_encryption_of_single_use_id_suid_in_survey_url": "Ativar encriptação do Id de Uso Único (suId) no URL do inquérito.", + "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir aos participantes mudar a língua do inquérito a qualquer momento durante o inquérito.", + "end_screen_card": "Cartão de ecrã final", + "ending_card": "Cartão de encerramento", + "ending_card_used_in_logic": "Este cartão final é usado na lógica da pergunta {questionIndex}.", + "ends_with": "Termina com", + "equals": "Igual", + "equals_one_of": "Igual a um de", + "error_publishing_survey": "Ocorreu um erro ao publicar o questionário.", + "error_saving_changes": "Erro ao guardar alterações", + "even_after_they_submitted_a_response_e_g_feedback_box": "Mesmo depois de terem enviado uma resposta (por exemplo, Caixa de Feedback)", + "everyone": "Todos", + "fallback_missing": "Substituição em falta", + "fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} é usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.", + "field_name_eg_score_price": "Nome do campo, por exemplo, pontuação, preço", + "first_name": "Primeiro Nome", + "five_points_recommended": "5 pontos (recomendado)", + "follow_ups": "Acompanhamentos", + "follow_ups_delete_modal_text": "Tem a certeza de que deseja eliminar este acompanhamento?", + "follow_ups_delete_modal_title": "Eliminar seguimento?", + "follow_ups_empty_description": "Enviar mensagens para os respondentes, para si ou para os colegas de equipa.", + "follow_ups_empty_heading": "Enviar acompanhamentos automáticos", + "follow_ups_ending_card_delete_modal_text": "Este cartão de encerramento é utilizado em seguimentos. Eliminá-lo irá removê-lo de todos os seguimentos. Tem a certeza de que deseja eliminá-lo?", + "follow_ups_ending_card_delete_modal_title": "Eliminar cartão de encerramento?", + "follow_ups_hidden_field_error": "O campo oculto é usado num seguimento. Por favor, remova-o do seguimento primeiro.", + "follow_ups_item_ending_tag": "Encerramento(s)", + "follow_ups_item_issue_detected_tag": "Problema detetado", + "follow_ups_item_response_tag": "Qualquer resposta", + "follow_ups_item_send_email_tag": "Enviar email", + "follow_ups_modal_action_attach_response_data_description": "Adicionar os dados da resposta do inquérito ao acompanhamento", + "follow_ups_modal_action_attach_response_data_label": "Anexar dados de resposta", + "follow_ups_modal_action_body_label": "Corpo", + "follow_ups_modal_action_body_placeholder": "Corpo do email", + "follow_ups_modal_action_email_content": "Conteúdo do email", + "follow_ups_modal_action_email_settings": "Configurações de email", + "follow_ups_modal_action_from_description": "Endereço de email para enviar o email de", + "follow_ups_modal_action_from_label": "De", + "follow_ups_modal_action_label": "Ação", + "follow_ups_modal_action_replyTo_description": "Se o destinatário clicar em responder, o seguinte endereço de email irá recebê-lo", + "follow_ups_modal_action_replyTo_label": "Responder A", + "follow_ups_modal_action_subject": "Obrigado pelas suas respostas!", + "follow_ups_modal_action_subject_label": "Assunto", + "follow_ups_modal_action_subject_placeholder": "Assunto do email", + "follow_ups_modal_action_to_description": "Endereço de email para enviar o email", + "follow_ups_modal_action_to_label": "Para", + "follow_ups_modal_action_to_warning": "Nenhum campo de email detetado no inquérito", + "follow_ups_modal_create_heading": "Criar um novo acompanhamento", + "follow_ups_modal_edit_heading": "Editar este acompanhamento", + "follow_ups_modal_edit_no_id": "Nenhum ID de acompanhamento do inquérito fornecido, não é possível atualizar o acompanhamento do inquérito", + "follow_ups_modal_name_label": "Nome do acompanhamento", + "follow_ups_modal_name_placeholder": "Dê um nome ao seu acompanhamento", + "follow_ups_modal_subheading": "Enviar mensagens para os respondentes, para si ou para os colegas de equipa", + "follow_ups_modal_trigger_description": "Quando deve ser acionado este acompanhamento?", + "follow_ups_modal_trigger_label": "Desencadeador", + "follow_ups_modal_trigger_type_ending": "O respondente vê um final específico", + "follow_ups_modal_trigger_type_ending_select": "Selecionar finais: ", + "follow_ups_modal_trigger_type_ending_warning": "Não foram encontrados finais no inquérito!", + "follow_ups_modal_trigger_type_response": "Respondente conclui inquérito", + "follow_ups_new": "Novo acompanhamento", + "follow_ups_upgrade_button_text": "Atualize para ativar os acompanhamentos", + "form_styling": "Estilo do formulário", + "formbricks_ai_description": "Descreva o seu inquérito e deixe a Formbricks AI criar o inquérito para si", + "formbricks_ai_generate": "Gerar", + "formbricks_ai_prompt_placeholder": "Introduza as informações do inquérito (por exemplo, tópicos principais a abordar)", + "formbricks_sdk_is_not_connected": "O SDK do Formbricks não está conectado", + "four_points": "4 pontos", + "heading": "Cabeçalho", + "hidden_field_added_successfully": "Campo oculto adicionado com sucesso", + "hide_advanced_settings": "Ocultar definições avançadas", + "hide_back_button": "Ocultar botão 'Retroceder'", + "hide_back_button_description": "Não mostrar o botão de retroceder no inquérito", + "hide_logo": "Esconder logótipo", + "hide_progress_bar": "Ocultar barra de progresso", + "hide_the_logo_in_this_specific_survey": "Ocultar o logótipo neste inquérito específico", + "hostname": "Nome do host", + "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}", + "how_it_works": "Como funciona", + "if_you_need_more_please": "Se precisar de mais, por favor", + "if_you_really_want_that_answer_ask_until_you_get_it": "Se realmente quiser essa resposta, pergunte até obtê-la.", + "ignore_waiting_time_between_surveys": "Ignorar tempo de espera entre inquéritos", + "image": "Imagem", + "includes_all_of": "Inclui todos de", + "includes_one_of": "Inclui um de", + "initial_value": "Valor inicial", + "inner_text": "Texto Interno", + "input_border_color": "Cor da borda do campo de entrada", + "input_color": "Cor do campo de entrada", + "invalid_targeting": "Segmentação inválida: Por favor, verifique os seus filtros de audiência", + "invalid_video_url_warning": "Por favor, insira um URL válido do YouTube, Vimeo ou Loom. Atualmente, não suportamos outros fornecedores de hospedagem de vídeo.", + "invalid_youtube_url": "URL do YouTube inválido", + "is_accepted": "É aceite", + "is_after": "É depois", + "is_before": "É antes", + "is_booked": "Está reservado", + "is_clicked": "É clicado", + "is_completely_submitted": "Está completamente submetido", + "is_not_set": "Não está definido", + "is_partially_submitted": "Está parcialmente submetido", + "is_set": "Está definido", + "is_skipped": "É ignorado", + "is_submitted": "Está submetido", + "jump_to_question": "Saltar para a pergunta", + "keep_current_order": "Manter ordem atual", + "keep_showing_while_conditions_match": "Continuar a mostrar enquanto as condições corresponderem", + "key": "Chave", + "last_name": "Apelido", + "let_people_upload_up_to_25_files_at_the_same_time": "Permitir que as pessoas carreguem até 25 ficheiros ao mesmo tempo.", + "limit_file_types": "Limitar tipos de ficheiros", + "limit_the_maximum_file_size": "Limitar o tamanho máximo do ficheiro", + "limit_upload_file_size_to": "Limitar tamanho do ficheiro carregado a", + "link_survey_description": "Partilhe um link para uma página de inquérito ou incorpore-o numa página web ou email.", + "link_used_message": "Link Utilizado", + "load_segment": "Carregar segmento", + "logic_error_warning": "A alteração causará erros de lógica", + "logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta", + "long_answer": "Resposta longa", + "lower_label": "Etiqueta Inferior", + "manage_languages": "Gerir Idiomas", + "max_file_size": "Tamanho máximo do ficheiro", + "max_file_size_limit_is": "O limite do tamanho máximo do ficheiro é", + "multiply": "Multiplicar *", + "needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com", + "next_button_label": "Rótulo do botão \"Seguinte\"", + "next_question": "Próxima pergunta", + "no_hidden_fields_yet_add_first_one_below": "Ainda não há campos ocultos. Adicione o primeiro abaixo.", + "no_images_found_for": "Não foram encontradas imagens para ''{query}\"", + "no_languages_found_add_first_one_to_get_started": "Nenhuma língua encontrada. Adicione a primeira para começar.", + "no_variables_yet_add_first_one_below": "Ainda não há variáveis. Adicione a primeira abaixo.", + "number": "Número", + "once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations": "Depois de definido, o idioma padrão desta pesquisa só pode ser alterado desativando a opção de vários idiomas e eliminando todas as traduções.", + "only_display_the_survey_to_a_subset_of_the_users": "Mostrar o inquérito apenas a um subconjunto dos utilizadores", + "only_lower_case_letters_numbers_and_underscores_are_allowed": "Apenas letras minúsculas, números e sublinhados são permitidos.", + "only_people_who_match_your_targeting_can_be_surveyed": "Apenas as pessoas que correspondem ao seu alvo podem ser inquiridas.", + "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", + "override_theme_with_individual_styles_for_this_survey": "Substituir o tema com estilos individuais para este inquérito.", + "overwrite_placement": "Substituir colocação", + "overwrite_the_global_placement_of_the_survey": "Substituir a colocação global do inquérito", + "overwrites_waiting_period_between_surveys_to_x_days": "Substitui o período de espera entre inquéritos para {days} dia(s).", + "pick_a_background_from_our_library_or_upload_your_own": "Escolha um fundo da nossa biblioteca ou carregue o seu próprio.", + "picture_idx": "Imagem {idx}", + "pin_can_only_contain_numbers": "O PIN só pode conter números.", + "pin_must_be_a_four_digit_number": "O PIN deve ser um número de quatro dígitos.", + "please_enter_a_file_extension": "Por favor, insira uma extensão de ficheiro.", + "please_set_a_survey_trigger": "Por favor, defina um desencadeador de inquérito", + "please_specify": "Por favor, especifique", + "prevent_double_submission": "Impedir submissão dupla", + "prevent_double_submission_description": "Permitir apenas 1 resposta por endereço de email", + "protect_survey_with_pin": "Proteger inquérito com um PIN", + "protect_survey_with_pin_description": "Apenas utilizadores com o PIN podem aceder ao inquérito.", + "publish": "Publicar", + "question": "Pergunta", + "question_color": "Cor da pergunta", + "question_deleted": "Pergunta eliminada.", + "question_duplicated": "Pergunta duplicada.", + "question_id_updated": "ID da pergunta atualizado", + "question_used_in_logic": "Esta pergunta é usada na lógica da pergunta {questionIndex}.", + "randomize_all": "Aleatorizar todos", + "randomize_all_except_last": "Aleatorizar todos exceto o último", + "range": "Intervalo", + "recontact_options": "Opções de Recontacto", + "redirect_thank_you_card": "Redirecionar cartão de agradecimento", + "redirect_to_url": "Redirecionar para Url", + "redirect_to_url_not_available_on_free_plan": "Redirecionar para URL não está disponível no plano gratuito", + "release_survey_on_date": "Lançar inquérito na data", + "remove_description": "Remover descrição", + "remove_translations": "Remover traduções", + "require_answer": "Exigir Resposta", + "required": "Obrigatório", + "reset_to_theme_styles": "Repor para estilos do tema", + "reset_to_theme_styles_main_text": "Tem a certeza de que deseja repor o estilo para os estilos do tema? Isto irá remover todos os estilos personalizados.", + "response_limit_can_t_be_set_to_0": "O limite de respostas não pode ser definido como 0", + "response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).", + "response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.", + "response_options": "Opções de Resposta", + "roundness": "Arredondamento", + "rows": "Linhas", + "save_and_close": "Guardar e Fechar", + "scale": "Escala", + "search_for_images": "Procurar imagens", + "seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos após o acionamento o inquérito será fechado se não houver resposta", + "seconds_before_showing_the_survey": "segundos antes de mostrar o inquérito.", + "select_or_type_value": "Selecionar ou digitar valor", + "select_ordering": "Selecionar ordem", + "select_saved_action": "Selecionar ação guardada", + "select_type": "Selecionar tipo", + "send_survey_to_audience_who_match": "Enviar inquérito para o público que corresponde...", + "send_your_respondents_to_a_page_of_your_choice": "Envie os seus respondentes para uma página à sua escolha.", + "set_the_global_placement_in_the_look_feel_settings": "Definir a colocação global nas definições de Aparência.", + "seven_points": "7 pontos", + "show_advanced_settings": "Mostrar definições avançadas", + "show_button": "Mostrar Botão", + "show_language_switch": "Mostrar alternador de idioma", + "show_multiple_times": "Mostrar várias vezes", + "show_only_once": "Mostrar apenas uma vez", + "show_survey_maximum_of": "Mostrar inquérito máximo de", + "show_survey_to_users": "Mostrar inquérito a % dos utilizadores", + "show_to_x_percentage_of_targeted_users": "Mostrar a {percentage}% dos utilizadores alvo", + "simple": "Simples", + "single_use_survey_links": "Links de inquérito de uso único", + "single_use_survey_links_description": "Permitir apenas 1 resposta por link de inquérito.", + "skip_button_label": "Rótulo do botão Ignorar", + "smiley": "Sorridente", + "star": "Estrela", + "starts_with": "Começa com", + "state": "Estado", + "straight": "Direto", + "style_the_question_texts_descriptions_and_input_fields": "Estilo dos textos das perguntas, descrições e campos de entrada.", + "style_the_survey_card": "Estilo do cartão do inquérito", + "styling_set_to_theme_styles": "Estilo definido para estilos do tema", + "subheading": "Subtítulo", + "subtract": "Subtrair -", + "suggest_colors": "Sugerir cores", + "survey_already_answered_heading": "O inquérito já foi respondido.", + "survey_already_answered_subheading": "Só pode usar este link uma vez.", + "survey_completed_heading": "Inquérito Concluído", + "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_trigger": "Desencadeador de Inquérito", + "switch_multi_lanugage_on_to_get_started": "Ative o modo multilingue para começar \uD83D\uDC49", + "targeted": "Alvo", + "ten_points": "10 pontos", + "the_survey_will_be_shown_multiple_times_until_they_respond": "O inquérito será mostrado várias vezes até que respondam", + "the_survey_will_be_shown_once_even_if_person_doesnt_respond": "O inquérito será mostrado uma vez, mesmo que a pessoa não responda.", + "then": "Então", + "this_action_will_remove_all_the_translations_from_this_survey": "Esta ação irá remover todas as traduções deste inquérito.", + "this_extension_is_already_added": "Esta extensão já está adicionada.", + "this_file_type_is_not_supported": "Este tipo de ficheiro não é suportado.", + "this_setting_overwrites_your": "Esta configuração substitui o seu", + "three_points": "3 pontos", + "times": "tempos", + "to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todos os questionários, pode", + "trigger_survey_when_one_of_the_actions_is_fired": "Desencadear inquérito quando uma das ações for disparada...", + "try_lollipop_or_mountain": "Experimente 'lollipop' ou 'mountain'...", + "type_field_id": "Escreva o id do campo", + "unlock_targeting_description": "Alvo de grupos de utilizadores específicos com base em atributos ou informações do dispositivo", + "unlock_targeting_title": "Desbloqueie a segmentação com um plano superior", + "unsaved_changes_warning": "Tem alterações não guardadas no seu inquérito. Gostaria de as guardar antes de sair?", + "until_they_submit_a_response": "Até que enviem uma resposta", + "upgrade_notice_description": "Crie inquéritos multilingues e desbloqueie muitas mais funcionalidades", + "upgrade_notice_title": "Desbloqueie inquéritos multilingues com um plano superior", + "upload": "Carregar", + "upload_at_least_2_images": "Carregue pelo menos 2 imagens", + "upper_label": "Etiqueta Superior", + "url_encryption": "Encriptação de URL", + "url_filters": "Filtros de URL", + "url_not_supported": "URL não suportado", + "use_with_caution": "Usar com cautela", + "variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.", + "variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.", + "variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.", + "verify_email_before_submission": "Verificar email antes da submissão", + "verify_email_before_submission_description": "Permitir apenas que pessoas com um email real respondam.", + "wait": "Aguardar", + "wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Aguarde alguns segundos após o gatilho antes de mostrar o inquérito", + "waiting_period": "período de espera", + "welcome_message": "Mensagem de boas-vindas", + "when": "Quando", + "when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Quando as condições corresponderem, o tempo de espera será ignorado e o inquérito será mostrado.", + "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_project_to_work_with_translations": "Precisa de ter duas ou mais línguas configuradas no seu projeto 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", + "zip": "Comprimir" + }, + "error_deleting_survey": "Ocorreu um erro ao eliminar o questionário", + "failed_to_copy_link_to_results": "Falha ao copiar link para resultados", + "failed_to_copy_url": "Falha ao copiar URL: não está num ambiente de navegador.", + "new_single_use_link_generated": "Novo link de uso único gerado", + "new_survey": "Novo inquérito", + "no_surveys_created_yet": "Ainda não foram criados questionários", + "open_options": "Abrir opções", + "preview_survey_in_a_new_tab": "Pré-visualizar inquérito num novo separador", + "read_only_user_not_allowed_to_create_survey_warning": "Como utilizador de leitura apenas, não tem permissão para criar questionários. Por favor, peça a um utilizador com acesso de escrita para criar um questionário ou a um gestor para atualizar o seu papel.", + "relevance": "Relevância", + "responses": { + "address_line_1": "Endereço Linha 1", + "address_line_2": "Endereço Linha 2", + "an_error_occurred_creating_a_new_note": "Ocorreu um erro ao criar uma nova nota", + "an_error_occurred_deleting_the_tag": "Ocorreu um erro ao eliminar a etiqueta", + "an_error_occurred_resolving_a_note": "Ocorreu um erro ao resolver uma nota", + "an_error_occurred_updating_a_note": "Ocorreu um erro ao atualizar uma nota", + "browser": "Navegador", + "city": "Cidade", + "company": "Empresa", + "completed": "Concluído ✅", + "country": "País", + "device": "Dispositivo", + "device_info": "Informações do dispositivo", + "email": "Email", + "first_name": "Primeiro Nome", + "how_to_identify_users": "Como identificar utilizadores", + "last_name": "Apelido", + "not_completed": "Não Concluído ⏳", + "os": "SO", + "person_attributes": "Atributos da pessoa", + "phone": "Telefone", + "resolve": "Resolver", + "respondent_skipped_questions": "O respondente saltou estas perguntas.", + "response_deleted_successfully": "Resposta eliminada com sucesso.", + "single_use_id": "ID de Uso Único", + "source": "Fonte", + "state_region": "Estado / Região", + "survey_closed": "Inquérito encerrado", + "tag_already_exists": "A etiqueta já existe", + "this_response_is_in_progress": "Esta resposta está em progresso.", + "zip_post_code": "Código Postal" + }, + "results_unpublished_successfully": "Resultados despublicados com sucesso.", + "search_by_survey_name": "Pesquisar por nome do inquérito", + "summary": { + "added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ", + "added_filter_for_responses_where_answer_to_question_is_skipped": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é ignorada", + "all_responses_csv": "Todas as respostas (CSV)", + "all_responses_excel": "Todas as respostas (Excel)", + "all_time": "Todo o tempo", + "almost_there": "Quase lá! Instale o widget para começar a receber respostas.", + "average": "Média", + "completed": "Concluído", + "completed_tooltip": "Número de vezes que o inquérito foi concluído.", + "configure_alerts": "Configurar alertas", + "congrats": "Parabéns! O seu inquérito está ativo.", + "connect_your_website_or_app_with_formbricks_to_get_started": "Ligue o seu website ou aplicação ao Formbricks para começar.", + "copy_link_to_public_results": "Copiar link para resultados públicos", + "create_single_use_links": "Criar links de uso único", + "create_single_use_links_description": "Aceitar apenas uma submissão por link. Aqui está como.", + "custom_range": "Intervalo personalizado...", + "data_prefilling": "Pré-preenchimento de dados", + "data_prefilling_description": "Quer pré-preencher alguns campos no inquérito? Aqui está como.", + "define_when_and_where_the_survey_should_pop_up": "Defina quando e onde o inquérito deve aparecer", + "drop_offs": "Desistências", + "drop_offs_tooltip": "Número de vezes que o inquérito foi iniciado mas não concluído.", + "dynamic_popup": "Dinâmico (Pop-up)", + "email_sent": "Email enviado!", + "embed_code_copied_to_clipboard": "Código incorporado copiado para a área de transferência!", + "embed_in_an_email": "Incorporar num email", + "embed_in_app": "Incorporar na aplicação", + "embed_mode": "Modo de Incorporação", + "embed_mode_description": "Incorpore o seu inquérito com um design minimalista, descartando o preenchimento e o fundo.", + "embed_on_website": "Incorporar no site", + "embed_pop_up_survey_title": "Como incorporar um questionário pop-up no seu site", + "embed_survey": "Incorporar inquérito", + "enable_ai_insights_banner_button": "Ativar insights", + "enable_ai_insights_banner_description": "Pode ativar a nova funcionalidade de insights para o inquérito para obter insights baseados em IA para as suas respostas de texto aberto.", + "enable_ai_insights_banner_success": "A gerar insights para este inquérito. Por favor, volte a verificar dentro de alguns minutos.", + "enable_ai_insights_banner_title": "Pronto para testar as perceções de IA?", + "enable_ai_insights_banner_tooltip": "Por favor, contacte-nos em hola@formbricks.com para gerar insights para este inquérito", + "failed_to_copy_link": "Falha ao copiar link", + "filter_added_successfully": "Filtro adicionado com sucesso", + "filter_updated_successfully": "Filtro atualizado com sucesso", + "filtered_responses_csv": "Respostas filtradas (CSV)", + "filtered_responses_excel": "Respostas filtradas (Excel)", + "formbricks_email_survey_preview": "Pré-visualização da Pesquisa de E-mail do Formbricks", + "go_to_setup_checklist": "Ir para a Lista de Verificação de Configuração \uD83D\uDC49", + "hide_embed_code": "Ocultar código de incorporação", + "how_to_create_a_panel": "Como criar um painel", + "how_to_create_a_panel_step_1": "Passo 1: Crie uma conta com a Prolific", + "how_to_create_a_panel_step_1_description": "Crie uma conta no Prolific e verifique o seu endereço de email.", + "how_to_create_a_panel_step_2": "Passo 2: Criar um estudo", + "how_to_create_a_panel_step_2_description": "No Prolific, cria um novo estudo onde pode escolher o seu público preferido com base em centenas de características.", + "how_to_create_a_panel_step_3": "Passo 3: Conecte o seu inquérito", + "how_to_create_a_panel_step_3_description": "Configure campos ocultos no seu inquérito Formbricks para rastrear qual participante forneceu qual resposta.", + "how_to_create_a_panel_step_4": "Passo 4: Lançar o seu estudo", + "how_to_create_a_panel_step_4_description": "Depois de tudo configurado, pode lançar o seu estudo. Dentro de algumas horas, receberá as primeiras respostas.", + "impressions": "Impressões", + "impressions_tooltip": "Número de vezes que o inquérito foi visualizado.", + "includes_all": "Inclui tudo", + "includes_either": "Inclui qualquer um", + "insights_disabled": "Informações desativadas", + "install_widget": "Instalar Widget Formbricks", + "is_equal_to": "É igual a", + "is_less_than": "É menos que", + "last_30_days": "Últimos 30 dias", + "last_6_months": "Últimos 6 meses", + "last_7_days": "Últimos 7 dias", + "last_month": "Último mês", + "last_quarter": "Último trimestre", + "last_year": "Ano passado", + "link_to_public_results_copied": "Link para resultados públicos copiado", + "make_sure_the_survey_type_is_set_to": "Certifique-se de que o tipo de inquérito está definido para", + "mobile_app": "Aplicação móvel", + "no_response_matches_filter": "Nenhuma resposta corresponde ao seu filtro", + "only_completed": "Apenas concluído", + "other_values_found": "Outros valores encontrados", + "overall": "Geral", + "publish_to_web": "Publicar na web", + "publish_to_web_warning": "Está prestes a divulgar estes resultados do inquérito ao público.", + "publish_to_web_warning_description": "Os resultados do seu inquérito serão públicos. Qualquer pessoa fora da sua organização pode aceder a eles se tiver o link.", + "quickstart_mobile_apps": "Início rápido: Aplicações móveis", + "quickstart_mobile_apps_description": "Para começar com inquéritos em aplicações móveis, por favor, siga o guia de início rápido:", + "quickstart_web_apps": "Início rápido: Aplicações web", + "quickstart_web_apps_description": "Por favor, siga o guia de início rápido para começar:", + "results_are_public": "Os resultados são públicos", + "send_preview": "Enviar pré-visualização", + "send_to_panel": "Enviar para painel", + "setup_instructions": "Instruções de configuração", + "setup_integrations": "Configurar integrações", + "share_results": "Partilhar resultados", + "share_the_link": "Partilhar o link", + "share_the_link_to_get_responses": "Partilhe o link para obter respostas", + "show_all_responses_that_match": "Mostrar todas as respostas que correspondem", + "show_all_responses_where": "Mostrar todas as respostas onde...", + "single_use_links": "Links de uso único", + "source_tracking": "Rastreamento de origem", + "source_tracking_description": "Execute o rastreamento de origem em conformidade com o GDPR e o CCPA sem ferramentas adicionais.", + "starts": "Começa", + "starts_tooltip": "Número de vezes que o inquérito foi iniciado.", + "static_iframe": "Estático (iframe)", + "survey_results_are_public": "Os resultados do seu inquérito são públicos!", + "survey_results_are_shared_with_anyone_who_has_the_link": "Os resultados do seu inquérito são partilhados com qualquer pessoa que tenha o link. Os resultados não serão indexados pelos motores de busca.", + "this_month": "Este mês", + "this_quarter": "Este trimestre", + "this_year": "Este ano", + "time_to_complete": "Tempo para Concluir", + "to_connect_your_website_with_formbricks": "para ligar o seu website ao Formbricks", + "ttc_tooltip": "Tempo médio para concluir o inquérito.", + "unknown_question_type": "Tipo de Pergunta Desconhecido", + "unpublish_from_web": "Despublicar da web", + "unsupported_video_tag_warning": "O seu navegador não suporta a tag de vídeo.", + "view_embed_code": "Ver código de incorporação", + "view_embed_code_for_email": "Ver código de incorporação para email", + "view_site": "Ver site", + "waiting_for_response": "A aguardar uma resposta \uD83E\uDDD8‍♂️", + "web_app": "Aplicação web", + "what_is_a_panel": "O que é um painel?", + "what_is_a_panel_answer": "Um painel é um grupo de participantes selecionados com base em características como idade, profissão, género, etc.", + "what_is_prolific": "O que é o Prolific?", + "what_is_prolific_answer": "Estamos a colaborar com a Prolific para lhe dar acesso a um grupo de mais de 200.000 participantes verificados.", + "whats_next": "O que se segue?", + "when_do_i_need_it": "Quando é que preciso disso?", + "when_do_i_need_it_answer": "Se não tiver acesso a pessoas suficientes que correspondam ao seu público-alvo, faz sentido pagar pelo acesso a um painel.", + "you_can_do_a_lot_more_with_links_surveys": "Pode fazer muito mais com inquéritos de links \uD83D\uDCA1", + "your_survey_is_public": "O seu inquérito é público", + "youre_not_plugged_in_yet": "Ainda não está ligado!" + }, + "survey_deleted_successfully": "Inquérito eliminado com sucesso!", + "survey_duplicated_successfully": "Inquérito duplicado com sucesso.", + "survey_duplication_error": "Falha ao duplicar o inquérito.", + "survey_status_tooltip": "Para atualizar o estado do inquérito, atualize o agendamento e feche a configuração nas opções de resposta do inquérito.", + "templates": { + "all_channels": "Todos os canais", + "all_industries": "Todas as indústrias", + "all_roles": "Todos os papéis", + "create_a_new_survey": "Criar um novo inquérito", + "multiple_industries": "Várias indústrias", + "use_this_template": "Usar este modelo", + "uses_branching_logic": "Este questionário usa lógica de ramificação." + } + }, + "xm-templates": { + "ces": "CES", + "ces_description": "Aproveite todos os pontos de contato para entender a facilidade de interação do cliente.", + "csat": "CSAT", + "csat_description": "Implemente práticas recomendadas para medir a satisfação do cliente.", + "enps": "eNPS", + "enps_description": "Feedback universal para entender o envolvimento e a satisfação dos funcionários.", + "five_star_rating": "Classificação de 5 estrelas", + "five_star_rating_description": "Solução universal de feedback para medir a satisfação geral.", + "headline": "Que tipo de feedback gostaria de receber?", + "nps": "NPS", + "nps_description": "Implemente práticas recomendadas comprovadas para entender POR QUE as pessoas compram.", + "smileys": "Smileys", + "smileys_description": "Use indicadores visuais para capturar feedback em todos os pontos de contato com o cliente." + } + }, + "organizations": { + "landing": { + "no_projects_warning_subtitle": "Contacte o proprietário da sua organização para obter acesso aos projetos. Ou crie a sua própria organização para começar.", + "no_projects_warning_title": "A sua conta ainda não tem acesso a nenhum projeto." + }, + "projects": { + "new": { + "channel": { + "channel_select_subtitle": "Partilhe um link ou exiba o seu inquérito em aplicações ou em websites.", + "channel_select_title": "Que tipo de inquéritos precisa?", + "in_product_surveys": "Inquéritos no produto", + "in_product_surveys_description": "Incorporado em aplicações ou websites.", + "link_and_email_surveys": "Inquéritos por link e email", + "link_and_email_surveys_description": "Alcance pessoas em qualquer lugar online." + }, + "mode": { + "formbricks_cx": "Formbricks CX", + "formbricks_cx_description": "Inquéritos e relatórios para entender o que os seus clientes precisam.", + "formbricks_surveys": "Formbricks Inquéritos", + "formbricks_surveys_description": "Plataforma de inquéritos multiusos para inquéritos na web, app e email.", + "what_are_you_here_for": "Para que está aqui?" + }, + "settings": { + "brand_color": "Cor da marca", + "brand_color_description": "Combine a cor principal dos inquéritos com a sua marca.", + "create_new_team": "Criar nova equipa", + "project_creation_failed": "Falha na criação do projeto", + "project_name": "Nome do produto", + "project_name_description": "Como se chama o seu produto?", + "project_settings_subtitle": "Quando as pessoas reconhecem a sua marca, é muito mais provável que comecem e concluam as respostas.", + "project_settings_title": "Deixe os respondentes saberem que é você", + "team_description": "Quem pode aceder a este projeto?" + } + } + } + }, + "s": { + "check_inbox_or_spam": "Por favor, verifique também a sua pasta de spam se não vir o email na sua caixa de entrada.", + "completed": "Este inquérito gratuito e de código aberto foi encerrado.", + "create_your_own": "Crie o seu próprio", + "enter_pin": "Este inquérito está protegido. Introduza o PIN abaixo", + "just_curious": "Só por curiosidade?", + "link_invalid": "Este inquérito só pode ser respondido por convite.", + "paused": "Este inquérito gratuito e de código aberto está temporariamente pausado.", + "please_try_again_with_the_original_link": "Por favor, tente novamente com o link original", + "preview_survey_questions": "Pré-visualizar perguntas do inquérito.", + "question_preview": "Pré-visualização da Pergunta", + "response_already_received": "Já recebemos uma resposta para este endereço de email.", + "response_submitted": "Já existe uma resposta associada a este inquérito e contacto", + "survey_already_answered_heading": "O inquérito já foi respondido.", + "survey_already_answered_subheading": "Só pode usar este link uma vez.", + "survey_sent_to": "Inquérito enviado para {email}", + "this_looks_fishy": "Isto parece suspeito.", + "verify_email": "Verificar email.", + "verify_email_before_submission": "Verifique o seu email para responder", + "verify_email_before_submission_button": "Verificar", + "verify_email_before_submission_description": "Para responder a este questionário, por favor verifique o seu email", + "want_to_respond": "Quer responder?" + }, + "setup": { + "intro": { + "get_started": "Começar", + "made_with_love_in_kiel": "Feito com \uD83E\uDD0D na Alemanha", + "paragraph_1": "Formbricks é uma Suite de Gestão de Experiência construída na plataforma de inquéritos de código aberto de crescimento mais rápido do mundo.", + "paragraph_2": "Execute inquéritos direcionados em websites, em apps ou em qualquer lugar online. Recolha informações valiosas para criar experiências irresistíveis para clientes, utilizadores e funcionários.", + "paragraph_3": "Estamos comprometidos com o mais alto grau de privacidade de dados. Auto-hospede para manter controlo total sobre os seus dados.", + "welcome_to_formbricks": "Bem-vindo ao Formbricks!" + }, + "invite": { + "add_another_member": "Adicionar outro membro", + "continue": "Continuar", + "failed_to_invite": "Falha ao convidar", + "invitation_sent_to": "Convite enviado para", + "invite_your_organization_members": "Convide os membros da sua organização", + "life_s_no_fun_alone": "A vida não é divertida sozinho.", + "skip": "Saltar", + "smtp_not_configured": "SMTP não configurado", + "smtp_not_configured_description": "Os convites não podem ser enviados neste momento porque o serviço de email não está configurado. Pode copiar o link de convite nas definições da organização mais tarde." + }, + "organization": { + "create": { + "continue": "Continuar", + "delete_account": "Eliminar conta", + "delete_account_description": "Se quiser eliminar a sua conta, pode fazê-lo clicando no botão abaixo.", + "description": "Faça-o seu.", + "no_membership_found": "Nenhuma associação encontrada!", + "no_membership_found_description": "Não é membro de nenhuma organização neste momento. Se acredita que isto é um erro, por favor contacte o proprietário da organização.", + "title": "Configurar a sua organização" + } + }, + "signup": { + "create_administrator": "Criar Administrador", + "this_user_has_all_the_power": "Este utilizador tem todo o poder." + } + }, + "share": { + "back_to_home": "Voltar para casa", + "page_not_found": "Página não encontrada", + "page_not_found_description": "Desculpe, não conseguimos encontrar o ID de partilha de respostas que está a procurar." + }, + "templates": { + "address": "Endereço", + "address_description": "Pedir um endereço de correspondência", + "alignment_and_engagement_survey_description": "Avalie o alinhamento dos funcionários com a visão, estratégia e comunicação da empresa, bem como a colaboração da equipa.", + "alignment_and_engagement_survey_name": "Alinhamento e Envolvimento com a Visão da Empresa", + "alignment_and_engagement_survey_question_1_headline": "Compreendo como o meu papel contribui para a estratégia geral da empresa.", + "alignment_and_engagement_survey_question_1_lower_label": "Sem compreensão", + "alignment_and_engagement_survey_question_1_upper_label": "Compreensão completa", + "alignment_and_engagement_survey_question_2_headline": "Sinto que os meus valores estão alinhados com a missão e a cultura da empresa.", + "alignment_and_engagement_survey_question_2_lower_label": "Não alinhado", + "alignment_and_engagement_survey_question_2_upper_label": "Completamente alinhado", + "alignment_and_engagement_survey_question_3_headline": "Colaboro eficazmente com a minha equipa para alcançar os nossos objetivos.", + "alignment_and_engagement_survey_question_3_lower_label": "Colaboração fraca", + "alignment_and_engagement_survey_question_3_upper_label": "Excelente colaboração", + "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", + "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", + "build_product_roadmap_name_with_project_name": "Contributo para o Roteiro de $[projectName]", + "build_product_roadmap_question_1_headline": "Quão satisfeito está com as funcionalidades e características de $[projectName]?", + "build_product_roadmap_question_1_lower_label": "Nada satisfeito", + "build_product_roadmap_question_1_upper_label": "Extremamente satisfeito", + "build_product_roadmap_question_2_headline": "Qual é a ÚNICA mudança que poderíamos fazer para melhorar mais a sua experiência com $[projectName]?", + "build_product_roadmap_question_2_placeholder": "Escreva a sua resposta aqui...", + "card_abandonment_survey": "Inquérito de Abandono de Carrinho", + "card_abandonment_survey_description": "Compreenda as razões por trás do abandono do carrinho na sua loja online.", + "card_abandonment_survey_question_1_button_label": "Claro!", + "card_abandonment_survey_question_1_dismiss_button_label": "Não, obrigado.", + "card_abandonment_survey_question_1_headline": "Tem 2 minutos para nos ajudar a melhorar?", + "card_abandonment_survey_question_1_html": "

Notámos que deixou alguns itens no seu carrinho. Gostaríamos de entender porquê.

", + "card_abandonment_survey_question_2_choice_1": "Custos de envio elevados", + "card_abandonment_survey_question_2_choice_2": "Encontrei um preço melhor noutro lugar", + "card_abandonment_survey_question_2_choice_3": "Apenas a navegar", + "card_abandonment_survey_question_2_choice_4": "Decidi não comprar", + "card_abandonment_survey_question_2_choice_5": "Problemas de pagamento", + "card_abandonment_survey_question_2_choice_6": "Outro", + "card_abandonment_survey_question_2_headline": "Qual foi o principal motivo para não ter concluído a sua compra?", + "card_abandonment_survey_question_2_subheader": "Por favor, selecione uma das seguintes opções:", + "card_abandonment_survey_question_3_headline": "Por favor, explique o motivo de não ter concluído a compra:", + "card_abandonment_survey_question_4_headline": "Como classificaria a sua experiência geral de compra?", + "card_abandonment_survey_question_4_lower_label": "Muito insatisfeito", + "card_abandonment_survey_question_4_upper_label": "Muito satisfeito", + "card_abandonment_survey_question_5_choice_1": "Custos de envio mais baixos", + "card_abandonment_survey_question_5_choice_2": "Descontos ou promoções", + "card_abandonment_survey_question_5_choice_3": "Mais opções de pagamento", + "card_abandonment_survey_question_5_choice_4": "Melhores descrições de produtos", + "card_abandonment_survey_question_5_choice_5": "Navegação melhorada no site", + "card_abandonment_survey_question_5_choice_6": "Outro", + "card_abandonment_survey_question_5_headline": "Que fatores o incentivariam a concluir a sua compra no futuro?", + "card_abandonment_survey_question_5_subheader": "Por favor, selecione todas as opções aplicáveis:", + "card_abandonment_survey_question_6_headline": "Gostaria de receber um código de desconto por email?", + "card_abandonment_survey_question_6_label": "Sim, por favor entre em contacto.", + "card_abandonment_survey_question_7_headline": "Por favor, partilhe o seu endereço de email:", + "card_abandonment_survey_question_8_headline": "Algum comentário ou sugestão adicional?", + "career_development_survey_description": "Avaliar a satisfação dos funcionários com as oportunidades de crescimento e desenvolvimento de carreira.", + "career_development_survey_name": "Inquérito de Desenvolvimento de Carreira", + "career_development_survey_question_1_headline": "Estou satisfeito com as oportunidades de crescimento pessoal e profissional no $[projectName].", + "career_development_survey_question_1_lower_label": "Discordo totalmente", + "career_development_survey_question_1_upper_label": "Concordo totalmente", + "career_development_survey_question_2_headline": "Estou satisfeito com as oportunidades de progressão na carreira disponíveis para mim em $[projectName].", + "career_development_survey_question_2_lower_label": "Discordo totalmente", + "career_development_survey_question_2_upper_label": "Concordo totalmente", + "career_development_survey_question_3_headline": "Estou satisfeito com a formação relacionada com o trabalho que a minha organização oferece.", + "career_development_survey_question_3_lower_label": "Discordo totalmente", + "career_development_survey_question_3_upper_label": "Concordo totalmente", + "career_development_survey_question_4_headline": "Estou satisfeito com o investimento que a minha organização faz em formação e educação.", + "career_development_survey_question_4_lower_label": "Discordo totalmente", + "career_development_survey_question_4_upper_label": "Concordo totalmente", + "career_development_survey_question_5_choice_1": "Desenvolvimento de Produto", + "career_development_survey_question_5_choice_2": "Marketing", + "career_development_survey_question_5_choice_3": "Relações Públicas", + "career_development_survey_question_5_choice_4": "Contabilidade", + "career_development_survey_question_5_choice_5": "Operações", + "career_development_survey_question_5_choice_6": "Outro", + "career_development_survey_question_5_headline": "Em que função trabalha?", + "career_development_survey_question_5_subheader": "Por favor, selecione uma das seguintes", + "career_development_survey_question_6_choice_1": "Contribuidor Individual", + "career_development_survey_question_6_choice_2": "Gestor", + "career_development_survey_question_6_choice_3": "Gestor Sénior", + "career_development_survey_question_6_choice_4": "Vice-Presidente", + "career_development_survey_question_6_choice_5": "Executivo", + "career_development_survey_question_6_choice_6": "Outro", + "career_development_survey_question_6_headline": "Qual das seguintes opções descreve melhor o seu nível de emprego atual?", + "career_development_survey_question_6_subheader": "Por favor, selecione uma das seguintes", + "cess_survey_name": "Inquérito CES", + "cess_survey_question_1_headline": "$[projectName] torna fácil para mim [ADD GOAL]", + "cess_survey_question_1_lower_label": "Discordo totalmente", + "cess_survey_question_1_upper_label": "Concordo totalmente", + "cess_survey_question_2_headline": "Obrigado! Como poderíamos tornar mais fácil para si [ADD GOAL]?", + "cess_survey_question_2_placeholder": "Escreva a sua resposta aqui...", + "changing_subscription_experience_description": "Descubra o que passa pela cabeça das pessoas quando mudam as suas subscrições.", + "changing_subscription_experience_name": "Alterar Experiência de Subscrição", + "changing_subscription_experience_question_1_choice_1": "Extremamente difícil", + "changing_subscription_experience_question_1_choice_2": "Demorou um pouco, mas consegui", + "changing_subscription_experience_question_1_choice_3": "Foi razoável", + "changing_subscription_experience_question_1_choice_4": "Bastante fácil", + "changing_subscription_experience_question_1_choice_5": "Muito fácil, adoro!", + "changing_subscription_experience_question_1_headline": "Quão fácil foi mudar o seu plano?", + "changing_subscription_experience_question_2_choice_1": "Sim, muito claro.", + "changing_subscription_experience_question_2_choice_2": "Fiquei confuso no início, mas encontrei o que precisava.", + "changing_subscription_experience_question_2_choice_3": "Bastante complicado.", + "changing_subscription_experience_question_2_headline": "A informação sobre preços é fácil de entender?", + "churn_survey": "Inquérito de Churn", + "churn_survey_description": "Descubra por que as pessoas cancelam as suas subscrições. Estes insights são ouro puro!", + "churn_survey_question_1_choice_1": "Difícil de usar", + "churn_survey_question_1_choice_2": "É muito caro", + "churn_survey_question_1_choice_3": "Faltam-me funcionalidades", + "churn_survey_question_1_choice_4": "Mau serviço ao cliente", + "churn_survey_question_1_choice_5": "Simplesmente já não precisava", + "churn_survey_question_1_headline": "Por que cancelou a sua subscrição?", + "churn_survey_question_1_subheader": "Lamentamos vê-lo partir. Ajude-nos a melhorar:", + "churn_survey_question_2_button_label": "Enviar", + "churn_survey_question_2_headline": "O que teria tornado $[projectName] mais fácil de usar?", + "churn_survey_question_3_button_label": "Obtenha 30% de desconto", + "churn_survey_question_3_dismiss_button_label": "Saltar", + "churn_survey_question_3_headline": "Obtenha 30% de desconto no próximo ano!", + "churn_survey_question_3_html": "

Adoraríamos mantê-lo como cliente. Estamos felizes por lhe oferecer um desconto de 30% para o próximo ano.

", + "churn_survey_question_4_headline": "Que funcionalidades lhe faltam?", + "churn_survey_question_5_button_label": "Enviar email para o CEO", + "churn_survey_question_5_dismiss_button_label": "Saltar", + "churn_survey_question_5_headline": "Lamentamos ouvir isso \uD83D\uDE14 Fale diretamente com o nosso CEO!", + "churn_survey_question_5_html": "

O nosso objetivo é fornecer o melhor serviço ao cliente possível. Por favor, envie um email à nossa CEO e ela tratará pessoalmente do seu problema.

", + "collect_feedback_description": "Recolha feedback abrangente sobre o seu produto ou serviço.", + "collect_feedback_name": "Recolher Feedback", + "collect_feedback_question_1_headline": "Como avalia a sua experiência geral?", + "collect_feedback_question_1_lower_label": "Não é bom", + "collect_feedback_question_1_subheader": "Não se preocupe, seja honesto.", + "collect_feedback_question_1_upper_label": "Muito bom", + "collect_feedback_question_2_headline": "Adorável! O que gostou nisso?", + "collect_feedback_question_2_placeholder": "Escreva a sua resposta aqui...", + "collect_feedback_question_3_headline": "Obrigado por partilhar! O que não gostou?", + "collect_feedback_question_3_placeholder": "Escreva a sua resposta aqui...", + "collect_feedback_question_4_headline": "Como avalia a nossa comunicação?", + "collect_feedback_question_4_lower_label": "Não é bom", + "collect_feedback_question_4_upper_label": "Muito bom", + "collect_feedback_question_5_headline": "Mais alguma coisa que gostaria de partilhar com a nossa equipa?", + "collect_feedback_question_5_placeholder": "Escreva a sua resposta aqui...", + "collect_feedback_question_6_choice_1": "Google", + "collect_feedback_question_6_choice_2": "Redes Sociais", + "collect_feedback_question_6_choice_3": "Amigos", + "collect_feedback_question_6_choice_4": "Podcast", + "collect_feedback_question_6_choice_5": "Outro", + "collect_feedback_question_6_headline": "Como ouviu falar de nós?", + "collect_feedback_question_7_headline": "Por fim, gostaríamos de responder ao seu feedback. Por favor, partilhe o seu email:", + "collect_feedback_question_7_placeholder": "exemplo@email.com", + "consent": "Consentimento", + "consent_description": "Pedir para concordar com os termos, condições ou uso de dados", + "contact_info": "Informações de Contacto", + "contact_info_description": "Peça nome, apelido, email, número de telefone e empresa em conjunto", + "csat_description": "Medir o Customer Satisfaction Score do seu produto ou serviço.", + "csat_name": "Customer Satisfaction Score (CSAT)", + "csat_question_10_headline": "Tem mais algum comentário, pergunta ou preocupação?", + "csat_question_10_placeholder": "Escreva a sua resposta aqui...", + "csat_question_1_headline": "Qual a probabilidade de recomendar este $[projectName] a um amigo ou colega?", + "csat_question_1_lower_label": "Pouco provável", + "csat_question_1_upper_label": "Muito provável", + "csat_question_2_choice_1": "Algo satisfeito", + "csat_question_2_choice_2": "Muito satisfeito", + "csat_question_2_choice_3": "Nem satisfeito nem insatisfeito", + "csat_question_2_choice_4": "Algo insatisfeito", + "csat_question_2_choice_5": "Muito insatisfeito", + "csat_question_2_headline": "No geral, quão satisfeito ou insatisfeito está com o nosso $[projectName]", + "csat_question_2_subheader": "Por favor, selecione um:", + "csat_question_3_choice_1": "Ineficaz", + "csat_question_3_choice_10": "Único", + "csat_question_3_choice_2": "Útil", + "csat_question_3_choice_3": "Impraticável", + "csat_question_3_choice_4": "Demasiado caro", + "csat_question_3_choice_5": "Alta qualidade", + "csat_question_3_choice_6": "Fiável", + "csat_question_3_choice_7": "Boa relação qualidade/preço", + "csat_question_3_choice_8": "Má qualidade", + "csat_question_3_choice_9": "Pouco fiável", + "csat_question_3_headline": "Qual das seguintes palavras usaria para descrever o nosso $[projectName]?", + "csat_question_3_subheader": "Selecione todas as opções aplicáveis:", + "csat_question_4_choice_1": "Extremamente bem", + "csat_question_4_choice_2": "Muito bem", + "csat_question_4_choice_3": "Razoavelmente bem", + "csat_question_4_choice_4": "Não muito bem", + "csat_question_4_choice_5": "Nada bem", + "csat_question_4_headline": "Quão bem o nosso $[projectName] atende às suas necessidades?", + "csat_question_4_subheader": "Selecione uma opção:", + "csat_question_5_choice_1": "Qualidade muito alta", + "csat_question_5_choice_2": "Alta qualidade", + "csat_question_5_choice_3": "Qualidade baixa", + "csat_question_5_choice_4": "Qualidade muito baixa", + "csat_question_5_choice_5": "Nem alto nem baixo", + "csat_question_5_headline": "Como classificaria a qualidade do $[projectName]?", + "csat_question_5_subheader": "Selecione uma opção:", + "csat_question_6_choice_1": "Excelente", + "csat_question_6_choice_2": "Acima da média", + "csat_question_6_choice_3": "Média", + "csat_question_6_choice_4": "Abaixo da média", + "csat_question_6_choice_5": "Fraco", + "csat_question_6_headline": "Como classificaria a relação qualidade-preço do $[projectName]?", + "csat_question_6_subheader": "Por favor, selecione um:", + "csat_question_7_choice_1": "Extremamente responsivo", + "csat_question_7_choice_2": "Muito responsivo", + "csat_question_7_choice_3": "Um pouco responsivo", + "csat_question_7_choice_4": "Não tão responsivo", + "csat_question_7_choice_5": "Nada responsivo", + "csat_question_7_choice_6": "Não aplicável", + "csat_question_7_headline": "Quão responsivos temos sido às suas perguntas sobre os nossos serviços?", + "csat_question_7_subheader": "Por favor, selecione um:", + "csat_question_8_choice_1": "Esta é a minha primeira compra", + "csat_question_8_choice_2": "Menos de seis meses", + "csat_question_8_choice_3": "Seis meses a um ano", + "csat_question_8_choice_4": "1 - 2 anos", + "csat_question_8_choice_5": "3 ou mais anos", + "csat_question_8_choice_6": "Ainda não fiz uma compra", + "csat_question_8_headline": "Há quanto tempo é cliente de $[projectName]?", + "csat_question_8_subheader": "Por favor, selecione um:", + "csat_question_9_choice_1": "Extremamente provável", + "csat_question_9_choice_2": "Muito provável", + "csat_question_9_choice_3": "Algo provável", + "csat_question_9_choice_4": "Pouco provável", + "csat_question_9_choice_5": "Nada provável", + "csat_question_9_headline": "Qual é a probabilidade de voltar a comprar algum dos nossos $[projectName]?", + "csat_question_9_subheader": "Selecione uma opção:", + "csat_survey_name": "$[projectName] CSAT", + "csat_survey_question_1_headline": "Quão satisfeito está com a sua experiência no $[projectName]?", + "csat_survey_question_1_lower_label": "Extremamente insatisfeito", + "csat_survey_question_1_upper_label": "Extremamente satisfeito", + "csat_survey_question_2_headline": "Ótimo! Há algo que possamos fazer para melhorar a sua experiência?", + "csat_survey_question_2_placeholder": "Escreva a sua resposta aqui...", + "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_description": "Criar um inquérito sem modelo.", + "custom_survey_name": "Começar do zero", + "custom_survey_question_1_headline": "O que gostaria de saber?", + "custom_survey_question_1_placeholder": "Escreva a sua resposta aqui...", + "customer_effort_score_description": "Determinar quão fácil é usar uma funcionalidade.", + "customer_effort_score_name": "Pontuação de Esforço do Cliente (CES)", + "customer_effort_score_question_1_headline": "$[projectName] torna fácil para mim [ADD GOAL]", + "customer_effort_score_question_1_lower_label": "Discordo totalmente", + "customer_effort_score_question_1_upper_label": "Concordo totalmente", + "customer_effort_score_question_2_headline": "Obrigado! Como poderíamos tornar mais fácil para si [ADD GOAL]?", + "customer_effort_score_question_2_placeholder": "Escreva a sua resposta aqui...", + "date": "Data", + "date_description": "Pedir uma seleção de data", + "default_ending_card_button_label": "Crie o seu próprio Inquérito", + "default_ending_card_headline": "Obrigado!", + "default_ending_card_subheader": "Agradecemos o seu feedback.", + "default_welcome_card_button_label": "Seguinte", + "default_welcome_card_headline": "Bem-vindo!", + "default_welcome_card_html": "Obrigado por fornecer o seu feedback - vamos a isso!", + "docs_feedback_description": "Medir a clareza de cada página da sua documentação de desenvolvedor.", + "docs_feedback_name": "Feedback de Documentos", + "docs_feedback_question_1_choice_1": "Sim \uD83D\uDC4D", + "docs_feedback_question_1_choice_2": "Não \uD83D\uDC4E", + "docs_feedback_question_1_headline": "Esta página foi útil?", + "docs_feedback_question_2_headline": "Por favor, elabore:", + "docs_feedback_question_3_headline": "URL da página", + "earned_advocacy_score_description": "O EAS é uma variação do NPS, mas pergunta sobre comportamentos passados reais em vez de intenções elevadas.", + "earned_advocacy_score_name": "Pontuação de Advocacia Ganha (EAS)", + "earned_advocacy_score_question_1_choice_1": "Sim", + "earned_advocacy_score_question_1_choice_2": "Não", + "earned_advocacy_score_question_1_headline": "Recomendou ativamente $[projectName] a outros?", + "earned_advocacy_score_question_2_headline": "Por que nos recomendou?", + "earned_advocacy_score_question_2_placeholder": "Escreva a sua resposta aqui...", + "earned_advocacy_score_question_3_headline": "Que pena. Porquê?", + "earned_advocacy_score_question_3_placeholder": "Escreva a sua resposta aqui...", + "earned_advocacy_score_question_4_choice_1": "Sim", + "earned_advocacy_score_question_4_choice_2": "Não", + "earned_advocacy_score_question_4_headline": "Desencorajou ativamente outros de escolherem $[projectName]?", + "earned_advocacy_score_question_5_headline": "O que o fez desencorajá-los?", + "earned_advocacy_score_question_5_placeholder": "Escreva a sua resposta aqui...", + "employee_satisfaction_description": "Avalie a satisfação dos funcionários e identifique áreas de melhoria.", + "employee_satisfaction_name": "Satisfação dos Funcionários", + "employee_satisfaction_question_1_headline": "Quão satisfeito está com o seu cargo atual?", + "employee_satisfaction_question_1_lower_label": "Não satisfeito", + "employee_satisfaction_question_1_upper_label": "Muito satisfeito", + "employee_satisfaction_question_2_choice_1": "Extremamente significativo", + "employee_satisfaction_question_2_choice_2": "Muito significativo", + "employee_satisfaction_question_2_choice_3": "Moderadamente significativo", + "employee_satisfaction_question_2_choice_4": "Ligeiramente significativo", + "employee_satisfaction_question_2_choice_5": "Nada significativo", + "employee_satisfaction_question_2_headline": "Quão significativo acha que é o seu trabalho?", + "employee_satisfaction_question_3_headline": "O que mais gosta de trabalhar aqui?", + "employee_satisfaction_question_3_placeholder": "Escreva a sua resposta aqui...", + "employee_satisfaction_question_5_headline": "Avalie o apoio que recebe do seu gestor.", + "employee_satisfaction_question_5_lower_label": "Fraco", + "employee_satisfaction_question_5_upper_label": "Excelente", + "employee_satisfaction_question_6_headline": "Que melhorias sugeriria para o nosso local de trabalho?", + "employee_satisfaction_question_6_placeholder": "Escreva a sua resposta aqui...", + "employee_satisfaction_question_7_choice_1": "Extremamente provável", + "employee_satisfaction_question_7_choice_2": "Muito provável", + "employee_satisfaction_question_7_choice_3": "Moderadamente provável", + "employee_satisfaction_question_7_choice_4": "Pouco provável", + "employee_satisfaction_question_7_choice_5": "Nada provável", + "employee_satisfaction_question_7_headline": "Qual a probabilidade de recomendar a nossa empresa a um amigo?", + "employee_well_being_description": "Avalie o bem-estar dos seus funcionários através do equilíbrio entre vida pessoal e profissional, carga de trabalho e ambiente.", + "employee_well_being_name": "Bem-Estar dos Funcionários", + "employee_well_being_question_1_headline": "Sinto que tenho um bom equilíbrio entre o meu trabalho e a minha vida pessoal.", + "employee_well_being_question_1_lower_label": "Equilíbrio muito fraco", + "employee_well_being_question_1_upper_label": "Equilíbrio excelente", + "employee_well_being_question_2_headline": "A minha carga de trabalho é gerível, permitindo-me manter produtivo sem me sentir sobrecarregado.", + "employee_well_being_question_2_lower_label": "Carga de trabalho esmagadora", + "employee_well_being_question_2_upper_label": "Perfeitamente gerível", + "employee_well_being_question_3_headline": "O ambiente de trabalho apoia o meu bem-estar físico e mental", + "employee_well_being_question_3_lower_label": "Não apoiante", + "employee_well_being_question_3_upper_label": "Altamente apoiante", + "employee_well_being_question_4_headline": "Que mudanças, se houver, melhorariam o seu bem-estar geral no trabalho?", + "employee_well_being_question_4_placeholder": "Escreva a sua resposta aqui...", + "enps_survey_name": "Inquérito eNPS", + "enps_survey_question_1_headline": "Qual a probabilidade de recomendar trabalhar nesta empresa a um amigo ou colega?", + "enps_survey_question_1_lower_label": "Nada provável", + "enps_survey_question_1_upper_label": "Extremamente provável", + "enps_survey_question_2_headline": "Para nos ajudar a melhorar, pode descrever a(s) razão(ões) para a sua classificação?", + "enps_survey_question_3_headline": "Algum outro comentário, feedback ou preocupação?", + "evaluate_a_product_idea_description": "Pesquise os utilizadores sobre ideias de produtos ou funcionalidades. Obtenha feedback rapidamente.", + "evaluate_a_product_idea_name": "Avaliar uma Ideia de Produto", + "evaluate_a_product_idea_question_1_button_label": "Vamos a isso!", + "evaluate_a_product_idea_question_1_dismiss_button_label": "Saltar", + "evaluate_a_product_idea_question_1_headline": "Adoramos como usa $[projectName]! Gostaríamos de saber a sua opinião sobre uma ideia de funcionalidade. Tem um minuto?", + "evaluate_a_product_idea_question_1_html": "

Respeitamos o seu tempo e mantivemos isto curto \uD83E\uDD38

", + "evaluate_a_product_idea_question_2_headline": "Obrigado! Quão difícil ou fácil é para si [PROBLEM AREA] hoje?", + "evaluate_a_product_idea_question_2_lower_label": "Muito difícil", + "evaluate_a_product_idea_question_2_upper_label": "Muito fácil", + "evaluate_a_product_idea_question_3_headline": "O que é mais difícil para si quando se trata de [PROBLEM AREA]?", + "evaluate_a_product_idea_question_3_placeholder": "Escreva a sua resposta aqui...", + "evaluate_a_product_idea_question_4_button_label": "Seguinte", + "evaluate_a_product_idea_question_4_dismiss_button_label": "Saltar", + "evaluate_a_product_idea_question_4_headline": "Estamos a trabalhar numa ideia para ajudar com [PROBLEM AREA].", + "evaluate_a_product_idea_question_4_html": "

Insira aqui o resumo do conceito. Adicione os detalhes necessários, mas mantenha-o conciso e fácil de entender.

", + "evaluate_a_product_idea_question_5_headline": "Quão valiosa seria esta funcionalidade para si?", + "evaluate_a_product_idea_question_5_lower_label": "Sem valor", + "evaluate_a_product_idea_question_5_upper_label": "Muito valioso", + "evaluate_a_product_idea_question_6_headline": "Entendi. Porque é que esta funcionalidade não seria valiosa para si?", + "evaluate_a_product_idea_question_6_placeholder": "Escreva a sua resposta aqui...", + "evaluate_a_product_idea_question_7_headline": "O que seria mais valioso para si nesta funcionalidade?", + "evaluate_a_product_idea_question_7_placeholder": "Escreva a sua resposta aqui...", + "evaluate_a_product_idea_question_8_headline": "Mais alguma coisa que devamos ter em mente?", + "evaluate_a_product_idea_question_8_placeholder": "Escreva a sua resposta aqui...", + "evaluate_content_quality_description": "Meça se as suas peças de marketing de conteúdo acertam em cheio.", + "evaluate_content_quality_name": "Avaliar Qualidade do Conteúdo", + "evaluate_content_quality_question_1_headline": "Quão bem este artigo abordou o que esperava aprender?", + "evaluate_content_quality_question_1_lower_label": "Nada bem", + "evaluate_content_quality_question_1_upper_label": "Extremamente bem", + "evaluate_content_quality_question_2_headline": "Hmpft! O que esperavas?", + "evaluate_content_quality_question_2_placeholder": "Escreva a sua resposta aqui...", + "evaluate_content_quality_question_3_headline": "Adorável! Há mais alguma coisa que gostaria que abordássemos?", + "evaluate_content_quality_question_3_placeholder": "Tópicos, tendências, tutoriais...", + "fake_door_follow_up_description": "Acompanhe os utilizadores que encontraram um dos seus experimentos de Porta Falsa.", + "fake_door_follow_up_name": "Acompanhamento de Porta Falsa", + "fake_door_follow_up_question_1_headline": "Quão importante é esta funcionalidade para si?", + "fake_door_follow_up_question_1_lower_label": "Não é importante", + "fake_door_follow_up_question_1_upper_label": "Muito importante", + "fake_door_follow_up_question_2_choice_1": "Aspeto 1", + "fake_door_follow_up_question_2_choice_2": "Aspeto 2", + "fake_door_follow_up_question_2_choice_3": "Aspeto 3", + "fake_door_follow_up_question_2_choice_4": "Aspeto 4", + "fake_door_follow_up_question_2_headline": "O que deve ser definitivamente incluído na construção disto?", + "feature_chaser_description": "Acompanhe os utilizadores que acabaram de usar uma funcionalidade específica.", + "feature_chaser_name": "Perseguidor de Funcionalidades", + "feature_chaser_question_1_headline": "Quão importante é [ADD FEATURE] para si?", + "feature_chaser_question_1_lower_label": "Não é importante", + "feature_chaser_question_1_upper_label": "Muito importante", + "feature_chaser_question_2_choice_1": "Aspeto 1", + "feature_chaser_question_2_choice_2": "Aspeto 2", + "feature_chaser_question_2_choice_3": "Aspeto 3", + "feature_chaser_question_2_choice_4": "Aspeto 4", + "feature_chaser_question_2_headline": "Qual é o aspeto mais importante?", + "feedback_box_description": "Dê aos seus utilizadores a oportunidade de partilhar facilmente o que têm em mente.", + "feedback_box_name": "Caixa de Feedback", + "feedback_box_question_1_choice_1": "Relatório de erro \uD83D\uDC1E", + "feedback_box_question_1_choice_2": "Pedido de Funcionalidade \uD83D\uDCA1", + "feedback_box_question_1_headline": "O que tem em mente, chefe?", + "feedback_box_question_1_subheader": "Obrigado por partilhar. Entraremos em contacto consigo o mais breve possível.", + "feedback_box_question_2_headline": "O que está quebrado?", + "feedback_box_question_2_subheader": "Quanto mais detalhes, melhor :)", + "feedback_box_question_3_button_label": "Sim, notifique-me", + "feedback_box_question_3_dismiss_button_label": "Não, obrigado", + "feedback_box_question_3_headline": "Quer manter-se atualizado?", + "feedback_box_question_3_html": "

Vamos resolver isto o mais rápido possível. Quer ser notificado quando o fizermos?

", + "feedback_box_question_4_button_label": "Pedir funcionalidade", + "feedback_box_question_4_headline": "Adorável, conte-nos mais!", + "feedback_box_question_4_placeholder": "Escreva a sua resposta aqui...", + "feedback_box_question_4_subheader": "Que problema quer que resolvamos?", + "file_upload": "Carregar Ficheiro", + "file_upload_description": "Permitir que os respondentes carreguem documentos, imagens ou outros ficheiros", + "finish": "Concluir", + "follow_ups_modal_action_body": "

Olá \uD83D\uDC4B

Obrigado por dedicar o seu tempo a responder, entraremos em contacto em breve.

Tenha um ótimo dia!

", + "free_text": "Texto livre", + "free_text_description": "Recolher feedback aberto", + "free_text_placeholder": "Escreva a sua resposta aqui...", + "gauge_feature_satisfaction_description": "Avaliar a satisfação com funcionalidades específicas do seu produto.", + "gauge_feature_satisfaction_name": "Medir Satisfação com Funcionalidades", + "gauge_feature_satisfaction_question_1_headline": "Quão fácil foi alcançar ... ?", + "gauge_feature_satisfaction_question_1_lower_label": "Nada fácil", + "gauge_feature_satisfaction_question_1_upper_label": "Muito fácil", + "gauge_feature_satisfaction_question_2_headline": "O que é uma coisa que poderíamos fazer melhor?", + "identify_customer_goals_description": "Compreenda melhor se a sua mensagem cria as expectativas certas sobre o valor que o seu produto oferece.", + "identify_customer_goals_name": "Identificar Objetivos do Cliente", + "identify_sign_up_barriers_description": "Ofereça um desconto para obter informações sobre as barreiras de inscrição.", + "identify_sign_up_barriers_name": "Identificar Barreiras de Inscrição", + "identify_sign_up_barriers_question_1_button_label": "Obtenha 10% de desconto", + "identify_sign_up_barriers_question_1_dismiss_button_label": "Não, obrigado", + "identify_sign_up_barriers_question_1_headline": "Responda a este breve questionário, obtenha 10% de desconto!", + "identify_sign_up_barriers_question_1_html": "Parece que está a considerar inscrever-se. Responda a quatro perguntas e obtenha 10% em qualquer plano.", + "identify_sign_up_barriers_question_2_headline": "Qual é a probabilidade de se inscrever no $[projectName]?", + "identify_sign_up_barriers_question_2_lower_label": "Nada provável", + "identify_sign_up_barriers_question_2_upper_label": "Muito provável", + "identify_sign_up_barriers_question_3_choice_1_label": "Pode não ter o que procuro", + "identify_sign_up_barriers_question_3_choice_2_label": "Ainda a comparar opções", + "identify_sign_up_barriers_question_3_choice_3_label": "Parece complicado", + "identify_sign_up_barriers_question_3_choice_4_label": "O preço é uma preocupação", + "identify_sign_up_barriers_question_3_choice_5_label": "Outra coisa", + "identify_sign_up_barriers_question_3_headline": "O que o está a impedir de experimentar $[projectName]?", + "identify_sign_up_barriers_question_4_headline": "O que precisa mas $[projectName] não oferece?", + "identify_sign_up_barriers_question_4_placeholder": "Escreva a sua resposta aqui...", + "identify_sign_up_barriers_question_5_headline": "Que opções está a considerar?", + "identify_sign_up_barriers_question_5_placeholder": "Escreva a sua resposta aqui...", + "identify_sign_up_barriers_question_6_headline": "O que lhe parece complicado?", + "identify_sign_up_barriers_question_6_placeholder": "Escreva a sua resposta aqui...", + "identify_sign_up_barriers_question_7_headline": "O que o preocupa em relação aos preços?", + "identify_sign_up_barriers_question_7_placeholder": "Escreva a sua resposta aqui...", + "identify_sign_up_barriers_question_8_headline": "Por favor, explique:", + "identify_sign_up_barriers_question_8_placeholder": "Escreva a sua resposta aqui...", + "identify_sign_up_barriers_question_9_button_label": "Inscrever-se", + "identify_sign_up_barriers_question_9_dismiss_button_label": "Saltar por agora", + "identify_sign_up_barriers_question_9_headline": "Obrigado! Aqui está o seu código: SIGNUPNOW10", + "identify_sign_up_barriers_question_9_html": "

Muito obrigado por dedicar tempo a partilhar feedback \uD83D\uDE4F

", + "identify_sign_up_barriers_with_project_name": "Barreiras de Inscrição do $[projectName]", + "identify_upsell_opportunities_description": "Descubra quanto tempo o seu produto poupa ao seu utilizador. Use isso para vender mais.", + "identify_upsell_opportunities_name": "Identificar Oportunidades de Venda Adicional", + "identify_upsell_opportunities_question_1_choice_1": "Menos de 1 hora", + "identify_upsell_opportunities_question_1_choice_2": "1 a 2 horas", + "identify_upsell_opportunities_question_1_choice_3": "3 a 5 horas", + "identify_upsell_opportunities_question_1_choice_4": "5+ horas", + "identify_upsell_opportunities_question_1_headline": "Quantas horas a sua equipa poupa por semana ao usar $[projectName]?", + "improve_activation_rate_description": "Identifique fraquezas no seu fluxo de integração para aumentar a ativação do utilizador.", + "improve_activation_rate_name": "Melhorar a Taxa de Ativação", + "improve_activation_rate_question_1_choice_1": "Não me pareceu útil", + "improve_activation_rate_question_1_choice_2": "Difícil de configurar ou usar", + "improve_activation_rate_question_1_choice_3": "Faltavam funcionalidades", + "improve_activation_rate_question_1_choice_4": "Simplesmente não tive tempo", + "improve_activation_rate_question_1_choice_5": "Outra coisa", + "improve_activation_rate_question_1_headline": "Qual é a principal razão pela qual não terminou de configurar o $[projectName]?", + "improve_activation_rate_question_2_headline": "O que o fez pensar que $[projectName] não seria útil?", + "improve_activation_rate_question_2_placeholder": "Escreva a sua resposta aqui...", + "improve_activation_rate_question_3_headline": "O que foi difícil em configurar ou usar o $[projectName]?", + "improve_activation_rate_question_3_placeholder": "Escreva a sua resposta aqui...", + "improve_activation_rate_question_4_headline": "Que funcionalidades ou características estavam em falta?", + "improve_activation_rate_question_4_placeholder": "Escreva a sua resposta aqui...", + "improve_activation_rate_question_5_headline": "Como poderíamos tornar mais fácil para si começar?", + "improve_activation_rate_question_5_placeholder": "Escreva a sua resposta aqui...", + "improve_activation_rate_question_6_headline": "O que foi? Por favor, explique:", + "improve_activation_rate_question_6_placeholder": "Escreva a sua resposta aqui...", + "improve_activation_rate_question_6_subheader": "Estamos ansiosos por corrigi-lo o mais rápido possível.", + "improve_newsletter_content_description": "Descubra como os seus subscritores gostam do conteúdo da sua newsletter.", + "improve_newsletter_content_name": "Melhorar o Conteúdo da Newsletter", + "improve_newsletter_content_question_1_headline": "Como classificaria a newsletter desta semana?", + "improve_newsletter_content_question_1_lower_label": "Mais ou menos", + "improve_newsletter_content_question_1_upper_label": "Ótimo", + "improve_newsletter_content_question_2_headline": "O que teria tornado a newsletter desta semana mais útil?", + "improve_newsletter_content_question_2_placeholder": "Escreva a sua resposta aqui...", + "improve_newsletter_content_question_3_button_label": "Feliz por ajudar!", + "improve_newsletter_content_question_3_dismiss_button_label": "Encontre os seus próprios amigos", + "improve_newsletter_content_question_3_headline": "Obrigado! ❤️ Espalhe o amor com UM amigo.", + "improve_newsletter_content_question_3_html": "

Quem pensa como tu? Farias-nos um grande favor se partilhasses o episódio desta semana com o teu amigo cérebro!

", + "improve_trial_conversion_description": "Descubra por que as pessoas interromperam o seu teste. Estes insights ajudam-no a melhorar o seu funil.", + "improve_trial_conversion_name": "Melhorar a Conversão de Testes", + "improve_trial_conversion_question_1_choice_1": "Não obtive muito valor com isso", + "improve_trial_conversion_question_1_choice_2": "Eu esperava outra coisa", + "improve_trial_conversion_question_1_choice_3": "É muito caro para o que faz", + "improve_trial_conversion_question_1_choice_4": "Falta-me uma funcionalidade", + "improve_trial_conversion_question_1_choice_5": "Eu estava apenas a ver", + "improve_trial_conversion_question_1_headline": "Porque parou o seu teste?", + "improve_trial_conversion_question_1_subheader": "Ajude-nos a compreendê-lo melhor:", + "improve_trial_conversion_question_2_button_label": "Seguinte", + "improve_trial_conversion_question_2_headline": "Lamentamos saber. Qual foi o maior problema ao usar $[projectName]?", + "improve_trial_conversion_question_4_button_label": "Obtenha 20% de desconto", + "improve_trial_conversion_question_4_dismiss_button_label": "Saltar", + "improve_trial_conversion_question_4_headline": "Lamentamos saber! Obtenha 20% de desconto no primeiro ano.", + "improve_trial_conversion_question_4_html": "

Estamos felizes por lhe oferecer um desconto de 20% num plano anual.

", + "improve_trial_conversion_question_5_button_label": "Seguinte", + "improve_trial_conversion_question_5_headline": "O que gostaria de alcançar?", + "improve_trial_conversion_question_5_subheader": "Por favor, selecione uma das seguintes opções:", + "improve_trial_conversion_question_6_headline": "Como está a resolver o seu problema agora?", + "improve_trial_conversion_question_6_subheader": "Por favor, nomeie soluções alternativas:", + "integration_setup_survey_description": "Avalie a facilidade com que os utilizadores podem adicionar integrações ao seu produto. Encontre pontos cegos.", + "integration_setup_survey_name": "Inquérito de Utilização da Integração", + "integration_setup_survey_question_1_headline": "Quão fácil foi configurar esta integração?", + "integration_setup_survey_question_1_lower_label": "Nada fácil", + "integration_setup_survey_question_1_upper_label": "Muito fácil", + "integration_setup_survey_question_2_headline": "Porque foi difícil?", + "integration_setup_survey_question_2_placeholder": "Escreva a sua resposta aqui...", + "integration_setup_survey_question_3_headline": "Que outras ferramentas gostaria de usar com $[projectName]?", + "integration_setup_survey_question_3_subheader": "Continuamos a criar integrações, a sua pode ser a próxima:", + "interview_prompt_description": "Convide um subconjunto específico dos seus utilizadores para agendar uma entrevista com a sua equipa de produto.", + "interview_prompt_name": "Sugestão de Entrevista", + "interview_prompt_question_1_button_label": "Reservar horário", + "interview_prompt_question_1_headline": "Tens 15 minutos para falar connosco? \uD83D\uDE4F", + "interview_prompt_question_1_html": "És um dos nossos utilizadores avançados. Adoraríamos entrevistar-te brevemente!", + "long_term_retention_check_in_description": "Avalie a satisfação a longo prazo dos utilizadores, lealdade e áreas de melhoria para reter utilizadores leais.", + "long_term_retention_check_in_name": "Verificação de Retenção a Longo Prazo", + "long_term_retention_check_in_question_10_headline": "Algum comentário ou feedback adicional?", + "long_term_retention_check_in_question_10_placeholder": "Partilhe quaisquer pensamentos ou feedback que nos possam ajudar a melhorar...", + "long_term_retention_check_in_question_1_headline": "Quão satisfeito está com o $[projectName] no geral?", + "long_term_retention_check_in_question_1_lower_label": "Não satisfeito", + "long_term_retention_check_in_question_1_upper_label": "Muito satisfeito", + "long_term_retention_check_in_question_2_headline": "O que considera mais valioso no $[projectName]?", + "long_term_retention_check_in_question_2_placeholder": "Descreva a funcionalidade ou benefício que mais valoriza...", + "long_term_retention_check_in_question_3_choice_1": "Funcionalidades", + "long_term_retention_check_in_question_3_choice_2": "Apoio ao cliente", + "long_term_retention_check_in_question_3_choice_3": "Experiência do utilizador", + "long_term_retention_check_in_question_3_choice_4": "Preços", + "long_term_retention_check_in_question_3_choice_5": "Confiabilidade e tempo de atividade", + "long_term_retention_check_in_question_3_headline": "Qual o aspeto de $[projectName] que considera mais essencial para a sua experiência?", + "long_term_retention_check_in_question_4_headline": "Quão bem o $[projectName] atende às suas expectativas?", + "long_term_retention_check_in_question_4_lower_label": "Fica aquém", + "long_term_retention_check_in_question_4_upper_label": "Excede as expectativas", + "long_term_retention_check_in_question_5_headline": "Que desafios ou frustrações enfrentou ao usar $[projectName]?", + "long_term_retention_check_in_question_5_placeholder": "Descreva quaisquer desafios ou melhorias que gostaria de ver...", + "long_term_retention_check_in_question_6_headline": "Qual a probabilidade de recomendar $[projectName] a um amigo ou colega?", + "long_term_retention_check_in_question_6_lower_label": "Pouco provável", + "long_term_retention_check_in_question_6_upper_label": "Muito provável", + "long_term_retention_check_in_question_7_choice_1": "Novas funcionalidades e melhorias", + "long_term_retention_check_in_question_7_choice_2": "Apoio ao cliente melhorado", + "long_term_retention_check_in_question_7_choice_3": "Melhores opções de preços", + "long_term_retention_check_in_question_7_choice_4": "Mais integrações", + "long_term_retention_check_in_question_7_choice_5": "Aperfeiçoamentos da experiência do utilizador", + "long_term_retention_check_in_question_7_headline": "O que o faria mais propenso a permanecer um utilizador a longo prazo?", + "long_term_retention_check_in_question_8_headline": "Se pudesse mudar uma coisa sobre $[projectName], o que seria?", + "long_term_retention_check_in_question_8_placeholder": "Partilhe quaisquer alterações ou funcionalidades que gostaria que considerássemos...", + "long_term_retention_check_in_question_9_headline": "Quão satisfeito está com as nossas atualizações de produto e frequência?", + "long_term_retention_check_in_question_9_lower_label": "Não estou satisfeito", + "long_term_retention_check_in_question_9_upper_label": "Muito feliz", + "market_attribution_description": "Saiba como os utilizadores ouviram falar do seu produto pela primeira vez.", + "market_attribution_name": "Atribuição de Marketing", + "market_attribution_question_1_choice_1": "Recomendação", + "market_attribution_question_1_choice_2": "Redes Sociais", + "market_attribution_question_1_choice_3": "Anúncios", + "market_attribution_question_1_choice_4": "Pesquisa Google", + "market_attribution_question_1_choice_5": "Num Podcast", + "market_attribution_question_1_headline": "Como ouviu falar de nós pela primeira vez?", + "market_attribution_question_1_subheader": "Por favor, selecione uma das seguintes opções:", + "market_site_clarity_description": "Identificar utilizadores que abandonam o seu site de marketing. Melhorar a sua mensagem.", + "market_site_clarity_name": "Clareza do Site de Marketing", + "market_site_clarity_question_1_choice_1": "Sim, totalmente", + "market_site_clarity_question_1_choice_2": "Mais ou menos...", + "market_site_clarity_question_1_choice_3": "Não, de todo", + "market_site_clarity_question_1_headline": "Tem todas as informações de que precisa para experimentar $[projectName]?", + "market_site_clarity_question_2_headline": "O que está em falta ou não está claro para si sobre $[projectName]?", + "market_site_clarity_question_3_button_label": "Obtenha desconto", + "market_site_clarity_question_3_headline": "Obrigado pela sua resposta! Obtenha 25% de desconto nos primeiros 6 meses:", + "matrix": "Matriz", + "matrix_description": "Crie uma grelha para avaliar vários itens com o mesmo conjunto de critérios", + "measure_search_experience_description": "Meça quão relevantes são os seus resultados de pesquisa.", + "measure_search_experience_name": "Medir Experiência de Pesquisa", + "measure_search_experience_question_1_headline": "Quão relevantes são estes resultados de pesquisa?", + "measure_search_experience_question_1_lower_label": "Nada relevante", + "measure_search_experience_question_1_upper_label": "Muito relevante", + "measure_search_experience_question_2_headline": "Argh! O que torna os resultados irrelevantes para si?", + "measure_search_experience_question_2_placeholder": "Escreva a sua resposta aqui...", + "measure_search_experience_question_3_headline": "Ótimo! Há algo que possamos fazer para melhorar a sua experiência?", + "measure_search_experience_question_3_placeholder": "Escreva a sua resposta aqui...", + "measure_task_accomplishment_description": "Veja se as pessoas realizam a sua 'Tarefa a Ser Feita'. Pessoas bem-sucedidas são melhores clientes.", + "measure_task_accomplishment_name": "Medir Realização de Tarefas", + "measure_task_accomplishment_question_1_headline": "Conseguiu realizar o que veio fazer hoje?", + "measure_task_accomplishment_question_1_option_1_label": "Sim", + "measure_task_accomplishment_question_1_option_2_label": "A trabalhar nisso, chefe", + "measure_task_accomplishment_question_1_option_3_label": "Não", + "measure_task_accomplishment_question_2_headline": "Quão fácil foi alcançar o seu objetivo?", + "measure_task_accomplishment_question_2_lower_label": "Muito difícil", + "measure_task_accomplishment_question_2_upper_label": "Muito fácil", + "measure_task_accomplishment_question_3_headline": "O que tornou difícil?", + "measure_task_accomplishment_question_3_placeholder": "Escreva a sua resposta aqui...", + "measure_task_accomplishment_question_4_button_label": "Enviar", + "measure_task_accomplishment_question_4_headline": "Ótimo! O que veio fazer aqui hoje?", + "measure_task_accomplishment_question_5_button_label": "Enviar", + "measure_task_accomplishment_question_5_headline": "O que te impediu?", + "measure_task_accomplishment_question_5_placeholder": "Escreva a sua resposta aqui...", + "multi_select": "Seleção Múltipla", + "multi_select_description": "Peça aos respondentes para escolherem uma ou mais opções", + "new_integration_survey_description": "Descubra quais integrações os seus utilizadores gostariam de ver a seguir.", + "new_integration_survey_name": "Novo Inquérito de Integração", + "new_integration_survey_question_1_choice_1": "PostHog", + "new_integration_survey_question_1_choice_2": "Segmento", + "new_integration_survey_question_1_choice_3": "Hubspot", + "new_integration_survey_question_1_choice_4": "Twilio", + "new_integration_survey_question_1_choice_5": "Outro", + "new_integration_survey_question_1_headline": "Quais outras ferramentas está a utilizar?", + "next": "Seguinte", + "nps": "Net Promoter Score (NPS)", + "nps_description": "Medir o Net-Promoter-Score (0-10)", + "nps_lower_label": "Nada provável", + "nps_name": "Net Promoter Score (NPS)", + "nps_question_1_headline": "Qual a probabilidade de recomendar $[projectName] a um amigo ou colega?", + "nps_question_1_lower_label": "Pouco provável", + "nps_question_1_upper_label": "Muito provável", + "nps_question_2_headline": "O que o levou a dar essa classificação?", + "nps_survey_name": "Inquérito NPS", + "nps_survey_question_1_headline": "Qual a probabilidade de recomendar $[projectName] a um amigo ou colega?", + "nps_survey_question_1_lower_label": "Nada provável", + "nps_survey_question_1_upper_label": "Extremamente provável", + "nps_survey_question_2_headline": "Para nos ajudar a melhorar, pode descrever a(s) razão(ões) para a sua classificação?", + "nps_survey_question_3_headline": "Algum outro comentário, feedback ou preocupação?", + "nps_upper_label": "Extremamente provável", + "onboarding_segmentation": "Segmentação de Onboarding", + "onboarding_segmentation_description": "Saiba mais sobre quem se inscreveu no seu produto e porquê.", + "onboarding_segmentation_question_1_choice_1": "Fundador", + "onboarding_segmentation_question_1_choice_2": "Executivo", + "onboarding_segmentation_question_1_choice_3": "Gestor de Produto", + "onboarding_segmentation_question_1_choice_4": "Proprietário do Produto", + "onboarding_segmentation_question_1_choice_5": "Engenheiro de Software", + "onboarding_segmentation_question_1_headline": "Qual é o seu papel?", + "onboarding_segmentation_question_1_subheader": "Por favor, selecione uma das seguintes opções:", + "onboarding_segmentation_question_2_choice_1": "só eu", + "onboarding_segmentation_question_2_choice_2": "1-5 funcionários", + "onboarding_segmentation_question_2_choice_3": "6-10 funcionários", + "onboarding_segmentation_question_2_choice_4": "11-100 funcionários", + "onboarding_segmentation_question_2_choice_5": "mais de 100 funcionários", + "onboarding_segmentation_question_2_headline": "Qual é o tamanho da sua empresa?", + "onboarding_segmentation_question_2_subheader": "Por favor, selecione uma das seguintes opções:", + "onboarding_segmentation_question_3_choice_1": "Recomendação", + "onboarding_segmentation_question_3_choice_2": "Redes Sociais", + "onboarding_segmentation_question_3_choice_3": "Anúncios", + "onboarding_segmentation_question_3_choice_4": "Pesquisa Google", + "onboarding_segmentation_question_3_choice_5": "Num Podcast", + "onboarding_segmentation_question_3_headline": "Como ouviu falar de nós pela primeira vez?", + "onboarding_segmentation_question_3_subheader": "Por favor, selecione uma das seguintes opções:", + "picture_selection": "Seleção de Imagens", + "picture_selection_description": "Peça aos respondentes para escolherem uma ou mais imagens", + "preview_survey_ending_card_description": "Por favor, continue o seu onboarding.", + "preview_survey_ending_card_headline": "Conseguiste!", + "preview_survey_name": "Novo inquérito", + "preview_survey_question_1_headline": "Como classificaria {projectName}?", + "preview_survey_question_1_lower_label": "Não é bom", + "preview_survey_question_1_subheader": "Esta é uma pré-visualização do inquérito.", + "preview_survey_question_1_upper_label": "Muito bom", + "preview_survey_question_2_back_button_label": "Voltar", + "preview_survey_question_2_choice_1_label": "Sim, mantenha-me informado.", + "preview_survey_question_2_choice_2_label": "Não, obrigado!", + "preview_survey_question_2_headline": "Quer manter-se atualizado?", + "preview_survey_welcome_card_headline": "Bem-vindo!", + "preview_survey_welcome_card_html": "Obrigado por fornecer o seu feedback - vamos a isso!", + "prioritize_features_description": "Identificar as funcionalidades que os seus utilizadores mais e menos precisam.", + "prioritize_features_name": "Priorizar Funcionalidades", + "prioritize_features_question_1_choice_1": "Funcionalidade 1", + "prioritize_features_question_1_choice_2": "Funcionalidade 2", + "prioritize_features_question_1_choice_3": "Funcionalidade 3", + "prioritize_features_question_1_choice_4": "Outro", + "prioritize_features_question_1_headline": "Qual destas funcionalidades seria MAIS valiosa para si?", + "prioritize_features_question_2_choice_1": "Funcionalidade 1", + "prioritize_features_question_2_choice_2": "Funcionalidade 2", + "prioritize_features_question_2_choice_3": "Funcionalidade 3", + "prioritize_features_question_2_headline": "Qual destas funcionalidades seria MENOS valiosa para si?", + "prioritize_features_question_3_headline": "De que outra forma poderíamos melhorar a sua experiência com $[projectName]?", + "prioritize_features_question_3_placeholder": "Escreva a sua resposta aqui...", + "product_market_fit_short_description": "Meça a adequação do produto ao mercado avaliando o quão desapontados os utilizadores ficariam se o seu produto desaparecesse.", + "product_market_fit_short_name": "Inquérito de Adequação do Produto ao Mercado (Curto)", + "product_market_fit_short_question_1_choice_1": "Nada desapontado", + "product_market_fit_short_question_1_choice_2": "Um pouco desapontado", + "product_market_fit_short_question_1_choice_3": "Muito desapontado", + "product_market_fit_short_question_1_headline": "Quão desapontado ficaria se já não pudesse usar $[projectName]?", + "product_market_fit_short_question_1_subheader": "Por favor, selecione uma das seguintes opções:", + "product_market_fit_short_question_2_headline": "Como podemos melhorar $[projectName] para si?", + "product_market_fit_short_question_2_subheader": "Por favor, seja o mais específico possível.", + "product_market_fit_superhuman": "Adequação do Produto ao Mercado (Superhuman)", + "product_market_fit_superhuman_description": "Meça a adequação do produto ao mercado avaliando o quão desapontados os utilizadores ficariam se o seu produto desaparecesse.", + "product_market_fit_superhuman_question_1_button_label": "Feliz por ajudar!", + "product_market_fit_superhuman_question_1_dismiss_button_label": "Não, obrigado.", + "product_market_fit_superhuman_question_1_headline": "É um dos nossos utilizadores avançados! Tem 5 minutos?", + "product_market_fit_superhuman_question_1_html": "

Gostaríamos de entender melhor a sua experiência de utilizador. Partilhar a sua opinião ajuda muito.

", + "product_market_fit_superhuman_question_2_choice_1": "Nada desapontado", + "product_market_fit_superhuman_question_2_choice_2": "Um pouco desiludido", + "product_market_fit_superhuman_question_2_choice_3": "Muito desapontado", + "product_market_fit_superhuman_question_2_headline": "Quão desapontado ficaria se já não pudesse usar $[projectName]?", + "product_market_fit_superhuman_question_2_subheader": "Por favor, selecione uma das seguintes opções:", + "product_market_fit_superhuman_question_3_choice_1": "Fundador", + "product_market_fit_superhuman_question_3_choice_2": "Executivo", + "product_market_fit_superhuman_question_3_choice_3": "Gestor de Produto", + "product_market_fit_superhuman_question_3_choice_4": "Proprietário do Produto", + "product_market_fit_superhuman_question_3_choice_5": "Engenheiro de Software", + "product_market_fit_superhuman_question_3_headline": "Qual é o seu papel?", + "product_market_fit_superhuman_question_3_subheader": "Por favor, selecione uma das seguintes opções:", + "product_market_fit_superhuman_question_4_headline": "Que tipo de pessoas acha que mais beneficiariam de $[projectName]?", + "product_market_fit_superhuman_question_5_headline": "Qual é o principal benefício que recebe de $[projectName]?", + "product_market_fit_superhuman_question_6_headline": "Como podemos melhorar $[projectName] para si?", + "product_market_fit_superhuman_question_6_subheader": "Por favor, seja o mais específico possível.", + "professional_development_growth_survey_description": "Avaliar a satisfação dos funcionários com as oportunidades de crescimento e desenvolvimento profissional.", + "professional_development_growth_survey_name": "Inquérito de Crescimento e Desenvolvimento Profissional", + "professional_development_growth_survey_question_1_headline": "Sinto que tenho oportunidades para crescer e desenvolver as minhas competências no trabalho.", + "professional_development_growth_survey_question_1_lower_label": "Sem oportunidades de crescimento", + "professional_development_growth_survey_question_1_upper_label": "Muitas oportunidades de crescimento", + "professional_development_growth_survey_question_2_headline": "Tenho autonomia suficiente para tomar decisões sobre como faço o meu trabalho.", + "professional_development_growth_survey_question_2_lower_label": "Sem autonomia", + "professional_development_growth_survey_question_2_upper_label": "Autonomia completa", + "professional_development_growth_survey_question_3_headline": "Os meus objetivos no trabalho são claros e alinhados com o meu desenvolvimento.", + "professional_development_growth_survey_question_3_lower_label": "Objetivos pouco claros", + "professional_development_growth_survey_question_3_upper_label": "Objetivos claros e alinhados", + "professional_development_growth_survey_question_4_headline": "O que poderia ser melhorado para apoiar o seu crescimento profissional?", + "professional_development_growth_survey_question_4_placeholder": "Escreva a sua resposta aqui...", + "professional_development_survey_description": "Avaliar a satisfação dos funcionários com as oportunidades de crescimento e desenvolvimento profissional.", + "professional_development_survey_name": "Inquérito de Desenvolvimento Profissional", + "professional_development_survey_question_1_choice_1": "Sim", + "professional_development_survey_question_1_choice_2": "Não", + "professional_development_survey_question_1_headline": "Está interessado em atividades de desenvolvimento profissional?", + "professional_development_survey_question_2_choice_1": "Eventos de networking", + "professional_development_survey_question_2_choice_2": "Conferências ou seminários", + "professional_development_survey_question_2_choice_3": "Cursos ou workshops", + "professional_development_survey_question_2_choice_4": "Mentoria", + "professional_development_survey_question_2_choice_5": "Pesquisa individual", + "professional_development_survey_question_2_choice_6": "Outro", + "professional_development_survey_question_2_headline": "Que tipos de atividades de desenvolvimento profissional acha que seriam mais valiosas para o seu crescimento?", + "professional_development_survey_question_2_subheader": "Selecione todas as opções aplicáveis", + "professional_development_survey_question_3_choice_1": "Sim", + "professional_development_survey_question_3_choice_2": "Não", + "professional_development_survey_question_3_headline": "Dedicou tempo ao seu desenvolvimento profissional no passado?", + "professional_development_survey_question_4_headline": "Quão apoiado se sente no seu local de trabalho quando se trata de prosseguir o desenvolvimento profissional?", + "professional_development_survey_question_4_lower_label": "Nada apoiado", + "professional_development_survey_question_4_upper_label": "Extremamente apoiado", + "professional_development_survey_question_5_choice_1": "Para o meu próprio conhecimento", + "professional_development_survey_question_5_choice_2": "Para ganhar mais responsabilidades", + "professional_development_survey_question_5_choice_3": "Melhorar as minhas competências", + "professional_development_survey_question_5_choice_4": "Progredir na minha carreira atual", + "professional_development_survey_question_5_choice_5": "À procura de um novo emprego", + "professional_development_survey_question_5_choice_6": "Outro", + "professional_development_survey_question_5_headline": "Quais são as suas principais razões para querer dedicar tempo ao desenvolvimento profissional?", + "ranking": "Classificação", + "ranking_description": "Peça aos respondentes para ordenar os itens por preferência ou importância", + "rate_checkout_experience_description": "Permitir que os clientes avaliem a experiência de finalização de compra para ajustar a conversão.", + "rate_checkout_experience_name": "Avaliar Experiência de Finalização de Compra", + "rate_checkout_experience_question_1_headline": "Quão fácil ou difícil foi concluir o checkout?", + "rate_checkout_experience_question_1_lower_label": "Muito difícil", + "rate_checkout_experience_question_1_upper_label": "Muito fácil", + "rate_checkout_experience_question_2_headline": "Lamentamos! O que teria facilitado para si?", + "rate_checkout_experience_question_2_placeholder": "Escreva a sua resposta aqui...", + "rate_checkout_experience_question_3_headline": "Ótimo! Há algo que possamos fazer para melhorar a sua experiência?", + "rate_checkout_experience_question_3_placeholder": "Escreva a sua resposta aqui...", + "rating": "Classificação", + "rating_description": "Peça aos respondentes uma classificação (estrelas, smileys, números)", + "rating_lower_label": "Não é bom", + "rating_upper_label": "Muito bom", + "recognition_and_reward_survey_description": "Avaliar a satisfação dos funcionários com o reconhecimento, recompensas, apoio da liderança e liberdade de expressão.", + "recognition_and_reward_survey_name": "Reconhecimento e Recompensa", + "recognition_and_reward_survey_question_1_headline": "Quando desempenho bem, as minhas contribuições são reconhecidas pela organização.", + "recognition_and_reward_survey_question_1_lower_label": "Nada reconhecido", + "recognition_and_reward_survey_question_1_upper_label": "Altamente reconhecido", + "recognition_and_reward_survey_question_2_headline": "Sinto-me recompensado de forma justa pelo trabalho que faço.", + "recognition_and_reward_survey_question_2_lower_label": "Não recompensado de forma justa", + "recognition_and_reward_survey_question_2_upper_label": "Muito recompensado de forma justa", + "recognition_and_reward_survey_question_3_headline": "Sinto-me confortável em partilhar abertamente as minhas opiniões no trabalho.", + "recognition_and_reward_survey_question_3_lower_label": "Não confortável", + "recognition_and_reward_survey_question_3_upper_label": "Muito confortável", + "recognition_and_reward_survey_question_4_headline": "Como poderia a organização melhorar o reconhecimento e as recompensas?", + "recognition_and_reward_survey_question_4_placeholder": "Escreva a sua resposta aqui...", + "review_prompt_description": "Convide utilizadores que adoram o seu produto a avaliá-lo publicamente.", + "review_prompt_name": "Pedido de Avaliação", + "review_prompt_question_1_headline": "Como gosta de $[projectName]?", + "review_prompt_question_1_lower_label": "Não é bom", + "review_prompt_question_1_upper_label": "Muito satisfeito", + "review_prompt_question_2_button_label": "Escrever avaliação", + "review_prompt_question_2_headline": "Ficamos felizes em saber \uD83D\uDE4F Por favor, escreva uma avaliação para nós!", + "review_prompt_question_2_html": "

Isto ajuda-nos imenso.

", + "review_prompt_question_3_button_label": "Enviar", + "review_prompt_question_3_headline": "Lamentamos saber! O que é UMA coisa que podemos fazer melhor?", + "review_prompt_question_3_placeholder": "Escreva a sua resposta aqui...", + "review_prompt_question_3_subheader": "Ajude-nos a melhorar a sua experiência.", + "schedule_a_meeting": "Agendar uma reunião", + "schedule_a_meeting_description": "Peça aos respondentes para reservarem um horário para reuniões ou chamadas", + "single_select": "Seleção Única", + "single_select_description": "Ofereça uma lista de opções (escolha uma)", + "site_abandonment_survey": "Inquérito de Abandono do Site", + "site_abandonment_survey_description": "Compreenda as razões por trás do abandono do site na sua loja online.", + "site_abandonment_survey_question_1_html": "

Notámos que está a sair do nosso site sem fazer uma compra. Gostaríamos de entender porquê.

", + "site_abandonment_survey_question_2_button_label": "Claro!", + "site_abandonment_survey_question_2_dismiss_button_label": "Não, obrigado.", + "site_abandonment_survey_question_2_headline": "Tens um minuto?", + "site_abandonment_survey_question_3_choice_1": "Não consigo encontrar o que procuro", + "site_abandonment_survey_question_3_choice_2": "Encontrei um site melhor", + "site_abandonment_survey_question_3_choice_3": "O site é muito lento", + "site_abandonment_survey_question_3_choice_4": "Apenas a navegar", + "site_abandonment_survey_question_3_choice_5": "Encontrei um preço melhor noutro lugar", + "site_abandonment_survey_question_3_choice_6": "Outro", + "site_abandonment_survey_question_3_headline": "Qual é a principal razão para sair do nosso site?", + "site_abandonment_survey_question_3_subheader": "Por favor, selecione uma das seguintes opções:", + "site_abandonment_survey_question_4_headline": "Por favor, explique o motivo de ter abandonado o site:", + "site_abandonment_survey_question_5_headline": "Como classificaria a sua experiência geral no nosso site?", + "site_abandonment_survey_question_5_lower_label": "Muito insatisfeito", + "site_abandonment_survey_question_5_upper_label": "Muito satisfeito", + "site_abandonment_survey_question_6_choice_1": "Tempos de carregamento mais rápidos", + "site_abandonment_survey_question_6_choice_2": "Melhor funcionalidade de pesquisa de produtos", + "site_abandonment_survey_question_6_choice_3": "Mais variedade de produtos", + "site_abandonment_survey_question_6_choice_4": "Design do site melhorado", + "site_abandonment_survey_question_6_choice_5": "Mais avaliações de clientes", + "site_abandonment_survey_question_6_choice_6": "Outro", + "site_abandonment_survey_question_6_headline": "Que melhorias o incentivariam a permanecer mais tempo no nosso site?", + "site_abandonment_survey_question_6_subheader": "Por favor, selecione todas as opções aplicáveis:", + "site_abandonment_survey_question_7_headline": "Gostaria de receber atualizações sobre novos produtos e promoções?", + "site_abandonment_survey_question_7_label": "Sim, por favor entre em contacto.", + "site_abandonment_survey_question_8_headline": "Por favor, partilhe o seu endereço de email:", + "site_abandonment_survey_question_9_headline": "Algum comentário ou sugestão adicional?", + "skip": "Saltar", + "smileys_survey_name": "Inquérito Sorridente", + "smileys_survey_question_1_headline": "Como gosta de $[projectName]?", + "smileys_survey_question_1_lower_label": "Não é bom", + "smileys_survey_question_1_upper_label": "Muito satisfeito", + "smileys_survey_question_2_button_label": "Escrever avaliação", + "smileys_survey_question_2_headline": "Ficamos felizes em saber \uD83D\uDE4F Por favor, escreva uma avaliação para nós!", + "smileys_survey_question_2_html": "

Isto ajuda-nos imenso.

", + "smileys_survey_question_3_button_label": "Enviar", + "smileys_survey_question_3_headline": "Lamentamos saber! O que é UMA coisa que podemos fazer melhor?", + "smileys_survey_question_3_placeholder": "Escreva a sua resposta aqui...", + "smileys_survey_question_3_subheader": "Ajude-nos a melhorar a sua experiência.", + "star_rating_survey_name": "Inquérito de Avaliação de $[projectName]", + "star_rating_survey_question_1_headline": "Como gosta de $[projectName]?", + "star_rating_survey_question_1_lower_label": "Extremamente insatisfeito", + "star_rating_survey_question_1_upper_label": "Extremamente satisfeito", + "star_rating_survey_question_2_button_label": "Escrever avaliação", + "star_rating_survey_question_2_headline": "Ficamos felizes em saber \uD83D\uDE4F Por favor, escreva uma avaliação para nós!", + "star_rating_survey_question_2_html": "

Isto ajuda-nos imenso.

", + "star_rating_survey_question_3_button_label": "Enviar", + "star_rating_survey_question_3_headline": "Lamentamos saber! O que é UMA coisa que podemos fazer melhor?", + "star_rating_survey_question_3_placeholder": "Escreva a sua resposta aqui...", + "star_rating_survey_question_3_subheader": "Ajude-nos a melhorar a sua experiência.", + "statement_call_to_action": "Declaração (Chamada para Ação)", + "supportive_work_culture_survey_description": "Avaliar as perceções dos funcionários sobre o apoio da liderança, comunicação e o ambiente de trabalho geral.", + "supportive_work_culture_survey_name": "Cultura de Trabalho de Apoio", + "supportive_work_culture_survey_question_1_headline": "O meu gestor fornece-me o apoio de que preciso para concluir o meu trabalho.", + "supportive_work_culture_survey_question_1_lower_label": "Não apoiado", + "supportive_work_culture_survey_question_1_upper_label": "Altamente apoiado", + "supportive_work_culture_survey_question_2_headline": "A comunicação dentro da organização é aberta e eficaz.", + "supportive_work_culture_survey_question_2_lower_label": "Má comunicação", + "supportive_work_culture_survey_question_2_upper_label": "Excelente comunicação", + "supportive_work_culture_survey_question_3_headline": "O ambiente de trabalho é positivo e apoia o meu bem-estar.", + "supportive_work_culture_survey_question_3_lower_label": "Não apoiante", + "supportive_work_culture_survey_question_3_upper_label": "Muito apoiante", + "supportive_work_culture_survey_question_4_headline": "Como poderia a cultura de trabalho ser melhorada para o apoiar melhor?", + "supportive_work_culture_survey_question_4_placeholder": "Escreva a sua resposta aqui...", + "uncover_strengths_and_weaknesses_description": "Descubra o que os utilizadores gostam e não gostam no seu produto ou oferta.", + "uncover_strengths_and_weaknesses_name": "Descobrir Pontos Fortes e Fracos", + "uncover_strengths_and_weaknesses_question_1_choice_1": "Facilidade de uso", + "uncover_strengths_and_weaknesses_question_1_choice_2": "Boa relação qualidade/preço", + "uncover_strengths_and_weaknesses_question_1_choice_3": "É de código aberto", + "uncover_strengths_and_weaknesses_question_1_choice_4": "Os fundadores são giros", + "uncover_strengths_and_weaknesses_question_1_choice_5": "Outro", + "uncover_strengths_and_weaknesses_question_1_headline": "O que considera mais valioso no $[projectName]?", + "uncover_strengths_and_weaknesses_question_2_choice_1": "Documentação", + "uncover_strengths_and_weaknesses_question_2_choice_2": "Personalização", + "uncover_strengths_and_weaknesses_question_2_choice_3": "Preços", + "uncover_strengths_and_weaknesses_question_2_choice_4": "Outro", + "uncover_strengths_and_weaknesses_question_2_headline": "O que devemos melhorar?", + "uncover_strengths_and_weaknesses_question_2_subheader": "Por favor, selecione uma das seguintes opções:", + "uncover_strengths_and_weaknesses_question_3_headline": "Gostaria de acrescentar algo?", + "uncover_strengths_and_weaknesses_question_3_subheader": "Sinta-se à vontade para falar o que pensa, nós também o fazemos.", + "understand_low_engagement_description": "Identifique as razões para o baixo envolvimento para melhorar a adoção dos utilizadores.", + "understand_low_engagement_name": "Compreender o Baixo Envolvimento", + "understand_low_engagement_question_1_choice_1": "Difícil de usar", + "understand_low_engagement_question_1_choice_2": "Encontrei uma alternativa melhor", + "understand_low_engagement_question_1_choice_3": "Simplesmente não tive tempo", + "understand_low_engagement_question_1_choice_4": "Faltavam funcionalidades que preciso", + "understand_low_engagement_question_1_choice_5": "Outro", + "understand_low_engagement_question_1_headline": "Qual é a principal razão pela qual não voltou ao $[projectName] recentemente?", + "understand_low_engagement_question_2_headline": "O que é difícil em usar $[projectName]?", + "understand_low_engagement_question_2_placeholder": "Escreva a sua resposta aqui...", + "understand_low_engagement_question_3_headline": "Entendido. Qual a alternativa que está a usar em vez disso?", + "understand_low_engagement_question_3_placeholder": "Escreva a sua resposta aqui...", + "understand_low_engagement_question_4_headline": "Entendido. Como poderíamos tornar mais fácil para si começar?", + "understand_low_engagement_question_4_placeholder": "Escreva a sua resposta aqui...", + "understand_low_engagement_question_5_headline": "Entendido. Que funcionalidades ou características estavam em falta?", + "understand_low_engagement_question_5_placeholder": "Escreva a sua resposta aqui...", + "understand_low_engagement_question_6_headline": "Por favor, adicione mais detalhes:", + "understand_low_engagement_question_6_placeholder": "Escreva a sua resposta aqui...", + "understand_purchase_intention_description": "Descubra quão perto estão os seus visitantes de comprar ou subscrever.", + "understand_purchase_intention_name": "Compreender a Intenção de Compra", + "understand_purchase_intention_question_1_headline": "Qual a probabilidade de fazer compras connosco hoje?", + "understand_purchase_intention_question_1_lower_label": "Nada provável", + "understand_purchase_intention_question_1_upper_label": "Extremamente provável", + "understand_purchase_intention_question_2_headline": "Entendido. Qual é a sua principal razão para visitar hoje?", + "understand_purchase_intention_question_2_placeholder": "Escreva a sua resposta aqui...", + "understand_purchase_intention_question_3_headline": "O que, se alguma coisa, o está a impedir de fazer uma compra hoje?", + "understand_purchase_intention_question_3_placeholder": "Escreva a sua resposta aqui..." + } +} diff --git a/apps/web/lib/messages/zh-Hant-TW.json b/apps/web/lib/messages/zh-Hant-TW.json new file mode 100644 index 0000000000..6a6f694875 --- /dev/null +++ b/apps/web/lib/messages/zh-Hant-TW.json @@ -0,0 +1,2845 @@ +{ + "auth": { + "continue_with_azure": "使用 Azure 繼續", + "continue_with_email": "使用電子郵件繼續", + "continue_with_github": "使用 GitHub 繼續", + "continue_with_google": "使用 Google 繼續", + "continue_with_oidc": "使用 '{'oidcDisplayName'}' 繼續", + "continue_with_openid": "使用 OpenID 繼續", + "continue_with_saml": "使用 SAML SSO 繼續", + "forgot-password": { + "back_to_login": "返回登入", + "email-sent": { + "heading": "已成功請求重設密碼", + "text": "如果此電子郵件存在帳戶,您將很快收到重設密碼的說明。" + }, + "reset": { + "confirm_password": "確認密碼", + "new_password": "新密碼", + "no_token_provided": "未提供權杖", + "passwords_do_not_match": "密碼不符", + "success": { + "heading": "密碼重設成功", + "text": "您現在可以使用新密碼登入" + } + }, + "reset_password": "重設密碼" + }, + "invite": { + "create_account": "建立帳戶", + "email_does_not_match": "哎呀!電子郵件不符 \uD83E\uDD26", + "email_does_not_match_description": "邀請中的電子郵件與您的不符。", + "go_to_app": "前往應用程式", + "happy_to_have_you": "很高興能有你 \uD83E\uDD17", + "happy_to_have_you_description": "請建立帳戶或登入。", + "invite_expired": "邀請已過期 \uD83D\uDE25", + "invite_expired_description": "邀請有效期為 7 天。請請求新的邀請。", + "invite_not_found": "找不到邀請 \uD83D\uDE25", + "invite_not_found_description": "找不到邀請碼或已使用過。", + "login": "登入", + "welcome_to_organization": "您已加入 \uD83C\uDF89", + "welcome_to_organization_description": "歡迎加入組織。" + }, + "last_used": "上次使用", + "login": { + "backup_code": "備份碼", + "create_an_account": "建立帳戶", + "enter_your_backup_code": "輸入您的備份碼", + "enter_your_two_factor_authentication_code": "輸入您的雙重驗證碼", + "forgot_your_password": "忘記密碼?", + "login_to_your_account": "登入您的帳戶", + "login_with_email": "使用電子郵件登入", + "lost_access": "無法存取?", + "new_to_formbricks": "初次使用 Formbricks?", + "use_a_backup_code": "使用備份碼" + }, + "saml_connection_error": "發生錯誤。請檢查您的 app 主控台以取得更多詳細資料。", + "signup": { + "captcha_failed": "驗證碼失敗", + "have_an_account": "已有帳戶?", + "log_in": "登入", + "password_validation_contain_at_least_1_number": "包含至少 1 個數字", + "password_validation_minimum_8_and_maximum_128_characters": "最少 8 個 & 最多 128 個字元", + "password_validation_uppercase_and_lowercase": "混合使用大小寫字母", + "please_verify_captcha": "請驗證 reCAPTCHA", + "privacy_policy": "隱私權政策", + "terms_of_service": "服務條款", + "title": "建立您的 Formbricks 帳戶" + }, + "signup_without_verification_success": { + "user_successfully_created": "使用者建立成功", + "user_successfully_created_description": "您的新使用者已成功建立。請點擊下方按鈕並登入您的帳戶。" + }, + "testimonial_1": "我們在同一個平台上測量文件的清晰度,並從客戶流失中學習。很棒的產品,團隊反應非常迅速!", + "testimonial_all_features_included": "包含所有功能", + "testimonial_free_and_open_source": "免費且開源", + "testimonial_no_credit_card_required": "無需信用卡", + "testimonial_title": "將客戶洞察轉化為無法抗拒的體驗。", + "verification-requested": { + "invalid_email_address": "無效的電子郵件地址", + "invalid_token": "無效的權杖 ☹️", + "no_email_provided": "未提供電子郵件", + "please_click_the_link_in_the_email_to_activate_your_account": "請點擊電子郵件中的連結以啟用您的帳戶。", + "please_confirm_your_email_address": "請確認您的電子郵件地址", + "resend_verification_email": "重新發送驗證電子郵件", + "verification_email_successfully_sent": "驗證電子郵件已成功發送。請檢查您的收件匣。", + "we_sent_an_email_to": "我們已發送一封電子郵件至 '{'email'}'。", + "you_didnt_receive_an_email_or_your_link_expired": "您沒有收到電子郵件或您的連結已過期?" + }, + "verify": { + "no_token_provided": "未提供權杖", + "verifying": "驗證中..." + } + }, + "billing_confirmation": { + "back_to_billing_overview": "返回帳單概覽", + "thanks_for_upgrading": "非常感謝您升級您的 Formbricks 訂閱。", + "upgrade_successful": "升級成功" + }, + "common": { + "accepted": "已接受", + "account": "帳戶", + "account_settings": "帳戶設定", + "action": "操作", + "actions": "操作", + "active_surveys": "啟用中的問卷", + "activity": "活動", + "add": "新增", + "add_action": "新增操作", + "add_filter": "新增篩選器", + "add_logo": "新增標誌", + "add_project": "新增專案", + "add_to_team": "新增至團隊", + "all": "全部", + "all_questions": "所有問題", + "allow": "允許", + "allow_users_to_exit_by_clicking_outside_the_survey": "允許使用者點擊問卷外退出", + "an_unknown_error_occurred_while_deleting_table_items": "刪除 '{'type'}' 時發生未知錯誤", + "and": "且", + "and_response_limit_of": "且回應上限為", + "anonymous": "匿名", + "api_keys": "API 金鑰", + "app": "應用程式", + "app_survey": "應用程式問卷", + "apply_filters": "套用篩選器", + "are_you_sure": "您確定嗎?", + "are_you_sure_this_action_cannot_be_undone": "您確定嗎?此操作無法復原。", + "attributes": "屬性", + "avatar": "頭像", + "back": "返回", + "billing": "帳單", + "booked": "已預訂", + "bottom_left": "左下", + "bottom_right": "右下", + "cancel": "取消", + "centered_modal": "置中彈窗", + "choices": "選項", + "clear_all": "全部清除", + "clear_filters": "清除篩選器", + "clear_selection": "清除選取", + "click": "點擊", + "clicks": "點擊數", + "close": "關閉", + "code": "程式碼", + "collapse_rows": "摺疊列", + "completed": "已完成", + "configuration": "組態", + "confirm": "確認", + "connect": "連線", + "connect_formbricks": "連線 Formbricks", + "connected": "已連線", + "contacts": "聯絡人", + "copied_to_clipboard": "已複製到剪貼簿", + "copy": "複製", + "copy_code": "複製程式碼", + "copy_link": "複製連結", + "create_new_organization": "建立新組織", + "create_segment": "建立區隔", + "create_survey": "建立問卷", + "created": "已建立", + "created_at": "建立時間", + "created_by": "建立者", + "customer_success": "客戶成功", + "danger_zone": "危險區域", + "dark_overlay": "深色覆蓋", + "date": "日期", + "default": "預設", + "delete": "刪除", + "description": "描述", + "dev_env": "開發環境", + "development_environment_banner": "您正在開發環境中。設定它以測試問卷、操作和屬性。", + "disable": "停用", + "disallow": "不允許", + "discard": "捨棄", + "dismissed": "已關閉", + "docs": "文件", + "documentation": "文件", + "download": "下載", + "draft": "草稿", + "duplicate": "複製", + "e_commerce": "電子商務", + "edit": "編輯", + "email": "電子郵件", + "embed": "嵌入", + "enterprise_license": "企業授權", + "environment_not_found": "找不到環境", + "environment_notice": "您目前在 '{'environment'}' 環境中。", + "error": "錯誤", + "error_component_description": "此資源不存在或您沒有存取權限。", + "error_component_title": "載入資源錯誤", + "expand_rows": "展開列", + "finish": "完成", + "follow_these": "按照這些步驟", + "formbricks_version": "Formbricks 版本", + "full_name": "全名", + "gathering_responses": "收集回應中", + "general": "一般", + "go_back": "返回", + "go_to_dashboard": "前往儀表板", + "hidden": "隱藏", + "hidden_field": "隱藏欄位", + "hidden_fields": "隱藏欄位", + "hide": "隱藏", + "hide_column": "隱藏欄位", + "image": "圖片", + "images": "圖片", + "import": "匯入", + "impressions": "曝光數", + "imprint": "版本訊息", + "in_progress": "進行中", + "inactive_surveys": "停用中的問卷", + "input_type": "輸入類型", + "insights": "洞察", + "integration": "整合", + "integrations": "整合", + "invalid_date": "無效日期", + "invalid_file_type": "無效的檔案類型", + "invite": "邀請", + "invite_them": "邀請他們", + "key": "金鑰", + "label": "標籤", + "language": "語言", + "learn_more": "瞭解更多", + "license": "授權", + "light_overlay": "淺色覆蓋", + "limits_reached": "已達上限", + "link": "連結", + "link_and_email": "連結與電子郵件", + "link_copied": "連結已複製到剪貼簿!", + "link_survey": "連結問卷", + "link_surveys": "連結問卷", + "load_more": "載入更多", + "loading": "載入中", + "logo": "標誌", + "logout": "登出", + "look_and_feel": "外觀與風格", + "manage": "管理", + "marketing": "行銷", + "maximum": "最大值", + "member": "成員", + "members": "成員", + "membership_not_found": "找不到成員資格", + "metadata": "元數據", + "minimum": "最小值", + "mobile_overlay_text": "Formbricks 不適用於較小解析度的裝置。", + "move_down": "下移", + "move_up": "上移", + "multiple_languages": "多種語言", + "name": "名稱", + "negative": "負面", + "neutral": "中性", + "new": "新增", + "new_survey": "新增問卷", + "new_version_available": "Formbricks '{'version'}' 已推出。立即升級!", + "next": "下一步", + "no_background_image_found": "找不到背景圖片。", + "no_code": "無程式碼", + "no_files_uploaded": "沒有上傳任何檔案", + "no_result_found": "找不到結果", + "no_results": "沒有結果", + "no_surveys_found": "找不到問卷。", + "not_authenticated": "您未經授權執行此操作。", + "not_authorized": "未授權", + "not_connected": "未連線", + "note": "筆記", + "notes": "筆記", + "notifications": "通知", + "number": "數字", + "off": "關閉", + "on": "開啟", + "only_one_file_allowed": "僅允許一個檔案", + "only_owners_managers_and_manage_access_members_can_perform_this_action": "只有擁有者、管理員和管理存取權限的成員才能執行此操作。", + "or": "或", + "organization": "組織", + "organization_id": "組織 ID", + "organization_not_found": "找不到組織", + "organization_teams_not_found": "找不到組織團隊", + "other": "其他", + "others": "其他", + "overview": "概覽", + "password": "密碼", + "paused": "已暫停", + "pending_downgrade": "等待降級", + "people_manager": "人事經理", + "person": "人員", + "phone": "電話", + "photo_by": "照片來源:", + "pick_a_date": "選擇日期", + "placeholder": "提示文字", + "please_select_at_least_one_survey": "請選擇至少一個問卷", + "please_select_at_least_one_trigger": "請選擇至少一個觸發器", + "please_upgrade_your_plan": "請升級您的方案。", + "positive": "正面", + "preview": "預覽", + "preview_survey": "預覽問卷", + "privacy": "隱私權政策", + "privacy_policy": "隱私權政策", + "product_manager": "產品經理", + "profile": "個人資料", + "project": "專案", + "project_configuration": "專案組態", + "project_id": "專案 ID", + "project_name": "專案名稱", + "project_not_found": "找不到專案", + "project_permission_not_found": "找不到專案權限", + "projects": "專案", + "projects_limit_reached": "已達到專案上限", + "question": "問題", + "question_id": "問題 ID", + "questions": "問題", + "read_docs": "閱讀文件", + "remove": "移除", + "reorder_and_hide_columns": "重新排序和隱藏欄位", + "report_survey": "報告問卷", + "request_trial_license": "請求試用授權", + "reset_to_default": "重設為預設值", + "response": "回應", + "responses": "回應", + "restart": "重新開始", + "role": "角色", + "role_organization": "角色(組織)", + "saas": "SaaS", + "sales": "銷售", + "save": "儲存", + "save_changes": "儲存變更", + "scheduled": "已排程", + "search": "搜尋", + "security": "安全性", + "segment": "區隔", + "segments": "區隔", + "select": "選擇", + "select_all": "全選", + "select_survey": "選擇問卷", + "selected": "已選取", + "selected_questions": "選取的問題", + "selection": "選取", + "selections": "選取", + "send": "發送", + "send_test_email": "發送測試電子郵件", + "session_not_found": "找不到工作階段", + "settings": "設定", + "share_feedback": "分享回饋", + "show": "顯示", + "show_response_count": "顯示回應數", + "shown": "已顯示", + "size": "大小", + "skipped": "已跳過", + "skips": "跳過次數", + "some_files_failed_to_upload": "部分檔案上傳失敗", + "something_went_wrong_please_try_again": "發生錯誤。請再試一次。", + "sort_by": "排序方式", + "start_free_trial": "開始免費試用", + "status": "狀態", + "step_by_step_manual": "逐步手冊", + "styling": "樣式設定", + "submit": "提交", + "summary": "摘要", + "survey": "問卷", + "survey_completed": "問卷已完成。", + "survey_id": "問卷 ID", + "survey_languages": "問卷語言", + "survey_live": "問卷已上線", + "survey_not_found": "找不到問卷", + "survey_paused": "問卷已暫停。", + "survey_scheduled": "問卷已排程。", + "survey_type": "問卷類型", + "surveys": "問卷", + "switch_organization": "切換組織", + "switch_to": "切換至 '{'environment'}'", + "table_items_deleted_successfully": "'{'type'}' 已成功刪除", + "table_settings": "表格設定", + "tags": "標籤", + "targeting": "目標設定", + "team": "團隊", + "team_access": "團隊存取權限", + "team_name": "團隊名稱", + "teams": "存取控制", + "teams_not_found": "找不到團隊", + "text": "文字", + "time": "時間", + "time_to_finish": "完成時間", + "title": "標題", + "top_left": "左上", + "top_right": "右上", + "try_again": "再試一次", + "type": "類型", + "unlock_more_projects_with_a_higher_plan": "使用更高等級的方案解鎖更多專案。", + "update": "更新", + "updated": "已更新", + "updated_at": "更新時間", + "upload": "上傳", + "upload_input_description": "點擊或拖曳以上傳檔案。", + "url": "網址", + "user": "使用者", + "user_id": "使用者 ID", + "user_not_found": "找不到使用者", + "variable": "變數", + "variables": "變數", + "verified_email": "已驗證的電子郵件", + "video": "影片", + "warning": "警告", + "we_were_unable_to_verify_your_license_because_the_license_server_is_unreachable": "我們無法驗證您的授權,因為授權伺服器無法連線。", + "webhook": "Webhook", + "webhooks": "Webhooks", + "website_and_app_connection": "網站與應用程式連線", + "website_app_survey": "網站與應用程式問卷", + "website_survey": "網站問卷", + "weekly_summary": "每週摘要", + "welcome_card": "歡迎卡片", + "yes": "是", + "you": "您", + "you_are_downgraded_to_the_community_edition": "您已降級至社群版。", + "you_are_not_authorised_to_perform_this_action": "您未獲授權執行此操作。", + "you_have_reached_your_limit_of_project_limit": "您已達到 '{'projectLimit'}' 個專案的上限。", + "you_have_reached_your_monthly_miu_limit_of": "您已達到每月 MIU 上限:", + "you_have_reached_your_monthly_response_limit_of": "您已達到每月回應上限:", + "you_will_be_downgraded_to_the_community_edition_on_date": "您將於 '{'date'}' 降級至社群版。" + }, + "emails": { + "accept": "接受", + "click_or_drag_to_upload_files": "點擊或拖曳以上傳檔案。", + "email_customization_preview_email_heading": "嗨,'{'userName'}'", + "email_customization_preview_email_subject": "Formbricks 電子郵件自訂預覽", + "email_customization_preview_email_text": "這是電子郵件預覽,向您展示電子郵件中將呈現哪個標誌。", + "email_footer_text_1": "祝你有美好的一天!", + "email_footer_text_2": "Formbricks 團隊", + "email_template_text_1": "此電子郵件是通過 Formbricks 發送的。", + "embed_survey_preview_email_didnt_request": "沒有要求這個?", + "embed_survey_preview_email_environment_id": "環境 ID", + "embed_survey_preview_email_fight_spam": "幫助我們打擊垃圾郵件,並將此郵件轉寄至 hola@formbricks.com", + "embed_survey_preview_email_heading": "預覽電子郵件嵌入", + "embed_survey_preview_email_subject": "Formbricks 電子郵件問卷預覽", + "embed_survey_preview_email_text": "這是程式碼片段嵌入電子郵件中的樣子:", + "forgot_password_email_change_password": "變更密碼", + "forgot_password_email_did_not_request": "如果您沒有要求此操作,請忽略此電子郵件。", + "forgot_password_email_heading": "變更密碼", + "forgot_password_email_link_valid_for_24_hours": "此連結有效期為 24 小時。", + "forgot_password_email_subject": "重設您的 Formbricks 密碼", + "forgot_password_email_text": "您已請求變更密碼的連結。您可以點擊以下連結來執行此操作:", + "imprint": "版本訊息", + "invite_accepted_email_heading": "嗨", + "invite_accepted_email_subject": "您有一位新的組織成員!", + "invite_accepted_email_text_par1": "通知您,", + "invite_accepted_email_text_par2": "接受了您的邀請。合作愉快!", + "invite_email_button_label": "加入組織", + "invite_email_heading": "嗨", + "invite_email_text_par1": "您的同事", + "invite_email_text_par2": "邀請您加入 Formbricks。若要接受邀請,請點擊以下連結:", + "invite_member_email_subject": "您被邀請協作 Formbricks!", + "live_survey_notification_completed": "已完成", + "live_survey_notification_draft": "草稿", + "live_survey_notification_in_progress": "進行中", + "live_survey_notification_no_new_response": "本週沒有收到新的回應 \uD83D\uDD75️", + "live_survey_notification_no_responses_yet": "尚無回應!", + "live_survey_notification_paused": "已暫停", + "live_survey_notification_scheduled": "已排程", + "live_survey_notification_view_more_responses": "檢視另外 '{'responseCount'}' 個回應", + "live_survey_notification_view_previous_responses": "檢視先前的回應", + "live_survey_notification_view_response": "檢視回應", + "notification_footer_all_the_best": "祝您一切順利,", + "notification_footer_in_your_settings": "在您的設定中 \uD83D\uDE4F", + "notification_footer_please_turn_them_off": "請關閉它們", + "notification_footer_the_formbricks_team": "Formbricks 團隊 \uD83E\uDD0D", + "notification_footer_to_halt_weekly_updates": "若要停止每週更新,", + "notification_header_hey": "嗨 \uD83D\uDC4B", + "notification_header_weekly_report_for": "每週報告,適用於", + "notification_insight_completed": "已完成", + "notification_insight_completion_rate": "完成率 %", + "notification_insight_displays": "顯示次數", + "notification_insight_responses": "回應數", + "notification_insight_surveys": "問卷數", + "onboarding_invite_email_button_label": "加入 {inviterName} 的組織", + "onboarding_invite_email_connect_formbricks": "在幾分鐘內透過 HTML 片段或 NPM 將 Formbricks 連接到您的應用程式或網站。", + "onboarding_invite_email_create_account": "建立帳戶以加入 '{'inviterName'}' 的組織。", + "onboarding_invite_email_done": "完成 ✅", + "onboarding_invite_email_get_started_in_minutes": "在幾分鐘內開始使用", + "onboarding_invite_email_heading": "嗨 ", + "onboarding_invite_email_subject": "{inviterName} 需要幫忙設置 Formbricks。你能幫忙嗎?", + "password_changed_email_heading": "密碼已變更", + "password_changed_email_text": "您的密碼已成功變更。", + "password_reset_notify_email_subject": "您的 Formbricks 密碼已變更", + "privacy_policy": "隱私權政策", + "reject": "拒絕", + "render_email_response_value_file_upload_response_link_not_included": "由於資料隱私原因,未包含上傳檔案的連結", + "response_finished_email_subject": "{surveyName} 的回應已完成 ✅", + "response_finished_email_subject_with_email": "{personEmail} 剛剛完成了您的 {surveyName} 調查 ✅", + "schedule_your_meeting": "安排你的會議", + "select_a_date": "選擇日期", + "survey_response_finished_email_congrats": "恭喜,您收到了新的問卷回應!有人剛完成您的問卷:'{'surveyName'}'", + "survey_response_finished_email_dont_want_notifications": "不想收到這些通知?", + "survey_response_finished_email_hey": "嗨 \uD83D\uDC4B", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "關閉所有新建立表單的通知", + "survey_response_finished_email_turn_off_notifications_for_this_form": "關閉此表單的通知", + "survey_response_finished_email_view_more_responses": "檢視另外 '{'responseCount'}' 個回應", + "survey_response_finished_email_view_survey_summary": "檢視問卷摘要", + "verification_email_click_on_this_link": "您也可以點擊此連結:", + "verification_email_heading": "快完成了!", + "verification_email_hey": "嗨 \uD83D\uDC4B", + "verification_email_if_expired_request_new_token": "如果已過期,請在此處請求新的權杖:", + "verification_email_link_valid_for_24_hours": "此連結有效期為 24 小時。", + "verification_email_request_new_verification": "請求新的驗證", + "verification_email_subject": "請驗證您的電子郵件以使用 Formbricks", + "verification_email_survey_name": "問卷名稱", + "verification_email_take_survey": "填寫問卷", + "verification_email_text": "若要開始使用 Formbricks,請驗證您下方的電子郵件:", + "verification_email_thanks": "感謝您驗證您的電子郵件!", + "verification_email_to_fill_survey": "若要填寫問卷,請點擊下方的按鈕:", + "verification_email_verify_email": "驗證電子郵件", + "verified_link_survey_email_subject": "您的 survey 已準備好填寫。", + "weekly_summary_create_reminder_notification_body_cal_slot": "在我們 CEO 的日曆中選擇一個 15 分鐘的時段", + "weekly_summary_create_reminder_notification_body_dont_let_a_week_pass": "不要讓一週過去而沒有了解您的使用者:", + "weekly_summary_create_reminder_notification_body_need_help": "需要協助找到適合您產品的問卷嗎?", + "weekly_summary_create_reminder_notification_body_reply_email": "或回覆此電子郵件 :)", + "weekly_summary_create_reminder_notification_body_setup_a_new_survey": "設定新的問卷", + "weekly_summary_create_reminder_notification_body_text": "我們很樂意向您發送每週摘要,但目前 '{'projectName'}' 沒有正在執行的問卷。", + "weekly_summary_email_subject": "{projectName} 用戶洞察 - 上週 by Formbricks" + }, + "environments": { + "actions": { + "action_copied_successfully": "操作已成功複製", + "action_copy_failed": "操作複製失敗", + "action_created_successfully": "操作已成功建立", + "action_deleted_successfully": "操作已成功刪除", + "action_type": "操作類型", + "action_updated_successfully": "操作已成功更新", + "action_with_key_already_exists": "金鑰為 '{'key'}' 的操作已存在", + "action_with_name_already_exists": "名稱為 '{'name'}' 的操作已存在", + "add_css_class_or_id": "新增 CSS 類別或 ID", + "add_url": "新增網址", + "click": "點擊", + "contains": "包含", + "create_action": "建立操作", + "css_selector": "CSS 選取器", + "delete_action_text": "您確定要刪除此操作嗎?這也會從您的所有問卷中移除此操作作為觸發器。", + "display_name": "顯示名稱", + "does_not_contain": "不包含", + "does_not_exactly_match": "不完全相符", + "eg_clicked_download": "例如,點擊下載", + "eg_download_cta_click_on_home": "例如,download_cta_click_on_home", + "eg_install_app": "例如,安裝應用程式", + "eg_user_clicked_download_button": "例如,使用者點擊了下載按鈕", + "ends_with": "結尾為", + "enter_a_url_to_see_if_a_user_visiting_it_would_be_tracked": "輸入網址以查看造訪該網址的使用者是否會被追蹤。", + "exactly_matches": "完全相符", + "exit_intent": "離開意圖", + "fifty_percent_scroll": "50% 捲動", + "how_do_code_actions_work": "程式碼操作如何運作?", + "if_a_user_clicks_a_button_with_a_specific_css_class_or_id": "如果使用者點擊具有特定 CSS 類別或 ID 的按鈕", + "if_a_user_clicks_a_button_with_a_specific_text": "如果使用者點擊具有特定文字的按鈕", + "in_your_code_read_more_in_our": "在您的程式碼中。在我們的文件中閱讀更多內容", + "inner_text": "內部文字", + "invalid_css_selector": "無效的 CSS 選取器", + "limit_the_pages_on_which_this_action_gets_captured": "限制擷取此操作的頁面", + "limit_to_specific_pages": "限制為特定頁面", + "on_all_pages": "在所有頁面上", + "page_filter": "頁面篩選器", + "page_view": "頁面檢視", + "select_match_type": "選取比對類型", + "starts_with": "開頭為", + "test_match": "測試比對", + "test_your_url": "測試您的網址", + "this_action_was_created_automatically_you_cannot_make_changes_to_it": "此操作是自動建立的。您無法對其進行變更。", + "this_action_will_be_triggered_when_the_page_is_loaded": "當頁面載入時,將觸發此操作。", + "this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "當使用者捲動頁面 50% 時,將觸發此操作。", + "this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "當使用者嘗試離開頁面時,將觸發此操作。", + "this_is_a_code_action_please_make_changes_in_your_code_base": "這是一個 code 動作。請在您的 code base 中進行更改。", + "track_new_user_action": "追蹤新使用者操作", + "track_user_action_to_display_surveys_or_create_user_segment": "追蹤使用者操作以顯示問卷或建立使用者區隔。", + "url": "網址", + "user_actions": "使用者操作", + "user_clicked_download_button": "使用者點擊了下載按鈕", + "what_did_your_user_do": "您的使用者做了什麼?", + "what_is_the_user_doing": "使用者正在做什麼?", + "you_can_track_code_action_anywhere_in_your_app_using": "您可以使用以下方式在您的應用程式中的任何位置追蹤程式碼操作" + }, + "connect": { + "congrats": "恭喜!", + "connection_successful_message": "做得好!我們已連線。", + "do_it_later": "稍後再做", + "finish_onboarding": "完成新手上路", + "headline": "連線您的應用程式或網站", + "import_formbricks_and_initialize_the_widget_in_your_component": "匯入 Formbricks 並在您的元件中初始化小工具(例如,App.tsx):", + "insert_this_code_into_the_head_tag_of_your_website": "將此程式碼插入您網站的 head 標籤中:", + "subtitle": "只需不到 4 分鐘。", + "waiting_for_your_signal": "正在等待您的訊號..." + }, + "contacts": { + "contact_deleted_successfully": "聯絡人已成功刪除", + "contact_not_found": "找不到此聯絡人", + "contacts_table_refresh": "重新整理聯絡人", + "contacts_table_refresh_error": "重新整理聯絡人時發生錯誤,請再試一次", + "contacts_table_refresh_success": "聯絡人已成功重新整理", + "first_name": "名字", + "last_name": "姓氏", + "no_responses_found": "找不到回應", + "not_provided": "未提供", + "search_contact": "搜尋聯絡人", + "select_attribute": "選取屬性", + "unlock_contacts_description": "管理聯絡人並發送目標問卷", + "unlock_contacts_title": "使用更高等級的方案解鎖聯絡人", + "upload_contacts_modal_attributes_description": "將 CSV 中的欄位對應到 Formbricks 中的屬性。", + "upload_contacts_modal_attributes_new": "新增屬性", + "upload_contacts_modal_attributes_search_or_add": "搜尋或新增屬性", + "upload_contacts_modal_attributes_should_be_mapped_to": "應對應到", + "upload_contacts_modal_attributes_title": "屬性", + "upload_contacts_modal_description": "上傳 CSV 以快速匯入具有屬性的聯絡人", + "upload_contacts_modal_download_example_csv": "下載範例 CSV", + "upload_contacts_modal_duplicates_description": "如果聯絡人已存在於您的聯絡人中,我們應該如何處理?", + "upload_contacts_modal_duplicates_overwrite_description": "覆寫現有聯絡人", + "upload_contacts_modal_duplicates_overwrite_title": "覆寫", + "upload_contacts_modal_duplicates_skip_description": "略過重複的聯絡人", + "upload_contacts_modal_duplicates_skip_title": "略過", + "upload_contacts_modal_duplicates_title": "重複項目", + "upload_contacts_modal_duplicates_update_description": "更新現有聯絡人", + "upload_contacts_modal_duplicates_update_title": "更新", + "upload_contacts_modal_pick_different_file": "選取不同的檔案", + "upload_contacts_modal_preview": "這是您的資料預覽。", + "upload_contacts_modal_upload_btn": "上傳聯絡人" + }, + "experience": { + "all": "全部", + "all_time": "全部時間", + "analysed_feedbacks": "已分析的自由文字答案", + "category": "類別", + "category_updated_successfully": "類別已成功更新!", + "complaint": "投訴", + "did_you_find_this_insight_helpful": "您覺得此洞察有幫助嗎?", + "failed_to_update_category": "更新類別失敗", + "feature_request": "請求", + "good_afternoon": "\uD83C\uDF24️ 午安", + "good_evening": "\uD83C\uDF19 晚安", + "good_morning": "☀️ 早安", + "insights_description": "從您所有問卷的回應中產生的所有洞察", + "insights_for_project": "'{'projectName'}' 的洞察", + "new_responses": "回應數", + "no_insights_for_this_filter": "此篩選器沒有洞察", + "no_insights_found": "找不到洞察。收集更多問卷回應或為您現有的問卷啟用洞察以開始使用。", + "praise": "讚美", + "sentiment_score": "情緒分數", + "templates_card_description": "選擇一個範本或從頭開始", + "templates_card_title": "衡量您的客戶體驗", + "this_month": "本月", + "this_quarter": "本季", + "this_week": "本週", + "today": "今天" + }, + "formbricks_logo": "Formbricks 標誌", + "integrations": { + "activepieces_integration_description": "立即將 Formbricks 與熱門應用程式連接,以在無需編碼的情況下自動執行任務。", + "additional_settings": "其他設定", + "airtable": { + "airtable_base": "Airtable 資料庫", + "airtable_integration": "Airtable 整合", + "airtable_integration_description": "直接與 Airtable 同步回應。", + "airtable_integration_is_not_configured": "尚未設定 Airtable 整合", + "connect_with_airtable": "連線 Airtable", + "link_airtable_table": "連結 Airtable 表格", + "link_new_table": "連結新表格", + "no_bases_found": "找不到 Airtable 資料庫", + "no_integrations_yet": "您的 airtable 整合將在您新增後立即顯示在此處。⏲️", + "please_create_a_base": "請在 Airtable 上建立資料庫", + "please_select_a_base": "請選取資料庫", + "please_select_a_table": "請選取表格", + "sync_responses_with_airtable": "與 Airtable 同步回應", + "table_name": "表格名稱" + }, + "airtable_integration_description": "使用問卷資料立即填入您的 Airtable 表格", + "connected_with_email": "已與 '{'email'}' 連線", + "connecting_integration_failed_please_try_again": "連線整合失敗。請再試一次!", + "create_survey_warning": "您必須建立問卷才能設定此整合", + "delete_integration": "刪除整合", + "delete_integration_confirmation": "您確定要刪除此整合嗎?", + "google_sheet_integration_description": "使用問卷資料立即填入您的試算表", + "google_sheets": { + "connect_with_google_sheets": "連線 Google 試算表", + "enter_a_valid_spreadsheet_url_error": "請輸入有效的試算表網址", + "google_connection": "Google 連線", + "google_connection_deletion_description": "直接與 Google 試算表同步回應。", + "google_sheet_integration_is_not_configured": "您的 Formbricks 執行個體中尚未設定 Google 試算表整合。", + "google_sheet_logo": "Google 試算表標誌", + "google_sheet_name": "Google 試算表名稱", + "google_sheets_integration": "Google 試算表整合", + "google_sheets_integration_description": "直接與 Google 試算表同步回應。", + "link_google_sheet": "連結 Google 試算表", + "link_new_sheet": "連結新試算表", + "no_integrations_yet": "您的 Google 試算表整合將在您新增後立即顯示在此處。⏲️", + "spreadsheet_url": "試算表網址" + }, + "include_created_at": "包含建立於", + "include_hidden_fields": "包含隱藏欄位", + "include_metadata": "包含元數據(瀏覽器、國家/地區等)", + "include_variables": "包含變數", + "integration_added_successfully": "整合已成功新增", + "integration_removed_successfully": "整合已成功移除", + "integration_updated_successfully": "整合已成功更新", + "make_integration_description": "透過 Make 將 Formbricks 與 1000 多個應用程式整合", + "manage_webhooks": "管理 Webhook", + "n8n_integration_description": "透過 n8n 將 Formbricks 與 350 多個應用程式整合", + "notion": { + "col_name_of_type_is_not_supported": "Notion API 不支援類型為 '{'type'}' 的 '{'col_name'}'。資料將不會反映在您的 Notion 資料庫中。", + "connect_with_notion": "連線 Notion", + "connected_with_workspace": "已與 '{'workspace'}' 工作區連線", + "create_at_least_one_database_to_setup_this_integration": "您必須建立至少一個資料庫才能設定此整合", + "database_name": "資料庫名稱", + "duplicate_connection_warning": "與此資料庫的連線處於活動狀態。請謹慎變更。", + "link_database": "連結資料庫", + "link_new_database": "連結新資料庫", + "link_notion_database": "連結 Notion 資料庫", + "map_formbricks_fields_to_notion_property": "將 Formbricks 欄位對應到 Notion 屬性", + "no_databases_found": "您的 Notion 整合將在您新增後立即顯示在此處。⏲️", + "notion_integration": "Notion 整合", + "notion_integration_description": "直接將回應傳送至 Notion。", + "notion_integration_is_not_configured": "您的 Formbricks 執行個體中尚未設定 Notion 整合。", + "notion_logo": "Notion 標誌", + "please_complete_mapping_fields_with_notion_property": "請完成將欄位對應到 Notion 屬性", + "please_resolve_mapping_errors": "請解決對應錯誤", + "please_select_a_database": "請選取資料庫", + "please_select_at_least_one_mapping": "請選取至少一個對應", + "que_name_of_type_cant_be_mapped_to": "類型為 '{'question_label'}' 的 '{'que_name'}' 無法對應到類型為 '{'col_type'}' 的欄位 '{'col_name'}'。請改用類型為 '{'mapped_type'}' 的欄位。", + "select_a_database": "選取資料庫", + "select_a_field_to_map": "選取要對應的欄位", + "select_a_survey_question": "選取問卷問題", + "sync_responses_with_a_notion_database": "與 Notion 資料庫同步回應", + "update_connection": "重新連線 Notion", + "update_connection_tooltip": "重新連接整合以包含新添加的資料庫。您現有的整合將保持不變。" + }, + "notion_integration_description": "將資料傳送至您的 Notion 資料庫", + "please_select_a_survey_error": "請選取問卷", + "select_at_least_one_question_error": "請選取至少一個問題", + "slack": { + "already_connected_another_survey": "您已將另一個問卷連線到此頻道。", + "channel_name": "頻道名稱", + "connect_with_slack": "連線 Slack", + "connect_your_first_slack_channel": "連線您的第一個 Slack 頻道以開始使用。", + "connected_with_team": "已與 '{'team'}' 連線", + "create_at_least_one_channel_error": "您必須建立至少一個頻道才能設定此整合", + "dont_see_your_channel": "找不到您的頻道?", + "link_channel": "連結頻道", + "link_slack_channel": "連結 Slack 頻道", + "please_select_a_channel": "請選取頻道", + "select_channel": "選取頻道", + "slack_integration": "Slack 整合", + "slack_integration_description": "直接將回應傳送至 Slack。", + "slack_integration_is_not_configured": "您的 Formbricks 執行個體中尚未設定 Slack 整合。", + "slack_reconnect_button": "重新連線", + "slack_reconnect_button_description": "注意:我們最近變更了我們的 Slack 整合以支援私人頻道。請重新連線您的 Slack 工作區。" + }, + "slack_integration_description": "將您的 Slack 工作區與 Formbricks 立即連線", + "to_configure_it": "進行設定。", + "webhook_integration_description": "根據您問卷中的操作觸發 Webhook", + "webhooks": { + "add_webhook": "新增 Webhook", + "add_webhook_description": "將問卷回應資料傳送至自訂端點", + "all_current_and_new_surveys": "所有目前和新的問卷", + "created_by_third_party": "由第三方建立", + "discord_webhook_not_supported": "目前不支援 Discord webhooks。", + "empty_webhook_message": "您的 Webhook 將在您新增後立即顯示在此處。⏲️", + "endpoint_pinged": "耶!我們能夠 ping Webhook!", + "endpoint_pinged_error": "無法 ping Webhook!", + "please_check_console": "請檢查主控台以取得更多詳細資料", + "please_enter_a_url": "請輸入網址", + "response_created": "已建立回應", + "response_finished": "已完成回應", + "response_updated": "已更新回應", + "source": "來源", + "test_endpoint": "測試端點", + "triggers": "觸發器", + "webhook_added_successfully": "Webhook 已成功新增", + "webhook_delete_confirmation": "您確定要刪除此 Webhook 嗎?這將停止向您發送任何進一步的通知。", + "webhook_deleted_successfully": "Webhook 已成功刪除", + "webhook_name_placeholder": "選填:為您的 Webhook 加上標籤以便於識別", + "webhook_test_failed_due_to": "Webhook 測試因以下原因失敗", + "webhook_updated_successfully": "Webhook 已成功更新。", + "webhook_url_placeholder": "貼上您要觸發事件的網址" + }, + "website_or_app_integration_description": "將 Formbricks 整合到您的網站或應用程式中", + "zapier_integration_description": "透過 Zapier 將 Formbricks 與 5000 多個應用程式整合" + }, + "project": { + "api_keys": { + "add_api_key": "新增 API 金鑰", + "api_key": "API 金鑰", + "api_key_copied_to_clipboard": "API 金鑰已複製到剪貼簿", + "api_key_created": "API 金鑰已建立", + "api_key_deleted": "API 金鑰已刪除", + "api_key_label": "API 金鑰標籤", + "api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。", + "api_key_updated": "API 金鑰已更新", + "duplicate_access": "不允許重複的 project 存取", + "no_api_keys_yet": "您還沒有任何 API 金鑰", + "no_env_permissions_found": "找不到環境權限", + "organization_access": "組織 Access", + "permissions": "權限", + "project_access": "專案存取", + "secret": "密碼", + "unable_to_delete_api_key": "無法刪除 API 金鑰" + }, + "app-connection": { + "api_host_description": "這是您 Formbricks 後端的網址。", + "app_connection": "應用程式連線", + "app_connection_description": "將您的應用程式連線至 Formbricks。", + "check_out_the_docs": "查看文件。", + "dive_into_the_docs": "深入瞭解文件。", + "does_your_widget_work": "您的小工具運作嗎?", + "environment_id": "您的 EnvironmentId", + "environment_id_description": "此 ID 可唯一識別此 Formbricks 環境。", + "environment_id_description_with_environment_id": "用於識別正確的環境:'{'environmentId'}' 是您的。", + "formbricks_sdk": "Formbricks SDK", + "formbricks_sdk_connected": "Formbricks SDK 已連線", + "formbricks_sdk_not_connected": "Formbricks SDK 尚未連線。", + "formbricks_sdk_not_connected_description": "將您的網站或應用程式與 Formbricks 連線", + "have_a_problem": "有問題嗎?", + "how_to_setup": "如何設定", + "how_to_setup_description": "請按照這些步驟在您的應用程式中設定 Formbricks 小工具。", + "identifying_your_users": "識別您的使用者", + "if_you_are_planning_to": "如果您計劃", + "insert_this_code_into_the": "將此程式碼插入", + "need_a_more_detailed_setup_guide_for": "需要更詳細的設定指南,適用於", + "not_working": "無法運作?", + "open_an_issue_on_github": "在 GitHub 上開啟問題", + "open_the_browser_console_to_see_the_logs": "開啟瀏覽器主控台以查看記錄。", + "receiving_data": "正在接收資料 \uD83D\uDC83\uD83D\uDD7A", + "recheck": "重新檢查", + "scroll_to_the_top": "捲動至頂端!", + "step_1": "步驟 1:使用 pnpm、npm 或 yarn 安裝", + "step_2": "步驟 2:初始化小工具", + "step_2_description": "匯入 Formbricks 並在您的元件中初始化小工具(例如,App.tsx):", + "step_3": "步驟 3:偵錯模式", + "switch_on_the_debug_mode_by_appending": "藉由附加以下項目開啟偵錯模式", + "tag_of_your_app": "您應用程式的標籤", + "to_the_url_where_you_load_the": "到您載入", + "want_to_learn_how_to_add_user_attributes": "想瞭解如何新增使用者屬性、自訂事件等嗎?", + "you_are_done": "您已完成 \uD83C\uDF89", + "you_can_set_the_user_id_with": "您可以使用 user id 設定", + "your_app_now_communicates_with_formbricks": "您的應用程式現在可與 Formbricks 通訊 - 自動傳送事件和載入問卷!" + }, + "general": { + "cannot_delete_only_project": "這是您唯一的專案,無法刪除。請先建立新專案。", + "delete_project": "刪除專案", + "delete_project_confirmation": "您確定要刪除 '{'projectName'}' 嗎?此操作無法復原。", + "delete_project_name_includes_surveys_responses_people_and_more": "刪除 '{'projectName'}',包括所有問卷、回應、人員、操作和屬性。", + "delete_project_settings_description": "刪除包含所有問卷、回應、人員、操作和屬性的專案。此操作無法復原。", + "error_saving_project_information": "儲存專案資訊時發生錯誤", + "only_owners_or_managers_can_delete_projects": "只有擁有者或管理員可以刪除專案", + "project_deleted_successfully": "專案已成功刪除", + "project_name_settings_description": "變更您的專案名稱。", + "project_name_updated_successfully": "專案名稱已成功更新", + "recontact_waiting_time": "重新聯絡等待時間", + "recontact_waiting_time_settings_description": "控制使用者在所有應用程式問卷中可以被調查的頻率。", + "this_action_cannot_be_undone": "此操作無法復原。", + "wait_x_days_before_showing_next_survey": "在顯示下一個問卷之前等待 X 天:", + "waiting_period_updated_successfully": "等待時間已成功更新", + "whats_your_project_called": "您的專案名稱為何?" + }, + "languages": { + "add_language": "新增語言", + "alias": "別名", + "alias_tooltip": "別名是替代名稱,用於在連結問卷和 SDK 中識別語言(選填)", + "cannot_remove_language_warning": "您無法移除此語言,因為它仍在這些問卷中使用:", + "conflict_between_identifier_and_alias": "新增語言的識別碼與您的別名之一之間存在衝突。別名和識別碼不能相同。", + "conflict_between_selected_alias_and_another_language": "所選別名與另一個具有此識別碼的語言之間存在衝突。請將具有此識別碼的語言新增至您的專案,以避免不一致。", + "delete_language_confirmation": "您確定要刪除此語言嗎?此操作無法復原。", + "duplicate_language_or_language_id": "重複的語言或語言 ID", + "edit_languages": "編輯語言", + "identifier": "識別碼 (ISO)", + "incomplete_translations": "不完整的翻譯", + "language": "語言", + "language_deleted_successfully": "語言已成功刪除", + "languages_updated_successfully": "語言已成功更新", + "multi_language_surveys": "多語言問卷", + "multi_language_surveys_description": "新增語言以建立多語言問卷。", + "no_language_found": "找不到語言。在下方新增您的第一個語言。", + "please_select_a_language": "請選取語言", + "remove_language": "移除語言", + "remove_language_from_surveys_to_remove_it_from_project": "請從這些問卷中移除語言,以便從專案中移除。", + "search_items": "搜尋項目", + "translate": "翻譯" + }, + "look": { + "add_background_color": "新增背景顏色", + "add_background_color_description": "將背景顏色新增至標誌容器。", + "app_survey_placement": "應用程式問卷位置", + "app_survey_placement_settings_description": "變更問卷在您的 Web 應用程式或網站中的顯示位置。", + "centered_modal_overlay_color": "置中彈窗覆蓋顏色", + "email_customization": "電子郵件自訂", + "email_customization_description": "變更 Formbricks 代表您發送的電子郵件的外觀和風格。", + "enable_custom_styling": "啟用自訂樣式", + "enable_custom_styling_description": "允許使用者在問卷編輯器中覆寫此主題。", + "failed_to_remove_logo": "無法移除標誌", + "failed_to_update_logo": "無法更新標誌", + "formbricks_branding": "Formbricks 品牌", + "formbricks_branding_hidden": "Formbricks 品牌已隱藏。", + "formbricks_branding_settings_description": "我們很感謝您的支持,但如果您關閉它,我們也理解。", + "formbricks_branding_shown": "Formbricks 品牌已顯示。", + "logo_removed_successfully": "標誌已成功移除", + "logo_settings_description": "上傳您的公司標誌以品牌化問卷和連結預覽。", + "logo_updated_successfully": "標誌已成功更新", + "logo_upload_failed": "標誌上傳失敗。請再試一次。", + "placement_updated_successfully": "位置已成功更新", + "remove_branding_with_a_higher_plan": "使用更高等級的方案移除品牌", + "remove_logo": "移除標誌", + "remove_logo_confirmation": "您確定要移除標誌嗎?", + "replace_logo": "取代標誌", + "reset_styling": "重設樣式", + "reset_styling_confirmation": "您確定要將樣式重設為預設值嗎?", + "show_formbricks_branding_in": "在 '{'type'}' 問卷中顯示 Formbricks 品牌", + "show_powered_by_formbricks": "顯示「由 Formbricks 提供技術支援」簽名", + "styling_updated_successfully": "樣式已成功更新", + "theme": "主題", + "theme_settings_description": "為所有問卷建立樣式主題。您可以為每個問卷啟用自訂樣式。" + }, + "tags": { + "add": "新增", + "add_tag": "新增標籤", + "count": "計數", + "delete_tag_confirmation": "您確定要刪除此標籤嗎?", + "empty_message": "標記提交內容,在此處找到您的標籤清單。", + "manage_tags": "管理標籤", + "manage_tags_description": "合併和移除回應標籤。", + "merge": "合併", + "no_tag_found": "找不到標籤", + "search_tags": "搜尋標籤...", + "tag": "標籤", + "tag_already_exists": "標籤已存在", + "tag_deleted": "標籤已刪除", + "tag_updated": "標籤已更新", + "tags_merged": "標籤已合併", + "unique_constraint_failed_on_the_fields": "欄位上唯一性限制失敗" + }, + "teams": { + "manage_teams": "管理團隊", + "no_teams_found": "找不到團隊", + "only_organization_owners_and_managers_can_manage_teams": "只有組織擁有者和管理員才能管理團隊。", + "permission": "權限", + "team_name": "團隊名稱", + "team_settings_description": "查看哪些團隊可以存取此專案。" + } + }, + "projects_environments_organizations_not_found": "找不到專案、環境或組織", + "segments": { + "add_filter_below": "在下方新增篩選器", + "add_your_first_filter_to_get_started": "新增您的第一個篩選器以開始使用", + "cannot_delete_segment_used_in_surveys": "您無法刪除此區隔,因為它仍在這些問卷中使用:", + "clone_and_edit_segment": "複製和編輯區隔", + "create_group": "建立群組", + "create_your_first_segment": "建立您的第一個區隔以開始使用", + "delete_segment": "刪除區隔", + "desktop": "桌面版", + "devices": "裝置", + "edit_segment": "編輯區隔", + "error_resetting_filters": "重設篩選器時發生錯誤", + "error_saving_segment": "儲存區隔時發生錯誤", + "ex_fully_activated_recurring_users": "例如:完全啟用的定期使用者", + "ex_power_users": "例如:進階使用者", + "filters_reset_successfully": "篩選器已成功重設", + "here": "這裡", + "hide_filters": "隱藏篩選器", + "identifying_users": "識別使用者", + "invalid_segment": "無效區隔", + "invalid_segment_filters": "無效的篩選器。請檢查篩選器並再試一次。", + "load_segment": "載入區隔", + "most_active_users_in_the_last_30_days": "最近 30 天內最活躍的使用者", + "no_attributes_yet": "尚無屬性!", + "no_filters_yet": "尚無篩選器!", + "no_segments_yet": "您目前沒有已儲存的區隔。", + "person_and_attributes": "人員與屬性", + "phone": "電話", + "please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "請從這些問卷中移除區隔,以便將其刪除。", + "pre_segment_users": "使用屬性篩選器預先區隔您的使用者。", + "remove_all_filters": "移除所有篩選器", + "reset_all_filters": "重設所有篩選器", + "save_as_new_segment": "另存為新區隔", + "save_your_filters_as_a_segment_to_use_it_in_other_surveys": "將您的篩選器儲存為區隔,以便在其他問卷中使用", + "segment_created_successfully": "區隔已成功建立!", + "segment_deleted_successfully": "區隔已成功刪除!", + "segment_id": "區隔 ID", + "segment_saved_successfully": "區隔已成功儲存", + "segment_updated_successfully": "區隔已成功更新!", + "segments_help_you_target_users_with_same_characteristics_easily": "區隔可協助您輕鬆針對具有相同特徵的使用者", + "target_audience": "目標受眾", + "this_action_resets_all_filters_in_this_survey": "此操作會重設此問卷中的所有篩選器。", + "this_segment_is_used_in_other_surveys": "此區隔在其他問卷中使用。請謹慎變更", + "title_is_required": "標題為必填項。", + "unknown_filter_type": "未知的篩選器類型", + "unlock_segments_description": "將聯絡人整理到區隔中,以鎖定特定的使用者群組", + "unlock_segments_title": "使用更高等級的方案解鎖區隔", + "user_targeting_is_currently_only_available_when": "使用者目標設定目前僅在以下情況下可用:", + "value_cannot_be_empty": "值不能為空。", + "value_must_be_a_number": "值必須是數字。", + "view_filters": "檢視篩選器", + "where": "何處", + "with_the_formbricks_sdk": "使用 Formbricks SDK" + }, + "settings": { + "api_keys": { + "add_api_key": "新增 API 金鑰", + "add_permission": "新增權限", + "api_keys_description": "管理 API 金鑰以存取 Formbricks 管理 API" + }, + "billing": { + "10000_monthly_responses": "10000 個每月回應", + "1500_monthly_responses": "1500 個每月回應", + "2000_monthly_identified_users": "2000 個每月識別使用者", + "30000_monthly_identified_users": "30000 個每月識別使用者", + "3_projects": "3 個專案", + "5000_monthly_responses": "5000 個每月回應", + "5_projects": "5 個專案", + "7500_monthly_identified_users": "7500 個每月識別使用者", + "advanced_targeting": "進階目標設定", + "all_integrations": "所有整合", + "all_surveying_features": "所有調查功能", + "annually": "每年", + "api_webhooks": "API 和 Webhook", + "app_surveys": "應用程式問卷", + "contact_us": "聯絡我們", + "current": "目前", + "current_plan": "目前方案", + "current_tier_limit": "目前層級限制", + "custom_miu_limit": "自訂 MIU 上限", + "custom_project_limit": "自訂專案上限", + "customer_success_manager": "客戶成功經理", + "email_embedded_surveys": "電子郵件嵌入式問卷", + "email_support": "電子郵件支援", + "enterprise": "企業版", + "enterprise_description": "頂級支援和自訂限制。", + "everybody_has_the_free_plan_by_default": "每個人預設都有免費方案!", + "everything_in_free": "免費方案中的所有功能", + "everything_in_scale": "進階方案中的所有功能", + "everything_in_startup": "啟動方案中的所有功能", + "free": "免費", + "free_description": "無限問卷、團隊成員等。", + "get_2_months_free": "免費獲得 2 個月", + "get_in_touch": "取得聯繫", + "link_surveys": "連結問卷(可分享)", + "logic_jumps_hidden_fields_recurring_surveys": "邏輯跳躍、隱藏欄位、定期問卷等。", + "manage_card_details": "管理卡片詳細資料", + "manage_subscription": "管理訂閱", + "monthly": "每月", + "monthly_identified_users": "每月識別使用者", + "multi_language_surveys": "多語言問卷", + "per_month": "每月", + "per_year": "每年", + "plan_upgraded_successfully": "方案已成功升級", + "premium_support_with_slas": "具有 SLA 的頂級支援", + "priority_support": "優先支援", + "remove_branding": "移除品牌", + "say_hi": "打個招呼!", + "scale": "進階版", + "scale_description": "用於擴展業務的進階功能。", + "startup": "啟動版", + "startup_description": "免費方案中的所有功能以及其他功能。", + "switch_plan": "切換方案", + "switch_plan_confirmation_text": "您確定要切換到 {plan} 計劃嗎?您將被收取 {price} {period}。", + "team_access_roles": "團隊存取角色", + "technical_onboarding": "技術新手上路", + "unable_to_upgrade_plan": "無法升級方案", + "unlimited_apps_websites": "無限應用程式和網站", + "unlimited_miu": "無限 MIU", + "unlimited_projects": "無限專案", + "unlimited_responses": "無限回應", + "unlimited_surveys": "無限問卷", + "unlimited_team_members": "無限團隊成員", + "upgrade": "升級", + "uptime_sla_99": "正常運作時間 SLA (99%)", + "website_surveys": "網站問卷" + }, + "enterprise": { + "ai": "AI 分析", + "audit_logs": "稽核記錄", + "coming_soon": "即將推出", + "contacts_and_segments": "聯絡人管理和區隔", + "enterprise_features": "企業版功能", + "get_an_enterprise_license_to_get_access_to_all_features": "取得企業授權以存取所有功能。", + "keep_full_control_over_your_data_privacy_and_security": "完全掌控您的資料隱私權和安全性。", + "no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "無需通話,無附加條件:填寫此表單,請求免費 30 天試用授權以測試所有功能:", + "no_credit_card_no_sales_call_just_test_it": "無需信用卡。無需銷售電話。只需測試一下 :)", + "on_request": "依要求", + "organization_roles": "組織角色(管理員、編輯者、開發人員等)", + "questions_please_reach_out_to": "有任何問題?請聯絡", + "request_30_day_trial_license": "請求 30 天試用授權", + "saml_sso": "SAML SSO", + "service_level_agreement": "服務等級協定", + "soc2_hipaa_iso_27001_compliance_check": "SOC2、HIPAA、ISO 27001 合規性檢查", + "sso": "SSO(Google、Microsoft、OpenID Connect)", + "teams": "團隊和存取角色(讀取、讀取和寫入、管理)", + "unlock_the_full_power_of_formbricks_free_for_30_days": "免費解鎖 Formbricks 的全部功能,為期 30 天。", + "your_enterprise_license_is_active_all_features_unlocked": "您的企業授權處於活動狀態。所有功能都已解鎖。" + }, + "general": { + "bulk_invite_warning_description": "在免費方案中,所有組織成員始終會被指派「擁有者」角色。", + "cannot_delete_only_organization": "這是您唯一的組織,無法刪除。請先建立新組織。", + "cannot_leave_only_organization": "您無法離開此組織,因為它是您唯一的組織。請先建立新組織。", + "copy_invite_link_to_clipboard": "將邀請連結複製到剪貼簿", + "create_new_organization": "建立新組織", + "create_new_organization_description": "建立新組織以處理一組不同的專案。", + "customize_email_with_a_higher_plan": "使用更高等級的方案自訂電子郵件", + "delete_organization": "刪除組織", + "delete_organization_description": "刪除包含所有專案的組織,包括所有問卷、回應、人員、操作和屬性", + "delete_organization_warning": "在您繼續刪除此組織之前,請注意以下後果:", + "delete_organization_warning_1": "永久移除與此組織相關聯的所有專案。", + "delete_organization_warning_2": "此操作無法復原。一旦刪除,即永久消失。", + "delete_organization_warning_3": "請在下列欄位中輸入 '{'organizationName'}' 以確認永久刪除此組織:", + "eliminate_branding_with_whitelabel": "消除 Formbricks 品牌並啟用其他白標自訂選項。", + "email_customization_preview_email_heading": "嗨,'{'userName'}'", + "email_customization_preview_email_text": "這是電子郵件預覽,向您展示電子郵件中將呈現哪個標誌。", + "enable_formbricks_ai": "啟用 Formbricks AI", + "error_deleting_organization_please_try_again": "刪除組織時發生錯誤。請再試一次。", + "formbricks_ai": "Formbricks AI", + "formbricks_ai_description": "使用 Formbricks AI 從您的問卷回應中取得個人化洞察", + "formbricks_ai_disable_success_message": "已成功停用 Formbricks AI。", + "formbricks_ai_enable_success_message": "已成功啟用 Formbricks AI。", + "formbricks_ai_privacy_policy_text": "藉由啟用 Formbricks AI,您同意更新後的", + "from_your_organization": "來自您的組織", + "invitation_sent_once_more": "已再次發送邀請。", + "invite_deleted_successfully": "邀請已成功刪除", + "invited_on": "邀請於 '{'date'}'", + "invites_failed": "邀請失敗", + "leave_organization": "離開組織", + "leave_organization_description": "您將離開此組織並失去對所有問卷和回應的存取權限。只有再次收到邀請,您才能重新加入。", + "leave_organization_ok_btn_text": "是,離開組織", + "leave_organization_title": "您確定嗎?", + "logo_in_email_header": "電子郵件頁首中的標誌", + "logo_removed_successfully": "標誌已成功移除", + "logo_saved_successfully": "標誌已成功儲存", + "manage_members": "管理成員", + "manage_members_description": "新增或移除您組織中的成員。", + "member_deleted_successfully": "成員已成功刪除", + "member_invited_successfully": "成員已成功邀請", + "once_its_gone_its_gone": "一旦刪除,即永久消失。", + "only_org_owner_can_perform_action": "只有組織擁有者才能存取此設定。", + "organization_created_successfully": "組織已成功建立!", + "organization_deleted_successfully": "組織已成功刪除。", + "organization_invite_link_ready": "您的組織邀請連結已準備就緒!", + "organization_name": "組織名稱", + "organization_name_description": "為您的組織提供描述性名稱。", + "organization_name_placeholder": "例如:飛天小女警", + "organization_name_updated_successfully": "組織名稱已成功更新", + "organization_settings": "組織設定", + "please_add_a_logo": "請新增標誌", + "please_check_csv_file": "請檢查 CSV 檔案,並確保其符合我們的格式", + "please_save_logo_before_sending_test_email": "請在發送測試電子郵件之前儲存標誌。", + "remove_logo": "移除標誌", + "replace_logo": "取代標誌", + "resend_invitation_email": "重新發送邀請電子郵件", + "share_invite_link": "分享邀請連結", + "share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:", + "test_email_sent_successfully": "測試電子郵件已成功發送", + "use_multi_language_surveys_with_a_higher_plan": "使用更高等級的方案使用多語言問卷", + "use_multi_language_surveys_with_a_higher_plan_description": "用不同語言調查您的用戶。" + }, + "notifications": { + "auto_subscribe_to_new_surveys": "自動訂閱新問卷", + "email_alerts_surveys": "電子郵件警示(問卷)", + "every_response": "每個回應", + "every_response_tooltip": "傳送完整的回應,沒有部分回應。", + "need_slack_or_discord_notifications": "需要 Slack 或 Discord 通知嗎?", + "notification_settings_updated": "通知設定已更新", + "set_up_an_alert_to_get_an_email_on_new_responses": "設定警示以在收到新回應時收到電子郵件", + "stay_up_to_date_with_a_Weekly_every_Monday": "每週一使用每週摘要保持最新資訊", + "use_the_integration": "使用整合", + "want_to_loop_in_organization_mates": "想要讓組織夥伴也參與嗎?", + "weekly_summary_projects": "每週摘要(專案)", + "you_will_not_be_auto_subscribed_to_this_organizations_surveys_anymore": "您將不會再自動訂閱此組織的問卷!", + "you_will_not_receive_any_more_emails_for_responses_on_this_survey": "您將不會再收到此問卷回應的電子郵件!" + }, + "profile": { + "account_deletion_consequences_warning": "帳戶刪除後果", + "avatar_update_failed": "頭像更新失敗。請再試一次。", + "backup_code": "備份碼", + "change_image": "變更圖片", + "confirm_delete_account": "刪除您的帳戶以及您的所有個人資訊和資料", + "confirm_delete_my_account": "刪除我的帳戶", + "confirm_your_current_password_to_get_started": "確認您目前的密碼以開始使用。", + "delete_account": "刪除帳戶", + "disable_two_factor_authentication": "停用雙重驗證", + "disable_two_factor_authentication_description": "如果您需要停用 2FA,我們建議您盡快重新啟用它。", + "each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "每個備份碼只能使用一次,以便在沒有驗證器的情況下授予存取權限。", + "enable_two_factor_authentication": "啟用雙重驗證", + "enter_the_code_from_your_authenticator_app_below": "在下方輸入您驗證器應用程式中的程式碼。", + "file_size_must_be_less_than_10mb": "檔案大小必須小於 10MB。", + "invalid_file_type": "無效的檔案類型。僅允許 JPEG、PNG 和 WEBP 檔案。", + "lost_access": "無法存取", + "or_enter_the_following_code_manually": "或手動輸入下列程式碼:", + "organization_identification": "協助您的組織在 Formbricks 上識別您", + "organizations_delete_message": "您是這些組織的唯一擁有者,因此它們也 將被刪除。", + "permanent_removal_of_all_of_your_personal_information_and_data": "永久移除您的所有個人資訊和資料", + "personal_information": "個人資訊", + "please_enter_email_to_confirm_account_deletion": "請在以下欄位中輸入 '{'email'}' 以確認永久刪除您的帳戶:", + "profile_updated_successfully": "您的個人資料已成功更新", + "remove_image": "移除圖片", + "save_the_following_backup_codes_in_a_safe_place": "將下列備份碼儲存在安全的地方。", + "scan_the_qr_code_below_with_your_authenticator_app": "使用您的驗證器應用程式掃描下方的 QR 碼。", + "security_description": "管理您的密碼和其他安全性設定,例如雙重驗證 (2FA)。", + "two_factor_authentication": "雙重驗證", + "two_factor_authentication_description": "在您的密碼被盜時,為您的帳戶新增額外的安全層。", + "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "已啟用雙重驗證。請輸入您驗證器應用程式中的六位數程式碼。", + "two_factor_code": "雙重驗證碼", + "unlock_two_factor_authentication": "使用更高等級的方案解鎖雙重驗證", + "update_personal_info": "更新您的個人資訊", + "upload_image": "上傳圖片", + "warning_cannot_delete_account": "您是此組織的唯一擁有者。請先將所有權轉讓給其他成員。", + "warning_cannot_undo": "此操作無法復原", + "you_must_select_a_file": "您必須選取檔案。" + }, + "teams": { + "add_members_description": "將成員新增至團隊並確定其角色。", + "add_projects_description": "控制團隊成員可以存取哪些專案。", + "all_members_added": "所有成員都已新增至此團隊。", + "all_projects_added": "所有專案都已新增至此團隊。", + "are_you_sure_you_want_to_delete_this_team": "您確定要刪除此團隊嗎?這也會移除對此團隊相關的所有專案和問卷的存取權限。", + "billing_role_description": "只能存取帳單資訊。", + "bulk_invite": "大量邀請", + "contributor": "投稿人", + "create": "建立", + "create_first_team_message": "您必須先建立團隊。", + "create_new_team": "建立新團隊", + "delete_team": "刪除團隊", + "empty_teams_state": "建立您的第一個團隊。", + "enter_team_name": "輸入團隊名稱", + "individual": "個人", + "invite_member": "邀請成員", + "invite_member_description": "將您的同事新增至此組織。", + "manage": "管理", + "manage_team": "管理團隊", + "manage_team_disabled": "只有組織擁有者、管理員和團隊管理員才能管理團隊。", + "manager_role_description": "管理員可以存取所有專案,並新增和移除成員。", + "member_role_description": "成員可以在選定的專案中工作。", + "member_role_info_message": "若要授予新成員存取專案的權限,請將他們新增至下方的團隊。藉由團隊,您可以管理誰可以存取哪些專案。", + "owner_role_description": "擁有者對組織具有完全控制權。", + "please_fill_all_member_fields": "請填寫所有欄位以新增新成員。", + "please_fill_all_project_fields": "請填寫所有欄位以新增新專案。", + "read": "讀取", + "read_write": "讀取和寫入", + "team_admin": "團隊管理員", + "team_created_successfully": "團隊已成功建立。", + "team_deleted_successfully": "團隊已成功刪除。", + "team_deletion_not_allowed": "您不得刪除此團隊。", + "team_name": "團隊名稱", + "team_name_settings_title": "'{'teamName'}' 設定", + "team_select_placeholder": "搜尋團隊名稱...", + "team_settings_description": "管理團隊成員、存取權限等。", + "team_updated_successfully": "團隊已成功更新", + "teams": "團隊", + "teams_description": "將成員指派到團隊中,並授予團隊存取專案的權限。", + "unlock_teams_description": "管理哪些組織成員可以存取特定專案和問卷。", + "unlock_teams_title": "使用更高等級的方案解鎖團隊。", + "upgrade_plan_notice_message": "使用更高等級的方案解鎖組織角色。", + "you_are_a_member": "您是成員" + } + }, + "surveys": { + "all_set_time_to_create_first_survey": "您已準備就緒!是時候建立您的第一個問卷", + "alphabetical": "依字母順序", + "copy_survey": "複製問卷", + "copy_survey_description": "將此問卷複製到另一個環境", + "copy_survey_error": "無法複製問卷", + "copy_survey_link_to_clipboard": "將問卷連結複製到剪貼簿", + "copy_survey_success": "問卷已成功複製!", + "delete_survey_and_responses_warning": "您確定要刪除此問卷及其所有回應嗎?此操作無法復原。", + "edit": { + "1_choose_the_default_language_for_this_survey": "1. 選擇此問卷的預設語言:", + "2_activate_translation_for_specific_languages": "2. 啟用特定語言的翻譯:", + "add": "新增 +", + "add_a_delay_or_auto_close_the_survey": "新增延遲或自動關閉問卷", + "add_a_four_digit_pin": "新增四位數 PIN 碼", + "add_a_new_question_to_your_survey": "在您的問卷中新增一個新問題", + "add_a_variable_to_calculate": "新增要計算的變數", + "add_action_below": "在下方新增操作", + "add_choice_below": "在下方新增選項", + "add_color_coding": "新增顏色編碼", + "add_color_coding_description": "為選項新增紅色、橘色和綠色顏色代碼。", + "add_column": "新增欄位", + "add_condition_below": "在下方新增條件", + "add_custom_styles": "新增自訂樣式", + "add_delay_before_showing_survey": "新增顯示問卷之前的延遲", + "add_description": "新增描述", + "add_ending": "新增結尾", + "add_ending_below": "在下方新增結尾", + "add_hidden_field_id": "新增隱藏欄位 ID", + "add_highlight_border": "新增醒目提示邊框", + "add_highlight_border_description": "在您的問卷卡片新增外邊框。", + "add_logic": "新增邏輯", + "add_option": "新增選項", + "add_other": "新增「其他」", + "add_photo_or_video": "新增照片或影片", + "add_pin": "新增 PIN 碼", + "add_question": "新增問題", + "add_question_below": "在下方新增問題", + "add_row": "新增列", + "add_variable": "新增變數", + "address_fields": "地址欄位", + "address_line_1": "地址 1", + "address_line_2": "地址 2", + "adjust_survey_closed_message": "調整「問卷已關閉」訊息", + "adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。", + "adjust_the_theme_in_the": "在", + "all_other_answers_will_continue_to": "所有其他答案將繼續", + "allow_file_type": "允許檔案類型", + "allow_multi_select": "允許多重選取", + "allow_multiple_files": "允許上傳多個檔案", + "allow_users_to_select_more_than_one_image": "允許使用者選取多張圖片", + "always_show_survey": "始終顯示問卷", + "and_launch_surveys_in_your_website_or_app": "並在您的網站或應用程式中啟動問卷。", + "animation": "動畫", + "app_survey_description": "將問卷嵌入您的 Web 應用程式或網站中以收集回應。", + "assign": "等於 =", + "audience": "受眾", + "auto_close_on_inactivity": "非活動時自動關閉", + "automatically_close_survey_after": "在指定時間自動關閉問卷", + "automatically_close_the_survey_after_a_certain_number_of_responses": "在收到一定數量的回覆後自動關閉問卷。", + "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "如果用戶在特定秒數後未回應,則自動關閉問卷。", + "automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "在指定日期(UTC時間)自動關閉問卷。", + "automatically_mark_the_survey_as_complete_after": "在指定時間後自動將問卷標記為完成", + "automatically_release_the_survey_at_the_beginning_of_the_day_utc": "在指定日期(UTC時間)自動發佈問卷。", + "back_button_label": "「返回」按鈕標籤", + "background_styling": "背景樣式設定", + "blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already": "如果已存在具有單次使用 ID (suId) 的提交,則封鎖問卷。", + "blocks_survey_if_the_survey_url_has_no_single_use_id_suid": "如果問卷網址沒有單次使用 ID (suId),則封鎖問卷。", + "brand_color": "品牌顏色", + "brightness": "亮度", + "button_label": "按鈕標籤", + "button_to_continue_in_survey": "問卷中繼續的按鈕", + "button_to_link_to_external_url": "連結到外部網址的按鈕", + "button_url": "按鈕網址", + "cal_username": "Cal.com 使用者名稱或使用者名稱/事件", + "calculate": "計算", + "capture_a_new_action_to_trigger_a_survey_on": "擷取新的操作以觸發問卷。", + "capture_new_action": "擷取新操作", + "card_arrangement_for_survey_type_derived": "'{'surveyTypeDerived'}' 問卷的卡片排列", + "card_background_color": "卡片背景顏色", + "card_border_color": "卡片邊框顏色", + "card_shadow_color": "卡片陰影顏色", + "card_styling": "卡片樣式設定", + "casual": "隨意", + "caution_text": "變更會導致不一致", + "centered_modal_overlay_color": "置中彈窗覆蓋顏色", + "change_anyway": "仍然變更", + "change_background": "變更背景", + "change_question_type": "變更問題類型", + "change_the_background_color_of_the_card": "變更卡片的背景顏色。", + "change_the_background_color_of_the_input_fields": "變更輸入欄位的背景顏色。", + "change_the_background_to_a_color_image_or_animation": "將背景變更為顏色、圖片或動畫。", + "change_the_border_color_of_the_card": "變更卡片的邊框顏色。", + "change_the_border_color_of_the_input_fields": "變更輸入欄位的邊框顏色。", + "change_the_border_radius_of_the_card_and_the_inputs": "變更卡片和輸入的邊框半徑。", + "change_the_brand_color_of_the_survey": "變更問卷的品牌顏色。", + "change_the_placement_of_this_survey": "變更此問卷的位置。", + "change_the_question_color_of_the_survey": "變更問卷的問題顏色。", + "change_the_shadow_color_of_the_card": "變更卡片的陰影顏色。", + "changes_saved": "已儲存變更。", + "character_limit_toggle_description": "限制答案的長度或短度。", + "character_limit_toggle_title": "新增字元限制", + "checkbox_label": "核取方塊標籤", + "choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。", + "choose_where_to_run_the_survey": "選擇在哪裡執行問卷。", + "city": "城市", + "close_survey_on_date": "在指定日期關閉問卷", + "close_survey_on_response_limit": "在回應次數上限關閉問卷", + "color": "顏色", + "columns": "欄位", + "company": "公司", + "company_logo": "公司標誌", + "completed_responses": "完成的回應。", + "concat": "串連 +", + "conditional_logic": "條件邏輯", + "confirm_default_language": "確認預設語言", + "confirm_survey_changes": "確認問卷變更", + "contact_fields": "聯絡人欄位", + "contains": "包含", + "continue_to_settings": "繼續設定", + "control_which_file_types_can_be_uploaded": "控制可以上傳哪些檔案類型。", + "convert_to_multiple_choice": "轉換為多選", + "convert_to_single_choice": "轉換為單選", + "country": "國家/地區", + "create_group": "建立群組", + "create_your_own_survey": "建立您自己的問卷", + "css_selector": "CSS 選取器", + "custom_hostname": "自訂主機名稱", + "darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。", + "date_format": "日期格式", + "days_before_showing_this_survey_again": "天後再次顯示此問卷。", + "decide_how_often_people_can_answer_this_survey": "決定人們可以回答此問卷的頻率。", + "delete_choice": "刪除選項", + "description": "描述", + "disable_the_visibility_of_survey_progress": "停用問卷進度的可見性。", + "display_an_estimate_of_completion_time_for_survey": "顯示問卷的估計完成時間", + "display_number_of_responses_for_survey": "顯示問卷的回應數", + "divide": "除 /", + "does_not_contain": "不包含", + "does_not_end_with": "不以...結尾", + "does_not_equal": "不等於", + "does_not_include_all_of": "不包含全部", + "does_not_include_one_of": "不包含其中之一", + "does_not_start_with": "不以...開頭", + "edit_recall": "編輯回憶", + "edit_translations": "編輯 '{'language'}' 翻譯", + "enable_encryption_of_single_use_id_suid_in_survey_url": "啟用問卷網址中單次使用 ID (suId) 的加密。", + "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "允許參與者在問卷中的任何時間點切換問卷語言。", + "end_screen_card": "結束畫面卡片", + "ending_card": "結尾卡片", + "ending_card_used_in_logic": "此結尾卡片用於問題 '{'questionIndex'}' 的邏輯中。", + "ends_with": "結尾為", + "equals": "等於", + "equals_one_of": "等於其中之一", + "error_publishing_survey": "發布問卷時發生錯誤。", + "error_saving_changes": "儲存變更時發生錯誤", + "even_after_they_submitted_a_response_e_g_feedback_box": "即使他們提交回應之後(例如,意見反應方塊)", + "everyone": "所有人", + "fallback_missing": "遺失的回退", + "fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'fieldId'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。", + "field_name_eg_score_price": "欄位名稱,例如:分數、價格", + "first_name": "名字", + "five_points_recommended": "5 分(建議)", + "follow_ups": "後續追蹤", + "follow_ups_delete_modal_text": "您確定要刪除此後續追蹤嗎?", + "follow_ups_delete_modal_title": "刪除後續追蹤?", + "follow_ups_empty_description": "向回應者、您自己或團隊夥伴傳送訊息。", + "follow_ups_empty_heading": "傳送自動後續追蹤", + "follow_ups_ending_card_delete_modal_text": "此結尾卡片用於後續追蹤中。刪除它將會從所有後續追蹤中移除。您確定要刪除它嗎?", + "follow_ups_ending_card_delete_modal_title": "刪除結尾卡片?", + "follow_ups_hidden_field_error": "隱藏欄位在後續追蹤中使用。請先從後續追蹤中移除。", + "follow_ups_item_ending_tag": "結尾", + "follow_ups_item_issue_detected_tag": "偵測到問題", + "follow_ups_item_response_tag": "任何回應", + "follow_ups_item_send_email_tag": "發送電子郵件", + "follow_ups_modal_action_attach_response_data_description": "將調查回應的數據添加到後續", + "follow_ups_modal_action_attach_response_data_label": "附加 response data", + "follow_ups_modal_action_body_label": "內文", + "follow_ups_modal_action_body_placeholder": "電子郵件內文", + "follow_ups_modal_action_email_content": "電子郵件內容", + "follow_ups_modal_action_email_settings": "電子郵件設定", + "follow_ups_modal_action_from_description": "傳送電子郵件的電子郵件地址", + "follow_ups_modal_action_from_label": "寄件者", + "follow_ups_modal_action_label": "操作", + "follow_ups_modal_action_replyTo_description": "如果收件者按下回覆,則以下電子郵件地址將會收到", + "follow_ups_modal_action_replyTo_label": "回覆至", + "follow_ups_modal_action_subject": "感謝您的回答!", + "follow_ups_modal_action_subject_label": "主旨", + "follow_ups_modal_action_subject_placeholder": "電子郵件主旨", + "follow_ups_modal_action_to_description": "傳送電子郵件的電子郵件地址", + "follow_ups_modal_action_to_label": "收件者", + "follow_ups_modal_action_to_warning": "問卷中未偵測到電子郵件欄位", + "follow_ups_modal_create_heading": "建立新的後續追蹤", + "follow_ups_modal_edit_heading": "編輯此後續追蹤", + "follow_ups_modal_edit_no_id": "未提供問卷後續追蹤 ID,無法更新問卷後續追蹤", + "follow_ups_modal_name_label": "後續追蹤名稱", + "follow_ups_modal_name_placeholder": "為您的後續追蹤命名", + "follow_ups_modal_subheading": "向回應者、您自己或團隊夥伴傳送訊息", + "follow_ups_modal_trigger_description": "應在何時觸發此後續追蹤?", + "follow_ups_modal_trigger_label": "觸發器", + "follow_ups_modal_trigger_type_ending": "回應者看到特定結尾", + "follow_ups_modal_trigger_type_ending_select": "選取結尾:", + "follow_ups_modal_trigger_type_ending_warning": "問卷中找不到結尾!", + "follow_ups_modal_trigger_type_response": "回應者完成問卷", + "follow_ups_new": "新增後續追蹤", + "follow_ups_upgrade_button_text": "升級以啟用後續追蹤", + "form_styling": "表單樣式設定", + "formbricks_ai_description": "描述您的問卷並讓 Formbricks AI 為您建立問卷", + "formbricks_ai_generate": "產生", + "formbricks_ai_prompt_placeholder": "輸入問卷資訊(例如,要涵蓋的關鍵主題)", + "formbricks_sdk_is_not_connected": "Formbricks SDK 未連線", + "four_points": "4 分", + "heading": "標題", + "hidden_field_added_successfully": "隱藏欄位已成功新增", + "hide_advanced_settings": "隱藏進階設定", + "hide_back_button": "隱藏「Back」按鈕", + "hide_back_button_description": "不要在問卷中顯示返回按鈕", + "hide_logo": "隱藏標誌", + "hide_progress_bar": "隱藏進度列", + "hide_the_logo_in_this_specific_survey": "在此特定問卷中隱藏標誌", + "hostname": "主機名稱", + "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫", + "how_it_works": "運作方式", + "if_you_need_more_please": "如果您需要更多,請", + "if_you_really_want_that_answer_ask_until_you_get_it": "如果您真的想要該答案,請詢問直到您獲得它。", + "ignore_waiting_time_between_surveys": "忽略問卷之間的等待時間", + "image": "圖片", + "includes_all_of": "包含全部", + "includes_one_of": "包含其中之一", + "initial_value": "初始值", + "inner_text": "內部文字", + "input_border_color": "輸入邊框顏色", + "input_color": "輸入顏色", + "invalid_targeting": "目標設定無效:請檢查您的受眾篩選器", + "invalid_video_url_warning": "請輸入有效的 YouTube、Vimeo 或 Loom 網址。我們目前不支援其他影片託管提供者。", + "invalid_youtube_url": "無效的 YouTube 網址", + "is_accepted": "已接受", + "is_after": "在之後", + "is_before": "在之前", + "is_booked": "已預訂", + "is_clicked": "已點擊", + "is_completely_submitted": "已完全提交", + "is_not_set": "未設定", + "is_partially_submitted": "已部分提交", + "is_set": "已設定", + "is_skipped": "已跳過", + "is_submitted": "已提交", + "jump_to_question": "跳至問題", + "keep_current_order": "保留目前順序", + "keep_showing_while_conditions_match": "在條件符合時持續顯示", + "key": "金鑰", + "last_name": "姓氏", + "let_people_upload_up_to_25_files_at_the_same_time": "允許使用者同時上傳最多 25 個檔案。", + "limit_file_types": "限制檔案類型", + "limit_the_maximum_file_size": "限制最大檔案大小", + "limit_upload_file_size_to": "限制上傳檔案大小為", + "link_survey_description": "分享問卷頁面的連結或將其嵌入網頁或電子郵件中。", + "link_used_message": "已使用連結", + "load_segment": "載入區隔", + "logic_error_warning": "變更將導致邏輯錯誤", + "logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件", + "long_answer": "長回答", + "lower_label": "下標籤", + "manage_languages": "管理語言", + "max_file_size": "最大檔案大小", + "max_file_size_limit_is": "最大檔案大小限制為", + "multiply": "乘 *", + "needed_for_self_hosted_cal_com_instance": "自行託管 Cal.com 執行個體時需要", + "next_button_label": "「下一步」按鈕標籤", + "next_question": "下一個問題", + "no_hidden_fields_yet_add_first_one_below": "尚無隱藏欄位。在下方新增第一個隱藏欄位。", + "no_images_found_for": "找不到「'{'query'}'」的圖片", + "no_languages_found_add_first_one_to_get_started": "找不到語言。新增第一個語言以開始使用。", + "no_variables_yet_add_first_one_below": "尚無變數。在下方新增第一個變數。", + "number": "數字", + "once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations": "設定後,此問卷的預設語言只能藉由停用多語言選項並刪除所有翻譯來變更。", + "only_display_the_survey_to_a_subset_of_the_users": "僅向部分使用者顯示問卷", + "only_lower_case_letters_numbers_and_underscores_are_allowed": "僅允許小寫字母、數字和底線。", + "only_people_who_match_your_targeting_can_be_surveyed": "只有符合您目標設定的人員才能被調查。", + "option_idx": "選項 '{'choiceIndex'}'", + "option_used_in_logic_error": "此選項用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。", + "optional": "選填", + "options": "選項", + "override_theme_with_individual_styles_for_this_survey": "使用此問卷的個別樣式覆寫主題。", + "overwrite_placement": "覆寫位置", + "overwrite_the_global_placement_of_the_survey": "覆寫問卷的整體位置", + "overwrites_waiting_period_between_surveys_to_x_days": "將問卷之間的等待時間覆寫為 '{'days'}' 天。", + "pick_a_background_from_our_library_or_upload_your_own": "從我們的媒體庫中選取背景或上傳您自己的背景。", + "picture_idx": "圖片 '{'idx'}'", + "pin_can_only_contain_numbers": "PIN 碼只能包含數字。", + "pin_must_be_a_four_digit_number": "PIN 碼必須是四位數的數字。", + "please_enter_a_file_extension": "請輸入檔案副檔名。", + "please_set_a_survey_trigger": "請設定問卷觸發器", + "please_specify": "請指定", + "prevent_double_submission": "防止重複提交", + "prevent_double_submission_description": "每個電子郵件地址僅允許 1 個回應", + "protect_survey_with_pin": "使用 PIN 碼保護問卷", + "protect_survey_with_pin_description": "只有擁有 PIN 碼的使用者才能存取問卷。", + "publish": "發布", + "question": "問題", + "question_color": "問題顏色", + "question_deleted": "問題已刪除。", + "question_duplicated": "問題已複製。", + "question_id_updated": "問題 ID 已更新", + "question_used_in_logic": "此問題用於問題 '{'questionIndex'}' 的邏輯中。", + "randomize_all": "全部隨機排序", + "randomize_all_except_last": "全部隨機排序(最後一項除外)", + "range": "範圍", + "recontact_options": "重新聯絡選項", + "redirect_thank_you_card": "重新導向感謝卡片", + "redirect_to_url": "重新導向至網址", + "redirect_to_url_not_available_on_free_plan": "重新導向至網址在免費方案中不可用", + "release_survey_on_date": "在指定日期發佈問卷", + "remove_description": "移除描述", + "remove_translations": "移除翻譯", + "require_answer": "要求回答", + "required": "必填", + "reset_to_theme_styles": "重設為主題樣式", + "reset_to_theme_styles_main_text": "您確定要將樣式重設為主題樣式嗎?這將移除所有自訂樣式。", + "response_limit_can_t_be_set_to_0": "回應限制不能設定為 0", + "response_limit_needs_to_exceed_number_of_received_responses": "回應限制必須超過收到的回應數 ('{'responseCount'}')。", + "response_limits_redirections_and_more": "回應限制、重新導向等。", + "response_options": "回應選項", + "roundness": "圓角", + "rows": "列", + "save_and_close": "儲存並關閉", + "scale": "比例", + "search_for_images": "搜尋圖片", + "seconds_after_trigger_the_survey_will_be_closed_if_no_response": "如果沒有回應,則在觸發後幾秒關閉問卷", + "seconds_before_showing_the_survey": "秒後顯示問卷。", + "select_or_type_value": "選取或輸入值", + "select_ordering": "選取排序", + "select_saved_action": "選取已儲存的操作", + "select_type": "選取類型", + "send_survey_to_audience_who_match": "將問卷發送給符合以下條件的受眾:", + "send_your_respondents_to_a_page_of_your_choice": "將您的回應者傳送到您選擇的頁面。", + "set_the_global_placement_in_the_look_feel_settings": "在「外觀與風格」設定中設定整體位置。", + "seven_points": "7 分", + "show_advanced_settings": "顯示進階設定", + "show_button": "顯示按鈕", + "show_language_switch": "顯示語言切換", + "show_multiple_times": "多次顯示", + "show_only_once": "僅顯示一次", + "show_survey_maximum_of": "最多顯示問卷", + "show_survey_to_users": "將問卷顯示給 % 的使用者", + "show_to_x_percentage_of_targeted_users": "顯示給 '{'percentage'}'% 的目標使用者", + "simple": "簡單", + "single_use_survey_links": "單次使用問卷連結", + "single_use_survey_links_description": "每個問卷連結只允許 1 個回應。", + "skip_button_label": "「跳過」按鈕標籤", + "smiley": "表情符號", + "star": "星形", + "starts_with": "開頭為", + "state": "州/省", + "straight": "直線", + "style_the_question_texts_descriptions_and_input_fields": "設定問題文字、描述和輸入欄位的樣式。", + "style_the_survey_card": "設定問卷卡片的樣式。", + "styling_set_to_theme_styles": "樣式設定為主題樣式", + "subheading": "副標題", + "subtract": "減 -", + "suggest_colors": "建議顏色", + "survey_already_answered_heading": "問卷已回答。", + "survey_already_answered_subheading": "您只能使用此連結一次。", + "survey_completed_heading": "問卷已完成", + "survey_completed_subheading": "此免費且開源的問卷已關閉", + "survey_display_settings": "問卷顯示設定", + "survey_placement": "問卷位置", + "survey_trigger": "問卷觸發器", + "switch_multi_lanugage_on_to_get_started": "開啟多語言以開始使用 \uD83D\uDC49", + "targeted": "目標", + "ten_points": "10 分", + "the_survey_will_be_shown_multiple_times_until_they_respond": "將多次顯示問卷,直到他們回應", + "the_survey_will_be_shown_once_even_if_person_doesnt_respond": "即使使用者沒有回應,也只會顯示一次問卷。", + "then": "然後", + "this_action_will_remove_all_the_translations_from_this_survey": "此操作將從此問卷中移除所有翻譯。", + "this_extension_is_already_added": "已新增此擴充功能。", + "this_file_type_is_not_supported": "不支援此檔案類型。", + "this_setting_overwrites_your": "此設定會覆寫您的", + "three_points": "3 分", + "times": "次", + "to_keep_the_placement_over_all_surveys_consistent_you_can": "若要保持所有問卷的位置一致,您可以", + "trigger_survey_when_one_of_the_actions_is_fired": "當觸發其中一個操作時,觸發問卷...", + "try_lollipop_or_mountain": "嘗試「棒棒糖」或「山峰」...", + "type_field_id": "輸入欄位 ID", + "unlock_targeting_description": "根據屬性或裝置資訊鎖定特定使用者群組", + "unlock_targeting_title": "使用更高等級的方案解鎖目標設定", + "unsaved_changes_warning": "您的問卷中有未儲存的變更。您要先儲存它們再離開嗎?", + "until_they_submit_a_response": "直到他們提交回應", + "upgrade_notice_description": "建立多語言問卷並解鎖更多功能", + "upgrade_notice_title": "使用更高等級的方案解鎖多語言問卷", + "upload": "上傳", + "upload_at_least_2_images": "上傳至少 2 張圖片", + "upper_label": "上標籤", + "url_encryption": "網址加密", + "url_filters": "網址篩選器", + "url_not_supported": "不支援網址", + "use_with_caution": "謹慎使用", + "variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。", + "variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。", + "variable_name_must_start_with_a_letter": "變數名稱必須以字母開頭。", + "verify_email_before_submission": "提交前驗證電子郵件", + "verify_email_before_submission_description": "僅允許擁有真實電子郵件的人員回應。", + "wait": "等待", + "wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "在觸發後等待幾秒鐘再顯示問卷", + "waiting_period": "等待時間", + "welcome_message": "歡迎訊息", + "when": "何時", + "when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "當條件符合時,等待時間將被忽略且顯示問卷。", + "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_project_to_work_with_translations": "您需要在您的專案中設定兩個或更多語言,才能使用翻譯。", + "your_description_here_recall_information_with": "您的描述在這裡。使用 @ 回憶資訊", + "your_question_here_recall_information_with": "您的問題在這裡。使用 @ 回憶資訊", + "your_web_app": "您的 Web 應用程式", + "zip": "郵遞區號" + }, + "error_deleting_survey": "刪除問卷時發生錯誤", + "failed_to_copy_link_to_results": "無法複製結果連結", + "failed_to_copy_url": "無法複製網址:不在瀏覽器環境中。", + "new_single_use_link_generated": "已產生新的單次使用連結", + "new_survey": "新增問卷", + "no_surveys_created_yet": "尚未建立任何問卷", + "open_options": "開啟選項", + "preview_survey_in_a_new_tab": "在新分頁中預覽問卷", + "read_only_user_not_allowed_to_create_survey_warning": "身為唯讀使用者,您不得建立問卷。請要求具有寫入權限的使用者建立問卷或管理員升級您的角色。", + "relevance": "相關性", + "responses": { + "address_line_1": "地址 1", + "address_line_2": "地址 2", + "an_error_occurred_creating_a_new_note": "建立新筆記時發生錯誤", + "an_error_occurred_deleting_the_tag": "刪除標籤時發生錯誤", + "an_error_occurred_resolving_a_note": "解決筆記時發生錯誤", + "an_error_occurred_updating_a_note": "更新筆記時發生錯誤", + "browser": "瀏覽器", + "city": "城市", + "company": "公司", + "completed": "已完成 ✅", + "country": "國家/地區", + "device": "裝置", + "device_info": "裝置資訊", + "email": "電子郵件", + "first_name": "名字", + "how_to_identify_users": "如何識別使用者", + "last_name": "姓氏", + "not_completed": "未完成 ⏳", + "os": "作業系統", + "person_attributes": "人員屬性", + "phone": "電話", + "resolve": "解決", + "respondent_skipped_questions": "回應者跳過這些問題。", + "response_deleted_successfully": "回應已成功刪除。", + "single_use_id": "單次使用 ID", + "source": "來源", + "state_region": "州/地區", + "survey_closed": "問卷已關閉", + "tag_already_exists": "標籤已存在", + "this_response_is_in_progress": "此回應正在進行中。", + "zip_post_code": "郵遞區號" + }, + "results_unpublished_successfully": "結果已成功取消發布。", + "search_by_survey_name": "依問卷名稱搜尋", + "summary": { + "added_filter_for_responses_where_answer_to_question": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案為 '{'filterComboBoxValue'}' - '{'filterValue'}'", + "added_filter_for_responses_where_answer_to_question_is_skipped": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案被跳過", + "all_responses_csv": "所有回應 (CSV)", + "all_responses_excel": "所有回應 (Excel)", + "all_time": "全部時間", + "almost_there": "快完成了!安裝小工具以開始接收回應。", + "average": "平均", + "completed": "已完成", + "completed_tooltip": "問卷已完成的次數。", + "configure_alerts": "設定警示", + "congrats": "恭喜!您的問卷已上線。", + "connect_your_website_or_app_with_formbricks_to_get_started": "將您的網站或應用程式與 Formbricks 連線以開始使用。", + "copy_link_to_public_results": "複製公開結果的連結", + "create_single_use_links": "建立單次使用連結", + "create_single_use_links_description": "每個連結只接受一次提交。以下是如何操作。", + "custom_range": "自訂範圍...", + "data_prefilling": "資料預先填寫", + "data_prefilling_description": "您想要預先填寫問卷中的某些欄位嗎?以下是如何操作。", + "define_when_and_where_the_survey_should_pop_up": "定義問卷應該在哪裡和何時彈出", + "drop_offs": "放棄", + "drop_offs_tooltip": "問卷已開始但未完成的次數。", + "dynamic_popup": "動態(彈窗)", + "email_sent": "已發送電子郵件!", + "embed_code_copied_to_clipboard": "嵌入程式碼已複製到剪貼簿!", + "embed_in_an_email": "嵌入電子郵件中", + "embed_in_app": "嵌入應用程式", + "embed_mode": "嵌入模式", + "embed_mode_description": "以簡約設計嵌入您的問卷,捨棄邊距和背景。", + "embed_on_website": "嵌入網站", + "embed_pop_up_survey_title": "如何在您的網站上嵌入彈出式問卷", + "embed_survey": "嵌入問卷", + "enable_ai_insights_banner_button": "啟用洞察", + "enable_ai_insights_banner_description": "您可以為問卷啟用新的洞察功能,以取得針對您開放文字回應的 AI 洞察。", + "enable_ai_insights_banner_success": "正在為此問卷產生洞察。請稍後再查看。", + "enable_ai_insights_banner_title": "準備好測試 AI 洞察了嗎?", + "enable_ai_insights_banner_tooltip": "請透過 hola@formbricks.com 與我們聯絡,以產生此問卷的洞察", + "failed_to_copy_link": "無法複製連結", + "filter_added_successfully": "篩選器已成功新增", + "filter_updated_successfully": "篩選器已成功更新", + "filtered_responses_csv": "篩選回應 (CSV)", + "filtered_responses_excel": "篩選回應 (Excel)", + "formbricks_email_survey_preview": "Formbricks 電子郵件問卷預覽", + "go_to_setup_checklist": "前往設定檢查清單 \uD83D\uDC49", + "hide_embed_code": "隱藏嵌入程式碼", + "how_to_create_a_panel": "如何建立小組", + "how_to_create_a_panel_step_1": "步驟 1:使用 Prolific 建立帳戶", + "how_to_create_a_panel_step_1_description": "使用 Prolific 建立帳戶並驗證您的電子郵件地址。", + "how_to_create_a_panel_step_2": "步驟 2:建立研究", + "how_to_create_a_panel_step_2_description": "在 Prolific 中,您建立一個新的研究,您可以在其中根據數百個特徵選擇您偏好的受眾。", + "how_to_create_a_panel_step_3": "步驟 3:連線您的問卷", + "how_to_create_a_panel_step_3_description": "在您的 Formbricks 問卷中設定隱藏欄位,以追蹤哪個參與者提供了哪個答案。", + "how_to_create_a_panel_step_4": "步驟 4:啟動您的研究", + "how_to_create_a_panel_step_4_description": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。", + "impressions": "曝光數", + "impressions_tooltip": "問卷已檢視的次數。", + "includes_all": "包含全部", + "includes_either": "包含其中一個", + "insights_disabled": "洞察已停用", + "install_widget": "安裝 Formbricks 小工具", + "is_equal_to": "等於", + "is_less_than": "小於", + "last_30_days": "過去 30 天", + "last_6_months": "過去 6 個月", + "last_7_days": "過去 7 天", + "last_month": "上個月", + "last_quarter": "上一季", + "last_year": "去年", + "link_to_public_results_copied": "已複製公開結果的連結", + "make_sure_the_survey_type_is_set_to": "請確保問卷類型設定為", + "mobile_app": "行動應用程式", + "no_response_matches_filter": "沒有任何回應符合您的篩選器", + "only_completed": "僅已完成", + "other_values_found": "找到其他值", + "overall": "整體", + "publish_to_web": "發布至網站", + "publish_to_web_warning": "您即將將這些問卷結果發布到公共領域。", + "publish_to_web_warning_description": "您的問卷結果將會是公開的。任何組織外的人員都可以存取這些結果(如果他們有連結)。", + "quickstart_mobile_apps": "快速入門:Mobile apps", + "quickstart_mobile_apps_description": "要開始使用行動應用程式中的調查,請按照 Quickstart 指南:", + "quickstart_web_apps": "快速入門:Web apps", + "quickstart_web_apps_description": "請按照 Quickstart 指南開始:", + "results_are_public": "結果是公開的", + "send_preview": "發送預覽", + "send_to_panel": "發送到小組", + "setup_instructions": "設定說明", + "setup_integrations": "設定整合", + "share_results": "分享結果", + "share_the_link": "分享連結", + "share_the_link_to_get_responses": "分享連結以取得回應", + "show_all_responses_that_match": "顯示所有相符的回應", + "show_all_responses_where": "顯示所有回應,其中...", + "single_use_links": "單次使用連結", + "source_tracking": "來源追蹤", + "source_tracking_description": "執行符合 GDPR 和 CCPA 的來源追蹤,無需額外工具。", + "starts": "開始次數", + "starts_tooltip": "問卷已開始的次數。", + "static_iframe": "靜態 (iframe)", + "survey_results_are_public": "您的問卷結果是公開的!", + "survey_results_are_shared_with_anyone_who_has_the_link": "您的問卷結果與任何擁有連結的人員分享。這些結果將不會被搜尋引擎編入索引。", + "this_month": "本月", + "this_quarter": "本季", + "this_year": "今年", + "time_to_complete": "完成時間", + "to_connect_your_website_with_formbricks": "以將您的網站與 Formbricks 連線", + "ttc_tooltip": "完成問卷的平均時間。", + "unknown_question_type": "未知的問題類型", + "unpublish_from_web": "從網站取消發布", + "unsupported_video_tag_warning": "您的瀏覽器不支援 video 標籤。", + "view_embed_code": "檢視嵌入程式碼", + "view_embed_code_for_email": "檢視電子郵件的嵌入程式碼", + "view_site": "檢視網站", + "waiting_for_response": "正在等待回應 \uD83E\uDDD8‍♂️", + "web_app": "Web 應用程式", + "what_is_a_panel": "什麼是小組?", + "what_is_a_panel_answer": "小組是一組根據年齡、職業、性別等特徵選取的參與者。", + "what_is_prolific": "什麼是 Prolific?", + "what_is_prolific_answer": "我們正在與 Prolific 合作,為您提供超過 200,000 名經過審核的參與者。", + "whats_next": "下一步是什麼?", + "when_do_i_need_it": "我何時需要它?", + "when_do_i_need_it_answer": "如果您無法存取足夠的符合您目標受眾的人員,則可以付費存取小組。", + "you_can_do_a_lot_more_with_links_surveys": "使用連結問卷,您可以做更多事情 \uD83D\uDCA1", + "your_survey_is_public": "您的問卷是公開的", + "youre_not_plugged_in_yet": "您尚未插入任何內容!" + }, + "survey_deleted_successfully": "問卷已成功刪除!", + "survey_duplicated_successfully": "問卷已成功複製。", + "survey_duplication_error": "無法複製問卷。", + "survey_status_tooltip": "若要更新問卷狀態,請更新問卷回應選項中的排程和關閉設定。", + "templates": { + "all_channels": "所有管道", + "all_industries": "所有產業", + "all_roles": "所有角色", + "create_a_new_survey": "建立新的問卷", + "multiple_industries": "多個產業", + "use_this_template": "使用此範本", + "uses_branching_logic": "此問卷使用分支邏輯。" + } + }, + "xm-templates": { + "ces": "CES", + "ces_description": "利用每個接觸點來瞭解客戶互動的便利性。", + "csat": "CSAT", + "csat_description": "實施最佳實務以衡量客戶滿意度。", + "enps": "eNPS", + "enps_description": "通用回饋,瞭解員工投入程度和滿意度。", + "five_star_rating": "5 星評分", + "five_star_rating_description": "用於衡量整體滿意度的通用回饋解決方案。", + "headline": "您想要取得哪種回饋?", + "nps": "NPS", + "nps_description": "實施經過驗證的最佳實務,以瞭解人們為何購買。", + "smileys": "表情符號", + "smileys_description": "使用視覺指標來擷取客戶接觸點的回饋。" + } + }, + "organizations": { + "landing": { + "no_projects_warning_subtitle": "請聯絡您的組織擁有者以取得專案存取權限。或建立自己的組織以開始使用。", + "no_projects_warning_title": "您的帳戶目前無法存取任何專案。" + }, + "projects": { + "new": { + "channel": { + "channel_select_subtitle": "分享連結或在應用程式或網站中顯示您的問卷。", + "channel_select_title": "您需要哪種問卷?", + "in_product_surveys": "產品內問卷", + "in_product_surveys_description": "嵌入應用程式或網站中。", + "link_and_email_surveys": "連結和電子郵件問卷", + "link_and_email_surveys_description": "隨時隨地線上觸及人員。" + }, + "mode": { + "formbricks_cx": "Formbricks CX", + "formbricks_cx_description": "用於瞭解您的客戶需求的問卷和報告。", + "formbricks_surveys": "Formbricks 問卷", + "formbricks_surveys_description": "適用於網站、應用程式和電子郵件問卷的多用途問卷平台。", + "what_are_you_here_for": "您來這裡是為了什麼?" + }, + "settings": { + "brand_color": "品牌顏色", + "brand_color_description": "讓問卷的主要顏色與您的品牌一致。", + "create_new_team": "建立新團隊", + "project_creation_failed": "專案建立失敗", + "project_name": "產品名稱", + "project_name_description": "您的產品名稱為何?", + "project_settings_subtitle": "當人們認出您的品牌時,他們會更願意開始並完成回應。", + "project_settings_title": "讓回應者知道是您", + "team_description": "哪些人可以存取此專案?" + } + } + } + }, + "s": { + "check_inbox_or_spam": "如果您的收件匣中沒有看到電子郵件,也請檢查您的垃圾郵件資料夾。", + "completed": "此免費且開源的問卷已關閉。", + "create_your_own": "建立您自己的", + "enter_pin": "此問卷已受保護。請輸入下方 PIN 碼", + "just_curious": "只是好奇?", + "link_invalid": "此問卷只能透過邀請填寫。", + "paused": "此免費且開源的問卷已暫時暫停。", + "please_try_again_with_the_original_link": "請使用原始連結再試一次", + "preview_survey_questions": "預覽問卷問題。", + "question_preview": "問題預覽", + "response_already_received": "我們已收到此電子郵件地址的回應。", + "response_submitted": "與此問卷和聯絡人相關的回應已經存在", + "survey_already_answered_heading": "問卷已回答。", + "survey_already_answered_subheading": "您只能使用此連結一次。", + "survey_sent_to": "問卷已發送至 '{'email'}'", + "this_looks_fishy": "這看起來可疑。", + "verify_email": "驗證電子郵件。", + "verify_email_before_submission": "驗證您的電子郵件以回應", + "verify_email_before_submission_button": "驗證", + "verify_email_before_submission_description": "若要回應此問卷,請驗證您的電子郵件", + "want_to_respond": "想要回應嗎?" + }, + "setup": { + "intro": { + "get_started": "開始使用", + "made_with_love_in_kiel": "用 \uD83E\uDD0D 在德國製造", + "paragraph_1": "Formbricks 是一套體驗管理套件,建立於全球成長最快的開源問卷平台之上。", + "paragraph_2": "在網站、應用程式或線上任何地方執行目標問卷。收集寶貴的洞察,為客戶、使用者和員工打造無法抗拒的體驗。", + "paragraph_3": "我們致力於最高程度的資料隱私權。自行託管以完全掌控您的資料。", + "welcome_to_formbricks": "歡迎使用 Formbricks!" + }, + "invite": { + "add_another_member": "新增另一位成員", + "continue": "繼續", + "failed_to_invite": "無法邀請", + "invitation_sent_to": "已發送邀請至", + "invite_your_organization_members": "邀請您的組織成員", + "life_s_no_fun_alone": "孤單一人生活不好玩。", + "skip": "跳過", + "smtp_not_configured": "SMTP 未設定", + "smtp_not_configured_description": "由於未設定電子郵件服務,因此目前無法發送邀請。您可以在稍後在組織設定中複製邀請連結。" + }, + "organization": { + "create": { + "continue": "繼續", + "delete_account": "刪除帳戶", + "delete_account_description": "如果您要刪除帳戶,可以點擊下方按鈕執行此操作。", + "description": "讓它成為您的。", + "no_membership_found": "找不到成員資格!", + "no_membership_found_description": "您目前不是任何組織的成員。如果您認為這是錯誤,請聯絡組織擁有者。", + "title": "設定您的組織" + } + }, + "signup": { + "create_administrator": "建立管理員", + "this_user_has_all_the_power": "此使用者擁有所有權限。" + } + }, + "share": { + "back_to_home": "返回首頁", + "page_not_found": "找不到頁面", + "page_not_found_description": "抱歉,我們找不到您要尋找的回應分享 ID。" + }, + "templates": { + "address": "地址", + "address_description": "要求郵寄地址", + "alignment_and_engagement_survey_description": "衡量員工與公司願景、策略和溝通的一致性,以及團隊協作。", + "alignment_and_engagement_survey_name": "與公司願景的一致性和投入程度", + "alignment_and_engagement_survey_question_1_headline": "我瞭解我的角色如何貢獻於公司的整體策略。", + "alignment_and_engagement_survey_question_1_lower_label": "不瞭解", + "alignment_and_engagement_survey_question_1_upper_label": "完全瞭解", + "alignment_and_engagement_survey_question_2_headline": "我覺得我的價值觀與公司的使命和文化一致。", + "alignment_and_engagement_survey_question_2_lower_label": "不一致", + "alignment_and_engagement_survey_question_2_upper_label": "完全一致", + "alignment_and_engagement_survey_question_3_headline": "我與我的團隊有效協作以實現我們的目標。", + "alignment_and_engagement_survey_question_3_lower_label": "協作不佳", + "alignment_and_engagement_survey_question_3_upper_label": "良好的協作", + "alignment_and_engagement_survey_question_4_headline": "公司如何改善其願景和策略一致性?", + "alignment_and_engagement_survey_question_4_placeholder": "在此輸入您的答案...", + "back": "返回", + "book_interview": "預訂面試", + "build_product_roadmap_description": "找出您的使用者最想要的一件事,然後建立它。", + "build_product_roadmap_name": "建立產品路線圖", + "build_product_roadmap_name_with_project_name": "{projectName} 路線圖輸入", + "build_product_roadmap_question_1_headline": "您對 {projectName} 的功能和特性感到滿意嗎?", + "build_product_roadmap_question_1_lower_label": "完全不滿意", + "build_product_roadmap_question_1_upper_label": "非常滿意", + "build_product_roadmap_question_2_headline": "我們應該做出哪一項變更才能最改善您的 {projectName} 體驗?", + "build_product_roadmap_question_2_placeholder": "在此輸入您的答案...", + "card_abandonment_survey": "購物車放棄問卷", + "card_abandonment_survey_description": "瞭解您網路商店中購物車放棄的原因。", + "card_abandonment_survey_question_1_button_label": "當然!", + "card_abandonment_survey_question_1_dismiss_button_label": "不用了,謝謝。", + "card_abandonment_survey_question_1_headline": "您有 2 分鐘的時間來協助我們改進嗎?", + "card_abandonment_survey_question_1_html": "

我們注意到您在購物車中留下了一些商品。我們很想瞭解原因。

", + "card_abandonment_survey_question_2_choice_1": "運費高昂", + "card_abandonment_survey_question_2_choice_2": "在其他地方找到更優惠的價格", + "card_abandonment_survey_question_2_choice_3": "只是瀏覽", + "card_abandonment_survey_question_2_choice_4": "決定不購買", + "card_abandonment_survey_question_2_choice_5": "付款問題", + "card_abandonment_survey_question_2_choice_6": "其他", + "card_abandonment_survey_question_2_headline": "您未完成購買的主要原因是什麼?", + "card_abandonment_survey_question_2_subheader": "請選取以下其中一個選項:", + "card_abandonment_survey_question_3_headline": "請詳細說明您未完成購買的原因:", + "card_abandonment_survey_question_4_headline": "您對整體購物體驗的評分如何?", + "card_abandonment_survey_question_4_lower_label": "非常不滿意", + "card_abandonment_survey_question_4_upper_label": "非常滿意", + "card_abandonment_survey_question_5_choice_1": "降低運費", + "card_abandonment_survey_question_5_choice_2": "折扣或促銷", + "card_abandonment_survey_question_5_choice_3": "更多付款選項", + "card_abandonment_survey_question_5_choice_4": "更佳的產品描述", + "card_abandonment_survey_question_5_choice_5": "改進的網站導覽", + "card_abandonment_survey_question_5_choice_6": "其他", + "card_abandonment_survey_question_5_headline": "哪些因素會鼓勵您將來完成購買?", + "card_abandonment_survey_question_5_subheader": "請選取所有適用的選項:", + "card_abandonment_survey_question_6_headline": "您是否要透過電子郵件收到折扣碼?", + "card_abandonment_survey_question_6_label": "是的,請聯絡我。", + "card_abandonment_survey_question_7_headline": "請分享您的電子郵件地址:", + "card_abandonment_survey_question_8_headline": "任何其他意見或建議?", + "career_development_survey_description": "評估員工對職業發展和發展機會的滿意度。", + "career_development_survey_name": "職涯發展問卷", + "career_development_survey_question_1_headline": "我對 {projectName} 的個人和專業成長機會感到滿意。", + "career_development_survey_question_1_lower_label": "非常不同意", + "career_development_survey_question_1_upper_label": "非常同意", + "career_development_survey_question_2_headline": "我對我在 {projectName} 的職涯發展機會感到滿意。", + "career_development_survey_question_2_lower_label": "非常不同意", + "career_development_survey_question_2_upper_label": "非常同意", + "career_development_survey_question_3_headline": "我對我的組織提供的與工作相關的訓練感到滿意。", + "career_development_survey_question_3_lower_label": "非常不同意", + "career_development_survey_question_3_upper_label": "非常同意", + "career_development_survey_question_4_headline": "我對我的組織在訓練和教育方面的投資感到滿意。", + "career_development_survey_question_4_lower_label": "非常不同意", + "career_development_survey_question_4_upper_label": "非常同意", + "career_development_survey_question_5_choice_1": "產品開發", + "career_development_survey_question_5_choice_2": "行銷", + "career_development_survey_question_5_choice_3": "公共關係", + "career_development_survey_question_5_choice_4": "會計", + "career_development_survey_question_5_choice_5": "營運", + "career_development_survey_question_5_choice_6": "其他", + "career_development_survey_question_5_headline": "您在哪個職能部門工作?", + "career_development_survey_question_5_subheader": "請選取以下其中一個", + "career_development_survey_question_6_choice_1": "個人貢獻者", + "career_development_survey_question_6_choice_2": "經理", + "career_development_survey_question_6_choice_3": "資深經理", + "career_development_survey_question_6_choice_4": "副總裁", + "career_development_survey_question_6_choice_5": "主管", + "career_development_survey_question_6_choice_6": "其他", + "career_development_survey_question_6_headline": "以下哪一項最能描述您目前的工作層級?", + "career_development_survey_question_6_subheader": "請選取以下其中一個", + "cess_survey_name": "CES 問卷", + "cess_survey_question_1_headline": "{projectName} 讓我很輕鬆地 [新增目標]", + "cess_survey_question_1_lower_label": "非常不同意", + "cess_survey_question_1_upper_label": "非常同意", + "cess_survey_question_2_headline": "謝謝!我們可以如何讓您更輕鬆地 [新增目標]?", + "cess_survey_question_2_placeholder": "在此輸入您的答案...", + "changing_subscription_experience_description": "找出人們在變更訂閱時的想法。", + "changing_subscription_experience_name": "變更訂閱體驗", + "changing_subscription_experience_question_1_choice_1": "極為困難", + "changing_subscription_experience_question_1_choice_2": "花了一段時間,但我完成了", + "changing_subscription_experience_question_1_choice_3": "還可以", + "changing_subscription_experience_question_1_choice_4": "非常容易", + "changing_subscription_experience_question_1_choice_5": "非常容易,我喜歡!", + "changing_subscription_experience_question_1_headline": "變更您的方案有多容易?", + "changing_subscription_experience_question_2_choice_1": "是,非常清楚。", + "changing_subscription_experience_question_2_choice_2": "我一開始感到困惑,但找到了我需要的內容。", + "changing_subscription_experience_question_2_choice_3": "相當複雜。", + "changing_subscription_experience_question_2_headline": "定價資訊是否容易理解?", + "churn_survey": "客戶流失問卷", + "churn_survey_description": "找出人們取消訂閱的原因。這些洞察是純金!", + "churn_survey_question_1_choice_1": "難以使用", + "churn_survey_question_1_choice_2": "太貴了", + "churn_survey_question_1_choice_3": "我缺少功能", + "churn_survey_question_1_choice_4": "糟糕的客戶服務", + "churn_survey_question_1_choice_5": "我只是不再需要它了", + "churn_survey_question_1_headline": "您為何取消訂閱?", + "churn_survey_question_1_subheader": "很抱歉看到您離開。請協助我們做得更好:", + "churn_survey_question_2_button_label": "發送", + "churn_survey_question_2_headline": "是什麼讓 {projectName} 更易於使用?", + "churn_survey_question_3_button_label": "獲得 30% 折扣", + "churn_survey_question_3_dismiss_button_label": "跳過", + "churn_survey_question_3_headline": "在未來一年獲得 30% 的折扣!", + "churn_survey_question_3_html": "

我們很樂意讓您成為客戶。我們很樂意在未來一年提供 30% 的折扣。

", + "churn_survey_question_4_headline": "您缺少哪些功能?", + "churn_survey_question_5_button_label": "發送電子郵件給 CEO", + "churn_survey_question_5_dismiss_button_label": "跳過", + "churn_survey_question_5_headline": "很抱歉聽到 \uD83D\uDE14 直接與我們的 CEO 對話!", + "churn_survey_question_5_html": "

我們旨在提供最佳的客戶服務。請發送電子郵件給我們的 CEO,她將親自處理您的問題。

", + "collect_feedback_description": "收集有關您的產品或服務的全面回饋。", + "collect_feedback_name": "收集回饋", + "collect_feedback_question_1_headline": "您對整體體驗的評分如何?", + "collect_feedback_question_1_lower_label": "不好", + "collect_feedback_question_1_subheader": "別擔心,請誠實作答。", + "collect_feedback_question_1_upper_label": "很好", + "collect_feedback_question_2_headline": "太棒了!您喜歡它什麼?", + "collect_feedback_question_2_placeholder": "在此輸入您的答案...", + "collect_feedback_question_3_headline": "感謝分享!您不喜歡什麼?", + "collect_feedback_question_3_placeholder": "在此輸入您的答案...", + "collect_feedback_question_4_headline": "您對我們的溝通評分如何?", + "collect_feedback_question_4_lower_label": "不好", + "collect_feedback_question_4_upper_label": "很好", + "collect_feedback_question_5_headline": "您還想與我們的團隊分享什麼?", + "collect_feedback_question_5_placeholder": "在此輸入您的答案...", + "collect_feedback_question_6_choice_1": "Google", + "collect_feedback_question_6_choice_2": "社群媒體", + "collect_feedback_question_6_choice_3": "朋友", + "collect_feedback_question_6_choice_4": "Podcast", + "collect_feedback_question_6_choice_5": "其他", + "collect_feedback_question_6_headline": "您如何得知我們?", + "collect_feedback_question_7_headline": "最後,我們很樂意回覆您的回饋。請分享您的電子郵件:", + "collect_feedback_question_7_placeholder": "example@email.com", + "consent": "同意", + "consent_description": "要求同意條款、條件或資料使用", + "contact_info": "聯絡資訊", + "contact_info_description": "要求姓名、電子郵件、電話號碼和公司", + "csat_description": "衡量您的產品或服務的客戶滿意度分數。", + "csat_name": "客戶滿意度分數 (CSAT)", + "csat_question_10_headline": "您有任何其他意見、問題或疑慮嗎?", + "csat_question_10_placeholder": "在此輸入您的答案...", + "csat_question_1_headline": "您向朋友或同事推薦此 {projectName} 的可能性有多高?", + "csat_question_1_lower_label": "不太可能", + "csat_question_1_upper_label": "非常可能", + "csat_question_2_choice_1": "有點滿意", + "csat_question_2_choice_2": "非常滿意", + "csat_question_2_choice_3": "既不滿意也不不滿意", + "csat_question_2_choice_4": "有點不滿意", + "csat_question_2_choice_5": "非常不滿意", + "csat_question_2_headline": "整體而言,您對我們的 {projectName} 的滿意度如何?", + "csat_question_2_subheader": "請選取其中一項:", + "csat_question_3_choice_1": "無效", + "csat_question_3_choice_10": "獨特的", + "csat_question_3_choice_2": "有用的", + "csat_question_3_choice_3": "不切實際", + "csat_question_3_choice_4": "價格過高", + "csat_question_3_choice_5": "高品質", + "csat_question_3_choice_6": "可靠", + "csat_question_3_choice_7": "物有所值", + "csat_question_3_choice_8": "品質差", + "csat_question_3_choice_9": "不可靠", + "csat_question_3_headline": "您會使用以下哪些詞語來描述我們的 {projectName}?", + "csat_question_3_subheader": "選取所有適用的項目:", + "csat_question_4_choice_1": "非常好", + "csat_question_4_choice_2": "很好", + "csat_question_4_choice_3": "還可以", + "csat_question_4_choice_4": "不太好", + "csat_question_4_choice_5": "完全不好", + "csat_question_4_headline": "我們的 {projectName} 在多大程度上滿足您的需求?", + "csat_question_4_subheader": "選取一個選項:", + "csat_question_5_choice_1": "非常高品質", + "csat_question_5_choice_2": "高品質", + "csat_question_5_choice_3": "低品質", + "csat_question_5_choice_4": "非常低品質", + "csat_question_5_choice_5": "不高也不低", + "csat_question_5_headline": "您如何評價 {projectName} 的品質?", + "csat_question_5_subheader": "選取一個選項:", + "csat_question_6_choice_1": "極佳", + "csat_question_6_choice_2": "高於平均", + "csat_question_6_choice_3": "平均", + "csat_question_6_choice_4": "低於平均", + "csat_question_6_choice_5": "差", + "csat_question_6_headline": "您如何評價 {projectName} 的性價比?", + "csat_question_6_subheader": "請選取其中一項:", + "csat_question_7_choice_1": "非常快速回應", + "csat_question_7_choice_2": "非常快速回應", + "csat_question_7_choice_3": "有點快速回應", + "csat_question_7_choice_4": "不太快速回應", + "csat_question_7_choice_5": "完全不快速回應", + "csat_question_7_choice_6": "不適用", + "csat_question_7_headline": "我們對您有關我們服務的問題的回應有多迅速?", + "csat_question_7_subheader": "請選取其中一項:", + "csat_question_8_choice_1": "這是我的第一次購買", + "csat_question_8_choice_2": "不到六個月", + "csat_question_8_choice_3": "六個月到一年", + "csat_question_8_choice_4": "1 - 2 年", + "csat_question_8_choice_5": "3 年或以上", + "csat_question_8_choice_6": "我尚未購買", + "csat_question_8_headline": "您成為 {projectName} 的客戶有多久了?", + "csat_question_8_subheader": "請選取其中一項:", + "csat_question_9_choice_1": "非常有可能", + "csat_question_9_choice_2": "非常有可能", + "csat_question_9_choice_3": "有點可能", + "csat_question_9_choice_4": "不太可能", + "csat_question_9_choice_5": "完全不可能", + "csat_question_9_headline": "您再次購買我們的任何 {projectName} 的可能性有多高?", + "csat_question_9_subheader": "選取一個選項:", + "csat_survey_name": "{projectName} CSAT", + "csat_survey_question_1_headline": "您對您的 {projectName} 體驗感到滿意嗎?", + "csat_survey_question_1_lower_label": "極度不滿意", + "csat_survey_question_1_upper_label": "極度滿意", + "csat_survey_question_2_headline": "太棒了!我們是否有任何可以改善您體驗的地方?", + "csat_survey_question_2_placeholder": "在此輸入您的答案...", + "csat_survey_question_3_headline": "唉,抱歉!我們是否有任何可以改善您體驗的地方?", + "csat_survey_question_3_placeholder": "在此輸入您的答案...", + "cta_description": "顯示資訊並提示使用者採取特定操作", + "custom_survey_description": "建立沒有範本的問卷。", + "custom_survey_name": "從頭開始", + "custom_survey_question_1_headline": "您想瞭解什麼?", + "custom_survey_question_1_placeholder": "在此輸入您的答案...", + "customer_effort_score_description": "判斷使用功能有多容易。", + "customer_effort_score_name": "客戶費力分數 (CES)", + "customer_effort_score_question_1_headline": "{projectName} 讓我很輕鬆地 [新增目標]", + "customer_effort_score_question_1_lower_label": "非常不同意", + "customer_effort_score_question_1_upper_label": "非常同意", + "customer_effort_score_question_2_headline": "謝謝!我們可以如何讓您更輕鬆地 [新增目標]?", + "customer_effort_score_question_2_placeholder": "在此輸入您的答案...", + "date": "日期", + "date_description": "要求選擇日期", + "default_ending_card_button_label": "建立您自己的問卷", + "default_ending_card_headline": "謝謝!", + "default_ending_card_subheader": "我們感謝您的回饋。", + "default_welcome_card_button_label": "下一步", + "default_welcome_card_headline": "歡迎!", + "default_welcome_card_html": "感謝您提供回饋 - 開始吧!", + "docs_feedback_description": "衡量您的開發人員文件中的每個頁面有多清晰。", + "docs_feedback_name": "文件回饋", + "docs_feedback_question_1_choice_1": "是 \uD83D\uDC4D", + "docs_feedback_question_1_choice_2": "否 \uD83D\uDC4E", + "docs_feedback_question_1_headline": "這個頁面有幫助嗎?", + "docs_feedback_question_2_headline": "請詳細說明:", + "docs_feedback_question_3_headline": "頁面網址", + "earned_advocacy_score_description": "EAS 是 NPS 的一種變體,但要求過去的實際行為,而不是崇高的意圖。", + "earned_advocacy_score_name": "已獲得倡議分數 (EAS)", + "earned_advocacy_score_question_1_choice_1": "是", + "earned_advocacy_score_question_1_choice_2": "否", + "earned_advocacy_score_question_1_headline": "您是否曾積極向其他人推薦 {projectName}?", + "earned_advocacy_score_question_2_headline": "您為何推薦我們?", + "earned_advocacy_score_question_2_placeholder": "在此輸入您的答案...", + "earned_advocacy_score_question_3_headline": "真可惜。為何不推薦?", + "earned_advocacy_score_question_3_placeholder": "在此輸入您的答案...", + "earned_advocacy_score_question_4_choice_1": "是", + "earned_advocacy_score_question_4_choice_2": "否", + "earned_advocacy_score_question_4_headline": "您是否曾積極勸阻其他人選擇 {projectName}?", + "earned_advocacy_score_question_5_headline": "是什麼讓您勸阻他們?", + "earned_advocacy_score_question_5_placeholder": "在此輸入您的答案...", + "employee_satisfaction_description": "衡量員工滿意度並找出需要改進的地方。", + "employee_satisfaction_name": "員工滿意度", + "employee_satisfaction_question_1_headline": "您對目前的角色感到滿意嗎?", + "employee_satisfaction_question_1_lower_label": "不滿意", + "employee_satisfaction_question_1_upper_label": "非常滿意", + "employee_satisfaction_question_2_choice_1": "極具意義", + "employee_satisfaction_question_2_choice_2": "非常重要", + "employee_satisfaction_question_2_choice_3": "中等程度有意義", + "employee_satisfaction_question_2_choice_4": "稍微有意義", + "employee_satisfaction_question_2_choice_5": "完全沒有意義", + "employee_satisfaction_question_2_headline": "您覺得您的工作有多大意義?", + "employee_satisfaction_question_3_headline": "您最喜歡在這裡工作的原因是什麼?", + "employee_satisfaction_question_3_placeholder": "在此輸入您的答案...", + "employee_satisfaction_question_5_headline": "對您從經理收到的支援進行評分。", + "employee_satisfaction_question_5_lower_label": "不佳", + "employee_satisfaction_question_5_upper_label": "極佳", + "employee_satisfaction_question_6_headline": "您對我們的工作場所會建議哪些改進?", + "employee_satisfaction_question_6_placeholder": "在此輸入您的答案...", + "employee_satisfaction_question_7_choice_1": "非常有可能", + "employee_satisfaction_question_7_choice_2": "非常有可能", + "employee_satisfaction_question_7_choice_3": "中等程度可能", + "employee_satisfaction_question_7_choice_4": "稍微有可能", + "employee_satisfaction_question_7_choice_5": "完全不可能", + "employee_satisfaction_question_7_headline": "您推薦我們的公司給朋友的可能性有多高?", + "employee_well_being_description": "透過工作與生活平衡、工作量和環境評估您的員工福祉。", + "employee_well_being_name": "員工福祉", + "employee_well_being_question_1_headline": "我覺得我的工作與個人生活之間取得了良好的平衡。", + "employee_well_being_question_1_lower_label": "非常不平衡", + "employee_well_being_question_1_upper_label": "極佳的平衡", + "employee_well_being_question_2_headline": "我的工作量是可管理的,讓我能夠保持生產力而不會感到壓力過大。", + "employee_well_being_question_2_lower_label": "工作量過大", + "employee_well_being_question_2_upper_label": "完全可管理", + "employee_well_being_question_3_headline": "工作環境支援我的身心健康。", + "employee_well_being_question_3_lower_label": "不支援", + "employee_well_being_question_3_upper_label": "高度支援", + "employee_well_being_question_4_headline": "在工作場所的整體福祉方面,如果有任何改進,那會是什麼?", + "employee_well_being_question_4_placeholder": "在此輸入您的答案...", + "enps_survey_name": "eNPS 問卷", + "enps_survey_question_1_headline": "您向朋友或同事推薦在此公司工作的可能性有多高?", + "enps_survey_question_1_lower_label": "完全不可能", + "enps_survey_question_1_upper_label": "非常有可能", + "enps_survey_question_2_headline": "若要協助我們改進,您能否描述您給予此評分的原因?", + "enps_survey_question_3_headline": "任何其他意見、回饋或疑慮?", + "evaluate_a_product_idea_description": "調查使用者對產品或功能想法的意見。快速取得回饋。", + "evaluate_a_product_idea_name": "評估產品想法", + "evaluate_a_product_idea_question_1_button_label": "開始吧!", + "evaluate_a_product_idea_question_1_dismiss_button_label": "跳過", + "evaluate_a_product_idea_question_1_headline": "我們喜歡您使用 {projectName} 的方式!我們很樂意請教您一個功能想法。您有時間嗎?", + "evaluate_a_product_idea_question_1_html": "

我們尊重您的時間,並盡量簡短 \uD83E\uDD38

", + "evaluate_a_product_idea_question_2_headline": "謝謝!您今天達成 [問題區域] 的難易程度如何?", + "evaluate_a_product_idea_question_2_lower_label": "非常困難", + "evaluate_a_product_idea_question_2_upper_label": "非常容易", + "evaluate_a_product_idea_question_3_headline": "當您處理 [問題區域] 時,最困難的事情是什麼?", + "evaluate_a_product_idea_question_3_placeholder": "在此輸入您的答案...", + "evaluate_a_product_idea_question_4_button_label": "下一步", + "evaluate_a_product_idea_question_4_dismiss_button_label": "跳過", + "evaluate_a_product_idea_question_4_headline": "我們正在努力解決協助處理 [問題區域] 的想法。", + "evaluate_a_product_idea_question_4_html": "

在此處插入概念簡介。新增必要的詳細資料,但保持簡潔易懂。

", + "evaluate_a_product_idea_question_5_headline": "此功能對您有多大的價值?", + "evaluate_a_product_idea_question_5_lower_label": "沒有價值", + "evaluate_a_product_idea_question_5_upper_label": "非常有價值", + "evaluate_a_product_idea_question_6_headline": "瞭解了。為何此功能對您沒有價值?", + "evaluate_a_product_idea_question_6_placeholder": "在此輸入您的答案...", + "evaluate_a_product_idea_question_7_headline": "在此功能中,對您而言最有價值的是什麼?", + "evaluate_a_product_idea_question_7_placeholder": "在此輸入您的答案...", + "evaluate_a_product_idea_question_8_headline": "我們還應該注意什麼?", + "evaluate_a_product_idea_question_8_placeholder": "在此輸入您的答案...", + "evaluate_content_quality_description": "衡量您的內容行銷文章是否恰到好處。", + "evaluate_content_quality_name": "評估內容品質", + "evaluate_content_quality_question_1_headline": "這篇文章在多大程度上解決了您希望學習的內容?", + "evaluate_content_quality_question_1_lower_label": "完全不好", + "evaluate_content_quality_question_1_upper_label": "非常好", + "evaluate_content_quality_question_2_headline": "哼!您希望看到什麼?", + "evaluate_content_quality_question_2_placeholder": "在此輸入您的答案...", + "evaluate_content_quality_question_3_headline": "太棒了!您希望我們涵蓋其他任何內容嗎?", + "evaluate_content_quality_question_3_placeholder": "主題、趨勢、教學課程...", + "fake_door_follow_up_description": "追蹤遇到您其中一個假門實驗的使用者。", + "fake_door_follow_up_name": "假門後續追蹤", + "fake_door_follow_up_question_1_headline": "此功能對您有多重要?", + "fake_door_follow_up_question_1_lower_label": "不重要", + "fake_door_follow_up_question_1_upper_label": "非常重要", + "fake_door_follow_up_question_2_choice_1": "方面 1", + "fake_door_follow_up_question_2_choice_2": "方面 2", + "fake_door_follow_up_question_2_choice_3": "方面 3", + "fake_door_follow_up_question_2_choice_4": "方面 4", + "fake_door_follow_up_question_2_headline": "在建構此功能時,絕對應該包含什麼?", + "feature_chaser_description": "追蹤剛使用特定功能的使用者。", + "feature_chaser_name": "功能追蹤", + "feature_chaser_question_1_headline": "[新增功能] 對您有多重要?", + "feature_chaser_question_1_lower_label": "不重要", + "feature_chaser_question_1_upper_label": "非常重要", + "feature_chaser_question_2_choice_1": "方面 1", + "feature_chaser_question_2_choice_2": "方面 2", + "feature_chaser_question_2_choice_3": "方面 3", + "feature_chaser_question_2_choice_4": "方面 4", + "feature_chaser_question_2_headline": "哪個方面最重要?", + "feedback_box_description": "讓您的使用者有機會順暢地分享他們的想法。", + "feedback_box_name": "意見反應方塊", + "feedback_box_question_1_choice_1": "錯誤報告 \uD83D\uDC1E", + "feedback_box_question_1_choice_2": "功能要求 \uD83D\uDCA1", + "feedback_box_question_1_headline": "您有什麼想法,老闆?", + "feedback_box_question_1_subheader": "感謝分享。我們會盡快回覆您。", + "feedback_box_question_2_headline": "哪裡壞了?", + "feedback_box_question_2_subheader": "越詳細越好 :)", + "feedback_box_question_3_button_label": "是,通知我", + "feedback_box_question_3_dismiss_button_label": "不用了,謝謝", + "feedback_box_question_3_headline": "要隨時掌握最新資訊嗎?", + "feedback_box_question_3_html": "

我們將盡快修復此問題。您想要在我們完成修復時收到通知嗎?

", + "feedback_box_question_4_button_label": "要求功能", + "feedback_box_question_4_headline": "太棒了,請告訴我們更多資訊!", + "feedback_box_question_4_placeholder": "在此輸入您的答案...", + "feedback_box_question_4_subheader": "您希望我們解決什麼問題?", + "file_upload": "檔案上傳", + "file_upload_description": "讓回應者上傳文件、圖片或其他檔案", + "finish": "完成", + "follow_ups_modal_action_body": "

嗨 \uD83D\uDC4B

感謝您撥冗回應,我們將很快與您聯繫。

祝您有美好的一天!

", + "free_text": "開放式回答", + "free_text_description": "收集開放式回饋", + "free_text_placeholder": "在此輸入您的答案...", + "gauge_feature_satisfaction_description": "評估您的產品特定功能的滿意度。", + "gauge_feature_satisfaction_name": "衡量功能滿意度", + "gauge_feature_satisfaction_question_1_headline": "達成...有多容易?", + "gauge_feature_satisfaction_question_1_lower_label": "不容易", + "gauge_feature_satisfaction_question_1_upper_label": "非常容易", + "gauge_feature_satisfaction_question_2_headline": "我們可以做哪一件事來改進?", + "identify_customer_goals_description": "更瞭解您的訊息傳遞是否符合您的產品所提供價值的正確期望。", + "identify_customer_goals_name": "識別客戶目標", + "identify_sign_up_barriers_description": "提供折扣以收集有關註冊障礙的洞察。", + "identify_sign_up_barriers_name": "識別註冊障礙", + "identify_sign_up_barriers_question_1_button_label": "獲得 10% 折扣", + "identify_sign_up_barriers_question_1_dismiss_button_label": "不用了,謝謝", + "identify_sign_up_barriers_question_1_headline": "回答這個簡短的問卷,即可獲得 10% 的折扣!", + "identify_sign_up_barriers_question_1_html": "您似乎正在考慮註冊。回答四個問題,即可在任何方案中獲得 10% 的折扣。", + "identify_sign_up_barriers_question_2_headline": "您註冊 {projectName} 的可能性有多高?", + "identify_sign_up_barriers_question_2_lower_label": "完全不可能", + "identify_sign_up_barriers_question_2_upper_label": "非常有可能", + "identify_sign_up_barriers_question_3_choice_1_label": "可能沒有我需要的內容", + "identify_sign_up_barriers_question_3_choice_2_label": "仍在比較選項", + "identify_sign_up_barriers_question_3_choice_3_label": "似乎很複雜", + "identify_sign_up_barriers_question_3_choice_4_label": "定價是個問題", + "identify_sign_up_barriers_question_3_choice_5_label": "其他", + "identify_sign_up_barriers_question_3_headline": "是什麼讓您無法嘗試 {projectName}?", + "identify_sign_up_barriers_question_4_headline": "您需要什麼,但 {projectName} 沒有提供?", + "identify_sign_up_barriers_question_4_placeholder": "在此輸入您的答案...", + "identify_sign_up_barriers_question_5_headline": "您正在查看哪些選項?", + "identify_sign_up_barriers_question_5_placeholder": "在此輸入您的答案...", + "identify_sign_up_barriers_question_6_headline": "您覺得什麼很複雜?", + "identify_sign_up_barriers_question_6_placeholder": "在此輸入您的答案...", + "identify_sign_up_barriers_question_7_headline": "您對定價有什麼顧慮?", + "identify_sign_up_barriers_question_7_placeholder": "在此輸入您的答案...", + "identify_sign_up_barriers_question_8_headline": "請說明:", + "identify_sign_up_barriers_question_8_placeholder": "在此輸入您的答案...", + "identify_sign_up_barriers_question_9_button_label": "註冊", + "identify_sign_up_barriers_question_9_dismiss_button_label": "暫時跳過", + "identify_sign_up_barriers_question_9_headline": "謝謝!這是您的程式碼:SIGNUPNOW10", + "identify_sign_up_barriers_question_9_html": "

非常感謝您撥冗分享回饋 \uD83D\uDE4F

", + "identify_sign_up_barriers_with_project_name": "{projectName} 註冊障礙", + "identify_upsell_opportunities_description": "找出您的產品為使用者節省了多少時間。使用它來追加銷售。", + "identify_upsell_opportunities_name": "識別追加銷售機會", + "identify_upsell_opportunities_question_1_choice_1": "不到 1 小時", + "identify_upsell_opportunities_question_1_choice_2": "1 到 2 小時", + "identify_upsell_opportunities_question_1_choice_3": "3 到 5 小時", + "identify_upsell_opportunities_question_1_choice_4": "5 小時以上", + "identify_upsell_opportunities_question_1_headline": "透過使用 {projectName},您的團隊每週可以節省多少小時?", + "improve_activation_rate_description": "找出您新手上路流程中的弱點,以提高使用者啟用率。", + "improve_activation_rate_name": "提高啟用率", + "improve_activation_rate_question_1_choice_1": "對我來說似乎沒有用處", + "improve_activation_rate_question_1_choice_2": "難以設定或使用", + "improve_activation_rate_question_1_choice_3": "缺少功能/特性", + "improve_activation_rate_question_1_choice_4": "只是還沒有時間", + "improve_activation_rate_question_1_choice_5": "其他", + "improve_activation_rate_question_1_headline": "您未完成設定 {projectName} 的主要原因是什麼?", + "improve_activation_rate_question_2_headline": "是什麼讓您認為 {projectName} 沒有用處?", + "improve_activation_rate_question_2_placeholder": "在此輸入您的答案...", + "improve_activation_rate_question_3_headline": "設定或使用 {projectName} 的困難之處是什麼?", + "improve_activation_rate_question_3_placeholder": "在此輸入您的答案...", + "improve_activation_rate_question_4_headline": "缺少哪些功能或特性?", + "improve_activation_rate_question_4_placeholder": "在此輸入您的答案...", + "improve_activation_rate_question_5_headline": "我們如何讓您更輕鬆地開始使用?", + "improve_activation_rate_question_5_placeholder": "在此輸入您的答案...", + "improve_activation_rate_question_6_headline": "那是什麼?請說明:", + "improve_activation_rate_question_6_placeholder": "在此輸入您的答案...", + "improve_activation_rate_question_6_subheader": "我們很樂意盡快修復它。", + "improve_newsletter_content_description": "找出您的訂閱者喜歡您的電子報內容的程度。", + "improve_newsletter_content_name": "改善電子報內容", + "improve_newsletter_content_question_1_headline": "您對本週電子報的評分如何?", + "improve_newsletter_content_question_1_lower_label": "還好", + "improve_newsletter_content_question_1_upper_label": "很好", + "improve_newsletter_content_question_2_headline": "是什麼讓本週的電子報更有幫助?", + "improve_newsletter_content_question_2_placeholder": "在此輸入您的答案...", + "improve_newsletter_content_question_3_button_label": "樂意協助!", + "improve_newsletter_content_question_3_dismiss_button_label": "自己找朋友", + "improve_newsletter_content_question_3_headline": "謝謝!❤️ 與一位朋友分享。", + "improve_newsletter_content_question_3_html": "

誰的想法和您一樣?如果您與您的一位好朋友分享本週的內容,這會對我們有很大幫助!

", + "improve_trial_conversion_description": "找出人們停止試用的原因。這些洞察可幫助您改善轉換程序。", + "improve_trial_conversion_name": "提高試用轉換率", + "improve_trial_conversion_question_1_choice_1": "我沒有從中獲得太多價值", + "improve_trial_conversion_question_1_choice_2": "我期待其他內容", + "improve_trial_conversion_question_1_choice_3": "它對於它的功能來說太貴了", + "improve_trial_conversion_question_1_choice_4": "我缺少功能", + "improve_trial_conversion_question_1_choice_5": "我只是隨便看看", + "improve_trial_conversion_question_1_headline": "您為何停止試用?", + "improve_trial_conversion_question_1_subheader": "協助我們更瞭解您:", + "improve_trial_conversion_question_2_button_label": "下一步", + "improve_trial_conversion_question_2_headline": "很抱歉聽到。使用 {projectName} 時,最大的問題是什麼?", + "improve_trial_conversion_question_4_button_label": "獲得 20% 折扣", + "improve_trial_conversion_question_4_dismiss_button_label": "跳過", + "improve_trial_conversion_question_4_headline": "很抱歉聽到!在第一年獲得 20% 的折扣。", + "improve_trial_conversion_question_4_html": "

我們很樂意為您提供年度方案的 20% 折扣。

", + "improve_trial_conversion_question_5_button_label": "下一步", + "improve_trial_conversion_question_5_headline": "您想要達成什麼?", + "improve_trial_conversion_question_5_subheader": "請選取以下其中一個選項:", + "improve_trial_conversion_question_6_headline": "您現在如何解決您的問題?", + "improve_trial_conversion_question_6_subheader": "請列出替代解決方案:", + "integration_setup_survey_description": "評估使用者將整合新增至您的產品的容易程度。找出盲點。", + "integration_setup_survey_name": "整合使用情況問卷", + "integration_setup_survey_question_1_headline": "設定此整合有多容易?", + "integration_setup_survey_question_1_lower_label": "不容易", + "integration_setup_survey_question_1_upper_label": "非常容易", + "integration_setup_survey_question_2_headline": "為何困難?", + "integration_setup_survey_question_2_placeholder": "在此輸入您的答案...", + "integration_setup_survey_question_3_headline": "您希望將哪些其他工具與 {projectName} 搭配使用?", + "integration_setup_survey_question_3_subheader": "我們不斷建構整合,您的整合可以是下一個:", + "interview_prompt_description": "邀請特定的使用者子集安排與您產品團隊的面試。", + "interview_prompt_name": "面試提示", + "interview_prompt_question_1_button_label": "預訂時段", + "interview_prompt_question_1_headline": "您有 15 分鐘的時間與我們談話嗎?\uD83D\uDE4F", + "interview_prompt_question_1_html": "您是我們的進階使用者之一。我們很樂意簡短訪問您!", + "long_term_retention_check_in_description": "衡量長期使用者滿意度、忠誠度和需要改進的領域,以保留忠實使用者。", + "long_term_retention_check_in_name": "長期保留檢查", + "long_term_retention_check_in_question_10_headline": "任何其他回饋或意見?", + "long_term_retention_check_in_question_10_placeholder": "分享任何可能有助於我們改進的想法或回饋...", + "long_term_retention_check_in_question_1_headline": "您對 {projectName} 的整體滿意度如何?", + "long_term_retention_check_in_question_1_lower_label": "不滿意", + "long_term_retention_check_in_question_1_upper_label": "非常滿意", + "long_term_retention_check_in_question_2_headline": "您認為 {projectName} 最有價值的是什麼?", + "long_term_retention_check_in_question_2_placeholder": "描述您最重視的功能或優勢...", + "long_term_retention_check_in_question_3_choice_1": "功能", + "long_term_retention_check_in_question_3_choice_2": "客戶支援", + "long_term_retention_check_in_question_3_choice_3": "使用者體驗", + "long_term_retention_check_in_question_3_choice_4": "定價", + "long_term_retention_check_in_question_3_choice_5": "可靠性和正常運作時間", + "long_term_retention_check_in_question_3_headline": "您認為 {projectName} 的哪個方面對您的體驗最重要?", + "long_term_retention_check_in_question_4_headline": "{projectName} 在多大程度上符合您的期望?", + "long_term_retention_check_in_question_4_lower_label": "未達標準", + "long_term_retention_check_in_question_4_upper_label": "超出期望", + "long_term_retention_check_in_question_5_headline": "您在使用 {projectName} 時遇到哪些挑戰或挫折?", + "long_term_retention_check_in_question_5_placeholder": "描述您希望看到的任何挑戰或改進...", + "long_term_retention_check_in_question_6_headline": "您向朋友或同事推薦 {projectName} 的可能性有多高?", + "long_term_retention_check_in_question_6_lower_label": "不太可能", + "long_term_retention_check_in_question_6_upper_label": "非常有可能", + "long_term_retention_check_in_question_7_choice_1": "新功能和改進", + "long_term_retention_check_in_question_7_choice_2": "加強的客戶支援", + "long_term_retention_check_in_question_7_choice_3": "更佳的定價選項", + "long_term_retention_check_in_question_7_choice_4": "更多整合", + "long_term_retention_check_in_question_7_choice_5": "使用者體驗改進", + "long_term_retention_check_in_question_7_headline": "哪些因素會讓您更可能保持長期使用者?", + "long_term_retention_check_in_question_8_headline": "如果可以變更 {projectName} 的一件事,您會變更什麼?", + "long_term_retention_check_in_question_8_placeholder": "分享您希望我們考慮的任何變更或功能...", + "long_term_retention_check_in_question_9_headline": "您對我們的產品更新和頻率感到滿意嗎?", + "long_term_retention_check_in_question_9_lower_label": "不滿意", + "long_term_retention_check_in_question_9_upper_label": "非常滿意", + "market_attribution_description": "瞭解使用者最初如何得知您的產品。", + "market_attribution_name": "行銷歸因", + "market_attribution_question_1_choice_1": "推薦", + "market_attribution_question_1_choice_2": "社群媒體", + "market_attribution_question_1_choice_3": "廣告", + "market_attribution_question_1_choice_4": "Google 搜尋", + "market_attribution_question_1_choice_5": "在 Podcast 中", + "market_attribution_question_1_headline": "您最初是如何得知我們的?", + "market_attribution_question_1_subheader": "請選取以下其中一個選項:", + "market_site_clarity_description": "找出放棄您行銷網站的使用者。改善您的訊息傳遞。", + "market_site_clarity_name": "行銷網站清晰度", + "market_site_clarity_question_1_choice_1": "是,完全如此", + "market_site_clarity_question_1_choice_2": "算是吧...", + "market_site_clarity_question_1_choice_3": "否,完全不是", + "market_site_clarity_question_1_headline": "您是否擁有足夠的資訊可以試用 {projectName}?", + "market_site_clarity_question_2_headline": "關於 {projectName},您缺少或不清楚什麼?", + "market_site_clarity_question_3_button_label": "獲得折扣", + "market_site_clarity_question_3_headline": "感謝您的回答!在您前 6 個月獲得 25% 的折扣:", + "matrix": "矩陣", + "matrix_description": "建立網格以針對同一組條件對多個項目進行評分", + "measure_search_experience_description": "衡量您的搜尋結果有多相關。", + "measure_search_experience_name": "衡量搜尋體驗", + "measure_search_experience_question_1_headline": "這些搜尋結果的相關性如何?", + "measure_search_experience_question_1_lower_label": "完全不相關", + "measure_search_experience_question_1_upper_label": "非常相關", + "measure_search_experience_question_2_headline": "唉!是什麼讓結果對您而言不相關?", + "measure_search_experience_question_2_placeholder": "在此輸入您的答案...", + "measure_search_experience_question_3_headline": "太棒了!我們是否有任何可以改善您體驗的地方?", + "measure_search_experience_question_3_placeholder": "在此輸入您的答案...", + "measure_task_accomplishment_description": "查看使用者是否完成了他們要完成的工作。成功的人是更好的客戶。", + "measure_task_accomplishment_name": "衡量任務完成情況", + "measure_task_accomplishment_question_1_headline": "您今天是否能夠完成您來這裡的目的?", + "measure_task_accomplishment_question_1_option_1_label": "是", + "measure_task_accomplishment_question_1_option_2_label": "正在進行中,老闆", + "measure_task_accomplishment_question_1_option_3_label": "否", + "measure_task_accomplishment_question_2_headline": "您完成目標有多容易?", + "measure_task_accomplishment_question_2_lower_label": "非常困難", + "measure_task_accomplishment_question_2_upper_label": "非常容易", + "measure_task_accomplishment_question_3_headline": "是什麼讓它變得困難?", + "measure_task_accomplishment_question_3_placeholder": "在此輸入您的答案...", + "measure_task_accomplishment_question_4_button_label": "發送", + "measure_task_accomplishment_question_4_headline": "太棒了!您今天來這裡的目的是什麼?", + "measure_task_accomplishment_question_5_button_label": "發送", + "measure_task_accomplishment_question_5_headline": "是什麼阻止了您?", + "measure_task_accomplishment_question_5_placeholder": "在此輸入您的答案...", + "multi_select": "多選", + "multi_select_description": "要求回應者選擇一個或多個選項", + "new_integration_survey_description": "找出您的使用者接下來想要看到哪些整合。", + "new_integration_survey_name": "新的整合問卷", + "new_integration_survey_question_1_choice_1": "PostHog", + "new_integration_survey_question_1_choice_2": "Segment", + "new_integration_survey_question_1_choice_3": "Hubspot", + "new_integration_survey_question_1_choice_4": "Twilio", + "new_integration_survey_question_1_choice_5": "其他", + "new_integration_survey_question_1_headline": "您正在使用哪些其他工具?", + "next": "下一步", + "nps": "淨推薦分數 (NPS)", + "nps_description": "衡量淨推薦分數 (0-10)", + "nps_lower_label": "完全不可能", + "nps_name": "淨推薦分數 (NPS)", + "nps_question_1_headline": "您向朋友或同事推薦 {projectName} 的可能性有多高?", + "nps_question_1_lower_label": "不太可能", + "nps_question_1_upper_label": "非常有可能", + "nps_question_2_headline": "是什麼讓您給予此評分?", + "nps_survey_name": "NPS 問卷", + "nps_survey_question_1_headline": "您向朋友或同事推薦 {projectName} 的可能性有多高?", + "nps_survey_question_1_lower_label": "完全不可能", + "nps_survey_question_1_upper_label": "非常有可能", + "nps_survey_question_2_headline": "若要協助我們改進,您能否描述您給予此評分的原因?", + "nps_survey_question_3_headline": "任何其他意見、回饋或疑慮?", + "nps_upper_label": "非常有可能", + "onboarding_segmentation": "新手上路區隔", + "onboarding_segmentation_description": "瞭解有關誰註冊了您的產品以及原因的詳細資訊。", + "onboarding_segmentation_question_1_choice_1": "創辦人", + "onboarding_segmentation_question_1_choice_2": "主管", + "onboarding_segmentation_question_1_choice_3": "產品經理", + "onboarding_segmentation_question_1_choice_4": "產品負責人", + "onboarding_segmentation_question_1_choice_5": "軟體工程師", + "onboarding_segmentation_question_1_headline": "您的角色是什麼?", + "onboarding_segmentation_question_1_subheader": "請選取以下其中一個選項:", + "onboarding_segmentation_question_2_choice_1": "只有我", + "onboarding_segmentation_question_2_choice_2": "1-5 位員工", + "onboarding_segmentation_question_2_choice_3": "6-10 位員工", + "onboarding_segmentation_question_2_choice_4": "11-100 位員工", + "onboarding_segmentation_question_2_choice_5": "超過 100 位員工", + "onboarding_segmentation_question_2_headline": "您的公司規模有多大?", + "onboarding_segmentation_question_2_subheader": "請選取以下其中一個選項:", + "onboarding_segmentation_question_3_choice_1": "推薦", + "onboarding_segmentation_question_3_choice_2": "社群媒體", + "onboarding_segmentation_question_3_choice_3": "廣告", + "onboarding_segmentation_question_3_choice_4": "Google 搜尋", + "onboarding_segmentation_question_3_choice_5": "在 Podcast 中", + "onboarding_segmentation_question_3_headline": "您最初是如何得知我們的?", + "onboarding_segmentation_question_3_subheader": "請選取以下其中一個選項:", + "picture_selection": "圖片選取", + "picture_selection_description": "要求回應者選擇一張或多張圖片", + "preview_survey_ending_card_description": "請繼續您的新手上路程序。", + "preview_survey_ending_card_headline": "您完成了!", + "preview_survey_name": "新問卷", + "preview_survey_question_1_headline": "您對 '{'projectName'}' 的評分如何?", + "preview_survey_question_1_lower_label": "不好", + "preview_survey_question_1_subheader": "這是問卷預覽。", + "preview_survey_question_1_upper_label": "很好", + "preview_survey_question_2_back_button_label": "返回", + "preview_survey_question_2_choice_1_label": "是,請保持通知我。", + "preview_survey_question_2_choice_2_label": "不用了,謝謝!", + "preview_survey_question_2_headline": "想要保持最新消息嗎?", + "preview_survey_welcome_card_headline": "歡迎!", + "preview_survey_welcome_card_html": "感謝您提供回饋 - 開始吧!", + "prioritize_features_description": "找出您的使用者最需要和最不需要的功能。", + "prioritize_features_name": "優先排序功能", + "prioritize_features_question_1_choice_1": "功能 1", + "prioritize_features_question_1_choice_2": "功能 2", + "prioritize_features_question_1_choice_3": "功能 3", + "prioritize_features_question_1_choice_4": "其他", + "prioritize_features_question_1_headline": "這些功能中,哪項對您而言最有價值?", + "prioritize_features_question_2_choice_1": "功能 1", + "prioritize_features_question_2_choice_2": "功能 2", + "prioritize_features_question_2_choice_3": "功能 3", + "prioritize_features_question_2_headline": "這些功能中,哪項對您而言最沒有價值?", + "prioritize_features_question_3_headline": "我們還可以如何改善您對 {projectName} 的體驗?", + "prioritize_features_question_3_placeholder": "在此輸入您的答案...", + "product_market_fit_short_description": "藉由評估使用者在您的產品消失時會有多失望來衡量 PMF。", + "product_market_fit_short_name": "產品市場匹配度問卷 (短)", + "product_market_fit_short_question_1_choice_1": "完全不會失望", + "product_market_fit_short_question_1_choice_2": "有點失望", + "product_market_fit_short_question_1_choice_3": "非常失望", + "product_market_fit_short_question_1_headline": "如果您無法再使用 {projectName},您會感到多失望?", + "product_market_fit_short_question_1_subheader": "請選取以下其中一個選項:", + "product_market_fit_short_question_2_headline": "我們如何改善 {projectName}?", + "product_market_fit_short_question_2_subheader": "請盡可能明確。", + "product_market_fit_superhuman": "產品市場匹配度 (Superhuman)", + "product_market_fit_superhuman_description": "藉由評估使用者在您的產品消失時會有多失望來衡量 PMF。", + "product_market_fit_superhuman_question_1_button_label": "樂意協助!", + "product_market_fit_superhuman_question_1_dismiss_button_label": "不用了,謝謝。", + "product_market_fit_superhuman_question_1_headline": "您是我們的進階使用者之一!您有 5 分鐘的時間嗎?", + "product_market_fit_superhuman_question_1_html": "

我們很樂意更瞭解您的使用者體驗。分享您的洞察力有很大幫助。

", + "product_market_fit_superhuman_question_2_choice_1": "完全不會失望", + "product_market_fit_superhuman_question_2_choice_2": "有點失望", + "product_market_fit_superhuman_question_2_choice_3": "非常失望", + "product_market_fit_superhuman_question_2_headline": "如果您無法再使用 {projectName},您會感到多失望?", + "product_market_fit_superhuman_question_2_subheader": "請選取以下其中一個選項:", + "product_market_fit_superhuman_question_3_choice_1": "創辦人", + "product_market_fit_superhuman_question_3_choice_2": "主管", + "product_market_fit_superhuman_question_3_choice_3": "產品經理", + "product_market_fit_superhuman_question_3_choice_4": "產品負責人", + "product_market_fit_superhuman_question_3_choice_5": "軟體工程師", + "product_market_fit_superhuman_question_3_headline": "您的角色是什麼?", + "product_market_fit_superhuman_question_3_subheader": "請選取以下其中一個選項:", + "product_market_fit_superhuman_question_4_headline": "您認為哪些類型的人最能從 {projectName} 中受益?", + "product_market_fit_superhuman_question_5_headline": "您從 {projectName} 獲得的主要好處是什麼?", + "product_market_fit_superhuman_question_6_headline": "我們如何為您改善 {projectName}?", + "product_market_fit_superhuman_question_6_subheader": "請盡可能明確。", + "professional_development_growth_survey_description": "評估員工對專業成長和發展機會的滿意度。", + "professional_development_growth_survey_name": "專業發展成長問卷", + "professional_development_growth_survey_question_1_headline": "我覺得我有機會在工作中成長和發展我的技能。", + "professional_development_growth_survey_question_1_lower_label": "沒有成長機會", + "professional_development_growth_survey_question_1_upper_label": "許多成長機會", + "professional_development_growth_survey_question_2_headline": "我有足夠的自主權來決定我如何完成我的工作。", + "professional_development_growth_survey_question_2_lower_label": "沒有自主權", + "professional_development_growth_survey_question_2_upper_label": "完全自主", + "professional_development_growth_survey_question_3_headline": "我在工作中的目標很明確,並符合我的發展。", + "professional_development_growth_survey_question_3_lower_label": "不明確的目標", + "professional_development_growth_survey_question_3_upper_label": "明確且一致的目標", + "professional_development_growth_survey_question_4_headline": "可以改善什麼以支援您的專業成長?", + "professional_development_growth_survey_question_4_placeholder": "在此輸入您的答案...", + "professional_development_survey_description": "評估員工對專業成長和發展機會的滿意度。", + "professional_development_survey_name": "專業發展問卷", + "professional_development_survey_question_1_choice_1": "是", + "professional_development_survey_question_1_choice_2": "否", + "professional_development_survey_question_1_headline": "您對專業發展活動感興趣嗎?", + "professional_development_survey_question_2_choice_1": "人脈交流活動", + "professional_development_survey_question_2_choice_2": "研討會或研討會", + "professional_development_survey_question_2_choice_3": "課程或工作坊", + "professional_development_survey_question_2_choice_4": "指導", + "professional_development_survey_question_2_choice_5": "個人研究", + "professional_development_survey_question_2_choice_6": "其他", + "professional_development_survey_question_2_headline": "您認為哪種類型的專業發展活動對您的成長最有價值?", + "professional_development_survey_question_2_subheader": "選取所有適用的項目", + "professional_development_survey_question_3_choice_1": "是", + "professional_development_survey_question_3_choice_2": "否", + "professional_development_survey_question_3_headline": "您過去是否曾投入時間進行專業發展?", + "professional_development_survey_question_4_headline": "在追求專業發展時,您在工作場所感到多大的支援?", + "professional_development_survey_question_4_lower_label": "完全不受支援", + "professional_development_survey_question_4_upper_label": "極度受支援", + "professional_development_survey_question_5_choice_1": "為了我自己的知識", + "professional_development_survey_question_5_choice_2": "為了獲得更多責任", + "professional_development_survey_question_5_choice_3": "改進我的技能", + "professional_development_survey_question_5_choice_4": "在目前職涯道路上晉升", + "professional_development_survey_question_5_choice_5": "尋找新工作", + "professional_development_survey_question_5_choice_6": "其他", + "professional_development_survey_question_5_headline": "您想要花時間進行專業發展的主要原因是什麼?", + "ranking": "排名", + "ranking_description": "要求回應者按喜好或重要性排列項目", + "rate_checkout_experience_description": "讓客戶評價結帳體驗,以調整轉換率。", + "rate_checkout_experience_name": "評價結帳體驗", + "rate_checkout_experience_question_1_headline": "完成結帳有多容易或多困難?", + "rate_checkout_experience_question_1_lower_label": "非常困難", + "rate_checkout_experience_question_1_upper_label": "非常容易", + "rate_checkout_experience_question_2_headline": "很抱歉!是什麼讓您更容易完成?", + "rate_checkout_experience_question_2_placeholder": "在此輸入您的答案...", + "rate_checkout_experience_question_3_headline": "太棒了!我們是否有任何可以改善您體驗的地方?", + "rate_checkout_experience_question_3_placeholder": "在此輸入您的答案...", + "rating": "評分", + "rating_description": "要求回應者評分(星級、表情符號、數字)", + "rating_lower_label": "不好", + "rating_upper_label": "很好", + "recognition_and_reward_survey_description": "評估員工對認可、獎勵、領導層支援和自由表達的滿意度。", + "recognition_and_reward_survey_name": "認可和獎勵", + "recognition_and_reward_survey_question_1_headline": "當我表現良好時,我的貢獻會獲得組織的認可。", + "recognition_and_reward_survey_question_1_lower_label": "完全沒有被認可", + "recognition_and_reward_survey_question_1_upper_label": "高度認可", + "recognition_and_reward_survey_question_2_headline": "我對我所做的工作感到公平地受到獎勵。", + "recognition_and_reward_survey_question_2_lower_label": "未獲得公平的獎勵", + "recognition_and_reward_survey_question_2_upper_label": "獲得非常公平的獎勵", + "recognition_and_reward_survey_question_3_headline": "我覺得在工作時可以自在地公開分享我的意見。", + "recognition_and_reward_survey_question_3_lower_label": "不自在", + "recognition_and_reward_survey_question_3_upper_label": "非常自在", + "recognition_and_reward_survey_question_4_headline": "組織如何改善認可和獎勵?", + "recognition_and_reward_survey_question_4_placeholder": "在此輸入您的答案...", + "review_prompt_description": "邀請喜歡您產品的使用者公開評論它。", + "review_prompt_name": "評論提示", + "review_prompt_question_1_headline": "您覺得 {projectName} 如何?", + "review_prompt_question_1_lower_label": "不好", + "review_prompt_question_1_upper_label": "非常滿意", + "review_prompt_question_2_button_label": "撰寫評論", + "review_prompt_question_2_headline": "很高興聽見 \uD83D\uDE4F 請為我們撰寫評論!", + "review_prompt_question_2_html": "

這對我們有很大的幫助。

", + "review_prompt_question_3_button_label": "發送", + "review_prompt_question_3_headline": "很抱歉聽到!我們應該改進哪一件事?", + "review_prompt_question_3_placeholder": "在此輸入您的答案...", + "review_prompt_question_3_subheader": "協助我們改善您的體驗。", + "schedule_a_meeting": "安排會議", + "schedule_a_meeting_description": "要求回應者預訂會議或通話的時段", + "single_select": "單選", + "single_select_description": "提供一個選項列表(僅能選擇一項)", + "site_abandonment_survey": "網站放棄問卷", + "site_abandonment_survey_description": "瞭解您網站商店中網站放棄的原因。", + "site_abandonment_survey_question_1_html": "

我們注意到您在未進行購買的情況下離開了我們的網站。我們很想瞭解原因。

", + "site_abandonment_survey_question_2_button_label": "當然!", + "site_abandonment_survey_question_2_dismiss_button_label": "不用了,謝謝。", + "site_abandonment_survey_question_2_headline": "您有時間嗎?", + "site_abandonment_survey_question_3_choice_1": "找不到我要找的東西", + "site_abandonment_survey_question_3_choice_2": "找到更好的網站", + "site_abandonment_survey_question_3_choice_3": "網站速度太慢", + "site_abandonment_survey_question_3_choice_4": "只是瀏覽", + "site_abandonment_survey_question_3_choice_5": "在其他地方找到更優惠的價格", + "site_abandonment_survey_question_3_choice_6": "其他", + "site_abandonment_survey_question_3_headline": "您離開我們網站的主要原因是什麼?", + "site_abandonment_survey_question_3_subheader": "請選取以下其中一個選項:", + "site_abandonment_survey_question_4_headline": "請詳細說明您離開網站的原因:", + "site_abandonment_survey_question_5_headline": "您對我們網站的整體體驗評分如何?", + "site_abandonment_survey_question_5_lower_label": "非常不滿意", + "site_abandonment_survey_question_5_upper_label": "非常滿意", + "site_abandonment_survey_question_6_choice_1": "更快的載入時間", + "site_abandonment_survey_question_6_choice_2": "更佳的產品搜尋功能", + "site_abandonment_survey_question_6_choice_3": "更多產品種類", + "site_abandonment_survey_question_6_choice_4": "改進的網站設計", + "site_abandonment_survey_question_6_choice_5": "更多客戶評論", + "site_abandonment_survey_question_6_choice_6": "其他", + "site_abandonment_survey_question_6_headline": "哪些改進措施可以鼓勵您在我們的網站上停留更久?", + "site_abandonment_survey_question_6_subheader": "請選取所有適用的選項:", + "site_abandonment_survey_question_7_headline": "您是否要接收有關新產品和促銷活動的更新資訊?", + "site_abandonment_survey_question_7_label": "是的,請聯絡我。", + "site_abandonment_survey_question_8_headline": "請分享您的電子郵件地址:", + "site_abandonment_survey_question_9_headline": "任何其他意見或建議?", + "skip": "跳過", + "smileys_survey_name": "表情符號問卷", + "smileys_survey_question_1_headline": "您覺得 {projectName} 如何?", + "smileys_survey_question_1_lower_label": "不好", + "smileys_survey_question_1_upper_label": "非常滿意", + "smileys_survey_question_2_button_label": "撰寫評論", + "smileys_survey_question_2_headline": "很高興聽見 \uD83D\uDE4F 請為我們撰寫評論!", + "smileys_survey_question_2_html": "

這對我們有很大的幫助。

", + "smileys_survey_question_3_button_label": "發送", + "smileys_survey_question_3_headline": "很抱歉聽到!我們應該改進哪一件事?", + "smileys_survey_question_3_placeholder": "在此輸入您的答案...", + "smileys_survey_question_3_subheader": "協助我們改善您的體驗。", + "star_rating_survey_name": "{projectName} 的評分問卷", + "star_rating_survey_question_1_headline": "您覺得 {projectName} 如何?", + "star_rating_survey_question_1_lower_label": "極度不滿意", + "star_rating_survey_question_1_upper_label": "極度滿意", + "star_rating_survey_question_2_button_label": "撰寫評論", + "star_rating_survey_question_2_headline": "很高興聽見 \uD83D\uDE4F 請為我們撰寫評論!", + "star_rating_survey_question_2_html": "

這對我們有很大的幫助。

", + "star_rating_survey_question_3_button_label": "發送", + "star_rating_survey_question_3_headline": "很抱歉聽到!我們應該改進哪一件事?", + "star_rating_survey_question_3_placeholder": "在此輸入您的答案...", + "star_rating_survey_question_3_subheader": "協助我們改善您的體驗。", + "statement_call_to_action": "陳述(行動呼籲)", + "supportive_work_culture_survey_description": "評估員工對領導層支援、溝通和整體工作環境的看法。", + "supportive_work_culture_survey_name": "支援性工作文化", + "supportive_work_culture_survey_question_1_headline": "我的經理為我提供了完成工作所需的支援。", + "supportive_work_culture_survey_question_1_lower_label": "不受支援", + "supportive_work_culture_survey_question_1_upper_label": "高度支援", + "supportive_work_culture_survey_question_2_headline": "組織內的溝通是開放且有效的。", + "supportive_work_culture_survey_question_2_lower_label": "溝通不良", + "supportive_work_culture_survey_question_2_upper_label": "良好的溝通", + "supportive_work_culture_survey_question_3_headline": "工作環境是積極的且支援我的福祉。", + "supportive_work_culture_survey_question_3_lower_label": "不支援", + "supportive_work_culture_survey_question_3_upper_label": "非常支援", + "supportive_work_culture_survey_question_4_headline": "如何改進工作文化以更好地支援您?", + "supportive_work_culture_survey_question_4_placeholder": "在此輸入您的答案...", + "uncover_strengths_and_weaknesses_description": "找出使用者喜歡和不喜歡您產品或服務的地方。", + "uncover_strengths_and_weaknesses_name": "找出優點和缺點", + "uncover_strengths_and_weaknesses_question_1_choice_1": "易於使用", + "uncover_strengths_and_weaknesses_question_1_choice_2": "物有所值", + "uncover_strengths_and_weaknesses_question_1_choice_3": "它是開源的", + "uncover_strengths_and_weaknesses_question_1_choice_4": "創辦人很可愛", + "uncover_strengths_and_weaknesses_question_1_choice_5": "其他", + "uncover_strengths_and_weaknesses_question_1_headline": "您最重視 {projectName} 的哪一點?", + "uncover_strengths_and_weaknesses_question_2_choice_1": "文件", + "uncover_strengths_and_weaknesses_question_2_choice_2": "自訂性", + "uncover_strengths_and_weaknesses_question_2_choice_3": "定價", + "uncover_strengths_and_weaknesses_question_2_choice_4": "其他", + "uncover_strengths_and_weaknesses_question_2_headline": "我們應該改進什麼?", + "uncover_strengths_and_weaknesses_question_2_subheader": "請選取以下其中一個選項:", + "uncover_strengths_and_weaknesses_question_3_headline": "您想要新增什麼嗎?", + "uncover_strengths_and_weaknesses_question_3_subheader": "請隨意發表您的意見,我們也是。", + "understand_low_engagement_description": "找出低參與度的原因以改善使用者採用率。", + "understand_low_engagement_name": "瞭解低參與度", + "understand_low_engagement_question_1_choice_1": "難以使用", + "understand_low_engagement_question_1_choice_2": "找到更好的替代方案", + "understand_low_engagement_question_1_choice_3": "只是還沒有時間", + "understand_low_engagement_question_1_choice_4": "缺少我需要的功能", + "understand_low_engagement_question_1_choice_5": "其他", + "understand_low_engagement_question_1_headline": "您最近沒有回到 {projectName} 的主要原因是什麼?", + "understand_low_engagement_question_2_headline": "使用 {projectName} 的困難之處是什麼?", + "understand_low_engagement_question_2_placeholder": "在此輸入您的答案...", + "understand_low_engagement_question_3_headline": "瞭解了。您使用哪種替代方案?", + "understand_low_engagement_question_3_placeholder": "在此輸入您的答案...", + "understand_low_engagement_question_4_headline": "瞭解了。我們如何才能讓您更容易上手?", + "understand_low_engagement_question_4_placeholder": "在此輸入您的答案...", + "understand_low_engagement_question_5_headline": "瞭解了。缺少哪些功能或特性?", + "understand_low_engagement_question_5_placeholder": "在此輸入您的答案...", + "understand_low_engagement_question_6_headline": "請新增更多詳細資料:", + "understand_low_engagement_question_6_placeholder": "在此輸入您的答案...", + "understand_purchase_intention_description": "找出您的訪客有多接近購買或訂閱。", + "understand_purchase_intention_name": "瞭解購買意願", + "understand_purchase_intention_question_1_headline": "您今天從我們這裡購物的可能性有多高?", + "understand_purchase_intention_question_1_lower_label": "完全不可能", + "understand_purchase_intention_question_1_upper_label": "非常有可能", + "understand_purchase_intention_question_2_headline": "瞭解了。您今天來訪的主要原因是什麼?", + "understand_purchase_intention_question_2_placeholder": "在此輸入您的答案...", + "understand_purchase_intention_question_3_headline": "有什麼阻礙您今天進行購買嗎?", + "understand_purchase_intention_question_3_placeholder": "在此輸入您的答案..." + } +} diff --git a/packages/lib/notion/service.ts b/apps/web/lib/notion/service.ts similarity index 94% rename from packages/lib/notion/service.ts rename to apps/web/lib/notion/service.ts index 76c03467ae..d508473783 100644 --- a/packages/lib/notion/service.ts +++ b/apps/web/lib/notion/service.ts @@ -1,10 +1,10 @@ +import { ENCRYPTION_KEY } from "@/lib/constants"; +import { symmetricDecrypt } from "@/lib/crypto"; import { TIntegrationNotion, TIntegrationNotionConfig, TIntegrationNotionDatabase, } from "@formbricks/types/integration/notion"; -import { ENCRYPTION_KEY } from "../constants"; -import { symmetricDecrypt } from "../crypto"; import { getIntegrationByType } from "../integration/service"; const fetchPages = async (config: TIntegrationNotionConfig) => { diff --git a/packages/lib/organization/auth.ts b/apps/web/lib/organization/auth.ts similarity index 95% rename from packages/lib/organization/auth.ts rename to apps/web/lib/organization/auth.ts index 133b43ea62..4795149411 100644 --- a/packages/lib/organization/auth.ts +++ b/apps/web/lib/organization/auth.ts @@ -1,10 +1,10 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; import { getMembershipByUserIdOrganizationId } from "../membership/service"; import { getAccessFlags } from "../membership/utils"; -import { organizationCache } from "../organization/cache"; import { validateInputs } from "../utils/validate"; +import { organizationCache } from "./cache"; import { getOrganizationsByUserId } from "./service"; export const canUserAccessOrganization = (userId: string, organizationId: string): Promise => diff --git a/packages/lib/organization/cache.ts b/apps/web/lib/organization/cache.ts similarity index 100% rename from packages/lib/organization/cache.ts rename to apps/web/lib/organization/cache.ts diff --git a/packages/lib/organization/service.ts b/apps/web/lib/organization/service.ts similarity index 94% rename from packages/lib/organization/service.ts rename to apps/web/lib/organization/service.ts index 7d42053fff..106a1ece7c 100644 --- a/packages/lib/organization/service.ts +++ b/apps/web/lib/organization/service.ts @@ -1,4 +1,9 @@ import "server-only"; +import { cache } from "@/lib/cache"; +import { BILLING_LIMITS, ITEMS_PER_PAGE, PROJECT_FEATURE_KEYS } from "@/lib/constants"; +import { getProjects } from "@/lib/project/service"; +import { updateUser } from "@/lib/user/service"; +import { getBillingPeriodStartDate } from "@/lib/utils/billing"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -13,11 +18,7 @@ import { ZOrganizationCreateInput, } from "@formbricks/types/organizations"; import { TUserNotificationSettings } from "@formbricks/types/user"; -import { cache } from "../cache"; -import { BILLING_LIMITS, ITEMS_PER_PAGE, PROJECT_FEATURE_KEYS } from "../constants"; import { environmentCache } from "../environment/cache"; -import { getProjects } from "../project/service"; -import { updateUser } from "../user/service"; import { validateInputs } from "../utils/validate"; import { organizationCache } from "./cache"; @@ -337,19 +338,8 @@ export const getMonthlyOrganizationResponseCount = reactCache( throw new ResourceNotFoundError("Organization", organizationId); } - // Determine the start date based on the plan type - let startDate: Date; - if (organization.billing.plan === "free") { - // For free plans, use the first day of the current calendar month - const now = new Date(); - startDate = new Date(now.getFullYear(), now.getMonth(), 1); - } else { - // For other plans, use the periodStart from billing - if (!organization.billing.periodStart) { - throw new Error("Organization billing period start is not set"); - } - startDate = organization.billing.periodStart; - } + // Use the utility function to calculate the start date + const startDate = getBillingPeriodStartDate(organization.billing); // Get all environment IDs for the organization const projects = await getProjects(organizationId); diff --git a/packages/lib/pollyfills/structuredClone.ts b/apps/web/lib/pollyfills/structuredClone.ts similarity index 100% rename from packages/lib/pollyfills/structuredClone.ts rename to apps/web/lib/pollyfills/structuredClone.ts diff --git a/packages/lib/posthogServer.ts b/apps/web/lib/posthogServer.ts similarity index 97% rename from packages/lib/posthogServer.ts rename to apps/web/lib/posthogServer.ts index 09bcde0e08..69deb88e4b 100644 --- a/packages/lib/posthogServer.ts +++ b/apps/web/lib/posthogServer.ts @@ -1,7 +1,7 @@ +import { cache } from "@/lib/cache"; import { PostHog } from "posthog-node"; import { logger } from "@formbricks/logger"; import { TOrganizationBillingPlan, TOrganizationBillingPlanLimits } from "@formbricks/types/organizations"; -import { cache } from "./cache"; import { IS_POSTHOG_CONFIGURED, IS_PRODUCTION, POSTHOG_API_HOST, POSTHOG_API_KEY } from "./constants"; const enabled = IS_PRODUCTION && IS_POSTHOG_CONFIGURED; diff --git a/packages/lib/project/cache.ts b/apps/web/lib/project/cache.ts similarity index 100% rename from packages/lib/project/cache.ts rename to apps/web/lib/project/cache.ts diff --git a/packages/lib/project/service.ts b/apps/web/lib/project/service.ts similarity index 99% rename from packages/lib/project/service.ts rename to apps/web/lib/project/service.ts index d6c34da087..76b1a06491 100644 --- a/packages/lib/project/service.ts +++ b/apps/web/lib/project/service.ts @@ -1,4 +1,5 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -7,7 +8,6 @@ import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ValidationError } from "@formbricks/types/errors"; import type { TProject } from "@formbricks/types/project"; -import { cache } from "../cache"; import { ITEMS_PER_PAGE } from "../constants"; import { validateInputs } from "../utils/validate"; import { projectCache } from "./cache"; diff --git a/packages/lib/response/auth.ts b/apps/web/lib/response/auth.ts similarity index 96% rename from packages/lib/response/auth.ts rename to apps/web/lib/response/auth.ts index 689c3c64af..9e8c10819f 100644 --- a/packages/lib/response/auth.ts +++ b/apps/web/lib/response/auth.ts @@ -1,6 +1,6 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; import { hasUserEnvironmentAccess } from "../environment/auth"; import { getSurvey } from "../survey/service"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/response/cache.ts b/apps/web/lib/response/cache.ts similarity index 100% rename from packages/lib/response/cache.ts rename to apps/web/lib/response/cache.ts diff --git a/packages/lib/response/service.ts b/apps/web/lib/response/service.ts similarity index 99% rename from packages/lib/response/service.ts rename to apps/web/lib/response/service.ts index 0bdbd40112..21bb6ee7e4 100644 --- a/packages/lib/response/service.ts +++ b/apps/web/lib/response/service.ts @@ -1,4 +1,5 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -15,7 +16,6 @@ import { } from "@formbricks/types/responses"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TTag } from "@formbricks/types/tags"; -import { cache } from "../cache"; import { ITEMS_PER_PAGE, WEBAPP_URL } from "../constants"; import { deleteDisplay } from "../display/service"; import { responseNoteCache } from "../responseNote/cache"; @@ -390,7 +390,7 @@ export const getResponseDownloadUrl = async ( "Notes", "Tags", ...metaDataFields, - ...questions, + ...questions.flat(), ...variables, ...hiddenFields, ...userAttributes, diff --git a/packages/lib/response/tests/__mocks__/data.mock.ts b/apps/web/lib/response/tests/__mocks__/data.mock.ts similarity index 99% rename from packages/lib/response/tests/__mocks__/data.mock.ts rename to apps/web/lib/response/tests/__mocks__/data.mock.ts index 6c833929ea..5ea9b1214d 100644 --- a/packages/lib/response/tests/__mocks__/data.mock.ts +++ b/apps/web/lib/response/tests/__mocks__/data.mock.ts @@ -1,6 +1,6 @@ +import { mockWelcomeCard } from "@/lib/i18n/i18n.mock"; import { Prisma } from "@prisma/client"; import { isAfter, isBefore, isSameDay } from "date-fns"; -import { mockWelcomeCard } from "i18n/i18n.mock"; import { TDisplay } from "@formbricks/types/displays"; import { TResponse, TResponseFilterCriteria, TResponseUpdateInput } from "@formbricks/types/responses"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; diff --git a/packages/lib/response/tests/constants.ts b/apps/web/lib/response/tests/constants.ts similarity index 100% rename from packages/lib/response/tests/constants.ts rename to apps/web/lib/response/tests/constants.ts diff --git a/packages/lib/response/tests/response.test.ts b/apps/web/lib/response/tests/response.test.ts similarity index 83% rename from packages/lib/response/tests/response.test.ts rename to apps/web/lib/response/tests/response.test.ts index b64b3b5d85..7701b29e4c 100644 --- a/packages/lib/response/tests/response.test.ts +++ b/apps/web/lib/response/tests/response.test.ts @@ -1,4 +1,3 @@ -import { prisma } from "../../__mocks__/database"; import { getMockUpdateResponseInput, mockContact, @@ -12,14 +11,15 @@ import { mockSurveySummaryOutput, mockTags, } from "./__mocks__/data.mock"; +import { prisma } from "@/lib/__mocks__/database"; +import { getSurveySummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary"; import { Prisma } from "@prisma/client"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { testInputValidation } from "vitestSetup"; import { PrismaErrorType } from "@formbricks/database/types/error"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponse } from "@formbricks/types/responses"; import { TTag } from "@formbricks/types/tags"; -import { getSurveySummary } from "../../../../apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary"; import { mockContactAttributeKey, mockOrganizationOutput, @@ -93,7 +93,7 @@ beforeEach(() => { describe("Tests for getResponsesBySingleUseId", () => { describe("Happy Path", () => { - it("Retrieves responses linked to a specific single-use ID", async () => { + test("Retrieves responses linked to a specific single-use ID", async () => { const responses = await getResponseBySingleUseId(mockSurveyId, mockSingleUseId); expect(responses).toEqual(expectedResponseWithoutPerson); }); @@ -102,7 +102,7 @@ describe("Tests for getResponsesBySingleUseId", () => { describe("Sad Path", () => { testInputValidation(getResponseBySingleUseId, "123#", "123#"); - it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -114,7 +114,7 @@ describe("Tests for getResponsesBySingleUseId", () => { await expect(getResponseBySingleUseId(mockSurveyId, mockSingleUseId)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for other exceptions", async () => { + test("Throws a generic Error for other exceptions", async () => { const mockErrorMessage = "Mock error message"; prisma.response.findUnique.mockRejectedValue(new Error(mockErrorMessage)); @@ -125,7 +125,7 @@ describe("Tests for getResponsesBySingleUseId", () => { describe("Tests for getResponse service", () => { describe("Happy Path", () => { - it("Retrieves a specific response by its ID", async () => { + test("Retrieves a specific response by its ID", async () => { const response = await getResponse(mockResponse.id); expect(response).toEqual(expectedResponseWithoutPerson); }); @@ -134,13 +134,13 @@ describe("Tests for getResponse service", () => { describe("Sad Path", () => { testInputValidation(getResponse, "123#"); - it("Throws ResourceNotFoundError if no response is found", async () => { + test("Throws ResourceNotFoundError if no response is found", async () => { prisma.response.findUnique.mockResolvedValue(null); const response = await getResponse(mockResponse.id); expect(response).toBeNull(); }); - it("Throws DatabaseError on PrismaClientKnownRequestError", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -152,7 +152,7 @@ describe("Tests for getResponse service", () => { await expect(getResponse(mockResponse.id)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for other unexpected issues", async () => { + test("Throws a generic Error for other unexpected issues", async () => { const mockErrorMessage = "Mock error message"; prisma.response.findUnique.mockRejectedValue(new Error(mockErrorMessage)); @@ -163,7 +163,7 @@ describe("Tests for getResponse service", () => { describe("Tests for getSurveySummary service", () => { describe("Happy Path", () => { - it("Returns a summary of the survey responses", async () => { + test("Returns a summary of the survey responses", async () => { prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); prisma.response.findMany.mockResolvedValue([mockResponse]); prisma.contactAttributeKey.findMany.mockResolvedValueOnce([mockContactAttributeKey]); @@ -176,7 +176,7 @@ describe("Tests for getSurveySummary service", () => { describe("Sad Path", () => { testInputValidation(getSurveySummary, 1); - it("Throws DatabaseError on PrismaClientKnownRequestError", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -190,7 +190,7 @@ describe("Tests for getSurveySummary service", () => { await expect(getSurveySummary(mockSurveyId)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for unexpected problems", async () => { + test("Throws a generic Error for unexpected problems", async () => { const mockErrorMessage = "Mock error message"; prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); @@ -204,7 +204,7 @@ describe("Tests for getSurveySummary service", () => { describe("Tests for getResponseDownloadUrl service", () => { describe("Happy Path", () => { - it("Returns a download URL for the csv response file", async () => { + test("Returns a download URL for the csv response file", async () => { prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); prisma.response.count.mockResolvedValue(1); prisma.response.findMany.mockResolvedValue([mockResponse]); @@ -214,7 +214,7 @@ describe("Tests for getResponseDownloadUrl service", () => { expect(fileExtension).toEqual("csv"); }); - it("Returns a download URL for the xlsx response file", async () => { + test("Returns a download URL for the xlsx response file", async () => { prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); prisma.response.count.mockResolvedValue(1); prisma.response.findMany.mockResolvedValue([mockResponse]); @@ -228,7 +228,7 @@ describe("Tests for getResponseDownloadUrl service", () => { describe("Sad Path", () => { testInputValidation(getResponseDownloadUrl, mockSurveyId, 123); - it("Throws error if response file is of different format than expected", async () => { + test("Throws error if response file is of different format than expected", async () => { prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); prisma.response.count.mockResolvedValue(1); prisma.response.findMany.mockResolvedValue([mockResponse]); @@ -238,7 +238,7 @@ describe("Tests for getResponseDownloadUrl service", () => { expect(fileExtension).not.toEqual("xlsx"); }); - it("Throws DatabaseError on PrismaClientKnownRequestError, when the getResponseCountBySurveyId fails", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError, when the getResponseCountBySurveyId fails", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -250,7 +250,7 @@ describe("Tests for getResponseDownloadUrl service", () => { await expect(getResponseDownloadUrl(mockSurveyId, "csv")).rejects.toThrow(DatabaseError); }); - it("Throws DatabaseError on PrismaClientKnownRequestError, when the getResponses fails", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError, when the getResponses fails", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -264,7 +264,7 @@ describe("Tests for getResponseDownloadUrl service", () => { await expect(getResponseDownloadUrl(mockSurveyId, "csv")).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for unexpected problems", async () => { + test("Throws a generic Error for unexpected problems", async () => { const mockErrorMessage = "Mock error message"; // error from getSurvey @@ -277,7 +277,7 @@ describe("Tests for getResponseDownloadUrl service", () => { describe("Tests for getResponsesByEnvironmentId", () => { describe("Happy Path", () => { - it("Obtains all responses associated with a specific environment ID", async () => { + test("Obtains all responses associated with a specific environment ID", async () => { const responses = await getResponsesByEnvironmentId(mockEnvironmentId); expect(responses).toEqual([expectedResponseWithoutPerson]); }); @@ -286,7 +286,7 @@ describe("Tests for getResponsesByEnvironmentId", () => { describe("Sad Path", () => { testInputValidation(getResponsesByEnvironmentId, "123#"); - it("Throws DatabaseError on PrismaClientKnownRequestError", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -298,7 +298,7 @@ describe("Tests for getResponsesByEnvironmentId", () => { await expect(getResponsesByEnvironmentId(mockEnvironmentId)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for any other unhandled exceptions", async () => { + test("Throws a generic Error for any other unhandled exceptions", async () => { const mockErrorMessage = "Mock error message"; prisma.response.findMany.mockRejectedValue(new Error(mockErrorMessage)); @@ -309,7 +309,7 @@ describe("Tests for getResponsesByEnvironmentId", () => { describe("Tests for updateResponse Service", () => { describe("Happy Path", () => { - it("Updates a response (finished = true)", async () => { + test("Updates a response (finished = true)", async () => { const response = await updateResponse(mockResponse.id, getMockUpdateResponseInput(true)); expect(response).toEqual({ ...expectedResponseWithoutPerson, @@ -317,7 +317,7 @@ describe("Tests for updateResponse Service", () => { }); }); - it("Updates a response (finished = false)", async () => { + test("Updates a response (finished = false)", async () => { const response = await updateResponse(mockResponse.id, getMockUpdateResponseInput(false)); expect(response).toEqual({ ...expectedResponseWithoutPerson, @@ -330,14 +330,14 @@ describe("Tests for updateResponse Service", () => { describe("Sad Path", () => { testInputValidation(updateResponse, "123#", {}); - it("Throws ResourceNotFoundError if no response is found", async () => { + test("Throws ResourceNotFoundError if no response is found", async () => { prisma.response.findUnique.mockResolvedValue(null); await expect(updateResponse(mockResponse.id, getMockUpdateResponseInput())).rejects.toThrow( ResourceNotFoundError ); }); - it("Throws DatabaseError on PrismaClientKnownRequestError", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -351,7 +351,7 @@ describe("Tests for updateResponse Service", () => { ); }); - it("Throws a generic Error for other unexpected issues", async () => { + test("Throws a generic Error for other unexpected issues", async () => { const mockErrorMessage = "Mock error message"; prisma.response.update.mockRejectedValue(new Error(mockErrorMessage)); @@ -362,7 +362,7 @@ describe("Tests for updateResponse Service", () => { describe("Tests for deleteResponse service", () => { describe("Happy Path", () => { - it("Successfully deletes a response based on its ID", async () => { + test("Successfully deletes a response based on its ID", async () => { const response = await deleteResponse(mockResponse.id); expect(response).toEqual(expectedResponseWithoutPerson); }); @@ -371,7 +371,7 @@ describe("Tests for deleteResponse service", () => { describe("Sad Path", () => { testInputValidation(deleteResponse, "123#"); - it("Throws DatabaseError on PrismaClientKnownRequestError", async () => { + test("Throws DatabaseError on PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -383,7 +383,7 @@ describe("Tests for deleteResponse service", () => { await expect(deleteResponse(mockResponse.id)).rejects.toThrow(DatabaseError); }); - it("Throws a generic Error for any unhandled exception during deletion", async () => { + test("Throws a generic Error for any unhandled exception during deletion", async () => { const mockErrorMessage = "Mock error message"; prisma.response.delete.mockRejectedValue(new Error(mockErrorMessage)); @@ -394,14 +394,14 @@ describe("Tests for deleteResponse service", () => { describe("Tests for getResponseCountBySurveyId service", () => { describe("Happy Path", () => { - it("Counts the total number of responses for a given survey ID", async () => { + test("Counts the total number of responses for a given survey ID", async () => { prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); const count = await getResponseCountBySurveyId(mockSurveyId); expect(count).toEqual(1); }); - it("Returns zero count when there are no responses for a given survey ID", async () => { + test("Returns zero count when there are no responses for a given survey ID", async () => { prisma.response.count.mockResolvedValue(0); const count = await getResponseCountBySurveyId(mockSurveyId); expect(count).toEqual(0); @@ -411,7 +411,7 @@ describe("Tests for getResponseCountBySurveyId service", () => { describe("Sad Path", () => { testInputValidation(getResponseCountBySurveyId, "123#"); - it("Throws a generic Error for other unexpected issues", async () => { + test("Throws a generic Error for other unexpected issues", async () => { const mockErrorMessage = "Mock error message"; prisma.response.count.mockRejectedValue(new Error(mockErrorMessage)); prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput); diff --git a/packages/lib/response/utils.ts b/apps/web/lib/response/utils.ts similarity index 94% rename from packages/lib/response/utils.ts rename to apps/web/lib/response/utils.ts index dc56c043d4..238b092048 100644 --- a/packages/lib/response/utils.ts +++ b/apps/web/lib/response/utils.ts @@ -1,4 +1,5 @@ import "server-only"; +import { getLocalizedValue } from "@/lib/i18n/utils"; import { Prisma } from "@prisma/client"; import { TResponse, @@ -9,7 +10,6 @@ import { TSurveyMetaFieldFilter, } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { getLocalizedValue } from "../i18n/utils"; import { processResponseData } from "../responses"; import { getTodaysDateTimeFormatted } from "../time"; import { getFormattedDateTimeString } from "../utils/datetime"; @@ -472,7 +472,13 @@ export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) => const metaDataFields = responses.length > 0 ? extracMetadataKeys(responses[0].meta) : []; const questions = survey.questions.map((question, idx) => { const headline = getLocalizedValue(question.headline, "default") ?? question.id; - return `${idx + 1}. ${headline}`; + if (question.type === "matrix") { + return question.rows.map((row) => { + return `${idx + 1}. ${headline} - ${getLocalizedValue(row, "default")}`; + }); + } else { + return [`${idx + 1}. ${headline}`]; + } }); const hiddenFields = survey.hiddenFields?.fieldIds || []; const userAttributes = @@ -487,7 +493,7 @@ export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) => export const getResponsesJson = ( survey: TSurvey, responses: TResponse[], - questions: string[], + questionsHeadlines: string[][], userAttributes: string[], hiddenFields: string[] ): Record[] => { @@ -519,10 +525,26 @@ export const getResponsesJson = ( }); // survey response data - questions.forEach((question, i) => { - const questionId = survey?.questions[i].id || ""; - const answer = response.data[questionId]; - jsonData[idx][question] = processResponseData(answer); + questionsHeadlines.forEach((questionHeadline) => { + const questionIndex = parseInt(questionHeadline[0]) - 1; + const question = survey?.questions[questionIndex]; + const answer = response.data[question.id]; + + if (question.type === "matrix") { + // For matrix questions, we need to handle each row separately + questionHeadline.forEach((headline, index) => { + if (answer) { + const row = question.rows[index]; + if (row && row.default && answer[row.default] !== undefined) { + jsonData[idx][headline] = answer[row.default]; + } else { + jsonData[idx][headline] = ""; + } + } + }); + } else { + jsonData[idx][questionHeadline[0]] = processResponseData(answer); + } }); survey.variables?.forEach((variable) => { diff --git a/packages/lib/responseNote/auth.ts b/apps/web/lib/responseNote/auth.ts similarity index 98% rename from packages/lib/responseNote/auth.ts rename to apps/web/lib/responseNote/auth.ts index 289c162ca6..14666e49db 100644 --- a/packages/lib/responseNote/auth.ts +++ b/apps/web/lib/responseNote/auth.ts @@ -1,5 +1,5 @@ +import { cache } from "@/lib/cache"; import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; import { canUserAccessResponse } from "../response/auth"; import { getResponse } from "../response/service"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/responseNote/cache.ts b/apps/web/lib/responseNote/cache.ts similarity index 100% rename from packages/lib/responseNote/cache.ts rename to apps/web/lib/responseNote/cache.ts diff --git a/packages/lib/responseNote/service.ts b/apps/web/lib/responseNote/service.ts similarity index 99% rename from packages/lib/responseNote/service.ts rename to apps/web/lib/responseNote/service.ts index 0af939c021..296f62dd08 100644 --- a/packages/lib/responseNote/service.ts +++ b/apps/web/lib/responseNote/service.ts @@ -1,4 +1,5 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -7,7 +8,6 @@ import { ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TResponseNote } from "@formbricks/types/responses"; -import { cache } from "../cache"; import { responseCache } from "../response/cache"; import { validateInputs } from "../utils/validate"; import { responseNoteCache } from "./cache"; diff --git a/packages/lib/responses.ts b/apps/web/lib/responses.ts similarity index 92% rename from packages/lib/responses.ts rename to apps/web/lib/responses.ts index 0e4bdeddee..0880d2104a 100644 --- a/packages/lib/responses.ts +++ b/apps/web/lib/responses.ts @@ -1,7 +1,7 @@ +import { parseRecallInfo } from "@/lib/utils/recall"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys/types"; import { getLocalizedValue } from "./i18n/utils"; -import { parseRecallInfo } from "./utils/recall"; // function to convert response value of type string | number | string[] or Record to string | string[] export const convertResponseValue = ( @@ -43,7 +43,10 @@ export const getQuestionResponseMapping = ( const answer = response.data[question.id]; questionResponseMapping.push({ - question: parseRecallInfo(getLocalizedValue(question.headline, "default"), response.data), + question: parseRecallInfo( + getLocalizedValue(question.headline, response.language ?? "default"), + response.data + ), response: convertResponseValue(answer, question), type: question.type, }); diff --git a/packages/lib/shortUrl/cache.ts b/apps/web/lib/shortUrl/cache.ts similarity index 100% rename from packages/lib/shortUrl/cache.ts rename to apps/web/lib/shortUrl/cache.ts diff --git a/packages/lib/shortUrl/service.ts b/apps/web/lib/shortUrl/service.ts similarity index 97% rename from packages/lib/shortUrl/service.ts rename to apps/web/lib/shortUrl/service.ts index fde7c1778e..bf05270e07 100644 --- a/packages/lib/shortUrl/service.ts +++ b/apps/web/lib/shortUrl/service.ts @@ -1,12 +1,12 @@ // DEPRECATED // The ShortUrl feature is deprecated and only available for backward compatibility. +import { cache } from "@/lib/cache"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { DatabaseError } from "@formbricks/types/errors"; import { TShortUrl, ZShortUrlId } from "@formbricks/types/short-url"; -import { cache } from "../cache"; import { validateInputs } from "../utils/validate"; import { shortUrlCache } from "./cache"; diff --git a/packages/lib/slack/service.ts b/apps/web/lib/slack/service.ts similarity index 100% rename from packages/lib/slack/service.ts rename to apps/web/lib/slack/service.ts diff --git a/packages/lib/storage/cache.ts b/apps/web/lib/storage/cache.ts similarity index 100% rename from packages/lib/storage/cache.ts rename to apps/web/lib/storage/cache.ts diff --git a/packages/lib/storage/service.ts b/apps/web/lib/storage/service.ts similarity index 99% rename from packages/lib/storage/service.ts rename to apps/web/lib/storage/service.ts index 371aa97f1c..dd2cd1d053 100644 --- a/packages/lib/storage/service.ts +++ b/apps/web/lib/storage/service.ts @@ -12,6 +12,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { randomUUID } from "crypto"; import { access, mkdir, readFile, rmdir, unlink, writeFile } from "fs/promises"; import { lookup } from "mime-types"; +import type { WithImplicitCoercion } from "node:buffer"; import path, { join } from "path"; import { logger } from "@formbricks/logger"; import { TAccessType } from "@formbricks/types/storage"; diff --git a/packages/lib/storage/utils.ts b/apps/web/lib/storage/utils.ts similarity index 100% rename from packages/lib/storage/utils.ts rename to apps/web/lib/storage/utils.ts diff --git a/packages/lib/styling/constants.ts b/apps/web/lib/styling/constants.ts similarity index 100% rename from packages/lib/styling/constants.ts rename to apps/web/lib/styling/constants.ts diff --git a/packages/lib/survey/auth.ts b/apps/web/lib/survey/auth.ts similarity index 96% rename from packages/lib/survey/auth.ts rename to apps/web/lib/survey/auth.ts index ab9dde031f..5ff107d193 100644 --- a/packages/lib/survey/auth.ts +++ b/apps/web/lib/survey/auth.ts @@ -1,5 +1,5 @@ +import { cache } from "@/lib/cache"; import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; import { hasUserEnvironmentAccess } from "../environment/auth"; import { validateInputs } from "../utils/validate"; import { surveyCache } from "./cache"; diff --git a/packages/lib/survey/cache.ts b/apps/web/lib/survey/cache.ts similarity index 100% rename from packages/lib/survey/cache.ts rename to apps/web/lib/survey/cache.ts diff --git a/packages/lib/survey/service.ts b/apps/web/lib/survey/service.ts similarity index 99% rename from packages/lib/survey/service.ts rename to apps/web/lib/survey/service.ts index 593c4456b8..3a82509a78 100644 --- a/packages/lib/survey/service.ts +++ b/apps/web/lib/survey/service.ts @@ -1,4 +1,10 @@ import "server-only"; +import { cache } from "@/lib/cache"; +import { segmentCache } from "@/lib/cache/segment"; +import { + getOrganizationByEnvironmentId, + subscribeOrganizationMembersToSurveyResponses, +} from "@/lib/organization/service"; import { ActionClass, Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -16,13 +22,7 @@ import { ZSurveyCreateInput, } from "@formbricks/types/surveys/types"; import { getActionClasses } from "../actionClass/service"; -import { cache } from "../cache"; -import { segmentCache } from "../cache/segment"; import { ITEMS_PER_PAGE } from "../constants"; -import { - getOrganizationByEnvironmentId, - subscribeOrganizationMembersToSurveyResponses, -} from "../organization/service"; import { capturePosthogEnvironmentEvent } from "../posthogServer"; import { getIsAIEnabled } from "../utils/ai"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/survey/tests/__mock__/survey.mock.ts b/apps/web/lib/survey/tests/__mock__/survey.mock.ts similarity index 100% rename from packages/lib/survey/tests/__mock__/survey.mock.ts rename to apps/web/lib/survey/tests/__mock__/survey.mock.ts diff --git a/packages/lib/survey/tests/survey.test.ts b/apps/web/lib/survey/tests/survey.test.ts similarity index 82% rename from packages/lib/survey/tests/survey.test.ts rename to apps/web/lib/survey/tests/survey.test.ts index 277a7de4ce..0fe4eb62e1 100644 --- a/packages/lib/survey/tests/survey.test.ts +++ b/apps/web/lib/survey/tests/survey.test.ts @@ -1,7 +1,7 @@ -import { prisma } from "../../__mocks__/database"; +import { prisma } from "@/lib/__mocks__/database"; +import { evaluateLogic } from "@/lib/surveyLogic/utils"; import { Prisma } from "@prisma/client"; -import { evaluateLogic } from "surveyLogic/utils"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { testInputValidation } from "vitestSetup"; import { PrismaErrorType } from "@formbricks/database/types/error"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; @@ -13,7 +13,6 @@ import { mockSurveyOutput, mockSurveyWithLogic, mockTransformedSurveyOutput, - mockUser, updateSurveyInput, } from "./__mock__/survey.mock"; @@ -22,7 +21,7 @@ beforeEach(() => { }); describe("evaluateLogic with mockSurveyWithLogic", () => { - it("should return true when q1 answer is blue", () => { + test("should return true when q1 answer is blue", () => { const data = { q1: "blue" }; const variablesData = {}; @@ -36,7 +35,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(true); }); - it("should return false when q1 answer is not blue", () => { + test("should return false when q1 answer is not blue", () => { const data = { q1: "red" }; const variablesData = {}; @@ -50,7 +49,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(false); }); - it("should return true when q1 is blue and q2 is pizza", () => { + test("should return true when q1 is blue and q2 is pizza", () => { const data = { q1: "blue", q2: "pizza" }; const variablesData = {}; @@ -64,7 +63,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(true); }); - it("should return false when q1 is blue but q2 is not pizza", () => { + test("should return false when q1 is blue but q2 is not pizza", () => { const data = { q1: "blue", q2: "burger" }; const variablesData = {}; @@ -78,7 +77,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(false); }); - it("should return true when q2 is pizza or q3 is Inception", () => { + test("should return true when q2 is pizza or q3 is Inception", () => { const data = { q2: "pizza", q3: "Inception" }; const variablesData = {}; @@ -92,7 +91,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(true); }); - it("should return true when var1 is equal to single select question value", () => { + test("should return true when var1 is equal to single select question value", () => { const data = { q4: "lmao" }; const variablesData = { siog1dabtpo3l0a3xoxw2922: "lmao" }; @@ -106,7 +105,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(true); }); - it("should return false when var1 is not equal to single select question value", () => { + test("should return false when var1 is not equal to single select question value", () => { const data = { q4: "lol" }; const variablesData = { siog1dabtpo3l0a3xoxw2922: "damn" }; @@ -120,7 +119,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(false); }); - it("should return true when var2 is greater than 30 and less than open text number value", () => { + test("should return true when var2 is greater than 30 and less than open text number value", () => { const data = { q5: "40" }; const variablesData = { km1srr55owtn2r7lkoh5ny1u: 35 }; @@ -134,7 +133,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(true); }); - it("should return false when var2 is not greater than 30 or greater than open text number value", () => { + test("should return false when var2 is not greater than 30 or greater than open text number value", () => { const data = { q5: "40" }; const variablesData = { km1srr55owtn2r7lkoh5ny1u: 25 }; @@ -148,7 +147,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { expect(result).toBe(false); }); - it("should return for complex condition", () => { + test("should return for complex condition", () => { const data = { q6: ["lmao", "XD"], q1: "green", q2: "pizza", q3: "inspection", name: "pizza" }; const variablesData = { siog1dabtpo3l0a3xoxw2922: "tokyo" }; @@ -165,13 +164,13 @@ describe("evaluateLogic with mockSurveyWithLogic", () => { describe("Tests for getSurvey", () => { describe("Happy Path", () => { - it("Returns a survey", async () => { + test("Returns a survey", async () => { prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); const survey = await getSurvey(mockId); expect(survey).toEqual(mockTransformedSurveyOutput); }); - it("Returns null if survey is not found", async () => { + test("Returns null if survey is not found", async () => { prisma.survey.findUnique.mockResolvedValueOnce(null); const survey = await getSurvey(mockId); expect(survey).toBeNull(); @@ -181,7 +180,7 @@ describe("Tests for getSurvey", () => { describe("Sad Path", () => { testInputValidation(getSurvey, "123#"); - it("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => { + test("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -191,7 +190,7 @@ describe("Tests for getSurvey", () => { await expect(getSurvey(mockId)).rejects.toThrow(DatabaseError); }); - it("should throw an error if there is an unknown error", async () => { + test("should throw an error if there is an unknown error", async () => { const mockErrorMessage = "Mock error message"; prisma.survey.findUnique.mockRejectedValue(new Error(mockErrorMessage)); await expect(getSurvey(mockId)).rejects.toThrow(Error); @@ -201,13 +200,13 @@ describe("Tests for getSurvey", () => { describe("Tests for getSurveysByActionClassId", () => { describe("Happy Path", () => { - it("Returns an array of surveys for a given actionClassId", async () => { + test("Returns an array of surveys for a given actionClassId", async () => { prisma.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]); const surveys = await getSurveysByActionClassId(mockId); expect(surveys).toEqual([mockTransformedSurveyOutput]); }); - it("Returns an empty array if no surveys are found", async () => { + test("Returns an empty array if no surveys are found", async () => { prisma.survey.findMany.mockResolvedValueOnce([]); const surveys = await getSurveysByActionClassId(mockId); expect(surveys).toEqual([]); @@ -217,7 +216,7 @@ describe("Tests for getSurveysByActionClassId", () => { describe("Sad Path", () => { testInputValidation(getSurveysByActionClassId, "123#"); - it("should throw an error if there is an unknown error", async () => { + test("should throw an error if there is an unknown error", async () => { const mockErrorMessage = "Unknown error occurred"; prisma.survey.findMany.mockRejectedValue(new Error(mockErrorMessage)); await expect(getSurveysByActionClassId(mockId)).rejects.toThrow(Error); @@ -227,13 +226,13 @@ describe("Tests for getSurveysByActionClassId", () => { describe("Tests for getSurveys", () => { describe("Happy Path", () => { - it("Returns an array of surveys for a given environmentId, limit(optional) and offset(optional)", async () => { + test("Returns an array of surveys for a given environmentId, limit(optional) and offset(optional)", async () => { prisma.survey.findMany.mockResolvedValueOnce([mockSurveyOutput]); const surveys = await getSurveys(mockId); expect(surveys).toEqual([mockTransformedSurveyOutput]); }); - it("Returns an empty array if no surveys are found", async () => { + test("Returns an empty array if no surveys are found", async () => { prisma.survey.findMany.mockResolvedValueOnce([]); const surveys = await getSurveys(mockId); @@ -244,7 +243,7 @@ describe("Tests for getSurveys", () => { describe("Sad Path", () => { testInputValidation(getSurveysByActionClassId, "123#"); - it("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => { + test("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -255,7 +254,7 @@ describe("Tests for getSurveys", () => { await expect(getSurveys(mockId)).rejects.toThrow(DatabaseError); }); - it("should throw an error if there is an unknown error", async () => { + test("should throw an error if there is an unknown error", async () => { const mockErrorMessage = "Unknown error occurred"; prisma.survey.findMany.mockRejectedValue(new Error(mockErrorMessage)); await expect(getSurveys(mockId)).rejects.toThrow(Error); @@ -268,7 +267,7 @@ describe("Tests for updateSurvey", () => { prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]); }); describe("Happy Path", () => { - it("Updates a survey successfully", async () => { + test("Updates a survey successfully", async () => { prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput); prisma.survey.update.mockResolvedValueOnce(mockSurveyOutput); @@ -280,14 +279,14 @@ describe("Tests for updateSurvey", () => { describe("Sad Path", () => { testInputValidation(updateSurvey, "123#"); - it("Throws ResourceNotFoundError if the survey does not exist", async () => { + test("Throws ResourceNotFoundError if the survey does not exist", async () => { prisma.survey.findUnique.mockRejectedValueOnce( new ResourceNotFoundError("Survey", updateSurveyInput.id) ); await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(ResourceNotFoundError); }); - it("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => { + test("should throw a DatabaseError error if there is a PrismaClientKnownRequestError", async () => { const mockErrorMessage = "Mock error message"; const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { code: PrismaErrorType.UniqueConstraintViolation, @@ -299,7 +298,7 @@ describe("Tests for updateSurvey", () => { await expect(updateSurvey(updateSurveyInput)).rejects.toThrow(DatabaseError); }); - it("should throw an error if there is an unknown error", async () => { + test("should throw an error if there is an unknown error", async () => { const mockErrorMessage = "Unknown error occurred"; prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); prisma.survey.update.mockRejectedValue(new Error(mockErrorMessage)); @@ -314,7 +313,7 @@ describe("Tests for updateSurvey", () => { // }); // describe("Happy Path", () => { -// it("Creates a survey successfully", async () => { +// test("Creates a survey successfully", async () => { // prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput); // prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput); // prisma.actionClass.findMany.mockResolvedValue([mockActionClass]); @@ -346,7 +345,7 @@ describe("Tests for updateSurvey", () => { // describe("Sad Path", () => { // testInputValidation(createSurvey, "123#", createSurveyInput); -// it("should throw an error if there is an unknown error", async () => { +// test("should throw an error if there is an unknown error", async () => { // const mockErrorMessage = "Unknown error occurred"; // prisma.survey.delete.mockRejectedValue(new Error(mockErrorMessage)); // await expect(createSurvey(mockId, createSurveyInput)).rejects.toThrow(Error); @@ -360,7 +359,7 @@ describe("Tests for updateSurvey", () => { // }); // describe("Happy Path", () => { -// it("Duplicates a survey successfully", async () => { +// test("Duplicates a survey successfully", async () => { // prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); // prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput); // // @ts-expect-error @@ -378,14 +377,14 @@ describe("Tests for updateSurvey", () => { // describe("Sad Path", () => { // testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#"); -// it("Throws ResourceNotFoundError if the survey does not exist", async () => { +// test("Throws ResourceNotFoundError if the survey does not exist", async () => { // prisma.survey.findUnique.mockRejectedValueOnce(new ResourceNotFoundError("Survey", mockId)); // await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow( // ResourceNotFoundError // ); // }); -// it("should throw an error if there is an unknown error", async () => { +// test("should throw an error if there is an unknown error", async () => { // const mockErrorMessage = "Unknown error occurred"; // prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage)); // await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(Error); @@ -406,7 +405,7 @@ describe("Tests for updateSurvey", () => { // prisma.attributeClass.findMany.mockResolvedValueOnce([mockAttributeClass]); // }); -// it("Returns synced surveys", async () => { +// test("Returns synced surveys", async () => { // prisma.survey.findMany.mockResolvedValueOnce([mockSyncSurveyOutput]); // prisma.person.findUnique.mockResolvedValueOnce(mockPrismaPerson); // prisma.response.findMany.mockResolvedValue([mockResponseWithMockPerson]); @@ -418,7 +417,7 @@ describe("Tests for updateSurvey", () => { // expect(surveys).toEqual([mockTransformedSyncSurveyOutput]); // }); -// it("Returns an empty array if no surveys are found", async () => { +// test("Returns an empty array if no surveys are found", async () => { // prisma.survey.findMany.mockResolvedValueOnce([]); // prisma.person.findUnique.mockResolvedValueOnce(mockPrismaPerson); // const surveys = await getSyncSurveys(mockId, mockPrismaPerson.id, "desktop", { @@ -431,7 +430,7 @@ describe("Tests for updateSurvey", () => { // describe("Sad Path", () => { // testInputValidation(getSyncSurveys, "123#", {}); -// it("does not find a Project", async () => { +// test("does not find a Project", async () => { // prisma.project.findFirst.mockResolvedValueOnce(null); // await expect( @@ -439,7 +438,7 @@ describe("Tests for updateSurvey", () => { // ).rejects.toThrow(Error); // }); -// it("should throw an error if there is an unknown error", async () => { +// test("should throw an error if there is an unknown error", async () => { // const mockErrorMessage = "Unknown error occurred"; // prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]); // prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage)); @@ -452,12 +451,12 @@ describe("Tests for updateSurvey", () => { describe("Tests for getSurveyCount service", () => { describe("Happy Path", () => { - it("Counts the total number of surveys for a given environment ID", async () => { + test("Counts the total number of surveys for a given environment ID", async () => { const count = await getSurveyCount(mockId); expect(count).toEqual(1); }); - it("Returns zero count when there are no surveys for a given environment ID", async () => { + test("Returns zero count when there are no surveys for a given environment ID", async () => { prisma.survey.count.mockResolvedValue(0); const count = await getSurveyCount(mockId); expect(count).toEqual(0); @@ -467,7 +466,7 @@ describe("Tests for getSurveyCount service", () => { describe("Sad Path", () => { testInputValidation(getSurveyCount, "123#"); - it("Throws a generic Error for other unexpected issues", async () => { + test("Throws a generic Error for other unexpected issues", async () => { const mockErrorMessage = "Mock error message"; prisma.survey.count.mockRejectedValue(new Error(mockErrorMessage)); diff --git a/packages/lib/survey/utils.ts b/apps/web/lib/survey/utils.ts similarity index 100% rename from packages/lib/survey/utils.ts rename to apps/web/lib/survey/utils.ts diff --git a/packages/lib/surveyLogic/utils.ts b/apps/web/lib/surveyLogic/utils.ts similarity index 99% rename from packages/lib/surveyLogic/utils.ts rename to apps/web/lib/surveyLogic/utils.ts index 46ee9a4215..152d6e1eae 100644 --- a/packages/lib/surveyLogic/utils.ts +++ b/apps/web/lib/surveyLogic/utils.ts @@ -1,3 +1,4 @@ +import { getLocalizedValue } from "@/lib/i18n/utils"; import { createId } from "@paralleldrive/cuid2"; import { TJsEnvironmentStateSurvey } from "@formbricks/types/js"; import { TResponseData, TResponseVariables } from "@formbricks/types/responses"; @@ -12,7 +13,6 @@ import { TSurveyQuestionTypeEnum, TSurveyVariable, } from "@formbricks/types/surveys/types"; -import { getLocalizedValue } from "../i18n/utils"; type TCondition = TSingleCondition | TConditionGroup; diff --git a/packages/lib/tag/auth.ts b/apps/web/lib/tag/auth.ts similarity index 100% rename from packages/lib/tag/auth.ts rename to apps/web/lib/tag/auth.ts diff --git a/packages/lib/tag/cache.ts b/apps/web/lib/tag/cache.ts similarity index 100% rename from packages/lib/tag/cache.ts rename to apps/web/lib/tag/cache.ts diff --git a/packages/lib/tag/service.ts b/apps/web/lib/tag/service.ts similarity index 98% rename from packages/lib/tag/service.ts rename to apps/web/lib/tag/service.ts index ae87c71372..900a7a880b 100644 --- a/packages/lib/tag/service.ts +++ b/apps/web/lib/tag/service.ts @@ -1,10 +1,10 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; import { TTag } from "@formbricks/types/tags"; -import { cache } from "../cache"; import { ITEMS_PER_PAGE } from "../constants"; import { validateInputs } from "../utils/validate"; import { tagCache } from "./cache"; diff --git a/packages/lib/tagOnResponse/auth.ts b/apps/web/lib/tagOnResponse/auth.ts similarity index 96% rename from packages/lib/tagOnResponse/auth.ts rename to apps/web/lib/tagOnResponse/auth.ts index 840fcade0f..7a90757f0d 100644 --- a/packages/lib/tagOnResponse/auth.ts +++ b/apps/web/lib/tagOnResponse/auth.ts @@ -1,6 +1,6 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; import { canUserAccessResponse } from "../response/auth"; import { canUserAccessTag } from "../tag/auth"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/tagOnResponse/cache.ts b/apps/web/lib/tagOnResponse/cache.ts similarity index 100% rename from packages/lib/tagOnResponse/cache.ts rename to apps/web/lib/tagOnResponse/cache.ts diff --git a/packages/lib/tagOnResponse/service.ts b/apps/web/lib/tagOnResponse/service.ts similarity index 98% rename from packages/lib/tagOnResponse/service.ts rename to apps/web/lib/tagOnResponse/service.ts index 2c456de387..26f49aa979 100644 --- a/packages/lib/tagOnResponse/service.ts +++ b/apps/web/lib/tagOnResponse/service.ts @@ -1,11 +1,11 @@ import "server-only"; +import { cache } from "@/lib/cache"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; import { TTagsCount, TTagsOnResponses } from "@formbricks/types/tags"; -import { cache } from "../cache"; import { responseCache } from "../response/cache"; import { getResponse } from "../response/service"; import { validateInputs } from "../utils/validate"; diff --git a/packages/lib/telemetry.ts b/apps/web/lib/telemetry.ts similarity index 97% rename from packages/lib/telemetry.ts rename to apps/web/lib/telemetry.ts index 4a06f18b20..da96a81103 100644 --- a/packages/lib/telemetry.ts +++ b/apps/web/lib/telemetry.ts @@ -2,9 +2,9 @@ and how we can improve it. All data including the IP address is collected anonymously and we cannot trace anything back to you or your customers. If you still want to disable telemetry, set the environment variable TELEMETRY_DISABLED=1 */ +import { env } from "@/lib/env"; import { logger } from "@formbricks/logger"; import { IS_PRODUCTION } from "./constants"; -import { env } from "./env"; const crypto = require("crypto"); diff --git a/packages/lib/time.ts b/apps/web/lib/time.ts similarity index 100% rename from packages/lib/time.ts rename to apps/web/lib/time.ts diff --git a/packages/lib/useDocumentVisibility.ts b/apps/web/lib/useDocumentVisibility.ts similarity index 100% rename from packages/lib/useDocumentVisibility.ts rename to apps/web/lib/useDocumentVisibility.ts diff --git a/packages/lib/user/cache.ts b/apps/web/lib/user/cache.ts similarity index 100% rename from packages/lib/user/cache.ts rename to apps/web/lib/user/cache.ts diff --git a/packages/lib/user/service.ts b/apps/web/lib/user/service.ts similarity index 98% rename from packages/lib/user/service.ts rename to apps/web/lib/user/service.ts index b6350c3640..4ad3873b3e 100644 --- a/packages/lib/user/service.ts +++ b/apps/web/lib/user/service.ts @@ -1,4 +1,6 @@ import "server-only"; +import { cache } from "@/lib/cache"; +import { deleteOrganization, getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { z } from "zod"; @@ -7,8 +9,6 @@ import { PrismaErrorType } from "@formbricks/database/types/error"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TUser, TUserLocale, TUserUpdateInput, ZUserUpdateInput } from "@formbricks/types/user"; -import { cache } from "../cache"; -import { deleteOrganization, getOrganizationsWhereUserIsSingleOwner } from "../organization/service"; import { validateInputs } from "../utils/validate"; import { userCache } from "./cache"; diff --git a/apps/web/lib/utils/action-client-middleware.ts b/apps/web/lib/utils/action-client-middleware.ts index 1a5d36d21b..c2f18e0621 100644 --- a/apps/web/lib/utils/action-client-middleware.ts +++ b/apps/web/lib/utils/action-client-middleware.ts @@ -1,9 +1,9 @@ +import { getMembershipRole } from "@/lib/membership/hooks/actions"; import { getProjectPermissionByUserId, getTeamRoleByTeamIdUserId } from "@/modules/ee/teams/lib/roles"; import { type TTeamPermission } from "@/modules/ee/teams/project-teams/types/team"; import { type TTeamRole } from "@/modules/ee/teams/team-list/types/team"; import { returnValidationErrors } from "next-safe-action"; import { ZodIssue, z } from "zod"; -import { getMembershipRole } from "@formbricks/lib/membership/hooks/actions"; import { AuthorizationError } from "@formbricks/types/errors"; import { type TOrganizationRole } from "@formbricks/types/memberships"; diff --git a/apps/web/lib/utils/action-client.ts b/apps/web/lib/utils/action-client.ts index ba73be13de..555336d7f1 100644 --- a/apps/web/lib/utils/action-client.ts +++ b/apps/web/lib/utils/action-client.ts @@ -1,7 +1,7 @@ +import { getUser } from "@/lib/user/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from "next-safe-action"; -import { getUser } from "@formbricks/lib/user/service"; import { logger } from "@formbricks/logger"; import { AuthenticationError, diff --git a/packages/lib/utils/ai.ts b/apps/web/lib/utils/ai.ts similarity index 93% rename from packages/lib/utils/ai.ts rename to apps/web/lib/utils/ai.ts index 18d639f9cf..00f1a13660 100644 --- a/packages/lib/utils/ai.ts +++ b/apps/web/lib/utils/ai.ts @@ -1,5 +1,5 @@ +import { IS_AI_CONFIGURED } from "@/lib/constants"; import { TOrganization } from "@formbricks/types/organizations"; -import { IS_AI_CONFIGURED } from "../constants"; export const getPromptText = (questionHeadline: string, response: string) => { return `**${questionHeadline.trim()}**\n${response.trim()}`; diff --git a/apps/web/lib/utils/billing.test.ts b/apps/web/lib/utils/billing.test.ts new file mode 100644 index 0000000000..f00ed8d30e --- /dev/null +++ b/apps/web/lib/utils/billing.test.ts @@ -0,0 +1,176 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { getBillingPeriodStartDate } from "./billing"; + +describe("getBillingPeriodStartDate", () => { + let originalDate: DateConstructor; + + beforeEach(() => { + // Store the original Date constructor + originalDate = global.Date; + }); + + afterEach(() => { + // Restore the original Date constructor + global.Date = originalDate; + vi.useRealTimers(); + }); + + test("returns first day of month for free plans", () => { + // Mock the current date to be 2023-03-15 + vi.setSystemTime(new Date(2023, 2, 15)); + + const organization = { + billing: { + plan: "free", + periodStart: new Date("2023-01-15"), + period: "monthly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // For free plans, should return first day of current month + expect(result).toEqual(new Date(2023, 2, 1)); + }); + + test("returns correct date for monthly plans", () => { + // Mock the current date to be 2023-03-15 + vi.setSystemTime(new Date(2023, 2, 15)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2023-02-10"), + period: "monthly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // For monthly plans, should return periodStart directly + expect(result).toEqual(new Date("2023-02-10")); + }); + + test("returns current month's subscription day for yearly plans when today is after subscription day", () => { + // Mock the current date to be March 20, 2023 + vi.setSystemTime(new Date(2023, 2, 20)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2022-05-15"), // Original subscription on 15th + period: "yearly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // Should return March 15, 2023 (same day in current month) + expect(result).toEqual(new Date(2023, 2, 15)); + }); + + test("returns previous month's subscription day for yearly plans when today is before subscription day", () => { + // Mock the current date to be March 10, 2023 + vi.setSystemTime(new Date(2023, 2, 10)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2022-05-15"), // Original subscription on 15th + period: "yearly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // Should return February 15, 2023 (same day in previous month) + expect(result).toEqual(new Date(2023, 1, 15)); + }); + + test("handles subscription day that doesn't exist in current month (February edge case)", () => { + // Mock the current date to be February 15, 2023 + vi.setSystemTime(new Date(2023, 1, 15)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2022-01-31"), // Original subscription on 31st + period: "yearly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // Should return January 31, 2023 (previous month's subscription day) + // since today (Feb 15) is less than the subscription day (31st) + expect(result).toEqual(new Date(2023, 0, 31)); + }); + + test("handles subscription day that doesn't exist in previous month (February to March transition)", () => { + // Mock the current date to be March 10, 2023 + vi.setSystemTime(new Date(2023, 2, 10)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2022-01-30"), // Original subscription on 30th + period: "yearly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // Should return February 28, 2023 (last day of February) + // since February 2023 doesn't have a 30th day + expect(result).toEqual(new Date(2023, 1, 28)); + }); + + test("handles subscription day that doesn't exist in previous month (leap year)", () => { + // Mock the current date to be March 10, 2024 (leap year) + vi.setSystemTime(new Date(2024, 2, 10)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2023-01-30"), // Original subscription on 30th + period: "yearly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // Should return February 29, 2024 (last day of February in leap year) + expect(result).toEqual(new Date(2024, 1, 29)); + }); + test("handles current month with fewer days than subscription day", () => { + // Mock the current date to be April 25, 2023 (April has 30 days) + vi.setSystemTime(new Date(2023, 3, 25)); + + const organization = { + billing: { + plan: "scale", + periodStart: new Date("2022-01-31"), // Original subscription on 31st + period: "yearly", + }, + }; + + const result = getBillingPeriodStartDate(organization.billing); + + // Should return March 31, 2023 (since today is before April's adjusted subscription day) + expect(result).toEqual(new Date(2023, 2, 31)); + }); + + test("throws error when periodStart is not set for non-free plans", () => { + const organization = { + billing: { + plan: "scale", + periodStart: null, + period: "monthly", + }, + }; + + expect(() => { + getBillingPeriodStartDate(organization.billing); + }).toThrow("billing period start is not set"); + }); +}); diff --git a/apps/web/lib/utils/billing.ts b/apps/web/lib/utils/billing.ts new file mode 100644 index 0000000000..58d88764cf --- /dev/null +++ b/apps/web/lib/utils/billing.ts @@ -0,0 +1,54 @@ +import { TOrganizationBilling } from "@formbricks/types/organizations"; + +// Function to calculate billing period start date based on organization plan and billing period +export const getBillingPeriodStartDate = (billing: TOrganizationBilling): Date => { + const now = new Date(); + if (billing.plan === "free") { + // For free plans, use the first day of the current calendar month + return new Date(now.getFullYear(), now.getMonth(), 1); + } else if (billing.period === "yearly" && billing.periodStart) { + // For yearly plans, use the same day of the month as the original subscription date + const periodStart = new Date(billing.periodStart); + // Use UTC to avoid timezone-offset shifting when parsing ISO date-only strings + const subscriptionDay = periodStart.getUTCDate(); + + // Helper function to get the last day of a specific month + const getLastDayOfMonth = (year: number, month: number): number => { + // Create a date for the first day of the next month, then subtract one day + return new Date(year, month + 1, 0).getDate(); + }; + + // Calculate the adjusted day for the current month + const lastDayOfCurrentMonth = getLastDayOfMonth(now.getFullYear(), now.getMonth()); + const adjustedCurrentMonthDay = Math.min(subscriptionDay, lastDayOfCurrentMonth); + + // Calculate the current month's adjusted subscription date + const currentMonthSubscriptionDate = new Date(now.getFullYear(), now.getMonth(), adjustedCurrentMonthDay); + + // If today is before the subscription day in the current month (or its adjusted equivalent), + // we should use the previous month's subscription day as our start date + if (now.getDate() < adjustedCurrentMonthDay) { + // Calculate previous month and year + const prevMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1; + const prevYear = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(); + + // Calculate the adjusted day for the previous month + const lastDayOfPreviousMonth = getLastDayOfMonth(prevYear, prevMonth); + const adjustedPreviousMonthDay = Math.min(subscriptionDay, lastDayOfPreviousMonth); + + // Return the adjusted previous month date + return new Date(prevYear, prevMonth, adjustedPreviousMonthDay); + } else { + return currentMonthSubscriptionDate; + } + } else if (billing.period === "monthly" && billing.periodStart) { + // For monthly plans with a periodStart, use that date + return new Date(billing.periodStart); + } else { + // For other plans, use the periodStart from billing + if (!billing.periodStart) { + throw new Error("billing period start is not set"); + } + return new Date(billing.periodStart); + } +}; diff --git a/packages/lib/utils/colors.ts b/apps/web/lib/utils/colors.ts similarity index 100% rename from packages/lib/utils/colors.ts rename to apps/web/lib/utils/colors.ts diff --git a/packages/lib/utils/contact.ts b/apps/web/lib/utils/contact.ts similarity index 100% rename from packages/lib/utils/contact.ts rename to apps/web/lib/utils/contact.ts diff --git a/packages/lib/utils/datetime.ts b/apps/web/lib/utils/datetime.ts similarity index 100% rename from packages/lib/utils/datetime.ts rename to apps/web/lib/utils/datetime.ts diff --git a/packages/lib/utils/email.ts b/apps/web/lib/utils/email.ts similarity index 100% rename from packages/lib/utils/email.ts rename to apps/web/lib/utils/email.ts diff --git a/packages/lib/utils/fileConversion.ts b/apps/web/lib/utils/fileConversion.ts similarity index 100% rename from packages/lib/utils/fileConversion.ts rename to apps/web/lib/utils/fileConversion.ts diff --git a/packages/lib/utils/headers.ts b/apps/web/lib/utils/headers.ts similarity index 100% rename from packages/lib/utils/headers.ts rename to apps/web/lib/utils/headers.ts diff --git a/packages/lib/utils/hooks/useClickOutside.ts b/apps/web/lib/utils/hooks/useClickOutside.ts similarity index 100% rename from packages/lib/utils/hooks/useClickOutside.ts rename to apps/web/lib/utils/hooks/useClickOutside.ts diff --git a/packages/lib/utils/hooks/useIntervalWhenFocused.ts b/apps/web/lib/utils/hooks/useIntervalWhenFocused.ts similarity index 100% rename from packages/lib/utils/hooks/useIntervalWhenFocused.ts rename to apps/web/lib/utils/hooks/useIntervalWhenFocused.ts diff --git a/packages/lib/utils/hooks/useSyncScroll.ts b/apps/web/lib/utils/hooks/useSyncScroll.ts similarity index 100% rename from packages/lib/utils/hooks/useSyncScroll.ts rename to apps/web/lib/utils/hooks/useSyncScroll.ts diff --git a/packages/lib/utils/locale.ts b/apps/web/lib/utils/locale.ts similarity index 94% rename from packages/lib/utils/locale.ts rename to apps/web/lib/utils/locale.ts index 1e4c0d0637..63ebdc2cb0 100644 --- a/packages/lib/utils/locale.ts +++ b/apps/web/lib/utils/locale.ts @@ -1,6 +1,6 @@ +import { AVAILABLE_LOCALES, DEFAULT_LOCALE } from "@/lib/constants"; import { headers } from "next/headers"; import { TUserLocale } from "@formbricks/types/user"; -import { AVAILABLE_LOCALES, DEFAULT_LOCALE } from "../constants"; export const findMatchingLocale = async (): Promise => { const headersList = await headers(); diff --git a/packages/lib/utils/promises.ts b/apps/web/lib/utils/promises.ts similarity index 100% rename from packages/lib/utils/promises.ts rename to apps/web/lib/utils/promises.ts diff --git a/packages/lib/utils/recall.ts b/apps/web/lib/utils/recall.ts similarity index 98% rename from packages/lib/utils/recall.ts rename to apps/web/lib/utils/recall.ts index 88f610883d..57f429cc9a 100644 --- a/packages/lib/utils/recall.ts +++ b/apps/web/lib/utils/recall.ts @@ -1,7 +1,7 @@ +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { structuredClone } from "@/lib/pollyfills/structuredClone"; import { TResponseData, TResponseDataValue, TResponseVariables } from "@formbricks/types/responses"; import { TI18nString, TSurvey, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys/types"; -import { getLocalizedValue } from "../i18n/utils"; -import { structuredClone } from "../pollyfills/structuredClone"; import { formatDateWithOrdinal, isValidDateString } from "./datetime"; export interface fallbacks { diff --git a/apps/web/lib/utils/services.ts b/apps/web/lib/utils/services.ts index 05f2d3ab3c..20936d6390 100644 --- a/apps/web/lib/utils/services.ts +++ b/apps/web/lib/utils/services.ts @@ -1,24 +1,24 @@ "use server"; +import { actionClassCache } from "@/lib/actionClass/cache"; +import { cache } from "@/lib/cache"; import { apiKeyCache } from "@/lib/cache/api-key"; import { contactCache } from "@/lib/cache/contact"; import { inviteCache } from "@/lib/cache/invite"; +import { segmentCache } from "@/lib/cache/segment"; import { teamCache } from "@/lib/cache/team"; import { webhookCache } from "@/lib/cache/webhook"; +import { environmentCache } from "@/lib/environment/cache"; +import { integrationCache } from "@/lib/integration/cache"; +import { projectCache } from "@/lib/project/cache"; +import { responseCache } from "@/lib/response/cache"; +import { responseNoteCache } from "@/lib/responseNote/cache"; +import { surveyCache } from "@/lib/survey/cache"; +import { tagCache } from "@/lib/tag/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { actionClassCache } from "@formbricks/lib/actionClass/cache"; -import { cache } from "@formbricks/lib/cache"; -import { segmentCache } from "@formbricks/lib/cache/segment"; -import { environmentCache } from "@formbricks/lib/environment/cache"; -import { integrationCache } from "@formbricks/lib/integration/cache"; -import { projectCache } from "@formbricks/lib/project/cache"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { tagCache } from "@formbricks/lib/tag/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId, ZString } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/packages/lib/utils/singleUseSurveys.ts b/apps/web/lib/utils/singleUseSurveys.ts similarity index 70% rename from packages/lib/utils/singleUseSurveys.ts rename to apps/web/lib/utils/singleUseSurveys.ts index fe3c0cfe6c..926a9a132b 100644 --- a/packages/lib/utils/singleUseSurveys.ts +++ b/apps/web/lib/utils/singleUseSurveys.ts @@ -1,6 +1,6 @@ +import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto"; +import { env } from "@/lib/env"; import cuid2 from "@paralleldrive/cuid2"; -import { decryptAES128, symmetricDecrypt, symmetricEncrypt } from "../../lib/crypto"; -import { env } from "../../lib/env"; // generate encrypted single use id for the survey export const generateSurveySingleUseId = (isEncrypted: boolean): string => { @@ -36,15 +36,7 @@ export const validateSurveySingleUseId = (surveySingleUseId: string): string | u throw new Error("ENCRYPTION_KEY is not set"); } - if (surveySingleUseId.length === 64) { - if (!env.FORMBRICKS_ENCRYPTION_KEY) { - throw new Error("FORMBRICKS_ENCRYPTION_KEY is not defined"); - } - - decryptedCuid = decryptAES128(env.FORMBRICKS_ENCRYPTION_KEY!, surveySingleUseId); - } else { - decryptedCuid = symmetricDecrypt(surveySingleUseId, env.ENCRYPTION_KEY); - } + decryptedCuid = symmetricDecrypt(surveySingleUseId, env.ENCRYPTION_KEY); if (cuid2.isCuid(decryptedCuid)) { return decryptedCuid; diff --git a/packages/lib/utils/strings.ts b/apps/web/lib/utils/strings.ts similarity index 100% rename from packages/lib/utils/strings.ts rename to apps/web/lib/utils/strings.ts diff --git a/packages/lib/utils/styling.ts b/apps/web/lib/utils/styling.ts similarity index 100% rename from packages/lib/utils/styling.ts rename to apps/web/lib/utils/styling.ts diff --git a/packages/lib/utils/templates.ts b/apps/web/lib/utils/templates.ts similarity index 91% rename from packages/lib/utils/templates.ts rename to apps/web/lib/utils/templates.ts index cd4763e156..3506caf358 100644 --- a/packages/lib/utils/templates.ts +++ b/apps/web/lib/utils/templates.ts @@ -1,8 +1,8 @@ +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { structuredClone } from "@/lib/pollyfills/structuredClone"; import { TProject } from "@formbricks/types/project"; import { TSurveyQuestion } from "@formbricks/types/surveys/types"; import { TTemplate } from "@formbricks/types/templates"; -import { getLocalizedValue } from "../i18n/utils"; -import { structuredClone } from "../pollyfills/structuredClone"; export const replaceQuestionPresetPlaceholders = ( question: TSurveyQuestion, diff --git a/packages/lib/utils/url.ts b/apps/web/lib/utils/url.ts similarity index 100% rename from packages/lib/utils/url.ts rename to apps/web/lib/utils/url.ts diff --git a/packages/lib/utils/validate.ts b/apps/web/lib/utils/validate.ts similarity index 100% rename from packages/lib/utils/validate.ts rename to apps/web/lib/utils/validate.ts diff --git a/packages/lib/utils/videoUpload.ts b/apps/web/lib/utils/videoUpload.ts similarity index 100% rename from packages/lib/utils/videoUpload.ts rename to apps/web/lib/utils/videoUpload.ts diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index e79837acc8..9edf547d57 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -18,20 +18,14 @@ import { isSyncWithUserIdentificationEndpoint, isVerifyEmailRoute, } from "@/app/middleware/endpoint-validator"; +import { E2E_TESTING, IS_PRODUCTION, RATE_LIMITING_DISABLED, SURVEY_URL, WEBAPP_URL } from "@/lib/constants"; +import { isValidCallbackUrl } from "@/lib/utils/url"; import { logApiError } from "@/modules/api/v2/lib/utils"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { ipAddress } from "@vercel/functions"; import { getToken } from "next-auth/jwt"; import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; -import { - E2E_TESTING, - IS_PRODUCTION, - RATE_LIMITING_DISABLED, - SURVEY_URL, - WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { isValidCallbackUrl } from "@formbricks/lib/utils/url"; import { logger } from "@formbricks/logger"; const enforceHttps = (request: NextRequest): Response | null => { @@ -42,7 +36,7 @@ const enforceHttps = (request: NextRequest): Response | null => { details: [ { field: "", - issue: "Only HTTPS connections are allowed on the management and contacts bulk endpoints.", + issue: "Only HTTPS connections are allowed on the management endpoints.", }, ], }; @@ -54,18 +48,22 @@ const enforceHttps = (request: NextRequest): Response | null => { const handleAuth = async (request: NextRequest): Promise => { const token = await getToken({ req: request as any }); + if (isAuthProtectedRoute(request.nextUrl.pathname) && !token) { const loginUrl = `${WEBAPP_URL}/auth/login?callbackUrl=${encodeURIComponent(WEBAPP_URL + request.nextUrl.pathname + request.nextUrl.search)}`; return NextResponse.redirect(loginUrl); } const callbackUrl = request.nextUrl.searchParams.get("callbackUrl"); + if (callbackUrl && !isValidCallbackUrl(callbackUrl, WEBAPP_URL)) { return NextResponse.json({ error: "Invalid callback URL" }, { status: 400 }); } + if (token && callbackUrl) { - return NextResponse.redirect(WEBAPP_URL + callbackUrl); + return NextResponse.redirect(callbackUrl); } + return null; }; diff --git a/apps/web/modules/account/components/DeleteAccountModal/actions.test.ts b/apps/web/modules/account/components/DeleteAccountModal/actions.test.ts new file mode 100644 index 0000000000..9dea047327 --- /dev/null +++ b/apps/web/modules/account/components/DeleteAccountModal/actions.test.ts @@ -0,0 +1,74 @@ +import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service"; +import { deleteUser } from "@/lib/user/service"; +import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; +import { describe, expect, test, vi } from "vitest"; +import { OperationNotAllowedError } from "@formbricks/types/errors"; +import { TOrganization } from "@formbricks/types/organizations"; +import { TUser } from "@formbricks/types/user"; +import { deleteUserAction } from "./actions"; + +// Mock all dependencies +vi.mock("@/lib/user/service", () => ({ + deleteUser: vi.fn(), +})); + +vi.mock("@/lib/organization/service", () => ({ + getOrganizationsWhereUserIsSingleOwner: vi.fn(), +})); + +vi.mock("@/modules/ee/license-check/lib/utils", () => ({ + getIsMultiOrgEnabled: vi.fn(), +})); + +// add a mock to authenticatedActionClient.action +vi.mock("@/lib/utils/action-client", () => ({ + authenticatedActionClient: { + action: (fn: any) => { + return fn; + }, + }, +})); + +describe("deleteUserAction", () => { + test("deletes user successfully when multi-org is enabled", async () => { + const ctx = { user: { id: "test-user" } }; + vi.mocked(deleteUser).mockResolvedValueOnce({ id: "test-user" } as TUser); + vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValueOnce([]); + vi.mocked(getIsMultiOrgEnabled).mockResolvedValueOnce(true); + + const result = await deleteUserAction({ ctx } as any); + + expect(result).toStrictEqual({ id: "test-user" } as TUser); + expect(deleteUser).toHaveBeenCalledWith("test-user"); + expect(getOrganizationsWhereUserIsSingleOwner).toHaveBeenCalledWith("test-user"); + expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1); + }); + + test("deletes user successfully when multi-org is disabled but user is not sole owner of any org", async () => { + const ctx = { user: { id: "another-user" } }; + vi.mocked(deleteUser).mockResolvedValueOnce({ id: "another-user" } as TUser); + vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValueOnce([]); + vi.mocked(getIsMultiOrgEnabled).mockResolvedValueOnce(false); + + const result = await deleteUserAction({ ctx } as any); + + expect(result).toStrictEqual({ id: "another-user" } as TUser); + expect(deleteUser).toHaveBeenCalledWith("another-user"); + expect(getOrganizationsWhereUserIsSingleOwner).toHaveBeenCalledWith("another-user"); + expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1); + }); + + test("throws OperationNotAllowedError when user is sole owner in at least one org and multi-org is disabled", async () => { + const ctx = { user: { id: "sole-owner-user" } }; + vi.mocked(deleteUser).mockResolvedValueOnce({ id: "test-user" } as TUser); + vi.mocked(getOrganizationsWhereUserIsSingleOwner).mockResolvedValueOnce([ + { id: "org-1" } as TOrganization, + ]); + vi.mocked(getIsMultiOrgEnabled).mockResolvedValueOnce(false); + + await expect(() => deleteUserAction({ ctx } as any)).rejects.toThrow(OperationNotAllowedError); + expect(deleteUser).not.toHaveBeenCalled(); + expect(getOrganizationsWhereUserIsSingleOwner).toHaveBeenCalledWith("sole-owner-user"); + expect(getIsMultiOrgEnabled).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/web/modules/account/components/DeleteAccountModal/actions.ts b/apps/web/modules/account/components/DeleteAccountModal/actions.ts index 87b4d9ac40..4d195fe724 100644 --- a/apps/web/modules/account/components/DeleteAccountModal/actions.ts +++ b/apps/web/modules/account/components/DeleteAccountModal/actions.ts @@ -1,9 +1,9 @@ "use server"; +import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service"; +import { deleteUser } from "@/lib/user/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service"; -import { deleteUser } from "@formbricks/lib/user/service"; import { OperationNotAllowedError } from "@formbricks/types/errors"; export const deleteUserAction = authenticatedActionClient.action(async ({ ctx }) => { diff --git a/apps/web/modules/account/components/DeleteAccountModal/index.test.tsx b/apps/web/modules/account/components/DeleteAccountModal/index.test.tsx new file mode 100644 index 0000000000..80c49f2630 --- /dev/null +++ b/apps/web/modules/account/components/DeleteAccountModal/index.test.tsx @@ -0,0 +1,161 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import * as nextAuth from "next-auth/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TOrganization } from "@formbricks/types/organizations"; +import { TUser } from "@formbricks/types/user"; +import * as actions from "./actions"; +import { DeleteAccountModal } from "./index"; + +vi.mock("next-auth/react", async () => { + const actual = await vi.importActual("next-auth/react"); + return { + ...actual, + signOut: vi.fn(), + }; +}); + +vi.mock("./actions", () => ({ + deleteUserAction: vi.fn(), +})); + +describe("DeleteAccountModal", () => { + const mockUser: TUser = { + email: "test@example.com", + } as TUser; + + const mockOrgs: TOrganization[] = [{ name: "Org1" }, { name: "Org2" }] as TOrganization[]; + + const mockSetOpen = vi.fn(); + const mockLogout = vi.fn(); + + afterEach(() => { + cleanup(); + }); + + test("renders modal with correct props", () => { + render( + + ); + + expect(screen.getByText("Org1")).toBeInTheDocument(); + expect(screen.getByText("Org2")).toBeInTheDocument(); + }); + + test("disables delete button when email does not match", () => { + render( + + ); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: "wrong@example.com" } }); + expect(input).toHaveValue("wrong@example.com"); + }); + + test("allows account deletion flow (non-cloud)", async () => { + const deleteUserAction = vi + .spyOn(actions, "deleteUserAction") + .mockResolvedValue("deleted-user-id" as any); // the return doesn't matter here + const signOut = vi.spyOn(nextAuth, "signOut").mockResolvedValue(undefined); + + render( + + ); + + const input = screen.getByTestId("deleteAccountConfirmation"); + fireEvent.change(input, { target: { value: mockUser.email } }); + + const form = screen.getByTestId("deleteAccountForm"); + fireEvent.submit(form); + + await waitFor(() => { + expect(deleteUserAction).toHaveBeenCalled(); + expect(mockLogout).toHaveBeenCalled(); + expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/auth/login" }); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); + + test("allows account deletion flow (cloud)", async () => { + const deleteUserAction = vi + .spyOn(actions, "deleteUserAction") + .mockResolvedValue("deleted-user-id" as any); // the return doesn't matter here + const signOut = vi.spyOn(nextAuth, "signOut").mockResolvedValue(undefined); + + Object.defineProperty(window, "location", { + writable: true, + value: { replace: vi.fn() }, + }); + + render( + + ); + + const input = screen.getByTestId("deleteAccountConfirmation"); + fireEvent.change(input, { target: { value: mockUser.email } }); + + const form = screen.getByTestId("deleteAccountForm"); + fireEvent.submit(form); + + await waitFor(() => { + expect(deleteUserAction).toHaveBeenCalled(); + expect(mockLogout).toHaveBeenCalled(); + expect(signOut).toHaveBeenCalledWith({ redirect: true }); + expect(window.location.replace).toHaveBeenCalled(); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); + + test("handles deletion errors", async () => { + const deleteUserAction = vi.spyOn(actions, "deleteUserAction").mockRejectedValue(new Error("fail")); + + render( + + ); + + const input = screen.getByTestId("deleteAccountConfirmation"); + fireEvent.change(input, { target: { value: mockUser.email } }); + + const form = screen.getByTestId("deleteAccountForm"); + fireEvent.submit(form); + + await waitFor(() => { + expect(deleteUserAction).toHaveBeenCalled(); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/apps/web/modules/account/components/DeleteAccountModal/index.tsx b/apps/web/modules/account/components/DeleteAccountModal/index.tsx index 7c5c50fb1f..158da36101 100644 --- a/apps/web/modules/account/components/DeleteAccountModal/index.tsx +++ b/apps/web/modules/account/components/DeleteAccountModal/index.tsx @@ -2,8 +2,7 @@ import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { Input } from "@/modules/ui/components/input"; -import { useTranslate } from "@tolgee/react"; -import { T } from "@tolgee/react"; +import { T, useTranslate } from "@tolgee/react"; import { signOut } from "next-auth/react"; import { Dispatch, SetStateAction, useState } from "react"; import toast from "react-hot-toast"; @@ -88,6 +87,7 @@ export const DeleteAccountModal = ({
  • {t("environments.settings.profile.warning_cannot_undo")}
  • { e.preventDefault(); await deleteAccount(); @@ -98,6 +98,7 @@ export const DeleteAccountModal = ({ })} ({ + TiredFace: (props: any) => ( + + TiredFace + + ), + WearyFace: (props: any) => ( + + WearyFace + + ), + PerseveringFace: (props: any) => ( + + PerseveringFace + + ), + FrowningFace: (props: any) => ( + + FrowningFace + + ), + ConfusedFace: (props: any) => ( + + ConfusedFace + + ), + NeutralFace: (props: any) => ( + + NeutralFace + + ), + SlightlySmilingFace: (props: any) => ( + + SlightlySmilingFace + + ), + SmilingFaceWithSmilingEyes: (props: any) => ( + + SmilingFaceWithSmilingEyes + + ), + GrinningFaceWithSmilingEyes: (props: any) => ( + + GrinningFaceWithSmilingEyes + + ), + GrinningSquintingFace: (props: any) => ( + + GrinningSquintingFace + + ), +})); + +describe("RatingSmiley", () => { + afterEach(() => { + cleanup(); + }); + + const activeClass = "fill-rating-fill"; + + // Test branch: range === 10 => iconsIdx = [0,1,2,...,9] + test("renders correct icon for range 10 when active", () => { + // For idx 0, iconsIdx[0] === 0, which corresponds to TiredFace. + const { getByTestId } = render(); + const icon = getByTestId("TiredFace"); + expect(icon).toBeDefined(); + expect(icon.className).toContain(activeClass); + }); + + test("renders correct icon for range 10 when inactive", () => { + const { getByTestId } = render(); + const icon = getByTestId("TiredFace"); + expect(icon).toBeDefined(); + expect(icon.className).toContain("fill-none"); + }); + + // Test branch: range === 7 => iconsIdx = [1,3,4,5,6,8,9] + test("renders correct icon for range 7 when active", () => { + // For idx 0, iconsIdx[0] === 1, which corresponds to WearyFace. + const { getByTestId } = render(); + const icon = getByTestId("WearyFace"); + expect(icon).toBeDefined(); + expect(icon.className).toContain(activeClass); + }); + + // Test branch: range === 5 => iconsIdx = [3,4,5,6,7] + test("renders correct icon for range 5 when active", () => { + // For idx 0, iconsIdx[0] === 3, which corresponds to FrowningFace. + const { getByTestId } = render(); + const icon = getByTestId("FrowningFace"); + expect(icon).toBeDefined(); + expect(icon.className).toContain(activeClass); + }); + + // Test branch: range === 4 => iconsIdx = [4,5,6,7] + test("renders correct icon for range 4 when active", () => { + // For idx 0, iconsIdx[0] === 4, corresponding to ConfusedFace. + const { getByTestId } = render(); + const icon = getByTestId("ConfusedFace"); + expect(icon).toBeDefined(); + expect(icon.className).toContain(activeClass); + }); + + // Test branch: range === 3 => iconsIdx = [4,5,7] + test("renders correct icon for range 3 when active", () => { + // For idx 0, iconsIdx[0] === 4, corresponding to ConfusedFace. + const { getByTestId } = render(); + const icon = getByTestId("ConfusedFace"); + expect(icon).toBeDefined(); + expect(icon.className).toContain(activeClass); + }); +}); diff --git a/apps/web/modules/analysis/components/RatingSmiley/index.tsx b/apps/web/modules/analysis/components/RatingSmiley/index.tsx index b91207866f..90c0c2cf53 100644 --- a/apps/web/modules/analysis/components/RatingSmiley/index.tsx +++ b/apps/web/modules/analysis/components/RatingSmiley/index.tsx @@ -40,16 +40,28 @@ const getSmiley = (iconIdx: number, idx: number, range: number, active: boolean, const inactiveColor = addColors ? getSmileyColor(range, idx) : "fill-none"; const icons = [ - , - , - , - , - , - , - , - , - , - , + , + , + , + , + , + , + , + , + , + , ]; return icons[iconIdx]; diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx new file mode 100644 index 0000000000..dec906f64b --- /dev/null +++ b/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.test.tsx @@ -0,0 +1,94 @@ +import { getEnabledLanguages } from "@/lib/i18n/utils"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils"; +import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types"; +import { LanguageDropdown } from "./LanguageDropdown"; + +vi.mock("@/lib/i18n/utils", () => ({ + getEnabledLanguages: vi.fn(), +})); + +vi.mock("@formbricks/i18n-utils/src/utils", () => ({ + getLanguageLabel: vi.fn(), +})); + +describe("LanguageDropdown", () => { + const dummySurveyMultiple = { + languages: [ + { language: { code: "en" } } as TSurveyLanguage, + { language: { code: "fr" } } as TSurveyLanguage, + ], + } as TSurvey; + const dummySurveySingle = { + languages: [{ language: { code: "en" } }], + } as TSurvey; + const dummyLocale = "en-US"; + const setLanguageMock = vi.fn(); + + afterEach(() => { + cleanup(); + }); + + test("renders nothing when enabledLanguages length is 1", () => { + vi.mocked(getEnabledLanguages).mockReturnValueOnce([{ language: { code: "en" } } as TSurveyLanguage]); + render( + + ); + // Since enabledLanguages.length === 1, component should render null. + expect(screen.queryByRole("button")).toBeNull(); + }); + + test("renders button and toggles dropdown when multiple languages exist", async () => { + vi.mocked(getEnabledLanguages).mockReturnValue(dummySurveyMultiple.languages); + vi.mocked(getLanguageLabel).mockImplementation((code: string, _locale: string) => code.toUpperCase()); + + render( + + ); + + const button = screen.getByRole("button", { name: "Select Language" }); + expect(button).toBeDefined(); + + await userEvent.click(button); + // Wait for the dropdown options to appear. They are wrapped in a div with no specific role, + // so we query for texts (our mock labels) instead. + const optionEn = await screen.findByText("EN"); + const optionFr = await screen.findByText("FR"); + + expect(optionEn).toBeDefined(); + expect(optionFr).toBeDefined(); + + await userEvent.click(optionFr); + expect(setLanguageMock).toHaveBeenCalledWith("fr"); + + // After clicking, dropdown should no longer be visible. + await waitFor(() => { + expect(screen.queryByText("EN")).toBeNull(); + expect(screen.queryByText("FR")).toBeNull(); + }); + }); + + test("closes dropdown when clicking outside", async () => { + vi.mocked(getEnabledLanguages).mockReturnValue(dummySurveyMultiple.languages); + vi.mocked(getLanguageLabel).mockImplementation((code: string, _locale: string) => code); + + render( + + ); + const button = screen.getByRole("button", { name: "Select Language" }); + await userEvent.click(button); + + // Confirm dropdown shown + expect(await screen.findByText("en")).toBeDefined(); + + // Simulate clicking outside by dispatching a click event on the container's parent. + await userEvent.click(document.body); + + // Wait for dropdown to close + await waitFor(() => { + expect(screen.queryByText("en")).toBeNull(); + }); + }); +}); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.tsx index ab291129a6..44d79cee52 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/components/LanguageDropdown.tsx @@ -1,9 +1,9 @@ +import { getEnabledLanguages } from "@/lib/i18n/utils"; +import { useClickOutside } from "@/lib/utils/hooks/useClickOutside"; import { Button } from "@/modules/ui/components/button"; import { Languages } from "lucide-react"; import { useRef, useState } from "react"; -import { getEnabledLanguages } from "@formbricks/lib/i18n/utils"; -import { getLanguageLabel } from "@formbricks/lib/i18n/utils"; -import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside"; +import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; @@ -28,7 +28,7 @@ export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdo className="absolute top-12 z-30 w-fit rounded-lg border bg-slate-900 p-1 text-sm text-white" ref={languageDropdownRef}> {enabledLanguages.map((surveyLanguage) => ( -
    { @@ -36,7 +36,7 @@ export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdo setShowLanguageSelect(false); }}> {getLanguageLabel(surveyLanguage.language.code, locale)} -
    + ))}
    )} diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx new file mode 100644 index 0000000000..ea6ffc749d --- /dev/null +++ b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.test.tsx @@ -0,0 +1,22 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test } from "vitest"; +import { SurveyLinkDisplay } from "./SurveyLinkDisplay"; + +describe("SurveyLinkDisplay", () => { + afterEach(() => { + cleanup(); + }); + + test("renders the Input when surveyUrl is provided", () => { + const surveyUrl = "http://example.com/s/123"; + render(); + const input = screen.getByTestId("survey-url-input"); + expect(input).toBeInTheDocument(); + }); + + test("renders loading state when surveyUrl is empty", () => { + render(); + const loadingDiv = screen.getByTestId("loading-div"); + expect(loadingDiv).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx index 61e0e5bf05..eb99af8299 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx @@ -9,13 +9,16 @@ export const SurveyLinkDisplay = ({ surveyUrl }: SurveyLinkDisplayProps) => { <> {surveyUrl ? ( ) : ( //loading state -
    +
    )} ); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx new file mode 100644 index 0000000000..7c74b897cc --- /dev/null +++ b/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx @@ -0,0 +1,247 @@ +import { useSurveyQRCode } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { copySurveyLink } from "@/modules/survey/lib/client-utils"; +import { generateSingleUseIdAction } from "@/modules/survey/list/actions"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { toast } from "react-hot-toast"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { ShareSurveyLink } from "./index"; + +const dummySurvey = { + id: "survey123", + singleUse: { enabled: true, isEncrypted: false }, + type: "link", + status: "completed", +} as any; +const dummySurveyDomain = "http://dummy.com"; +const dummyLocale = "en-US"; + +vi.mock("@/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + POSTHOG_API_KEY: "mock-posthog-api-key", + POSTHOG_HOST: "mock-posthog-host", + IS_POSTHOG_CONFIGURED: true, + ENCRYPTION_KEY: "mock-encryption-key", + ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key", + GITHUB_ID: "mock-github-id", + GITHUB_SECRET: "test-githubID", + GOOGLE_CLIENT_ID: "test-google-client-id", + GOOGLE_CLIENT_SECRET: "test-google-client-secret", + AZUREAD_CLIENT_ID: "test-azuread-client-id", + AZUREAD_CLIENT_SECRET: "test-azure", + AZUREAD_TENANT_ID: "test-azuread-tenant-id", + OIDC_DISPLAY_NAME: "test-oidc-display-name", + OIDC_CLIENT_ID: "test-oidc-client-id", + OIDC_ISSUER: "test-oidc-issuer", + OIDC_CLIENT_SECRET: "test-oidc-client-secret", + OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", + WEBAPP_URL: "test-webapp-url", + IS_PRODUCTION: false, + AI_AZURE_LLM_RESSOURCE_NAME: "mock-ai-azure-llm-ressource-name", + AI_AZURE_LLM_API_KEY: "mock-ai", + AI_AZURE_LLM_DEPLOYMENT_ID: "mock-ai-azure-llm-deployment-id", + AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-ai-azure-embeddings-ressource-name", + AI_AZURE_EMBEDDINGS_API_KEY: "mock-ai-azure-embeddings-api-key", + AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-ai-azure-embeddings-deployment-id", +})); + +vi.mock("@/modules/survey/list/actions", () => ({ + generateSingleUseIdAction: vi.fn(), +})); + +vi.mock("@/modules/survey/lib/client-utils", () => ({ + copySurveyLink: vi.fn(), +})); + +vi.mock( + "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code", + () => ({ + useSurveyQRCode: vi.fn(() => ({ + downloadQRCode: vi.fn(), + })), + }) +); + +vi.mock("@/lib/utils/helper", () => ({ + getFormattedErrorMessage: vi.fn((error: any) => error.message), +})); + +vi.mock("./components/LanguageDropdown", () => { + const React = require("react"); + return { + LanguageDropdown: (props: { setLanguage: (lang: string) => void }) => { + // Call setLanguage("fr-FR") when the component mounts to simulate a language change. + React.useEffect(() => { + props.setLanguage("fr-FR"); + }, [props.setLanguage]); + return
    Mocked LanguageDropdown
    ; + }, + }; +}); + +describe("ShareSurveyLink", () => { + beforeEach(() => { + Object.assign(navigator, { + clipboard: { writeText: vi.fn().mockResolvedValue(undefined) }, + }); + window.open = vi.fn(); + }); + + afterEach(() => { + cleanup(); + }); + + test("calls getUrl on mount and sets surveyUrl accordingly with singleUse enabled and default language", async () => { + // Inline mocks for this test + vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" }); + + const setSurveyUrl = vi.fn(); + render( + + ); + await waitFor(() => { + expect(setSurveyUrl).toHaveBeenCalled(); + }); + const url = setSurveyUrl.mock.calls[0][0]; + expect(url).toContain(`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`); + expect(url).not.toContain("lang="); + }); + + test("appends language query when language is changed from default", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" }); + + const setSurveyUrl = vi.fn(); + const DummyWrapper = () => ( + + ); + render(); + await waitFor(() => { + const generatedUrl = setSurveyUrl.mock.calls[1][0]; + expect(generatedUrl).toContain("lang=fr-FR"); + }); + }); + + test("preview button opens new window with preview query", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" }); + + const setSurveyUrl = vi.fn().mockReturnValue(`${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`); + render( + + ); + const previewButton = await screen.findByRole("button", { + name: /environments.surveys.preview_survey_in_a_new_tab/i, + }); + fireEvent.click(previewButton); + await waitFor(() => { + expect(window.open).toHaveBeenCalled(); + const previewUrl = vi.mocked(window.open).mock.calls[0][0]; + expect(previewUrl).toMatch(/\?suId=dummySuId(&|\\?)preview=true/); + }); + }); + + test("copy button writes surveyUrl to clipboard and shows toast", async () => { + vi.mocked(getFormattedErrorMessage).mockReturnValue("common.copied_to_clipboard"); + vi.mocked(copySurveyLink).mockImplementation((url: string, newId: string) => `${url}?suId=${newId}`); + + const setSurveyUrl = vi.fn(); + const surveyUrl = `${dummySurveyDomain}/s/${dummySurvey.id}?suId=dummySuId`; + render( + + ); + const copyButton = await screen.findByRole("button", { + name: /environments.surveys.copy_survey_link_to_clipboard/i, + }); + fireEvent.click(copyButton); + await waitFor(() => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(surveyUrl); + expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard"); + }); + }); + + test("download QR code button calls downloadQRCode", async () => { + const dummyDownloadQRCode = vi.fn(); + vi.mocked(getFormattedErrorMessage).mockReturnValue("common.copied_to_clipboard"); + vi.mocked(useSurveyQRCode).mockReturnValue({ downloadQRCode: dummyDownloadQRCode } as any); + + const setSurveyUrl = vi.fn(); + render( + + ); + const downloadButton = await screen.findByRole("button", { + name: /environments.surveys.summary.download_qr_code/i, + }); + fireEvent.click(downloadButton); + expect(dummyDownloadQRCode).toHaveBeenCalled(); + }); + + test("renders regenerate button when survey.singleUse.enabled is true and calls generateNewSingleUseLink", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: "dummySuId" }); + + const setSurveyUrl = vi.fn(); + render( + + ); + const regenButton = await screen.findByRole("button", { name: /Regenerate single use survey link/i }); + fireEvent.click(regenButton); + await waitFor(() => { + expect(generateSingleUseIdAction).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith("environments.surveys.new_single_use_link_generated"); + }); + }); + + test("handles error when generating single-use link fails", async () => { + vi.mocked(generateSingleUseIdAction).mockResolvedValue({ data: undefined }); + vi.mocked(getFormattedErrorMessage).mockReturnValue("Failed to generate link"); + + const setSurveyUrl = vi.fn(); + render( + + ); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Failed to generate link"); + }); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/actions.test.ts b/apps/web/modules/analysis/components/SingleResponseCard/actions.test.ts new file mode 100644 index 0000000000..f6edcf92bc --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/actions.test.ts @@ -0,0 +1,202 @@ +import { deleteResponse, getResponse } from "@/lib/response/service"; +import { createResponseNote, resolveResponseNote, updateResponseNote } from "@/lib/responseNote/service"; +import { createTag } from "@/lib/tag/service"; +import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service"; +import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; +import { + getEnvironmentIdFromResponseId, + getOrganizationIdFromEnvironmentId, + getOrganizationIdFromResponseId, + getOrganizationIdFromResponseNoteId, + getProjectIdFromEnvironmentId, + getProjectIdFromResponseId, + getProjectIdFromResponseNoteId, +} from "@/lib/utils/helper"; +import { getTag } from "@/lib/utils/services"; +import { describe, expect, test, vi } from "vitest"; +import { + createResponseNoteAction, + createTagAction, + createTagToResponseAction, + deleteResponseAction, + deleteTagOnResponseAction, + getResponseAction, + resolveResponseNoteAction, + updateResponseNoteAction, +} from "./actions"; + +// Dummy inputs and context +const dummyCtx = { user: { id: "user1" } }; +const dummyTagInput = { environmentId: "env1", tagName: "tag1" }; +const dummyTagToResponseInput = { responseId: "resp1", tagId: "tag1" }; +const dummyResponseIdInput = { responseId: "resp1" }; +const dummyResponseNoteInput = { responseNoteId: "note1", text: "Updated note" }; +const dummyCreateNoteInput = { responseId: "resp1", text: "New note" }; +const dummyGetResponseInput = { responseId: "resp1" }; + +// Mocks for external dependencies +vi.mock("@/lib/utils/action-client-middleware", () => ({ + checkAuthorizationUpdated: vi.fn(), +})); +vi.mock("@/lib/utils/helper", () => ({ + getOrganizationIdFromEnvironmentId: vi.fn(), + getProjectIdFromEnvironmentId: vi.fn().mockResolvedValue("proj-env"), + getOrganizationIdFromResponseId: vi.fn().mockResolvedValue("org-resp"), + getOrganizationIdFromResponseNoteId: vi.fn().mockResolvedValue("org-resp-note"), + getProjectIdFromResponseId: vi.fn().mockResolvedValue("proj-resp"), + getProjectIdFromResponseNoteId: vi.fn().mockResolvedValue("proj-resp-note"), + getEnvironmentIdFromResponseId: vi.fn(), +})); +vi.mock("@/lib/utils/services", () => ({ + getTag: vi.fn(), +})); +vi.mock("@/lib/response/service", () => ({ + deleteResponse: vi.fn().mockResolvedValue("deletedResponse"), + getResponse: vi.fn().mockResolvedValue({ data: "responseData" }), +})); +vi.mock("@/lib/responseNote/service", () => ({ + createResponseNote: vi.fn().mockResolvedValue("createdNote"), + updateResponseNote: vi.fn().mockResolvedValue("updatedNote"), + resolveResponseNote: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("@/lib/tag/service", () => ({ + createTag: vi.fn().mockResolvedValue("createdTag"), +})); +vi.mock("@/lib/tagOnResponse/service", () => ({ + addTagToRespone: vi.fn().mockResolvedValue("tagAdded"), + deleteTagOnResponse: vi.fn().mockResolvedValue("tagDeleted"), +})); + +vi.mock("@/lib/utils/action-client", () => ({ + authenticatedActionClient: { + schema: () => ({ + action: (fn: any) => async (input: any) => { + const { user, ...rest } = input; + return fn({ + parsedInput: rest, + ctx: { user }, + }); + }, + }), + }, +})); + +describe("createTagAction", () => { + test("successfully creates a tag", async () => { + vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true); + vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValueOnce("org1"); + await createTagAction({ ...dummyTagInput, ...dummyCtx }); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(getOrganizationIdFromEnvironmentId).toHaveBeenCalledWith(dummyTagInput.environmentId); + expect(getProjectIdFromEnvironmentId).toHaveBeenCalledWith(dummyTagInput.environmentId); + expect(createTag).toHaveBeenCalledWith(dummyTagInput.environmentId, dummyTagInput.tagName); + }); +}); + +describe("createTagToResponseAction", () => { + test("adds tag to response when environments match", async () => { + vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1"); + vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "env1" }); + await createTagToResponseAction({ ...dummyTagToResponseInput, ...dummyCtx }); + expect(getEnvironmentIdFromResponseId).toHaveBeenCalledWith(dummyTagToResponseInput.responseId); + expect(getTag).toHaveBeenCalledWith(dummyTagToResponseInput.tagId); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(addTagToRespone).toHaveBeenCalledWith( + dummyTagToResponseInput.responseId, + dummyTagToResponseInput.tagId + ); + }); + + test("throws error when environments do not match", async () => { + vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1"); + vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "differentEnv" }); + await expect(createTagToResponseAction({ ...dummyTagToResponseInput, ...dummyCtx })).rejects.toThrow( + "Response and tag are not in the same environment" + ); + }); +}); + +describe("deleteTagOnResponseAction", () => { + test("deletes tag on response when environments match", async () => { + vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1"); + vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "env1" }); + await deleteTagOnResponseAction({ ...dummyTagToResponseInput, ...dummyCtx }); + expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyTagToResponseInput.responseId); + expect(getTag).toHaveBeenCalledWith(dummyTagToResponseInput.tagId); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(deleteTagOnResponse).toHaveBeenCalledWith( + dummyTagToResponseInput.responseId, + dummyTagToResponseInput.tagId + ); + }); + + test("throws error when environments do not match", async () => { + vi.mocked(getEnvironmentIdFromResponseId).mockResolvedValueOnce("env1"); + vi.mocked(getTag).mockResolvedValueOnce({ environmentId: "differentEnv" }); + await expect(deleteTagOnResponseAction({ ...dummyTagToResponseInput, ...dummyCtx })).rejects.toThrow( + "Response and tag are not in the same environment" + ); + }); +}); + +describe("deleteResponseAction", () => { + test("deletes response successfully", async () => { + vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true); + await deleteResponseAction({ ...dummyResponseIdInput, ...dummyCtx }); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyResponseIdInput.responseId); + expect(getProjectIdFromResponseId).toHaveBeenCalledWith(dummyResponseIdInput.responseId); + expect(deleteResponse).toHaveBeenCalledWith(dummyResponseIdInput.responseId); + }); +}); + +describe("updateResponseNoteAction", () => { + test("updates response note successfully", async () => { + vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true); + await updateResponseNoteAction({ ...dummyResponseNoteInput, ...dummyCtx }); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(getOrganizationIdFromResponseNoteId).toHaveBeenCalledWith(dummyResponseNoteInput.responseNoteId); + expect(getProjectIdFromResponseNoteId).toHaveBeenCalledWith(dummyResponseNoteInput.responseNoteId); + expect(updateResponseNote).toHaveBeenCalledWith( + dummyResponseNoteInput.responseNoteId, + dummyResponseNoteInput.text + ); + }); +}); + +describe("resolveResponseNoteAction", () => { + test("resolves response note successfully", async () => { + vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true); + await resolveResponseNoteAction({ responseNoteId: "note1", ...dummyCtx }); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(getOrganizationIdFromResponseNoteId).toHaveBeenCalledWith("note1"); + expect(getProjectIdFromResponseNoteId).toHaveBeenCalledWith("note1"); + expect(resolveResponseNote).toHaveBeenCalledWith("note1"); + }); +}); + +describe("createResponseNoteAction", () => { + test("creates a response note successfully", async () => { + vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true); + await createResponseNoteAction({ ...dummyCreateNoteInput, ...dummyCtx }); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyCreateNoteInput.responseId); + expect(getProjectIdFromResponseId).toHaveBeenCalledWith(dummyCreateNoteInput.responseId); + expect(createResponseNote).toHaveBeenCalledWith( + dummyCreateNoteInput.responseId, + dummyCtx.user.id, + dummyCreateNoteInput.text + ); + }); +}); + +describe("getResponseAction", () => { + test("retrieves response successfully", async () => { + vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(true); + await getResponseAction({ ...dummyGetResponseInput, ...dummyCtx }); + expect(checkAuthorizationUpdated).toHaveBeenCalled(); + expect(getOrganizationIdFromResponseId).toHaveBeenCalledWith(dummyGetResponseInput.responseId); + expect(getProjectIdFromResponseId).toHaveBeenCalledWith(dummyGetResponseInput.responseId); + expect(getResponse).toHaveBeenCalledWith(dummyGetResponseInput.responseId); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/actions.ts b/apps/web/modules/analysis/components/SingleResponseCard/actions.ts index 8e953980e1..9dd25d7c00 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/actions.ts +++ b/apps/web/modules/analysis/components/SingleResponseCard/actions.ts @@ -1,5 +1,9 @@ "use server"; +import { deleteResponse, getResponse } from "@/lib/response/service"; +import { createResponseNote, resolveResponseNote, updateResponseNote } from "@/lib/responseNote/service"; +import { createTag } from "@/lib/tag/service"; +import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { @@ -13,14 +17,6 @@ import { } from "@/lib/utils/helper"; import { getTag } from "@/lib/utils/services"; import { z } from "zod"; -import { deleteResponse, getResponse } from "@formbricks/lib/response/service"; -import { - createResponseNote, - resolveResponseNote, - updateResponseNote, -} from "@formbricks/lib/responseNote/service"; -import { createTag } from "@formbricks/lib/tag/service"; -import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service"; import { ZId } from "@formbricks/types/common"; const ZCreateTagAction = z.object({ diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx new file mode 100644 index 0000000000..b509bd91de --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.test.tsx @@ -0,0 +1,70 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TSurveyHiddenFields } from "@formbricks/types/surveys/types"; +import { HiddenFields } from "./HiddenFields"; + +// Mock tooltip components to always render their children +vi.mock("@/modules/ui/components/tooltip", () => ({ + Tooltip: ({ children }: { children: React.ReactNode }) =>
    {children}
    , + TooltipContent: ({ children }: { children: React.ReactNode }) =>
    {children}
    , + TooltipProvider: ({ children }: { children: React.ReactNode }) =>
    {children}
    , + TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
    {children}
    , +})); + +describe("HiddenFields", () => { + afterEach(() => { + cleanup(); + }); + + test("renders empty container when no fieldIds are provided", () => { + render( + + ); + const container = screen.getByTestId("main-hidden-fields-div"); + expect(container).toBeDefined(); + }); + + test("renders nothing for fieldIds with no corresponding response data", () => { + render( + + ); + expect(screen.queryByText("field1")).toBeNull(); + }); + + test("renders field and value when responseData exists and is a string", async () => { + render( + + ); + expect(screen.getByText("field1")).toBeInTheDocument(); + expect(screen.getByText("Value 1")).toBeInTheDocument(); + expect(screen.queryByText("field2")).toBeNull(); + }); + + test("renders empty text when responseData value is not a string", () => { + render( + + ); + expect(screen.getByText("field1")).toBeInTheDocument(); + const valueParagraphs = screen.getAllByText("", { selector: "p" }); + expect(valueParagraphs.length).toBeGreaterThan(0); + }); + + test("displays tooltip content for hidden field", async () => { + render( + + ); + expect(screen.getByText("common.hidden_field")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.tsx index 0cd17fbf98..06e9af31be 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/HiddenFields.tsx @@ -15,7 +15,7 @@ export const HiddenFields = ({ hiddenFields, responseData }: HiddenFieldsProps) const { t } = useTranslate(); const fieldIds = hiddenFields.fieldIds ?? []; return ( -
    +
    {fieldIds.map((field) => { if (!responseData[field]) return; return ( diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx new file mode 100644 index 0000000000..0257a368c0 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.test.tsx @@ -0,0 +1,98 @@ +import { parseRecallInfo } from "@/lib/utils/recall"; +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TSurveyQuestion } from "@formbricks/types/surveys/types"; +import { QuestionSkip } from "./QuestionSkip"; + +vi.mock("@/modules/ui/components/tooltip", () => ({ + Tooltip: ({ children }: any) =>
    {children}
    , + TooltipContent: ({ children }: any) =>
    {children}
    , + TooltipProvider: ({ children }: any) =>
    {children}
    , + TooltipTrigger: ({ children }: any) =>
    {children}
    , +})); + +vi.mock("@/modules/i18n/utils", () => ({ + getLocalizedValue: vi.fn((value, _) => value), +})); + +// Mock recall utils +vi.mock("@/lib/utils/recall", () => ({ + parseRecallInfo: vi.fn((headline, _) => { + return `parsed: ${headline}`; + }), +})); + +const dummyQuestions = [ + { id: "f1", headline: "headline1" }, + { id: "f2", headline: "headline2" }, +] as unknown as TSurveyQuestion[]; + +const dummyResponseData = { f1: "Answer 1", f2: "Answer 2" }; + +describe("QuestionSkip", () => { + afterEach(() => { + cleanup(); + }); + + test("renders nothing when skippedQuestions is falsy", () => { + render( + + ); + expect(screen.queryByText("headline1")).toBeNull(); + expect(screen.queryByText("headline2")).toBeNull(); + }); + + test("renders welcomeCard branch", () => { + render( + + ); + expect(screen.getByText("common.welcome_card")).toBeInTheDocument(); + }); + + test("renders skipped branch with tooltip and parsed headlines", () => { + vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline1"); + vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline2"); + + render( + + ); + // Check tooltip text from TooltipContent + expect(screen.getByTestId("tooltip-respondent_skipped_questions")).toBeInTheDocument(); + // Check mapping: parseRecallInfo should be called on each headline value, so expect the parsed text to appear. + expect(screen.getByText("parsed: headline1")).toBeInTheDocument(); + expect(screen.getByText("parsed: headline2")).toBeInTheDocument(); + }); + + test("renders aborted branch with closed message and parsed headlines", () => { + vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline1"); + vi.mocked(parseRecallInfo).mockReturnValueOnce("parsed: headline2"); + + render( + + ); + expect(screen.getByTestId("tooltip-survey_closed")).toBeInTheDocument(); + expect(screen.getByText("parsed: headline1")).toBeInTheDocument(); + expect(screen.getByText("parsed: headline2")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.tsx index 764c0aaff2..ae0abe8aab 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/QuestionSkip.tsx @@ -1,10 +1,10 @@ "use client"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { parseRecallInfo } from "@/lib/utils/recall"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; import { useTranslate } from "@tolgee/react"; import { CheckCircle2Icon, ChevronsDownIcon, XCircleIcon } from "lucide-react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { parseRecallInfo } from "@formbricks/lib/utils/recall"; import { TResponseData } from "@formbricks/types/responses"; import { TSurveyQuestion } from "@formbricks/types/surveys/types"; @@ -39,7 +39,7 @@ export const QuestionSkip = ({ background: "repeating-linear-gradient(rgb(148, 163, 184), rgb(148, 163, 184) 5px, transparent 5px, transparent 8px)", // adjust the values to fit your design }}> - +
    }
    {t("common.welcome_card")}
    @@ -60,27 +60,28 @@ export const QuestionSkip = ({ -

    {t("environments.surveys.responses.respondent_skipped_questions")}

    +

    + {t("environments.surveys.responses.respondent_skipped_questions")} +

    )}
    - {skippedQuestions && - skippedQuestions.map((questionId) => { - return ( -

    - {parseRecallInfo( - getLocalizedValue( - questions.find((question) => question.id === questionId)!.headline, - "default" - ), - responseData - )} -

    - ); - })} + {skippedQuestions?.map((questionId) => { + return ( +

    + {parseRecallInfo( + getLocalizedValue( + questions.find((question) => question.id === questionId)!.headline, + "default" + ), + responseData + )} +

    + ); + })}
    )} @@ -97,7 +98,9 @@ export const QuestionSkip = ({
    -

    +

    {t("environments.surveys.responses.survey_closed")}

    {skippedQuestions && diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx new file mode 100644 index 0000000000..22d4b19996 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.test.tsx @@ -0,0 +1,277 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { RenderResponse } from "./RenderResponse"; + +// Mocks for dependencies +vi.mock("@/modules/ui/components/rating-response", () => ({ + RatingResponse: ({ answer }: any) =>
    Rating: {answer}
    , +})); +vi.mock("@/modules/ui/components/file-upload-response", () => ({ + FileUploadResponse: ({ selected }: any) => ( +
    FileUpload: {selected.join(",")}
    + ), +})); +vi.mock("@/modules/ui/components/picture-selection-response", () => ({ + PictureSelectionResponse: ({ selected, isExpanded }: any) => ( +
    + PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"}) +
    + ), +})); +vi.mock("@/modules/ui/components/array-response", () => ({ + ArrayResponse: ({ value }: any) =>
    {value.join(",")}
    , +})); +vi.mock("@/modules/ui/components/response-badges", () => ({ + ResponseBadges: ({ items }: any) =>
    {items.join(",")}
    , +})); +vi.mock("@/modules/ui/components/ranking-response", () => ({ + RankingRespone: ({ value }: any) =>
    {value.join(",")}
    , +})); +vi.mock("@/modules/analysis/utils", () => ({ + renderHyperlinkedContent: vi.fn((text: string) => "hyper:" + text), +})); +vi.mock("@/lib/responses", () => ({ + processResponseData: (val: any) => "processed:" + val, +})); +vi.mock("@/lib/utils/datetime", () => ({ + formatDateWithOrdinal: (d: Date) => "formatted_" + d.toISOString(), +})); +vi.mock("@/lib/cn", () => ({ + cn: (...classes: (string | boolean | undefined)[]) => classes.filter(Boolean).join(" "), +})); +vi.mock("@/lib/i18n/utils", () => ({ + getLocalizedValue: vi.fn((val, _) => val), + getLanguageCode: vi.fn().mockReturnValue("default"), +})); + +describe("RenderResponse", () => { + afterEach(() => { + cleanup(); + }); + + const defaultSurvey = { languages: [] } as any; + const defaultQuestion = { id: "q1", type: "Unknown" } as any; + const dummyLanguage = "default"; + + test("returns '-' for empty responseData (string)", () => { + const { container } = render( + + ); + expect(container.textContent).toBe("-"); + }); + + test("returns '-' for empty responseData (array)", () => { + const { container } = render( + + ); + expect(container.textContent).toBe("-"); + }); + + test("returns '-' for empty responseData (object)", () => { + const { container } = render( + + ); + expect(container.textContent).toBe("-"); + }); + + test("renders RatingResponse for 'Rating' question with number", () => { + const question = { ...defaultQuestion, type: "rating", scale: 5, range: [1, 5] }; + render( + + ); + expect(screen.getByTestId("RatingResponse")).toHaveTextContent("Rating: 4"); + }); + + test("renders formatted date for 'Date' question", () => { + const question = { ...defaultQuestion, type: "date" }; + const dateStr = new Date("2023-01-01T12:00:00Z").toISOString(); + render( + + ); + expect(screen.getByText(/formatted_/)).toBeInTheDocument(); + }); + + test("renders PictureSelectionResponse for 'PictureSelection' question", () => { + const question = { ...defaultQuestion, type: "pictureSelection", choices: ["a", "b"] }; + render( + + ); + expect(screen.getByTestId("PictureSelectionResponse")).toHaveTextContent( + "PictureSelection: choice1,choice2" + ); + }); + + test("renders FileUploadResponse for 'FileUpload' question", () => { + const question = { ...defaultQuestion, type: "fileUpload" }; + render( + + ); + expect(screen.getByTestId("FileUploadResponse")).toHaveTextContent("FileUpload: file1,file2"); + }); + + test("renders Matrix response", () => { + const question = { id: "q1", type: "matrix", rows: ["row1", "row2"] } as any; + // getLocalizedValue returns the row value itself + const responseData = { row1: "answer1", row2: "answer2" }; + render( + + ); + expect(screen.getByText("row1:processed:answer1")).toBeInTheDocument(); + expect(screen.getByText("row2:processed:answer2")).toBeInTheDocument(); + }); + + test("renders ArrayResponse for 'Address' question", () => { + const question = { ...defaultQuestion, type: "address" }; + render( + + ); + expect(screen.getByTestId("ArrayResponse")).toHaveTextContent("addr1,addr2"); + }); + + test("renders ResponseBadges for 'Cal' question (string)", () => { + const question = { ...defaultQuestion, type: "cal" }; + render( + + ); + expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Value"); + }); + + test("renders ResponseBadges for 'Consent' question (number)", () => { + const question = { ...defaultQuestion, type: "consent" }; + render( + + ); + expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("5"); + }); + + test("renders ResponseBadges for 'CTA' question (string)", () => { + const question = { ...defaultQuestion, type: "cta" }; + render( + + ); + expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Click"); + }); + + test("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => { + const question = { ...defaultQuestion, type: "multipleChoiceSingle" }; + render( + + ); + expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("option1"); + }); + + test("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => { + const question = { ...defaultQuestion, type: "multipleChoiceMulti" }; + render( + + ); + expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1,opt2"); + }); + + test("renders ResponseBadges for 'NPS' question (number)", () => { + const question = { ...defaultQuestion, type: "nps" }; + render( + + ); + expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("9"); + }); + + test("renders RankingRespone for 'Ranking' question", () => { + const question = { ...defaultQuestion, type: "ranking" }; + render( + + ); + expect(screen.getByTestId("RankingRespone")).toHaveTextContent("first,second"); + }); + + test("renders default branch for unknown question type with string", () => { + const question = { ...defaultQuestion, type: "unknown" }; + render( + + ); + expect(screen.getByText("hyper:some text")).toBeInTheDocument(); + }); + + test("renders default branch for unknown question type with array", () => { + const question = { ...defaultQuestion, type: "unknown" }; + render( + + ); + expect(screen.getByText("a, b")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.tsx index c1bd3ebcd4..6b7b4ab8bf 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/RenderResponse.tsx @@ -1,3 +1,8 @@ +import { cn } from "@/lib/cn"; +import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils"; +import { processResponseData } from "@/lib/responses"; +import { formatDateWithOrdinal } from "@/lib/utils/datetime"; +import { capitalizeFirstLetter } from "@/lib/utils/strings"; import { renderHyperlinkedContent } from "@/modules/analysis/utils"; import { ArrayResponse } from "@/modules/ui/components/array-response"; import { FileUploadResponse } from "@/modules/ui/components/file-upload-response"; @@ -7,11 +12,6 @@ import { RatingResponse } from "@/modules/ui/components/rating-response"; import { ResponseBadges } from "@/modules/ui/components/response-badges"; import { CheckCheckIcon, MousePointerClickIcon, PhoneIcon } from "lucide-react"; import React from "react"; -import { cn } from "@formbricks/lib/cn"; -import { getLanguageCode, getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { processResponseData } from "@formbricks/lib/responses"; -import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime"; -import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings"; import { TSurvey, TSurveyMatrixQuestion, diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.test.tsx new file mode 100644 index 0000000000..e2f658ef6f --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.test.tsx @@ -0,0 +1,192 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TResponseNote } from "@formbricks/types/responses"; +import { TUser } from "@formbricks/types/user"; +import { createResponseNoteAction, resolveResponseNoteAction, updateResponseNoteAction } from "../actions"; +import { ResponseNotes } from "./ResponseNote"; + +const dummyUser = { id: "user1", name: "User One" } as TUser; +const dummyResponseId = "resp1"; +const dummyLocale = "en-US"; +const dummyNote = { + id: "note1", + text: "Initial note", + isResolved: true, + isEdited: false, + updatedAt: new Date(), + user: { id: "user1", name: "User One" }, +} as TResponseNote; +const dummyUnresolvedNote = { + id: "note1", + text: "Initial note", + isResolved: false, + isEdited: false, + updatedAt: new Date(), + user: { id: "user1", name: "User One" }, +} as TResponseNote; +const updateFetchedResponses = vi.fn(); +const setIsOpen = vi.fn(); + +vi.mock("../actions", () => ({ + createResponseNoteAction: vi.fn().mockResolvedValue("createdNote"), + updateResponseNoteAction: vi.fn().mockResolvedValue("updatedNote"), + resolveResponseNoteAction: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@/modules/ui/components/button", () => ({ + Button: (props: any) => , +})); + +// Mock icons for edit and resolve buttons with test ids +vi.mock("lucide-react", () => { + const actual = vi.importActual("lucide-react"); + return { + ...actual, + PencilIcon: (props: any) => ( + + ), + CheckIcon: (props: any) => ( + + ), + PlusIcon: (props: any) => ( + + Plus + + ), + Maximize2Icon: (props: any) => ( + + Maximize + + ), + Minimize2Icon: (props: any) => ( + + ), + }; +}); + +// Mock tooltip components +vi.mock("@/modules/ui/components/tooltip", () => ({ + Tooltip: ({ children }: any) =>
    {children}
    , + TooltipContent: ({ children }: any) =>
    {children}
    , + TooltipProvider: ({ children }: any) =>
    {children}
    , + TooltipTrigger: ({ children }: any) =>
    {children}
    , +})); + +describe("ResponseNotes", () => { + afterEach(() => { + cleanup(); + }); + + test("renders collapsed view when isOpen is false", () => { + render( + + ); + expect(screen.getByText(/note/i)).toBeInTheDocument(); + }); + + test("opens panel on click when collapsed", async () => { + render( + + ); + await userEvent.click(screen.getByText(/note/i)); + expect(setIsOpen).toHaveBeenCalledWith(true); + }); + + test("submits a new note", async () => { + vi.mocked(createResponseNoteAction).mockResolvedValueOnce("createdNote" as any); + render( + + ); + const textarea = screen.getByRole("textbox"); + await userEvent.type(textarea, "New note"); + await userEvent.type(textarea, "{enter}"); + await waitFor(() => { + expect(createResponseNoteAction).toHaveBeenCalledWith({ + responseId: dummyResponseId, + text: "New note", + }); + expect(updateFetchedResponses).toHaveBeenCalled(); + }); + }); + + test("edits an existing note", async () => { + vi.mocked(updateResponseNoteAction).mockResolvedValueOnce("updatedNote" as any); + render( + + ); + const pencilButton = screen.getByTestId("pencil-button"); + await userEvent.click(pencilButton); + const textarea = screen.getByRole("textbox"); + expect(textarea).toHaveValue("Initial note"); + await userEvent.clear(textarea); + await userEvent.type(textarea, "Updated note"); + await userEvent.type(textarea, "{enter}"); + await waitFor(() => { + expect(updateResponseNoteAction).toHaveBeenCalledWith({ + responseNoteId: dummyNote.id, + text: "Updated note", + }); + expect(updateFetchedResponses).toHaveBeenCalled(); + }); + }); + + test("resolves a note", async () => { + vi.mocked(resolveResponseNoteAction).mockResolvedValueOnce(undefined); + render( + + ); + const checkButton = screen.getByTestId("check-button"); + userEvent.click(checkButton); + await waitFor(() => { + expect(resolveResponseNoteAction).toHaveBeenCalledWith({ responseNoteId: dummyNote.id }); + expect(updateFetchedResponses).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.tsx index 5a0670b4ad..950a788c83 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseNote.tsx @@ -1,15 +1,14 @@ "use client"; +import { cn } from "@/lib/cn"; +import { timeSince } from "@/lib/time"; import { Button } from "@/modules/ui/components/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; import { useTranslate } from "@tolgee/react"; import clsx from "clsx"; -import { CheckIcon, PencilIcon, PlusIcon } from "lucide-react"; -import { Maximize2Icon, Minimize2Icon } from "lucide-react"; +import { CheckIcon, Maximize2Icon, Minimize2Icon, PencilIcon, PlusIcon } from "lucide-react"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; -import { cn } from "@formbricks/lib/cn"; -import { timeSince } from "@formbricks/lib/time"; import { TResponseNote } from "@formbricks/types/responses"; import { TUser, TUserLocale } from "@formbricks/types/user"; import { createResponseNoteAction, resolveResponseNoteAction, updateResponseNoteAction } from "../actions"; @@ -105,10 +104,10 @@ export const ResponseNotes = ({ !isOpen && unresolvedNotes.length && "group/hint cursor-pointer bg-white hover:-right-3", !isOpen && !unresolvedNotes.length && "cursor-pointer bg-slate-50", isOpen - ? "-right-2 top-0 h-5/6 max-h-[600px] w-1/4 bg-white" + ? "top-0 -right-2 h-5/6 max-h-[600px] w-1/4 bg-white" : unresolvedNotes.length - ? "right-0 top-[8.33%] h-5/6 max-h-[600px] w-1/12" - : "right-[120px] top-[8.333%] h-5/6 max-h-[600px] w-1/12 group-hover:right-[0]" + ? "top-[8.33%] right-0 h-5/6 max-h-[600px] w-1/12" + : "top-[8.333%] right-[120px] h-5/6 max-h-[600px] w-1/12 group-hover:right-[0]" )} onClick={() => { if (!isOpen) setIsOpen(true); @@ -117,7 +116,7 @@ export const ResponseNotes = ({
    {!unresolvedNotes.length ? ( @@ -128,7 +127,7 @@ export const ResponseNotes = ({
    ) : (
    - +
    )}
    @@ -142,7 +141,7 @@ export const ResponseNotes = ({
    ) : (
    -
    +

    {t("common.note")}

    @@ -228,9 +227,7 @@ export const ResponseNotes = ({ onKeyDown={(e) => { if (e.key === "Enter" && noteText) { e.preventDefault(); - { - isUpdatingNote ? handleNoteUpdate(e) : handleNoteSubmission(e); - } + isUpdatingNote ? handleNoteUpdate(e) : handleNoteSubmission(e); } }} required> diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx new file mode 100644 index 0000000000..7adcb6a531 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.test.tsx @@ -0,0 +1,245 @@ +import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import toast from "react-hot-toast"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TTag } from "@formbricks/types/tags"; +import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions"; +import { ResponseTagsWrapper } from "./ResponseTagsWrapper"; + +const dummyTags = [ + { tagId: "tag1", tagName: "Tag One" }, + { tagId: "tag2", tagName: "Tag Two" }, +]; +const dummyEnvironmentId = "env1"; +const dummyResponseId = "resp1"; +const dummyEnvironmentTags = [ + { id: "tag1", name: "Tag One" }, + { id: "tag2", name: "Tag Two" }, + { id: "tag3", name: "Tag Three" }, +] as TTag[]; +const dummyUpdateFetchedResponses = vi.fn(); +const dummyRouterPush = vi.fn(); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: dummyRouterPush, + }), +})); + +vi.mock("@/lib/utils/helper", () => ({ + getFormattedErrorMessage: vi.fn((res) => res.error?.details[0].issue || "error"), +})); + +vi.mock("../actions", () => ({ + createTagAction: vi.fn(), + createTagToResponseAction: vi.fn(), + deleteTagOnResponseAction: vi.fn(), +})); + +// Mock Button, Tag and TagsCombobox components +vi.mock("@/modules/ui/components/button", () => ({ + Button: (props: any) => , +})); +vi.mock("@/modules/ui/components/tag", () => ({ + Tag: (props: any) => ( +
    + {props.tagName} + {props.allowDelete && } +
    + ), +})); +vi.mock("@/modules/ui/components/tags-combobox", () => ({ + TagsCombobox: (props: any) => ( +
    + + +
    + ), +})); + +describe("ResponseTagsWrapper", () => { + afterEach(() => { + cleanup(); + }); + + test("renders settings button when not readOnly and navigates on click", async () => { + render( + + ); + const settingsButton = screen.getByRole("button", { name: "" }); + await userEvent.click(settingsButton); + expect(dummyRouterPush).toHaveBeenCalledWith(`/environments/${dummyEnvironmentId}/project/tags`); + }); + + test("does not render settings button when readOnly", () => { + render( + + ); + expect(screen.queryByRole("button")).toBeNull(); + }); + + test("renders provided tags", () => { + render( + + ); + expect(screen.getAllByTestId("tag").length).toBe(2); + expect(screen.getByText("Tag One")).toBeInTheDocument(); + expect(screen.getByText("Tag Two")).toBeInTheDocument(); + }); + + test("calls deleteTagOnResponseAction on tag delete success", async () => { + vi.mocked(deleteTagOnResponseAction).mockResolvedValueOnce({ data: "deleted" } as any); + render( + + ); + const deleteButtons = screen.getAllByText("Delete"); + await userEvent.click(deleteButtons[0]); + await waitFor(() => { + expect(deleteTagOnResponseAction).toHaveBeenCalledWith({ responseId: dummyResponseId, tagId: "tag1" }); + expect(dummyUpdateFetchedResponses).toHaveBeenCalled(); + }); + }); + + test("shows toast error on deleteTagOnResponseAction error", async () => { + vi.mocked(deleteTagOnResponseAction).mockRejectedValueOnce(new Error("delete error")); + render( + + ); + const deleteButtons = screen.getAllByText("Delete"); + await userEvent.click(deleteButtons[0]); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + "environments.surveys.responses.an_error_occurred_deleting_the_tag" + ); + }); + }); + + test("creates a new tag via TagsCombobox and calls updateFetchedResponses on success", async () => { + vi.mocked(createTagAction).mockResolvedValueOnce({ data: { id: "newTagId", name: "NewTag" } } as any); + vi.mocked(createTagToResponseAction).mockResolvedValueOnce({ data: "tagAdded" } as any); + render( + + ); + const createButton = screen.getByTestId("tags-combobox").querySelector("button"); + await userEvent.click(createButton!); + await waitFor(() => { + expect(createTagAction).toHaveBeenCalledWith({ environmentId: dummyEnvironmentId, tagName: "NewTag" }); + expect(createTagToResponseAction).toHaveBeenCalledWith({ + responseId: dummyResponseId, + tagId: "newTagId", + }); + expect(dummyUpdateFetchedResponses).toHaveBeenCalled(); + }); + }); + + test("handles createTagAction failure and shows toast error", async () => { + vi.mocked(createTagAction).mockResolvedValueOnce({ + error: { details: [{ issue: "Unique constraint failed on the fields" }] }, + } as any); + render( + + ); + const createButton = screen.getByTestId("tags-combobox").querySelector("button"); + await userEvent.click(createButton!); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("environments.surveys.responses.tag_already_exists", { + duration: 2000, + icon: expect.anything(), + }); + }); + }); + + test("calls addTag correctly via TagsCombobox", async () => { + vi.mocked(createTagToResponseAction).mockResolvedValueOnce({ data: "tagAdded" } as any); + render( + + ); + const addButton = screen.getByTestId("tags-combobox").querySelectorAll("button")[1]; + await userEvent.click(addButton); + await waitFor(() => { + expect(createTagToResponseAction).toHaveBeenCalledWith({ responseId: dummyResponseId, tagId: "tag3" }); + expect(dummyUpdateFetchedResponses).toHaveBeenCalled(); + }); + }); + + test("clears tagIdToHighlight after timeout", async () => { + vi.useFakeTimers(); + + render( + + ); + // We simulate that tagIdToHighlight is set (simulate via setState if possible) + // Here we directly invoke the effect by accessing component instance is not trivial in RTL; + // Instead, we manually advance timers to ensure cleanup timeout is executed. + + await act(async () => { + vi.advanceTimersByTime(2000); + }); + + // No error expected; test passes if timer runs without issue. + expect(true).toBe(true); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx index 700e0df57a..e08d14dde0 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx @@ -8,7 +8,7 @@ import { useTranslate } from "@tolgee/react"; import { AlertCircleIcon, SettingsIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import React, { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; +import toast from "react-hot-toast"; import { TTag } from "@formbricks/types/tags"; import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions"; diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx new file mode 100644 index 0000000000..94a7a36e2c --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseVariables.test.tsx @@ -0,0 +1,80 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TResponseVariables } from "@formbricks/types/responses"; +import { TSurveyVariables } from "@formbricks/types/surveys/types"; +import { ResponseVariables } from "./ResponseVariables"; + +const dummyVariables = [ + { id: "v1", name: "Variable One", type: "number" }, + { id: "v2", name: "Variable Two", type: "string" }, + { id: "v3", name: "Variable Three", type: "object" }, +] as unknown as TSurveyVariables; + +const dummyVariablesData = { + v1: 123, + v2: "abc", + v3: { not: "valid" }, +} as unknown as TResponseVariables; + +// Mock tooltip components +vi.mock("@/modules/ui/components/tooltip", () => ({ + Tooltip: ({ children }: any) =>
    {children}
    , + TooltipContent: ({ children }: any) =>
    {children}
    , + TooltipProvider: ({ children }: any) =>
    {children}
    , + TooltipTrigger: ({ children }: any) =>
    {children}
    , +})); + +// Mock useTranslate +vi.mock("@tolgee/react", () => ({ + useTranslate: () => ({ t: (key: string) => key }), +})); + +// Mock i18n utils +vi.mock("@/modules/i18n/utils", () => ({ + getLocalizedValue: vi.fn((val, _) => val), + getLanguageCode: vi.fn().mockReturnValue("default"), +})); + +// Mock lucide-react icons to render identifiable elements +vi.mock("lucide-react", () => ({ + FileDigitIcon: () =>
    , + FileType2Icon: () =>
    , +})); + +describe("ResponseVariables", () => { + afterEach(() => { + cleanup(); + }); + + test("renders nothing when no variable in variablesData meets type check", () => { + render( + + ); + expect(screen.queryByText("Variable One")).toBeNull(); + expect(screen.queryByText("Variable Two")).toBeNull(); + }); + + test("renders variables with valid response data", () => { + render(); + expect(screen.getByText("Variable One")).toBeInTheDocument(); + expect(screen.getByText("Variable Two")).toBeInTheDocument(); + // Check that the value is rendered + expect(screen.getByText("123")).toBeInTheDocument(); + expect(screen.getByText("abc")).toBeInTheDocument(); + }); + + test("renders FileDigitIcon for number type and FileType2Icon for string type", () => { + render(); + expect(screen.getByTestId("FileDigitIcon")).toBeInTheDocument(); + expect(screen.getByTestId("FileType2Icon")).toBeInTheDocument(); + }); + + test("displays tooltip content with 'common.variable'", () => { + render(); + // TooltipContent mock always renders its children directly. + expect(screen.getAllByText("common.variable")[0]).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx new file mode 100644 index 0000000000..866619c9ca --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.test.tsx @@ -0,0 +1,125 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TResponse } from "@formbricks/types/responses"; +import { TSurvey } from "@formbricks/types/surveys/types"; +import { SingleResponseCardBody } from "./SingleResponseCardBody"; + +// Mocks for imported components to return identifiable elements +vi.mock("./QuestionSkip", () => ({ + QuestionSkip: (props: any) =>
    {props.status}
    , +})); +vi.mock("./RenderResponse", () => ({ + RenderResponse: (props: any) =>
    {props.responseData.toString()}
    , +})); +vi.mock("./ResponseVariables", () => ({ + ResponseVariables: (props: any) =>
    Variables
    , +})); +vi.mock("./HiddenFields", () => ({ + HiddenFields: (props: any) =>
    Hidden
    , +})); +vi.mock("./VerifiedEmail", () => ({ + VerifiedEmail: (props: any) =>
    VerifiedEmail
    , +})); + +// Mocks for utility functions used inside component +vi.mock("@/lib/utils/recall", () => ({ + parseRecallInfo: vi.fn((headline, data) => "parsed:" + headline), +})); +vi.mock("@/lib/i18n/utils", () => ({ + getLocalizedValue: vi.fn((headline) => headline), +})); +vi.mock("../util", () => ({ + isValidValue: (val: any) => { + if (typeof val === "string") return val.trim() !== ""; + if (Array.isArray(val)) return val.length > 0; + if (typeof val === "number") return true; + if (typeof val === "object") return Object.keys(val).length > 0; + return false; + }, +})); +// Mock CheckCircle2Icon from lucide-react +vi.mock("lucide-react", () => ({ + CheckCircle2Icon: () =>
    CheckCircle
    , +})); + +describe("SingleResponseCardBody", () => { + afterEach(() => { + cleanup(); + }); + + const dummySurvey = { + welcomeCard: { enabled: true }, + isVerifyEmailEnabled: true, + questions: [ + { id: "q1", headline: "headline1" }, + { id: "q2", headline: "headline2" }, + ], + variables: [{ id: "var1", name: "Variable1", type: "string" }], + hiddenFields: { enabled: true, fieldIds: ["hf1"] }, + } as unknown as TSurvey; + const dummyResponse = { + id: "resp1", + finished: true, + data: { q1: "answer1", q2: "", verifiedEmail: true, hf1: "hiddenVal" }, + variables: { var1: "varValue" }, + language: "en", + } as unknown as TResponse; + + test("renders welcomeCard branch when enabled", () => { + render(); + expect(screen.getAllByTestId("QuestionSkip")[0]).toHaveTextContent("welcomeCard"); + }); + + test("renders VerifiedEmail when enabled and response verified", () => { + render(); + expect(screen.getByTestId("VerifiedEmail")).toBeInTheDocument(); + }); + + test("renders RenderResponse for valid answer", () => { + const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey; + const responseCopy = { ...dummyResponse, data: { q1: "answer1", q2: "" } }; + render(); + // For question q1 answer is valid so RenderResponse is rendered + expect(screen.getByTestId("RenderResponse")).toHaveTextContent("answer1"); + }); + + test("renders QuestionSkip for invalid answer", () => { + const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey; + const responseCopy = { ...dummyResponse, data: { q1: "", q2: "" } }; + render( + + ); + // Renders QuestionSkip for q1 or q2 branch + expect(screen.getAllByTestId("QuestionSkip")[1]).toBeInTheDocument(); + }); + + test("renders ResponseVariables when variables exist", () => { + render(); + expect(screen.getByTestId("ResponseVariables")).toBeInTheDocument(); + }); + + test("renders HiddenFields when hiddenFields enabled", () => { + render(); + expect(screen.getByTestId("HiddenFields")).toBeInTheDocument(); + }); + + test("renders completion indicator when response finished", () => { + render(); + expect(screen.getByTestId("CheckCircle2Icon")).toBeInTheDocument(); + expect(screen.getByText("common.completed")).toBeInTheDocument(); + }); + + test("processes question mapping correctly with skippedQuestions modification", () => { + // Provide one question valid and one not valid, with skippedQuestions for the invalid one. + const surveyCopy = { ...dummySurvey, welcomeCard: { enabled: false } } as TSurvey; + const responseCopy = { ...dummyResponse, data: { q1: "answer1", q2: "" } }; + // Initially, skippedQuestions contains ["q2"]. + render( + + ); + // For q1, RenderResponse is rendered since answer valid. + expect(screen.getByTestId("RenderResponse")).toBeInTheDocument(); + // For q2, QuestionSkip is rendered. Our mock for QuestionSkip returns text "skipped". + expect(screen.getByText("skipped")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.tsx index aaaebd1b02..9547470f58 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardBody.tsx @@ -1,9 +1,9 @@ "use client"; +import { getLocalizedValue } from "@/lib/i18n/utils"; +import { parseRecallInfo } from "@/lib/utils/recall"; import { useTranslate } from "@tolgee/react"; import { CheckCircle2Icon } from "lucide-react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { parseRecallInfo } from "@formbricks/lib/utils/recall"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; import { isValidValue } from "../util"; @@ -37,7 +37,7 @@ export const SingleResponseCardBody = ({ return ( + 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"> @{part} ); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx new file mode 100644 index 0000000000..c0817faed9 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.test.tsx @@ -0,0 +1,159 @@ +import { isSubmissionTimeMoreThan5Minutes } from "@/modules/analysis/components/SingleResponseCard/util"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TResponse } from "@formbricks/types/responses"; +import { TSurvey } from "@formbricks/types/surveys/types"; +import { TUser } from "@formbricks/types/user"; +import { SingleResponseCardHeader } from "./SingleResponseCardHeader"; + +// Mocks +vi.mock("@/modules/ui/components/avatars", () => ({ + PersonAvatar: ({ personId }: any) =>
    Avatar: {personId}
    , +})); +vi.mock("@/modules/ui/components/survey-status-indicator", () => ({ + SurveyStatusIndicator: ({ status }: any) =>
    Status: {status}
    , +})); +vi.mock("@/modules/ui/components/tooltip", () => ({ + Tooltip: ({ children }: any) =>
    {children}
    , + TooltipContent: ({ children }: any) =>
    {children}
    , + TooltipProvider: ({ children }: any) =>
    {children}
    , + TooltipTrigger: ({ children }: any) =>
    {children}
    , +})); +vi.mock("@formbricks/i18n-utils/src/utils", () => ({ + getLanguageLabel: vi.fn(), +})); +vi.mock("@/modules/lib/time", () => ({ + timeSince: vi.fn(() => "5 minutes ago"), +})); +vi.mock("@/modules/lib/utils/contact", () => ({ + getContactIdentifier: vi.fn((contact, attributes) => attributes?.email || contact?.userId || ""), +})); +vi.mock("../util", () => ({ + isSubmissionTimeMoreThan5Minutes: vi.fn(), +})); + +describe("SingleResponseCardHeader", () => { + afterEach(() => { + cleanup(); + }); + + const dummySurvey = { + id: "survey1", + name: "Test Survey", + environmentId: "env1", + } as TSurvey; + const dummyResponse = { + id: "resp1", + finished: false, + updatedAt: new Date("2023-01-01T12:00:00Z"), + createdAt: new Date("2023-01-01T11:00:00Z"), + language: "en", + contact: { id: "contact1", name: "Alice" }, + contactAttributes: { attr: "value" }, + meta: { + userAgent: { browser: "Chrome", os: "Windows", device: "PC" }, + url: "http://example.com", + action: "click", + source: "web", + country: "USA", + }, + singleUseId: "su123", + } as unknown as TResponse; + const dummyEnvironment = { id: "env1" } as TEnvironment; + const dummyUser = { id: "user1", email: "user1@example.com" } as TUser; + const dummyLocale = "en-US"; + + test("renders response view with contact (user exists)", () => { + vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true); + render( + + ); + // Expect Link wrapping PersonAvatar and display identifier + expect(screen.getByTestId("PersonAvatar")).toHaveTextContent("Avatar: contact1"); + expect(screen.getByRole("link")).toBeInTheDocument(); + }); + + test("renders response view with no contact (anonymous)", () => { + const responseNoContact = { ...dummyResponse, contact: null }; + render( + + ); + expect(screen.getByText("common.anonymous")).toBeInTheDocument(); + }); + + test("renders people view", () => { + render( + + ); + expect(screen.getByRole("link")).toBeInTheDocument(); + expect(screen.getByText("Test Survey")).toBeInTheDocument(); + expect(screen.getByTestId("SurveyStatusIndicator")).toBeInTheDocument(); + }); + + test("renders enabled trash icon and handles click", async () => { + vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(true); + const setDeleteDialogOpen = vi.fn(); + render( + + ); + const trashIcon = screen.getByLabelText("Delete response"); + await userEvent.click(trashIcon); + expect(setDeleteDialogOpen).toHaveBeenCalledWith(true); + }); + + test("renders disabled trash icon when deletion not allowed", async () => { + vi.mocked(isSubmissionTimeMoreThan5Minutes).mockReturnValue(false); + render( + + ); + const disabledTrash = screen.getByLabelText("Cannot delete response in progress"); + expect(disabledTrash).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.tsx index eeedfd492c..86f0df73bd 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/SingleResponseCardHeader.tsx @@ -1,5 +1,7 @@ "use client"; +import { timeSince } from "@/lib/time"; +import { getContactIdentifier } from "@/lib/utils/contact"; import { PersonAvatar } from "@/modules/ui/components/avatars"; import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; @@ -7,9 +9,7 @@ import { useTranslate } from "@tolgee/react"; import { LanguagesIcon, TrashIcon } from "lucide-react"; import Link from "next/link"; import { ReactNode } from "react"; -import { getLanguageLabel } from "@formbricks/lib/i18n/utils"; -import { timeSince } from "@formbricks/lib/time"; -import { getContactIdentifier } from "@formbricks/lib/utils/contact"; +import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils"; import { TEnvironment } from "@formbricks/types/environment"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -153,7 +153,7 @@ export const SingleResponseCardHeader = ({ const deleteSubmissionToolTip = <>{t("environments.surveys.responses.this_response_is_in_progress")}; return ( -
    +
    {pageType === "response" && ( diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.test.tsx new file mode 100644 index 0000000000..c2c22afe54 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.test.tsx @@ -0,0 +1,60 @@ +import { cleanup, render } from "@testing-library/react"; +import { afterEach, describe, expect, test } from "vitest"; +import { + ConfusedFace, + FrowningFace, + GrinningFaceWithSmilingEyes, + GrinningSquintingFace, + NeutralFace, + PerseveringFace, + SlightlySmilingFace, + SmilingFaceWithSmilingEyes, + TiredFace, + WearyFace, +} from "./Smileys"; + +const checkSvg = (Component: React.FC>) => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + expect(svg).toHaveAttribute("viewBox", "0 0 72 72"); + expect(svg).toHaveAttribute("width", "36"); + expect(svg).toHaveAttribute("height", "36"); +}; + +describe("Smileys", () => { + afterEach(() => { + cleanup(); + }); + + test("renders TiredFace", () => { + checkSvg(TiredFace); + }); + test("renders WearyFace", () => { + checkSvg(WearyFace); + }); + test("renders PerseveringFace", () => { + checkSvg(PerseveringFace); + }); + test("renders FrowningFace", () => { + checkSvg(FrowningFace); + }); + test("renders ConfusedFace", () => { + checkSvg(ConfusedFace); + }); + test("renders NeutralFace", () => { + checkSvg(NeutralFace); + }); + test("renders SlightlySmilingFace", () => { + checkSvg(SlightlySmilingFace); + }); + test("renders SmilingFaceWithSmilingEyes", () => { + checkSvg(SmilingFaceWithSmilingEyes); + }); + test("renders GrinningFaceWithSmilingEyes", () => { + checkSvg(GrinningFaceWithSmilingEyes); + }); + test("renders GrinningSquintingFace", () => { + checkSvg(GrinningSquintingFace); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx new file mode 100644 index 0000000000..092d802139 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/VerifiedEmail.test.tsx @@ -0,0 +1,31 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { VerifiedEmail } from "./VerifiedEmail"; + +vi.mock("lucide-react", () => ({ + MailIcon: (props: any) => ( +
    + MailIcon +
    + ), +})); + +describe("VerifiedEmail", () => { + afterEach(() => { + cleanup(); + }); + + test("renders verified email text and value when provided", () => { + render(); + expect(screen.getByText("common.verified_email")).toBeInTheDocument(); + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + expect(screen.getByTestId("MailIcon")).toBeInTheDocument(); + }); + + test("renders empty value when verifiedEmail is not a string", () => { + render(); + expect(screen.getByText("common.verified_email")).toBeInTheDocument(); + const emptyParagraph = screen.getByText("", { selector: "p.ph-no-capture" }); + expect(emptyParagraph.textContent).toBe(""); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx b/apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx new file mode 100644 index 0000000000..f1ce0d3e29 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/index.test.tsx @@ -0,0 +1,190 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import toast from "react-hot-toast"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TResponse } from "@formbricks/types/responses"; +import { TSurvey } from "@formbricks/types/surveys/types"; +import { TUser } from "@formbricks/types/user"; +import { deleteResponseAction, getResponseAction } from "./actions"; +import { SingleResponseCard } from "./index"; + +// Dummy data for props +const dummySurvey = { + id: "survey1", + environmentId: "env1", + name: "Test Survey", + status: "completed", + type: "link", + questions: [{ id: "q1" }, { id: "q2" }], + responseCount: 10, + notes: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +} as unknown as TSurvey; +const dummyResponse = { + id: "resp1", + finished: true, + data: { q1: "answer1", q2: null }, + notes: [], + tags: [], +} as unknown as TResponse; +const dummyEnvironment = { id: "env1" } as TEnvironment; +const dummyUser = { id: "user1", email: "user1@example.com", name: "User One" } as TUser; +const dummyLocale = "en-US"; + +const dummyDeleteResponses = vi.fn(); +const dummyUpdateResponse = vi.fn(); +const dummySetSelectedResponseId = vi.fn(); + +// Mock internal components to return identifiable elements +vi.mock("./components/SingleResponseCardHeader", () => ({ + SingleResponseCardHeader: (props: any) => ( +
    + +
    + ), +})); +vi.mock("./components/SingleResponseCardBody", () => ({ + SingleResponseCardBody: () =>
    Body Content
    , +})); +vi.mock("./components/ResponseTagsWrapper", () => ({ + ResponseTagsWrapper: (props: any) => ( +
    + +
    + ), +})); +vi.mock("@/modules/ui/components/delete-dialog", () => ({ + DeleteDialog: ({ open, onDelete }: any) => + open ? ( + + ) : null, +})); +vi.mock("./components/ResponseNote", () => ({ + ResponseNotes: (props: any) =>
    Notes ({props.notes.length})
    , +})); + +vi.mock("./actions", () => ({ + deleteResponseAction: vi.fn().mockResolvedValue("deletedResponse"), + getResponseAction: vi.fn(), +})); + +vi.mock("./util", () => ({ + isValidValue: (value: any) => value !== null && value !== undefined, +})); + +describe("SingleResponseCard", () => { + afterEach(() => { + cleanup(); + }); + + test("renders as a plain div when survey is draft and isReadOnly", () => { + const draftSurvey = { ...dummySurvey, status: "draft" } as TSurvey; + render( + + ); + + expect(screen.getByTestId("SingleResponseCardHeader")).toBeInTheDocument(); + expect(screen.queryByRole("link")).toBeNull(); + }); + + test("calls deleteResponseAction and refreshes router on successful deletion", async () => { + render( + + ); + + userEvent.click(screen.getByText("Open Delete")); + + const deleteButton = await screen.findByTestId("DeleteDialog"); + await userEvent.click(deleteButton); + await waitFor(() => { + expect(deleteResponseAction).toHaveBeenCalledWith({ responseId: dummyResponse.id }); + }); + + expect(dummyDeleteResponses).toHaveBeenCalledWith([dummyResponse.id]); + }); + + test("calls toast.error when deleteResponseAction throws error", async () => { + vi.mocked(deleteResponseAction).mockRejectedValueOnce(new Error("Delete failed")); + render( + + ); + await userEvent.click(screen.getByText("Open Delete")); + const deleteButton = await screen.findByTestId("DeleteDialog"); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Delete failed"); + }); + }); + + test("calls updateResponse when getResponseAction returns updated response", async () => { + vi.mocked(getResponseAction).mockResolvedValueOnce({ data: { updated: true } as any }); + render( + + ); + + expect(screen.getByTestId("ResponseTagsWrapper")).toBeInTheDocument(); + + await userEvent.click(screen.getByText("Update Responses")); + + await waitFor(() => { + expect(getResponseAction).toHaveBeenCalledWith({ responseId: dummyResponse.id }); + }); + + await waitFor(() => { + expect(dummyUpdateResponse).toHaveBeenCalledWith(dummyResponse.id, { updated: true }); + }); + }); +}); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/index.tsx b/apps/web/modules/analysis/components/SingleResponseCard/index.tsx index 64cbe02ea6..822bd5d106 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/index.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/index.tsx @@ -1,18 +1,17 @@ "use client"; +import { cn } from "@/lib/cn"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { useTranslate } from "@tolgee/react"; import clsx from "clsx"; import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; -import { cn } from "@formbricks/lib/cn"; import { TEnvironment } from "@formbricks/types/environment"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TTag } from "@formbricks/types/tags"; -import { TUser } from "@formbricks/types/user"; -import { TUserLocale } from "@formbricks/types/user"; +import { TUser, TUserLocale } from "@formbricks/types/user"; import { deleteResponseAction, getResponseAction } from "./actions"; import { ResponseNotes } from "./components/ResponseNote"; import { ResponseTagsWrapper } from "./components/ResponseTagsWrapper"; @@ -61,28 +60,24 @@ export const SingleResponseCard = ({ survey.questions.forEach((question) => { if (!isValidValue(response.data[question.id])) { temp.push(question.id); - } else { - if (temp.length > 0) { - skippedQuestions.push([...temp]); - temp = []; - } + } else if (temp.length > 0) { + skippedQuestions.push([...temp]); + temp = []; } }); } else { for (let index = survey.questions.length - 1; index >= 0; index--) { const question = survey.questions[index]; - if (!response.data[question.id]) { - if (skippedQuestions.length === 0) { - temp.push(question.id); - } else if (skippedQuestions.length > 0 && !isValidValue(response.data[question.id])) { - temp.push(question.id); - } - } else { - if (temp.length > 0) { - temp.reverse(); - skippedQuestions.push([...temp]); - temp = []; - } + if ( + !response.data[question.id] && + (skippedQuestions.length === 0 || + (skippedQuestions.length > 0 && !isValidValue(response.data[question.id]))) + ) { + temp.push(question.id); + } else if (temp.length > 0) { + temp.reverse(); + skippedQuestions.push([...temp]); + temp = []; } } } diff --git a/apps/web/modules/analysis/components/SingleResponseCard/util.test.ts b/apps/web/modules/analysis/components/SingleResponseCard/util.test.ts new file mode 100644 index 0000000000..ebfc8f5530 --- /dev/null +++ b/apps/web/modules/analysis/components/SingleResponseCard/util.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "vitest"; +import { isSubmissionTimeMoreThan5Minutes, isValidValue } from "./util"; + +describe("isValidValue", () => { + test("returns false for an empty string", () => { + expect(isValidValue("")).toBe(false); + }); + + test("returns false for a blank string", () => { + expect(isValidValue(" ")).toBe(false); + }); + + test("returns true for a non-empty string", () => { + expect(isValidValue("hello")).toBe(true); + }); + + test("returns true for numbers", () => { + expect(isValidValue(0)).toBe(true); + expect(isValidValue(42)).toBe(true); + }); + + test("returns false for an empty array", () => { + expect(isValidValue([])).toBe(false); + }); + + test("returns true for a non-empty array", () => { + expect(isValidValue(["item"])).toBe(true); + }); + + test("returns false for an empty object", () => { + expect(isValidValue({})).toBe(false); + }); + + test("returns true for a non-empty object", () => { + expect(isValidValue({ key: "value" })).toBe(true); + }); +}); + +describe("isSubmissionTimeMoreThan5Minutes", () => { + test("returns true if submission time is more than 5 minutes ago", () => { + const currentTime = new Date(); + const oldTime = new Date(currentTime.getTime() - 6 * 60 * 1000); // 6 minutes ago + expect(isSubmissionTimeMoreThan5Minutes(oldTime)).toBe(true); + }); + + test("returns false if submission time is less than or equal to 5 minutes ago", () => { + const currentTime = new Date(); + const recentTime = new Date(currentTime.getTime() - 4 * 60 * 1000); // 4 minutes ago + expect(isSubmissionTimeMoreThan5Minutes(recentTime)).toBe(false); + }); +}); diff --git a/apps/web/modules/analysis/utils.test.tsx b/apps/web/modules/analysis/utils.test.tsx new file mode 100644 index 0000000000..ab9ec61103 --- /dev/null +++ b/apps/web/modules/analysis/utils.test.tsx @@ -0,0 +1,67 @@ +import { cleanup } from "@testing-library/react"; +import { isValidElement } from "react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { renderHyperlinkedContent } from "./utils"; + +describe("renderHyperlinkedContent", () => { + afterEach(() => { + cleanup(); + }); + + test("returns a single span element when input has no url", () => { + const input = "Hello world"; + const elements = renderHyperlinkedContent(input); + expect(elements).toHaveLength(1); + const element = elements[0]; + expect(isValidElement(element)).toBe(true); + // element.type should be "span" + expect(element.type).toBe("span"); + expect(element.props.children).toEqual("Hello world"); + }); + + test("splits input with a valid url into span, anchor, span", () => { + const input = "Visit https://example.com for info"; + const elements = renderHyperlinkedContent(input); + // Expect three elements: before text, URL link, after text. + expect(elements).toHaveLength(3); + // First element should be span with "Visit " + expect(elements[0].type).toBe("span"); + expect(elements[0].props.children).toEqual("Visit "); + // Second element should be an anchor with the URL. + expect(elements[1].type).toBe("a"); + expect(elements[1].props.href).toEqual("https://example.com"); + expect(elements[1].props.className).toContain("text-blue-500"); + // Third element: span with " for info" + expect(elements[2].type).toBe("span"); + expect(elements[2].props.children).toEqual(" for info"); + }); + + test("handles multiple valid urls in the input", () => { + const input = "Link1: https://example.com and Link2: https://vitejs.dev"; + const elements = renderHyperlinkedContent(input); + // Expected parts: "Link1: ", "https://example.com", " and Link2: ", "https://vitejs.dev", "" + expect(elements).toHaveLength(5); + expect(elements[1].type).toBe("a"); + expect(elements[1].props.href).toEqual("https://example.com"); + expect(elements[3].type).toBe("a"); + expect(elements[3].props.href).toEqual("https://vitejs.dev"); + }); + + test("renders a span instead of anchor when URL constructor throws", () => { + // Force global.URL to throw for this test. + const originalURL = global.URL; + vi.spyOn(global, "URL").mockImplementation(() => { + throw new Error("Invalid URL"); + }); + const input = "Visit https://broken-url.com now"; + const elements = renderHyperlinkedContent(input); + // Expect the URL not to be rendered as anchor because isValidUrl returns false + // The split will still occur, but the element corresponding to the URL should be a span. + expect(elements).toHaveLength(3); + // Check the element that would have been an anchor is now a span. + expect(elements[1].type).toBe("span"); + expect(elements[1].props.children).toEqual("https://broken-url.com"); + // Restore original URL + global.URL = originalURL; + }); +}); diff --git a/apps/web/modules/api/v2/auth/api-wrapper.ts b/apps/web/modules/api/v2/auth/api-wrapper.ts index 1a4cc8d1c8..90a7a4cba7 100644 --- a/apps/web/modules/api/v2/auth/api-wrapper.ts +++ b/apps/web/modules/api/v2/auth/api-wrapper.ts @@ -21,11 +21,19 @@ export type ExtendedSchemas = { }; // Define a type that returns separate keys for each input type. -export type ParsedSchemas = { - body?: S extends { body: z.ZodObject } ? z.infer : undefined; - query?: S extends { query: z.ZodObject } ? z.infer : undefined; - params?: S extends { params: z.ZodObject } ? z.infer : undefined; -}; +// It uses mapped types to create a new type based on the input schemas. +// It checks if each schema is defined and if it is a ZodObject, then infers the type from it. +// It also uses conditional types to ensure that the keys are only included if the schema is defined and valid. +// This allows for more flexibility and type safety when working with the input schemas. +export type ParsedSchemas = S extends object + ? { + [K in keyof S as NonNullable extends z.ZodObject ? K : never]: NonNullable< + S[K] + > extends z.ZodObject + ? z.infer> + : never; + } + : {}; export const apiWrapper = async ({ request, diff --git a/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts b/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts index dba952054f..9903a83c6b 100644 --- a/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts +++ b/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts @@ -3,7 +3,7 @@ import { authenticateRequest } from "@/modules/api/v2/auth/authenticate-request" import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit"; import { handleApiError } from "@/modules/api/v2/lib/utils"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { z } from "zod"; import { err, ok, okVoid } from "@formbricks/types/error-handlers"; @@ -25,7 +25,7 @@ vi.mock("@/modules/api/v2/lib/utils", () => ({ })); describe("apiWrapper", () => { - it("should handle request and return response", async () => { + test("should handle request and return response", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); @@ -49,7 +49,7 @@ describe("apiWrapper", () => { expect(handler).toHaveBeenCalled(); }); - it("should handle errors and return error response", async () => { + test("should handle errors and return error response", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "invalid-api-key" }, }); @@ -67,7 +67,7 @@ describe("apiWrapper", () => { expect(handler).not.toHaveBeenCalled(); }); - it("should parse body schema correctly", async () => { + test("should parse body schema correctly", async () => { const request = new Request("http://localhost", { method: "POST", body: JSON.stringify({ key: "value" }), @@ -100,7 +100,7 @@ describe("apiWrapper", () => { ); }); - it("should handle body schema errors", async () => { + test("should handle body schema errors", async () => { const request = new Request("http://localhost", { method: "POST", body: JSON.stringify({ key: 123 }), @@ -131,7 +131,7 @@ describe("apiWrapper", () => { expect(handler).not.toHaveBeenCalled(); }); - it("should parse query schema correctly", async () => { + test("should parse query schema correctly", async () => { const request = new Request("http://localhost?key=value"); vi.mocked(authenticateRequest).mockResolvedValue( @@ -160,7 +160,7 @@ describe("apiWrapper", () => { ); }); - it("should handle query schema errors", async () => { + test("should handle query schema errors", async () => { const request = new Request("http://localhost?foo%ZZ=abc"); vi.mocked(authenticateRequest).mockResolvedValue( @@ -187,7 +187,7 @@ describe("apiWrapper", () => { expect(handler).not.toHaveBeenCalled(); }); - it("should parse params schema correctly", async () => { + test("should parse params schema correctly", async () => { const request = new Request("http://localhost"); vi.mocked(authenticateRequest).mockResolvedValue( @@ -217,7 +217,7 @@ describe("apiWrapper", () => { ); }); - it("should handle no external params", async () => { + test("should handle no external params", async () => { const request = new Request("http://localhost"); vi.mocked(authenticateRequest).mockResolvedValue( @@ -245,7 +245,7 @@ describe("apiWrapper", () => { expect(handler).not.toHaveBeenCalled(); }); - it("should handle params schema errors", async () => { + test("should handle params schema errors", async () => { const request = new Request("http://localhost"); vi.mocked(authenticateRequest).mockResolvedValue( @@ -273,7 +273,7 @@ describe("apiWrapper", () => { expect(handler).not.toHaveBeenCalled(); }); - it("should handle rate limit errors", async () => { + test("should handle rate limit errors", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); diff --git a/apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts b/apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts index 27f4f78cae..459d5e526e 100644 --- a/apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts +++ b/apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts @@ -1,5 +1,5 @@ import { hashApiKey } from "@/modules/api/v2/management/lib/utils"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { authenticateRequest } from "../authenticate-request"; @@ -17,7 +17,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({ })); describe("authenticateRequest", () => { - it("should return authentication data if apiKey is valid", async () => { + test("should return authentication data if apiKey is valid", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); @@ -87,7 +87,7 @@ describe("authenticateRequest", () => { } }); - it("should return unauthorized error if apiKey is not found", async () => { + test("should return unauthorized error if apiKey is not found", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "invalid-api-key" }, }); @@ -101,7 +101,7 @@ describe("authenticateRequest", () => { } }); - it("should return unauthorized error if apiKey is missing", async () => { + test("should return unauthorized error if apiKey is missing", async () => { const request = new Request("http://localhost"); const result = await authenticateRequest(request); diff --git a/apps/web/modules/api/v2/auth/tests/authenticated-api-client.test.ts b/apps/web/modules/api/v2/auth/tests/authenticated-api-client.test.ts index 77fc37a951..900633e62b 100644 --- a/apps/web/modules/api/v2/auth/tests/authenticated-api-client.test.ts +++ b/apps/web/modules/api/v2/auth/tests/authenticated-api-client.test.ts @@ -1,5 +1,5 @@ import { logApiRequest } from "@/modules/api/v2/lib/utils"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { apiWrapper } from "../api-wrapper"; import { authenticatedApiClient } from "../authenticated-api-client"; @@ -12,7 +12,7 @@ vi.mock("@/modules/api/v2/lib/utils", () => ({ })); describe("authenticatedApiClient", () => { - it("should log request and return response", async () => { + test("should log request and return response", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); diff --git a/apps/web/modules/api/v2/lib/rate-limit.ts b/apps/web/modules/api/v2/lib/rate-limit.ts index 2ca3d695eb..0ebf99b183 100644 --- a/apps/web/modules/api/v2/lib/rate-limit.ts +++ b/apps/web/modules/api/v2/lib/rate-limit.ts @@ -1,6 +1,6 @@ +import { MANAGEMENT_API_RATE_LIMIT, RATE_LIMITING_DISABLED, UNKEY_ROOT_KEY } from "@/lib/constants"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { type LimitOptions, Ratelimit, type RatelimitResponse } from "@unkey/ratelimit"; -import { MANAGEMENT_API_RATE_LIMIT, RATE_LIMITING_DISABLED, UNKEY_ROOT_KEY } from "@formbricks/lib/constants"; import { logger } from "@formbricks/logger"; import { Result, err, okVoid } from "@formbricks/types/error-handlers"; diff --git a/apps/web/modules/api/v2/lib/response.ts b/apps/web/modules/api/v2/lib/response.ts index a378f75a36..4aa2689c90 100644 --- a/apps/web/modules/api/v2/lib/response.ts +++ b/apps/web/modules/api/v2/lib/response.ts @@ -260,6 +260,34 @@ const successResponse = ({ ); }; +export const createdResponse = ({ + data, + meta, + cors = false, + cache = "private, no-store", +}: { + data: Object; + meta?: Record; + cors?: boolean; + cache?: string; +}) => { + const headers = { + ...(cors && corsHeaders), + "Cache-Control": cache, + }; + + return Response.json( + { + data, + meta, + } as ApiSuccessResponse, + { + status: 201, + headers, + } + ); +}; + export const multiStatusResponse = ({ data, meta, @@ -298,5 +326,6 @@ export const responses = { tooManyRequestsResponse, internalServerErrorResponse, successResponse, + createdResponse, multiStatusResponse, }; diff --git a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts index 323854abc3..5b1f70aa41 100644 --- a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts +++ b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts @@ -14,8 +14,8 @@ vi.mock("@unkey/ratelimit", () => ({ describe("when rate limiting is disabled", () => { beforeEach(async () => { vi.resetModules(); - const constants = await vi.importActual("@formbricks/lib/constants"); - vi.doMock("@formbricks/lib/constants", () => ({ + const constants = await vi.importActual("@/lib/constants"); + vi.doMock("@/lib/constants", () => ({ ...constants, MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 }, RATE_LIMITING_DISABLED: true, @@ -41,8 +41,8 @@ describe("when rate limiting is disabled", () => { describe("when UNKEY_ROOT_KEY is missing", () => { beforeEach(async () => { vi.resetModules(); - const constants = await vi.importActual("@formbricks/lib/constants"); - vi.doMock("@formbricks/lib/constants", () => ({ + const constants = await vi.importActual("@/lib/constants"); + vi.doMock("@/lib/constants", () => ({ ...constants, MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 }, RATE_LIMITING_DISABLED: false, @@ -68,8 +68,8 @@ describe("when rate limiting is active (enabled)", () => { beforeEach(async () => { vi.resetModules(); - const constants = await vi.importActual("@formbricks/lib/constants"); - vi.doMock("@formbricks/lib/constants", () => ({ + const constants = await vi.importActual("@/lib/constants"); + vi.doMock("@/lib/constants", () => ({ ...constants, MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 }, RATE_LIMITING_DISABLED: false, diff --git a/apps/web/modules/api/v2/lib/tests/response.test.ts b/apps/web/modules/api/v2/lib/tests/response.test.ts index c5e5d233d9..a58f78fd4e 100644 --- a/apps/web/modules/api/v2/lib/tests/response.test.ts +++ b/apps/web/modules/api/v2/lib/tests/response.test.ts @@ -120,7 +120,7 @@ describe("API Responses", () => { }); test("include CORS headers when cors is true", () => { - const res = responses.unprocessableEntityResponse({ cors: true }); + const res = responses.unprocessableEntityResponse({ cors: true, details: [] }); expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*"); }); }); @@ -182,4 +182,38 @@ describe("API Responses", () => { expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*"); }); }); + + describe("createdResponse", () => { + test("return a success response with the provided data", async () => { + const data = { foo: "bar" }; + const meta = { page: 1 }; + const res = responses.createdResponse({ data, meta }); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body.data).toEqual(data); + expect(body.meta).toEqual(meta); + }); + + test("include CORS headers when cors is true", () => { + const data = { foo: "bar" }; + const res = responses.createdResponse({ data, cors: true }); + expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*"); + }); + }); + + describe("multiStatusResponse", () => { + test("return a 207 response with the provided data", async () => { + const data = { foo: "bar" }; + const res = responses.multiStatusResponse({ data }); + expect(res.status).toBe(207); + const body = await res.json(); + expect(body.data).toEqual(data); + }); + + test("include CORS headers when cors is true", () => { + const data = { foo: "bar" }; + const res = responses.multiStatusResponse({ data, cors: true }); + expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*"); + }); + }); }); diff --git a/apps/web/modules/api/v2/lib/tests/utils.test.ts b/apps/web/modules/api/v2/lib/tests/utils.test.ts index 0885a565cd..82bebb05ab 100644 --- a/apps/web/modules/api/v2/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/lib/tests/utils.test.ts @@ -1,4 +1,5 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import * as Sentry from "@sentry/nextjs"; import { describe, expect, test, vi } from "vitest"; import { ZodError } from "zod"; import { logger } from "@formbricks/logger"; @@ -9,6 +10,16 @@ const mockRequest = new Request("http://localhost"); // Add the request id header mockRequest.headers.set("x-request-id", "123"); +vi.mock("@sentry/nextjs", () => ({ + captureException: vi.fn(), +})); + +// Mock SENTRY_DSN constant +vi.mock("@/lib/constants", () => ({ + SENTRY_DSN: "mocked-sentry-dsn", + IS_PRODUCTION: true, +})); + describe("utils", () => { describe("handleApiError", () => { test('return bad request response for "bad_request" error', async () => { @@ -257,5 +268,45 @@ describe("utils", () => { // Restore the original method logger.withContext = originalWithContext; }); + + test("log API error details with SENTRY_DSN set", () => { + // Mock the withContext method and its returned error method + const errorMock = vi.fn(); + const withContextMock = vi.fn().mockReturnValue({ + error: errorMock, + }); + + // Mock Sentry's captureException method + vi.mocked(Sentry.captureException).mockImplementation((() => {}) as any); + + // Replace the original withContext with our mock + const originalWithContext = logger.withContext; + logger.withContext = withContextMock; + + const mockRequest = new Request("http://localhost/api/test"); + mockRequest.headers.set("x-request-id", "123"); + + const error: ApiErrorResponseV2 = { + type: "internal_server_error", + details: [{ field: "server", issue: "error occurred" }], + }; + + logApiError(mockRequest, error); + + // Verify withContext was called with the expected context + expect(withContextMock).toHaveBeenCalledWith({ + correlationId: "123", + error, + }); + + // Verify error was called on the child logger + expect(errorMock).toHaveBeenCalledWith("API Error Details"); + + // Verify Sentry.captureException was called + expect(Sentry.captureException).toHaveBeenCalled(); + + // Restore the original method + logger.withContext = originalWithContext; + }); }); }); diff --git a/apps/web/modules/api/v2/lib/utils.ts b/apps/web/modules/api/v2/lib/utils.ts index 845e22a7b6..1cb0472379 100644 --- a/apps/web/modules/api/v2/lib/utils.ts +++ b/apps/web/modules/api/v2/lib/utils.ts @@ -1,5 +1,9 @@ +// @ts-nocheck // We can remove this when we update the prisma client and the typescript version +// if we don't add this we get build errors with prisma due to type-nesting +import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants"; import { responses } from "@/modules/api/v2/lib/response"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import * as Sentry from "@sentry/nextjs"; import { ZodCustomIssue, ZodIssue } from "zod"; import { logger } from "@formbricks/logger"; @@ -59,7 +63,6 @@ export const logApiRequest = (request: Request, responseStatus: number): void => Object.entries(queryParams).filter(([key]) => !sensitiveParams.includes(key.toLowerCase())) ); - // Info: Conveys general, operational messages about system progress and state. logger .withContext({ method, @@ -73,7 +76,22 @@ export const logApiRequest = (request: Request, responseStatus: number): void => }; export const logApiError = (request: Request, error: ApiErrorResponseV2): void => { - const correlationId = request.headers.get("x-request-id") || ""; + const correlationId = request.headers.get("x-request-id") ?? ""; + + // Send the error to Sentry if the DSN is set and the error type is internal_server_error + // This is useful for tracking down issues without overloading Sentry with errors + if (SENTRY_DSN && IS_PRODUCTION && error.type === "internal_server_error") { + const err = new Error(`API V2 error, id: ${correlationId}`); + + Sentry.captureException(err, { + extra: { + details: error.details, + type: error.type, + correlationId, + }, + }); + } + logger .withContext({ correlationId, diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/contact-attribute-key.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/contact-attribute-key.ts new file mode 100644 index 0000000000..a9c25a5411 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/contact-attribute-key.ts @@ -0,0 +1,183 @@ +import { cache } from "@/lib/cache"; +import { contactCache } from "@/lib/cache/contact"; +import { contactAttributeCache } from "@/lib/cache/contact-attribute"; +import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key"; +import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { ContactAttributeKey } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getContactAttributeKey = reactCache(async (contactAttributeKeyId: string) => + cache( + async (): Promise> => { + try { + const contactAttributeKey = await prisma.contactAttributeKey.findUnique({ + where: { + id: contactAttributeKeyId, + }, + }); + + if (!contactAttributeKey) { + return err({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + + return ok(contactAttributeKey); + } catch (error) { + return err({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: error.message }], + }); + } + }, + [`management-getContactAttributeKey-${contactAttributeKeyId}`], + { + tags: [contactAttributeKeyCache.tag.byId(contactAttributeKeyId)], + } + )() +); + +export const updateContactAttributeKey = async ( + contactAttributeKeyId: string, + contactAttributeKeyInput: TContactAttributeKeyUpdateSchema +): Promise> => { + try { + const updatedKey = await prisma.contactAttributeKey.update({ + where: { + id: contactAttributeKeyId, + }, + data: contactAttributeKeyInput, + }); + + const associatedContactAttributes = await prisma.contactAttribute.findMany({ + where: { + attributeKeyId: updatedKey.id, + }, + select: { + id: true, + contactId: true, + }, + }); + + contactAttributeKeyCache.revalidate({ + id: contactAttributeKeyId, + environmentId: updatedKey.environmentId, + key: updatedKey.key, + }); + contactAttributeCache.revalidate({ + key: updatedKey.key, + environmentId: updatedKey.environmentId, + }); + + contactCache.revalidate({ + environmentId: updatedKey.environmentId, + }); + + associatedContactAttributes.forEach((contactAttribute) => { + contactAttributeCache.revalidate({ + contactId: contactAttribute.contactId, + }); + contactCache.revalidate({ + id: contactAttribute.contactId, + }); + }); + + return ok(updatedKey); + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + if ( + error.code === PrismaErrorType.RecordDoesNotExist || + error.code === PrismaErrorType.RelatedRecordDoesNotExist + ) { + return err({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + if (error.code === PrismaErrorType.UniqueConstraintViolation) { + return err({ + type: "conflict", + details: [ + { + field: "contactAttributeKey", + issue: `Contact attribute key with "${contactAttributeKeyInput.key}" already exists`, + }, + ], + }); + } + } + return err({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: error.message }], + }); + } +}; + +export const deleteContactAttributeKey = async ( + contactAttributeKeyId: string +): Promise> => { + try { + const deletedKey = await prisma.contactAttributeKey.delete({ + where: { + id: contactAttributeKeyId, + }, + }); + + const associatedContactAttributes = await prisma.contactAttribute.findMany({ + where: { + attributeKeyId: deletedKey.id, + }, + select: { + id: true, + contactId: true, + }, + }); + + contactAttributeKeyCache.revalidate({ + id: contactAttributeKeyId, + environmentId: deletedKey.environmentId, + key: deletedKey.key, + }); + contactAttributeCache.revalidate({ + key: deletedKey.key, + environmentId: deletedKey.environmentId, + }); + + contactCache.revalidate({ + environmentId: deletedKey.environmentId, + }); + + associatedContactAttributes.forEach((contactAttribute) => { + contactAttributeCache.revalidate({ + contactId: contactAttribute.contactId, + }); + contactCache.revalidate({ + id: contactAttribute.contactId, + }); + }); + + return ok(deletedKey); + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + if ( + error.code === PrismaErrorType.RecordDoesNotExist || + error.code === PrismaErrorType.RelatedRecordDoesNotExist + ) { + return err({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + } + return err({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: error.message }], + }); + } +}; diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/openapi.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/openapi.ts index e16ce064e6..bd9bd0d3a7 100644 --- a/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/openapi.ts @@ -1,4 +1,8 @@ -import { ZContactAttributeKeyInput } from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; +import { + ZContactAttributeKeyIdSchema, + ZContactAttributeKeyUpdateSchema, +} from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys"; +import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; import { z } from "zod"; import { ZodOpenApiOperationObject } from "zod-openapi"; import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys"; @@ -9,7 +13,7 @@ export const getContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { description: "Gets a contact attribute key from the database.", requestParams: { path: z.object({ - contactAttributeKeyId: z.string().cuid2(), + id: ZContactAttributeKeyIdSchema, }), }, tags: ["Management API > Contact Attribute Keys"], @@ -18,29 +22,7 @@ export const getContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { description: "Contact attribute key retrieved successfully.", content: { "application/json": { - schema: ZContactAttributeKey, - }, - }, - }, - }, -}; - -export const deleteContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { - operationId: "deleteContactAttributeKey", - summary: "Delete a contact attribute key", - description: "Deletes a contact attribute key from the database.", - tags: ["Management API > Contact Attribute Keys"], - requestParams: { - path: z.object({ - contactAttributeId: z.string().cuid2(), - }), - }, - responses: { - "200": { - description: "Contact attribute key deleted successfully.", - content: { - "application/json": { - schema: ZContactAttributeKey, + schema: makePartialSchema(ZContactAttributeKey), }, }, }, @@ -54,7 +36,7 @@ export const updateContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Contact Attribute Keys"], requestParams: { path: z.object({ - contactAttributeKeyId: z.string().cuid2(), + id: ZContactAttributeKeyIdSchema, }), }, requestBody: { @@ -62,7 +44,7 @@ export const updateContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { description: "The contact attribute key to update", content: { "application/json": { - schema: ZContactAttributeKeyInput, + schema: ZContactAttributeKeyUpdateSchema, }, }, }, @@ -71,7 +53,29 @@ export const updateContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { description: "Contact attribute key updated successfully.", content: { "application/json": { - schema: ZContactAttributeKey, + schema: makePartialSchema(ZContactAttributeKey), + }, + }, + }, + }, +}; + +export const deleteContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { + operationId: "deleteContactAttributeKey", + summary: "Delete a contact attribute key", + description: "Deletes a contact attribute key from the database.", + tags: ["Management API > Contact Attribute Keys"], + requestParams: { + path: z.object({ + id: ZContactAttributeKeyIdSchema, + }), + }, + responses: { + "200": { + description: "Contact attribute key deleted successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZContactAttributeKey), }, }, }, diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/tests/contact-attribute-key.test.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/tests/contact-attribute-key.test.ts new file mode 100644 index 0000000000..74c92ba32e --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/tests/contact-attribute-key.test.ts @@ -0,0 +1,222 @@ +import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key"; +import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys"; +import { ContactAttributeKey } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { describe, expect, test, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { + deleteContactAttributeKey, + getContactAttributeKey, + updateContactAttributeKey, +} from "../contact-attribute-key"; + +// Mock dependencies +vi.mock("@formbricks/database", () => ({ + prisma: { + contactAttributeKey: { + findUnique: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + findMany: vi.fn(), + }, + contactAttribute: { + findMany: vi.fn(), + }, + }, +})); + +vi.mock("@/lib/cache/contact-attribute-key", () => ({ + contactAttributeKeyCache: { + tag: { + byId: () => "mockTag", + }, + revalidate: vi.fn(), + }, +})); + +// Mock data +const mockContactAttributeKey: ContactAttributeKey = { + id: "cak123", + key: "email", + name: "Email", + description: "User's email address", + environmentId: "env123", + isUnique: true, + type: "default", + createdAt: new Date(), + updatedAt: new Date(), +}; + +const mockUpdateInput: TContactAttributeKeyUpdateSchema = { + key: "email", + name: "Email Address", + description: "User's verified email address", +}; + +const prismaNotFoundError = new PrismaClientKnownRequestError("Mock error message", { + code: PrismaErrorType.RelatedRecordDoesNotExist, + clientVersion: "0.0.1", +}); + +const prismaUniqueConstraintError = new PrismaClientKnownRequestError("Mock error message", { + code: PrismaErrorType.UniqueConstraintViolation, + clientVersion: "0.0.1", +}); + +describe("getContactAttributeKey", () => { + test("returns ok if contact attribute key is found", async () => { + vi.mocked(prisma.contactAttributeKey.findUnique).mockResolvedValueOnce(mockContactAttributeKey); + const result = await getContactAttributeKey("cak123"); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data).toEqual(mockContactAttributeKey); + } + }); + + test("returns err if contact attribute key not found", async () => { + vi.mocked(prisma.contactAttributeKey.findUnique).mockResolvedValueOnce(null); + const result = await getContactAttributeKey("cak999"); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + }); + + test("returns err on Prisma error", async () => { + vi.mocked(prisma.contactAttributeKey.findUnique).mockRejectedValueOnce(new Error("DB error")); + const result = await getContactAttributeKey("error"); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: "DB error" }], + }); + } + }); +}); + +describe("updateContactAttributeKey", () => { + test("returns ok on successful update", async () => { + const updatedKey = { ...mockContactAttributeKey, ...mockUpdateInput }; + vi.mocked(prisma.contactAttributeKey.update).mockResolvedValueOnce(updatedKey); + + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValueOnce([ + { id: "contact1", contactId: "contact1" }, + { id: "contact2", contactId: "contact2" }, + ]); + + const result = await updateContactAttributeKey("cak123", mockUpdateInput); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data).toEqual(updatedKey); + } + + expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({ + id: "cak123", + environmentId: mockContactAttributeKey.environmentId, + key: mockUpdateInput.key, + }); + }); + + test("returns not_found if record does not exist", async () => { + vi.mocked(prisma.contactAttributeKey.update).mockRejectedValueOnce(prismaNotFoundError); + + const result = await updateContactAttributeKey("cak999", mockUpdateInput); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + }); + + test("returns conflict error if key already exists", async () => { + vi.mocked(prisma.contactAttributeKey.update).mockRejectedValueOnce(prismaUniqueConstraintError); + + const result = await updateContactAttributeKey("cak123", mockUpdateInput); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "conflict", + details: [ + { field: "contactAttributeKey", issue: 'Contact attribute key with "email" already exists' }, + ], + }); + } + }); + + test("returns internal_server_error if other error occurs", async () => { + vi.mocked(prisma.contactAttributeKey.update).mockRejectedValueOnce(new Error("Unknown error")); + + const result = await updateContactAttributeKey("cak123", mockUpdateInput); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: "Unknown error" }], + }); + } + }); +}); + +describe("deleteContactAttributeKey", () => { + test("returns ok on successful delete", async () => { + vi.mocked(prisma.contactAttributeKey.delete).mockResolvedValueOnce(mockContactAttributeKey); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValueOnce([ + { id: "contact1", contactId: "contact1" }, + { id: "contact2", contactId: "contact2" }, + ]); + const result = await deleteContactAttributeKey("cak123"); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data).toEqual(mockContactAttributeKey); + } + + expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({ + id: "cak123", + environmentId: mockContactAttributeKey.environmentId, + key: mockContactAttributeKey.key, + }); + }); + + test("returns not_found if record does not exist", async () => { + vi.mocked(prisma.contactAttributeKey.delete).mockRejectedValueOnce(prismaNotFoundError); + + const result = await deleteContactAttributeKey("cak999"); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + }); + + test("returns internal_server_error on other errors", async () => { + vi.mocked(prisma.contactAttributeKey.delete).mockRejectedValueOnce(new Error("Delete error")); + + const result = await deleteContactAttributeKey("cak123"); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: "Delete error" }], + }); + } + }); +}); diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts new file mode 100644 index 0000000000..060682b026 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts @@ -0,0 +1,131 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { + deleteContactAttributeKey, + getContactAttributeKey, + updateContactAttributeKey, +} from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/lib/contact-attribute-key"; +import { + ZContactAttributeKeyIdSchema, + ZContactAttributeKeyUpdateSchema, +} from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys"; +import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; +import { NextRequest } from "next/server"; +import { z } from "zod"; + +export const GET = async ( + request: NextRequest, + props: { params: Promise<{ contactAttributeKeyId: string }> } +) => + authenticatedApiClient({ + request, + schemas: { + params: z.object({ contactAttributeKeyId: ZContactAttributeKeyIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput }) => { + const { params } = parsedInput; + + const res = await getContactAttributeKey(params.contactAttributeKeyId); + + if (!res.ok) { + return handleApiError(request, res.error); + } + + if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "GET")) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "environment", issue: "unauthorized" }], + }); + } + + return responses.successResponse(res); + }, + }); + +export const PUT = async ( + request: NextRequest, + props: { params: Promise<{ contactAttributeKeyId: string }> } +) => + authenticatedApiClient({ + request, + schemas: { + params: z.object({ contactAttributeKeyId: ZContactAttributeKeyIdSchema }), + body: ZContactAttributeKeyUpdateSchema, + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput }) => { + const { params, body } = parsedInput; + + const res = await getContactAttributeKey(params.contactAttributeKeyId); + + if (!res.ok) { + return handleApiError(request, res.error); + } + if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "PUT")) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "environment", issue: "unauthorized" }], + }); + } + + if (res.data.isUnique) { + return handleApiError(request, { + type: "bad_request", + details: [{ field: "contactAttributeKey", issue: "cannot update unique contact attribute key" }], + }); + } + + const updatedContactAttributeKey = await updateContactAttributeKey(params.contactAttributeKeyId, body); + + if (!updatedContactAttributeKey.ok) { + return handleApiError(request, updatedContactAttributeKey.error); + } + + return responses.successResponse(updatedContactAttributeKey); + }, + }); + +export const DELETE = async ( + request: NextRequest, + props: { params: Promise<{ contactAttributeKeyId: string }> } +) => + authenticatedApiClient({ + request, + schemas: { + params: z.object({ contactAttributeKeyId: ZContactAttributeKeyIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput }) => { + const { params } = parsedInput; + + const res = await getContactAttributeKey(params.contactAttributeKeyId); + + if (!res.ok) { + return handleApiError(request, res.error); + } + + if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "DELETE")) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "environment", issue: "unauthorized" }], + }); + } + + if (res.data.isUnique) { + return handleApiError(request, { + type: "bad_request", + details: [{ field: "contactAttributeKey", issue: "cannot delete unique contact attribute key" }], + }); + } + + const deletedContactAttributeKey = await deleteContactAttributeKey(params.contactAttributeKeyId); + + if (!deletedContactAttributeKey.ok) { + return handleApiError(request, deletedContactAttributeKey.error); + } + + return responses.successResponse(deletedContactAttributeKey); + }, + }); diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys.ts new file mode 100644 index 0000000000..b855994b92 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { extendZodWithOpenApi } from "zod-openapi"; +import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys"; + +extendZodWithOpenApi(z); + +export const ZContactAttributeKeyIdSchema = z + .string() + .cuid2() + .openapi({ + ref: "contactAttributeKeyId", + description: "The ID of the contact attribute key", + param: { + name: "id", + in: "path", + }, + }); + +export const ZContactAttributeKeyUpdateSchema = ZContactAttributeKey.pick({ + name: true, + description: true, + key: true, +}).openapi({ + ref: "contactAttributeKeyUpdate", + description: "A contact attribute key to update.", +}); + +export type TContactAttributeKeyUpdateSchema = z.infer; diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/lib/contact-attribute-key.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/contact-attribute-key.ts new file mode 100644 index 0000000000..d89c88e21c --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/contact-attribute-key.ts @@ -0,0 +1,105 @@ +import { cache } from "@/lib/cache"; +import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key"; +import { getContactAttributeKeysQuery } from "@/modules/api/v2/management/contact-attribute-keys/lib/utils"; +import { + TContactAttributeKeyInput, + TGetContactAttributeKeysFilter, +} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; +import { ContactAttributeKey, Prisma } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getContactAttributeKeys = reactCache( + async (environmentIds: string[], params: TGetContactAttributeKeysFilter) => + cache( + async (): Promise, ApiErrorResponseV2>> => { + try { + const query = getContactAttributeKeysQuery(environmentIds, params); + + const [keys, count] = await prisma.$transaction([ + prisma.contactAttributeKey.findMany({ + ...query, + }), + prisma.contactAttributeKey.count({ + where: query.where, + }), + ]); + + return ok({ data: keys, meta: { total: count, limit: params.limit, offset: params.skip } }); + } catch (error) { + return err({ + type: "internal_server_error", + details: [{ field: "contactAttributeKeys", issue: error.message }], + }); + } + }, + [`management-getContactAttributeKeys-${environmentIds.join(",")}-${JSON.stringify(params)}`], + { + tags: environmentIds.map((environmentId) => + contactAttributeKeyCache.tag.byEnvironmentId(environmentId) + ), + } + )() +); + +export const createContactAttributeKey = async ( + contactAttributeKey: TContactAttributeKeyInput +): Promise> => { + const { environmentId, name, description, key } = contactAttributeKey; + + try { + const prismaData: Prisma.ContactAttributeKeyCreateInput = { + environment: { + connect: { + id: environmentId, + }, + }, + name, + description, + key, + }; + + const createdContactAttributeKey = await prisma.contactAttributeKey.create({ + data: prismaData, + }); + + contactAttributeKeyCache.revalidate({ + environmentId: createdContactAttributeKey.environmentId, + key: createdContactAttributeKey.key, + }); + + return ok(createdContactAttributeKey); + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + if ( + error.code === PrismaErrorType.RecordDoesNotExist || + error.code === PrismaErrorType.RelatedRecordDoesNotExist + ) { + return err({ + type: "not_found", + details: [{ field: "contactAttributeKey", issue: "not found" }], + }); + } + if (error.code === PrismaErrorType.UniqueConstraintViolation) { + return err({ + type: "conflict", + details: [ + { + field: "contactAttributeKey", + issue: `Contact attribute key with "${contactAttributeKey.key}" already exists`, + }, + ], + }); + } + } + return err({ + type: "internal_server_error", + details: [{ field: "contactAttributeKey", issue: error.message }], + }); + } +}; diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/lib/openapi.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/openapi.ts index d33cdd8dd4..c8d2094059 100644 --- a/apps/web/modules/api/v2/management/contact-attribute-keys/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/openapi.ts @@ -8,9 +8,9 @@ import { ZGetContactAttributeKeysFilter, } from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; import { managementServer } from "@/modules/api/v2/management/lib/openapi"; -import { z } from "zod"; +import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/types/openapi-response"; import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; -import { ZContactAttributeKey } from "@formbricks/types/contact-attribute-key"; +import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys"; export const getContactAttributeKeysEndpoint: ZodOpenApiOperationObject = { operationId: "getContactAttributeKeys", @@ -18,14 +18,14 @@ export const getContactAttributeKeysEndpoint: ZodOpenApiOperationObject = { description: "Gets contact attribute keys from the database.", tags: ["Management API > Contact Attribute Keys"], requestParams: { - query: ZGetContactAttributeKeysFilter, + query: ZGetContactAttributeKeysFilter.sourceType(), }, responses: { "200": { description: "Contact attribute keys retrieved successfully.", content: { "application/json": { - schema: z.array(ZContactAttributeKey), + schema: responseWithMetaSchema(makePartialSchema(ZContactAttributeKey)), }, }, }, @@ -49,6 +49,11 @@ export const createContactAttributeKeyEndpoint: ZodOpenApiOperationObject = { responses: { "201": { description: "Contact attribute key created successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZContactAttributeKey), + }, + }, }, }, }; diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/contact-attribute-key.test.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/contact-attribute-key.test.ts new file mode 100644 index 0000000000..9345ed3d32 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/contact-attribute-key.test.ts @@ -0,0 +1,166 @@ +import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key"; +import { + TContactAttributeKeyInput, + TGetContactAttributeKeysFilter, +} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; +import { ContactAttributeKey } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { describe, expect, test, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { createContactAttributeKey, getContactAttributeKeys } from "../contact-attribute-key"; + +vi.mock("@formbricks/database", () => ({ + prisma: { + $transaction: vi.fn(), + contactAttributeKey: { + findMany: vi.fn(), + count: vi.fn(), + create: vi.fn(), + }, + }, +})); +vi.mock("@/lib/cache/contact-attribute-key", () => ({ + contactAttributeKeyCache: { + revalidate: vi.fn(), + tag: { + byEnvironmentId: vi.fn(), + }, + }, +})); + +describe("getContactAttributeKeys", () => { + const environmentIds = ["env1", "env2"]; + const params: TGetContactAttributeKeysFilter = { + limit: 10, + skip: 0, + order: "asc", + sortBy: "createdAt", + }; + const fakeContactAttributeKeys = [ + { id: "key1", environmentId: "env1", name: "Key One", key: "keyOne" }, + { id: "key2", environmentId: "env1", name: "Key Two", key: "keyTwo" }, + ]; + const count = fakeContactAttributeKeys.length; + + test("returns ok response with contact attribute keys and meta", async () => { + vi.mocked(prisma.$transaction).mockResolvedValueOnce([fakeContactAttributeKeys, count]); + + const result = await getContactAttributeKeys(environmentIds, params); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data.data).toEqual(fakeContactAttributeKeys); + expect(result.data.meta).toEqual({ + total: count, + limit: params.limit, + offset: params.skip, + }); + } + }); + + test("returns error when prisma.$transaction throws", async () => { + vi.mocked(prisma.$transaction).mockRejectedValueOnce(new Error("Test error")); + + const result = await getContactAttributeKeys(environmentIds, params); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error?.type).toEqual("internal_server_error"); + } + }); +}); + +describe("createContactAttributeKey", () => { + const inputContactAttributeKey: TContactAttributeKeyInput = { + environmentId: "env1", + name: "New Contact Attribute Key", + key: "newKey", + description: "Description for new key", + }; + + const createdContactAttributeKey: ContactAttributeKey = { + id: "key100", + environmentId: inputContactAttributeKey.environmentId, + name: inputContactAttributeKey.name, + key: inputContactAttributeKey.key, + description: inputContactAttributeKey.description, + isUnique: false, + type: "custom", + createdAt: new Date(), + updatedAt: new Date(), + }; + + test("creates a contact attribute key and revalidates cache", async () => { + vi.mocked(prisma.contactAttributeKey.create).mockResolvedValueOnce(createdContactAttributeKey); + + const result = await createContactAttributeKey(inputContactAttributeKey); + expect(prisma.contactAttributeKey.create).toHaveBeenCalled(); + expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({ + environmentId: createdContactAttributeKey.environmentId, + key: createdContactAttributeKey.key, + }); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data).toEqual(createdContactAttributeKey); + } + }); + + test("returns error when creation fails", async () => { + vi.mocked(prisma.contactAttributeKey.create).mockRejectedValueOnce(new Error("Creation failed")); + + const result = await createContactAttributeKey(inputContactAttributeKey); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error.type).toEqual("internal_server_error"); + } + }); + + test("returns conflict error when key already exists", async () => { + const errToThrow = new PrismaClientKnownRequestError("Mock error message", { + code: PrismaErrorType.UniqueConstraintViolation, + clientVersion: "0.0.1", + }); + vi.mocked(prisma.contactAttributeKey.create).mockRejectedValueOnce(errToThrow); + + const result = await createContactAttributeKey(inputContactAttributeKey); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "conflict", + details: [ + { + field: "contactAttributeKey", + issue: 'Contact attribute key with "newKey" already exists', + }, + ], + }); + } + }); + + test("returns not found error when related record does not exist", async () => { + const errToThrow = new PrismaClientKnownRequestError("Mock error message", { + code: PrismaErrorType.RelatedRecordDoesNotExist, + clientVersion: "0.0.1", + }); + vi.mocked(prisma.contactAttributeKey.create).mockRejectedValueOnce(errToThrow); + + const result = await createContactAttributeKey(inputContactAttributeKey); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error).toStrictEqual({ + type: "not_found", + details: [ + { + field: "contactAttributeKey", + issue: "not found", + }, + ], + }); + } + }); +}); diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/utils.test.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/utils.test.ts new file mode 100644 index 0000000000..4146b1f677 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/tests/utils.test.ts @@ -0,0 +1,106 @@ +import { TGetContactAttributeKeysFilter } from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { getContactAttributeKeysQuery } from "../utils"; + +describe("getContactAttributeKeysQuery", () => { + const environmentId = "env-123"; + const baseParams: TGetContactAttributeKeysFilter = { + limit: 10, + skip: 0, + order: "asc", + sortBy: "createdAt", + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("returns query with environmentId in array when no params are provided", () => { + const environmentIds = ["env-1", "env-2"]; + const result = getContactAttributeKeysQuery(environmentIds); + + expect(result).toEqual({ + where: { + environmentId: { + in: environmentIds, + }, + }, + }); + }); + + test("applies common filters when provided", () => { + const environmentIds = ["env-1", "env-2"]; + const params: TGetContactAttributeKeysFilter = { + ...baseParams, + environmentId, + }; + const result = getContactAttributeKeysQuery(environmentIds, params); + + expect(result).toEqual({ + where: { + environmentId: { + in: environmentIds, + }, + }, + take: 10, + orderBy: { + createdAt: "asc", + }, + }); + }); + + test("applies date filters when provided", () => { + const environmentIds = ["env-1", "env-2"]; + const startDate = new Date("2023-01-01"); + const endDate = new Date("2023-12-31"); + + const params: TGetContactAttributeKeysFilter = { + ...baseParams, + environmentId, + startDate, + endDate, + }; + const result = getContactAttributeKeysQuery(environmentIds, params); + + expect(result).toEqual({ + where: { + environmentId: { + in: environmentIds, + }, + createdAt: { + gte: startDate, + lte: endDate, + }, + }, + take: 10, + orderBy: { + createdAt: "asc", + }, + }); + }); + + test("handles multiple filter parameters correctly", () => { + const environmentIds = ["env-1", "env-2"]; + const params: TGetContactAttributeKeysFilter = { + environmentId, + limit: 5, + skip: 10, + sortBy: "updatedAt", + order: "asc", + }; + const result = getContactAttributeKeysQuery(environmentIds, params); + + expect(result).toEqual({ + where: { + environmentId: { + in: environmentIds, + }, + }, + take: 5, + skip: 10, + orderBy: { + updatedAt: "asc", + }, + }); + }); +}); diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/lib/utils.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/utils.ts new file mode 100644 index 0000000000..5d4e1881c4 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/lib/utils.ts @@ -0,0 +1,26 @@ +import { TGetContactAttributeKeysFilter } from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; +import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; +import { Prisma } from "@prisma/client"; + +export const getContactAttributeKeysQuery = ( + environmentIds: string[], + params?: TGetContactAttributeKeysFilter +): Prisma.ContactAttributeKeyFindManyArgs => { + let query: Prisma.ContactAttributeKeyFindManyArgs = { + where: { + environmentId: { + in: environmentIds, + }, + }, + }; + + if (!params) return query; + + const baseFilter = pickCommonFilter(params); + + if (baseFilter) { + query = buildCommonFilterQuery(query, baseFilter); + } + + return query; +}; diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts new file mode 100644 index 0000000000..eb97fa01d4 --- /dev/null +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts @@ -0,0 +1,73 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { + createContactAttributeKey, + getContactAttributeKeys, +} from "@/modules/api/v2/management/contact-attribute-keys/lib/contact-attribute-key"; +import { + ZContactAttributeKeyInput, + ZGetContactAttributeKeysFilter, +} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys"; +import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; +import { NextRequest } from "next/server"; + +export const GET = async (request: NextRequest) => + authenticatedApiClient({ + request, + schemas: { + query: ZGetContactAttributeKeysFilter.sourceType(), + }, + handler: async ({ authentication, parsedInput }) => { + const { query } = parsedInput; + + let environmentIds: string[] = []; + + if (query.environmentId) { + if (!hasPermission(authentication.environmentPermissions, query.environmentId, "GET")) { + return handleApiError(request, { + type: "unauthorized", + }); + } + environmentIds = [query.environmentId]; + } else { + environmentIds = authentication.environmentPermissions.map((permission) => permission.environmentId); + } + + const res = await getContactAttributeKeys(environmentIds, query); + + if (!res.ok) { + return handleApiError(request, res.error); + } + + return responses.successResponse(res.data); + }, + }); + +export const POST = async (request: NextRequest) => + authenticatedApiClient({ + request, + schemas: { + body: ZContactAttributeKeyInput, + }, + handler: async ({ authentication, parsedInput }) => { + const { body } = parsedInput; + + if (!hasPermission(authentication.environmentPermissions, body.environmentId, "POST")) { + return handleApiError(request, { + type: "forbidden", + details: [ + { field: "environmentId", issue: "does not have permission to create contact attribute key" }, + ], + }); + } + + const createContactAttributeKeyResult = await createContactAttributeKey(body); + + if (!createContactAttributeKeyResult.ok) { + return handleApiError(request, createContactAttributeKeyResult.error); + } + + return responses.createdResponse(createContactAttributeKeyResult); + }, + }); diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys.ts index 24e02f2ee4..386d966c53 100644 --- a/apps/web/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys.ts +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys.ts @@ -1,18 +1,13 @@ +import { ZGetFilter } from "@/modules/api/v2/types/api-filter"; import { z } from "zod"; import { extendZodWithOpenApi } from "zod-openapi"; import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys"; extendZodWithOpenApi(z); -export const ZGetContactAttributeKeysFilter = z - .object({ - limit: z.coerce.number().positive().min(1).max(100).optional().default(10), - skip: z.coerce.number().nonnegative().optional().default(0), - sortBy: z.enum(["createdAt", "updatedAt"]).optional().default("createdAt"), - order: z.enum(["asc", "desc"]).optional().default("desc"), - startDate: z.coerce.date().optional(), - endDate: z.coerce.date().optional(), - }) +export const ZGetContactAttributeKeysFilter = ZGetFilter.extend({ + environmentId: z.string().cuid2().optional().describe("The environment ID to filter by"), +}) .refine( (data) => { if (data.startDate && data.endDate && data.startDate > data.endDate) { @@ -23,13 +18,15 @@ export const ZGetContactAttributeKeysFilter = z { message: "startDate must be before endDate", } - ); + ) + .describe("Filter for retrieving contact attribute keys"); + +export type TGetContactAttributeKeysFilter = z.infer; export const ZContactAttributeKeyInput = ZContactAttributeKey.pick({ key: true, name: true, description: true, - type: true, environmentId: true, }).openapi({ ref: "contactAttributeKeyInput", diff --git a/apps/web/modules/api/v2/management/lib/services.ts b/apps/web/modules/api/v2/management/lib/services.ts index 9420165725..7598483615 100644 --- a/apps/web/modules/api/v2/management/lib/services.ts +++ b/apps/web/modules/api/v2/management/lib/services.ts @@ -1,12 +1,12 @@ "use server"; +import { cache } from "@/lib/cache"; +import { responseCache } from "@/lib/response/cache"; +import { responseNoteCache } from "@/lib/responseNote/cache"; +import { surveyCache } from "@/lib/survey/cache"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const fetchEnvironmentId = reactCache(async (id: string, isResponseId: boolean) => diff --git a/apps/web/modules/api/v2/management/lib/tests/helper.test.ts b/apps/web/modules/api/v2/management/lib/tests/helper.test.ts index 845c61cd15..e2558706b5 100644 --- a/apps/web/modules/api/v2/management/lib/tests/helper.test.ts +++ b/apps/web/modules/api/v2/management/lib/tests/helper.test.ts @@ -1,7 +1,7 @@ import { fetchEnvironmentIdFromSurveyIds } from "@/modules/api/v2/management/lib/services"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { createId } from "@paralleldrive/cuid2"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { err, ok } from "@formbricks/types/error-handlers"; import { getEnvironmentId, getEnvironmentIdFromSurveyIds } from "../helper"; import { fetchEnvironmentId } from "../services"; @@ -12,7 +12,7 @@ vi.mock("../services", () => ({ })); describe("Tests for getEnvironmentId", () => { - it("should return environmentId for surveyId", async () => { + test("should return environmentId for surveyId", async () => { vi.mocked(fetchEnvironmentId).mockResolvedValue(ok({ environmentId: "env-id" })); const result = await getEnvironmentId("survey-id", false); @@ -22,7 +22,7 @@ describe("Tests for getEnvironmentId", () => { } }); - it("should return environmentId for responseId", async () => { + test("should return environmentId for responseId", async () => { vi.mocked(fetchEnvironmentId).mockResolvedValue(ok({ environmentId: "env-id" })); const result = await getEnvironmentId("response-id", true); @@ -32,7 +32,7 @@ describe("Tests for getEnvironmentId", () => { } }); - it("should return error if getSurveyAndEnvironmentId fails", async () => { + test("should return error if getSurveyAndEnvironmentId fails", async () => { vi.mocked(fetchEnvironmentId).mockResolvedValue( err({ type: "not_found" } as unknown as ApiErrorResponseV2) ); @@ -49,7 +49,7 @@ describe("getEnvironmentIdFromSurveyIds", () => { const envId1 = createId(); const envId2 = createId(); - it("returns the common environment id when all survey ids are in the same environment", async () => { + test("returns the common environment id when all survey ids are in the same environment", async () => { vi.mocked(fetchEnvironmentIdFromSurveyIds).mockResolvedValueOnce({ ok: true, data: [envId1, envId1], @@ -58,7 +58,7 @@ describe("getEnvironmentIdFromSurveyIds", () => { expect(result).toEqual(ok(envId1)); }); - it("returns error when surveys are not in the same environment", async () => { + test("returns error when surveys are not in the same environment", async () => { vi.mocked(fetchEnvironmentIdFromSurveyIds).mockResolvedValueOnce({ ok: true, data: [envId1, envId2], @@ -73,7 +73,7 @@ describe("getEnvironmentIdFromSurveyIds", () => { } }); - it("returns error when API call fails", async () => { + test("returns error when API call fails", async () => { const apiError = { type: "server_error", details: [{ field: "api", issue: "failed" }], diff --git a/apps/web/modules/api/v2/management/lib/utils.ts b/apps/web/modules/api/v2/management/lib/utils.ts index 105cda6122..36d46ce1a1 100644 --- a/apps/web/modules/api/v2/management/lib/utils.ts +++ b/apps/web/modules/api/v2/management/lib/utils.ts @@ -14,7 +14,8 @@ type HasFindMany = | Prisma.ResponseFindManyArgs | Prisma.TeamFindManyArgs | Prisma.ProjectTeamFindManyArgs - | Prisma.UserFindManyArgs; + | Prisma.UserFindManyArgs + | Prisma.ContactAttributeKeyFindManyArgs; export function buildCommonFilterQuery(query: T, params: TGetFilter): T { const { limit, skip, sortBy, order, startDate, endDate } = params || {}; diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/display.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/display.ts index b13245d343..5e959f85f0 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/display.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/display.ts @@ -1,8 +1,8 @@ +import { displayCache } from "@/lib/display/cache"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { displayCache } from "@formbricks/lib/display/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const deleteDisplay = async (displayId: string): Promise> => { diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts index a9d890fe28..9634ae6b89 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts @@ -1,3 +1,6 @@ +import { cache } from "@/lib/cache"; +import { responseCache } from "@/lib/response/cache"; +import { responseNoteCache } from "@/lib/responseNote/cache"; import { deleteDisplay } from "@/modules/api/v2/management/responses/[responseId]/lib/display"; import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[responseId]/lib/survey"; import { findAndDeleteUploadedFilesInResponse } from "@/modules/api/v2/management/responses/[responseId]/lib/utils"; @@ -9,9 +12,6 @@ import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { cache } from "@formbricks/lib/cache"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getResponse = reactCache(async (responseId: string) => diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/survey.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/survey.ts index b0dd4b2be9..7828f708eb 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/survey.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/survey.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Survey } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getSurveyQuestions = reactCache(async (surveyId: string) => diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts index b1908799b8..a19b040c4e 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/utils.test.ts @@ -1,6 +1,6 @@ import { environmentId, fileUploadQuestion, openTextQuestion, responseData } from "./__mocks__/utils.mock"; +import { deleteFile } from "@/lib/storage/service"; import { beforeEach, describe, expect, test, vi } from "vitest"; -import { deleteFile } from "@formbricks/lib/storage/service"; import { logger } from "@formbricks/logger"; import { okVoid } from "@formbricks/types/error-handlers"; import { findAndDeleteUploadedFilesInResponse } from "../utils"; @@ -11,7 +11,7 @@ vi.mock("@formbricks/logger", () => ({ }, })); -vi.mock("@formbricks/lib/storage/service", () => ({ +vi.mock("@/lib/storage/service", () => ({ deleteFile: vi.fn(), })); diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts index 11655b2e09..b76fbd62f2 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/utils.ts @@ -1,6 +1,6 @@ +import { deleteFile } from "@/lib/storage/service"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Response, Survey } from "@prisma/client"; -import { deleteFile } from "@formbricks/lib/storage/service"; import { logger } from "@formbricks/logger"; import { Result, okVoid } from "@formbricks/types/error-handlers"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; diff --git a/apps/web/modules/api/v2/management/responses/lib/organization.ts b/apps/web/modules/api/v2/management/responses/lib/organization.ts index 334f892e02..232055978d 100644 --- a/apps/web/modules/api/v2/management/responses/lib/organization.ts +++ b/apps/web/modules/api/v2/management/responses/lib/organization.ts @@ -1,9 +1,10 @@ +import { cache } from "@/lib/cache"; +import { organizationCache } from "@/lib/organization/cache"; +import { getBillingPeriodStartDate } from "@/lib/utils/billing"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Organization } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { organizationCache } from "@formbricks/lib/organization/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getOrganizationIdFromEnvironmentId = reactCache(async (environmentId: string) => @@ -133,22 +134,7 @@ export const getMonthlyOrganizationResponseCount = reactCache(async (organizatio } // Determine the start date based on the plan type - let startDate: Date; - - if (billing.data.plan === "free") { - // For free plans, use the first day of the current calendar month - const now = new Date(); - startDate = new Date(now.getFullYear(), now.getMonth(), 1); - } else { - // For other plans, use the periodStart from billing - if (!billing.data.periodStart) { - return err({ - type: "internal_server_error", - details: [{ field: "organization", issue: "billing period start is not set" }], - }); - } - startDate = billing.data.periodStart; - } + const startDate = getBillingPeriodStartDate(billing.data); // Get all environment IDs for the organization const environmentIdsResult = await getAllEnvironmentsFromOrganizationId(organizationId); diff --git a/apps/web/modules/api/v2/management/responses/lib/response.ts b/apps/web/modules/api/v2/management/responses/lib/response.ts index 2eb80bf9ed..c64fb607cc 100644 --- a/apps/web/modules/api/v2/management/responses/lib/response.ts +++ b/apps/web/modules/api/v2/management/responses/lib/response.ts @@ -1,4 +1,10 @@ import "server-only"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer"; +import { responseCache } from "@/lib/response/cache"; +import { calculateTtcTotal } from "@/lib/response/utils"; +import { responseNoteCache } from "@/lib/responseNote/cache"; +import { captureTelemetry } from "@/lib/telemetry"; import { getMonthlyOrganizationResponseCount, getOrganizationBilling, @@ -10,12 +16,6 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; import { Prisma, Response } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { sendPlanLimitsReachedEventToPosthogWeekly } from "@formbricks/lib/posthogServer"; -import { responseCache } from "@formbricks/lib/response/cache"; -import { calculateTtcTotal } from "@formbricks/lib/response/utils"; -import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; import { logger } from "@formbricks/logger"; import { Result, err, ok } from "@formbricks/types/error-handlers"; diff --git a/apps/web/modules/api/v2/management/responses/lib/tests/response.test.ts b/apps/web/modules/api/v2/management/responses/lib/tests/response.test.ts index 524749896c..ddeda79802 100644 --- a/apps/web/modules/api/v2/management/responses/lib/tests/response.test.ts +++ b/apps/web/modules/api/v2/management/responses/lib/tests/response.test.ts @@ -9,6 +9,7 @@ import { responseInputWithoutDisplay, responseInputWithoutTtc, } from "./__mocks__/response.mock"; +import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer"; import { getMonthlyOrganizationResponseCount, getOrganizationBilling, @@ -16,11 +17,10 @@ import { } from "@/modules/api/v2/management/responses/lib/organization"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { sendPlanLimitsReachedEventToPosthogWeekly } from "@formbricks/lib/posthogServer"; import { err, ok } from "@formbricks/types/error-handlers"; import { createResponse, getResponses } from "../response"; -vi.mock("@formbricks/lib/posthogServer", () => ({ +vi.mock("@/lib/posthogServer", () => ({ sendPlanLimitsReachedEventToPosthogWeekly: vi.fn().mockResolvedValue(undefined), })); @@ -40,7 +40,7 @@ vi.mock("@formbricks/database", () => ({ }, })); -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ IS_FORMBRICKS_CLOUD: true, IS_PRODUCTION: false, })); diff --git a/apps/web/modules/api/v2/management/responses/lib/tests/utils.test.ts b/apps/web/modules/api/v2/management/responses/lib/tests/utils.test.ts index 14c0ab4fce..4c4331b6a2 100644 --- a/apps/web/modules/api/v2/management/responses/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/management/responses/lib/tests/utils.test.ts @@ -1,7 +1,7 @@ import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; import { TGetResponsesFilter } from "@/modules/api/v2/management/responses/types/responses"; import { Prisma } from "@prisma/client"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { getResponsesQuery } from "../utils"; vi.mock("@/modules/api/v2/management/lib/utils", () => ({ @@ -10,17 +10,17 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({ })); describe("getResponsesQuery", () => { - it("adds surveyId to where clause if provided", () => { + test("adds surveyId to where clause if provided", () => { const result = getResponsesQuery(["env-id"], { surveyId: "survey123" } as TGetResponsesFilter); expect(result?.where?.surveyId).toBe("survey123"); }); - it("adds contactId to where clause if provided", () => { + test("adds contactId to where clause if provided", () => { const result = getResponsesQuery(["env-id"], { contactId: "contact123" } as TGetResponsesFilter); expect(result?.where?.contactId).toBe("contact123"); }); - it("calls pickCommonFilter & buildCommonFilterQuery with correct arguments", () => { + test("calls pickCommonFilter & buildCommonFilterQuery with correct arguments", () => { vi.mocked(pickCommonFilter).mockReturnValueOnce({ someFilter: true } as any); vi.mocked(buildCommonFilterQuery).mockReturnValueOnce({ where: { combined: true } as any }); diff --git a/apps/web/modules/api/v2/management/responses/route.ts b/apps/web/modules/api/v2/management/responses/route.ts index 43961806ec..9a5833930f 100644 --- a/apps/web/modules/api/v2/management/responses/route.ts +++ b/apps/web/modules/api/v2/management/responses/route.ts @@ -81,6 +81,6 @@ export const POST = async (request: Request) => return handleApiError(request, createResponseResult.error); } - return responses.successResponse({ data: createResponseResult.data }); + return responses.createdResponse({ data: createResponseResult.data }); }, }); diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/contacts.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/contacts.ts index 470709b1ea..ce19a262c4 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/contacts.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/contacts.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; import { contactCache } from "@/lib/cache/contact"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Contact } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getContact = reactCache(async (contactId: string, environmentId: string) => diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/response.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/response.ts index f1056bbd32..fc9f84252f 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/response.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/response.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; +import { responseCache } from "@/lib/response/cache"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Response } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { responseCache } from "@formbricks/lib/response/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getResponse = reactCache(async (contactId: string, surveyId: string) => diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/surveys.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/surveys.ts index 1096154077..03dcc32bad 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/surveys.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/surveys.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Survey } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getSurvey = reactCache(async (surveyId: string) => diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact-attribute-key.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact-attribute-key.ts index ca7468f542..e56a02abe4 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact-attribute-key.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact-attribute-key.ts @@ -1,8 +1,8 @@ +import { cache } from "@/lib/cache"; import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getContactAttributeKeys = reactCache((environmentId: string) => diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact.ts index 2dcaea1913..ef81a7f119 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact.ts @@ -1,3 +1,6 @@ +import { cache } from "@/lib/cache"; +import { segmentCache } from "@/lib/cache/segment"; +import { surveyCache } from "@/lib/survey/cache"; import { getContactAttributeKeys } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact-attribute-key"; import { getSegment } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/segment"; import { getSurvey } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/surveys"; @@ -7,9 +10,6 @@ import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; import { segmentFilterToPrismaQuery } from "@/modules/ee/contacts/segments/lib/filter/prisma-query"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { segmentCache } from "@formbricks/lib/cache/segment"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { logger } from "@formbricks/logger"; import { Result, err, ok } from "@formbricks/types/error-handlers"; diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/segment.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/segment.ts index 0fe206a16a..3e4b73c1a8 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/segment.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/segment.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; +import { segmentCache } from "@/lib/cache/segment"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Segment } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { segmentCache } from "@formbricks/lib/cache/segment"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getSegment = reactCache(async (segmentId: string) => diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/surveys.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/surveys.ts index 8347d018fc..7ab4529e8a 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/surveys.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/surveys.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Survey } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getSurvey = reactCache(async (surveyId: string) => diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/segment.test.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/segment.test.ts index 0c8ffab670..6c7920bc5a 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/segment.test.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/segment.test.ts @@ -1,8 +1,8 @@ +import { cache } from "@/lib/cache"; +import { segmentCache } from "@/lib/cache/segment"; import { Segment } from "@prisma/client"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { segmentCache } from "@formbricks/lib/cache/segment"; import { getSegment } from "../segment"; // Mock dependencies @@ -14,11 +14,11 @@ vi.mock("@formbricks/database", () => ({ }, })); -vi.mock("@formbricks/lib/cache", () => ({ +vi.mock("@/lib/cache", () => ({ cache: vi.fn((fn) => fn), })); -vi.mock("@formbricks/lib/cache/segment", () => ({ +vi.mock("@/lib/cache/segment", () => ({ segmentCache: { tag: { byId: vi.fn((id) => `segment-${id}`), diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/surveys.test.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/surveys.test.ts index 3559dc580c..042b742046 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/surveys.test.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/surveys.test.ts @@ -1,7 +1,7 @@ +import { cache } from "@/lib/cache"; +import { surveyCache } from "@/lib/survey/cache"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { surveyCache } from "@formbricks/lib/survey/cache"; import { getSurvey } from "../surveys"; // Mock dependencies @@ -13,11 +13,11 @@ vi.mock("@formbricks/database", () => ({ }, })); -vi.mock("@formbricks/lib/cache", () => ({ +vi.mock("@/lib/cache", () => ({ cache: vi.fn((fn) => fn), })); -vi.mock("@formbricks/lib/survey/cache", () => ({ +vi.mock("@/lib/survey/cache", () => ({ surveyCache: { tag: { byId: vi.fn((id) => `survey-${id}`), diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts index a11645713e..3b9f674004 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts @@ -1,3 +1,4 @@ +import { cache } from "@/lib/cache"; import { webhookCache } from "@/lib/cache/webhook"; import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; @@ -6,7 +7,6 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { cache } from "@formbricks/lib/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getWebhook = async (webhookId: string) => diff --git a/apps/web/modules/api/v2/management/webhooks/lib/tests/utils.test.ts b/apps/web/modules/api/v2/management/webhooks/lib/tests/utils.test.ts index 278428e5b6..c95bede10a 100644 --- a/apps/web/modules/api/v2/management/webhooks/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/management/webhooks/lib/tests/utils.test.ts @@ -1,6 +1,6 @@ import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; import { TGetWebhooksFilter } from "@/modules/api/v2/management/webhooks/types/webhooks"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { getWebhooksQuery } from "../utils"; vi.mock("@/modules/api/v2/management/lib/utils", () => ({ @@ -11,7 +11,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({ describe("getWebhooksQuery", () => { const environmentId = "env-123"; - it("adds surveyIds condition when provided", () => { + test("adds surveyIds condition when provided", () => { const params = { surveyIds: ["survey1"] } as TGetWebhooksFilter; const result = getWebhooksQuery([environmentId], params); expect(result).toBeDefined(); @@ -21,14 +21,14 @@ describe("getWebhooksQuery", () => { }); }); - it("calls pickCommonFilter and buildCommonFilterQuery when baseFilter is present", () => { + test("calls pickCommonFilter and buildCommonFilterQuery when baseFilter is present", () => { vi.mocked(pickCommonFilter).mockReturnValue({ someFilter: "test" } as any); getWebhooksQuery([environmentId], { surveyIds: ["survey1"] } as TGetWebhooksFilter); expect(pickCommonFilter).toHaveBeenCalled(); expect(buildCommonFilterQuery).toHaveBeenCalled(); }); - it("buildCommonFilterQuery is not called if no baseFilter is picked", () => { + test("buildCommonFilterQuery is not called if no baseFilter is picked", () => { vi.mocked(pickCommonFilter).mockReturnValue(undefined as any); getWebhooksQuery([environmentId], {} as any); expect(buildCommonFilterQuery).not.toHaveBeenCalled(); diff --git a/apps/web/modules/api/v2/management/webhooks/lib/tests/webhook.test.ts b/apps/web/modules/api/v2/management/webhooks/lib/tests/webhook.test.ts index b0e2104d9c..507e8e4a7b 100644 --- a/apps/web/modules/api/v2/management/webhooks/lib/tests/webhook.test.ts +++ b/apps/web/modules/api/v2/management/webhooks/lib/tests/webhook.test.ts @@ -1,9 +1,9 @@ import { webhookCache } from "@/lib/cache/webhook"; +import { captureTelemetry } from "@/lib/telemetry"; import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks"; import { WebhookSource } from "@prisma/client"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; import { createWebhook, getWebhooks } from "../webhook"; vi.mock("@formbricks/database", () => ({ @@ -21,7 +21,7 @@ vi.mock("@/lib/cache/webhook", () => ({ revalidate: vi.fn(), }, })); -vi.mock("@formbricks/lib/telemetry", () => ({ +vi.mock("@/lib/telemetry", () => ({ captureTelemetry: vi.fn(), })); @@ -37,7 +37,7 @@ describe("getWebhooks", () => { ]; const count = fakeWebhooks.length; - it("returns ok response with webhooks and meta", async () => { + test("returns ok response with webhooks and meta", async () => { vi.mocked(prisma.$transaction).mockResolvedValueOnce([fakeWebhooks, count]); const result = await getWebhooks(environmentId, params as TGetWebhooksFilter); @@ -53,7 +53,7 @@ describe("getWebhooks", () => { } }); - it("returns error when prisma.$transaction throws", async () => { + test("returns error when prisma.$transaction throws", async () => { vi.mocked(prisma.$transaction).mockRejectedValueOnce(new Error("Test error")); const result = await getWebhooks(environmentId, params as TGetWebhooksFilter); @@ -87,7 +87,7 @@ describe("createWebhook", () => { updatedAt: new Date(), }; - it("creates a webhook and revalidates cache", async () => { + test("creates a webhook and revalidates cache", async () => { vi.mocked(prisma.webhook.create).mockResolvedValueOnce(createdWebhook); const result = await createWebhook(inputWebhook); @@ -104,7 +104,7 @@ describe("createWebhook", () => { } }); - it("returns error when creation fails", async () => { + test("returns error when creation fails", async () => { vi.mocked(prisma.webhook.create).mockRejectedValueOnce(new Error("Creation failed")); const result = await createWebhook(inputWebhook); diff --git a/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts b/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts index 175c6660b8..7b1004525d 100644 --- a/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts +++ b/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts @@ -1,11 +1,11 @@ import { webhookCache } from "@/lib/cache/webhook"; +import { captureTelemetry } from "@/lib/telemetry"; 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"; import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; import { Prisma, Webhook } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getWebhooks = async ( diff --git a/apps/web/modules/api/v2/management/webhooks/route.ts b/apps/web/modules/api/v2/management/webhooks/route.ts index b18ed34a80..e34d3ef105 100644 --- a/apps/web/modules/api/v2/management/webhooks/route.ts +++ b/apps/web/modules/api/v2/management/webhooks/route.ts @@ -72,6 +72,6 @@ export const POST = async (request: NextRequest) => return handleApiError(request, createWebhookResult.error); } - return responses.successResponse(createWebhookResult); + return responses.createdResponse(createWebhookResult); }, }); diff --git a/apps/web/modules/api/v2/openapi-document.ts b/apps/web/modules/api/v2/openapi-document.ts index d099f912c8..dd9a34bfbc 100644 --- a/apps/web/modules/api/v2/openapi-document.ts +++ b/apps/web/modules/api/v2/openapi-document.ts @@ -1,4 +1,4 @@ -// import { contactAttributeKeyPaths } from "@/modules/api/v2/management/contact-attribute-keys/lib/openapi"; +import { contactAttributeKeyPaths } from "@/modules/api/v2/management/contact-attribute-keys/lib/openapi"; // import { contactAttributePaths } from "@/modules/api/v2/management/contact-attributes/lib/openapi"; // import { contactPaths } from "@/modules/api/v2/management/contacts/lib/openapi"; import { responsePaths } from "@/modules/api/v2/management/responses/lib/openapi"; @@ -42,7 +42,7 @@ const document = createDocument({ ...bulkContactPaths, // ...contactPaths, // ...contactAttributePaths, - // ...contactAttributeKeyPaths, + ...contactAttributeKeyPaths, ...surveyPaths, ...surveyContactLinksBySegmentPaths, ...webhookPaths, diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts index d89131950b..61abd41ec6 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { logger } from "@formbricks/logger"; import { OrganizationAccessType } from "@formbricks/types/api-key"; import { hasOrganizationIdAndAccess } from "./utils"; @@ -8,7 +8,7 @@ describe("hasOrganizationIdAndAccess", () => { vi.restoreAllMocks(); }); - it("should return false and log error if authentication has no organizationId", () => { + test("should return false and log error if authentication has no organizationId", () => { const spyError = vi.spyOn(logger, "error").mockImplementation(() => {}); const authentication = { organizationAccess: { accessControl: { read: true } }, @@ -21,7 +21,7 @@ describe("hasOrganizationIdAndAccess", () => { ); }); - it("should return false and log error if param organizationId does not match authentication organizationId", () => { + test("should return false and log error if param organizationId does not match authentication organizationId", () => { const spyError = vi.spyOn(logger, "error").mockImplementation(() => {}); const authentication = { organizationId: "org2", @@ -35,7 +35,7 @@ describe("hasOrganizationIdAndAccess", () => { ); }); - it("should return false if access type is missing in organizationAccess", () => { + test("should return false if access type is missing in organizationAccess", () => { const authentication = { organizationId: "org1", organizationAccess: { accessControl: {} }, @@ -45,7 +45,7 @@ describe("hasOrganizationIdAndAccess", () => { expect(result).toBe(false); }); - it("should return true if organizationId and access type are valid", () => { + test("should return true if organizationId and access type are valid", () => { const authentication = { organizationId: "org1", organizationAccess: { accessControl: { read: true } }, diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts index 3c06e04237..4df6762c0f 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts @@ -1,4 +1,6 @@ import { teamCache } from "@/lib/cache/team"; +import { projectCache } from "@/lib/project/cache"; +import { captureTelemetry } from "@/lib/telemetry"; import { getProjectTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils"; import { TGetProjectTeamsFilter, @@ -10,8 +12,6 @@ import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; import { ProjectTeam } from "@prisma/client"; import { z } from "zod"; import { prisma } from "@formbricks/database"; -import { projectCache } from "@formbricks/lib/project/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getProjectTeams = async ( diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts index bf7c7dc4b6..e5ba8ae9a8 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts @@ -3,7 +3,7 @@ import { TProjectTeamInput, ZProjectZTeamUpdateSchema, } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { TypeOf } from "zod"; import { prisma } from "@formbricks/database"; import { createProjectTeam, deleteProjectTeam, getProjectTeams, updateProjectTeam } from "../project-teams"; @@ -27,7 +27,7 @@ describe("ProjectTeams Lib", () => { }); describe("getProjectTeams", () => { - it("returns projectTeams with meta on success", async () => { + test("returns projectTeams with meta on success", async () => { const mockTeams = [{ id: "projTeam1", organizationId: "orgx", projectId: "p1", teamId: "t1" }]; (prisma.$transaction as any).mockResolvedValueOnce([mockTeams, mockTeams.length]); const result = await getProjectTeams("orgx", { skip: 0, limit: 10 } as TGetProjectTeamsFilter); @@ -41,7 +41,7 @@ describe("ProjectTeams Lib", () => { } }); - it("returns internal_server_error on exception", async () => { + test("returns internal_server_error on exception", async () => { (prisma.$transaction as any).mockRejectedValueOnce(new Error("DB error")); const result = await getProjectTeams("orgx", { skip: 0, limit: 10 } as TGetProjectTeamsFilter); expect(result.ok).toBe(false); @@ -52,7 +52,7 @@ describe("ProjectTeams Lib", () => { }); describe("createProjectTeam", () => { - it("creates a projectTeam successfully", async () => { + test("creates a projectTeam successfully", async () => { const mockCreated = { id: "ptx", projectId: "p1", teamId: "t1", organizationId: "orgx" }; (prisma.projectTeam.create as any).mockResolvedValueOnce(mockCreated); const result = await createProjectTeam({ @@ -65,7 +65,7 @@ describe("ProjectTeams Lib", () => { } }); - it("returns internal_server_error on error", async () => { + test("returns internal_server_error on error", async () => { (prisma.projectTeam.create as any).mockRejectedValueOnce(new Error("Create error")); const result = await createProjectTeam({ projectId: "p1", @@ -79,7 +79,7 @@ describe("ProjectTeams Lib", () => { }); describe("updateProjectTeam", () => { - it("updates a projectTeam successfully", async () => { + test("updates a projectTeam successfully", async () => { (prisma.projectTeam.update as any).mockResolvedValueOnce({ id: "pt01", projectId: "p1", @@ -95,7 +95,7 @@ describe("ProjectTeams Lib", () => { } }); - it("returns internal_server_error on error", async () => { + test("returns internal_server_error on error", async () => { (prisma.projectTeam.update as any).mockRejectedValueOnce(new Error("Update error")); const result = await updateProjectTeam("t1", "p1", { permission: "READ" } as unknown as TypeOf< typeof ZProjectZTeamUpdateSchema @@ -108,7 +108,7 @@ describe("ProjectTeams Lib", () => { }); describe("deleteProjectTeam", () => { - it("deletes a projectTeam successfully", async () => { + test("deletes a projectTeam successfully", async () => { (prisma.projectTeam.delete as any).mockResolvedValueOnce({ projectId: "p1", teamId: "t1", @@ -122,7 +122,7 @@ describe("ProjectTeams Lib", () => { } }); - it("returns internal_server_error on error", async () => { + test("returns internal_server_error on error", async () => { (prisma.projectTeam.delete as any).mockRejectedValueOnce(new Error("Delete error")); const result = await deleteProjectTeam("t1", "p1"); expect(result.ok).toBe(false); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts index a1cdbea501..3bbe43c7bf 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts @@ -1,13 +1,13 @@ +import { cache } from "@/lib/cache"; import { teamCache } from "@/lib/cache/team"; +import { organizationCache } from "@/lib/organization/cache"; +import { projectCache } from "@/lib/project/cache"; import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; import { TGetProjectTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { organizationCache } from "@formbricks/lib/organization/cache"; -import { projectCache } from "@formbricks/lib/project/cache"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { Result, err, ok } from "@formbricks/types/error-handlers"; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts index 90a9d43c8c..bbdc3bc512 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts @@ -1,3 +1,4 @@ +import { cache } from "@/lib/cache"; import { organizationCache } from "@/lib/cache/organization"; import { teamCache } from "@/lib/cache/team"; import { ZTeamUpdateSchema } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams"; @@ -8,7 +9,6 @@ import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { cache } from "@formbricks/lib/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getTeam = reactCache(async (organizationId: string, teamId: string) => diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts index f7ae2215f6..04fcaf9147 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts @@ -1,6 +1,6 @@ import { teamCache } from "@/lib/cache/team"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; import { deleteTeam, getTeam, updateTeam } from "../teams"; @@ -25,7 +25,7 @@ const mockTeam = { describe("Teams Lib", () => { describe("getTeam", () => { - it("returns the team when found", async () => { + test("returns the team when found", async () => { (prisma.team.findUnique as any).mockResolvedValueOnce(mockTeam); const result = await getTeam("org456", "team123"); expect(result.ok).toBe(true); @@ -37,7 +37,7 @@ describe("Teams Lib", () => { }); }); - it("returns a not_found error when team is missing", async () => { + test("returns a not_found error when team is missing", async () => { (prisma.team.findUnique as any).mockResolvedValueOnce(null); const result = await getTeam("org456", "team123"); expect(result.ok).toBe(false); @@ -49,7 +49,7 @@ describe("Teams Lib", () => { } }); - it("returns an internal_server_error when prisma throws", async () => { + test("returns an internal_server_error when prisma throws", async () => { (prisma.team.findUnique as any).mockRejectedValueOnce(new Error("DB error")); const result = await getTeam("org456", "team123"); expect(result.ok).toBe(false); @@ -60,7 +60,7 @@ describe("Teams Lib", () => { }); describe("deleteTeam", () => { - it("deletes the team and revalidates cache", async () => { + test("deletes the team and revalidates cache", async () => { (prisma.team.delete as any).mockResolvedValueOnce(mockTeam); // Mock teamCache.revalidate const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {}); @@ -82,7 +82,7 @@ describe("Teams Lib", () => { } }); - it("returns not_found error on known prisma error", async () => { + test("returns not_found error on known prisma error", async () => { (prisma.team.delete as any).mockRejectedValueOnce( new PrismaClientKnownRequestError("Not found", { code: PrismaErrorType.RecordDoesNotExist, @@ -100,7 +100,7 @@ describe("Teams Lib", () => { } }); - it("returns internal_server_error on exception", async () => { + test("returns internal_server_error on exception", async () => { (prisma.team.delete as any).mockRejectedValueOnce(new Error("Delete failed")); const result = await deleteTeam("org456", "team123"); expect(result.ok).toBe(false); @@ -114,7 +114,7 @@ describe("Teams Lib", () => { const updateInput = { name: "Updated Team" }; const updatedTeam = { ...mockTeam, ...updateInput }; - it("updates the team successfully and revalidates cache", async () => { + test("updates the team successfully and revalidates cache", async () => { (prisma.team.update as any).mockResolvedValueOnce(updatedTeam); const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {}); const result = await updateTeam("org456", "team123", updateInput); @@ -136,7 +136,7 @@ describe("Teams Lib", () => { } }); - it("returns not_found error when update fails due to missing team", async () => { + test("returns not_found error when update fails due to missing team", async () => { (prisma.team.update as any).mockRejectedValueOnce( new PrismaClientKnownRequestError("Not found", { code: PrismaErrorType.RecordDoesNotExist, @@ -154,7 +154,7 @@ describe("Teams Lib", () => { } }); - it("returns internal_server_error on generic exception", async () => { + test("returns internal_server_error on generic exception", async () => { (prisma.team.update as any).mockRejectedValueOnce(new Error("Update failed")); const result = await updateTeam("org456", "team123", updateInput); expect(result.ok).toBe(false); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts index c6653cdf84..68fb33653e 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts @@ -1,5 +1,7 @@ import "server-only"; import { teamCache } from "@/lib/cache/team"; +import { organizationCache } from "@/lib/organization/cache"; +import { captureTelemetry } from "@/lib/telemetry"; import { getTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/utils"; import { TGetTeamsFilter, @@ -9,8 +11,6 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; import { Team } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { organizationCache } from "@formbricks/lib/organization/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const createTeam = async ( diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts index b7da581704..d620187190 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts @@ -1,7 +1,7 @@ +import { organizationCache } from "@/lib/organization/cache"; import { TGetTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { organizationCache } from "@formbricks/lib/organization/cache"; import { createTeam, getTeams } from "../teams"; // Define a mock team object @@ -32,7 +32,7 @@ vi.spyOn(organizationCache, "revalidate").mockImplementation(() => {}); describe("Teams Lib", () => { describe("createTeam", () => { - it("creates a team successfully and revalidates cache", async () => { + test("creates a team successfully and revalidates cache", async () => { (prisma.team.create as any).mockResolvedValueOnce(mockTeam); const teamInput = { name: "Test Team" }; @@ -49,7 +49,7 @@ describe("Teams Lib", () => { if (result.ok) expect(result.data).toEqual(mockTeam); }); - it("returns internal error when prisma.team.create fails", async () => { + test("returns internal error when prisma.team.create fails", async () => { (prisma.team.create as any).mockRejectedValueOnce(new Error("Create error")); const teamInput = { name: "Test Team" }; const organizationId = "org456"; @@ -63,7 +63,7 @@ describe("Teams Lib", () => { describe("getTeams", () => { const filter = { limit: 10, skip: 0 }; - it("returns teams with meta on success", async () => { + test("returns teams with meta on success", async () => { const teamsArray = [mockTeam]; // Simulate prisma transaction return [teams, count] (prisma.$transaction as any).mockResolvedValueOnce([teamsArray, teamsArray.length]); @@ -80,7 +80,7 @@ describe("Teams Lib", () => { } }); - it("returns internal_server_error when prisma transaction fails", async () => { + test("returns internal_server_error when prisma transaction fails", async () => { (prisma.$transaction as any).mockRejectedValueOnce(new Error("Transaction error")); const organizationId = "org456"; const result = await getTeams(organizationId, filter as TGetTeamsFilter); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts index 4d77520d2d..126b43d5f8 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts @@ -1,6 +1,6 @@ import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; import { Prisma } from "@prisma/client"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { getTeamsQuery } from "../utils"; // Mock the common utils functions @@ -12,12 +12,12 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({ describe("getTeamsQuery", () => { const organizationId = "org123"; - it("returns base query when no params provided", () => { + test("returns base query when no params provided", () => { const result = getTeamsQuery(organizationId); expect(result.where).toEqual({ organizationId }); }); - it("returns unchanged query if pickCommonFilter returns null/undefined", () => { + test("returns unchanged query if pickCommonFilter returns null/undefined", () => { vi.mocked(pickCommonFilter).mockReturnValueOnce(null as any); const params: any = { someParam: "test" }; const result = getTeamsQuery(organizationId, params); @@ -26,7 +26,7 @@ describe("getTeamsQuery", () => { expect(result.where).toEqual({ organizationId }); }); - it("calls buildCommonFilterQuery and returns updated query when base filter exists", () => { + test("calls buildCommonFilterQuery and returns updated query when base filter exists", () => { const baseFilter = { key: "value" }; vi.mocked(pickCommonFilter).mockReturnValueOnce(baseFilter as any); // Simulate buildCommonFilterQuery to merge base query with baseFilter diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts index 44b61a41bf..14f47636ee 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts @@ -59,6 +59,6 @@ export const POST = async (request: Request, props: { params: Promise<{ organiza return handleApiError(request, createTeamResult.error); } - return responses.successResponse({ data: createTeamResult.data }); + return responses.createdResponse({ data: createTeamResult.data }); }, }); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts index c94fc944ed..c8a973b06d 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts @@ -1,9 +1,9 @@ import { teamCache } from "@/lib/cache/team"; +import { membershipCache } from "@/lib/membership/cache"; +import { userCache } from "@/lib/user/cache"; import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { membershipCache } from "@formbricks/lib/membership/cache"; -import { userCache } from "@formbricks/lib/user/cache"; import { createUser, getUsers, updateUser } from "../users"; const mockUser = { @@ -45,7 +45,7 @@ vi.spyOn(teamCache, "revalidate").mockImplementation(() => {}); describe("Users Lib", () => { describe("getUsers", () => { - it("returns users with meta on success", async () => { + test("returns users with meta on success", async () => { const usersArray = [mockUser]; (prisma.$transaction as any).mockResolvedValueOnce([usersArray, usersArray.length]); const result = await getUsers("org456", { limit: 10, skip: 0 } as TGetUsersFilter); @@ -68,7 +68,7 @@ describe("Users Lib", () => { } }); - it("returns internal_server_error if prisma fails", async () => { + test("returns internal_server_error if prisma fails", async () => { (prisma.$transaction as any).mockRejectedValueOnce(new Error("Transaction error")); const result = await getUsers("org456", { limit: 10, skip: 0 } as TGetUsersFilter); expect(result.ok).toBe(false); @@ -79,7 +79,7 @@ describe("Users Lib", () => { }); describe("createUser", () => { - it("creates user and revalidates caches", async () => { + test("creates user and revalidates caches", async () => { (prisma.user.create as any).mockResolvedValueOnce(mockUser); const result = await createUser( { name: "Test User", email: "test@example.com", role: "member" }, @@ -92,7 +92,7 @@ describe("Users Lib", () => { } }); - it("returns internal_server_error if creation fails", async () => { + test("returns internal_server_error if creation fails", async () => { (prisma.user.create as any).mockRejectedValueOnce(new Error("Create error")); const result = await createUser({ name: "fail", email: "fail@example.com", role: "manager" }, "org456"); expect(result.ok).toBe(false); @@ -103,7 +103,7 @@ describe("Users Lib", () => { }); describe("updateUser", () => { - it("updates user and revalidates caches", async () => { + test("updates user and revalidates caches", async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); (prisma.$transaction as any).mockResolvedValueOnce([{ ...mockUser, name: "Updated User" }]); const result = await updateUser({ email: mockUser.email, name: "Updated User" }, "org456"); @@ -114,7 +114,7 @@ describe("Users Lib", () => { } }); - it("returns not_found if user doesn't exist", async () => { + test("returns not_found if user doesn't exist", async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(null); const result = await updateUser({ email: "unknown@example.com" }, "org456"); expect(result.ok).toBe(false); @@ -123,7 +123,7 @@ describe("Users Lib", () => { } }); - it("returns internal_server_error if update fails", async () => { + test("returns internal_server_error if update fails", async () => { (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); (prisma.$transaction as any).mockRejectedValueOnce(new Error("Update error")); const result = await updateUser({ email: mockUser.email }, "org456"); @@ -135,7 +135,7 @@ describe("Users Lib", () => { }); describe("createUser with teams", () => { - it("creates user with existing teams", async () => { + test("creates user with existing teams", async () => { (prisma.team.findMany as any).mockResolvedValueOnce([ { id: "team123", name: "MyTeam", projectTeams: [{ projectId: "proj789" }] }, ]); @@ -157,7 +157,7 @@ describe("Users Lib", () => { }); describe("updateUser with team changes", () => { - it("removes a team and adds new team", async () => { + test("removes a team and adds new team", async () => { (prisma.user.findUnique as any).mockResolvedValueOnce({ ...mockUser, teamUsers: [{ team: { id: "team123", name: "OldTeam", projectTeams: [{ projectId: "proj789" }] } }], diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts index dd3cb07a2c..df626d9b9c 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts @@ -1,6 +1,6 @@ import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { getUsersQuery } from "../utils"; vi.mock("@/modules/api/v2/management/lib/utils", () => ({ @@ -9,7 +9,7 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({ })); describe("getUsersQuery", () => { - it("returns default query if no params are provided", () => { + test("returns default query if no params are provided", () => { const result = getUsersQuery("org123"); expect(result).toEqual({ where: { @@ -22,7 +22,7 @@ describe("getUsersQuery", () => { }); }); - it("includes email filter if email param is provided", () => { + test("includes email filter if email param is provided", () => { const result = getUsersQuery("org123", { email: "test@example.com" } as TGetUsersFilter); expect(result.where?.email).toEqual({ contains: "test@example.com", @@ -30,12 +30,12 @@ describe("getUsersQuery", () => { }); }); - it("includes id filter if id param is provided", () => { + test("includes id filter if id param is provided", () => { const result = getUsersQuery("org123", { id: "user123" } as TGetUsersFilter); expect(result.where?.id).toBe("user123"); }); - it("applies baseFilter if pickCommonFilter returns something", () => { + test("applies baseFilter if pickCommonFilter returns something", () => { vi.mocked(pickCommonFilter).mockReturnValueOnce({ someField: "test" } as unknown as ReturnType< typeof pickCommonFilter >); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts index 85b7aac577..90f7eaa02a 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts @@ -1,4 +1,7 @@ import { teamCache } from "@/lib/cache/team"; +import { membershipCache } from "@/lib/membership/cache"; +import { captureTelemetry } from "@/lib/telemetry"; +import { userCache } from "@/lib/user/cache"; import { getUsersQuery } from "@/modules/api/v2/organizations/[organizationId]/users/lib/utils"; import { TGetUsersFilter, @@ -10,9 +13,6 @@ import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; import { OrganizationRole, Prisma, TeamUserRole } from "@prisma/client"; import { prisma } from "@formbricks/database"; import { TUser } from "@formbricks/database/zod/users"; -import { membershipCache } from "@formbricks/lib/membership/cache"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; -import { userCache } from "@formbricks/lib/user/cache"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getUsers = async ( diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts index 7097d2d56d..30f22e9bdc 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts @@ -1,3 +1,4 @@ +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; @@ -15,7 +16,6 @@ import { } from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; import { NextRequest } from "next/server"; import { z } from "zod"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { OrganizationAccessType } from "@formbricks/types/api-key"; export const GET = async (request: NextRequest, props: { params: Promise<{ organizationId: string }> }) => @@ -79,7 +79,7 @@ export const POST = async (request: Request, props: { params: Promise<{ organiza return handleApiError(request, createUserResult.error); } - return responses.successResponse({ data: createUserResult.data }); + return responses.createdResponse({ data: createUserResult.data }); }, }); diff --git a/apps/web/modules/api/v2/roles/lib/utils.ts b/apps/web/modules/api/v2/roles/lib/utils.ts index 48eff88d75..47db5d41f3 100644 --- a/apps/web/modules/api/v2/roles/lib/utils.ts +++ b/apps/web/modules/api/v2/roles/lib/utils.ts @@ -1,6 +1,6 @@ +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { OrganizationRole } from "@prisma/client"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { Result, err, ok } from "@formbricks/types/error-handlers"; export const getRoles = (): Result<{ data: string[] }, ApiErrorResponseV2> => { diff --git a/apps/web/modules/auth/actions.ts b/apps/web/modules/auth/actions.ts index 717e8ef250..707f001781 100644 --- a/apps/web/modules/auth/actions.ts +++ b/apps/web/modules/auth/actions.ts @@ -1,9 +1,9 @@ "use server"; +import { createEmailToken } from "@/lib/jwt"; +import { getUserByEmail } from "@/lib/user/service"; import { actionClient } from "@/lib/utils/action-client"; import { z } from "zod"; -import { createEmailToken } from "@formbricks/lib/jwt"; -import { getUserByEmail } from "@formbricks/lib/user/service"; import { InvalidInputError } from "@formbricks/types/errors"; const ZCreateEmailTokenAction = z.object({ diff --git a/apps/web/modules/auth/components/form-wrapper.tsx b/apps/web/modules/auth/components/form-wrapper.tsx index 85c74459de..0439d8f96d 100644 --- a/apps/web/modules/auth/components/form-wrapper.tsx +++ b/apps/web/modules/auth/components/form-wrapper.tsx @@ -1,4 +1,5 @@ import { Logo } from "@/modules/ui/components/logo"; +import Link from "next/link"; interface FormWrapperProps { children: React.ReactNode; @@ -9,7 +10,9 @@ export const FormWrapper = ({ children }: FormWrapperProps) => {
    - + + +
    {children}
    diff --git a/apps/web/modules/auth/forgot-password/reset/actions.ts b/apps/web/modules/auth/forgot-password/reset/actions.ts index afaf06aaf8..432ee58e6b 100644 --- a/apps/web/modules/auth/forgot-password/reset/actions.ts +++ b/apps/web/modules/auth/forgot-password/reset/actions.ts @@ -1,12 +1,12 @@ "use server"; +import { hashPassword } from "@/lib/auth"; +import { verifyToken } from "@/lib/jwt"; import { actionClient } from "@/lib/utils/action-client"; import { updateUser } from "@/modules/auth/lib/user"; import { getUser } from "@/modules/auth/lib/user"; import { sendPasswordResetNotifyEmail } from "@/modules/email"; import { z } from "zod"; -import { hashPassword } from "@formbricks/lib/auth"; -import { verifyToken } from "@formbricks/lib/jwt"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ZUserPassword } from "@formbricks/types/user"; diff --git a/apps/web/modules/auth/invite/lib/invite.ts b/apps/web/modules/auth/invite/lib/invite.ts index ae007c2081..577deece43 100644 --- a/apps/web/modules/auth/invite/lib/invite.ts +++ b/apps/web/modules/auth/invite/lib/invite.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; import { inviteCache } from "@/lib/cache/invite"; import { type InviteWithCreator } from "@/modules/auth/invite/types/invites"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const deleteInvite = async (inviteId: string): Promise => { diff --git a/apps/web/modules/auth/invite/lib/team.ts b/apps/web/modules/auth/invite/lib/team.ts index 00ddc6dab6..88e426a618 100644 --- a/apps/web/modules/auth/invite/lib/team.ts +++ b/apps/web/modules/auth/invite/lib/team.ts @@ -1,10 +1,10 @@ import "server-only"; import { teamCache } from "@/lib/cache/team"; +import { getAccessFlags } from "@/lib/membership/utils"; +import { projectCache } from "@/lib/project/cache"; import { CreateMembershipInvite } from "@/modules/auth/invite/types/invites"; import { Prisma } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { projectCache } from "@formbricks/lib/project/cache"; import { DatabaseError } from "@formbricks/types/errors"; export const createTeamMembership = async (invite: CreateMembershipInvite, userId: string): Promise => { diff --git a/apps/web/modules/auth/invite/page.tsx b/apps/web/modules/auth/invite/page.tsx index b91402f5af..21bfe6ab31 100644 --- a/apps/web/modules/auth/invite/page.tsx +++ b/apps/web/modules/auth/invite/page.tsx @@ -1,3 +1,7 @@ +import { WEBAPP_URL } from "@/lib/constants"; +import { verifyInviteToken } from "@/lib/jwt"; +import { createMembership } from "@/lib/membership/service"; +import { getUser, updateUser } from "@/lib/user/service"; import { deleteInvite, getInvite } from "@/modules/auth/invite/lib/invite"; import { createTeamMembership } from "@/modules/auth/invite/lib/team"; import { authOptions } from "@/modules/auth/lib/authOptions"; @@ -7,10 +11,6 @@ import { getTranslate } from "@/tolgee/server"; import { getServerSession } from "next-auth"; import Link from "next/link"; import { after } from "next/server"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; -import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { createMembership } from "@formbricks/lib/membership/service"; -import { getUser, updateUser } from "@formbricks/lib/user/service"; import { logger } from "@formbricks/logger"; import { ContentLayout } from "./components/content-layout"; diff --git a/apps/web/modules/auth/layout.tsx b/apps/web/modules/auth/layout.tsx index adefb87862..85221abc16 100644 --- a/apps/web/modules/auth/layout.tsx +++ b/apps/web/modules/auth/layout.tsx @@ -1,9 +1,9 @@ +import { getIsFreshInstance } from "@/lib/instance/service"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { Toaster } from "react-hot-toast"; -import { getIsFreshInstance } from "@formbricks/lib/instance/service"; export const AuthLayout = async ({ children }: { children: React.ReactNode }) => { const [session, isFreshInstance, isMultiOrgEnabled] = await Promise.all([ diff --git a/apps/web/modules/auth/lib/authOptions.test.ts b/apps/web/modules/auth/lib/authOptions.test.ts index 283dc228ce..8010fb8585 100644 --- a/apps/web/modules/auth/lib/authOptions.test.ts +++ b/apps/web/modules/auth/lib/authOptions.test.ts @@ -1,9 +1,9 @@ +import { EMAIL_VERIFICATION_DISABLED } from "@/lib/constants"; +import { createToken } from "@/lib/jwt"; import { randomBytes } from "crypto"; import { Provider } from "next-auth/providers/index"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; -import { EMAIL_VERIFICATION_DISABLED } from "@formbricks/lib/constants"; -import { createToken } from "@formbricks/lib/jwt"; import { authOptions } from "./authOptions"; import { mockUser } from "./mock-data"; import { hashPassword } from "./utils"; @@ -40,13 +40,13 @@ describe("authOptions", () => { describe("CredentialsProvider (credentials) - email/password login", () => { const credentialsProvider = getProviderById("credentials"); - it("should throw error if credentials are not provided", async () => { + test("should throw error if credentials are not provided", async () => { await expect(credentialsProvider.options.authorize(undefined, {})).rejects.toThrow( "Invalid credentials" ); }); - it("should throw error if user not found", async () => { + test("should throw error if user not found", async () => { vi.spyOn(prisma.user, "findUnique").mockResolvedValue(null); const credentials = { email: mockUser.email, password: mockPassword }; @@ -56,7 +56,7 @@ describe("authOptions", () => { ); }); - it("should throw error if user has no password stored", async () => { + test("should throw error if user has no password stored", async () => { vi.spyOn(prisma.user, "findUnique").mockResolvedValue({ id: mockUser.id, email: mockUser.email, @@ -70,7 +70,7 @@ describe("authOptions", () => { ); }); - it("should throw error if password verification fails", async () => { + test("should throw error if password verification fails", async () => { vi.spyOn(prisma.user, "findUnique").mockResolvedValue({ id: mockUserId, email: mockUser.email, @@ -84,7 +84,7 @@ describe("authOptions", () => { ); }); - it("should successfully login when credentials are valid", async () => { + test("should successfully login when credentials are valid", async () => { const fakeUser = { id: mockUserId, email: mockUser.email, @@ -108,7 +108,7 @@ describe("authOptions", () => { }); describe("Two-Factor Backup Code login", () => { - it("should throw error if backup codes are missing", async () => { + test("should throw error if backup codes are missing", async () => { const mockUser = { id: mockUserId, email: "2fa@example.com", @@ -130,13 +130,13 @@ describe("authOptions", () => { describe("CredentialsProvider (token) - Token-based email verification", () => { const tokenProvider = getProviderById("token"); - it("should throw error if token is not provided", async () => { + test("should throw error if token is not provided", async () => { await expect(tokenProvider.options.authorize({}, {})).rejects.toThrow( "Either a user does not match the provided token or the token is invalid" ); }); - it("should throw error if token is invalid or user not found", async () => { + test("should throw error if token is invalid or user not found", async () => { const credentials = { token: "badtoken" }; await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow( @@ -144,7 +144,7 @@ describe("authOptions", () => { ); }); - it("should throw error if email is already verified", async () => { + test("should throw error if email is already verified", async () => { vi.spyOn(prisma.user, "findUnique").mockResolvedValue(mockUser); const credentials = { token: createToken(mockUser.id, mockUser.email) }; @@ -154,7 +154,7 @@ describe("authOptions", () => { ); }); - it("should update user and verify email when token is valid", async () => { + test("should update user and verify email when token is valid", async () => { vi.spyOn(prisma.user, "findUnique").mockResolvedValue({ id: mockUser.id, emailVerified: null }); vi.spyOn(prisma.user, "update").mockResolvedValue({ ...mockUser, @@ -175,7 +175,7 @@ describe("authOptions", () => { describe("Callbacks", () => { describe("jwt callback", () => { - it("should add profile information to token if user is found", async () => { + test("should add profile information to token if user is found", async () => { vi.spyOn(prisma.user, "findFirst").mockResolvedValue({ id: mockUser.id, locale: mockUser.locale, @@ -194,7 +194,7 @@ describe("authOptions", () => { }); }); - it("should return token unchanged if no existing user is found", async () => { + test("should return token unchanged if no existing user is found", async () => { vi.spyOn(prisma.user, "findFirst").mockResolvedValue(null); const token = { email: "nonexistent@example.com" }; @@ -207,7 +207,7 @@ describe("authOptions", () => { }); describe("session callback", () => { - it("should add user profile to session", async () => { + test("should add user profile to session", async () => { const token = { id: "user6", profile: { id: "user6", email: "user6@example.com" }, @@ -223,7 +223,7 @@ describe("authOptions", () => { }); describe("signIn callback", () => { - it("should throw error if email is not verified and email verification is enabled", async () => { + test("should throw error if email is not verified and email verification is enabled", async () => { const user = { ...mockUser, emailVerified: null }; const account = { provider: "credentials" } as any; // EMAIL_VERIFICATION_DISABLED is imported from constants. @@ -239,7 +239,7 @@ describe("authOptions", () => { describe("Two-Factor Authentication (TOTP)", () => { const credentialsProvider = getProviderById("credentials"); - it("should throw error if TOTP code is missing when 2FA is enabled", async () => { + test("should throw error if TOTP code is missing when 2FA is enabled", async () => { const mockUser = { id: mockUserId, email: "2fa@example.com", @@ -256,7 +256,7 @@ describe("authOptions", () => { ); }); - it("should throw error if two factor secret is missing", async () => { + test("should throw error if two factor secret is missing", async () => { const mockUser = { id: mockUserId, email: "2fa@example.com", diff --git a/apps/web/modules/auth/lib/authOptions.ts b/apps/web/modules/auth/lib/authOptions.ts index 8711e00c8e..eb3e277e6b 100644 --- a/apps/web/modules/auth/lib/authOptions.ts +++ b/apps/web/modules/auth/lib/authOptions.ts @@ -1,3 +1,6 @@ +import { EMAIL_VERIFICATION_DISABLED, ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY } from "@/lib/constants"; +import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto"; +import { verifyToken } from "@/lib/jwt"; import { getUserByEmail, updateUser, updateUserLastLoginAt } from "@/modules/auth/lib/user"; import { verifyPassword } from "@/modules/auth/lib/utils"; import { getSSOProviders } from "@/modules/ee/sso/lib/providers"; @@ -6,13 +9,6 @@ import type { Account, NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { cookies } from "next/headers"; import { prisma } from "@formbricks/database"; -import { - EMAIL_VERIFICATION_DISABLED, - ENCRYPTION_KEY, - ENTERPRISE_LICENSE_KEY, -} from "@formbricks/lib/constants"; -import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto"; -import { verifyToken } from "@formbricks/lib/jwt"; import { logger } from "@formbricks/logger"; import { TUser } from "@formbricks/types/user"; import { createBrevoCustomer } from "./brevo"; diff --git a/apps/web/modules/auth/lib/brevo.test.ts b/apps/web/modules/auth/lib/brevo.test.ts index 16cff4885a..2448e69c6b 100644 --- a/apps/web/modules/auth/lib/brevo.test.ts +++ b/apps/web/modules/auth/lib/brevo.test.ts @@ -1,15 +1,15 @@ +import { validateInputs } from "@/lib/utils/validate"; import { Response } from "node-fetch"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { validateInputs } from "@formbricks/lib/utils/validate"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { logger } from "@formbricks/logger"; import { createBrevoCustomer } from "./brevo"; -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ BREVO_API_KEY: "mock_api_key", BREVO_LIST_ID: "123", })); -vi.mock("@formbricks/lib/utils/validate", () => ({ +vi.mock("@/lib/utils/validate", () => ({ validateInputs: vi.fn(), })); @@ -20,8 +20,8 @@ describe("createBrevoCustomer", () => { vi.clearAllMocks(); }); - it("should return early if BREVO_API_KEY is not defined", async () => { - vi.doMock("@formbricks/lib/constants", () => ({ + test("should return early if BREVO_API_KEY is not defined", async () => { + vi.doMock("@/lib/constants", () => ({ BREVO_API_KEY: undefined, BREVO_LIST_ID: "123", })); @@ -35,7 +35,7 @@ describe("createBrevoCustomer", () => { expect(validateInputs).not.toHaveBeenCalled(); }); - it("should log an error if fetch fails", async () => { + test("should log an error if fetch fails", async () => { const loggerSpy = vi.spyOn(logger, "error"); vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Fetch failed")); @@ -45,7 +45,7 @@ describe("createBrevoCustomer", () => { expect(loggerSpy).toHaveBeenCalledWith(expect.any(Error), "Error sending user to Brevo"); }); - it("should log the error response if fetch status is not 200", async () => { + test("should log the error response if fetch status is not 200", async () => { const loggerSpy = vi.spyOn(logger, "error"); vi.mocked(global.fetch).mockResolvedValueOnce( diff --git a/apps/web/modules/auth/lib/brevo.ts b/apps/web/modules/auth/lib/brevo.ts index 6fd9e4a06c..0b52812921 100644 --- a/apps/web/modules/auth/lib/brevo.ts +++ b/apps/web/modules/auth/lib/brevo.ts @@ -1,5 +1,5 @@ -import { BREVO_API_KEY, BREVO_LIST_ID } from "@formbricks/lib/constants"; -import { validateInputs } from "@formbricks/lib/utils/validate"; +import { BREVO_API_KEY, BREVO_LIST_ID } from "@/lib/constants"; +import { validateInputs } from "@/lib/utils/validate"; import { logger } from "@formbricks/logger"; import { ZId } from "@formbricks/types/common"; import { TUserEmail, ZUserEmail } from "@formbricks/types/user"; diff --git a/apps/web/modules/auth/lib/totp.test.ts b/apps/web/modules/auth/lib/totp.test.ts index 92052f4c7e..fe4167534e 100644 --- a/apps/web/modules/auth/lib/totp.test.ts +++ b/apps/web/modules/auth/lib/totp.test.ts @@ -2,7 +2,7 @@ import { Authenticator } from "@otplib/core"; import type { AuthenticatorOptions } from "@otplib/core/authenticator"; import { createDigest, createRandomBytes } from "@otplib/plugin-crypto"; import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { totpAuthenticatorCheck } from "./totp"; vi.mock("@otplib/core"); @@ -14,7 +14,7 @@ describe("totpAuthenticatorCheck", () => { const secret = "JBSWY3DPEHPK3PXP"; const opts: Partial = { window: [1, 0] }; - it("should check a TOTP token with a base32-encoded secret", () => { + test("should check a TOTP token with a base32-encoded secret", () => { const checkMock = vi.fn().mockReturnValue(true); (Authenticator as unknown as vi.Mock).mockImplementation(() => ({ check: checkMock, @@ -33,7 +33,7 @@ describe("totpAuthenticatorCheck", () => { expect(result).toBe(true); }); - it("should use default window if none is provided", () => { + test("should use default window if none is provided", () => { const checkMock = vi.fn().mockReturnValue(true); (Authenticator as unknown as vi.Mock).mockImplementation(() => ({ check: checkMock, @@ -52,7 +52,7 @@ describe("totpAuthenticatorCheck", () => { expect(result).toBe(true); }); - it("should throw an error for invalid token format", () => { + test("should throw an error for invalid token format", () => { (Authenticator as unknown as vi.Mock).mockImplementation(() => ({ check: () => { throw new Error("Invalid token format"); @@ -64,7 +64,7 @@ describe("totpAuthenticatorCheck", () => { }).toThrow("Invalid token format"); }); - it("should throw an error for invalid secret format", () => { + test("should throw an error for invalid secret format", () => { (Authenticator as unknown as vi.Mock).mockImplementation(() => ({ check: () => { throw new Error("Invalid secret format"); @@ -76,7 +76,7 @@ describe("totpAuthenticatorCheck", () => { }).toThrow("Invalid secret format"); }); - it("should return false if token verification fails", () => { + test("should return false if token verification fails", () => { const checkMock = vi.fn().mockReturnValue(false); (Authenticator as unknown as vi.Mock).mockImplementation(() => ({ check: checkMock, diff --git a/apps/web/modules/auth/lib/user.test.ts b/apps/web/modules/auth/lib/user.test.ts index 93cd4951e8..ef48d7ea8d 100644 --- a/apps/web/modules/auth/lib/user.test.ts +++ b/apps/web/modules/auth/lib/user.test.ts @@ -1,8 +1,8 @@ +import { userCache } from "@/lib/user/cache"; import { Prisma } from "@prisma/client"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { userCache } from "@formbricks/lib/user/cache"; import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { mockUser } from "./mock-data"; import { createUser, getUser, getUserByEmail, updateUser, updateUserLastLoginAt } from "./user"; @@ -27,7 +27,7 @@ vi.mock("@formbricks/database", () => ({ }, })); -vi.mock("@formbricks/lib/user/cache", () => ({ +vi.mock("@/lib/user/cache", () => ({ userCache: { revalidate: vi.fn(), tag: { @@ -43,7 +43,7 @@ describe("User Management", () => { }); describe("createUser", () => { - it("creates a user successfully", async () => { + test("creates a user successfully", async () => { vi.mocked(prisma.user.create).mockResolvedValueOnce(mockPrismaUser); const result = await createUser({ @@ -56,7 +56,7 @@ describe("User Management", () => { expect(userCache.revalidate).toHaveBeenCalled(); }); - it("throws InvalidInputError when email already exists", async () => { + test("throws InvalidInputError when email already exists", async () => { const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { code: PrismaErrorType.UniqueConstraintViolation, clientVersion: "0.0.1", @@ -76,7 +76,7 @@ describe("User Management", () => { describe("updateUser", () => { const mockUpdateData = { name: "Updated Name" }; - it("updates a user successfully", async () => { + test("updates a user successfully", async () => { vi.mocked(prisma.user.update).mockResolvedValueOnce({ ...mockPrismaUser, name: mockUpdateData.name }); const result = await updateUser(mockUser.id, mockUpdateData); @@ -85,7 +85,7 @@ describe("User Management", () => { expect(userCache.revalidate).toHaveBeenCalled(); }); - it("throws ResourceNotFoundError when user doesn't exist", async () => { + test("throws ResourceNotFoundError when user doesn't exist", async () => { const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { code: PrismaErrorType.RecordDoesNotExist, clientVersion: "0.0.1", @@ -99,7 +99,7 @@ describe("User Management", () => { describe("updateUserLastLoginAt", () => { const mockUpdateData = { name: "Updated Name" }; - it("updates a user successfully", async () => { + test("updates a user successfully", async () => { vi.mocked(prisma.user.update).mockResolvedValueOnce({ ...mockPrismaUser, name: mockUpdateData.name }); const result = await updateUserLastLoginAt(mockUser.email); @@ -108,7 +108,7 @@ describe("User Management", () => { expect(userCache.revalidate).toHaveBeenCalled(); }); - it("throws ResourceNotFoundError when user doesn't exist", async () => { + test("throws ResourceNotFoundError when user doesn't exist", async () => { const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { code: PrismaErrorType.RecordDoesNotExist, clientVersion: "0.0.1", @@ -122,7 +122,7 @@ describe("User Management", () => { describe("getUserByEmail", () => { const mockEmail = "test@example.com"; - it("retrieves a user by email successfully", async () => { + test("retrieves a user by email successfully", async () => { const mockUser = { id: "user123", email: mockEmail, @@ -136,7 +136,7 @@ describe("User Management", () => { expect(result).toEqual(mockUser); }); - it("throws DatabaseError on prisma error", async () => { + test("throws DatabaseError on prisma error", async () => { vi.mocked(prisma.user.findFirst).mockRejectedValueOnce(new Error("Database error")); await expect(getUserByEmail(mockEmail)).rejects.toThrow(); @@ -146,7 +146,7 @@ describe("User Management", () => { describe("getUser", () => { const mockUserId = "cm5xj580r00000cmgdj9ohups"; - it("retrieves a user by id successfully", async () => { + test("retrieves a user by id successfully", async () => { const mockUser = { id: mockUserId, }; @@ -157,7 +157,7 @@ describe("User Management", () => { expect(result).toEqual(mockUser); }); - it("returns null when user doesn't exist", async () => { + test("returns null when user doesn't exist", async () => { vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(null); const result = await getUser(mockUserId); @@ -165,7 +165,7 @@ describe("User Management", () => { expect(result).toBeNull(); }); - it("throws DatabaseError on prisma error", async () => { + test("throws DatabaseError on prisma error", async () => { vi.mocked(prisma.user.findUnique).mockRejectedValueOnce(new Error("Database error")); await expect(getUser(mockUserId)).rejects.toThrow(); diff --git a/apps/web/modules/auth/lib/user.ts b/apps/web/modules/auth/lib/user.ts index 3a19a0f7b3..d61a86280e 100644 --- a/apps/web/modules/auth/lib/user.ts +++ b/apps/web/modules/auth/lib/user.ts @@ -1,10 +1,10 @@ +import { cache } from "@/lib/cache"; +import { userCache } from "@/lib/user/cache"; +import { validateInputs } from "@/lib/utils/validate"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; -import { cache } from "@formbricks/lib/cache"; -import { userCache } from "@formbricks/lib/user/cache"; -import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { TUserCreateInput, TUserUpdateInput, ZUserEmail, ZUserUpdateInput } from "@formbricks/types/user"; diff --git a/apps/web/modules/auth/lib/utils.test.ts b/apps/web/modules/auth/lib/utils.test.ts index 50774174ea..bb6d67607c 100644 --- a/apps/web/modules/auth/lib/utils.test.ts +++ b/apps/web/modules/auth/lib/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { hashPassword, verifyPassword } from "./utils"; describe("Password Utils", () => { @@ -6,7 +6,7 @@ describe("Password Utils", () => { const hashedPassword = "$2a$12$LZsLq.9nkZlU0YDPx2aLNelnwD/nyavqbewLN.5.Q5h/UxRD8Ymcy"; describe("hashPassword", () => { - it("should hash a password", async () => { + test("should hash a password", async () => { const hashedPassword = await hashPassword(password); expect(typeof hashedPassword).toBe("string"); @@ -14,7 +14,7 @@ describe("Password Utils", () => { expect(hashedPassword.length).toBe(60); }); - it("should generate different hashes for the same password", async () => { + test("should generate different hashes for the same password", async () => { const hash1 = await hashPassword(password); const hash2 = await hashPassword(password); @@ -23,13 +23,13 @@ describe("Password Utils", () => { }); describe("verifyPassword", () => { - it("should verify a correct password", async () => { + test("should verify a correct password", async () => { const isValid = await verifyPassword(password, hashedPassword); expect(isValid).toBe(true); }); - it("should reject an incorrect password", async () => { + test("should reject an incorrect password", async () => { const isValid = await verifyPassword("WrongPassword123!", hashedPassword); expect(isValid).toBe(false); diff --git a/apps/web/modules/auth/login/components/login-form.tsx b/apps/web/modules/auth/login/components/login-form.tsx index 917fa0704c..6f976e9abc 100644 --- a/apps/web/modules/auth/login/components/login-form.tsx +++ b/apps/web/modules/auth/login/components/login-form.tsx @@ -1,5 +1,7 @@ "use client"; +import { cn } from "@/lib/cn"; +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@/lib/localStorage"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { createEmailTokenAction } from "@/modules/auth/actions"; import { SSOOptions } from "@/modules/ee/sso/components/sso-options"; @@ -17,8 +19,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { toast } from "react-hot-toast"; import { z } from "zod"; -import { cn } from "@formbricks/lib/cn"; -import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; const ZLoginForm = z.object({ email: z.string().email(), @@ -204,7 +204,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 shadow-sm sm:text-sm" + className="focus:border-brand-dark focus:ring-brand-dark block w-full pr-8 rounded-md border-slate-300 shadow-sm sm:text-sm" value={field.value} onChange={(password) => field.onChange(password)} /> diff --git a/apps/web/modules/auth/login/page.tsx b/apps/web/modules/auth/login/page.tsx index cc70cc03f9..f61fae7cc9 100644 --- a/apps/web/modules/auth/login/page.tsx +++ b/apps/web/modules/auth/login/page.tsx @@ -1,11 +1,3 @@ -import { FormWrapper } from "@/modules/auth/components/form-wrapper"; -import { Testimonial } from "@/modules/auth/components/testimonial"; -import { - getIsMultiOrgEnabled, - getIsSamlSsoEnabled, - getisSsoEnabled, -} from "@/modules/ee/license-check/lib/utils"; -import { Metadata } from "next"; import { AZURE_OAUTH_ENABLED, EMAIL_AUTH_ENABLED, @@ -18,7 +10,15 @@ import { SAML_PRODUCT, SAML_TENANT, SIGNUP_ENABLED, -} from "@formbricks/lib/constants"; +} from "@/lib/constants"; +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { Testimonial } from "@/modules/auth/components/testimonial"; +import { + getIsMultiOrgEnabled, + getIsSamlSsoEnabled, + getisSsoEnabled, +} from "@/modules/ee/license-check/lib/utils"; +import { Metadata } from "next"; import { LoginForm } from "./components/login-form"; export const metadata: Metadata = { diff --git a/apps/web/modules/auth/signup/actions.ts b/apps/web/modules/auth/signup/actions.ts index 13ecdb657f..01c3c437f8 100644 --- a/apps/web/modules/auth/signup/actions.ts +++ b/apps/web/modules/auth/signup/actions.ts @@ -1,5 +1,10 @@ "use server"; +import { hashPassword } from "@/lib/auth"; +import { IS_TURNSTILE_CONFIGURED, TURNSTILE_SECRET_KEY } from "@/lib/constants"; +import { verifyInviteToken } from "@/lib/jwt"; +import { createMembership } from "@/lib/membership/service"; +import { createOrganization, getOrganization } from "@/lib/organization/service"; import { actionClient } from "@/lib/utils/action-client"; import { createUser, updateUser } from "@/modules/auth/lib/user"; import { deleteInvite, getInvite } from "@/modules/auth/signup/lib/invite"; @@ -8,11 +13,6 @@ import { captureFailedSignup, verifyTurnstileToken } from "@/modules/auth/signup import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email"; import { z } from "zod"; -import { hashPassword } from "@formbricks/lib/auth"; -import { IS_TURNSTILE_CONFIGURED, TURNSTILE_SECRET_KEY } from "@formbricks/lib/constants"; -import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { createMembership } from "@formbricks/lib/membership/service"; -import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; import { UnknownError } from "@formbricks/types/errors"; import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships"; import { ZUserEmail, ZUserLocale, ZUserName, ZUserPassword } from "@formbricks/types/user"; diff --git a/apps/web/modules/auth/signup/components/signup-form.test.tsx b/apps/web/modules/auth/signup/components/signup-form.test.tsx index e494668f75..ff816c9921 100644 --- a/apps/web/modules/auth/signup/components/signup-form.test.tsx +++ b/apps/web/modules/auth/signup/components/signup-form.test.tsx @@ -4,13 +4,13 @@ import "@testing-library/jest-dom/vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { useSearchParams } from "next/navigation"; import toast from "react-hot-toast"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { createEmailTokenAction } from "../../../auth/actions"; import { SignupForm } from "./signup-form"; // Mock dependencies -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ IS_FORMBRICKS_CLOUD: false, POSTHOG_API_KEY: "mock-posthog-api-key", POSTHOG_HOST: "mock-posthog-host", @@ -129,7 +129,7 @@ describe("SignupForm", () => { cleanup(); }); - it("toggles the signup form on button click", () => { + test("toggles the signup form on button click", () => { render(); // Initially, the signup form is hidden. @@ -149,7 +149,7 @@ describe("SignupForm", () => { expect(screen.getByTestId("signup-password")).toBeInTheDocument(); }); - it("submits the form successfully", async () => { + test("submits the form successfully", async () => { // Set up mocks for the API actions. vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); @@ -194,7 +194,7 @@ describe("SignupForm", () => { expect(pushMock).toHaveBeenCalledWith("/auth/verification-requested?token=token123"); }); - it("submits the form successfully when turnstile is configured", async () => { + test("submits the form successfully when turnstile is configured", async () => { // Override props to enable Turnstile const props = { ...defaultProps, @@ -246,7 +246,7 @@ describe("SignupForm", () => { expect(pushMock).toHaveBeenCalledWith("/auth/signup-without-verification-success"); }); - it("submits the form successfully when turnstile is configured, but createEmailTokenAction don't return data", async () => { + test("submits the form successfully when turnstile is configured, but createEmailTokenAction don't return data", async () => { // Override props to enable Turnstile const props = { ...defaultProps, @@ -298,7 +298,7 @@ describe("SignupForm", () => { }); }); - it("shows an error message if turnstile is configured, but no token is received", async () => { + test("shows an error message if turnstile is configured, but no token is received", async () => { // Override props to enable Turnstile const props = { ...defaultProps, @@ -332,7 +332,7 @@ describe("SignupForm", () => { }); }); - it("Invite token is in the search params", async () => { + test("Invite token is in the search params", async () => { // Set up mocks for the API actions vi.mocked(createUserAction).mockResolvedValue({ data: true } as any); vi.mocked(createEmailTokenAction).mockResolvedValue({ data: "token123" }); diff --git a/apps/web/modules/auth/signup/lib/invite.test.ts b/apps/web/modules/auth/signup/lib/invite.test.ts index e2628d8aed..6297435cdb 100644 --- a/apps/web/modules/auth/signup/lib/invite.test.ts +++ b/apps/web/modules/auth/signup/lib/invite.test.ts @@ -1,6 +1,6 @@ import { inviteCache } from "@/lib/cache/invite"; import { Prisma } from "@prisma/client"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { PrismaErrorType } from "@formbricks/database/types/error"; import { logger } from "@formbricks/logger"; @@ -63,7 +63,7 @@ describe("Invite Management", () => { }); describe("deleteInvite", () => { - it("deletes an invite successfully and invalidates cache", async () => { + test("deletes an invite successfully and invalidates cache", async () => { vi.mocked(prisma.invite.delete).mockResolvedValue(mockInvite); const result = await deleteInvite(mockInviteId); @@ -79,7 +79,7 @@ describe("Invite Management", () => { }); }); - it("throws DatabaseError when invite doesn't exist", async () => { + test("throws DatabaseError when invite doesn't exist", async () => { const errToThrow = new Prisma.PrismaClientKnownRequestError("Record not found", { code: PrismaErrorType.RecordDoesNotExist, clientVersion: "0.0.1", @@ -89,7 +89,7 @@ describe("Invite Management", () => { await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError); }); - it("throws DatabaseError for other Prisma errors", async () => { + test("throws DatabaseError for other Prisma errors", async () => { const errToThrow = new Prisma.PrismaClientKnownRequestError("Database error", { code: "P2002", clientVersion: "0.0.1", @@ -99,7 +99,7 @@ describe("Invite Management", () => { await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError); }); - it("throws DatabaseError for generic errors", async () => { + test("throws DatabaseError for generic errors", async () => { vi.mocked(prisma.invite.delete).mockRejectedValue(new Error("Generic error")); await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError); @@ -107,7 +107,7 @@ describe("Invite Management", () => { }); describe("getInvite", () => { - it("retrieves an invite with creator details successfully", async () => { + test("retrieves an invite with creator details successfully", async () => { vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite); const result = await getInvite(mockInviteId); @@ -131,7 +131,7 @@ describe("Invite Management", () => { }); }); - it("returns null when invite doesn't exist", async () => { + test("returns null when invite doesn't exist", async () => { vi.mocked(prisma.invite.findUnique).mockResolvedValue(null); const result = await getInvite(mockInviteId); @@ -139,7 +139,7 @@ describe("Invite Management", () => { expect(result).toBeNull(); }); - it("throws DatabaseError on prisma error", async () => { + test("throws DatabaseError on prisma error", async () => { const errToThrow = new Prisma.PrismaClientKnownRequestError("Database error", { code: "P2002", clientVersion: "0.0.1", @@ -149,7 +149,7 @@ describe("Invite Management", () => { await expect(getInvite(mockInviteId)).rejects.toThrow(DatabaseError); }); - it("throws DatabaseError for generic errors", async () => { + test("throws DatabaseError for generic errors", async () => { vi.mocked(prisma.invite.findUnique).mockRejectedValue(new Error("Generic error")); await expect(getInvite(mockInviteId)).rejects.toThrow(DatabaseError); @@ -157,7 +157,7 @@ describe("Invite Management", () => { }); describe("getIsValidInviteToken", () => { - it("returns true for valid invite", async () => { + test("returns true for valid invite", async () => { vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite); const result = await getIsValidInviteToken(mockInviteId); @@ -168,7 +168,7 @@ describe("Invite Management", () => { }); }); - it("returns false when invite doesn't exist", async () => { + test("returns false when invite doesn't exist", async () => { vi.mocked(prisma.invite.findUnique).mockResolvedValue(null); const result = await getIsValidInviteToken(mockInviteId); @@ -176,7 +176,7 @@ describe("Invite Management", () => { expect(result).toBe(false); }); - it("returns false for expired invite", async () => { + test("returns false for expired invite", async () => { const expiredInvite = { ...mockInvite, expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago @@ -195,7 +195,7 @@ describe("Invite Management", () => { ); }); - it("returns false and logs error when database error occurs", async () => { + test("returns false and logs error when database error occurs", async () => { const error = new Error("Database error"); vi.mocked(prisma.invite.findUnique).mockRejectedValue(error); @@ -205,7 +205,7 @@ describe("Invite Management", () => { expect(logger.error).toHaveBeenCalledWith(error, "Error getting invite"); }); - it("returns false for invite with null expiresAt", async () => { + test("returns false for invite with null expiresAt", async () => { const invalidInvite = { ...mockInvite, expiresAt: null, @@ -224,7 +224,7 @@ describe("Invite Management", () => { ); }); - it("returns false for invite with invalid expiresAt", async () => { + test("returns false for invite with invalid expiresAt", async () => { const invalidInvite = { ...mockInvite, expiresAt: new Date("invalid-date"), diff --git a/apps/web/modules/auth/signup/lib/invite.ts b/apps/web/modules/auth/signup/lib/invite.ts index fd879abbef..7d5c60f597 100644 --- a/apps/web/modules/auth/signup/lib/invite.ts +++ b/apps/web/modules/auth/signup/lib/invite.ts @@ -1,9 +1,9 @@ +import { cache } from "@/lib/cache"; import { inviteCache } from "@/lib/cache/invite"; import { InviteWithCreator } from "@/modules/auth/signup/types/invites"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/modules/auth/signup/lib/team.ts b/apps/web/modules/auth/signup/lib/team.ts index d3564a1512..79e09923e8 100644 --- a/apps/web/modules/auth/signup/lib/team.ts +++ b/apps/web/modules/auth/signup/lib/team.ts @@ -1,10 +1,10 @@ import "server-only"; import { teamCache } from "@/lib/cache/team"; +import { getAccessFlags } from "@/lib/membership/utils"; +import { projectCache } from "@/lib/project/cache"; import { CreateMembershipInvite } from "@/modules/auth/signup/types/invites"; import { Prisma } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { projectCache } from "@formbricks/lib/project/cache"; import { DatabaseError } from "@formbricks/types/errors"; export const createTeamMembership = async (invite: CreateMembershipInvite, userId: string): Promise => { diff --git a/apps/web/modules/auth/signup/lib/utils.test.ts b/apps/web/modules/auth/signup/lib/utils.test.ts index 6564a213e5..4bf22150dd 100644 --- a/apps/web/modules/auth/signup/lib/utils.test.ts +++ b/apps/web/modules/auth/signup/lib/utils.test.ts @@ -1,5 +1,5 @@ import posthog from "posthog-js"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { captureFailedSignup, verifyTurnstileToken } from "./utils"; beforeEach(() => { @@ -16,7 +16,7 @@ describe("verifyTurnstileToken", () => { const secretKey = "test-secret"; const token = "test-token"; - it("should return true when verification is successful", async () => { + test("should return true when verification is successful", async () => { const mockResponse = { success: true }; (global.fetch as any).mockResolvedValue({ ok: true, @@ -36,7 +36,7 @@ describe("verifyTurnstileToken", () => { ); }); - it("should return false when response is not ok", async () => { + test("should return false when response is not ok", async () => { (global.fetch as any).mockResolvedValue({ ok: false, status: 400, @@ -46,14 +46,14 @@ describe("verifyTurnstileToken", () => { expect(result).toBe(false); }); - it("should return false when verification fails", async () => { + test("should return false when verification fails", async () => { (global.fetch as any).mockRejectedValue(new Error("Network error")); const result = await verifyTurnstileToken(secretKey, token); expect(result).toBe(false); }); - it("should return false when request times out", async () => { + test("should return false when request times out", async () => { const mockAbortError = new Error("The operation was aborted"); mockAbortError.name = "AbortError"; (global.fetch as any).mockRejectedValue(mockAbortError); @@ -64,7 +64,7 @@ describe("verifyTurnstileToken", () => { }); describe("captureFailedSignup", () => { - it("should capture TELEMETRY_FAILED_SIGNUP event with email and name", () => { + test("should capture TELEMETRY_FAILED_SIGNUP event with email and name", () => { const captureSpy = vi.spyOn(posthog, "capture"); const email = "test@example.com"; const name = "Test User"; diff --git a/apps/web/modules/auth/signup/page.test.tsx b/apps/web/modules/auth/signup/page.test.tsx index 88d4ac18f1..eaa58eeb41 100644 --- a/apps/web/modules/auth/signup/page.test.tsx +++ b/apps/web/modules/auth/signup/page.test.tsx @@ -1,3 +1,5 @@ +import { verifyInviteToken } from "@/lib/jwt"; +import { findMatchingLocale } from "@/lib/utils/locale"; import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; import { getIsMultiOrgEnabled, @@ -7,9 +9,7 @@ import { import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen } from "@testing-library/react"; import { notFound } from "next/navigation"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { SignupPage } from "./page"; // Mock the necessary dependencies @@ -37,11 +37,11 @@ vi.mock("@/modules/auth/signup/lib/invite", () => ({ getIsValidInviteToken: vi.fn(), })); -vi.mock("@formbricks/lib/jwt", () => ({ +vi.mock("@/lib/jwt", () => ({ verifyInviteToken: vi.fn(), })); -vi.mock("@formbricks/lib/utils/locale", () => ({ +vi.mock("@/lib/utils/locale", () => ({ findMatchingLocale: vi.fn(), })); @@ -50,7 +50,7 @@ vi.mock("next/navigation", () => ({ })); // Mock environment variables and constants -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ IS_FORMBRICKS_CLOUD: false, POSTHOG_API_KEY: "mock-posthog-api-key", POSTHOG_HOST: "mock-posthog-host", @@ -111,7 +111,7 @@ describe("SignupPage", () => { cleanup(); }); - it("renders the signup page with all components when signup is enabled", async () => { + test("renders the signup page with all components when signup is enabled", async () => { // Mock the license check functions to return true vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); vi.mocked(getisSsoEnabled).mockResolvedValue(true); @@ -132,7 +132,7 @@ describe("SignupPage", () => { expect(screen.getByTestId("signup-form")).toBeInTheDocument(); }); - it("calls notFound when signup is disabled and no valid invite token is provided", async () => { + test("calls notFound when signup is disabled and no valid invite token is provided", async () => { // Mock the license check functions to return false vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); vi.mocked(verifyInviteToken).mockImplementation(() => { @@ -144,7 +144,7 @@ describe("SignupPage", () => { expect(notFound).toHaveBeenCalled(); }); - it("calls notFound when invite token is invalid", async () => { + test("calls notFound when invite token is invalid", async () => { // Mock the license check functions to return false vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); vi.mocked(verifyInviteToken).mockImplementation(() => { @@ -156,7 +156,7 @@ describe("SignupPage", () => { expect(notFound).toHaveBeenCalled(); }); - it("calls notFound when invite token is valid but invite is not found", async () => { + test("calls notFound when invite token is valid but invite is not found", async () => { // Mock the license check functions to return false vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); vi.mocked(verifyInviteToken).mockReturnValue({ @@ -170,7 +170,7 @@ describe("SignupPage", () => { expect(notFound).toHaveBeenCalled(); }); - it("renders the page with email from search params", async () => { + test("renders the page with email from search params", async () => { // Mock the license check functions to return true vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); vi.mocked(getisSsoEnabled).mockResolvedValue(true); diff --git a/apps/web/modules/auth/signup/page.tsx b/apps/web/modules/auth/signup/page.tsx index 3d8f2c2fbc..8b8ec9fa54 100644 --- a/apps/web/modules/auth/signup/page.tsx +++ b/apps/web/modules/auth/signup/page.tsx @@ -1,12 +1,3 @@ -import { FormWrapper } from "@/modules/auth/components/form-wrapper"; -import { Testimonial } from "@/modules/auth/components/testimonial"; -import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; -import { - getIsMultiOrgEnabled, - getIsSamlSsoEnabled, - getisSsoEnabled, -} from "@/modules/ee/license-check/lib/utils"; -import { notFound } from "next/navigation"; import { AZURE_OAUTH_ENABLED, DEFAULT_ORGANIZATION_ID, @@ -26,9 +17,18 @@ import { TERMS_URL, TURNSTILE_SITE_KEY, WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +} from "@/lib/constants"; +import { verifyInviteToken } from "@/lib/jwt"; +import { findMatchingLocale } from "@/lib/utils/locale"; +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { Testimonial } from "@/modules/auth/components/testimonial"; +import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; +import { + getIsMultiOrgEnabled, + getIsSamlSsoEnabled, + getisSsoEnabled, +} from "@/modules/ee/license-check/lib/utils"; +import { notFound } from "next/navigation"; import { SignupForm } from "./components/signup-form"; export const SignupPage = async ({ searchParams: searchParamsProps }) => { diff --git a/apps/web/modules/auth/verification-requested/page.tsx b/apps/web/modules/auth/verification-requested/page.tsx index b6d1fafac2..5250075594 100644 --- a/apps/web/modules/auth/verification-requested/page.tsx +++ b/apps/web/modules/auth/verification-requested/page.tsx @@ -1,7 +1,7 @@ +import { getEmailFromEmailToken } from "@/lib/jwt"; import { FormWrapper } from "@/modules/auth/components/form-wrapper"; import { RequestVerificationEmail } from "@/modules/auth/verification-requested/components/request-verification-email"; import { T, getTranslate } from "@/tolgee/server"; -import { getEmailFromEmailToken } from "@formbricks/lib/jwt"; import { ZUserEmail } from "@formbricks/types/user"; export const VerificationRequestedPage = async ({ searchParams }) => { @@ -14,7 +14,7 @@ export const VerificationRequestedPage = async ({ searchParams }) => { return ( <> -

    +

    {t("auth.verification-requested.please_confirm_your_email_address")}

    diff --git a/apps/web/modules/ee/auth/saml/lib/jackson.ts b/apps/web/modules/ee/auth/saml/lib/jackson.ts index 09a2e7caad..2b883c9316 100644 --- a/apps/web/modules/ee/auth/saml/lib/jackson.ts +++ b/apps/web/modules/ee/auth/saml/lib/jackson.ts @@ -1,9 +1,9 @@ "use server"; +import { SAML_AUDIENCE, SAML_DATABASE_URL, SAML_PATH, WEBAPP_URL } from "@/lib/constants"; import { preloadConnection } from "@/modules/ee/auth/saml/lib/preload-connection"; import { getIsSamlSsoEnabled } from "@/modules/ee/license-check/lib/utils"; import type { IConnectionAPIController, IOAuthController, JacksonOption } from "@boxyhq/saml-jackson"; -import { SAML_AUDIENCE, SAML_DATABASE_URL, SAML_PATH, WEBAPP_URL } from "@formbricks/lib/constants"; const opts: JacksonOption = { externalUrl: WEBAPP_URL, diff --git a/apps/web/modules/ee/auth/saml/lib/preload-connection.ts b/apps/web/modules/ee/auth/saml/lib/preload-connection.ts index 5a140971a7..70a0a14d5b 100644 --- a/apps/web/modules/ee/auth/saml/lib/preload-connection.ts +++ b/apps/web/modules/ee/auth/saml/lib/preload-connection.ts @@ -1,8 +1,8 @@ +import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@/lib/constants"; import { SAMLSSOConnectionWithEncodedMetadata, SAMLSSORecord } from "@boxyhq/saml-jackson"; import { ConnectionAPIController } from "@boxyhq/saml-jackson/dist/controller/api"; import fs from "fs/promises"; import path from "path"; -import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@formbricks/lib/constants"; import { logger } from "@formbricks/logger"; const getPreloadedConnectionFile = async () => { diff --git a/apps/web/modules/ee/auth/saml/lib/tests/jackson.test.ts b/apps/web/modules/ee/auth/saml/lib/tests/jackson.test.ts index 3cbc857b03..74bd151abd 100644 --- a/apps/web/modules/ee/auth/saml/lib/tests/jackson.test.ts +++ b/apps/web/modules/ee/auth/saml/lib/tests/jackson.test.ts @@ -1,11 +1,11 @@ +import { SAML_AUDIENCE, SAML_DATABASE_URL, SAML_PATH, WEBAPP_URL } from "@/lib/constants"; import { preloadConnection } from "@/modules/ee/auth/saml/lib/preload-connection"; import { getIsSamlSsoEnabled } from "@/modules/ee/license-check/lib/utils"; import { controllers } from "@boxyhq/saml-jackson"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { SAML_AUDIENCE, SAML_DATABASE_URL, SAML_PATH, WEBAPP_URL } from "@formbricks/lib/constants"; import init from "../jackson"; -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ SAML_AUDIENCE: "test-audience", SAML_DATABASE_URL: "test-db-url", SAML_PATH: "/test-path", diff --git a/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts b/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts index 5bb8c60f45..c122d57ec6 100644 --- a/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts +++ b/apps/web/modules/ee/auth/saml/lib/tests/preload-connection.test.ts @@ -1,11 +1,11 @@ +import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@/lib/constants"; import fs from "fs/promises"; import path from "path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { SAML_PRODUCT, SAML_TENANT, SAML_XML_DIR, WEBAPP_URL } from "@formbricks/lib/constants"; import { logger } from "@formbricks/logger"; import { preloadConnection } from "../preload-connection"; -vi.mock("@formbricks/lib/constants", () => ({ +vi.mock("@/lib/constants", () => ({ SAML_PRODUCT: "test-product", SAML_TENANT: "test-tenant", SAML_XML_DIR: "test-xml-dir", diff --git a/apps/web/modules/ee/billing/actions.ts b/apps/web/modules/ee/billing/actions.ts index ec62a483e0..bfc4999163 100644 --- a/apps/web/modules/ee/billing/actions.ts +++ b/apps/web/modules/ee/billing/actions.ts @@ -1,5 +1,8 @@ "use server"; +import { STRIPE_PRICE_LOOKUP_KEYS } from "@/lib/constants"; +import { WEBAPP_URL } from "@/lib/constants"; +import { getOrganization } from "@/lib/organization/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper"; @@ -7,9 +10,6 @@ import { createCustomerPortalSession } from "@/modules/ee/billing/api/lib/create import { createSubscription } from "@/modules/ee/billing/api/lib/create-subscription"; import { isSubscriptionCancelled } from "@/modules/ee/billing/api/lib/is-subscription-cancelled"; import { z } from "zod"; -import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; -import { getOrganization } from "@formbricks/lib/organization/service"; import { ZId } from "@formbricks/types/common"; import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts b/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts index 65d360bc51..55da0a307c 100644 --- a/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts +++ b/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts @@ -1,7 +1,7 @@ +import { STRIPE_API_VERSION } from "@/lib/constants"; +import { env } from "@/lib/env"; +import { getOrganization } from "@/lib/organization/service"; import Stripe from "stripe"; -import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; -import { env } from "@formbricks/lib/env"; -import { getOrganization } from "@formbricks/lib/organization/service"; import { ResourceNotFoundError } from "@formbricks/types/errors"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { diff --git a/apps/web/modules/ee/billing/api/lib/create-customer-portal-session.ts b/apps/web/modules/ee/billing/api/lib/create-customer-portal-session.ts index 3ca8942690..07466d33ef 100644 --- a/apps/web/modules/ee/billing/api/lib/create-customer-portal-session.ts +++ b/apps/web/modules/ee/billing/api/lib/create-customer-portal-session.ts @@ -1,6 +1,6 @@ +import { STRIPE_API_VERSION } from "@/lib/constants"; +import { env } from "@/lib/env"; import Stripe from "stripe"; -import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; -import { env } from "@formbricks/lib/env"; export const createCustomerPortalSession = async (stripeCustomerId: string, returnUrl: string) => { if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set."); diff --git a/apps/web/modules/ee/billing/api/lib/create-subscription.ts b/apps/web/modules/ee/billing/api/lib/create-subscription.ts index 4c33229ca0..9cdec4e45f 100644 --- a/apps/web/modules/ee/billing/api/lib/create-subscription.ts +++ b/apps/web/modules/ee/billing/api/lib/create-subscription.ts @@ -1,8 +1,8 @@ +import { STRIPE_API_VERSION, WEBAPP_URL } from "@/lib/constants"; +import { STRIPE_PRICE_LOOKUP_KEYS } from "@/lib/constants"; +import { env } from "@/lib/env"; +import { getOrganization } from "@/lib/organization/service"; import Stripe from "stripe"; -import { STRIPE_API_VERSION, WEBAPP_URL } from "@formbricks/lib/constants"; -import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; -import { env } from "@formbricks/lib/env"; -import { getOrganization } from "@formbricks/lib/organization/service"; import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { diff --git a/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts b/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts index 77b7cfd779..c829802c2f 100644 --- a/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts +++ b/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts @@ -1,5 +1,5 @@ +import { getOrganization, updateOrganization } from "@/lib/organization/service"; import Stripe from "stripe"; -import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; export const handleInvoiceFinalized = async (event: Stripe.Event) => { const invoice = event.data.object as Stripe.Invoice; diff --git a/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts b/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts index 8f584ffb81..4406d59da7 100644 --- a/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts +++ b/apps/web/modules/ee/billing/api/lib/is-subscription-cancelled.ts @@ -1,7 +1,7 @@ +import { STRIPE_API_VERSION } from "@/lib/constants"; +import { env } from "@/lib/env"; +import { getOrganization } from "@/lib/organization/service"; import Stripe from "stripe"; -import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; -import { env } from "@formbricks/lib/env"; -import { getOrganization } from "@formbricks/lib/organization/service"; import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { diff --git a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts index 8103599f58..c93bb0ae88 100644 --- a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts +++ b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts @@ -1,10 +1,10 @@ +import { STRIPE_API_VERSION } from "@/lib/constants"; +import { env } from "@/lib/env"; import { handleCheckoutSessionCompleted } from "@/modules/ee/billing/api/lib/checkout-session-completed"; import { handleInvoiceFinalized } from "@/modules/ee/billing/api/lib/invoice-finalized"; import { handleSubscriptionCreatedOrUpdated } from "@/modules/ee/billing/api/lib/subscription-created-or-updated"; import { handleSubscriptionDeleted } from "@/modules/ee/billing/api/lib/subscription-deleted"; import Stripe from "stripe"; -import { STRIPE_API_VERSION } from "@formbricks/lib/constants"; -import { env } from "@formbricks/lib/env"; import { logger } from "@formbricks/logger"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { diff --git a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts index 11fd9c81f5..575fb26f5f 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts @@ -1,7 +1,7 @@ +import { PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@/lib/constants"; +import { env } from "@/lib/env"; +import { getOrganization, updateOrganization } from "@/lib/organization/service"; import Stripe from "stripe"; -import { PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@formbricks/lib/constants"; -import { env } from "@formbricks/lib/env"; -import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { diff --git a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts index 3b6af9e808..3ba799dd83 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts @@ -1,6 +1,6 @@ +import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@/lib/constants"; +import { getOrganization, updateOrganization } from "@/lib/organization/service"; import Stripe from "stripe"; -import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@formbricks/lib/constants"; -import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/modules/ee/billing/components/billing-slider.tsx b/apps/web/modules/ee/billing/components/billing-slider.tsx index 44ee26bd58..e6190298bb 100644 --- a/apps/web/modules/ee/billing/components/billing-slider.tsx +++ b/apps/web/modules/ee/billing/components/billing-slider.tsx @@ -1,9 +1,9 @@ "use client"; +import { cn } from "@/lib/cn"; import * as SliderPrimitive from "@radix-ui/react-slider"; import { useTranslate } from "@tolgee/react"; import * as React from "react"; -import { cn } from "@formbricks/lib/cn"; interface SliderProps { className?: string; @@ -19,7 +19,7 @@ export const BillingSlider = React.forwardRef

    {t(plan.name)} diff --git a/apps/web/modules/ee/billing/components/pricing-table.tsx b/apps/web/modules/ee/billing/components/pricing-table.tsx index 81041838b6..99d851213a 100644 --- a/apps/web/modules/ee/billing/components/pricing-table.tsx +++ b/apps/web/modules/ee/billing/components/pricing-table.tsx @@ -1,13 +1,13 @@ "use client"; +import { cn } from "@/lib/cn"; +import { capitalizeFirstLetter } from "@/lib/utils/strings"; import { Badge } from "@/modules/ui/components/badge"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; -import { cn } from "@formbricks/lib/cn"; -import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings"; import { TOrganization, TOrganizationBillingPeriod } from "@formbricks/types/organizations"; import { isSubscriptionCancelledAction, manageSubscriptionAction, upgradePlanAction } from "../actions"; import { getCloudPricingData } from "../api/lib/constants"; @@ -146,7 +146,7 @@ export const PricingTable = ({
    -

    +

    {t("environments.settings.billing.current_plan")}:{" "} {capitalizeFirstLetter(organization.billing.plan)} {cancellingOn && ( @@ -201,7 +201,7 @@ export const PricingTable = ({

    {t("environments.settings.billing.monthly_identified_users")} @@ -224,7 +224,7 @@ export const PricingTable = ({

    {t("common.projects")}

    {organization.billing.limits.projects && ( @@ -260,7 +260,7 @@ export const PricingTable = ({ {t("environments.settings.billing.monthly")}
    handleMonthlyToggle("yearly")}> @@ -272,7 +272,7 @@ export const PricingTable = ({

    @@ -65,7 +65,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
    +

    {rowLabel}

    @@ -81,7 +81,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma percentage, questionSummary.data[rowIndex].totalResponsesForRow )}> -
    @@ -94,7 +94,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma ) }> {percentage} -
    +