diff --git a/.env.example b/.env.example index 2a8ba5eebb..1d3dd9e019 100644 --- a/.env.example +++ b/.env.example @@ -167,9 +167,9 @@ ENTERPRISE_LICENSE_KEY= # DEFAULT_ORGANIZATION_ID= # DEFAULT_ORGANIZATION_ROLE=owner -# Send new users to customer.io -# CUSTOMER_IO_API_KEY= -# CUSTOMER_IO_SITE_ID= +# Send new users to Brevo +# BREVO_API_KEY= +# BREVO_LIST_ID= # Ignore Rate Limiting across the Formbricks app # RATE_LIMITING_DISABLED=1 diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml deleted file mode 100644 index b9f8fb7b64..0000000000 --- a/.github/workflows/build-docs.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Build Docs -on: - workflow_call: - -permissions: - contents: read - -jobs: - build: - name: Build Docs - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/dangerous-git-checkout - - - name: Setup Node.js 20.x - uses: actions/setup-node@v3 - with: - node-version: 20.x - - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - name: Install dependencies - run: pnpm install --config.platform=linux --config.architecture=x64 - shell: bash - - - run: | - pnpm build --filter=@formbricks/docs... - shell: bash diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..abc3199b35 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '17 17 * * 6' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 8903418c61..1104dbbd0f 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,11 +1,11 @@ name: SonarQube on: workflow_dispatch: -# push: -# branches: -# - main -# pull_request: -# types: [opened, synchronize, reopened] + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] permissions: contents: read jobs: @@ -16,6 +16,35 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup Node.js 20.x + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: 20.x + + - name: Install pnpm + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 + + - name: Install dependencies + run: pnpm install --config.platform=linux --config.architecture=x64 + + - name: create .env + run: cp .env.example .env + + - name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env + run: | + RANDOM_KEY=$(openssl rand -hex 32) + sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env + sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env + sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env + + - name: Run tests with coverage + run: | + cd apps/web + pnpm test:coverage + cd ../../ + # The Vitest coverage config is in your vite.config.mts + - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 env: diff --git a/.github/workflows/tolgee-missing-key-check.yml b/.github/workflows/tolgee-missing-key-check.yml new file mode 100644 index 0000000000..869cd3965b --- /dev/null +++ b/.github/workflows/tolgee-missing-key-check.yml @@ -0,0 +1,39 @@ +name: Check Missing Translations + +permissions: + contents: read + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check-missing-translations: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install Tolgee CLI + run: npm install -g @tolgee/cli + + - name: Compare Tolgee Keys + id: compare + run: | + tolgee compare --api-key ${{ secrets.TOLGEE_API_KEY }} > compare_output.txt + cat compare_output.txt + + - name: Check for Missing Translations + run: | + if grep -q "new key found" compare_output.txt; then + echo "New keys found that may require translations:" + exit 1 + else + echo "No new keys found." + fi diff --git a/.github/workflows/tolgee.yml b/.github/workflows/tolgee.yml new file mode 100644 index 0000000000..c356c75981 --- /dev/null +++ b/.github/workflows/tolgee.yml @@ -0,0 +1,42 @@ +name: Tolgee Tagging on PR Merge +permissions: + contents: read + +on: + push: + branches: + - main + +jobs: + tag-production-keys: + name: Tag Production Keys + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 # Ensure compatibility with your project + + - name: Install Tolgee CLI + run: npm install -g @tolgee/cli + + - name: Tag Production Keys + run: | + BRANCH_NAME=${GITHUB_REF##*/} + npx tolgee tag \ + --api-key ${{ secrets.TOLGEE_API_KEY }} \ + --filter-extracted \ + --filter-tag "draft: ${BRANCH_NAME}" \ + --tag production \ + --untag "draft: ${BRANCH_NAME}" + + - name: Tag Deprecated Keys + run: | + npx tolgee tag \ + --api-key ${{ secrets.TOLGEE_API_KEY }} \ + --filter-not-extracted --filter-tag production \ + --tag deprecated --untag production diff --git a/.gitignore b/.gitignore index da9df0ff90..7060b9683f 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,5 @@ packages/lib/uploads # js compiled assets apps/web/public/js + packages/database/migrations \ No newline at end of file diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100644 index 0000000000..6f71e4bb57 --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1 @@ +echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json \ No newline at end of file diff --git a/.husky/post-commit b/.husky/post-commit new file mode 100644 index 0000000000..6f71e4bb57 --- /dev/null +++ b/.husky/post-commit @@ -0,0 +1 @@ +echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index e02c24e2b5..09809e56ab 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,5 @@ -pnpm lint-staged \ No newline at end of file +pnpm lint-staged +pnpm tolgee-pull || true +echo "{\"branchName\": \"main\"}" > ../branch.json +git add branch.json packages/lib/messages/*.json + diff --git a/.tolgeerc.json b/.tolgeerc.json new file mode 100644 index 0000000000..2538f88177 --- /dev/null +++ b/.tolgeerc.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://docs.tolgee.io/cli-schema.json", + "format": "JSON_TOLGEE", + "patterns": ["./apps/web/**/*.ts?(x)"], + "projectId": 10304, + "pull": { + "path": "./packages/lib/messages" + }, + "push": { + "files": [ + { + "language": "en-US", + "path": "./packages/lib/messages/en-US.json" + }, + { + "language": "de-DE", + "path": "./packages/lib/messages/de-DE.json" + }, + { + "language": "fr-FR", + "path": "./packages/lib/messages/fr-FR.json" + }, + { + "language": "pt-BR", + "path": "./packages/lib/messages/pt-BR.json" + } + ], + "forceMode": "OVERRIDE" + }, + "strictNamespace": false +} diff --git a/apps/demo-react-native/.env.example b/apps/demo-react-native/.env.example index 3a2d97bdc4..340aecb341 100644 --- a/apps/demo-react-native/.env.example +++ b/apps/demo-react-native/.env.example @@ -1,2 +1,2 @@ -EXPO_PUBLIC_API_HOST=http://192.168.178.20:3000 -EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=clzr04nkd000bcdl110j0ijyq +EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000 +EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1 \ No newline at end of file diff --git a/apps/demo-react-native/app.json b/apps/demo-react-native/app.json index 66cd17cbb8..31d6cb2a53 100644 --- a/apps/demo-react-native/app.json +++ b/apps/demo-react-native/app.json @@ -18,6 +18,7 @@ }, "jsEngine": "hermes", "name": "react-native-demo", + "newArchEnabled": true, "orientation": "portrait", "slug": "react-native-demo", "splash": { diff --git a/apps/demo-react-native/package.json b/apps/demo-react-native/package.json index 4dc5955136..acd06c3451 100644 --- a/apps/demo-react-native/package.json +++ b/apps/demo-react-native/package.json @@ -13,16 +13,17 @@ "dependencies": { "@formbricks/js": "workspace:*", "@formbricks/react-native": "workspace:*", - "expo": "52.0.18", - "expo-status-bar": "2.0.0", + "@react-native-async-storage/async-storage": "2.1.0", + "expo": "52.0.28", + "expo-status-bar": "2.0.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-native": "0.76.5", + "react-native": "0.76.6", "react-native-webview": "13.12.5" }, "devDependencies": { "@babel/core": "7.26.0", - "@types/react": "19.0.1", + "@types/react": "18.3.18", "typescript": "5.7.2" }, "private": true diff --git a/apps/demo-react-native/src/app.tsx b/apps/demo-react-native/src/app.tsx index 28e0f50552..a4816481e3 100644 --- a/apps/demo-react-native/src/app.tsx +++ b/apps/demo-react-native/src/app.tsx @@ -1,7 +1,14 @@ import { StatusBar } from "expo-status-bar"; import React, { type JSX } from "react"; import { Button, LogBox, StyleSheet, Text, View } from "react-native"; -import Formbricks, { track } from "@formbricks/react-native"; +import Formbricks, { + logout, + setAttribute, + setAttributes, + setLanguage, + setUserId, + track, +} from "@formbricks/react-native"; LogBox.ignoreAllLogs(); @@ -10,35 +17,92 @@ export default function App(): JSX.Element { throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required"); } - if (!process.env.EXPO_PUBLIC_API_HOST) { - throw new Error("EXPO_PUBLIC_API_HOST is required"); + if (!process.env.EXPO_PUBLIC_APP_URL) { + throw new Error("EXPO_PUBLIC_APP_URL is required"); } - const config = { - environmentId: process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string, - apiHost: process.env.EXPO_PUBLIC_API_HOST as string, - userId: "random-user-id", - attributes: { - language: "en", - testAttr: "attr-test", - }, - }; - return ( Formbricks React Native SDK Demo - - { setLoading(false); }} options={redocTheme} /> - {loading ? : null} - - ); -} diff --git a/apps/docs/app/api-docs/components/style.css b/apps/docs/app/api-docs/components/style.css deleted file mode 100644 index c5463636c3..0000000000 --- a/apps/docs/app/api-docs/components/style.css +++ /dev/null @@ -1,86 +0,0 @@ -:root[data-theme="light"] { - --text-color: rgb(51, 65, 85); -} - -:root[data-theme="dark"] { - --text-color: rgb(203, 213, 225); -} - -h5, -.sc-dhCplO, -.sc-dpBQxM { - color: var(--text-color) !important; -} - -.tab-success, -.react-tabs__tab, -.tab-error { - background-color: transparent !important; - border: 1px solid var(--text-color) !important; - margin: 10px 5px !important; -} - -.sc-dwGkES, -.sc-ePpfBx { - background-color: rgb(24, 24, 27) !important; - border: 1px solid var(--text-color) !important; -} -.sc-ePpfBx, -.corVrN { - background-color: rgb(24, 24, 27) !important; - border: none !important; -} -.cqdCbT { - display: none !important; -} -.kiMaJz, .iZNUDY { - align-items: center !important; -} - -.react-tabs__tab-panel > div { - padding: 0 !important; - border-radius: 8px !important; - overflow: hidden !important; -} -.sc-Rjrgp { - background-color: rgb(15, 23, 42) !important; - display: flex !important; - justify-content: center !important; - flex-direction: column !important; - padding: 8px 16px !important; -} -.cugBNu { - position: static !important; -} -.daIHdK { - padding: 0 !important; -} -.kqHNPM { - margin-top: 0px !important; -} -.sc-iwXfZk, -.redoc-json { - padding: 1rem !important; - background-color: rgb(24, 35, 58) !important; -} -.sc-uYFMi { - background-color: rgb(24, 35, 58) !important; - padding: 0.5rem 0 !important; - color: #2dd4bf !important; - font-weight: 600 !important; -} -.token { - color: #2dd4bf !important; -} - -.property { - color: rgb(203, 213, 225) !important; -} -.sc-iPHsxv { - background-color: rgb(15, 23, 42) !important; - border-radius: 8px !important; -} - -.sc-hSyjfr { - background-color: rgb(15, 23, 42) !important; -} diff --git a/apps/docs/app/api-docs/page.tsx b/apps/docs/app/api-docs/page.tsx deleted file mode 100644 index 8ae8c9d144..0000000000 --- a/apps/docs/app/api-docs/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ApiDocs } from "./components/api-docs"; - -export default function ApiDocsPage() { - return ; -} diff --git a/apps/docs/app/app-surveys/advanced-targeting/germans-gpt.webp b/apps/docs/app/app-surveys/advanced-targeting/germans-gpt.webp deleted file mode 100644 index a36ec28252..0000000000 Binary files a/apps/docs/app/app-surveys/advanced-targeting/germans-gpt.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/advanced-targeting/hni.webp b/apps/docs/app/app-surveys/advanced-targeting/hni.webp deleted file mode 100644 index ad56505d05..0000000000 Binary files a/apps/docs/app/app-surveys/advanced-targeting/hni.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/advanced-targeting/page.mdx b/apps/docs/app/app-surveys/advanced-targeting/page.mdx deleted file mode 100644 index e7a62f609d..0000000000 --- a/apps/docs/app/app-surveys/advanced-targeting/page.mdx +++ /dev/null @@ -1,29 +0,0 @@ -export const metadata = { - title: "Advanced Targeting for App Surveys | Formbricks", - description: - "Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, metadata, and other segments. This helps you get more relevant feedback and make data-driven decisions.", -}; - -# Advanced Targeting - -Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, device type, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code. - -## How to setup Advanced Targeting - -Advanced Targeting is only available on the Pro plan! - -1. On the Formbricks dashboard, click on **People** tab from the top navigation bar. - -2. Switch to the **Segments** tab & click on **Create Segment**. - -3. Give your segment a title & a description to help you remember what this segment is about. - -4. Now click on the **Add Filter** button to add a filter. You can filter based on user attributes, other segments, devices, and more. - -5. To group a set of filters together, click on the Three Dots icon on the right side of the filter and click on **Create Group**. - -6. Try playing around with different filters & conditions that we have provided to see how the segment size changes. - -7. Once you are happy with the segment, click on **Save Segment**. - -8. Now, when you create a survey, you can select this segment to target your survey to. diff --git a/apps/docs/app/app-surveys/advanced-targeting/power-users.webp b/apps/docs/app/app-surveys/advanced-targeting/power-users.webp deleted file mode 100644 index 4c1bb564a1..0000000000 Binary files a/apps/docs/app/app-surveys/advanced-targeting/power-users.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/advanced-targeting/ride-hailing.webp b/apps/docs/app/app-surveys/advanced-targeting/ride-hailing.webp deleted file mode 100644 index 694cd90790..0000000000 Binary files a/apps/docs/app/app-surveys/advanced-targeting/ride-hailing.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/advanced-targeting/upsell-miro.webp b/apps/docs/app/app-surveys/advanced-targeting/upsell-miro.webp deleted file mode 100644 index 8da20d2c9e..0000000000 Binary files a/apps/docs/app/app-surveys/advanced-targeting/upsell-miro.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/framework-guides/components/libraries.tsx b/apps/docs/app/app-surveys/framework-guides/components/libraries.tsx deleted file mode 100644 index f3c0597516..0000000000 --- a/apps/docs/app/app-surveys/framework-guides/components/libraries.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import Image from "next/image"; -import { Button } from "@/components/button"; -import logoHtml from "@/images/frameworks/html5.svg"; -import logoNextjs from "@/images/frameworks/nextjs.svg"; -import logoReactJs from "@/images/frameworks/reactjs.svg"; -import logoVueJs from "@/images/frameworks/vuejs.svg"; - -const libraries = [ - { - href: "#html", - name: "HTML", - description: "All you need to do is add 3 lines of code to your HTML script and thats it, you're done!", - logo: logoHtml as string, - }, - { - href: "#react-js", - name: "React.js", - description: "Load the our Js library with your environment ID and you're ready to go!", - logo: logoReactJs as string, - }, - { - href: "#next-js", - name: "Next.js", - description: - "Natively add us to your NextJs project with support for both App as well as Pages project structure!", - logo: logoNextjs as string, - }, - { - href: "#vue-js", - name: "Vue.js", - description: "Simply add us to your router change and sit back!", - logo: logoVueJs as string, - }, - { - href: "#react-native", - name: "React Native", - description: "Easily integrate our SDK with your React Native app for seamless survey support!", - logo: logoReactJs as string, - }, -]; - -export function Libraries() { - return ( -
- -
- ); -} diff --git a/apps/docs/app/app-surveys/framework-guides/images/env-id.webp b/apps/docs/app/app-surveys/framework-guides/images/env-id.webp deleted file mode 100644 index 1c148f2e64..0000000000 Binary files a/apps/docs/app/app-surveys/framework-guides/images/env-id.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/framework-guides/images/react-in-app-survey-app-popup-form.webp b/apps/docs/app/app-surveys/framework-guides/images/react-in-app-survey-app-popup-form.webp deleted file mode 100644 index ae74a5d73b..0000000000 Binary files a/apps/docs/app/app-surveys/framework-guides/images/react-in-app-survey-app-popup-form.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/framework-guides/images/widget-connected.webp b/apps/docs/app/app-surveys/framework-guides/images/widget-connected.webp deleted file mode 100644 index 90b82de9f4..0000000000 Binary files a/apps/docs/app/app-surveys/framework-guides/images/widget-connected.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/framework-guides/images/widget-not-connected.webp b/apps/docs/app/app-surveys/framework-guides/images/widget-not-connected.webp deleted file mode 100644 index 2cd15d007c..0000000000 Binary files a/apps/docs/app/app-surveys/framework-guides/images/widget-not-connected.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/framework-guides/page.mdx b/apps/docs/app/app-surveys/framework-guides/page.mdx deleted file mode 100644 index deca5bbcf8..0000000000 --- a/apps/docs/app/app-surveys/framework-guides/page.mdx +++ /dev/null @@ -1,453 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import { Libraries } from "./components/libraries"; - -import ReactApp from "./images/react-in-app-survey-app-popup-form.webp"; -import WidgetConnected from "./images/widget-connected.webp"; -import WidgetNotConnected from "./images/widget-not-connected.webp"; - -export const metadata = { - title: "Integrate Formbricks: Comprehensive Framework Guide & Integration Tutorial", - description: - "Master the integration of Formbricks into your application with our detailed guides. From HTML to ReactJS, NextJS, and VueJS, get step-by-step instructions and ensure seamless setup.", -}; - -# Framework Guides - -One can integrate Formbricks App Survey SDK into their app using multiple options! Checkout the options below that we provide! If you are looking -for something else, please [open a new Github Discussion](https://github.com/formbricks/formbricks/discussions) and we would be glad to help. - - - ---- - -## Prerequisites - -Before getting started, make sure you have: - -1. A web application (behind your user authentication system) in your desired framework is set up and running. -2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings. - ---- - -## HTML - -All you need to do is copy a ` - -``` - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - URL of the hosted Formbricks instance. - - - -Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - ---- - -## ReactJS - -Install the Formbricks SDK using one of the package managers ie `npm`,`pnpm`,`yarn`. Note that zod is required as a peer dependency must also be installed in your project. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/js zod -``` -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/js zod -``` -```shell {{ title: 'yarn' }} -yarn add @formbricks/js zod -``` - - - -Now, update your App.js/ts file to initialise Formbricks. - - - -```js -// other imports -import formbricks from "@formbricks/js"; - -if (typeof window !== "undefined") { - formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", //optional - }); -} - -function App() { - // your own app -} - -export default App; -``` - - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - URL of the hosted Formbricks instance. - - - -Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - ---- - -## NextJS - -NextJs projects typically follow two main conventions: the App Directory and the Pages Directory. -To ensure smooth integration with the Formbricks SDK, which operates solely on the client side, follow the -guidelines for each convention below: - -- App directory: You will have to define a new component in `app/formbricks.tsx` file and call it in your `app/layout.tsx` file. -- Pages directory: You will have to visit your `_app.tsx` and just initialise Formbricks there. - -Code snippets for the integration for both conventions are provided to further assist you. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/js zod -``` -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/js zod -``` -```shell {{ title: 'yarn' }} -yarn add @formbricks/js zod -``` - - - - -### App Directory - - - - -```tsx {{title: 'Typescript'}} -"use client"; - -import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect } from "react"; -import formbricks from "@formbricks/js"; - -export default function FormbricksProvider() { - const pathname = usePathname(); - const searchParams = useSearchParams(); - - useEffect(() => { - formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", //optional - }); - }, []); - - useEffect(() => { - formbricks?.registerRouteChange(); - }, [pathname, searchParams]); - - return null; -} -``` - - - - -```tsx {{title: 'Typescript'}} -// other imports -import FormbricksProvider from "./formbricks"; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - ); -} -``` - - - - -### Pages Directory - - - - -```tsx {{ title: 'Typescript' }} -// other import -import { useRouter } from "next/router"; -import { useEffect } from "react"; -import formbricks from "@formbricks/js"; - -if (typeof window !== "undefined") { - formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", //optional - }); -} - -export default function App({ Component, pageProps }: AppProps) { - const router = useRouter(); - - useEffect(() => { - // Connect next.js router to Formbricks - const handleRouteChange = formbricks?.registerRouteChange; - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - }, []); - return ; -} -``` - - - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - URL of the hosted Formbricks instance. - - - -First we initialize the Formbricks SDK, ensuring that it only runs on the client side. -To connect the Next.js router to Formbricks and ensure the SDK can keep track of every page change, we are registering the route change event. - -Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - ---- - -## VueJs - -Integrating the Formbricks SDK with Vue.js is a straightforward process. -We will make sure the SDK is only loaded and used on the client side, as it's not intended for server-side usage. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/js -```` - -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/js -``` - -```shell {{ title: 'yarn' }} -yarn add @formbricks/js -``` - - - - - -```js -import formbricks from "@formbricks/js"; - -if (typeof window !== "undefined") { - formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", //optional - }); -} - -export default formbricks; -``` - - - - - -```js -// other imports -import formbricks from "@/formbricks"; - -const app = createApp(App); - -app.use(router); - -app.mount("#app"); - -router.afterEach((to, from) => { - if (typeof formbricks !== "undefined") { - formbricks.registerRouteChange(); - } -}); -``` - - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - - - URL of the hosted Formbricks instance. - - - -Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - -## React Native - -Install the Formbricks React Native SDK using one of the package managers, i.e., npm, pnpm, or yarn. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/react-native -``` -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/react-native -``` -```shell {{ title: 'yarn' }} -yarn add @formbricks/react-native -``` - - - -Now, update your App.js/App.tsx file to initialize Formbricks: - - - -```js -// other imports -import Formbricks from "@formbricks/react-native"; - -const config = { - environmentId: "", - apiHost: "", - userId: "", // optional -}; - -export default function App() { - return ( - <> - {/* Your app content */} - - - ); -} -``` - - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - URL of the hosted Formbricks instance. - - - ---- - -## Validate your setup - -Once you have completed the steps above, you can validate your setup by checking the **Setup Checklist** in the Settings. Your widget status indicator should go from this: - - - -To this: - - - -## Debugging Formbricks Integration - -Enabling Formbricks debug mode in your browser is a useful troubleshooting step for identifying and resolving complex issues. This section outlines how to activate debug mode, covers common use cases, and provides insights into specific debug log messages. - -### Activate Debug Mode - -To activate Formbricks debug mode: - -1. **Via URL Parameter:** - - - Enable debug mode mode by adding `?formbricksDebug=true` to your application's URL (e.g. `https://example.com?formbricksDebug=true` or `https://example.com?page=123&formbricksDebug=true`). This parameter will enable debugging for the current page. - -2. **View Debug Logs:** - - - Open your browser's developer tools by pressing `F12` or right-clicking and selecting "Inspect." - - Navigate to the "Console" tab to view Formbricks debugging information. - - **How to Open Browser Console:** - - - **Google Chrome:** Press `F12` or right-click, select "Inspect," and go to the "Console" tab. - - **Firefox:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab. - - **Safari:** Press `Option + Command + C` to open the developer tools and navigate to the "Console" tab. - - **Edge:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab. - -### Common Use Cases - -Debug mode is beneficial for scenarios such as: - -- Verifying Formbricks initialization. -- Identifying survey trigger issues. -- Troubleshooting unexpected behavior. - -### Debug Log Messages - -Debug log messages provide insights into: - -- API calls and responses. -- Event tracking, survey triggers and form interactions. -- Initialization errors. - -**Can’t figure it out?**: **[Get help in Github Discussions](https://github.com/formbricks/formbricks/discussions)** - ---- diff --git a/apps/docs/app/app-surveys/quickstart/images/i1.webp b/apps/docs/app/app-surveys/quickstart/images/i1.webp deleted file mode 100644 index 0714f4a19a..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i1.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i2.webp b/apps/docs/app/app-surveys/quickstart/images/i2.webp deleted file mode 100644 index 66b2db4cca..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i2.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i3.webp b/apps/docs/app/app-surveys/quickstart/images/i3.webp deleted file mode 100644 index 76b9658d5d..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i3.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i3_1.webp b/apps/docs/app/app-surveys/quickstart/images/i3_1.webp deleted file mode 100644 index 15b7efe1c0..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i3_1.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i4.webp b/apps/docs/app/app-surveys/quickstart/images/i4.webp deleted file mode 100644 index f7bae96938..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i4.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i5.webp b/apps/docs/app/app-surveys/quickstart/images/i5.webp deleted file mode 100644 index 1962df70a8..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i5.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i6.webp b/apps/docs/app/app-surveys/quickstart/images/i6.webp deleted file mode 100644 index a24c2af56d..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i6.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i7.webp b/apps/docs/app/app-surveys/quickstart/images/i7.webp deleted file mode 100644 index 4a3a352d2d..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i7.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/images/i8.webp b/apps/docs/app/app-surveys/quickstart/images/i8.webp deleted file mode 100644 index e5c0ab7442..0000000000 Binary files a/apps/docs/app/app-surveys/quickstart/images/i8.webp and /dev/null differ diff --git a/apps/docs/app/app-surveys/quickstart/page.mdx b/apps/docs/app/app-surveys/quickstart/page.mdx deleted file mode 100644 index 7a0f32db1b..0000000000 --- a/apps/docs/app/app-surveys/quickstart/page.mdx +++ /dev/null @@ -1,115 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import I1 from "./images/i1.webp"; -import I2 from "./images/i2.webp"; -import I3 from "./images/i3.webp"; -import I3_1 from "./images/i3_1.webp"; -import I4 from "./images/i4.webp"; -import I5 from "./images/i5.webp"; -import I6 from "./images/i6.webp"; -import I7 from "./images/i7.webp"; -import I8 from "./images/i8.webp"; - -export const metadata = { - title: "Formbricks Quickstart Guide: App Surveys Made Easier & Faster", - description: - "Formbricks is the easiest way to create and manage app surveys. This quickstart guide will show you how to create your first app survey in under 5 minutes.", -}; - -# Quickstart - -App surveys have 6-10x better conversion rates than emailed surveys. This tutorial explains how to run a survey in both your web app and mobile app (React Native) in just 10 to 15 minutes. Let’s go! - -1. **Create a free Formbricks Cloud account**: While you can [self-host](/self-hosting/deployment) Formbricks, but the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and you'll be guided to our onboarding like below, choose the "Formbricks Surveys" option: - - - -2. **Choose your Project Channel**: On this step, you have to choose between the various channels that you want your project to be created in, you can create both app and link surveys from all the channels, but for the onboarding, please choose between the app surveys or the public website options, upon doing this, you'll be prompted to connect your app / website to formbricks. - - - -3. **Connect your App/Website**: Once you get through a couple of onboarding steps, you’ll be asked to connect your app or website. This is where you’ll find the code snippet for both HTML as well as the npm package which you need to embed in your app: - - - -Paste the code snippet in your app and reload the page. You should now see a message being displayed that your app or website is now connected with formbricks. - - - -Onboarding is complete! Now let’s create our first survey as you should see templates to choose from after clicking on **Next**: - - - -4. **Create your first survey**: To be able to see a survey in your app, you need to create one. We’ll choose one of the templates and head over to the survey settings: - -Pick the Survey Type as **App Survey**. - - - -5. **Set Trigger for the Survey**: Scroll down to Survey Trigger and click on **+ Add action**, choose **New Session**. This will cause this survey to appear when the Formbricks Widget tracks a new user session: - - - -6. **Set Recontact Options for debugging**: In Recontact Options we choose the following settings, so that we can play around with the survey more easily. By default, each survey will be shown only once to each user to prevent survey fatigue: - - - Please change this setting later on after testing your survey to prevent survey fatigue for your users. - - - - -7. **Publish your survey**: Now hit **Publish** and you’ll be forwarded to the Summary Page. This is where you’ll find the responses to this survey. - - - ---- - -- We offer framework guides for various frontend tech, head over to the the [App Survey Framework Guides](/app-surveys/framework-guides) to get started with your app survey. -- Head over to our JS SDK documentation to get started with the [JS SDK](/developer-docs/js-sdk). - -Still struggling or something not working as expected? [Join us in Github Discussions](https://github.com/formbricks/formbricks/discussions) and we'd be glad to assist you! diff --git a/apps/docs/app/app-surveys/user-identification/page.mdx b/apps/docs/app/app-surveys/user-identification/page.mdx deleted file mode 100644 index 9fb7ccb67b..0000000000 --- a/apps/docs/app/app-surveys/user-identification/page.mdx +++ /dev/null @@ -1,115 +0,0 @@ -export const metadata = { - title: "Identifying Users in Formbricks App Surveys", - description: - "Dive into the importance of user identification in surveys. Boost your survey response rates and target the right users with Formbricks.", -}; - -# User Identification - -User Identification helps you to not only segment your users but also to see more information about the user who responded to a survey. This helps you to target surveys to specific user segments and see more information about the user who responded to a survey. - -### Understanding Identified vs Unidentified Users - -In Formbricks, understanding the distinction between identified and unidentified users is crucial for effective survey segmentation and targeted feedback collection. - -| Feature | Unidentified Users | Identified Users | -| -------------------------------------------------------- | ------------------ | ---------------- | -| Show surveys based on **trigger** actions | ✅ | ✅ | -| Set **recontact details** to avoid survey fatique | ✅ | ✅ | -| Target a subset of users using **attributes & segments** | ❌ | ✅ | -| Collect **user information** in Formbricks | ❌ | ✅ | -| Track **custom attributes** | ❌ | ✅ | -| Counts towards your **monthly tacked user (MTU)** limit | ❌ | ✅ | -| Recommended for **public-facing websites** | ✅ | ❌ | -| Recommended for **web apps after login** | ❌ | ✅ | - -## Identified Users - -Identified users are those for whom specific information has been set, notably the userId. This identification allows for more precise targeting of surveys and a deeper understanding of the feedback provided. When enabled, all information specified by you and all actions are sent to Formbricks. - -This method is recommended for applications where users are required to log in and will often return. - -### Setting User ID - -To enable the User identification feature you need to set the `userId` in the init() call of Formbricks. Only when the `userId` is set the person will be visible in the Formbricks dashboard. The `userId` can be any string and it's best to use the default identifier you use in your app (e.g. unique id from database or the email address if it's unique) but you can also anonymize these as long as they are unique for every user. - - - - -```javascript -formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", -}); -``` - - - - -### Enhanced Initialization with User Attributes - -In addition to setting the `userId`, Formbricks allows you to set user attributes right at the initialization. This ensures that your user data is seamlessly integrated from the start. Here's how you can include user attributes in the `init()` function: - - - - -```javascript -formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", - attributes: { - // your custom attributes - Plan: "premium", - }, -}); -``` - - - - -## Setting Custom User Attributes - -You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.): - - - Please note that the number of different attribute classes (e.g., "Plan," "First Name," etc.) is currently limited to 150 attributes per environment. - - - - - -```javascript -formbricks.setAttribute("Plan", "free"); -``` - - - - -Generally speaking, the setAttribute function works like this: - - - - -```javascript -formbricks.setAttribute("attribute_key", "attribute_value"); -``` - - - -Where `attributeName` is the name of the attribute you want to set, and `attributeValue` is the value of the attribute you want to set. - -### Logging Out Users - -When a user logs out of your webpage, make sure to log them out of Formbricks as well. This will prevent new activity from being associated with an incorrect user. Use the logout function: - - - - -```javascript -formbricks.logout(); -``` - - - diff --git a/apps/docs/app/best-practices/cancel-subscription/page.mdx b/apps/docs/app/best-practices/cancel-subscription/page.mdx deleted file mode 100644 index 8aa9958afa..0000000000 --- a/apps/docs/app/best-practices/cancel-subscription/page.mdx +++ /dev/null @@ -1,155 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import ChangeText from "./change-text.webp"; -import CreateChurnFlow from "./create-cancel-flow.webp"; -import PublishSurvey from "./publish-survey.webp"; -import RecontactOptions from "./recontact-options.webp"; -import SelectAction from "./select-action.webp"; -import TriggerCSS from "./trigger-css-selector.webp"; -import TriggerInnerText from "./trigger-inner-text.webp"; -import TriggerPageUrl from "./trigger-page-url.webp"; - -export const metadata = { - title: "Mastering Churn Surveys with Formbricks | Essential Tips & Steps", - description: - "Learn how to effectively utilize Formbricks' Churn Surveys to gain deeper insights into user departures. Dive into a step-by-step guide to craft, trigger, and optimize your churn surveys, ensuring you capture invaluable feedback at critical junctures", -}; - -#### Best Practices - -# Learn from Churn - -Churn is hard, but can teach you a lot. Whenever a user decides that your product isn’t worth it anymore, you have a unique opportunity to get deep insights. These insights are pure gold to reduce churn. - -## Purpose - -The Churn Survey is among the most effective ways to identify weaknesses in your offering. People were willing to pay but now are not anymore: What changed? Let’s find out! - -## Formbricks Approach - -- Ask at exactly the right point in time -- Follow-up to prevent bad reviews -- Coming soon: Make survey mandatory - -## Overview - -To run the Churn Survey in your app you want to proceed as follows: - -1. Create new Churn Survey at [app.formbricks.com](https://app.formbricks.com/) -2. Set up the user action to display survey at right point in time -3. Choose correct recontact options to never miss a feedback -4. Prevent that churn! - - - ## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/app-surveys/quickstart) - - -### 1. Create new Churn Survey - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Click on "Create Survey" and choose the template “Churn Survey”: - - - -### 2. Update questions (if you like) - -You’re free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊 - - - -_Want to change the button color? You can do so in the project settings._ - -Save, and move over to the “Audience” tab. - -### 3. Pre-segment your audience - -In this case, you don’t really need to pre-segment your audience. You likely want to ask everyone who hits the “Cancel subscription” button. - -### 4. Set up a trigger - -To create the trigger for your Churn Survey, you have three options to choose from: - -1. **Trigger by Inner Text:** You likely have a “Cancel Subscription” button in your app. You can setup a user Action with the according `Inner Text` to trigger the survey, like so: - - - -2. **Trigger by CSS Selector:** In case you have more than one button saying “Cancel Subscription” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“cancel-subscription”` and set your user action up like so: - - - -1. **Trigger by page view filters:** Lastly, you could also display your survey on a subpage “/subscription-cancelled” where you forward users once they cancelled the trial subscription. You can then create a user Action with the type `Page View` and add select `Limit to specific pages` to add url filters, with the following settings: - - - -Whenever a user visits this page, matches the filter conditions above and the recontact options (below) the survey will be displayed ✅ - -Here is our complete [Actions manual](/app-surveys/actions/) covering [No-Code](/app-surveys/actions#setting-up-no-code-actions) and [Code](/app-surveys/actions#setting-up-code-actions) Actions. - - - ## Pre-churn flow coming soon We’re currently building full-screen survey pop-ups. You’ll be able to prevent users from closing the survey unless they respond to it. It’s certainly debatable if you want that but you could force them to click through the survey before letting them cancel 🤷 - - -### 5. Select Action in the “When to ask” card - - - -### 6. Last step: Set Recontact Options correctly - -Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you milk these super valuable insights. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days: - - - -These settings make sure the survey is always displayed, when a user wants to Cancel their subscription. - -### 7. Congrats! You’re ready to publish your survey 💃 - - - - - ## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Churn Survey in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart) to install the widget. - - -### - -# Get those insights! 🎉 diff --git a/apps/docs/app/best-practices/contact-form/page.mdx b/apps/docs/app/best-practices/contact-form/page.mdx deleted file mode 100644 index d932e0898d..0000000000 --- a/apps/docs/app/best-practices/contact-form/page.mdx +++ /dev/null @@ -1,174 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; -import AddQuestion from "./images/add-question.webp"; -import EmailField from "./images/email-field.webp"; -import EmbedImage from "./images/embed.webp"; -import MessageField from "./images/message-field.webp"; -import NameField from "./images/name-field.webp"; -import QueryImage from "./images/query-form.webp"; -import SingleSelect from "./images/single-select-questionare.webp"; -import ToggleButton from "./images/welcome1.webp"; - -export const metadata = { - title: "Creating a Contact Form with Formbricks: A Step-by-Step Guide", - description: - "Learn how to easily create a professional contact form using Formbricks, perfect for beginners and experts alike.", -}; - -# Creating a Contact Form with Formbricks: A Step-by-Step Guide - -Welcome to this comprehensive guide on creating a contact form using Formbricks. Whether you're just starting out or you're a seasoned developer, this tutorial will walk you through every step of building an engaging and effective contact form. - -## What We’ll Build - -By the end of this tutorial, you'll have created a simple contact form featuring: - -1. A welcoming introduction. -2. Fields for collecting the user's name and email. -3. A question to find out why they’re contacting you. -4. A message field for users to share their queries. - -### Setting Up Your Form - -First, let's lay the groundwork for your form: - -1. Head to the Surveys page and click on **New Survey**. -2. Select **Start from Scratch** to create a new form. -3. In the form editor, click the three dots next to a question, then select **Change Question Type** and choose **Statement (Call to Action)**. - - - -4. Add a welcoming statement to greet your users and explain the form's purpose. -5. Personalize the greeting to make it inviting and encourage engagement. - -A warm welcome sets the tone for your form. Make it friendly to encourage users to participate. - -### Adding the Name Field - -Next, let's capture the user's name: - -1. Click **Add Question**. - -2. Enter the prompts for the name field. -3. Turn off the **Long Answer** option at the bottom right. -4. Adjust any **settings**, such as making the field required. - - - -### Adding the Email Field - -Now, let’s add a field to collect the user's email address: - -1. Click **Add Question** again. -2. Select Email as the input type. -3. Enter a prompt for the email field. - - - -### Adding a Reason for Contact - -Let’s now understand why the user is contacting you: - -1. Click **Add Question** once again. -2. Select **Change Question** Type and choose **Single Select**. -3. Add the question "Why are you contacting us today?" - - - -Predefined options help categorize inquiries, making it easier for you to respond appropriately. - -4. Add options like "General Inquiry," "Support," and "Feedback." - - - -### Adding a Message Field - -Finally, let’s provide a space for the user’s message: - -1. Click **Add Question** for the last time. -2. Add the question: "Your Message." -3. Set the placeholder text to something like "Please write your message here." - - - -4. Consider setting a minimum character count to ensure detailed messages. - -### Finalizing Your Form - -Once your form is complete, follow these final steps: - -1. Review and rearrange the questions if necessary. -2. Test the form by filling it out as a user. -3. Customize the **Thank You** message for submissions. -4. Publish the form to get a shareable link. -5. Enable submission notifications: - - Go to your Formbricks account settings. - - Verify your email address. - - Ensure that **Survey** notifications are enabled. - -### Integrating the Contact Form into Your Website - -After publishing the form, follow these steps to integrate it into your site: - -1. **Copy the Shareable Link** - - - Find your form in the Formbricks dashboard, and click Share. - - Select Embed in a Web Page. - - - -2. **Embed the Code** - - Copy the provided code and paste it into your website where you want the form to appear. - - - Note: There is an options toggle button called "Embed Mode." When enabled, it updates the `src` to `"?embed=true"` and displays your survey in a minimalist design, removing padding and background for a cleaner look. - - -3. **Test the Integration** - - Check if the form displays correctly on your site. - - Submit a test entry to ensure everything works and notifications are received. - -## Conclusion - -Congratulations! You’ve successfully created and integrated a professional contact form using Formbricks. This form will help you collect valuable information from your visitors in an efficient, user-friendly way. - -A great contact form strikes the balance between collecting necessary details and being simple enough to encourage submissions. **You’ve achieved just that!** diff --git a/apps/docs/app/best-practices/feature-chaser/page.mdx b/apps/docs/app/best-practices/feature-chaser/page.mdx deleted file mode 100644 index 3b33d14607..0000000000 --- a/apps/docs/app/best-practices/feature-chaser/page.mdx +++ /dev/null @@ -1,128 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import ActionCSS from "./action-css.webp"; -import ActionText from "./action-innertext.webp"; -import ChangeText from "./change-text.webp"; -import CreateSurvey from "./create-survey.webp"; -import Publish from "./publish.webp"; -import RecontactOptions from "./recontact-options.webp"; -import SelectAction from "./select-action.webp"; - -export const metadata = { - title: "Setting Up Feature Chaser Surveys with Formbricks: A Comprehensive Guide", - description: - "Learn how to harness the power of Formbricks to gather targeted user feedback on specific features. Dive deep into creating, triggering, and publishing the Feature Chaser survey to enhance your product with actionable insights for specific users.", -}; - -#### Best Practices - -# Feature Chaser - -Following up on specific features only makes sense with very targeted surveys. Formbricks is built for that. - -## Purpose - -Product analytics never tell you why a feature is used - and why not. Following up on specific features with highly relevant questions is a great way to gather feedback and improve your product. - -## Formbricks Approach - -- Trigger survey at exactly the right point in the user journey -- Never ask twice, keep your data clean -- Prevent survey fatigue with global waiting period - -## Overview - -To run the Feature Chaser survey in your app you want to proceed as follows: - -1. Create new Feature Chaser survey at [app.formbricks.com](https://app.formbricks.com/) -2. Setup a user action to display survey at the right point in time - - - ## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/app-surveys/quickstart) - - -### 1. Create new Feature Chaser - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Click on "Create Survey" and choose the template “Feature Chaser”: - - - -### 2. Update questions - -The questions you want to ask are dependent on your feature and can be very specific. In the template, we suggest a high-level check on how easy it was for the user to achieve their goal. We also add an opportunity to provide context: - - - -Save, and move over to where the magic happens: The “Audience” tab. - -### 3. Set up a trigger for the Feature Chaser survey: - -Before setting the right trigger, you need to identify a user action in your app which signals, that they have just used the feature you want to understand better. In most cases, it is clicking a specific button in your product. - -You can create [Code Actions](/app-surveys/actions#setting-up-code-actions) and [No Code Actions](/app-surveys/actions#setting-up-no-code-actions) to follow users through your app. In this example, we will create a No Code Action. - -There are two ways to track a button: - -1. **Trigger by Inner Text:** You might have a button with a unique text at the end of your feature e.g. "Export Report". You can setup a user Action with the according `Inner Text` to trigger the survey, like so: - - - -2. **Trigger by CSS Selector:** In case you have more than one button saying “Export Report” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“export-report-featurename”` and set your user action up like so: - - - -Please follow our [Actions manual](/actions/why) for an in-depth description of how Actions work. - -### 4. Select Action in the “When to ask” card - - - -### 5. Last step: Set Recontact Options correctly - -Lastly, scroll down to “Recontact Options”. Here you have full freedom to decide who you want to ask. Generally, you only want to ask every user once and prevent survey fatigue. It's up to you to decide if you want to ask again, when the user did not yet reply: - - - -### 7. Congrats! You’re ready to publish your survey 💃 - - - - - ## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feature Chaser in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart) to install the widget. - - -### - -# Get those insights! 🎉 diff --git a/apps/docs/app/best-practices/feedback-box/page.mdx b/apps/docs/app/best-practices/feedback-box/page.mdx deleted file mode 100644 index 6b71242e23..0000000000 --- a/apps/docs/app/best-practices/feedback-box/page.mdx +++ /dev/null @@ -1,118 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; -import Link from "next/link"; - -import ActionCSS from "./action-css.webp"; -import ActionText from "./action-innertext.webp"; -import AddAction from "./add-action.webp"; -import ChangeTextContent from "./change-text-content.webp"; -import CreateFeedbackBox from "./create-feedback-box-by-template.webp"; -import PublishSurvey from "./publish-survey.webp"; -import SelectAction from "./select-action.webp"; -import RecontactOptions from "./set-recontact-options.webp"; - -export const metadata = { - title: "Implementing the Feedback Box with Formbricks: A Step-by-Step Tutorial", - description: - "Unlock user insights effortlessly! Discover how to set up the Feedback Box in your app using Formbricks, allowing your users to provide real-time feedback. Follow our comprehensive guide to enhance user experience and respond rapidly to feedback", -}; - -#### Best Practices - -# Feedback Box - -The Feedback Box gives your users a direct channel to share their feedback and feel heard. - -## Purpose - -A low friction way to gather feedback helps catching even the smallest points of frustration in user experiences. Use automations to react rapidly and make users feel heard. - -## Formbricks Approach - -- Make it easy: 2 clicks to share feedback -- Pipe insights where team can see them and react quickly - -## Installation - -To add the Feedback Box to your app, you need to perform these steps: - -1. Create new Feedback Box at app.formbricks.com -2. Add user action to trigger Feedback Box -3. Update recontact settings to display correctly - -### 1. Create new Feedback Box - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Then, create a new survey and look for the "Feedback Box" template: - - - -### 2. Update question content - -Change the questions and answer options according to your preference: - - - -### 3. Create user action to trigger Feedback Box: - -Go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool User Action Tracker: - - - -We have two options to track the Feedback Button in your application: innerText and CSS-Selector: - -1. **Inner Text:** This means that whenever a user clicks any HTML item in your app which has an `Inner Text` of `Feedback` the Feedback Box will be displayed. - - - -2. **CSS Selector:** This means that when an element with a specific CSS-Selector like `#feedback-button` is clicked, your Feedback Box is triggered. - - -### 4. Select action in the “When to ask” card - - - -### 5. Set Recontact Options correctly - -Scroll down to “Recontact Options”. Here you have to choose the right settings so that the Feedback Box pops up every time the user action is performed. (Our default is that every user sees every survey only once): - - - -### 6. You’re ready publish your survey! - - - -## Setting up the Widget - - - ## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart) to install the widget. - - -###   - -# That’s it! 🎉 diff --git a/apps/docs/app/best-practices/improve-email-content/page.mdx b/apps/docs/app/best-practices/improve-email-content/page.mdx deleted file mode 100644 index 3a63d68ebe..0000000000 --- a/apps/docs/app/best-practices/improve-email-content/page.mdx +++ /dev/null @@ -1,108 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import NewsletterSurveyType from "./choose-survey-type.webp"; -import NewsletterSurveyEmbedCode from "./embed-survey-code-in-your-email.webp"; -import NewsletterSurveyEmbedPrompt from "./embed-survey-prompt.webp"; -import NewsletterSurveyEditor from "./improve-newsletter-content-editor-formbricks.webp"; -import NewsletterSurvey from "./improve-newsletter-content-survey-location.webp"; - -export const metadata = { - title: "Measure email content quality with Formbricks", - description: - "Measuring the content quality of both transactional and marketing email is a key element for improving customer communication.", -}; - -#### Best Practices - -# Improve Email Content - -Email remains the predominant way to communicate with your customers. Measure the effectiveness to improve your offering. - -## Purpose - -Measuring the content quality of both transactional and marketing email is a key element for improving customer communication. - -## Formbricks Approach - -- Embed the survey into your email so it’s part of the newsletter. -- Use link prefilling to store the answer users clicked on in the email. -- Dynamic user identification to append reader's email for personalized profiles and follow ups. - -## Installation - -To embed the newsletter survey into your email, follow these steps: - -1. Create new 'Improve Newsletter Content' survey at [app.formbricks.com](https://app.formbricks.com/) -2. Select how you where you want to display the survey. -3. Copy the embed code anywhere you want in your newsletter. - -### 1. Create new 'Improve Newsletter Content' Survey - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Then, create a new survey and look for the "Improve Newsletter Content" template: - - - -### 2. Customize Survey questions - -Customize survey questions, emojis or stars however you like: - - - -### 3. Configure Survey Settings - -When you are done customizing your survey questions, navigate to the Settings tab and choose the type of survey you want. You need to choose Link Survey: - - - -### 4. Choose how you want to embed your survey - -After publishing your survey, a modal that prompts you to embed your survey will pop up. - - - -Select the Embed Survey card and you will be directed to another modal, where the first embed option displayed will be to embed the survey in an email. - -### 5. Copy code to embed the survey in your newsletter - -Click the button with the “View Embed Code” text at the top right corner of the modal and simply paste the HTML code for your survey anywhere you want it in your newsletter. You can see the preview in the below image: - - - -And you're done! Send a test email to yourself and try it out 🤓 - -## Learn about data prefilling - - - ## How does data prefilling work? Learn about how link prefilling and user identification maximize your insights in [this detailed guide](/blog/how-smart-writers-use-formbricks-open-source-tool-to-measure-the-quality-of-their-newsletter-content). - - -###   - -# That’s it! 🎉 diff --git a/apps/docs/app/best-practices/improve-trial-cr/page.mdx b/apps/docs/app/best-practices/improve-trial-cr/page.mdx deleted file mode 100644 index b704e500a3..0000000000 --- a/apps/docs/app/best-practices/improve-trial-cr/page.mdx +++ /dev/null @@ -1,136 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import ActionText from "./action-innertext.webp"; -import ActionPageurl from "./action-pageurl.webp"; -import ChangeText from "./change-text.webp"; -import CreateSurvey from "./create-survey.webp"; -import Publish from "./publish.webp"; -import RecontactOptions from "./recontact-options.webp"; -import SelectAction from "./select-action.webp"; - -export const metadata = { - title: "Boost Your Trial Conversion Rates with Formbricks: Comprehensive Guide", - description: - "Unlock the secret to converting more trial users into paying customers using Formbricks. Understand insights behind trial cancellations and tailor your offering to fit user needs. Dive into our step-by-step tutorial and improve your conversion strategy today", -}; - -#### Best Practices - -# Improve Trial Conversion - -When a user doesn't convert, you want to know why. A micro-survey displayed at exactly the right time gives you a window into understanding the most relevant question: To pay or not to pay? - -## Purpose - -The better you understand why free users don’t convert to paid users, the higher your revenue. You can make an informed decision about what to change in your offering to make more people pay for your service. - -## Formbricks Approach - -- Ask at exactly the right point in time -- Ask to understand the problem, don’t ask for solutions - -## Installation - -To display the Trial Conversion Survey in your app you want to proceed as follows: - -1. Create new Trial Conversion Survey at [app.formbricks.com](https://app.formbricks.com/) -2. Set up the user action to display survey at right point in time -3. Print that 💸 - - - ## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/app-surveys/quickstart) - - -### 1. Create new Trial Conversion Survey - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Click on "Create Survey" and choose the template “Improve Trial Conversion”: - - - -### 2. Update questions (if you like) - -You’re free to update the questions and answer options. However, based on our experience, we suggest giving the provided template a go 😊 - - - -_Want to change the button color? You can do so in the project settings!_ - -Save, and move over to the “Audience” tab. - -### 3. Pre-segment your audience (coming soon) - - - ## Filter by attribute coming soon We're working on pre-segmenting users by attributes. We will update this manual in the next days. - - -Pre-segmentation isn't relevant for this survey because you likely want to solve all people who cancel their trial. You probably have a specific user action e.g. clicking on "Cancel Trial" you can use to only display the survey to users trialing your product. - -### 4. Set up a trigger for the Trial Conversion Survey: - -How you trigger your survey depends on your product. There are two options: - -1. **Trigger by Page view:** Let’s say you have a page under “/trial-cancelled” where you forward users once they cancelled the trial subscription. You can then create an user Action with the type `Page View` and add select `Limit to specific pages` to add url filters, with the following settings: - - - -Whenever a user visits this page, the survey will be displayed ✅ - -2. **Trigger by Button Click:** In a different case, you have a “Cancel Trial" button in your app. You can setup a user Action with the according `Inner Text` like so: - - - -Please have a look at our complete [Actions manual](/app-surveys/actions/) if you have questions. - -### 5. Select Action in the “When to ask” card - - - -### 6. Last step: Set Recontact Options correctly - -Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you gather as many insights as possible. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days: - - - -### 7. Congrats! You’re ready to publish your survey 💃 - - - - - ## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart) to install the widget. - - -### - -# Go get 'em 🎉 diff --git a/apps/docs/app/best-practices/interview-prompt/page.mdx b/apps/docs/app/best-practices/interview-prompt/page.mdx deleted file mode 100644 index e32e3dd36c..0000000000 --- a/apps/docs/app/best-practices/interview-prompt/page.mdx +++ /dev/null @@ -1,150 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import ActionCSS from "./action-css.webp"; -import ActionInner from "./action-innertext.webp"; -import ActionPageurl from "./action-pageurl.webp"; -import AddAction from "./add-action.webp"; -import ChangeText from "./change-text.webp"; -import CreatePrompt from "./create-prompt.webp"; -import InterviewExample from "./interview-example.webp"; -import Publish from "./publish-survey.webp"; -import RecontactOptions from "./recontact-options.webp"; -import SelectAction from "./select-action.webp"; - -export const metadata = { - title: "Maximize User Interview Participation with App Interview Prompts", - description: - "Engage with your power users seamlessly using Formbricks' App Interview Prompt. Ditch traditional email invites and experience way more more respondents. Dive into our comprehensive guide on setting up auto-scheduled interviews today and enhance your user understanding", -}; - -#### Best Practices - -# App Interview Prompt - -The Interview Prompt allows you to pick a specific user segment (e.g. Power Users) and invite them to a user interview. Bye, bye spammy email invites, benefit from up to 6x more respondents. - -## Purpose - -Product analytics and app surveys are incomplete without user interviews. Set the scheduling on autopilot for a continuous stream of interviews. - -## Formbricks Approach - -- Pre-segment users with custom attributes. Only invite highly relevant users. -- In-app prompts (app surveys or website surveys) have a 6x higher conversion rate than email invites. -- Set scheduling user interviews on auto pilot. -- Soon: Integrate directly with your [Cal.com](http://Cal.com) account. - -## Installation - -To display an Interview Prompt in your app you want to proceed as follows: - -1. Create new Interview Prompt at [app.formbricks.com](https://app.formbricks.com/) -2. Adjust content and settings -3. That’s it! 🎉 - - - ## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (15mins)](/app-surveys/quickstart) - - -### 1. Create new Interview Prompt - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Click on "Create Survey" and choose the template “Interview Prompt”: - - - -### 2. Update prompt and CTA - -Update the prompt, description and button text to match your products tonality. You can also update the button color in the Project Settings. - - - -In the button settings you have to make sure it is set to “External URL”. In the URL field, copy your booking link (e.g. https://cal.com/company/user-interview). If you don’t have a booking link yet, head over to [cal.com](http://cal.com) and get one - they have the best free plan out there! - - - -Save, and move over to the “Audience” tab. - -### 3. Pre-segment your audience (coming soon) - - - ## Filter by attribute coming soon. We're working on pre-segmenting users by attributes. We will update this manual in the next few days. - - -Once you clicked over to the “Audience” tab you can change the settings. In the **Who To Send** card, select “Filter audience by attribute”. This allows you to only show the prompt to a specific segment of your user base. - -In our case, we want to select users who we have assigned the attribute “Power User”. To learn how to assign attributes to your users, please [follow this guide](/app-surveys/user-identification). - -Great, now only the “Power User” segment will see our Interview Prompt. But when will they see it? - -### 4. Set up a trigger for the Interview Prompt: - -To create the trigger to show your Interview Prompt, go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool User Action Tracker: - - - -Generally, we have two types of user actions: Page views and clicks. The Interview Prompt, you’ll likely want to display it on a page visit since you already filter who sees the prompt by attributes. - -1. **Page view:** Whenever a user visits a page the survey will be displayed, as long as the other conditions match. Other conditions are pre-segmentation, if this user has seen a survey in the past 2 weeks, etc. - - - -2. **Click(Inner Text & CSS Selector):** When a user clicks an element (like a button) with a specific text content or CSS selector, the prompt will be displayed as long as the other conditions also match. - -
- - -
- -### 5. Select action in the “When to ask” card - - - -### 6. Set Recontact Options correctly - -Scroll down to “Recontact Options”. Here you have to choose the correct settings to strike the right balance between asking for user feedback and preventing survey fatigue. Your settings also depend on the size of your user base or segment. If you e.g. have thousands of “Power Users” you can easily afford to only display the prompt once. If you have a smaller user base you might want to ask twice to get a sufficient amount of bookings: - - - -### 7. Congrats! You’re ready to publish your survey 💃 🤸 - - - - - ## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart) to install the widget. - - -### - -# Learn about them users! 🎉 diff --git a/apps/docs/app/best-practices/pmf-survey/page.mdx b/apps/docs/app/best-practices/pmf-survey/page.mdx deleted file mode 100644 index bf61a0405e..0000000000 --- a/apps/docs/app/best-practices/pmf-survey/page.mdx +++ /dev/null @@ -1,128 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import ActionCSS from "./action-css.webp"; -import ActionPageurl from "./action-pageurl.webp"; -import ChangeText from "./change-text.webp"; -import CreateSurvey from "./create-survey.webp"; -import Publish from "./publish.webp"; -import RecontactOptions from "./recontact-options.webp"; -import SelectAction from "./select-action.webp"; - -export const metadata = { - title: "How to Set Up a Product-Market Fit Survey Using Formbricks - Step-by-Step Guide", - description: - "Learn to leverage Formbricks to create and implement a Product-Market Fit survey in your web app. Follow our detailed step-by-step guide to measure and understand your PMF effectively. Ensure high data quality, efficient triggers, and actionable insights.", -}; - -#### Best Practices - -# Product-Market Fit Survey - -## Purpose - -Measuring and understanding your PMF is essential to build a large, successful business. It helps you understand what users like, what they’re missing and what to build next. This survey is perfectly suited to measure PMF like [Superhuman](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit). - -## Formbricks Approach - -- Pre-segment users to only survey users who have experienced your products value -- Never ask twice, keep your data clean -- Run on autopilot: Set up once, keep surveying users continuously - -## Overview - -To display the Product-Market Fit survey in your app you want to proceed as follows: - -1. Create new Product-Market Fit survey at [app.formbricks.com](https://app.formbricks.com/) -2. Setup pre-segmentation to assure high data quality -3. Setup the user action to display survey at good point in time - - - ## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (15mins)](/app-surveys/quickstart) - - -### 1. Create new PMF survey - -If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) - -Click on "Create Survey" and choose one of the PMF survey templates. The first one is rather short, the latter builds on the ["Product-Market Fit Engine"](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit) developed by Superhuman: - - - -### 2. Update questions (if you like) - -You’re free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊 Here is a very [detailed description](https://coda.io/@rahulvohra/superhuman-product-market-fit-engine) of what to do with the data you’re collecting. - - - -_Want to change the button color? You can do so in the project settings!_ - -Save, and move over to where the magic happens: The “Audience” tab. - -### 3. Pre-segment your audience - -To run this survey properly, you should pre-segment your user base. As touched upon earlier: if you ask every user you’ll get lots of opinions which are often misleading. You only want to gather feedback from people who invested the time to get to know and use your product: - -**Filter by attribute**: You can keep the logic to decide if a user has (or has not) experienced value in your application. This makes most sense if you want to use historic usage data to decide if a user qualifies or not. Create your logic and if it applies, send an attribute to Formbricks by e.g. `formbricks.setAttribute("Loyalty", "Experienced Value");` Here is the full manual on how to [set attributes](/app-surveys/user-identification). - -### 4. Set up a trigger for the Product-Market Fit survey: - -You need a trigger to display the survey but in this case, the filtering does all the work. It’s up to you to decide to display the survey after the user viewed a specific subpage (pageURL) or after clicking an element. Have a look at the [Actions manual](/app-surveys/actions/) if you are not sure how to set them up: - - - -
- - -
- -### 5. Select Action in the “When to ask” card - - - -### 6. Last step: Set Recontact Options correctly - -Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure your data remains of high quality. You want to make sure that this survey is only responded to once per user. It is up to you to decide if you want to display it several times until the user responds: - - - -### 7. Congrats! You’re ready to publish your survey 💃 - - - - - ## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart) to install the widget. - - -### - -# Get those insights! diff --git a/apps/docs/app/developer-docs/api-sdk/page.mdx b/apps/docs/app/developer-docs/api-sdk/page.mdx deleted file mode 100644 index 9675c9136d..0000000000 --- a/apps/docs/app/developer-docs/api-sdk/page.mdx +++ /dev/null @@ -1,224 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -export const metadata = { - title: "Formbricks API SDK", - description: - "An overview of all available methods & how to integrate Formbricks API for backend developers in web applications. Learn the key methods, configuration settings, and best practices.", -}; - -#### Developer Docs - -# SDK: Formbricks API - -### Overview - -The Formbricks Client API Wrapper is a lightweight package designed to simplify the integration of Formbricks API endpoints into your JavaScript (JS) or TypeScript (TS) projects. With this wrapper, you can easily interact with Formbricks API endpoints without the need for complex setup or manual HTTP requests. - -### Install - - - - -```js {{ title: 'npm' }} -npm install @formbricks/api -``` - -```js {{ title: 'yarn' }} -yarn add @formbricks/api -``` - -```js {{ title: 'pnpm' }} -pnpm add @formbricks/api -``` - - - - -## Methods - -### Initialize Formbricks - -Initialize the Formbricks API Client for backend developers to interact with Formbricks API endpoints: - - - - -```javascript -import { FormbricksAPI } from "@formbricks/api"; - -const api = new FormbricksAPI({ - apiHost: `https://app.formbricks.com`, // If you have self-hosted Formbricks, change this to your self hosted instance's URL - environmentId: "", // Replace this with your Formbricks environment ID -}); -``` - - - - -The API client is now ready to be used across your project. It can be used to interact with the following models: - -## Displays - -- Create Display - - - - -```javascript {{ title: 'Create Display Method Call'}} -await api.client.display.create({ - surveyId: "", // required - userId: "", // optional - responseId: "", // optional -}); -``` - -```javascript {{ title: 'Create Display Method Return Type' }} -Promise<{ id: string }, NetworkError | Error> -``` - - - - -## Responses - -- Create Response - - - - -```javascript {{ title: 'Create Response Method Call'}} -await api.client.response.create({ - surveyId: "", // required - finished: boolean, // required - data: { - // required - questionId: "", - anotherQuestionId: 123, // answer to this question in number - yetAnotherQuestionId: ["option1", "option2"], // answer to this question in array, - }, - - userId: "", - singleUseId: "", - ttc: { - questionId: 123, - }, - meta: { - source: "", - url: "", - userAgent: { - browser: "", - device: "", - os: "", - }, - country: "", - }, -}); -``` - -```javascript {{ title: 'Create Response Method Return Type' }} -Promise<{ id: string }, NetworkError | Error> -``` - - - - -- Update Response - - - - -```javascript {{ title: 'Update Response Method Call'}} -await api.client.response.update({ - responseId: "", // required - finished: boolean, // required - data: { - // required - questionId: "", - anotherQuestionId: 123, // answer to this question in number - yetAnotherQuestionId: ["option1", "option2"], // answer to this question in array, - }, - ttc: { - // required - questionId: 123, - }, -}); -``` - -```javascript {{ title: 'Update Response Method Return Type' }} -Promise<{ }, NetworkError | Error]> -``` - - - - -## Attribute - -- Update Attribute - - - - -```javascript {{ title: 'Update Attribute Method Call'}} -await api.client.attribute.update({ - userId: "", // required - attributes: { - key1: "value1", - key2: "value2", - }, // required -}); -``` - -```javascript {{ title: 'Update Attribute Method Return Type' }} -Promise<{ changed: boolean, message: string }, NetworkError | Error> -``` - - - - -## People - -- Create Person - - - - -```javascript {{ title: 'Create Person Method Call'}} -await api.client.people.create({ - userId: "", // required -}); -``` - -```javascript {{ title: 'Create Person Method Return Type' }} -Promise<{ }, NetworkError | Error]> -``` - - - - -## Storage - -- Upload File: - - - - -```javascript {{ title: 'Upload Method Call'}} -await api.client.storage.uploadFile( - file: File, // required (of interface File of the browser's File API) - { - allowedFileTypes: ["file-type-allowed", "for-example", "image/jpeg"], - surveyId: "", - } -); -``` - -```javascript {{ title: 'Upload Method Return Type' }} -Promise -``` - - - - ---- - -If you have any questions or need help, feel free to reach out to us in **[Github Discussions](https://github.com/formbricks/formbricks/discussions)** diff --git a/apps/docs/app/developer-docs/contributing/codespaces/images/loading.webp b/apps/docs/app/developer-docs/contributing/codespaces/images/loading.webp deleted file mode 100644 index 89b3ceef2b..0000000000 Binary files a/apps/docs/app/developer-docs/contributing/codespaces/images/loading.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/contributing/codespaces/images/new.webp b/apps/docs/app/developer-docs/contributing/codespaces/images/new.webp deleted file mode 100644 index e415ffb87b..0000000000 Binary files a/apps/docs/app/developer-docs/contributing/codespaces/images/new.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/contributing/codespaces/images/ports.webp b/apps/docs/app/developer-docs/contributing/codespaces/images/ports.webp deleted file mode 100644 index 766a3b396d..0000000000 Binary files a/apps/docs/app/developer-docs/contributing/codespaces/images/ports.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/contributing/codespaces/page.mdx b/apps/docs/app/developer-docs/contributing/codespaces/page.mdx deleted file mode 100644 index 164db7fc22..0000000000 --- a/apps/docs/app/developer-docs/contributing/codespaces/page.mdx +++ /dev/null @@ -1,64 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import GithubCodespaceLoading from "./images/loading.webp"; -import GithubCodespaceNew from "./images/new.webp"; -import GithubCodespacePorts from "./images/ports.webp"; - -export const metadata = { - title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks", - description: - "Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Github Discussions", -}; - -#### Contributing - -# Github Codespaces Guide - -1. After clicking the one-click setup button, you will be redirected to the Github Codespaces page. Review the configuration and click on the 'Create Codespace' button to create a new Codespace. - - - -2. This will start loading the Codespace. Keep in mind this might take a few minutes to complete depending on your internet connection and the instance availability. - - - -3. Once the Codespace is loaded, you will be redirected to the VSCode editor. You can start working on your project in this environment. - -4. Monitor the logs in the terminal and once you see the following, you are good to go! - - - - -```bash -@formbricks/web:dev: ▲ Next.js 13.5.6 -@formbricks/web:dev: - Local: http://localhost:3000 -@formbricks/web:dev: - Environments: .env -@formbricks/web:dev: - Experiments (use at your own risk): -@formbricks/web:dev: · serverActions -@formbricks/web:dev: -@formbricks/web:dev: ✓ Ready in 9.4s -``` - - - - -5. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App! - - - -Now make the changes you want to and see them live in action! diff --git a/apps/docs/app/developer-docs/contributing/get-started/page.mdx b/apps/docs/app/developer-docs/contributing/get-started/page.mdx deleted file mode 100644 index 594070e69e..0000000000 --- a/apps/docs/app/developer-docs/contributing/get-started/page.mdx +++ /dev/null @@ -1,177 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -export const metadata = { - title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks", - description: - "Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Github Discussions", -}; - -#### Contributing - -# Get started - -We are so happy that you are interested in contributing to Formbricks 🤗 There are many ways to contribute to Formbricks like writing issues, fixing bugs, building new features or updating the docs. - -- **Issues**: Spotted a bug? Has deployment gone wrong? Do you have user feedback? [Raise an issue](https://github.com/formbricks/formbricks/issues/new/choose) for the fastest response. -- **Feature requests**: Raise an issue for these and tag it as an Enhancement. We love every idea. Please [open a feature request](https://github.com/formbricks/formbricks/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml&title=%5BFEATURE%5D) clearly describing the problem you want to solve. -- **Creating a PR**: Please fork the repository, make your changes and create a new pull request if you want to make an update. Please talk to us first before starting development of more complex features. Small fixes are always welcome! - -## Talk to us first - -We highly recommend connecting with us on [Github Discussions](https://github.com/formbricks/formbricks/discussions) before you ship a contribution. This will increase the likelihood of your PR being merged. And it will decrease the likelihood of you wasting your time :) - -## Contributor License Agreement (CLA) - -To be able to keep working on Formbricks over the coming years, we need to collect a CLA from all relevant contributors. - -Once you open a PR, you will get a message from the CLA bot to fill out the form. Please note that we can only get your contribution merged when we have a CLA signed by you. - -## Setup Dev Environment - -We currently officially support the below methods to set up your development environment for Formbricks: - -- [Gitpod](https://gitpod.io) -- [GitHub Codespaces](https://github.com/features/codespaces) -- [Local Machine Setup](#local-machine-setup) - -Both Gitpod and GitHub Codespaces have a **generous free tier** to explore and develop. For junior developers we suggest using either of these, because you can dive into coding within minutes, not hours. - -## Local Machine Setup - - -The below only works for **Mac**, **Linux** & **WSL2** on Windows (not on pure Windows)! - -This method is recommended **only for advanced users** & we won't be able to provide official support for this. - - - -To get the project running locally on your machine you need to have the following development tools installed: - -- Node.JS (we recommend v20) -- [pnpm](https://pnpm.io/) -- [Docker](https://www.docker.com/) (to run PostgreSQL / MailHog) - -1. Clone the project & move into the directory: - - - - -```bash -git clone https://github.com/formbricks/formbricks && cd formbricks -``` - - - - -2. Setup Node.JS with nvm: - - - - -```bash -nvm install && nvm use -``` - - - - -3. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation) - - - - -```bash -pnpm install -``` - - - - -4. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed. - - - - -```bash -cp .env.example .env -``` - - - - -5. Generate & set some secret values mandatory for the `ENCRYPTION_KEY`, `NEXTAUTH_SECRET` and `CRON_SECRET` in the .env file. You can use the following command to generate the random string of required length: - -- For Linux - - - - -```bash -sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env -sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env -sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env -``` - - - - -- For Mac - - - - -```bash -sed -i '' '/^ENCRYPTION_KEY=/s|.*|ENCRYPTION_KEY='$(openssl rand -hex 32)'|' .env -sed -i '' '/^NEXTAUTH_SECRET=/s|.*|NEXTAUTH_SECRET='$(openssl rand -hex 32)'|' .env -sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env -``` - - - - -6. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup: - - - - -```bash -pnpm go -``` - - - - This starts the Formbricks main app (plus all its dependencies) as well as the following services using Docker: - -- A `postgres` container for hosting your database, -- A `mailhog` container that acts as a mock SMTP server and shows received mails in a web UI (forwarded to your host's `localhost:8025`) -- Demo App at [http://localhost:3002](http://localhost:3002) -- Landing Page at [http://localhost:3001](http://localhost:3001) - - - **WSL2 users**: If you encounter connection issues with Prisma, ensure your WSL2 instance's PostgreSQL - service is stopped before running `pnpm go`. Use the command `sudo systemctl stop postgresql` to stop the service. - - -**You can now access the Formbricks app on [http://localhost:3000](http://localhost:3000)**. You will be automatically redirected to the login. To use your local installation of formbricks, create a new account. - -{" "} - - - A fresh setup does not have a default account. Please create a new account and proceed accordingly. - - -For viewing the emails sent by the system, you can access mailhog at [http://localhost:8025](http://localhost:8025) - -### Build - -To build all apps and packages and check for build errors, run the following command: - - - - -```bash -pnpm build -``` - - - diff --git a/apps/docs/app/developer-docs/contributing/troubleshooting/images/clear-app-data.webp b/apps/docs/app/developer-docs/contributing/troubleshooting/images/clear-app-data.webp deleted file mode 100644 index 240e97981b..0000000000 Binary files a/apps/docs/app/developer-docs/contributing/troubleshooting/images/clear-app-data.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/contributing/troubleshooting/images/logout.webp b/apps/docs/app/developer-docs/contributing/troubleshooting/images/logout.webp deleted file mode 100644 index f706ba643a..0000000000 Binary files a/apps/docs/app/developer-docs/contributing/troubleshooting/images/logout.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/contributing/troubleshooting/images/uncaught-promise.webp b/apps/docs/app/developer-docs/contributing/troubleshooting/images/uncaught-promise.webp deleted file mode 100644 index b698bca34b..0000000000 Binary files a/apps/docs/app/developer-docs/contributing/troubleshooting/images/uncaught-promise.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/contributing/troubleshooting/page.mdx b/apps/docs/app/developer-docs/contributing/troubleshooting/page.mdx deleted file mode 100644 index d6b7298ed1..0000000000 --- a/apps/docs/app/developer-docs/contributing/troubleshooting/page.mdx +++ /dev/null @@ -1,84 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import ClearAppData from "./images/clear-app-data.webp"; -import Logout from "./images/logout.webp"; -import UncaughtPromise from "./images/uncaught-promise.webp"; - -export const metadata = { - title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks", - description: - "Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Github Discussions", -}; - -#### Contributing - -# Troubleshooting - -Here you'll find help with frequently recurring problems - -## "The app doesn't work after doing a prisma migration" - -This can happen but fear not, the fix is easy: Delete the application storage of your browser and reload the page. This will force the app to re-fetch the data from the server: - - - -## "I ran 'pnpm i' but there seems to be an error with the packages" - -If nothing helps, run `pnpm clean` and then `pnpm i` again. This solves a lot. - -## "I get a full-screen error with cryptic strings" - -This usually happens when the Formbricks Widget wasn't correctly or completely built. - - - - -```bash -pnpm build --filter=@formbricks/js - -// Run the app again -pnpm dev -``` - - - -## My machine struggles with the repository - -Since we're working with a monorepo structure, the repository can get quite big. If you're having trouble working with the repository, try the following: - - - - -```bash {{ title: 'Formbricks Web-App' }} -pnpm dev --filter=@formbricks/web... -``` - -```bash {{ title: 'Formbricks Docs' }} -pnpm dev --filter=@formbricks/docs... -``` - -```bash {{ title: 'Formbricks Demo App' }} -pnpm dev --filter=@formbricks/demo... -``` - - - -However, in our experience it's better to run `pnpm dev` than having two terminals open (one with the Formbricks app and one with the demo). - -## Uncaught (in promise) SyntaxError: Unexpected token !DOCTYPE ... is not valid JSON - - - -This happens when you're using the Demo App and delete the Person within the Formbricks app which the widget is currently connected with. We're fixing it, but you can also just logout your test person and reload the page to get rid of it. - - diff --git a/apps/docs/app/developer-docs/integrations/make/page.mdx b/apps/docs/app/developer-docs/integrations/make/page.mdx deleted file mode 100644 index 39def46c7a..0000000000 --- a/apps/docs/app/developer-docs/integrations/make/page.mdx +++ /dev/null @@ -1,153 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import AddModule from "./add-module.webp"; -import CreateNewScenario from "./create-new-scenario.webp"; -import CreateWebhook from "./create-webhook.webp"; -import DuplicateSurvey from "./duplicate-survey.webp"; -import EnterApiKeyAndHost from "./enter-api-key-and-host.webp"; -import Result from "./result.webp"; -import SearchFormbricks from "./search-formbricks.webp"; -import SelectAction from "./select-action.webp"; -import SelectFields from "./select-fields.webp"; -import SelectSurvey from "./select-survey.webp"; -import SelectTriggers from "./select-trigger.webp"; -import SubmitTestResponse from "./submit-test-response.webp"; -import UpdateQuestionId from "./update-question-id.webp"; - -export const metadata = { - title: "Formbricks Integration with Make.com: A Step-by-Step Guide", - description: - "Discover how to seamlessly integrate Formbricks with Make.com. Dive into our comprehensive guide to set up scenarios, connect with a plethora of apps, and send your survey data to more than 1000 platforms.", -}; - -#### Integrations - -# Make.com Setup - -Make is a powerful tool to send information between Formbricks and thousands of apps. Here's how to set it up. - - - Nailed down your survey?? Any changes in the survey cause additional work in the _Scenario_. It makes sense to first settle on the survey you want to run and then get to setting up Make. - - -## Step 1: Setup your survey incl. `questionId` for every question - -Set up the `questionId`s of your survey questions before publishing. - - - -_Update the Question ID field in every question card under Advanced Settings._ - - - ### Already published? Duplicate survey You can only update the questionId before publishing the survey. If already published, simply duplicate it. - - - -## Step 2: Setup Make.com - -Visit [Make.com](https://make.com) to start a new scenario. - - - -Search for `Formbricks`: - - - -Choose the event to trigger the Scenario: - - - -## Step 3: Connect Formbricks with Make - -Click "Create a webhook": - - - -Enter the Formbricks API Host and API Key. API Host is by default set to https://app.formbricks.com but can be modified for self hosting instances. Learn how to get an API Key from the [API Key tutorial](/additional-features/api#how-to-generate-an-api-key). - - - -## Step 4: Select Survey - -Choose from your created surveys: - - - -## Step 5: Send a test response - -You need a test response for Make setup. For local Formbricks setup, use the [Demo App](/contributing/demo) to submit a test response. - - - -## Step 6: Set up Google Sheet - -Decide on the desired action for the data. Here, we'll send submissions to a Google Sheet: - - - -Choose "Add a Row" for the action: - - - -Specify the spreadsheet details and match the Formbricks data: - - - -A new row gets added to the spreadsheet for every response: - - diff --git a/apps/docs/app/developer-docs/integrations/n8n/page.mdx b/apps/docs/app/developer-docs/integrations/n8n/page.mdx deleted file mode 100644 index f35ac4b5e7..0000000000 --- a/apps/docs/app/developer-docs/integrations/n8n/page.mdx +++ /dev/null @@ -1,186 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import AddApiKey from "./add-api-key.webp"; -import AddDiscord from "./add-discord.webp"; -import AddFormbricksTrigger from "./add-formbricks-trigger.webp"; -import CreateNewCredentialBtn from "./create-new-credential-btn.webp"; -import DiscordResponse from "./discord-response.webp"; -import DuplicateSurvey from "./duplicate-survey.webp"; -import FillDiscordDetails from "./fill-discord-details.webp"; -import ListenForEvent from "./listen-for-event.webp"; -import SelectEvent from "./select-event.webp"; -import SelectSurvey from "./select-survey.webp"; -import SelectedSurveys from "./selected-surveys.webp"; -import SubmitTestResponse from "./submit-test-response.webp"; -import SuccessConnection from "./success-connection.webp"; -import TestResponseSuccess from "./test-response-success.webp"; -import UpdateQuestionId from "./update-question-id.webp"; - -export const metadata = { - title: "Comprehensive Guide to Integrating Formbricks with n8n", - description: - "Unlock the potential of combining Formbricks with n8n for a streamlined workflow experience. Dive into our step-by-step guide and send your survey data effortlessly to 350+ applications. Streamline your data processes now!", -}; - -#### Integrations - -# n8n Setup - - - The Formbricks n8n node is currently only available in the n8n self-hosted version as a community node. To install it go to "Settings" -> "Community Nodes" and install @formbricks/n8n-nodes-formbricks. - - -n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it. - -## Step 1: Setup your survey incl. `questionId` for every question - - - Nailed down your survey? Any changes in the survey cause additional work in the n8n node. It makes sense to first settle on the survey you want to run and then get to setting up n8n. - - -When setting up the node your life will be easier when you change the `questionId`s of your survey questions. You can only do so **before** you publish your survey. - - - -_In every question card in the Advanced Settings you find the Question ID field. Update it so that you'll recognize the response tied to this question._ - - - ### Already published? Duplicate survey You can only update the questionId when the survey was not yet - published. Already published it? Just **duplicate it** to update the questionIds. - - - -## Step 2: Setup your n8n workflow - -Go to [n8n.io](https://n8n.io) and create a new workflow. Search for “Formbricks” to get started: - - - -## Step 3: Connect Formbricks with n8n - -Now, you have to connect n8n with Formbricks via an API Key: - - - -Click on Create New Credentail button to add your host and API Key - - - -Now you need an API key. Please refer to the [API Key Setup](/additional-features/api#how-to-generate-an-api-key) page to learn how to create one. - -Once you copied it in the API Key field, hit Save button to test the connection and save the credentials. - - - -## Step 4: Select Event - -Next, you can choose the event you want to trigger the node on. You can select multiple events: - - - -Here, we are adding `Response Finished` as an event, which will trigger when the survey has been filled out. - -## Step 5: Select Survey - -Next, you can choose from all the surveys you have created in this environment. You can select multiple surveys: - - - -Here, we are selecting two surveys. - - - -## Step 6: Test your trigger - -In order to set up n8n you'll need a test response in the selected survey. This allows you to select the individual values of each response in your workflow. If you have Formbricks running locally and you want to set up an an app or a website survey, you can use our [Demo App](/contributing/demo) to trigger a survey and submit a response. - - - -Next, click on Listen for event button. - - - -Then, go to the survey which you selected. Fill it out, and wait for the particular event to trigger (in this case it's `Response Finished`). Once the event is triggered you will see the response that you filled out in the survey. - - - -Now you have all the data you need at hand. The next steps depend on what you want to do with it. In this tutorial, we will send submissions to a discord channel: - -## Step 7: Add discord to your workflow - -Click on the plus and search `Discord`. - - - -Fill in the `Webhook URL` and the `Content` that you want to receive in the respective discord channel. Next, click on `Execute Node` button to test the node. - - - -Once the execution is successful, you'll receive the content in the discord channel. - - diff --git a/apps/docs/app/developer-docs/integrations/overview/page.mdx b/apps/docs/app/developer-docs/integrations/overview/page.mdx deleted file mode 100644 index b76774597e..0000000000 --- a/apps/docs/app/developer-docs/integrations/overview/page.mdx +++ /dev/null @@ -1,27 +0,0 @@ -export const metadata = { - title: "Configuring Formbricks with third party applications", - description: "Configure third-party integrations with a Formbricks instance.", -}; - -#### Developer Docs - -# Overview - -At Formbricks, we understand the importance of integrating with third-party applications. We have step-by-step guides to configure our third-party integrations with a your Formbricks instance. We currently support the below integrations, click on them to see their individual guides: - - - If you are on a self-hosted instance, you will need to configure these integrations manually. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance. - - -- [Airtable](/developer-docs/integrations/airtable): Automatically send responses to an Airtable of your choice. -- [Google Sheets](/developer-docs/integrations/google-sheets): Automatically send responses to a Google Sheet of your choice. -- [Make](/developer-docs/integrations/make): Leverage Make's powerful automation capabilities to automate your workflows. -- [n8n](/developer-docs/integrations/n8n): Automate workflows with n8n's no-code automation tool. -- [Notion](/developer-docs/integrations/notion): Automatically send responses to a Notion database of your choice. -- [Slack](/developer-docs/integrations/slack): Automatically send responses to a Slack channel of your choice on response events. -- [Wordpress](/developer-docs/integrations/wordpress): Automatically integrate your Formbricks surveys with your Wordpress website. -- [Zapier](/developer-docs/integrations/zapier): Connect Formbricks with 2000+ apps on Zapier. - ---- - -If you have any questions or need help with any of the integrations or even want a new integration, please reach out to us on [Github Discussions](https://github.com/formbricks/formbricks/discussions). diff --git a/apps/docs/app/developer-docs/integrations/zapier/page.mdx b/apps/docs/app/developer-docs/integrations/zapier/page.mdx deleted file mode 100644 index cf44575a6b..0000000000 --- a/apps/docs/app/developer-docs/integrations/zapier/page.mdx +++ /dev/null @@ -1,151 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import AddNewZap from "./add-new-zap.webp"; -import ChooseEvent from "./choose-event.webp"; -import ConnectWithFB1 from "./connect-with-formbricks-1.webp"; -import ConnectWithFB2 from "./connect-with-formbricks-2.webp"; -import DuplicateSurvey from "./duplicate-survey.webp"; -import SelectSurvey from "./select-survey.webp"; -import SlackChannelMsg from "./slack-channel-msg.webp"; -import SlackMsg from "./slack-message.webp"; -import SubmitTestResponse from "./submit-test-response.webp"; -import SuccessConnection from "./success-connected.webp"; -import TestSubmission from "./test-submission.webp"; -import UpdateQuestionId from "./update-question-id.webp"; -import ZapierMessage from "./zapier-message.webp"; - -export const metadata = { - title: "Step-by-Step Guide to Integrating Formbricks with Zapier", - description: - "Master the integration of Formbricks with Zapier using our detailed guide. Seamlessly connect your surveys to 5000+ apps, automate data transfers, and enhance feedback management. Start optimizing your workflow today.", -}; - -#### Integrations - -# Zapier Setup - -Zapier is a powerful ally. Hook up Formbricks with Zapier and you can send your data to 5000+ other apps. Here is how to do it. - - - ### Nail down your survey first? Any changes in the survey cause additional work in the Zap. It makes sense to first settle on the survey you want to run and then get to setting up Zapier. - - -## Step 1: Setup your survey incl. `questionId` for every question - -When setting up the Zap your life will be easier when you change the `questionId`s of your survey questions. You can only do so **before** you publish your survey. - - - -_In every question card in the Advanced Settings you find the Question ID field. Update it so that you’ll recognize the response tied to this question._ - - - ### Already published? Duplicate survey You can only update the questionId when the survey was not yet - published. Already published it? Just **duplicate it** to update the questionIds. - - - -## Step 2: Send a test response - -In order to set up Zapier you’ll need a test response. This allows you to select the individual values of each response in your Zap. If you have Formbricks running locally and you want to set up an app or a website survey, you can use our [Demo App](/contributing/demo) to trigger a survey and submit a response. - - - -## Step 3: Setup your Zap - -Go to [zapier.com](https://zapier.com) and create a new Zap. Search for “Formbricks” to get started: - - - -Then, choose the event you want to trigger the Zap on: - - - -## Step 4: Connect Formbricks with Zapier - -Now, you have to connect Zapier with Formbricks via an API Key: - - - - -Now you need an API key. Please refer to the [API Key Setup](/additional-features/api#how-to-generate-an-api-key) page to learn how to create one. - -Once you copied it in the newly opened Zapier window, you will be connected: - - - -## Step 5: Select Survey - -Next, you can choose from all the surveys you have created in this environment: - - - -## Step 6: Test your trigger - -Once you hit “Test” you will see the three most recent submissions for this survey. If you don’t have any submissions in the survey, submit one to continue setting up your Zap: - - -_Now you're happy that you updated the questionId's_ - -## Step 7: Set up your Zap - -Now you have all the data you need at hand. The next steps depend on what you want to do with it. In this tutorial, we will send submissions to a Slack channel: - - - -In the action itself we can determine the data and layout of the message. Here, we only choose the submission data. You can also refer to the meta data of the submission and the [attributes](/app-surveys/user-identification) of the person who submitted the survey. - - - -We now receive a notifcation in our Slack channel whenever a Churn survey is completed: - - diff --git a/apps/docs/app/developer-docs/js-sdk/images/1-set-up-in-app-micro-survey-popup.webp b/apps/docs/app/developer-docs/js-sdk/images/1-set-up-in-app-micro-survey-popup.webp deleted file mode 100644 index 03859c00d5..0000000000 Binary files a/apps/docs/app/developer-docs/js-sdk/images/1-set-up-in-app-micro-survey-popup.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/js-sdk/images/2-micro-survey-pop-up-in-app.webp b/apps/docs/app/developer-docs/js-sdk/images/2-micro-survey-pop-up-in-app.webp deleted file mode 100644 index e74629fd62..0000000000 Binary files a/apps/docs/app/developer-docs/js-sdk/images/2-micro-survey-pop-up-in-app.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/js-sdk/images/3-survey-logs-in-app-survey-popup.webp b/apps/docs/app/developer-docs/js-sdk/images/3-survey-logs-in-app-survey-popup.webp deleted file mode 100644 index 841d7cea49..0000000000 Binary files a/apps/docs/app/developer-docs/js-sdk/images/3-survey-logs-in-app-survey-popup.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/js-sdk/page.mdx b/apps/docs/app/developer-docs/js-sdk/page.mdx deleted file mode 100644 index 659fd3cc8e..0000000000 --- a/apps/docs/app/developer-docs/js-sdk/page.mdx +++ /dev/null @@ -1,255 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import I1 from "./images/1-set-up-in-app-micro-survey-popup.webp"; -import I2 from "./images/2-micro-survey-pop-up-in-app.webp"; -import I3 from "./images/3-survey-logs-in-app-survey-popup.webp"; - -export const metadata = { - title: "Formbricks JS SDK", - description: - "Integrate Formbricks App Surveys into your web apps and websites with the Formbricks JS SDK. Learn how to initialize Formbricks, set attributes, track actions, and troubleshoot common issues.", -}; - -#### Developer Docs - -# SDK: Run Surveys Inside Your Web Apps and Websites - -### Overview - -The Formbricks JS SDK is a versatile solution for integrating surveys into both web apps and websites. It adapts based on the presence of a `userId`. If a `userId` is provided, the SDK handles authenticated surveys for logged-in users in web apps. If no `userId` is provided, the SDK can still seamlessly run surveys on public-facing websites. The SDK is available on npm [here](https://www.npmjs.com/package/@formbricks/js/). - -### Install - - - - -```js {{ title: 'npm' }} -npm install @formbricks/js -``` - -```js {{ title: 'yarn' }} -yarn add @formbricks/js -``` - -```js {{ title: 'pnpm' }} -pnpm add @formbricks/js -``` - - - - -## Methods - -### Initialize Formbricks - -Initialize the Formbricks JS Client for surveys. When used in a web app, pass a `userId` to create and target a specific user. When using it on a website without authentication, simply omit the `userId`. - - - - -```javascript -import formbricks from "@formbricks/js"; - -formbricks.init({ - environmentId: "", // required - apiHost: "", // required - userId: "", // optional -}); -``` - - - - -The moment you initialise Formbricks, your user will start seeing surveys that get triggered on simpler actions such as on New Session, Page Exit, & other custom actions! - - -Formbricks JS is a client SDK meant to be run client-side in their browser so make sure the window object is accessible. Below is an example of how you can set it! - - - - -```js -if (window !== undefined) { - formbricks.init({ - environmentId: "", - apiHost: "", - userId: "", //optional - }); -} else { - console.error("Window object not accessible to init Formbricks"); -} -``` - - - - - - -### Set Attribute - -Set custom attributes for the identified user to help segment them based on specific characteristics. This method only works when a `userId` is provided during initialization, allowing for targeted surveys in web apps. To learn how to set custom user attributes, refer to our [User Attributes Guide](/app-surveys/user-identification). - - - - -```js -formbricks.setAttribute("Plan", "Paid"); -``` - - - - -### Track Action - -Track user actions to trigger surveys based on user interactions, such as button clicks or scrolling: - - - - -```js -formbricks.track("Clicked on Claim"); -``` - - - - -### Logout - -To log out and deinitialize Formbricks, use the formbricks.logout() function. This action clears the current initialization configuration and erases stored frontend information, such as the surveys a user has viewed or completed. It's an important step when a user logs out of your application or when you want to reset Formbricks. - - - - -```js -formbricks.logout(); -``` - - - - -After calling formbricks.logout(), you'll need to reinitialize Formbricks before using any of its features again. Ensure that you properly reinitialize Formbricks to avoid unexpected errors or behavior in your application. - -### Reset - -Reset the current instance and fetch the latest surveys and state again: - - - - -```js -formbricks.reset(); -``` - - - - -### Register Route Change: - -Listen for page changes and dynamically show surveys configured via no-code actions in the Formbricks app: - - - This is only needed when your framework has a custom routing system and you want to trigger surveys on route changes. For example: NextJs - - - - - -```js -formbricks.registerRouteChange(); -``` - - - - -## Debug Mode - -To enable debug mode in Formbricks, add `?formbricksDebug=true` to your app’s URL. - -For example, if you’ve integrated Formbricks JS to your app hosted at `https://example.com`, then change the URL to `https://example.com?formbricksDebug=true` and refresh the page, now view the console logs to see the debug mode live in action. - -This activates detailed debug messages in the browser console, providing deeper insights into Formbricks' operation and potential issues. - ---- - -## Troubleshooting - -In case you don’t see your survey right away, here's what you can do. Go through these to find the error fast: - -### Formbricks Cloud and your app are not connected properly. - -Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted instance's URL and go to the App connection in the Configuration. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud: - - -**How to fix it:** - -1. Check if your app loads the Formbricks widget correctly. -2. Make sure you have `debug` mode enabled in your integration and you should see the Formbricks debug logs in your browser console while being in your app (right click in the browser, `Inspect`, switch to the console tab). If you don’t see them, double check your integration. - ---- - -### Survey not loaded - -If your app is connected with Formbricks Cloud, the survey might have not been loaded properly. Check the debug logs and search for the list of surveys loaded. It should look like so: - - - -**How to fix it:** - -The widget only loads surveys which are **public** and **in progress**. Go to Formbricks Cloud and to the Survey Summary page. Check if your survey is live: - - - ---- - -### Survey not triggered - -If the survey is loaded by the widget it might not have been triggered properly. - -**How to fix:** - -1. Open your local app in an incognito tab or window. The New Session event is only fired if a user was inactive for 60 minutes or was logged out of Formbricks via formrbicks.logout(). -2. Check the debug logs for “Event ‘New Session” tracked”. If you see it in the logs and the survey still did not get displayed, [please let us know.](mailto:support@formbricks.com) - ---- - -### Survey not displayed in HTML page - -If the survey is loaded by the widget in the HTML page, try the below steps: - -**How to fix:** - -1. Make sure you have added the [script](/app-surveys/framework-guides#html) in the head of the HTML page. -2. Verify that you have set the \ and \ as per your Formbricks instance. -3. Verify that you have the latest version of the JS Package. -4. Check the debug logs to see if you still see any errors. - ---- - -### Cannot read undefined of .init() - -If you see this error in the console, it means that the Formbricks JS package is not loaded properly. - -**How to fix:** - -1. Update to the latest version of the JS Package. -2. Verify this wherever you call initialise the Formbricks instance in your code. -3. It should now start working. - ---- - -If you have any questions or need help, feel free to reach out to us on **[Github Discussions](https://github.com/formbricks/formbricks/discussions)** diff --git a/apps/docs/app/developer-docs/overview/page.mdx b/apps/docs/app/developer-docs/overview/page.mdx deleted file mode 100644 index 2b0961a848..0000000000 --- a/apps/docs/app/developer-docs/overview/page.mdx +++ /dev/null @@ -1,41 +0,0 @@ -export const metadata = { - title: "Formbricks Developer Documentation Overview", - description: - "Welcome to the Developer Docs section, your comprehensive resource for integrating and utilizing Formbricks SDKs & APIs, as well as contributing to our open source codebase. Dive into the documentation to discover how to deploy surveys on your website and effectively engage with your audience.", -}; - -#### Developer Docs - -# Overview - -Welcome to the Developer Docs section, your comprehensive resource for integrating and utilizing Formbricks SDKs &APIs, as well as contributing to our open source codebase. Here's what you can expect to find in this section: - -### [SDK: React Native Apps](/developer-docs/react-native-in-app-surveys) - -The Formbricks React Native SDK for App Surveys is designed for React Native applications, enabling seamless integration of surveys within your mobile apps. Dive into the documentation to learn how to leverage the SDK for app surveys and engage with your users effectively. - -### [SDK: Formbricks JS](/developer-docs/js-sdk) - -The Formbricks JS SDK is a versatile solution for both web apps and public websites. It adapts based on how you provide user information. - -If a `userId` is provided, the SDK tailors surveys for logged-in users, enabling targeted and identified interactions. This allows for advanced user tracking, providing deeper insights into user behavior within your application. - -Alternatively, when no `userId` is supplied, the SDK seamlessly handles surveys for public-facing websites, making it ideal for high-traffic pages without authentication. This enables efficient survey collection without requiring user identification. - -### [SDK: Formbricks API](/developer-docs/api-sdk) - -Get acquainted with the Formbricks API JS SDK, empowering you to interact with Formbricks programmatically. Learn how to access Formbricks functionalities and automate tasks to streamline your workflow. - -### [REST API](/developer-docs/rest-api) - -Explore the Formbricks REST API documentation with Postman, providing detailed insights into available endpoints, request methods, and response formats. Integrate Formbricks seamlessly into your applications using RESTful principles. - -### [Webhooks](/developer-docs/webhooks) - -Learn about Formbricks Webhooks and how to set up event-driven notifications in your applications using our UI as well as API. Stay updated with real-time data and trigger actions based on specific response events within your Formbricks environment. - -### [Contributing to Formbricks](/developer-docs/contributing/get-started) - -Interested in contributing to the Formbricks ecosystem? This page provides guidance on how to run Formbricks locally, report issues, contribute through code, and collaborate with the Formbricks community to enhance the platform for everyone. - -If you have any questions or need help, feel free to reach out to us on **[Github Discussions](https://github.com/formbricks/formbricks/discussions)**. Happy coding! diff --git a/apps/docs/app/developer-docs/react-native-in-app-surveys/page.mdx b/apps/docs/app/developer-docs/react-native-in-app-surveys/page.mdx deleted file mode 100644 index 08fab4b9b9..0000000000 --- a/apps/docs/app/developer-docs/react-native-in-app-surveys/page.mdx +++ /dev/null @@ -1,127 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -export const metadata = { - title: "React Native: Formbricks App SDK", - description: - "Integrate Formbricks App Surveys into your React Native apps with the Formbricks React Native SDK.", -}; - -#### Developer Docs - -# React Native: In App Surveys - -### Overview - -The Formbricks React Native SDK can be used for seamlessly integrating App Surveys into your React Native Apps. Here, w'll explore how to leverage the SDK for in app surveys. The SDK is [available on npm.](https://www.npmjs.com/package/@formbricks/react-native) - -### Install - - - - -```js {{ title: 'npm' }} -npm install @formbricks/react-native -``` - -```js {{ title: 'yarn' }} -yarn add @formbricks/react-native -``` - -```js {{ title: 'pnpm' }} -pnpm add @formbricks/react-native -``` - - - - -## Methods - -### Initialize Formbricks - -In your React Native app, initialize the Formbricks React Native Client for app surveys where you pass the userId (creates a user if not existing in Formbricks) to attribute & target the user based on their actions. - - - - -```javascript -// other imports -import Formbricks from "@formbricks/react-native"; - -const config = { - environmentId: "", - apiHost: "", - userId: "", -}; - -export default function App() { - return ( - <> - {/* Your app content */} - - - ); -} -``` - - - - -The moment you initialise Formbricks, your user will start seeing surveys that get triggered on simpler actions such as on New Session. - -### Set Attribute - -You can set custom attributes for the identified user. This can be helpful for segmenting users based on specific characteristics or properties. To learn how to set custom user attributes, please check out our [User Attributes Guide](/app-surveys/user-identification). - - - - -```js -formbricks.setAttribute("Plan", "Paid"); -``` - - - - -### Track Action - -Track user actions to trigger surveys based on user interactions, such as button clicks or scrolling: - - - - -```js -formbricks.track("Clicked on Claim"); -``` - - - - -### Logout - -To log out and deinitialize Formbricks, use the formbricks.logout() function. This action clears the current initialization configuration and erases stored frontend information, such as the surveys a user has viewed or completed. It's an important step when a user logs out of your application or when you want to reset Formbricks. - - - - -```js -formbricks.logout(); -``` - - - - -After calling formbricks.logout(), you'll need to reinitialize Formbricks before using any of its features again. Ensure that you properly reinitialize Formbricks to avoid unexpected errors or behavior in your application. - -### Reset - -Reset the current instance and fetch the latest surveys and state again: - - - - -```js -formbricks.reset(); -``` - - - diff --git a/apps/docs/app/developer-docs/rest-api/images/add-api-key.webp b/apps/docs/app/developer-docs/rest-api/images/add-api-key.webp deleted file mode 100644 index cde1055849..0000000000 Binary files a/apps/docs/app/developer-docs/rest-api/images/add-api-key.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/rest-api/images/api-key-secret.webp b/apps/docs/app/developer-docs/rest-api/images/api-key-secret.webp deleted file mode 100644 index 5608c1f8c0..0000000000 Binary files a/apps/docs/app/developer-docs/rest-api/images/api-key-secret.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/rest-api/page.mdx b/apps/docs/app/developer-docs/rest-api/page.mdx deleted file mode 100644 index 3c1b0a8bff..0000000000 --- a/apps/docs/app/developer-docs/rest-api/page.mdx +++ /dev/null @@ -1,130 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import AddApiKey from "./images/add-api-key.webp"; -import ApiKeySecret from "./images/api-key-secret.webp"; - -export const metadata = { - title: "Formbricks API Overview: Public Client & Management API Breakdown", - description: - "Formbricks provides a powerful API to manage your surveys, responses, users, displays, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling", -}; - -#### API - -# API Overview - -Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings. - -View our [API Documentation](/api-docs) in more than 30 frameworks and languages. - -## Public Client API - -The [Public Client API](/api-docs#tag/Client-API) is designed for our SDKs and **does not require authentication**. This API is ideal for client-side interactions, as it doesn't expose sensitive information. - -We currently have the following Client API methods exposed and below is their documentation attached in Postman: - -- [Displays API](/api-docs#tag/Public-Client-API-greater-Displays) - Mark Survey as Displayed or Update an existing Display by linking it with a Response for a Person -- [People API](/api-docs#tag/Public-Client-API-greater-People) - Create & Update a Person (e.g. attributes, email, userId, etc) -- [Responses API](/api-docs#tag/Public-Client-API-greater-Responses) - Create & Update a Response for a Survey - -## Management API - -The [Management API](/api-docs#tag/Management-API) provides access to all data and settings that your account has access to in the Formbricks app. This API **requires a personal API Key** for authentication, which can be generated in the Settings section of the Formbricks app. Checkout the [API Key Setup](#how-to-generate-an-api-key) below to generate & manage API Keys. - -We currently have the following Management API methods exposed and below is their documentation attached in Postman: - -- [Action Class API](/api-docs#tag/Management-API-greater-Action-Class) - Create, List, and Delete Action Classes -- [Attribute Class API](/api-docs#tag/Management-API-greater-Attribute-Class) - Create, List, and Delete Attribute Classes -- [Me API](/api-docs#tag/Management-API-greater-Me) - Retrieve Account Information -- [People API](/api-docs#tag/Management-API-greater-People) - List and Delete People -- [Response API](/api-docs#tag/Management-API-greater-Response) - List, List by Survey, Update, and Delete Responses -- [Survey API](/api-docs#tag/Management-API-greater-Survey) - List, Create, Update, generate multiple suId and Delete Surveys -- [Webhook API](/api-docs#tag/Management-API-greater-Webhook) - List, Create, and Delete Webhooks - -## How to Generate an API key - -The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!** - -1. Go to your settings on [app.formbricks.com](https://app.formbricks.com). -2. Go to page “API keys” - -3. Create a key for the development or production environment. -4. Copy the key immediately. You won’t be able to see it again. - - - - ### Store API key safely! Anyone who has your API key has full control over your account. For security reasons, you cannot view the API key again. - - -### Test your API Key - -Hit the below request to verify that you are authenticated with your API Key and the server is responding. - -## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }} - - - - - Get the project details and environment type of your account. - - ### Mandatory Headers - - - - Your Formbricks API key. - - - - ### Delete a personal API key - - 1. Go to settings on [app.formbricks.com](https://app.formbricks.com/). - 2. Go to page “API keys”. - 3. Find the key you wish to revoke and select “Delete”. - 4. Your API key will stop working immediately. - - - - - - - ```bash {{ title: 'cURL' }} - curl --location \ - 'https://app.formbricks.com/api/v1/me' \ - --header \ - 'x-api-key: ' - ``` - - - - - - ```json {{title:'200 Success'}} - { - "id": "cll2m30r70004mx0huqkitgqv", - "createdAt": "2023-08-08T18:04:59.922Z", - "updatedAt": "2023-08-08T18:04:59.922Z", - "type": "production", - "project": { - "id": "cll2m30r60003mx0hnemjfckr", - "name": "My Project" - }, - "appSetupCompleted": false, - "websiteSetupCompleted": false, - } - ``` - ```json {{ title: '401 Not Authenticated' }} - Not authenticated - ``` - - - - - -**Can’t figure it out?**: **[Get help in Github Discussions](https://github.com/formbricks/formbricks/discussions)** - ---- diff --git a/apps/docs/app/developer-docs/webhooks/images/step-five.webp b/apps/docs/app/developer-docs/webhooks/images/step-five.webp deleted file mode 100644 index 1f0cef8a36..0000000000 Binary files a/apps/docs/app/developer-docs/webhooks/images/step-five.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/webhooks/images/step-four.webp b/apps/docs/app/developer-docs/webhooks/images/step-four.webp deleted file mode 100644 index ca5a560287..0000000000 Binary files a/apps/docs/app/developer-docs/webhooks/images/step-four.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/webhooks/images/step-one.webp b/apps/docs/app/developer-docs/webhooks/images/step-one.webp deleted file mode 100644 index ad3704d819..0000000000 Binary files a/apps/docs/app/developer-docs/webhooks/images/step-one.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/webhooks/images/step-three.webp b/apps/docs/app/developer-docs/webhooks/images/step-three.webp deleted file mode 100644 index 5fe2cfb98d..0000000000 Binary files a/apps/docs/app/developer-docs/webhooks/images/step-three.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/webhooks/images/step-two.webp b/apps/docs/app/developer-docs/webhooks/images/step-two.webp deleted file mode 100644 index 98f9bf3b0d..0000000000 Binary files a/apps/docs/app/developer-docs/webhooks/images/step-two.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/webhooks/page.mdx b/apps/docs/app/developer-docs/webhooks/page.mdx deleted file mode 100644 index 89e7ed3e04..0000000000 --- a/apps/docs/app/developer-docs/webhooks/page.mdx +++ /dev/null @@ -1,64 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; - -import StepFive from "./images/step-five.webp"; -import StepFour from "./images/step-four.webp"; -import StepOne from "./images/step-one.webp"; -import StepThree from "./images/step-three.webp"; -import StepTwo from "./images/step-two.webp"; - -export const metadata = { - title: "Formbricks Webhooks Overview", - description: - "Formbricks provides a powerful Webhooks system to notify your application about response related events that happen in your Formbricks surveys. Learn how to set up Webhooks, the available events, and how to handle them in your application.", -}; - -#### Developer Docs - -# Webhooks - -Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment. - -These APIs are designed to facilitate seamless integration of Formbricks with third-party systems. By making use of our webhook API, you can automate the process of sending data to these systems whenever significant events occur within your Formbricks environment. - -### Leveraging Webhooks - -The behavior of the webhooks is determined by their trigger settings. The trigger determines which updates the webhook sends. Current available triggers include: - -- "responseCreated" -- "responseUpdated", -- "responseFinished". - -### Creating Webhooks - -There are 2 ways for you to create webhooks with Formbricks ie either via our App UI or through API. - -**UI:** - -1. Login to the Formbricks App & switch to the Integrations Tab - - - -2. Click on **Manage Webhooks** & then **Add Webhook** button: - - - -3. Add your webhook listener endpoint & test it to make sure it can receive the test endpoint otherwise you will not be able to save it. - - - -4. Now add the triggers you want to listen to and the surveys! - - - -That’s it! Your webhooks will not start receiving data as soon as it arrives! - - - -- API: Use our documented methods on Creation, List, & Deletion endpoints of the Webhook API mentioned in the [Postman Documenter](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - ---- diff --git a/apps/docs/app/developer-docs/website-survey-sdk/images/1-set-up-website-micro-survey-popup.webp b/apps/docs/app/developer-docs/website-survey-sdk/images/1-set-up-website-micro-survey-popup.webp deleted file mode 100644 index 7781c1e9d2..0000000000 Binary files a/apps/docs/app/developer-docs/website-survey-sdk/images/1-set-up-website-micro-survey-popup.webp and /dev/null differ diff --git a/apps/docs/app/developer-docs/website-survey-sdk/page.mdx b/apps/docs/app/developer-docs/website-survey-sdk/page.mdx deleted file mode 100644 index e048f3342f..0000000000 --- a/apps/docs/app/developer-docs/website-survey-sdk/page.mdx +++ /dev/null @@ -1,166 +0,0 @@ -import { MdxImage } from "@/components/mdx-image"; -import I1 from "./images/1-set-up-website-micro-survey-popup.webp"; - -export const metadata = { - title: "Formbricks Website Survey SDK", - description: - "Run targeted pop-up surveys on your public websites with the Formbricks JS SDK for Website Surveys. Learn how to integrate the SDK, track user actions, and trigger surveys based on user interactions.", -}; - -#### Developer Docs - -# SDK: Run Surveys On Public Websites - -### Overview - -The Formbricks JS SDK is a 2-in-1 SDK for seamlessly integrating both App Surveys and Website Surveys into your projects. In this section, we'll explore how to leverage the SDK specifically for website surveys. - -### Install - - - - -```js {{ title: 'npm' }} -npm install @formbricks/js -``` - -```js {{ title: 'yarn' }} -yarn add @formbricks/js -``` - -```js {{ title: 'pnpm' }} -pnpm add @formbricks/js -``` - - - - -## Methods - -### Initialize Formbricks - -Initialize the Formbricks JS Client specifically for website surveys, ideal for public-facing websites: - -you cannot set any other attribute other than language (optional) in website surveys. - - - - -```javascript -import formbricks from "@formbricks/js/website"; - -formbricks.init({ - environmentId: "", // required - apiHost: "", // required - attributes: { - // optional - language: "de", // optional - }, -}); -``` - - - - -The moment you initialise Formbricks, your users will start seeing surveys that get triggered on simpler actions such as on New Session, Page Exit, & other custom actions! - - -Formbricks JS is a client SDK meant to be run client-side in their browser so make sure the window object is accessible. Below is an example of how you can set it! - - - - -```js -if (window !== undefined) { - formbricks.init({ - environmentId: "", - apiHost: "", - }); -} else { - console.error("Window object not accessible to init Formbricks"); -} -``` - - - - - - -### Track Action - -Track session actions to trigger surveys based on their interactions on your website, such as button clicks or scrolling: - - - - -```js -formbricks.track("Clicked on Claim"); -``` - - - - -### Reset - -Reset the current instance and fetch the latest surveys and state again: - - - - -```js -formbricks.reset(); -``` - - - - -### Register Route Change: - -Listen for page changes and dynamically show surveys configured via no-code actions in the Formbricks website: - - - This is only needed when your framework has a custom routing system and you want to trigger surveys on route changes. For example: NextJs - - - - - -```js -formbricks.registerRouteChange(); -``` - - - - -## Debug Mode - -To enable debug mode in Formbricks, add `?formbricksDebug=true` to your app’s URL. - -For example, if you’ve integrated Formbricks JS to your app hosted at `https://example.com`, then change the URL to `https://example.com?formbricksDebug=true` and refresh the page, now view the console logs to see the debug mode live in action. - -This activates detailed debug messages in the browser console, providing deeper insights into Formbricks' operation and potential issues. - ---- - -## Troubleshooting - -In case you don’t see your survey right away, here's what you can do. Go through these to find the error fast: - -### Formbricks Cloud and your website are not connected properly. - -Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted instance's URL and go to the Website connection in the Configuration. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud: - - -**How to fix it:** - -1. Check if your website loads the Formbricks widget correctly. -2. Make sure you have `debug` mode enabled in your integration and you should see the Formbricks debug logs in your browser console while being in your app (right click in the browser, `Inspect`, switch to the console tab). If you don’t see them, double check your integration. - ---- - -If you have any questions or need help, feel free to reach out to us on **[Github Discussions](https://github.com/formbricks/formbricks/discussions)** diff --git a/apps/docs/app/favicon.ico b/apps/docs/app/favicon.ico deleted file mode 100644 index ef1fd01ff7..0000000000 Binary files a/apps/docs/app/favicon.ico and /dev/null differ diff --git a/apps/docs/app/introduction/how-it-works/analytics.webp b/apps/docs/app/introduction/how-it-works/analytics.webp deleted file mode 100644 index ab96643833..0000000000 Binary files a/apps/docs/app/introduction/how-it-works/analytics.webp and /dev/null differ diff --git a/apps/docs/app/introduction/how-it-works/form-builder.webp b/apps/docs/app/introduction/how-it-works/form-builder.webp deleted file mode 100644 index dba81befce..0000000000 Binary files a/apps/docs/app/introduction/how-it-works/form-builder.webp and /dev/null differ diff --git a/apps/docs/app/introduction/how-it-works/integrations.webp b/apps/docs/app/introduction/how-it-works/integrations.webp deleted file mode 100644 index 8db3cf2b58..0000000000 Binary files a/apps/docs/app/introduction/how-it-works/integrations.webp and /dev/null differ diff --git a/apps/docs/app/introduction/how-it-works/page.mdx b/apps/docs/app/introduction/how-it-works/page.mdx deleted file mode 100644 index b2adef789a..0000000000 --- a/apps/docs/app/introduction/how-it-works/page.mdx +++ /dev/null @@ -1,75 +0,0 @@ -import Analytics from "./analytics.webp"; -import FormBuilder from "./form-builder.webp"; -import integrations from "./integrations.webp"; -import Targeting from "./targeting.webp"; -import Trigger from "./trigger.webp"; - -import { MdxImage } from "@/components/mdx-image"; - -export const metadata = { - title: "Formbricks Components Overview", - description: - "Formbricks is broadly composed of four components: An open source form builder, targeting & triggers, integrations and analytics & insights.", -}; - -#### Introduction - -# How Formbricks works - -Formbricks is broadly composed of four elements, which enable gathering, analyzing and reporting of experience data: - -1. **Survey builder**: Create and customize your surveys with a user-friendly, no-code interface. -2. **Targeting & Triggers**: Define user segments based on attributes and set event-based triggers to display your surveys to the right users at the right time. -3. **Integrations**: Seamlessly integrate Formbricks with your current stack using the provided SDKs, native integrations or open APIs. -4. **Analytics & Insights**: Analyze user responses and gain actionable insights to make informed product decisions. - -## Survey builder - -The survey builder is where you create and customize your surveys. With its intuitive drag-and-drop interface, you can easily add different question types, set response options, and apply your branding to the survey forms. The survey builder allows you to preview your survey in real-time, ensuring it looks and feels perfect for your users. - - - -## Targeting & Triggers - -Formbricks offers fine-grained user targeting and event-based triggers to help you display your surveys to the most relevant audience. Using the platform, you can define user segments based on attributes and behaviors, and set up triggers to show your surveys at specific moments within your product. This ensures that you're capturing the most accurate and valuable feedback possible. - - - - - -## Integration - -Integrating Formbricks into your web is a breeze. With SDKs for popular web frameworks like React, and an HTML snippet for non-framework based websites, you can quickly add Formbricks to your project. The provided code snippets make it easy to initialize the Formbricks widget and configure it to communicate with your backend. - - - -## Analytics & Insights - -Formbricks provides powerful analytics and insights to help you understand user responses and make data-driven decisions. The platform aggregates survey results and presents them in an easy-to-understand format, enabling you to identify trends, spot issues, and uncover opportunities for improvement. With Formbricks, you're always one step ahead in understanding your users and optimizing your product experience. - - diff --git a/apps/docs/app/introduction/how-it-works/targeting.webp b/apps/docs/app/introduction/how-it-works/targeting.webp deleted file mode 100644 index 9c27b02792..0000000000 Binary files a/apps/docs/app/introduction/how-it-works/targeting.webp and /dev/null differ diff --git a/apps/docs/app/introduction/how-it-works/trigger.webp b/apps/docs/app/introduction/how-it-works/trigger.webp deleted file mode 100644 index 435da6ff96..0000000000 Binary files a/apps/docs/app/introduction/how-it-works/trigger.webp and /dev/null differ diff --git a/apps/docs/app/introduction/what-is-formbricks/components/getting-started.tsx b/apps/docs/app/introduction/what-is-formbricks/components/getting-started.tsx deleted file mode 100644 index 89eae15fa5..0000000000 --- a/apps/docs/app/introduction/what-is-formbricks/components/getting-started.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Button } from "@/components/button"; -import { Heading } from "@/components/heading"; - -const gettingStarted = [ - { - href: "/website-surveys/framework-guides", - name: "Quickstart", - description: "Get up and running with our cloud and JavaScript widget for public-facing website surveys", - }, - { - href: "/website-surveys/framework-guides#next-js", - name: "Next.js App", - description: - "Integrate the Formbricks Website Survey SDK into a Next.js application with the new app directory", - }, - { - href: "/self-hosting/one-click", - name: "Self Host Single Click Deployment", - description: - "Host Formbricks on your own servers with just a single script. No need to worry about setting up databases, queues, or caches.", - }, - { - href: "/best-practices/interview-prompt", - name: "Interview Prompt", - description: "Set user interviews on autopilot for a continuous stream of interviews.", - }, -]; - -export function GettingStarted(): React.JSX.Element { - return ( -
- - Quick Resources - -
- {gettingStarted.map((guide) => ( -
-

{guide.name}

-

{guide.description}

-

- -

-
- ))} -
-
- ); -} diff --git a/apps/docs/app/introduction/what-is-formbricks/page.mdx b/apps/docs/app/introduction/what-is-formbricks/page.mdx deleted file mode 100644 index 4c6a9f2312..0000000000 --- a/apps/docs/app/introduction/what-is-formbricks/page.mdx +++ /dev/null @@ -1,58 +0,0 @@ -import { Button } from "@/components/button"; -import { HeroPattern } from "@/components/hero-pattern"; - -import { GettingStarted } from "./components/getting-started"; - -export const metadata = { - title: "Formbricks: Open Source Experience Management", - description: - "Formbricks is a versatile open source survey platform with an Experience Management Suite built on top of it. Survey customers, users or employee at any points with a perfectly timed and targeted survey.", -}; - -export const sections = []; - - - -# Formbricks – Open Source Experience Management - -Welcome to Formbricks! Formbricks is a versatile open source survey platform with an Experience Management Suite built on top of it. Survey customers, users or employee at any points with a perfectly timed and targeted survey. {{ className: 'lead' }} - -
-
- -## Formbricks - The Open Source Survey Platform - -The foundation of Formbricks is an open source (AGPLv3) survey platform. Our objective is to built a survey tool which can be used to survey any stakeholder of an organisation (user, customer, employee, etc.) at any point on any platform. - -Today, you can already replace many of the existing surveying solutions with Formbricks: - -- **Standalone surveys (share via link):** Replace Google Forms, Typeform or any other link survey tool [with Formbricks Form Builder](https://formbricks.com/open-source-form-builder). Use lots of question types and comprehensive customisations. -- **Scalable website surveys:** Even if you have millions of website visitors, Formbricks lets you run well-timed and anonymously targeted [surveys on any public website.](https://formbricks.com/website-survey) -- **Highly targeted app surveys:** Identify known users with Formbricks and enrich their profiles with attributes and specific actions. Build cohorts for [highly targeted in app surveys.](https://formbricks.com/in-app-survey) - -The surveying platform is **largely free, also for commercial use.** As we further develop the product offering, all surveying capacity will move into the forever free Community Edition. - -## Formbricks - The XM Solution - -To fund the development of the most powerful and versatile surveying platform there is, we're building a commercial offering on top of the surveying platform: The Formbricks Experience Management (XM) Suite. - -- **What is XM?** "Experience Management" describes the effort to gather, analyze and report data from any stakeholder (customers, users, employees, etc.) of an organisation to measure and then manage how their experience with the organisation is developing. - -- **Why are we excited about XM?** Empowering companies, governments and nonprofit organisations to measure how customers, citizens, employees or visitors experience their products and services is a meaningful undertaking. Life is too short for poorly managed service offerings. Formbricks XM provides the data to make human-centric decisions at scale. - -- **How does XM work on Formbricks?** Essentially, through reduction and contextualisation. In an app-like format, we strip away everything you don't need to measure a specific experience. We also provide meaningful context in matching templates, reports and best practices. - -So far, we have spent most of our time and energy building out the open source survey platform which powers the above. Stick around to see how Formbricks XM Apps will empower everyone to think and work human-centric. - -
-
- - diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx deleted file mode 100644 index ef1611cb21..0000000000 --- a/apps/docs/app/layout.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Providers } from "@/app/providers"; -import { Layout } from "@/components/layout"; -import { type Section } from "@/components/section-provider"; -import "@/styles/tailwind.css"; -import glob from "fast-glob"; -import { type Metadata } from "next"; -import { Jost } from "next/font/google"; -import Script from "next/script"; - -export const metadata: Metadata = { - title: { - template: "%s - Formbricks Docs", - default: "Formbricks Documentation", - }, -}; - -const jost = Jost({ subsets: ["latin"] }); - -async function RootLayout({ children }: { children: React.ReactNode }) { - const pages = await glob("**/*.mdx", { cwd: "src/app" }); - const allSectionsEntries: [string, Section[]][] = (await Promise.all( - pages.map(async (filename) => [ - `/${filename.replace(/(?:^|\/)page\.mdx$/, "")}`, - (await import(`./${filename}`) as { sections: Section[] }).sections, - ]) - )); - const allSections = Object.fromEntries(allSectionsEntries); - - return ( - - - {process.env.NEXT_PUBLIC_LAYER_API_KEY ? - - - - - - Formbricks - - - - - - - -
- - - - -

All systems are up and running

-
- - - - - - - - - "500": - description: Internal Server Error - headers: - Vary: - schema: - type: string - example: >- - RSC, Next-Router-State-Tree, Next-Router-Prefetch, - Accept-Encoding - Cache-Control: - schema: - type: string - example: no-store, must-revalidate - X-Powered-By: - schema: - type: string - example: Next.js - Content-Type: - schema: - type: string - example: text/html; charset=utf-8 - Content-Encoding: - schema: - type: string - example: gzip - Date: - schema: - type: string - example: Tue, 23 Apr 2024 07:05:08 GMT - Connection: - schema: - type: string - example: keep-alive - Keep-Alive: - schema: - type: string - example: timeout=5 - Transfer-Encoding: - schema: - type: string - example: chunked - content: - text/plain: - schema: - type: string - example: |- - - - - - - - - - - - - Formbricks - - - - - - - - - - - - - - - - /api/v1/management/contacts: - get: - tags: - - Management API > Contacts - summary: Get All Contacts - description: Retrieve a list of all contacts for the current environment - parameters: - - name: x-api-key - in: header - schema: - type: string - example: "{{apiKey}}" - required: true - responses: - "200": - description: OK - headers: - vary: - schema: - type: string - example: RSC, Next-Router-State-Tree, Next-Router-Prefetch - content-type: - schema: - type: string - example: application/json - Date: - schema: - type: string - example: Tue, 23 Apr 2024 12:37:25 GMT - Connection: - schema: - type: string - example: keep-alive - Keep-Alive: - schema: - type: string - example: timeout=5 - Transfer-Encoding: - schema: - type: string - example: chunked - content: - application/json: - schema: - type: object - example: - data: - - id: cm4jnsxcd012fh3aqz1b6zox6 - userId: null - createdAt: "2024-12-11T08:59:37.550Z" - updatedAt: "2024-12-11T08:59:37.550Z" - environmentId: cm37a748d0001hbwhqvvuba23 - - id: cm4la1bhn12g5cr5qh8p6v874 - userId: null - createdAt: "2024-12-12T12:09:46.859Z" - updatedAt: "2024-12-12T12:09:46.859Z" - environmentId: cm37a748d0001hbwhqvvuba23 - /api/v1/management/contacts/{contactId}: - get: - tags: - - Management API > Contacts - summary: Get Contact by ID - description: Retrieve a specific contact by its ID - parameters: - - name: x-api-key - in: header - schema: - type: string - example: "{{apiKey}}" - required: true - - name: contactId - in: path - schema: - type: string - required: true - description: The ID of the contact to retrieve - responses: - "200": - description: OK - headers: - vary: - schema: - type: string - example: RSC, Next-Router-State-Tree, Next-Router-Prefetch - content-type: - schema: - type: string - example: application/json - Date: - schema: - type: string - example: Tue, 23 Apr 2024 12:37:25 GMT - Connection: - schema: - type: string - example: keep-alive - Keep-Alive: - schema: - type: string - example: timeout=5 - Transfer-Encoding: - schema: - type: string - example: chunked - content: - application/json: - schema: - type: object - example: - data: - id: cm4jnsxcd00ufh3aqz1b6zox6 - userId: null - createdAt: "2024-12-11T08:59:37.550Z" - updatedAt: "2024-12-11T08:59:37.550Z" - environmentId: cm37a748d0008hbwhqvvuba23 - "404": - description: Not Found - content: - application/json: - schema: - type: object - example: - code: not_found - message: Contact not found - details: - resource_id: cm4jnsxcd00ufh3aqz1b6zox6 - resource_type: Contact - /api/v1/management/contact-attributes: - get: - tags: - - Management API > Contact Attributes - summary: Get All Contact Attributes - description: Retrieve a list of all contact attributes in the current environment - parameters: - - name: x-api-key - in: header - schema: - type: string - example: "{{apiKey}}" - required: true - responses: - "200": - description: OK - headers: - vary: - schema: - type: string - example: RSC, Next-Router-State-Tree, Next-Router-Prefetch - content-type: - schema: - type: string - example: application/json - Date: - schema: - type: string - example: Tue, 23 Apr 2024 12:37:25 GMT - Connection: - schema: - type: string - example: keep-alive - Keep-Alive: - schema: - type: string - example: timeout=5 - Transfer-Encoding: - schema: - type: string - example: chunked - content: - application/json: - schema: - type: object - example: - data: - - id: cm4jnsxxh00umh3aq76q08ah1 - createdAt: "2024-12-11T08:59:38.309Z" - updatedAt: "2024-12-11T08:59:38.309Z" - attributeKeyId: cm4jnsxxh00ukh3aq08gpdafw - contactId: cm4jnsxcd00ufh3aqz1b6zox6 - value: "eight" - /api/v1/management/contact-attribute-keys: - get: - tags: - - Management API > Contact Attribute Keys - summary: Get All Contact Attribute Keys - description: Retrieve a list of all contact attribute keys defined in the current environment - parameters: - - name: x-api-key - in: header - schema: - type: string - example: "{{apiKey}}" - required: true - responses: - "200": - description: OK - headers: - vary: - schema: - type: string - example: RSC, Next-Router-State-Tree, Next-Router-Prefetch - content-type: - schema: - type: string - example: application/json - Date: - schema: - type: string - example: Tue, 23 Apr 2024 12:37:25 GMT - Connection: - schema: - type: string - example: keep-alive - Keep-Alive: - schema: - type: string - example: timeout=5 - Transfer-Encoding: - schema: - type: string - example: chunked - content: - application/json: - schema: - type: object - example: - data: - - id: cm37a748d000ahbwhd5bbwc09 - createdAt: "2024-11-07T12:25:48.589Z" - updatedAt: "2024-12-03T13:30:28.620Z" - isUnique: true - key: email - name: email - description: The email of the person - type: default - environmentId: cm37a748d0008hbwhqvvuba23 - /api/v1/management/contact-attribute-keys/{contactAttributeKeyId}: - get: - tags: - - Management API > Contact Attribute Keys - summary: Get Contact Attribute Key by ID - description: Retrieve a specific contact attribute key by its ID. This returns detailed information about how a particular attribute is defined in your environment. - parameters: - - name: x-api-key - in: header - schema: - type: string - example: "{{apiKey}}" - required: true - - name: contactAttributeKeyId - in: path - schema: - type: string - required: true - description: The ID of the contact attribute key to retrieve - responses: - "200": - description: OK - headers: - vary: - schema: - type: string - example: RSC, Next-Router-State-Tree, Next-Router-Prefetch - content-type: - schema: - type: string - example: application/json - content: - application/json: - schema: - type: object - properties: - data: - type: object - properties: - id: - type: string - description: Unique identifier for the attribute key - createdAt: - type: string - format: date-time - description: Timestamp when the attribute key was created - updatedAt: - type: string - format: date-time - description: Timestamp when the attribute key was last updated - isUnique: - type: boolean - description: Whether this attribute must have unique values across contacts - key: - type: string - description: The machine-readable identifier for the attribute - name: - type: string - description: Human-readable name for the attribute - description: - type: string - description: Detailed description of what this attribute represents - type: - type: string - description: The type of attribute (e.g., "default") - environmentId: - type: string - description: ID of the environment this attribute key belongs to - example: - data: - id: cm37a748d000ahbwhd5bbwc09 - createdAt: "2024-11-07T12:25:48.589Z" - updatedAt: "2024-12-03T13:30:28.620Z" - isUnique: true - key: email - name: email - description: The email of the person - type: default - environmentId: cm37a748d0008hbwhqvvuba23 - "404": - description: Not Found - content: - application/json: - schema: - type: object - properties: - code: - type: string - example: not_found - message: - type: string - example: Contact attribute key not found - details: - type: object - properties: - resource_id: - type: string - resource_type: - type: string - example: ContactAttributeKey - example: - code: not_found - message: Contact attribute key not found - details: - resource_id: cm37a748d000ahbwhd5bbwc09 - resource_type: ContactAttributeKey diff --git a/apps/docs/public/robots.txt b/apps/docs/public/robots.txt deleted file mode 100644 index 71a3a790ab..0000000000 --- a/apps/docs/public/robots.txt +++ /dev/null @@ -1,9 +0,0 @@ -# * -User-agent: * -Allow: / - -# Host -Host: https://formbricks.com - -# Sitemaps -Sitemap: https://formbricks.com/sitemap.xml diff --git a/apps/docs/robots.ts b/apps/docs/robots.ts deleted file mode 100644 index f0bbed7992..0000000000 --- a/apps/docs/robots.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MetadataRoute } from "next"; - -const robots = (): MetadataRoute.Robots => { - return { - rules: { - userAgent: "*", - allow: "/", - }, - }; -}; - -export default robots; diff --git a/apps/docs/styles/tailwind.css b/apps/docs/styles/tailwind.css deleted file mode 100644 index 5a00a35a50..0000000000 --- a/apps/docs/styles/tailwind.css +++ /dev/null @@ -1,21 +0,0 @@ -@layer base { - :root { - --shiki-color-text: theme("colors.white"); - --shiki-token-constant: theme("colors.teal.300"); - --shiki-token-string: theme("colors.teal.300"); - --shiki-token-comment: theme("colors.slate.500"); - --shiki-token-keyword: theme("colors.sky.300"); - --shiki-token-parameter: theme("colors.pink.300"); - --shiki-token-function: theme("colors.violet.300"); - --shiki-token-string-expression: theme("colors.teal.300"); - --shiki-token-punctuation: theme("colors.slate.200"); - } - - [inert] ::-webkit-scrollbar { - display: none; - } -} - -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/apps/docs/tailwind.config.ts b/apps/docs/tailwind.config.ts deleted file mode 100644 index 9c2d63f55e..0000000000 --- a/apps/docs/tailwind.config.ts +++ /dev/null @@ -1,85 +0,0 @@ -import headlessuiPlugin from "@headlessui/tailwindcss"; -import typographyPlugin from "@tailwindcss/typography"; -import { type Config } from "tailwindcss"; -import defaultTheme from "tailwindcss/defaultTheme"; -import typographyStyles from "./typography"; - -export default { - trailingSlash: true, - content: [ - // app content - "./app/**/*.{js,mjs,jsx,ts,tsx,mdx}", - "./components/**/*.{js,mjs,jsx,ts,tsx,mdx}", - "./lib/**/*.{js,mjs,jsx,ts,tsx,mdx}", - "./mdx/**/*.{js,mjs,jsx,ts,tsx,mdx}", - ], - darkMode: "class", - theme: { - fontSize: { - "2xs": ["0.75rem", { lineHeight: "1.25rem" }], - xs: ["0.75rem", { lineHeight: "1rem" }], - sm: ["0.875rem", { lineHeight: "1.5rem" }], - base: ["1rem", { lineHeight: "2rem" }], - lg: ["1.125rem", { lineHeight: "1.75rem" }], - xl: ["1.25rem", { lineHeight: "2rem" }], - "2xl": ["1.5rem", { lineHeight: "2.5rem" }], - "3xl": ["2rem", { lineHeight: "2.5rem" }], - "4xl": ["2.5rem", { lineHeight: "3rem" }], - "5xl": ["3rem", { lineHeight: "3.5rem" }], - "6xl": ["3.75rem", { lineHeight: "1" }], - "7xl": ["4.5rem", { lineHeight: "1" }], - "8xl": ["6rem", { lineHeight: "1" }], - "9xl": ["8rem", { lineHeight: "1" }], - }, - typography: typographyStyles, - extend: { - keyframes: { - scroll: { - "0%": { transform: "translateX(0%)" }, - "100%": { transform: "translateX(-1990px)" }, - }, - }, - animation: { - scroll: "scroll 60s linear infinite", - }, - boxShadow: { - glow: "0 0 4px rgb(0 0 0 / 0.1)", - }, - dropShadow: { - card: "0px 4px 12px rgba(0, 0, 0, 0.5);", - }, - maxWidth: { - lg: "33rem", - "2xl": "40rem", - "3xl": "50rem", - "5xl": "66rem", - "8xl": "88rem", - }, - colors: { - brand: { - DEFAULT: "#00C4B8", - light: "#00C4B8", - dark: "#00C4B8", - }, - black: { - DEFAULT: "#0F172A", - }, - }, - fontFamily: { - sans: ["Jost", ...defaultTheme.fontFamily.sans], - display: ["Lexend", ...defaultTheme.fontFamily.sans], - kablammo: ["Kablammo", "sans"], - }, - screens: { - xs: "430px", - }, - opacity: { - 1: "0.01", - 2.5: "0.025", - 7.5: "0.075", - 15: "0.15", - }, - }, - }, - plugins: [typographyPlugin, headlessuiPlugin], -} satisfies Config; diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json deleted file mode 100644 index a9f3700021..0000000000 --- a/apps/docs/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "noUncheckedIndexedAccess": true, - "paths": { - "@/*": ["./*"] - }, - "plugins": [ - { - "name": "next" - } - ], - "strictNullChecks": true - }, - "exclude": ["../../.env", "node_modules"], - "extends": "@formbricks/config-typescript/nextjs.json", - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"] -} diff --git a/apps/docs/types.d.ts b/apps/docs/types.d.ts deleted file mode 100644 index 4aec4633a0..0000000000 --- a/apps/docs/types.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type SearchOptions } from "flexsearch"; - -declare module "@/mdx/search.mjs" { - export type Result = { - url: string; - title: string; - pageTitle?: string; - }; - - export const search: (query: string, options?: SearchOptions) => Array; -} diff --git a/apps/docs/typography.ts b/apps/docs/typography.ts deleted file mode 100644 index 33b592aebe..0000000000 --- a/apps/docs/typography.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { type PluginUtils } from "tailwindcss/types/config"; - -const typographyStyles = ({ theme }: PluginUtils) => { - return { - DEFAULT: { - css: { - "--tw-prose-body": theme("colors.slate.700"), - "--tw-prose-headings": theme("colors.slate.900"), - "--tw-prose-links": theme("colors.teal.500"), - "--tw-prose-links-hover": theme("colors.teal.600"), - "--tw-prose-links-underline": theme("colors.teal.500 / 0.3"), - "--tw-prose-bold": theme("colors.slate.900"), - "--tw-prose-counters": theme("colors.slate.500"), - "--tw-prose-bullets": theme("colors.slate.300"), - "--tw-prose-hr": theme("colors.slate.900 / 0.05"), - "--tw-prose-quotes": theme("colors.slate.900"), - "--tw-prose-quote-borders": theme("colors.slate.200"), - "--tw-prose-captions": theme("colors.slate.500"), - "--tw-prose-code": theme("colors.slate.900"), - "--tw-prose-code-bg": theme("colors.slate.100"), - "--tw-prose-code-ring": theme("colors.slate.300"), - "--tw-prose-th-borders": theme("colors.slate.300"), - "--tw-prose-td-borders": theme("colors.slate.200"), - - "--tw-prose-invert-body": theme("colors.slate.200"), - "--tw-prose-invert-headings": theme("colors.white"), - "--tw-prose-invert-links": theme("colors.teal.200"), - "--tw-prose-invert-links-hover": theme("colors.teal.500"), - "--tw-prose-invert-links-underline": theme("colors.teal.500 / 0.3"), - "--tw-prose-invert-bold": theme("colors.white"), - "--tw-prose-invert-counters": theme("colors.slate.200"), - "--tw-prose-invert-bullets": theme("colors.slate.600"), - "--tw-prose-invert-hr": theme("colors.white / 0.05"), - "--tw-prose-invert-quotes": theme("colors.slate.100"), - "--tw-prose-invert-quote-borders": theme("colors.slate.700"), - "--tw-prose-invert-captions": theme("colors.slate.200"), - "--tw-prose-invert-code": theme("colors.white"), - "--tw-prose-invert-code-bg": theme("colors.slate.700 / 0.15"), - "--tw-prose-invert-code-ring": theme("colors.white / 0.1"), - "--tw-prose-invert-th-borders": theme("colors.slate.600"), - "--tw-prose-invert-td-borders": theme("colors.slate.700"), - - // Base - color: "var(--tw-prose-body)", - fontSize: theme("fontSize.base")[0], - lineHeight: theme("lineHeight.7"), - - // Layout - "> *": { - maxWidth: theme("maxWidth.2xl"), - marginLeft: "auto", - marginRight: "auto", - "@screen lg": { - maxWidth: theme("maxWidth.3xl"), - marginLeft: `calc(50% - min(50%, ${theme("maxWidth.lg")}))`, - marginRight: `calc(50% - min(50%, ${theme("maxWidth.lg")}))`, - }, - }, - - // Text - p: { - marginTop: theme("spacing.6"), - marginBottom: theme("spacing.6"), - }, - '[class~="lead"]': { - fontSize: theme("fontSize.base")[0], - ...theme("fontSize.base")[1], - }, - - // Lists - ol: { - listStyleType: "decimal", - marginTop: theme("spacing.5"), - marginBottom: theme("spacing.5"), - paddingLeft: "1.625rem", - }, - 'ol[type="A"]': { - listStyleType: "upper-alpha", - }, - 'ol[type="a"]': { - listStyleType: "lower-alpha", - }, - 'ol[type="A" s]': { - listStyleType: "upper-alpha", - }, - 'ol[type="a" s]': { - listStyleType: "lower-alpha", - }, - 'ol[type="I"]': { - listStyleType: "upper-roman", - }, - 'ol[type="i"]': { - listStyleType: "lower-roman", - }, - 'ol[type="I" s]': { - listStyleType: "upper-roman", - }, - 'ol[type="i" s]': { - listStyleType: "lower-roman", - }, - 'ol[type="1"]': { - listStyleType: "decimal", - }, - ul: { - listStyleType: "disc", - marginTop: theme("spacing.5"), - marginBottom: theme("spacing.5"), - paddingLeft: "1.625rem", - }, - li: { - marginTop: theme("spacing.2"), - marginBottom: theme("spacing.2"), - }, - ":is(ol, ul) > li": { - paddingLeft: theme("spacing[1.5]"), - }, - "ol > li::marker": { - fontWeight: "400", - color: "var(--tw-prose-counters)", - }, - "ul > li::marker": { - color: "var(--tw-prose-bullets)", - }, - "> ul > li p": { - marginTop: theme("spacing.3"), - marginBottom: theme("spacing.3"), - }, - "> ul > li > *:first-child": { - marginTop: theme("spacing.5"), - }, - "> ul > li > *:last-child": { - marginBottom: theme("spacing.5"), - }, - "> ol > li > *:first-child": { - marginTop: theme("spacing.5"), - }, - "> ol > li > *:last-child": { - marginBottom: theme("spacing.5"), - }, - "ul ul, ul ol, ol ul, ol ol": { - marginTop: theme("spacing.3"), - marginBottom: theme("spacing.3"), - }, - - // Horizontal rules - hr: { - borderColor: "var(--tw-prose-hr)", - borderTopWidth: 1, - marginTop: theme("spacing.16"), - marginBottom: theme("spacing.16"), - maxWidth: "none", - marginLeft: `calc(-1 * ${theme("spacing.4")})`, - marginRight: `calc(-1 * ${theme("spacing.4")})`, - "@screen sm": { - marginLeft: `calc(-1 * ${theme("spacing.6")})`, - marginRight: `calc(-1 * ${theme("spacing.6")})`, - }, - "@screen lg": { - marginLeft: `calc(-1 * ${theme("spacing.8")})`, - marginRight: `calc(-1 * ${theme("spacing.8")})`, - }, - }, - - // Quotes - blockquote: { - fontWeight: "500", - fontStyle: "italic", - color: "var(--tw-prose-quotes)", - borderLeftWidth: "0.25rem", - borderLeftColor: "var(--tw-prose-quote-borders)", - quotes: '"\\201C""\\201D""\\2018""\\2019"', - marginTop: theme("spacing.8"), - marginBottom: theme("spacing.8"), - paddingLeft: theme("spacing.5"), - }, - "blockquote p:first-of-type::before": { - content: "open-quote", - }, - "blockquote p:last-of-type::after": { - content: "close-quote", - }, - - // Headings - h1: { - color: "var(--tw-prose-headings)", - fontWeight: "500", - fontSize: theme("fontSize.2xl")[0], - ...theme("fontSize.2xl")[1], - marginBottom: theme("spacing.2"), - }, - h2: { - color: "var(--tw-prose-headings)", - fontWeight: "600", - fontSize: theme("fontSize.lg")[0], - ...theme("fontSize.xl")[1], - marginTop: theme("spacing.16"), - marginBottom: theme("spacing.2"), - }, - h3: { - color: "var(--tw-prose-headings)", - fontSize: theme("fontSize.base")[0], - ...theme("fontSize.lg")[1], - fontWeight: "600", - marginTop: theme("spacing.10"), - marginBottom: theme("spacing.2"), - }, - - h4: { - color: "var(--tw-prose-body)", - fontSize: theme("fontSize.base")[0], - ...theme("fontSize.base")[1], - fontWeight: "300", - marginTop: theme("spacing.10"), - marginBottom: theme("spacing.2"), - }, - - // Media - "img, video, figure": { - marginTop: theme("spacing.8"), - marginBottom: theme("spacing.8"), - }, - "figure > *": { - marginTop: "0", - marginBottom: "0", - }, - figcaption: { - color: "var(--tw-prose-captions)", - fontSize: theme("fontSize.xs")[0], - ...theme("fontSize.xs")[1], - marginTop: theme("spacing.2"), - }, - - // Tables - table: { - width: "100%", - tableLayout: "auto", - textAlign: "left", - marginTop: theme("spacing.8"), - marginBottom: theme("spacing.8"), - lineHeight: theme("lineHeight.6"), - }, - thead: { - borderBottomWidth: "1px", - borderBottomColor: "var(--tw-prose-th-borders)", - }, - "thead th": { - color: "var(--tw-prose-headings)", - fontWeight: "600", - verticalAlign: "bottom", - paddingRight: theme("spacing.2"), - paddingBottom: theme("spacing.2"), - paddingLeft: theme("spacing.2"), - }, - "thead th:first-child": { - paddingLeft: "0", - }, - "thead th:last-child": { - paddingRight: "0", - }, - "tbody tr": { - borderBottomWidth: "1px", - borderBottomColor: "var(--tw-prose-td-borders)", - }, - "tbody tr:last-child": { - borderBottomWidth: "0", - }, - "tbody td": { - verticalAlign: "baseline", - }, - tfoot: { - borderTopWidth: "1px", - borderTopColor: "var(--tw-prose-th-borders)", - }, - "tfoot td": { - verticalAlign: "top", - }, - ":is(tbody, tfoot) td": { - paddingTop: theme("spacing.2"), - paddingRight: theme("spacing.2"), - paddingBottom: theme("spacing.2"), - paddingLeft: theme("spacing.2"), - }, - ":is(tbody, tfoot) td:first-child": { - paddingLeft: "0", - }, - ":is(tbody, tfoot) td:last-child": { - paddingRight: "0", - }, - - // Inline elements - a: { - color: "var(--tw-prose-links)", - textDecoration: "underline transparent", - fontWeight: "500", - transitionProperty: "color, text-decoration-color", - transitionDuration: theme("transitionDuration.DEFAULT"), - transitionTimingFunction: theme("transitionTimingFunction.DEFAULT"), - "&:hover": { - color: "var(--tw-prose-links-hover)", - textDecorationColor: "var(--tw-prose-links-underline)", - }, - }, - ":is(h1, h2, h3) a": { - fontWeight: "inherit", - }, - strong: { - color: "var(--tw-prose-bold)", - fontWeight: "600", - }, - ":is(a, blockquote, thead th) strong": { - color: "inherit", - }, - code: { - color: "var(--tw-prose-code)", - borderRadius: theme("borderRadius.lg"), - paddingTop: theme("padding.1"), - paddingRight: theme("padding[1.5]"), - paddingBottom: theme("padding.1"), - paddingLeft: theme("padding[1.5]"), - boxShadow: "inset 0 0 0 1px var(--tw-prose-code-ring)", - backgroundColor: "var(--tw-prose-code-bg)", - fontSize: theme("fontSize.2xs"), - }, - ":is(a, h1, h2, h3, blockquote, thead th) code": { - color: "inherit", - }, - "h2 code": { - fontSize: theme("fontSize.base")[0], - fontWeight: "inherit", - }, - "h3 code": { - fontSize: theme("fontSize.sm")[0], - fontWeight: "inherit", - }, - - // Overrides - ":is(h1, h2, h3) + *": { - marginTop: "0", - }, - "> :first-child": { - marginTop: "0 !important", - }, - "> :last-child": { - marginBottom: "0 !important", - }, - }, - }, - invert: { - css: { - "--tw-prose-body": "var(--tw-prose-invert-body)", - "--tw-prose-headings": "var(--tw-prose-invert-headings)", - "--tw-prose-links": "var(--tw-prose-invert-links)", - "--tw-prose-links-hover": "var(--tw-prose-invert-links-hover)", - "--tw-prose-links-underline": "var(--tw-prose-invert-links-underline)", - "--tw-prose-bold": "var(--tw-prose-invert-bold)", - "--tw-prose-counters": "var(--tw-prose-invert-counters)", - "--tw-prose-bullets": "var(--tw-prose-invert-bullets)", - "--tw-prose-hr": "var(--tw-prose-invert-hr)", - "--tw-prose-quotes": "var(--tw-prose-invert-quotes)", - "--tw-prose-quote-borders": "var(--tw-prose-invert-quote-borders)", - "--tw-prose-captions": "var(--tw-prose-invert-captions)", - "--tw-prose-code": "var(--tw-prose-invert-code)", - "--tw-prose-code-bg": "var(--tw-prose-invert-code-bg)", - "--tw-prose-code-ring": "var(--tw-prose-invert-code-ring)", - "--tw-prose-th-borders": "var(--tw-prose-invert-th-borders)", - "--tw-prose-td-borders": "var(--tw-prose-invert-td-borders)", - }, - }, - }; -}; - -export default typographyStyles; diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 188f7710a2..82fd1a6df2 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -18,6 +18,7 @@ FROM node:22-alpine3.20 AS base FROM base AS installer # Enable corepack and prepare pnpm +RUN npm install -g corepack@latest RUN corepack enable # Install necessary build tools and compilers @@ -61,6 +62,8 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver ## step 3: setup production runner # FROM base AS runner + +RUN npm install -g corepack@latest RUN corepack enable RUN apk add --no-cache curl \ 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 84afc9291f..1010f5a939 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,8 +1,8 @@ "use client"; import { Button } from "@/modules/ui/components/button"; +import { useTranslate } from "@tolgee/react"; import { ArrowRight } from "lucide-react"; -import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { cn } from "@formbricks/lib/cn"; @@ -23,7 +23,7 @@ export const ConnectWithFormbricks = ({ widgetSetupCompleted, channel, }: ConnectWithFormbricksProps) => { - const t = useTranslations(); + const { t } = useTranslate(); const router = useRouter(); const handleFinishOnboarding = async () => { router.push(`/environments/${environment.id}/surveys`); diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx index 284327e7bc..fa6c476a4c 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx @@ -4,7 +4,7 @@ import { Button } from "@/modules/ui/components/button"; import { CodeBlock } from "@/modules/ui/components/code-block"; import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons"; import { TabBar } from "@/modules/ui/components/tab-bar"; -import { useTranslations } from "next-intl"; +import { useTranslate } from "@tolgee/react"; import Link from "next/link"; import "prismjs/themes/prism.css"; import { useState } from "react"; @@ -29,7 +29,7 @@ export const OnboardingSetupInstructions = ({ channel, widgetSetupCompleted, }: OnboardingSetupInstructionsProps) => { - const t = useTranslations(); + const { t } = useTranslate(); const [activeTab, setActiveTab] = useState(tabs[0].id); const htmlSnippetForAppSurveys = ` \n \n \n \n \n \n Formbricks\n \n \n \n \n \n \n \n
\n \n \n \n \n

All systems are up and running

\n
\n \n \n \n \n \n \n \n", + "schema": { + "type": "string" + } + } + }, + "description": "OK", + "headers": { + "Cache-Control": { + "schema": { + "example": "no-store, must-revalidate", + "type": "string" + } + }, + "Connection": { + "schema": { + "example": "keep-alive", + "type": "string" + } + }, + "Content-Encoding": { + "schema": { + "example": "gzip", + "type": "string" + } + }, + "Content-Type": { + "schema": { + "example": "text/html; charset=utf-8", + "type": "string" + } + }, + "Date": { + "schema": { + "example": "Tue, 23 Apr 2024 07:03:50 GMT", + "type": "string" + } + }, + "Keep-Alive": { + "schema": { + "example": "timeout=5", + "type": "string" + } + }, + "Transfer-Encoding": { + "schema": { + "example": "chunked", + "type": "string" + } + }, + "Vary": { + "schema": { + "example": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding", + "type": "string" + } + }, + "X-Powered-By": { + "schema": { + "example": "Next.js", + "type": "string" + } + } + } + }, + "500": { + "content": { + "text/plain": { + "example": "\n\n \n \n \n \n \n \n \n \n \n Formbricks\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "schema": { + "type": "string" + } + } + }, + "description": "Internal Server Error", + "headers": { + "Cache-Control": { + "schema": { + "example": "no-store, must-revalidate", + "type": "string" + } + }, + "Connection": { + "schema": { + "example": "keep-alive", + "type": "string" + } + }, + "Content-Encoding": { + "schema": { + "example": "gzip", + "type": "string" + } + }, + "Content-Type": { + "schema": { + "example": "text/html; charset=utf-8", + "type": "string" + } + }, + "Date": { + "schema": { + "example": "Tue, 23 Apr 2024 07:05:08 GMT", + "type": "string" + } + }, + "Keep-Alive": { + "schema": { + "example": "timeout=5", + "type": "string" + } + }, + "Transfer-Encoding": { + "schema": { + "example": "chunked", + "type": "string" + } + }, + "Vary": { + "schema": { + "example": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding", + "type": "string" + } + }, + "X-Powered-By": { + "schema": { + "example": "Next.js", + "type": "string" + } + } + } + } + }, + "summary": "Health Check", + "tags": ["default"] + } + } + }, + "servers": [ + { + "url": "http://{{baseurl}}", + "variables": { + "baseurl": { + "default": "localhost:3000" + } + } + } + ], + "tags": [ + { + "description": "The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.\n- Displays API - Mark Survey as Displayed or Responded for a Person\n- Responses API - Create & update responses for a survey\n- Environment API - Get the environment state to be used in Formbricks SDKs\n- Contacts API - Identify & update contacts (e.g. attributes)\n- User API - Identify & track users based on their attributes, device type, etc.", + "name": "Client API" + }, + { + "description": "Displays are metrics used to measure the number of times a survey was viewed both by unidentified or identified users.", + "name": "Client API > Display" + }, + { + "description": "Responses are captured whenever a user fills in your survey either partially or completely.", + "name": "Client API > Response" + }, + { + "description": "Get the environment state to be used in Formbricks SDKs", + "name": "Client API > Environment" + }, + { + "description": "Contacts are the identified users on Formbricks app that get initated when you pass a userId and have user activation enabled. This now allows you to track & show them targeted surveys based on their attributes, device type, etc.", + "name": "Client API > Contacts" + }, + { + "description": "Users are the identified users on Formbricks app that get initated when you pass a userId and have user activation enabled. This now allows you to track & show them targeted surveys based on their attributes, device type, etc. Currently, this api is only being used in the react-native sdk.", + "name": "Client API > User" + }, + { + "description": "The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.\n\n> **For Auth:** we use the `x-api-key` header \n \n\nAPI requests made to the Management API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others.\n\nTo generate, store, or delete an API key, follow the instructions provided on the following page [API Key](https://formbricks.com/docs/api/management/api-key-setup).", + "name": "Management API" + }, + { + "description": "Action Classes allow you to set behaviour methods such as when a user clicks on a buy now button, or when a user leaves the page, etc. These allow you to trigger specific surveys to these users.\n\nMethods: Get All, Get, Create, Update, and Delete Action Classes", + "name": "Management API > Action Class" + }, + { + "description": "Attribute Classes help you categorize attributes so that you can target users based on specific values of their attributes. They are associated with a Person. Read more [here](https://formbricks.com/docs/in-app-surveys/attributes)", + "name": "Management API > Attribute Class" + }, + { + "description": "The Contacts API allows you to retrieve information about contacts in your environment. Contacts represent users who have interacted with your surveys or forms.", + "name": "Management API > Contacts" + }, + { + "description": "The Contact Attributes API allows you to retrieve information about attributes associated with contacts in your environment. These attributes can include custom properties and values assigned to your contacts.", + "name": "Management API > Contact Attributes" + }, + { + "description": "The Contact Attribute Keys API allows you to retrieve information about the defined attribute keys in your environment. These keys define the structure and properties that can be assigned to contacts, such as email, name, or custom attributes.", + "name": "Management API > Contact Attribute Keys" + }, + { + "description": "Retrieve Account Information", + "name": "Management API > Me" + }, + { + "description": "Enable User Identification in Formbricks to granularly target users to show surveys to without spraying across them. This API will help you utilise Formbricks User Identification to its maximum.\n\nMethods: Get All, Get, and Delete Persons", + "name": "Management API > People" + }, + { + "description": "Responses are what your users fill in as answers to a survey. Use this cautiosly and make sure you handle this data carefully.\n\nMethods allowed: Get All, Get by Survey, Get, Update, and Delete Responses", + "name": "Management API > Response" + }, + { + "description": "Surveys are the core of user data & feedback collection. We have various types of surveys to cater to you:\n\n- In-App Surveys\n \n- Website Surveys\n \n- Link Surveys\n \n\nCreate surveys & then trigger them as per your actions & attribute filterings that you have set to make the most of them.\n\nMethods allowed: Get All, Get, Create, Update, and Delete Surveys", + "name": "Management API > Survey" + }, + { + "description": "Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment.\n\nThe behavior of the webhooks is determined by their trigger settings. The trigger determines which updates the webhook sends. Current available triggers include:\n\n- \"responseCreated\"\n \n- \"responseUpdated\", and\n \n- \"responseFinished\".\n \n\nThese APIs are designed to facilitate seamless integration of Formbricks with third-party systems. By making use of our webhook API, you can automate the process of sending data to these systems whenever significant events occur within your Formbricks environment.\n\n \n\nMethods allowed: Get All, Get ,Create, and Delete Webhooks", + "name": "Management API > Webhook" + } + ] +} diff --git a/docs/api-reference/rest-api.mdx b/docs/api-reference/rest-api.mdx new file mode 100644 index 0000000000..404099eca0 --- /dev/null +++ b/docs/api-reference/rest-api.mdx @@ -0,0 +1,129 @@ +--- +title: "REST API" +icon: "code" +description: " +Formbricks provides two APIs: the Public Client API for frontend survey interactions and the Management API for backend management tasks." +--- + + + View our [API Documentation](/api-reference) in more than 30 frameworks and languages. + + +## Public Client API + +The **Public Client API** is used by our SDKs and doesn’t require authentication, making it ideal for client-side interactions without exposing sensitive data. + +We currently have the following Client API methods exposed and below is their documentation attached in Postman: + +- [Displays API](/api-reference/client-api->-display/create-display) - Mark a survey as displayed or link a display to a response for a person. + +- [People API](/api-reference/client-api->-people/create-person) - Create & Update a Person (e.g., attributes, email, userId, etc.) + +- [Responses API](/api-reference/client-api->-response/create-response) - Create & Update a Response for a Survey. + +## Management API + +The **Management API** gives full access to all data and settings available in your Formbricks account. It requires a personal API Key for authentication, which you can generate and manage in the Settings section of the Formbricks app. + +We currently have the following Management API methods exposed and below is their documentation attached in Postman: + +- [Action Class API](/api-reference/management-api->-action-class/get-all-action-classes) - Create, List, and Delete Action Classes + +- [Attribute Class API ](/api-reference/management-api->-attribute-class/get-all-attribute-classes)- Create, List, and Delete Attribute Classes + +- [Me API](/api-reference/management-api->-me/me) - Retrieve Account Information + +- [People API](/api-reference/management-api->-people/get-all-persons) - List and Delete People + +- [Response API](/api-reference/management-api->-response/get-survey-responses) - List, List by Survey, Update, and Delete Responses + +- [Survey API](/api-reference/management-api->-survey/get-all-surveys) - List, Create, Update, generate multiple suId, and Delete Surveys + +- [Webhook API](/api-reference/management-api->-webhook/get-all-webhooks) - List, Create, and Delete Webhooks + +## How to Generate an API key + +API requests require a personal API key for authorization. This API key gives you the same rights as if you were logged in at Formbricks UI - **keep it private!** + +- Go to your settings on [Formbricks UI](https://app.formbricks.com). + +- Go to page “API keys” + +![API Keys](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738097810/image_jvhqsd.jpg) + +- Create a key for the development or production environment. + +- Copy the key immediately. You won’t be able to see it again. + +![API Key Label](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738098072/image_zjkvok.jpg) + + + **Store API key safely! Anyone who has your API key has full control over your + account. For security reasons, you cannot view the API key again.** + + +## Test your API Key + +Hit the below request to verify that you are authenticated with your API Key and the server is responding. + +### Get My Profile + +Get the project details and environment type of your account. + +### Mandatory Headers + +| Name | x-Api-Key | +| --------------- | ------------------------ | +| **Type** | string | +| **Description** | Your Formbricks API key. | + +### Request + +```bash cURL +GET - /api/v1/me + +curl --location \ +'https://app.formbricks.com/api/v1/me' \ +--header \ +'x-api-key: ' +``` + +### Response + + + + +```bash 200 (Success) +{ + "id": "cll2m30r70004mx0huqkitgqv", + "createdAt": "2023-08-08T18:04:59.922Z", + "updatedAt": "2023-08-08T18:04:59.922Z", + "type": "production", + "project": { + "id": "cll2m30r60003mx0hnemjfckr", + "name": "My Project" + }, + "appSetupCompleted": false, + "websiteSetupCompleted": false, +} +``` + +```bash 401 (Not Authenticated) +Not authenticated +``` + + + +### Delete a personal API key + +- Go to settings on [app.formbricks.com](https://app.formbricks.com/). + +- Go to the page “API keys”. + +- Find the key you wish to revoke and select “Delete”. + +- Your API key will stop working immediately. + +--- + +**Can’t figure it out?** Get help in [GitHub Discussions](https://github.com/formbricks/formbricks/discussions). \ No newline at end of file diff --git a/docs/development/contribution/contribution.mdx b/docs/development/contribution/contribution.mdx new file mode 100644 index 0000000000..dcac633452 --- /dev/null +++ b/docs/development/contribution/contribution.mdx @@ -0,0 +1,43 @@ +--- +title: "Contributing to Formbricks 🤗" +description: "How to contribute to Formbricks" +icon: "code" +--- + +We’re excited that you want to contribute to Formbricks! There are many ways to help, including reporting issues, fixing bugs, adding new features, or improving documentation. + +#### How to Contribute + +- **Issues:** Found a bug? Facing deployment problems? Have user feedback? Report an issue for the fastest response. + +- **Feature Requests:** Have an idea? Open an issue, tag it as an **Enhancement**, and clearly describe the issue you're solving. + +- **Pull Requests (PRs):** Fork the repo, make your changes, and submit a PR. + + - For small fixes, go ahead! + + - For bigger features, please discuss them with us first to ensure they align with our roadmap. + +#### Talk to Us First + +We highly recommend engaging with us on [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions) before submitting contributions. +This helps improve the chances of your PR being accepted while avoiding unnecessary work. + +#### Contributor License Agreement (CLA) + +To keep Formbricks sustainable, we require a **CLA** from all contributors. + +Once you open a PR, our **CLA bot** will prompt you to sign the agreement. We can only merge contributions after the CLA is signed. + +#### Setting Up Your Development Environment + +You can set up your environment using: + +- [**Gitpod**](/development/local-setup/gitpod) + +- [**GitHub Codespaces**](/development/local-setup/github-codespaces) + +- [**Local Machine Setup**](/development/local-setup) + +For junior developers, **Gitpod or GitHub Codespaces** are recommended as they allow you to start coding in minutes. + diff --git a/docs/development/local-setup/github-codespaces.mdx b/docs/development/local-setup/github-codespaces.mdx new file mode 100644 index 0000000000..5a25415bcf --- /dev/null +++ b/docs/development/local-setup/github-codespaces.mdx @@ -0,0 +1,55 @@ +--- +title: GitHub Codespaces +description: How to set up Formbricks in a GitHub Codespaces environment +icon: "github" +--- + +### GitHub Codespaces Setup + + + This guide outlines how to set up Formbricks in a **GitHub Codespaces** environment. + + +**Requirements:** + +- A GitHub Codespace that has support for Node.JS, pnpm, and Docker. + +**Steps:** + +1. **Open your repository in GitHub Codespaces. If needed, clone the repository:** + ```bash + git clone https://github.com/formbricks/formbricks && cd formbricks + ``` + +2. **Setup NodeJS with nvm (if not already configured):** + ```bash + nvm install && nvm use + ``` + +3. **Install the dependencies:** + ```bash + pnpm install + ``` + +4. **Create a `.env` file from the template:** + ```bash + cp .env.example .env + ``` + +5. **Generate & set the required secrets:** + ```bash + sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env + sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env + sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env + ``` + +6. **Launch the development setup:** + ```bash + pnpm go + ``` + +Use the Codespaces port forwarding to access Formbricks at [http://localhost:3000](http://localhost:3000). + + + Make sure your Codespaces port configuration is set to allow access to the app. + \ No newline at end of file diff --git a/docs/development/local-setup/gitpod.mdx b/docs/development/local-setup/gitpod.mdx new file mode 100644 index 0000000000..fec0f4fa63 --- /dev/null +++ b/docs/development/local-setup/gitpod.mdx @@ -0,0 +1,55 @@ +--- +title: Gitpod +description: How to set up Formbricks in a Gitpod workspace +icon: "code" +--- + +### Gitpod Setup + + + This guide explains how to set up Formbricks in a **Gitpod** workspace. + + +**Requirements:** + +- A Gitpod workspace with Node.JS, pnpm, and Docker support. + +**Steps:** + +1. **Open the repository in Gitpod. The workspace typically clones the repo automatically. If not:** + ```bash + git clone https://github.com/formbricks/formbricks && cd formbricks + ``` + +2. **Setup NodeJS with nvm:** + ```bash + nvm install && nvm use + ``` + +3. **Install dependencies:** + ```bash + pnpm install + ``` + +4. **Create a `.env` file:** + ```bash + cp .env.example .env + ``` + +5. **Generate & set secret values:** + ```bash + sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env + sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env + sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env + ``` + +6. **Run the development setup:** + ```bash + pnpm go + ``` + +Access the running app via the forwarded port (typically [http://localhost:3000](http://localhost:3000) inside Gitpod). + + + Check your Gitpod settings to ensure Docker is enabled if required. + \ No newline at end of file diff --git a/docs/development/local-setup/linux.mdx b/docs/development/local-setup/linux.mdx new file mode 100644 index 0000000000..5cf559c8ff --- /dev/null +++ b/docs/development/local-setup/linux.mdx @@ -0,0 +1,57 @@ +--- +title: Linux +description: How to set up Formbricks on a Linux machine +icon: "linux" +--- + +### Local Machine Setup - Linux + + + This guide is recommended for advanced users setting up Formbricks on a **Linux** machine. + + +Here are the requirements for setting up Formbricks on Linux: + +- Node.JS (v20 recommended) +- [pnpm](https://pnpm.io/) +- [Docker](https://www.docker.com/) (to run PostgreSQL/MailHog) + +**Steps:** + +1. **Clone the project & move into the directory:** + ```bash + git clone https://github.com/formbricks/formbricks && cd formbricks + ``` + +2. **Setup NodeJS with nvm:** + ```bash + nvm install && nvm use + ``` + +3. **Install NodeJS packages via pnpm:** + ```bash + pnpm install + ``` + +4. **Create a `.env` file based on `.env.example`:** + ```bash + cp .env.example .env + ``` + +5. **Generate & set the secret values:** + ```bash + sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env + sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env + sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env + ``` + +6. **Start the development setup:** + ```bash + pnpm go + ``` + +You can now access Formbricks at [http://localhost:3000](http://localhost:3000). + + + Create a new account on first login as no default account is available. + \ No newline at end of file diff --git a/docs/development/local-setup/mac.mdx b/docs/development/local-setup/mac.mdx new file mode 100644 index 0000000000..8c9e33e382 --- /dev/null +++ b/docs/development/local-setup/mac.mdx @@ -0,0 +1,57 @@ +--- +title: Mac +description: How to set up Formbricks on a Mac machine +icon: "apple" +--- + +### Local Machine Setup - Mac + + + This guide is recommended for advanced users setting up Formbricks on a **Mac** machine. + + +**Requirements:** + +- Node.JS (v20 recommended) +- [pnpm](https://pnpm.io/) +- [Docker](https://www.docker.com/) + +**Steps:** + +1. **Clone the project & change directory:** + ```bash + git clone https://github.com/formbricks/formbricks && cd formbricks + ``` + +2. **Setup NodeJS with nvm:** + ```bash + nvm install && nvm use + ``` + +3. **Install NodeJS packages with pnpm:** + ```bash + pnpm install + ``` + +4. **Create a `.env` file from the example:** + ```bash + cp .env.example .env + ``` + +5. **Generate & set secret values (using BSD sed syntax for macOS):** + ```bash + sed -i '' '/^ENCRYPTION_KEY=/s|.*|ENCRYPTION_KEY='$(openssl rand -hex 32)'|' .env + sed -i '' '/^NEXTAUTH_SECRET=/s|.*|NEXTAUTH_SECRET='$(openssl rand -hex 32)'|' .env + sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env + ``` + +6. **Start the development setup:** + ```bash + pnpm go + ``` + +Visit [http://localhost:3000](http://localhost:3000) to access Formbricks. + + + Ensure you create a new account at first login. + \ No newline at end of file diff --git a/docs/development/local-setup/windows.mdx b/docs/development/local-setup/windows.mdx new file mode 100644 index 0000000000..44b085292c --- /dev/null +++ b/docs/development/local-setup/windows.mdx @@ -0,0 +1,57 @@ +--- +title: Windows +description: How to set up Formbricks on a Windows machine +icon: "windows" +--- + +### Local Machine Setup - Windows + + + This guide is intended for **Windows** users. For the best experience, use **WSL2** since pure Windows is not fully supported. + + +**Requirements:** + +- Node.JS (v20 recommended) via WSL2 +- [pnpm](https://pnpm.io/) +- [Docker](https://www.docker.com/) (ensure Docker Desktop is installed with WSL2 integration enabled) + +**Steps (Using WSL2):** + +1. **Open your WSL2 terminal and clone the project:** + ```bash + git clone https://github.com/formbricks/formbricks && cd formbricks + ``` + +2. **Setup NodeJS with nvm in WSL2:** + ```bash + nvm install && nvm use + ``` + +3. **Install packages using pnpm:** + ```bash + pnpm install + ``` + +4. **Create a `.env` file:** + ```bash + cp .env.example .env + ``` + +5. **Generate & set secret values (Linux commands work in WSL2):** + ```bash + sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env + sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env + sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env + ``` + +6. **Start the development setup:** + ```bash + pnpm go + ``` + +Access Formbricks at [http://localhost:3000](http://localhost:3000). + + + If you run into conflicts, ensure any local services (like PostgreSQL) are stopped. + \ No newline at end of file diff --git a/docs/development/overview.mdx b/docs/development/overview.mdx new file mode 100644 index 0000000000..704b99c44f --- /dev/null +++ b/docs/development/overview.mdx @@ -0,0 +1,16 @@ +--- +title: Development +description: Learn how to setup formbricks locally and develop on it +icon: "code" +--- + +Welcome to the Development section of Formbricks! This guide is designed to help you get started with setting up the project locally, contributing to the Formbricks codebase, and customizing it to suit your needs. + +Whether you're a seasoned developer or just getting started, you'll find valuable information on how to: + +- **Set Up Locally**: Step-by-step instructions to clone the repository, install dependencies, and run Formbricks on your local machine. +- **Contribute**: Guidelines on how to contribute to the project, including coding standards, submitting pull requests, and collaborating with other developers. +- **Customize**: Tips and tricks for customizing Formbricks to better fit your specific use cases, including modifying components and extending functionality. + +Dive in and start building with Formbricks today! + diff --git a/docs/development/support/troubleshooting.mdx b/docs/development/support/troubleshooting.mdx new file mode 100644 index 0000000000..09064108c6 --- /dev/null +++ b/docs/development/support/troubleshooting.mdx @@ -0,0 +1,38 @@ +--- +title: "Troubleshooting" +description: "Here, you'll find help with common issues." +icon: "wrench" +--- + +## "The app doesn't work after Prisma migration" + +If the app doesn’t work after a Prisma migration, clear your browser’s storage and reload the page. This will force the app to fetch data from the server again. ![prisma](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738108186/image_dwm9hp.jpg) + +## "I ran 'pnpm i' but there seems to be an error with the packages" + +If you run `pnpm i` and get an error with the packages, try running `pnpm clean` followed by `pnpm i` again. This often solves the problem. + +## "I get a full-screen error with cryptic strings" + +This usually happens when the Formbricks Widget isn't correctly or completely built. + +```bash +pnpm build --filter=@formbricks/js + +// Run the app again +pnpm dev +``` + +## "My machine struggles with the repository" + +Since we're working with a monorepo structure, the repository can get quite big. If you're having trouble working with the repository, try the following: + +```bash helloWorld.js +pnpm dev --filter=@formbricks/web... +``` + +It’s better to use a single terminal with `pnpm dev` rather than having multiple open (one with the Formbricks app and one with the demo). + +## Error: "Uncaught (in promise) SyntaxError: Unexpected token !DOCTYPE ... is not valid JSON"![Syntax Error](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738109837/image_wbxv8k.jpg) + +If you see this error, it happens when the person connected to the widget is deleted. To fix it, log out of the test person and reload the page.![Reset person](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738110212/image_nvkpku.jpg) diff --git a/docs/images/favicon.svg b/docs/images/favicon.svg new file mode 100644 index 0000000000..84c094c3bc --- /dev/null +++ b/docs/images/favicon.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/images/logo/logo-dark.svg b/docs/images/logo-dark.svg similarity index 100% rename from apps/docs/images/logo/logo-dark.svg rename to docs/images/logo-dark.svg diff --git a/apps/docs/images/logo/logo-light.svg b/docs/images/logo-light.svg similarity index 100% rename from apps/docs/images/logo/logo-light.svg rename to docs/images/logo-light.svg diff --git a/apps/docs/app/[survey-type]/global/access-roles/images/add-member.webp b/docs/images/xm-and-surveys/core-features/access-roles/add-member.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/access-roles/images/add-member.webp rename to docs/images/xm-and-surveys/core-features/access-roles/add-member.webp diff --git a/apps/docs/app/[survey-type]/global/access-roles/images/bulk-invite.webp b/docs/images/xm-and-surveys/core-features/access-roles/bulk-invite.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/access-roles/images/bulk-invite.webp rename to docs/images/xm-and-surveys/core-features/access-roles/bulk-invite.webp diff --git a/apps/docs/app/[survey-type]/global/access-roles/images/individual-invite.webp b/docs/images/xm-and-surveys/core-features/access-roles/individual-invite.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/access-roles/images/individual-invite.webp rename to docs/images/xm-and-surveys/core-features/access-roles/individual-invite.webp diff --git a/apps/docs/app/[survey-type]/global/access-roles/images/organization-settings-menu.webp b/docs/images/xm-and-surveys/core-features/access-roles/organization-settings-menu.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/access-roles/images/organization-settings-menu.webp rename to docs/images/xm-and-surveys/core-features/access-roles/organization-settings-menu.webp diff --git a/apps/docs/app/[survey-type]/global/access-roles/images/team-settings-menu.webp b/docs/images/xm-and-surveys/core-features/access-roles/team-settings-menu.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/access-roles/images/team-settings-menu.webp rename to docs/images/xm-and-surveys/core-features/access-roles/team-settings-menu.webp diff --git a/apps/docs/app/[survey-type]/global/email-customization/email-customization-card.webp b/docs/images/xm-and-surveys/core-features/email-customization/email-customization-card.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/email-customization/email-customization-card.webp rename to docs/images/xm-and-surveys/core-features/email-customization/email-customization-card.webp diff --git a/apps/docs/app/[survey-type]/global/email-customization/email-sample.webp b/docs/images/xm-and-surveys/core-features/email-customization/email-sample.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/email-customization/email-sample.webp rename to docs/images/xm-and-surveys/core-features/email-customization/email-sample.webp diff --git a/apps/docs/app/[survey-type]/global/email-customization/updated-logo.webp b/docs/images/xm-and-surveys/core-features/email-customization/updated-logo.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/email-customization/updated-logo.webp rename to docs/images/xm-and-surveys/core-features/email-customization/updated-logo.webp diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/configure-connection.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/configure-connection.webp new file mode 100644 index 0000000000..cd0daa6e95 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/configure-connection.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/create-connection.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/create-connection.webp new file mode 100644 index 0000000000..eec50e6364 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/create-connection.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/create-new-flow.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/create-new-flow.webp new file mode 100644 index 0000000000..ec1efff3da Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/create-new-flow.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/duplicate-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/duplicate-survey.webp new file mode 100644 index 0000000000..842612ca8b Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/duplicate-survey.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/match-data.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/match-data.webp new file mode 100644 index 0000000000..db12224e5c Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/match-data.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/result.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/result.webp new file mode 100644 index 0000000000..5fa6cac881 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/result.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/search-formbricks.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/search-formbricks.webp new file mode 100644 index 0000000000..5a804f56c2 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/search-formbricks.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-google-sheet.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-google-sheet.webp new file mode 100644 index 0000000000..824e560839 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-google-sheet.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-gs-sheet.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-gs-sheet.webp new file mode 100644 index 0000000000..2b34521238 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-gs-sheet.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-survey.webp new file mode 100644 index 0000000000..9547653fdf Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-survey.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/success-response.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/success-response.webp new file mode 100644 index 0000000000..dc790dc4fc Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/success-response.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/test-trigger.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/test-trigger.webp new file mode 100644 index 0000000000..e6756823da Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/test-trigger.webp differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/update-question-id.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/update-question-id.webp new file mode 100644 index 0000000000..b1db9d6140 Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/integrations/activepieces/update-question-id.webp differ diff --git a/apps/docs/app/developer-docs/integrations/airtable/add-base.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/add-base.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/add-base.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/add-base.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/airtable-connected.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/airtable-connected.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/airtable-connected.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/airtable-connected.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/connect-with-airtable.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/connect-with-airtable.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/connect-with-airtable.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/connect-with-airtable.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/create-new-integration.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/create-new-integration.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/create-new-integration.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/create-new-integration.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/delete-integration.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/delete-integration.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/delete-integration.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/delete-integration.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/integrations-tab.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/integrations-tab.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/integrations-tab.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/integrations-tab.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/link-survey-with-table.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/link-survey-with-table.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/link-survey-with-table.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/link-survey-with-table.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/link-with-questions.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/link-with-questions.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/link-with-questions.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/link-with-questions.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/list-linked-surveys.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/list-linked-surveys.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/list-linked-surveys.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/list-linked-surveys.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/open-developer-hub.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/open-developer-hub.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/open-developer-hub.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/open-developer-hub.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/register-new-integration.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/register-new-integration.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/register-new-integration.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/register-new-integration.webp diff --git a/apps/docs/app/developer-docs/integrations/airtable/select-scopes.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/select-scopes.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/airtable/select-scopes.webp rename to docs/images/xm-and-surveys/core-features/integrations/airtable/select-scopes.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/connect-with-google.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/connect-with-google.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/connect-with-google.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/connect-with-google.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/delete-connection.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/delete-connection.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/delete-connection.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/delete-connection.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/google-connected.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/google-connected.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/google-connected.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/google-connected.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/integrations-tab.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/integrations-tab.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/integrations-tab.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/integrations-tab.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/link-survey-with-sheet.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/link-survey-with-sheet.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/link-survey-with-sheet.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/link-survey-with-sheet.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/link-with-questions.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/link-with-questions.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/link-with-questions.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/link-with-questions.webp diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/list-linked-surveys.webp b/docs/images/xm-and-surveys/core-features/integrations/google-sheets/list-linked-surveys.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/google-sheets/list-linked-surveys.webp rename to docs/images/xm-and-surveys/core-features/integrations/google-sheets/list-linked-surveys.webp diff --git a/apps/docs/app/developer-docs/integrations/make/add-module.webp b/docs/images/xm-and-surveys/core-features/integrations/make/add-module.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/add-module.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/add-module.webp diff --git a/apps/docs/app/developer-docs/integrations/make/create-new-scenario.webp b/docs/images/xm-and-surveys/core-features/integrations/make/create-new-scenario.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/create-new-scenario.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/create-new-scenario.webp diff --git a/apps/docs/app/developer-docs/integrations/make/create-webhook.webp b/docs/images/xm-and-surveys/core-features/integrations/make/create-webhook.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/create-webhook.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/create-webhook.webp diff --git a/apps/docs/app/developer-docs/integrations/make/duplicate-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/make/duplicate-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/duplicate-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/duplicate-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/make/enter-api-key-and-host.webp b/docs/images/xm-and-surveys/core-features/integrations/make/enter-api-key-and-host.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/enter-api-key-and-host.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/enter-api-key-and-host.webp diff --git a/apps/docs/app/developer-docs/integrations/make/result.webp b/docs/images/xm-and-surveys/core-features/integrations/make/result.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/result.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/result.webp diff --git a/apps/docs/app/developer-docs/integrations/make/search-formbricks.webp b/docs/images/xm-and-surveys/core-features/integrations/make/search-formbricks.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/search-formbricks.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/search-formbricks.webp diff --git a/apps/docs/app/developer-docs/integrations/make/select-action.webp b/docs/images/xm-and-surveys/core-features/integrations/make/select-action.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/select-action.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/select-action.webp diff --git a/apps/docs/app/developer-docs/integrations/make/select-fields.webp b/docs/images/xm-and-surveys/core-features/integrations/make/select-fields.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/select-fields.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/select-fields.webp diff --git a/apps/docs/app/developer-docs/integrations/make/select-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/make/select-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/select-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/select-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/make/select-trigger.webp b/docs/images/xm-and-surveys/core-features/integrations/make/select-trigger.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/select-trigger.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/select-trigger.webp diff --git a/apps/docs/app/developer-docs/integrations/make/submit-test-response.webp b/docs/images/xm-and-surveys/core-features/integrations/make/submit-test-response.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/submit-test-response.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/submit-test-response.webp diff --git a/apps/docs/app/developer-docs/integrations/make/update-question-id.webp b/docs/images/xm-and-surveys/core-features/integrations/make/update-question-id.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/make/update-question-id.webp rename to docs/images/xm-and-surveys/core-features/integrations/make/update-question-id.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/add-api-key.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/add-api-key.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/add-api-key.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/add-api-key.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/add-discord.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/add-discord.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/add-discord.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/add-discord.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/add-formbricks-trigger.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/add-formbricks-trigger.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/add-formbricks-trigger.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/add-formbricks-trigger.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/create-new-credential-btn.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/create-new-credential-btn.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/create-new-credential-btn.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/create-new-credential-btn.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/discord-response.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/discord-response.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/discord-response.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/discord-response.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/duplicate-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/duplicate-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/duplicate-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/duplicate-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/fill-discord-details.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/fill-discord-details.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/fill-discord-details.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/fill-discord-details.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/listen-for-event.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/listen-for-event.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/listen-for-event.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/listen-for-event.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/select-event.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/select-event.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/select-event.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/select-event.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/select-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/select-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/select-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/select-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/selected-surveys.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/selected-surveys.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/selected-surveys.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/selected-surveys.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/submit-test-response.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/submit-test-response.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/submit-test-response.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/submit-test-response.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/success-connection.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/success-connection.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/success-connection.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/success-connection.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/test-response-success.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/test-response-success.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/test-response-success.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/test-response-success.webp diff --git a/apps/docs/app/developer-docs/integrations/n8n/update-question-id.webp b/docs/images/xm-and-surveys/core-features/integrations/n8n/update-question-id.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/n8n/update-question-id.webp rename to docs/images/xm-and-surveys/core-features/integrations/n8n/update-question-id.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/connect-with-notion.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/connect-with-notion.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/connect-with-notion.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/connect-with-notion.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/delete-connection.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/delete-connection.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/delete-connection.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/delete-connection.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/integrations-tab.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/integrations-tab.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/integrations-tab.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/integrations-tab.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/link-survey-with-database.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/link-survey-with-database.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/link-survey-with-database.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/link-survey-with-database.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/link-with-databases.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/link-with-databases.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/link-with-databases.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/link-with-databases.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/list-linked-databases.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/list-linked-databases.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/list-linked-databases.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/list-linked-databases.webp diff --git a/apps/docs/app/developer-docs/integrations/notion/images/notion-connected.webp b/docs/images/xm-and-surveys/core-features/integrations/notion/notion-connected.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/notion/images/notion-connected.webp rename to docs/images/xm-and-surveys/core-features/integrations/notion/notion-connected.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-1.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-1.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-1.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-1.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-2.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-2.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-2.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-2.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-3.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-3.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-3.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-3.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-4.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-4.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/add-slack-bot-4.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/add-slack-bot-4.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/connect-with-slack.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/connect-with-slack.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/connect-with-slack.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/connect-with-slack.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/delete-connection.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/delete-connection.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/delete-connection.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/delete-connection.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/integrations-tab.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/integrations-tab.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/integrations-tab.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/integrations-tab.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/link-survey-with-channel.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/link-survey-with-channel.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/link-survey-with-channel.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/link-survey-with-channel.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/link-with-questions.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/link-with-questions.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/link-with-questions.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/link-with-questions.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/list-linked-surveys.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/list-linked-surveys.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/list-linked-surveys.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/list-linked-surveys.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/slack-auth.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/slack-auth.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/slack-auth.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/slack-auth.webp diff --git a/apps/docs/app/developer-docs/integrations/slack/images/slack-connected.webp b/docs/images/xm-and-surveys/core-features/integrations/slack/slack-connected.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/slack/images/slack-connected.webp rename to docs/images/xm-and-surveys/core-features/integrations/slack/slack-connected.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/1-wordpress-targeted-survey-on-website-free.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/1-wordpress-targeted-survey-on-website-free.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/1-wordpress-targeted-survey-on-website-free.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/1-wordpress-targeted-survey-on-website-free.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/2-run-website-survey-wordpress-targeted-for-free.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/2-run-website-survey-wordpress-targeted-for-free.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/2-run-website-survey-wordpress-targeted-for-free.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/2-run-website-survey-wordpress-targeted-for-free.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/3-wordpress-setup-survey-on-website-targeted-free-open-source.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/3-wordpress-setup-survey-on-website-targeted-free-open-source.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/3-wordpress-setup-survey-on-website-targeted-free-open-source.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/3-wordpress-setup-survey-on-website-targeted-free-open-source.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/4-wordpress-website-survey-target-visitor-free.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/4-wordpress-website-survey-target-visitor-free.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/4-wordpress-website-survey-target-visitor-free.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/4-wordpress-website-survey-target-visitor-free.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/6-targeted-survey-on-wordpress-website-for-free.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/6-targeted-survey-on-wordpress-website-for-free.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/6-targeted-survey-on-wordpress-website-for-free.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/6-targeted-survey-on-wordpress-website-for-free.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/7-wordpress-free-hotjar-survey-open-source-website-survey-hotjar.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/7-wordpress-free-hotjar-survey-open-source-website-survey-hotjar.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/7-wordpress-free-hotjar-survey-open-source-website-survey-hotjar.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/7-wordpress-free-hotjar-survey-open-source-website-survey-hotjar.webp diff --git a/apps/docs/app/developer-docs/integrations/wordpress/step-4-copy-to-wordpress-for-free-targeted-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/wordpress/step-4-copy-to-wordpress-for-free-targeted-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/wordpress/step-4-copy-to-wordpress-for-free-targeted-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/wordpress/step-4-copy-to-wordpress-for-free-targeted-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/add-new-zap.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/add-new-zap.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/add-new-zap.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/add-new-zap.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/choose-event.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/choose-event.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/choose-event.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/choose-event.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/connect-with-formbricks-1.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/connect-with-formbricks-1.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/connect-with-formbricks-1.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/connect-with-formbricks-1.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/connect-with-formbricks-2.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/connect-with-formbricks-2.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/connect-with-formbricks-2.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/connect-with-formbricks-2.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/duplicate-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/duplicate-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/duplicate-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/duplicate-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/select-survey.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/select-survey.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/select-survey.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/select-survey.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/slack-channel-msg.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/slack-channel-msg.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/slack-channel-msg.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/slack-channel-msg.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/slack-message.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/slack-message.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/slack-message.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/slack-message.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/submit-test-response.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/submit-test-response.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/submit-test-response.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/submit-test-response.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/success-connected.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/success-connected.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/success-connected.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/success-connected.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/test-submission.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/test-submission.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/test-submission.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/test-submission.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/update-question-id.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/update-question-id.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/update-question-id.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/update-question-id.webp diff --git a/apps/docs/app/developer-docs/integrations/zapier/zapier-message.webp b/docs/images/xm-and-surveys/core-features/integrations/zapier/zapier-message.webp similarity index 100% rename from apps/docs/app/developer-docs/integrations/zapier/zapier-message.webp rename to docs/images/xm-and-surveys/core-features/integrations/zapier/zapier-message.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/address/images/address.webp b/docs/images/xm-and-surveys/core-features/question-type/address.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/address/images/address.webp rename to docs/images/xm-and-surveys/core-features/question-type/address.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/consent/images/consent.webp b/docs/images/xm-and-surveys/core-features/question-type/consent.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/consent/images/consent.webp rename to docs/images/xm-and-surveys/core-features/question-type/consent.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/contact/images/contact.webp b/docs/images/xm-and-surveys/core-features/question-type/contact-info.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/contact/images/contact.webp rename to docs/images/xm-and-surveys/core-features/question-type/contact-info.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/date/images/date.webp b/docs/images/xm-and-surveys/core-features/question-type/date.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/date/images/date.webp rename to docs/images/xm-and-surveys/core-features/question-type/date.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/file-upload/images/file-upload.webp b/docs/images/xm-and-surveys/core-features/question-type/file-upload.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/file-upload/images/file-upload.webp rename to docs/images/xm-and-surveys/core-features/question-type/file-upload.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/free-text/images/free-text.webp b/docs/images/xm-and-surveys/core-features/question-type/free-text.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/free-text/images/free-text.webp rename to docs/images/xm-and-surveys/core-features/question-type/free-text.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/matrix/images/matrix.webp b/docs/images/xm-and-surveys/core-features/question-type/matrix.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/matrix/images/matrix.webp rename to docs/images/xm-and-surveys/core-features/question-type/matrix.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/multi-select/images/multi-select.webp b/docs/images/xm-and-surveys/core-features/question-type/multi-select.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/multi-select/images/multi-select.webp rename to docs/images/xm-and-surveys/core-features/question-type/multi-select.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/net-promoter-score/images/net-promoter-score.webp b/docs/images/xm-and-surveys/core-features/question-type/net-promoter-score.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/net-promoter-score/images/net-promoter-score.webp rename to docs/images/xm-and-surveys/core-features/question-type/net-promoter-score.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/ranking/images/ranking.webp b/docs/images/xm-and-surveys/core-features/question-type/ranking.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/ranking/images/ranking.webp rename to docs/images/xm-and-surveys/core-features/question-type/ranking.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/rating/images/rating.webp b/docs/images/xm-and-surveys/core-features/question-type/rating.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/rating/images/rating.webp rename to docs/images/xm-and-surveys/core-features/question-type/rating.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/schedule/images/schedule-call.webp b/docs/images/xm-and-surveys/core-features/question-type/schedule-a-meeting.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/schedule/images/schedule-call.webp rename to docs/images/xm-and-surveys/core-features/question-type/schedule-a-meeting.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/picture-selection/images/picture-selection.webp b/docs/images/xm-and-surveys/core-features/question-type/select-picture.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/picture-selection/images/picture-selection.webp rename to docs/images/xm-and-surveys/core-features/question-type/select-picture.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/single-select/images/single-select.webp b/docs/images/xm-and-surveys/core-features/question-type/select-single.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/single-select/images/single-select.webp rename to docs/images/xm-and-surveys/core-features/question-type/select-single.webp diff --git a/apps/docs/app/[survey-type]/global/question-type/statement-cta/images/statement-cta.webp b/docs/images/xm-and-surveys/core-features/question-type/statement-cta.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/question-type/statement-cta/images/statement-cta.webp rename to docs/images/xm-and-surveys/core-features/question-type/statement-cta.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/allow-overwrite.webp b/docs/images/xm-and-surveys/core-features/styling-theme/allow-overwrite.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/allow-overwrite.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/allow-overwrite.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/background-settings.webp b/docs/images/xm-and-surveys/core-features/styling-theme/background-settings.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/background-settings.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/background-settings.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/card-settings.webp b/docs/images/xm-and-surveys/core-features/styling-theme/card-settings.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/card-settings.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/card-settings.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/doggo.webp b/docs/images/xm-and-surveys/core-features/styling-theme/doggo.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/doggo.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/doggo.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/form-settings.webp b/docs/images/xm-and-surveys/core-features/styling-theme/form-settings.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/form-settings.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/form-settings.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/hipster-living.webp b/docs/images/xm-and-surveys/core-features/styling-theme/hipster-living.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/hipster-living.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/hipster-living.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/look-and-feel.webp b/docs/images/xm-and-surveys/core-features/styling-theme/look-and-feel.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/look-and-feel.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/look-and-feel.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/mario.webp b/docs/images/xm-and-surveys/core-features/styling-theme/mario.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/mario.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/mario.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/pre-requisite.webp b/docs/images/xm-and-surveys/core-features/styling-theme/pre-requisite.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/pre-requisite.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/pre-requisite.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-eight.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-eight.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-eight.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-eight.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-eleven.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-eleven.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-eleven.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-eleven.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-five.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-five.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-five.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-five.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-four.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-four.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-four.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-four.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-nine.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-nine.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-nine.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-nine.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-one.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-one.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-one.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-one.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-seven.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-seven.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-seven.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-seven.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-six.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-six.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-six.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-six.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-ten.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-ten.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-ten.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-ten.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-three.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-three.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-three.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-three.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/step-two.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-two.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/step-two.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/step-two.webp diff --git a/apps/docs/app/[survey-type]/global/overwrite-styling/images/windows-xp.webp b/docs/images/xm-and-surveys/core-features/styling-theme/windows-xp.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/overwrite-styling/images/windows-xp.webp rename to docs/images/xm-and-surveys/core-features/styling-theme/windows-xp.webp diff --git a/apps/docs/app/[survey-type]/global/add-image-or-video-question/images/add-image-or-video-to-question-image.webp b/docs/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/add-image-or-video-question/images/add-image-or-video-to-question-image.webp rename to docs/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp diff --git a/apps/docs/app/[survey-type]/global/add-image-or-video-question/images/add-image-or-video-to-question-video.webp b/docs/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-video.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/add-image-or-video-question/images/add-image-or-video-to-question-video.webp rename to docs/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-video.webp diff --git a/apps/docs/app/[survey-type]/global/add-image-or-video-question/images/add-image-or-video-to-question.webp b/docs/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/add-image-or-video-question/images/add-image-or-video-to-question.webp rename to docs/images/xm-and-surveys/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate-operators.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate-operators.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate-operators.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate-operators.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate-value.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate-value.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate-value.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate-value.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate-variables.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate-variables.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate-variables.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate-variables.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-calculate.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-calculate.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-jump.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-jump.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-jump.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-jump.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-options.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-options.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-options.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-options.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/action-require.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-require.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/action-require.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/action-require.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/add-logic.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/add-logic.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/add-logic.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/add-logic.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/condition-chaining.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-chaining.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/condition-chaining.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-chaining.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/condition-operators.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-operators.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/condition-operators.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-operators.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/condition-options.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-options.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/condition-options.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-options.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/condition-value.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-value.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/condition-value.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/condition-value.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/conditions.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/conditions.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/conditions.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/conditions.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/editor.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/editor.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/editor.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/editor.webp diff --git a/apps/docs/app/[survey-type]/global/conditional-logic/images/question-logic.webp b/docs/images/xm-and-surveys/surveys/general-features/conditional-logic/question-logic.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/conditional-logic/images/question-logic.webp rename to docs/images/xm-and-surveys/surveys/general-features/conditional-logic/question-logic.webp diff --git a/apps/docs/app/[survey-type]/global/hidden-fields/filled-hidden-fields.webp b/docs/images/xm-and-surveys/surveys/general-features/hidden-fields/filled-hidden-fields.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/hidden-fields/filled-hidden-fields.webp rename to docs/images/xm-and-surveys/surveys/general-features/hidden-fields/filled-hidden-fields.webp diff --git a/apps/docs/app/[survey-type]/global/hidden-fields/hidden-field-responses.webp b/docs/images/xm-and-surveys/surveys/general-features/hidden-fields/hidden-field-responses.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/hidden-fields/hidden-field-responses.webp rename to docs/images/xm-and-surveys/surveys/general-features/hidden-fields/hidden-field-responses.webp diff --git a/apps/docs/app/[survey-type]/global/hidden-fields/hidden-fields.webp b/docs/images/xm-and-surveys/surveys/general-features/hidden-fields/hidden-fields.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/hidden-fields/hidden-fields.webp rename to docs/images/xm-and-surveys/surveys/general-features/hidden-fields/hidden-fields.webp diff --git a/apps/docs/app/[survey-type]/global/hidden-fields/input-hidden-fields.webp b/docs/images/xm-and-surveys/surveys/general-features/hidden-fields/input-hidden-fields.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/hidden-fields/input-hidden-fields.webp rename to docs/images/xm-and-surveys/surveys/general-features/hidden-fields/input-hidden-fields.webp diff --git a/apps/docs/app/[survey-type]/global/limit-submissions/images/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/limit-submissions/limit-submissions.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/limit-submissions/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/general-features/limit-submissions/limit-submissions.webp diff --git a/apps/docs/app/[survey-type]/global/metadata/filters.webp b/docs/images/xm-and-surveys/surveys/general-features/metadata/filters.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/metadata/filters.webp rename to docs/images/xm-and-surveys/surveys/general-features/metadata/filters.webp diff --git a/apps/docs/app/[survey-type]/global/metadata/metadata-card.webp b/docs/images/xm-and-surveys/surveys/general-features/metadata/metadata-card.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/metadata/metadata-card.webp rename to docs/images/xm-and-surveys/surveys/general-features/metadata/metadata-card.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/add-language-in-survey.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-language-in-survey.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/add-language-in-survey.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-language-in-survey.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/add-languages.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-languages.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/add-languages.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-languages.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/edit-multi-lang.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/edit-multi-lang.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/edit-multi-lang.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/edit-multi-lang.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/enable-multi-lang.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/enable-multi-lang.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/enable-multi-lang.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/enable-multi-lang.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/home-page.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/home-page.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/home-page.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/home-page.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/see-survey-in-language.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/see-survey-in-language.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/see-survey-in-language.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/see-survey-in-language.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/survey-languages-from-home.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languages-from-home.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/survey-languages-from-home.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languages-from-home.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/survey-languague-settings.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languague-settings.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/survey-languague-settings.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languague-settings.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/survey-sharing.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-sharing.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/survey-sharing.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-sharing.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/surveys-home.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/surveys-home.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/surveys-home.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/surveys-home.webp diff --git a/apps/docs/app/[survey-type]/global/multi-language-surveys/translate-as-per-language.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/translate-as-per-language.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/multi-language-surveys/translate-as-per-language.webp rename to docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/translate-as-per-language.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/doggo.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/doggo.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/doggo.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/doggo.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/hipster-living.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/hipster-living.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/hipster-living.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/hipster-living.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/mario.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/mario.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/mario.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/mario.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/pre-requisite.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/pre-requisite.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/pre-requisite.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/pre-requisite.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-eight.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eight.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-eight.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eight.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-eleven.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eleven.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-eleven.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eleven.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-five.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-five.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-five.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-five.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-four.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-four.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-four.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-four.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-nine.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-nine.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-nine.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-nine.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-one.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-one.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-seven.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-seven.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-seven.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-seven.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-six.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-six.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-six.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-six.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-ten.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-ten.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-ten.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-ten.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-three.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-three.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-three.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-three.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-two.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-two.webp diff --git a/apps/docs/app/[survey-type]/global/styling-theme/images/windows-xp.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/windows-xp.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/styling-theme/images/windows-xp.webp rename to docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/windows-xp.webp diff --git a/apps/docs/app/[survey-type]/global/partial-submissions/images/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/partial-submissions/step-one.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/partial-submissions/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/general-features/partial-submissions/step-one.webp diff --git a/apps/docs/app/[survey-type]/global/recall/images/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/recall/step-one.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/recall/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/general-features/recall/step-one.webp diff --git a/apps/docs/app/[survey-type]/global/recall/images/step-three.webp b/docs/images/xm-and-surveys/surveys/general-features/recall/step-three.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/recall/images/step-three.webp rename to docs/images/xm-and-surveys/surveys/general-features/recall/step-three.webp diff --git a/apps/docs/app/[survey-type]/global/recall/images/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/recall/step-two.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/recall/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/general-features/recall/step-two.webp diff --git a/apps/docs/app/[survey-type]/global/limit-submissions/images/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-one.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/limit-submissions/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-one.webp diff --git a/apps/docs/app/[survey-type]/global/limit-submissions/images/step-three.webp b/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-three.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/limit-submissions/images/step-three.webp rename to docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-three.webp diff --git a/apps/docs/app/[survey-type]/global/schedule-start-end-dates/images/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-two.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/schedule-start-end-dates/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-two.webp diff --git a/apps/docs/app/[survey-type]/global/shareable-dashboards/images/1-publish-to-web.webp b/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/1-publish-to-web.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/shareable-dashboards/images/1-publish-to-web.webp rename to docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/1-publish-to-web.webp diff --git a/apps/docs/app/[survey-type]/global/shareable-dashboards/images/2-warning-publish.webp b/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/2-warning-publish.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/shareable-dashboards/images/2-warning-publish.webp rename to docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/2-warning-publish.webp diff --git a/apps/docs/app/[survey-type]/global/shareable-dashboards/images/3-share-link.webp b/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/3-share-link.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/shareable-dashboards/images/3-share-link.webp rename to docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/3-share-link.webp diff --git a/apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/images/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp diff --git a/apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/images/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp diff --git a/apps/docs/app/[survey-type]/global/variables/images/created-variables.webp b/docs/images/xm-and-surveys/surveys/general-features/variables/created-variables.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/variables/images/created-variables.webp rename to docs/images/xm-and-surveys/surveys/general-features/variables/created-variables.webp diff --git a/apps/docs/app/[survey-type]/global/variables/images/input-variables.webp b/docs/images/xm-and-surveys/surveys/general-features/variables/input-variables.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/variables/images/input-variables.webp rename to docs/images/xm-and-surveys/surveys/general-features/variables/input-variables.webp diff --git a/apps/docs/app/[survey-type]/global/variables/images/logic-with-variables.webp b/docs/images/xm-and-surveys/surveys/general-features/variables/logic-with-variables.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/variables/images/logic-with-variables.webp rename to docs/images/xm-and-surveys/surveys/general-features/variables/logic-with-variables.webp diff --git a/apps/docs/app/[survey-type]/global/variables/images/variables-card.webp b/docs/images/xm-and-surveys/surveys/general-features/variables/variables-card.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/variables/images/variables-card.webp rename to docs/images/xm-and-surveys/surveys/general-features/variables/variables-card.webp diff --git a/apps/docs/app/[survey-type]/global/variables/images/variables-usage.webp b/docs/images/xm-and-surveys/surveys/general-features/variables/variables-usage.webp similarity index 100% rename from apps/docs/app/[survey-type]/global/variables/images/variables-usage.webp rename to docs/images/xm-and-surveys/surveys/general-features/variables/variables-usage.webp diff --git a/apps/docs/app/link-surveys/data-prefilling/question-id.webp b/docs/images/xm-and-surveys/surveys/link-surveys/data-prefilling/question-id.webp similarity index 100% rename from apps/docs/app/link-surveys/data-prefilling/question-id.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/data-prefilling/question-id.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/email-content-with-survey.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-with-survey.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/email-content-with-survey.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-with-survey.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/email-content-without-survey.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-without-survey.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/email-content-without-survey.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-without-survey.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/embed-mode-disabled.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-disabled.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/embed-mode-disabled.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-disabled.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/embed-mode-enabled.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-enabled.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/embed-mode-enabled.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-enabled.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/embed-mode-toggle.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/embed-mode-toggle.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/jo-signature.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/jo-signature.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/jo-signature.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/jo-signature.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/plugin-add-survey.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-add-survey.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/plugin-add-survey.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-add-survey.webp diff --git a/apps/docs/app/link-surveys/embed-surveys/images/plugin-source-tab.webp b/docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-source-tab.webp similarity index 100% rename from apps/docs/app/link-surveys/embed-surveys/images/plugin-source-tab.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-source-tab.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/copy-survey-link.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/copy-survey-link.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/copy-survey-link.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/copy-survey-link.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/create-study.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/create-study.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/create-study.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/create-study.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/hidden-fields.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/hidden-fields.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/hidden-fields.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/hidden-fields.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/preview-complete.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/preview-complete.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/preview-complete.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/preview-complete.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/preview-study.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/preview-study.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/preview-study.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/preview-study.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/redirect-url-formbricks.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/redirect-url-formbricks.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/redirect-url-formbricks.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/redirect-url-formbricks.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/redirect-url.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/redirect-url.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/redirect-url.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/redirect-url.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/screening-out.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/screening-out.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/screening-out.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/screening-out.webp diff --git a/apps/docs/app/link-surveys/market-research-panel/url-parameters.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/url-parameters.webp similarity index 100% rename from apps/docs/app/link-surveys/market-research-panel/url-parameters.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/url-parameters.webp diff --git a/apps/docs/app/link-surveys/pin-protected-surveys/images/step-five.webp b/docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-five.webp similarity index 100% rename from apps/docs/app/link-surveys/pin-protected-surveys/images/step-five.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-five.webp diff --git a/apps/docs/app/link-surveys/pin-protected-surveys/images/step-four.webp b/docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-four.webp similarity index 100% rename from apps/docs/app/link-surveys/pin-protected-surveys/images/step-four.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-four.webp diff --git a/apps/docs/app/link-surveys/pin-protected-surveys/images/step-one.webp b/docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-one.webp similarity index 100% rename from apps/docs/app/link-surveys/pin-protected-surveys/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-one.webp diff --git a/apps/docs/app/link-surveys/pin-protected-surveys/images/step-three.webp b/docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-three.webp similarity index 100% rename from apps/docs/app/link-surveys/pin-protected-surveys/images/step-three.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-three.webp diff --git a/apps/docs/app/link-surveys/pin-protected-surveys/images/step-two.webp b/docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-two.webp similarity index 100% rename from apps/docs/app/link-surveys/pin-protected-surveys/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-two.webp diff --git a/apps/docs/app/link-surveys/single-use-links/env-variable.webp b/docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/env-variable.webp similarity index 100% rename from apps/docs/app/link-surveys/single-use-links/env-variable.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/env-variable.webp diff --git a/apps/docs/app/link-surveys/single-use-links/metadata.webp b/docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/metadata.webp similarity index 100% rename from apps/docs/app/link-surveys/single-use-links/metadata.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/metadata.webp diff --git a/apps/docs/app/link-surveys/single-use-links/share-modal.webp b/docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/share-modal.webp similarity index 100% rename from apps/docs/app/link-surveys/single-use-links/share-modal.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/share-modal.webp diff --git a/apps/docs/app/link-surveys/single-use-links/single-use-setting.webp b/docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/single-use-setting.webp similarity index 100% rename from apps/docs/app/link-surveys/single-use-links/single-use-setting.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/single-use-setting.webp diff --git a/apps/docs/app/link-surveys/single-use-links/used-message.webp b/docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/used-message.webp similarity index 100% rename from apps/docs/app/link-surveys/single-use-links/used-message.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/single-use-links/used-message.webp diff --git a/apps/docs/app/link-surveys/source-tracking/share-link.webp b/docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/share-link.webp similarity index 100% rename from apps/docs/app/link-surveys/source-tracking/share-link.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/share-link.webp diff --git a/apps/docs/app/link-surveys/source-tracking/view-response.webp b/docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/view-response.webp similarity index 100% rename from apps/docs/app/link-surveys/source-tracking/view-response.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/view-response.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-five.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-five.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-five.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-five.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-four.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-four.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-four.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-four.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-one.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-one.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-one.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-one.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-seven.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-seven.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-seven.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-seven.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-six.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-six.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-six.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-six.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-three.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-three.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-three.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-three.webp diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/images/step-two.webp b/docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-two.webp similarity index 100% rename from apps/docs/app/link-surveys/verify-email-before-survey/images/step-two.webp rename to docs/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-two.webp diff --git a/apps/docs/app/app-surveys/actions/images/i1.webp b/docs/images/xm-and-surveys/surveys/website-app-surveys/actions/i1.webp similarity index 100% rename from apps/docs/app/app-surveys/actions/images/i1.webp rename to docs/images/xm-and-surveys/surveys/website-app-surveys/actions/i1.webp diff --git a/apps/docs/app/app-surveys/actions/images/i2.webp b/docs/images/xm-and-surveys/surveys/website-app-surveys/actions/i2.webp similarity index 100% rename from apps/docs/app/app-surveys/actions/images/i2.webp rename to docs/images/xm-and-surveys/surveys/website-app-surveys/actions/i2.webp diff --git a/apps/docs/app/app-surveys/recontact/app-survey.webp b/docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/app-survey.webp similarity index 100% rename from apps/docs/app/app-surveys/recontact/app-survey.webp rename to docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/app-survey.webp diff --git a/apps/docs/app/app-surveys/recontact/global-wait-time.webp b/docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/global-wait-time.webp similarity index 100% rename from apps/docs/app/app-surveys/recontact/global-wait-time.webp rename to docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/global-wait-time.webp diff --git a/apps/docs/app/app-surveys/recontact/ignore-wait-time.webp b/docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/ignore-wait-time.webp similarity index 100% rename from apps/docs/app/app-surveys/recontact/ignore-wait-time.webp rename to docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/ignore-wait-time.webp diff --git a/apps/docs/app/app-surveys/recontact/survey-recontact.webp b/docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/survey-recontact.webp similarity index 100% rename from apps/docs/app/app-surveys/recontact/survey-recontact.webp rename to docs/images/xm-and-surveys/surveys/website-app-surveys/recontact/survey-recontact.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/change-text.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/change-text.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/change-text.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/change-text.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/create-cancel-flow.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/create-cancel-flow.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/create-cancel-flow.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/create-cancel-flow.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/publish-survey.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/publish-survey.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/publish-survey.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/publish-survey.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/recontact-options.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/recontact-options.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/recontact-options.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/recontact-options.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/select-action.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/trigger-css-selector.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-css-selector.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/trigger-css-selector.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-css-selector.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/trigger-inner-text.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-inner-text.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/trigger-inner-text.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-inner-text.webp diff --git a/apps/docs/app/best-practices/cancel-subscription/trigger-page-url.webp b/docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-page-url.webp similarity index 100% rename from apps/docs/app/best-practices/cancel-subscription/trigger-page-url.webp rename to docs/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-page-url.webp diff --git a/apps/docs/app/best-practices/contact-form/images/add-question.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/add-question.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/add-question.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/add-question.webp diff --git a/apps/docs/app/best-practices/contact-form/images/email-field.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/email-field.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/email-field.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/email-field.webp diff --git a/apps/docs/app/best-practices/contact-form/images/embed.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/embed.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/embed.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/embed.webp diff --git a/apps/docs/app/best-practices/contact-form/images/message-field.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/message-field.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/message-field.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/message-field.webp diff --git a/apps/docs/app/best-practices/contact-form/images/name-field.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/name-field.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/name-field.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/name-field.webp diff --git a/apps/docs/app/best-practices/contact-form/images/query-form.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/query-form.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/query-form.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/query-form.webp diff --git a/apps/docs/app/best-practices/contact-form/images/single-select-questionare.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/single-select-questionare.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/single-select-questionare.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/single-select-questionare.webp diff --git a/apps/docs/app/best-practices/contact-form/images/welcome1.webp b/docs/images/xm-and-surveys/xm/best-practices/contact-form/welcome1.webp similarity index 100% rename from apps/docs/app/best-practices/contact-form/images/welcome1.webp rename to docs/images/xm-and-surveys/xm/best-practices/contact-form/welcome1.webp diff --git a/apps/docs/app/best-practices/docs-feedback/add-action.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/add-action.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/add-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/add-action.webp diff --git a/apps/docs/app/best-practices/docs-feedback/change-id.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/change-id.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/change-id.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/change-id.webp diff --git a/apps/docs/app/best-practices/docs-feedback/copy-ids.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/copy-ids.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/copy-ids.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/copy-ids.webp diff --git a/apps/docs/app/best-practices/docs-feedback/docs-navi.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/docs-navi.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/docs-navi.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/docs-navi.webp diff --git a/apps/docs/app/best-practices/docs-feedback/docs-template.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/docs-template.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/docs-template.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/docs-template.webp diff --git a/apps/docs/app/best-practices/docs-feedback/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/select-action.webp diff --git a/apps/docs/app/best-practices/docs-feedback/survey-trigger.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/survey-trigger.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/survey-trigger.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/survey-trigger.webp diff --git a/apps/docs/app/best-practices/docs-feedback/switch-to-dev.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/switch-to-dev.webp similarity index 100% rename from apps/docs/app/best-practices/docs-feedback/switch-to-dev.webp rename to docs/images/xm-and-surveys/xm/best-practices/docs-feedback/switch-to-dev.webp diff --git a/apps/docs/app/best-practices/feature-chaser/action-css.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/action-css.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/action-css.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/action-css.webp diff --git a/apps/docs/app/best-practices/feature-chaser/action-innertext.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/action-innertext.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/action-innertext.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/action-innertext.webp diff --git a/apps/docs/app/best-practices/feature-chaser/change-text.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/change-text.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/change-text.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/change-text.webp diff --git a/apps/docs/app/best-practices/feature-chaser/create-survey.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/create-survey.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/create-survey.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/create-survey.webp diff --git a/apps/docs/app/best-practices/feature-chaser/publish.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/publish.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/publish.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/publish.webp diff --git a/apps/docs/app/best-practices/feature-chaser/recontact-options.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/recontact-options.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/recontact-options.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/recontact-options.webp diff --git a/apps/docs/app/best-practices/feature-chaser/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/feature-chaser/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/feature-chaser/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/feature-chaser/select-action.webp diff --git a/apps/docs/app/best-practices/feedback-box/action-css.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/action-css.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/action-css.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/action-css.webp diff --git a/apps/docs/app/best-practices/feedback-box/action-innertext.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/action-innertext.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/action-innertext.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/action-innertext.webp diff --git a/apps/docs/app/best-practices/feedback-box/add-action.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/add-action.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/add-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/add-action.webp diff --git a/apps/docs/app/best-practices/feedback-box/change-text-content.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/change-text-content.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/change-text-content.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/change-text-content.webp diff --git a/apps/docs/app/best-practices/feedback-box/create-feedback-box-by-template.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/create-feedback-box-by-template.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/create-feedback-box-by-template.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/create-feedback-box-by-template.webp diff --git a/apps/docs/app/best-practices/feedback-box/publish-survey.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/publish-survey.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/publish-survey.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/publish-survey.webp diff --git a/apps/docs/app/best-practices/feedback-box/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/select-action.webp diff --git a/apps/docs/app/best-practices/feedback-box/set-recontact-options.webp b/docs/images/xm-and-surveys/xm/best-practices/feedback-box/set-recontact-options.webp similarity index 100% rename from apps/docs/app/best-practices/feedback-box/set-recontact-options.webp rename to docs/images/xm-and-surveys/xm/best-practices/feedback-box/set-recontact-options.webp diff --git a/apps/docs/app/best-practices/improve-email-content/choose-survey-type.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-email-content/choose-survey-type.webp similarity index 100% rename from apps/docs/app/best-practices/improve-email-content/choose-survey-type.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-email-content/choose-survey-type.webp diff --git a/apps/docs/app/best-practices/improve-email-content/embed-survey-code-in-your-email.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-email-content/embed-survey-code-in-your-email.webp similarity index 100% rename from apps/docs/app/best-practices/improve-email-content/embed-survey-code-in-your-email.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-email-content/embed-survey-code-in-your-email.webp diff --git a/apps/docs/app/best-practices/improve-email-content/embed-survey-prompt.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-email-content/embed-survey-prompt.webp similarity index 100% rename from apps/docs/app/best-practices/improve-email-content/embed-survey-prompt.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-email-content/embed-survey-prompt.webp diff --git a/apps/docs/app/best-practices/improve-email-content/improve-newsletter-content-editor-formbricks.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-email-content/improve-newsletter-content-editor-formbricks.webp similarity index 100% rename from apps/docs/app/best-practices/improve-email-content/improve-newsletter-content-editor-formbricks.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-email-content/improve-newsletter-content-editor-formbricks.webp diff --git a/apps/docs/app/best-practices/improve-email-content/improve-newsletter-content-survey-location.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-email-content/improve-newsletter-content-survey-location.webp similarity index 100% rename from apps/docs/app/best-practices/improve-email-content/improve-newsletter-content-survey-location.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-email-content/improve-newsletter-content-survey-location.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/action-innertext.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-innertext.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/action-innertext.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-innertext.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/action-pageurl.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-pageurl.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/action-pageurl.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-pageurl.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/change-text.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/change-text.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/change-text.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/change-text.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/create-survey.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/create-survey.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/create-survey.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/create-survey.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/publish.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/publish.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/publish.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/publish.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/recontact-options.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/recontact-options.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/recontact-options.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/recontact-options.webp diff --git a/apps/docs/app/best-practices/improve-trial-cr/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/improve-trial-cr/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/improve-trial-cr/select-action.webp diff --git a/apps/docs/app/best-practices/interview-prompt/action-css.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/action-css.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/action-css.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/action-css.webp diff --git a/apps/docs/app/best-practices/interview-prompt/action-innertext.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/action-innertext.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/action-innertext.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/action-innertext.webp diff --git a/apps/docs/app/best-practices/interview-prompt/action-pageurl.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/action-pageurl.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/action-pageurl.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/action-pageurl.webp diff --git a/apps/docs/app/best-practices/interview-prompt/add-action.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/add-action.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/add-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/add-action.webp diff --git a/apps/docs/app/best-practices/interview-prompt/change-text.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/change-text.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/change-text.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/change-text.webp diff --git a/apps/docs/app/best-practices/interview-prompt/create-prompt.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/create-prompt.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/create-prompt.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/create-prompt.webp diff --git a/apps/docs/app/best-practices/interview-prompt/interview-example.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/interview-example.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/interview-example.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/interview-example.webp diff --git a/apps/docs/app/best-practices/interview-prompt/publish-survey.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/publish-survey.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/publish-survey.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/publish-survey.webp diff --git a/apps/docs/app/best-practices/interview-prompt/recontact-options.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/recontact-options.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/recontact-options.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/recontact-options.webp diff --git a/apps/docs/app/best-practices/interview-prompt/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/interview-prompt/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/interview-prompt/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/interview-prompt/select-action.webp diff --git a/apps/docs/app/best-practices/pmf-survey/action-css.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/action-css.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/action-css.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/action-css.webp diff --git a/apps/docs/app/best-practices/pmf-survey/action-pageurl.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/action-pageurl.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/action-pageurl.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/action-pageurl.webp diff --git a/apps/docs/app/best-practices/pmf-survey/change-text.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/change-text.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/change-text.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/change-text.webp diff --git a/apps/docs/app/best-practices/pmf-survey/create-survey.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/create-survey.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/create-survey.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/create-survey.webp diff --git a/apps/docs/app/best-practices/pmf-survey/publish.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/publish.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/publish.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/publish.webp diff --git a/apps/docs/app/best-practices/pmf-survey/recontact-options.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/recontact-options.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/recontact-options.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/recontact-options.webp diff --git a/apps/docs/app/best-practices/pmf-survey/select-action.webp b/docs/images/xm-and-surveys/xm/best-practices/pmf-survey/select-action.webp similarity index 100% rename from apps/docs/app/best-practices/pmf-survey/select-action.webp rename to docs/images/xm-and-surveys/xm/best-practices/pmf-survey/select-action.webp diff --git a/apps/docs/app/best-practices/quiz-time/conditional-logic.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/conditional-logic.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/conditional-logic.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/conditional-logic.webp diff --git a/apps/docs/app/best-practices/quiz-time/ending-logic.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/ending-logic.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/ending-logic.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/ending-logic.webp diff --git a/apps/docs/app/best-practices/quiz-time/pass-fail.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/pass-fail.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/pass-fail.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/pass-fail.webp diff --git a/apps/docs/app/best-practices/quiz-time/quiz.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/quiz.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/quiz.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/quiz.webp diff --git a/apps/docs/app/best-practices/quiz-time/score.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/score.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/score.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/score.webp diff --git a/apps/docs/app/best-practices/quiz-time/single-select.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/single-select.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/single-select.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/single-select.webp diff --git a/apps/docs/app/best-practices/quiz-time/when-then.webp b/docs/images/xm-and-surveys/xm/best-practices/quiz-time/when-then.webp similarity index 100% rename from apps/docs/app/best-practices/quiz-time/when-then.webp rename to docs/images/xm-and-surveys/xm/best-practices/quiz-time/when-then.webp diff --git a/docs/mint.json b/docs/mint.json new file mode 100644 index 0000000000..6a5bc8a1ab --- /dev/null +++ b/docs/mint.json @@ -0,0 +1,715 @@ +{ + "$schema": "https://mintlify.com/schema.json", + "colors": { + "anchors": { + "from": "#0D9373", + "to": "#07C983" + }, + "dark": "#00C4B8", + "light": "#00C4B8", + "primary": "#00C4B8" + }, + "favicon": "/images/favicon.svg", + "footerSocials": { + "github": "https://github.com/formbricks/formbricks", + "linkedin": "https://linkedin.com/company/formbricks", + "x": "https://x.com/formbricks" + }, + "logo": { + "dark": "/images/logo-dark.svg", + "light": "/images/logo-light.svg" + }, + "name": "Documentation - Formbricks", + "navigation": [ + { + "group": "", + "pages": ["overview/introduction", "overview/what-is-formbricks", "overview/open-source"] + }, + { + "group": "", + "pages": ["xm-and-surveys/overview"] + }, + { + "group": "Surveys", + "pages": [ + { + "group": "General Features", + "icon": "wrench", + "pages": [ + "xm-and-surveys/surveys/general-features/add-image-or-video-question", + "xm-and-surveys/surveys/general-features/conditional-logic", + "xm-and-surveys/surveys/general-features/overwrite-styling", + "xm-and-surveys/surveys/general-features/hidden-fields", + "xm-and-surveys/surveys/general-features/limit-submissions", + "xm-and-surveys/surveys/general-features/multi-language-surveys", + "xm-and-surveys/surveys/general-features/partial-submissions", + "xm-and-surveys/surveys/general-features/recall", + "xm-and-surveys/surveys/general-features/shareable-dashboards", + "xm-and-surveys/surveys/general-features/schedule-start-end-dates", + "xm-and-surveys/surveys/general-features/metadata", + "xm-and-surveys/surveys/general-features/variables" + ] + }, + { + "group": "Link Surveys", + "icon": "link", + "pages": [ + "xm-and-surveys/surveys/link-surveys/quickstart", + { + "group": "Features", + "icon": "wrench", + "pages": [ + "xm-and-surveys/surveys/link-surveys/data-prefilling", + "xm-and-surveys/surveys/link-surveys/embed-surveys", + "xm-and-surveys/surveys/link-surveys/market-research-panel", + "xm-and-surveys/surveys/link-surveys/pin-protected-surveys", + "xm-and-surveys/surveys/link-surveys/single-use-links", + "xm-and-surveys/surveys/link-surveys/source-tracking", + "xm-and-surveys/surveys/link-surveys/start-at-question", + "xm-and-surveys/surveys/link-surveys/verify-email-before-survey" + ] + } + ] + }, + { + "group": "Website & App Surveys", + "icon": "mobile", + "pages": [ + "xm-and-surveys/surveys/website-app-surveys/quickstart", + "xm-and-surveys/surveys/website-app-surveys/framework-guides", + { + "group": "Features", + "icon": "wrench", + "pages": [ + "xm-and-surveys/surveys/website-app-surveys/actions", + "xm-and-surveys/surveys/website-app-surveys/advanced-targeting", + "xm-and-surveys/surveys/website-app-surveys/user-identification", + "xm-and-surveys/surveys/website-app-surveys/recontact", + "xm-and-surveys/surveys/website-app-surveys/show-survey-to-percent-of-users" + ] + } + ] + } + ] + }, + + { + "group": "Core Features", + "pages": [ + { + "group": "Question Types", + "icon": "question", + "pages": [ + "xm-and-surveys/core-features/question-type/address", + "xm-and-surveys/core-features/question-type/consent", + "xm-and-surveys/core-features/question-type/contact-info", + "xm-and-surveys/core-features/question-type/date", + "xm-and-surveys/core-features/question-type/file-upload", + "xm-and-surveys/core-features/question-type/free-text", + "xm-and-surveys/core-features/question-type/matrix", + "xm-and-surveys/core-features/question-type/net-promoter-score", + "xm-and-surveys/core-features/question-type/ranking", + "xm-and-surveys/core-features/question-type/rating", + "xm-and-surveys/core-features/question-type/schedule-a-meeting", + "xm-and-surveys/core-features/question-type/select-multiple", + "xm-and-surveys/core-features/question-type/select-picture", + "xm-and-surveys/core-features/question-type/select-single", + "xm-and-surveys/core-features/question-type/statement-cta" + ] + }, + { + "group": "Integrations", + "icon": "bridge", + "pages": [ + "xm-and-surveys/core-features/integrations/overview", + "xm-and-surveys/core-features/integrations/activepieces", + "xm-and-surveys/core-features/integrations/airtable", + "xm-and-surveys/core-features/integrations/google-sheets", + "xm-and-surveys/core-features/integrations/make", + "xm-and-surveys/core-features/integrations/n8n", + "xm-and-surveys/core-features/integrations/notion", + "xm-and-surveys/core-features/integrations/slack", + "xm-and-surveys/core-features/integrations/wordpress", + "xm-and-surveys/core-features/integrations/zapier", + "xm-and-surveys/core-features/integrations/webhooks" + ] + }, + "xm-and-surveys/core-features/user-management", + "xm-and-surveys/core-features/styling-theme", + "xm-and-surveys/core-features/email-customization" + ] + }, + { + "group": "XM", + "pages": [ + { + "group": "Best Practices", + "icon": "lightbulb", + "pages": [ + "xm-and-surveys/xm/best-practices/contact-form", + "xm-and-surveys/xm/best-practices/docs-feedback", + "xm-and-surveys/xm/best-practices/feature-chaser", + "xm-and-surveys/xm/best-practices/feedback-box", + "xm-and-surveys/xm/best-practices/improve-email-content", + "xm-and-surveys/xm/best-practices/interview-prompt", + "xm-and-surveys/xm/best-practices/cancel-subscription", + "xm-and-surveys/xm/best-practices/pmf-survey", + "xm-and-surveys/xm/best-practices/quiz-time", + "xm-and-surveys/xm/best-practices/improve-trial-cr" + ] + } + ] + }, + { + "group": "", + "pages": ["development/overview"] + }, + { + "group": "Local Setup", + "pages": [ + "development/local-setup/linux", + "development/local-setup/mac", + "development/local-setup/windows", + "development/local-setup/gitpod", + "development/local-setup/github-codespaces" + ] + }, + { + "group": "Contribution", + "pages": ["development/contribution/contribution"] + }, + { + "group": "Support", + "pages": ["development/support/troubleshooting"] + }, + { + "group": "", + "pages": ["self-hosting/overview"] + }, + { + "group": "Setup", + "pages": [ + "self-hosting/setup/one-click", + "self-hosting/setup/docker", + "self-hosting/setup/cluster-setup" + ] + }, + { + "group": "Configuration", + "pages": [ + "self-hosting/configuration/custom-ssl", + "self-hosting/configuration/environment-variables", + "self-hosting/configuration/oauth", + { + "group": "Integrations", + "icon": "bridge", + "pages": [ + "self-hosting/configuration/integrations/airtable", + "self-hosting/configuration/integrations/google-sheets", + "self-hosting/configuration/integrations/n8n", + "self-hosting/configuration/integrations/notion", + "self-hosting/configuration/integrations/slack", + "self-hosting/configuration/integrations/zapier" + ] + } + ] + }, + { + "group": "Advanced", + "pages": [ + "self-hosting/advanced/migration", + "self-hosting/advanced/license", + "self-hosting/advanced/rate-limiting" + ] + }, + { + "group": "API Documentation", + "pages": ["api-reference/introduction", "api-reference/rest-api"] + } + ], + "redirects": [ + { + "destination": "/docs/overview/what-is-formbricks", + "permanent": true, + "source": "/docs/introduction/what-is-formbricks" + }, + { + "destination": "/docs/overview/open-source", + "permanent": true, + "source": "/docs/introduction/why-open-source" + }, + { + "destination": "/docs/xm-and-surveys/overview", + "permanent": true, + "source": "/docs/introduction/how-it-works" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/contact-form", + "permanent": true, + "source": "/docs/best-practices/contact-form" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/docs-feedback", + "permanent": true, + "source": "/docs/best-practices/docs-feedback" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/feature-chaser", + "permanent": true, + "source": "/docs/best-practices/feature-chaser" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/feedback-box", + "permanent": true, + "source": "/docs/best-practices/feedback-box" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/improve-email-content", + "permanent": true, + "source": "/docs/best-practices/improve-email-content" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/interview-prompt", + "permanent": true, + "source": "/docs/best-practices/interview-prompt" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/cancel-subscription", + "permanent": true, + "source": "/docs/best-practices/cancel-subscription" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/pmf-survey", + "permanent": true, + "source": "/docs/best-practices/pmf-survey" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/quiz-time", + "permanent": true, + "source": "/docs/best-practices/quiz-time" + }, + { + "destination": "/docs/xm-and-surveys/xm/best-practices/improve-trial-cr", + "permanent": true, + "source": "/docs/best-practices/improve-trial-cr" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/quickstart", + "permanent": true, + "source": "/docs/link-surveys/quickstart" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/add-image-or-video-question", + "permanent": true, + "source": "/docs/link-surveys/global/add-image-or-video-question" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/conditional-logic", + "permanent": true, + "source": "/docs/link-surveys/global/conditional-logic" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/overwrite-styling", + "permanent": true, + "source": "/docs/link-surveys/global/overwrite-styling" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/data-prefilling", + "permanent": true, + "source": "/docs/link-surveys/global/data-prefilling" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/embed-surveys", + "permanent": true, + "source": "/docs/link-surveys/embed-surveys" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/hidden-fields", + "permanent": true, + "source": "/docs/link-surveys/global/hidden-fields" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/limit-submissions", + "permanent": true, + "source": "/docs/link-surveys/global/limit-submissions" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/market-research-panel", + "permanent": true, + "source": "/docs/link-surveys/market-research-panel" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/multi-language-surveys", + "permanent": true, + "source": "/docs/link-surveys/global/multi-language-surveys" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/partial-submissions", + "permanent": true, + "source": "/docs/link-surveys/global/partial-submissions" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/pin-protected-surveys", + "permanent": true, + "source": "/docs/link-surveys/pin-protected-surveys" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/recall", + "permanent": true, + "source": "/docs/link-surveys/global/recall" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/shareable-dashboards", + "permanent": true, + "source": "/docs/link-surveys/global/shareable-dashboards" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/single-use-links", + "permanent": true, + "source": "/docs/link-surveys/single-use-links" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/source-tracking", + "permanent": true, + "source": "/docs/link-surveys/source-tracking" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/schedule-start-end-dates", + "permanent": true, + "source": "/docs/link-surveys/global/schedule-start-end-dates" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/start-at-question", + "permanent": true, + "source": "/docs/link-surveys/start-at-question" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/metadata", + "permanent": true, + "source": "/docs/link-surveys/global/metadata" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/variables", + "permanent": true, + "source": "/docs/link-surveys/global/variables" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/verify-email-before-survey", + "permanent": true, + "source": "/docs/link-surveys/verify-email-before-survey" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/add-image-or-video-question", + "permanent": true, + "source": "/docs/app-surveys/global/add-image-or-video-question" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/consent", + "permanent": true, + "source": "/docs/core-features/global/question-type/consent" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/statement-cta", + "permanent": true, + "source": "/docs/core-features/global/question-type/statement-cta" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/airtable", + "permanent": true, + "source": "/docs/developer-docs/integrations/airtable" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/zapier", + "permanent": true, + "source": "/docs/developer-docs/integrations/zapier" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/wordpress", + "permanent": true, + "source": "/docs/developer-docs/integrations/wordpress" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/slack", + "permanent": true, + "source": "/docs/developer-docs/integrations/slack" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/n8n", + "permanent": true, + "source": "/docs/developer-docs/integrations/n8n" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/notion", + "permanent": true, + "source": "/docs/developer-docs/integrations/notion" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/google-sheets", + "permanent": true, + "source": "/docs/developer-docs/integrations/google-sheets" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/quickstart", + "permanent": true, + "source": "/docs/app-surveys/quickstart" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/address", + "permanent": true, + "source": "/docs/core-features/global/question-type/address" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides", + "permanent": true, + "source": "/docs/app-surveys/framework-guides" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/activepieces", + "permanent": true, + "source": "/docs/developer-docs/integrations/activepieces" + }, + { + "destination": "/docs/xm-and-surveys/core-features/user-management", + "permanent": true, + "source": "/docs/core-features/global/access-roles" + }, + { + "destination": "/docs/xm-and-surveys/core-features/styling-theme", + "permanent": true, + "source": "/docs/core-features/global/styling-theme" + }, + { + "destination": "/docs/xm-and-surveys/core-features/email-customization", + "permanent": true, + "source": "/docs/core-features/global/email-customization" + }, + { + "destination": "/docs/self-hosting/setup/one-click", + "permanent": true, + "source": "/docs/self-hosting/one-click" + }, + { + "destination": "/docs/self-hosting/configuration/custom-ssl", + "permanent": true, + "source": "/docs/self-hosting/custom-ssl" + }, + { + "destination": "/docs/self-hosting/setup/docker", + "permanent": true, + "source": "/docs/self-hosting/docker" + }, + { + "destination": "/docs/self-hosting/setup/cluster-setup", + "permanent": true, + "source": "/docs/self-hosting/cluster-setup" + }, + { + "destination": "/docs/self-hosting/advanced/migration", + "permanent": true, + "source": "/docs/self-hosting/migration-guide" + }, + { + "destination": "/docs/self-hosting/configuration/integrations", + "permanent": true, + "source": "/docs/self-hosting/integrations" + }, + { + "destination": "/docs/self-hosting/advanced/license", + "permanent": true, + "source": "/docs/self-hosting/license" + }, + { + "destination": "/docs/self-hosting/advanced/rate-limiting", + "permanent": true, + "source": "/docs/self-hosting/rate-limiting" + }, + { + "destination": "/docs/self-hosting/setup/cluster-setup", + "permanent": true, + "source": "/docs/self-hosting/kubernetes" + }, + { + "destination": "/docs/development/overview", + "permanent": true, + "source": "/docs/developer-docs/overview" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides", + "permanent": true, + "source": "/docs/developer-docs/js-sdk" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#react-native", + "permanent": true, + "source": "docs/developer-docs/react-native-in-app-surveys" + }, + { + "destination": "/docs/api-reference/rest-api", + "permanent": true, + "source": "/docs/developer-docs/rest-api" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/webhooks", + "permanent": true, + "source": "/docs/developer-docs/webhooks" + }, + { + "destination": "/docs/development/contribution/contribution", + "permanent": true, + "source": "/docs/developer-docs/contributing/get-started" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/actions", + "permanent": true, + "source": "/docs/app-surveys/actions" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting", + "permanent": true, + "source": "/docs/app-surveys/advanced-targeting" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/user-identification", + "permanent": true, + "source": "/docs/app-surveys/user-identification" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/recontact", + "permanent": true, + "source": "/docs/app-surveys/recontact" + }, + { + "destination": "/docs/xm-and-surveys/surveys/website-app-surveys/show-survey-to-percent-of-users", + "permanent": true, + "source": "/docs/app-surveys/global/show-survey-to-percent-of-users" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/metadata", + "permanent": true, + "source": "/docs/app-surveys/global/metadata" + }, + { + "destination": "/docs/api-reference", + "permanent": true, + "source": "/docs/api-docs" + }, + { + "destination": "/docs/development/troubleshooting", + "permanent": true, + "source": "/docs/developer-docs/contributing/troubleshooting" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/file-upload", + "permanent": true, + "source": "/docs/core-features/global/question-type/file-upload" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/select-picture", + "permanent": true, + "source": "/docs/core-features/global/question-type/picture-selection" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/rating", + "permanent": true, + "source": "/docs/core-features/global/question-type/rating" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/date", + "permanent": true, + "source": "/docs/core-features/global/question-type/date" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/schedule-a-meeting", + "permanent": true, + "source": "/docs/core-features/global/question-type/schedule" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/free-text", + "permanent": true, + "source": "/docs/core-features/global/question-type/free-text" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/select-single", + "permanent": true, + "source": "/docs/core-features/global/question-type/single-select" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/select-multiple", + "permanent": true, + "source": "/docs/core-features/global/question-type/multiple-select" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/matrix", + "permanent": true, + "source": "/docs/core-features/global/question-type/matrix" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/make", + "permanent": true, + "source": "/docs/developer-docs/integrations/make" + }, + { + "destination": "/docs/xm-and-surveys/core-features/integrations/overview", + "permanent": true, + "source": "/docs/developer-docs/integrations/overview" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/hidden-fields", + "permanent": true, + "source": "/docs/app-surveys/global/hidden-fields" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/limit-submissions", + "permanent": true, + "source": "/docs/app-surveys/global/limit-submissions" + }, + { + "destination": "/docs/xm-and-surveys/core-features/question-type/net-promoter-score", + "permanent": true, + "source": "/docs/core-features/global/question-type/net-promoter-score" + }, + { + "destination": "/docs/xm-and-surveys/surveys/link-surveys/data-prefilling", + "permanent": true, + "source": "/docs/link-surveys/data-prefilling" + }, + { + "destination": "/docs/xm-and-surveys/surveys/general-features/multi-language-surveys", + "permanent": true, + "source": "/docs/app-surveys/global/multi-language-surveys" + } + ], + "tabs": [ + { + "name": "Overview", + "url": "overview" + }, + { + "name": "XM & Surveys", + "url": "xm-and-surveys" + }, + { + "name": "Self Hosting", + "url": "self-hosting" + }, + { + "name": "API Reference", + "openapi": "/api-reference/openapi.json", + "url": "api-reference" + }, + { + "name": "Development", + "url": "development" + } + ], + "topbarCtaButton": { + "name": "Go to app", + "url": "https://app.formbricks.com" + }, + "topbarLinks": [ + { + "name": "Support", + "url": "https://github.com/formbricks/formbricks/discussions" + } + ] +} diff --git a/docs/overview/images/analytics.webp b/docs/overview/images/analytics.webp new file mode 100644 index 0000000000..5161cc3240 Binary files /dev/null and b/docs/overview/images/analytics.webp differ diff --git a/docs/overview/images/form-builder.webp b/docs/overview/images/form-builder.webp new file mode 100644 index 0000000000..c94bdbc8be Binary files /dev/null and b/docs/overview/images/form-builder.webp differ diff --git a/docs/overview/images/integrations.webp b/docs/overview/images/integrations.webp new file mode 100644 index 0000000000..d94c009b40 Binary files /dev/null and b/docs/overview/images/integrations.webp differ diff --git a/docs/overview/images/targeting.webp b/docs/overview/images/targeting.webp new file mode 100644 index 0000000000..7ca18df49b Binary files /dev/null and b/docs/overview/images/targeting.webp differ diff --git a/docs/overview/images/trigger.webp b/docs/overview/images/trigger.webp new file mode 100644 index 0000000000..62b2c8c20b Binary files /dev/null and b/docs/overview/images/trigger.webp differ diff --git a/docs/overview/introduction.mdx b/docs/overview/introduction.mdx new file mode 100644 index 0000000000..0dfc614209 --- /dev/null +++ b/docs/overview/introduction.mdx @@ -0,0 +1,32 @@ +--- +title: "Introduction" +description: "Welcome to the Formbricks Documentation!" +icon: "presentation-screen" +--- + +## Welcome to the Formbricks Documentation! + +Formbricks is a versatile open-source platform for collecting and analyzing feedback from customers, users, and employees through targeted surveys. Whether you need simple forms or complex experience management solutions, Formbricks scales with your needs. + +This guide covers everything you need to set up, use, and develop with Formbricks. You'll find step-by-step instructions, feature explanations, and best practices. It also includes advanced docs on extending Formbricks using its self-hosted option, APIs, and SDKs. + + + + + + Learn how to use Formbricks' XM & Surveys to collect feedback from your customers, users, and employees. + + + Learn how to self-host Formbricks on your infrastructure. + + + + Learn how to use Formbricks' API to CRUD various resources programmatically. + + + + Warm up with the Formbricks code base to make changes to the platform. + + + + diff --git a/apps/docs/app/introduction/why-open-source/page.mdx b/docs/overview/open-source.mdx similarity index 64% rename from apps/docs/app/introduction/why-open-source/page.mdx rename to docs/overview/open-source.mdx index fbf25bbcc8..5f1d9d792d 100644 --- a/apps/docs/app/introduction/why-open-source/page.mdx +++ b/docs/overview/open-source.mdx @@ -1,19 +1,16 @@ -export const metadata = { - title: "Why Formbricks is Open Source", - description: - "Open source software beats proprietary software in every aspect - except for value capture. We're investing in growing the value creation of our open source platform because it directly translates into business with large organisations.", -}; +--- +title: "Open-Source" +icon: "osi" +--- -#### Introduction +The open core of the Formbricks software is available under the AGPLv3 license. This means you can use, modify, and distribute the software as long as you adhere to the [terms of the license.](https://www.tldrlegal.com/license/gnu-affero-general-public-license-v3-agpl-3-0) The open-source version of Formbricks is free to use, even for commercial purposes. Over time, all survey features will remain part of the free Community Edition. -# Why is Formbricks open source? - -A lot has been written on why open source software beats proprietary software in all aspects - except for value capture for the company investing into its development. While this definitely poses a challenge for a profit-oriented organisation, it's also an interesting opportunity: Due to the open nature of our platform, it's usage is significantly higher. Capturing a small part of the value our platform generates translates into a decently-sized business. +Formbricks also offers a more advanced Enterprise Edition with additional features and support. | Advantage | Open Source Software | Proprietary Software | | --------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- | -| **Data Privacy** | Self-host for maximum control over data | Dependent on thrid party data processor. | -| **Cost** | Often free or significantly lower cost. | Typically requires a purchase or subscription. | +| **Data Privacy** | Self-host for maximum control over data | Dependent on third party data processor. | +| **Cost** | Often free or significantly lower cost. | Typically, requires a purchase or subscription. | | **Customizability** | Code can be modified to meet specific needs. | Limited customization, restricted to developer's features. | | **Security** | Frequent community reviews identify vulnerabilities quickly. | Security updates depend on vendor's schedule and interest. | | **Flexibility** | Supports a wide range of applications and integrations. | Designed for specific environments and integrations. | @@ -22,4 +19,4 @@ A lot has been written on why open source software beats proprietary software in | **Licensing** | Permissive licenses allow broad usage and modification. | Strict licensing with limited redistribution rights. | | **Independence** | Not dependent on a single vendor or developer. | Vendor lock-in can limit future choices. | | **Transparency** | Full visibility into the code base and development. | Closed-source, code is hidden from users. | -| **Interoperability** | Supports open standards, ensuring interoperability. | Often requires additional software or plugins for compatibility. | +| **Interoperability** | Supports open standards, ensuring interoperability. | Often requires additional software or plugins for compatibility. | \ No newline at end of file diff --git a/docs/overview/what-is-formbricks.mdx b/docs/overview/what-is-formbricks.mdx new file mode 100644 index 0000000000..8213a3f1c5 --- /dev/null +++ b/docs/overview/what-is-formbricks.mdx @@ -0,0 +1,34 @@ +--- +title: "What is Formbricks?" +description: "Get to know Formbricks and its capabilities." +icon: "laptop-mobile" +--- + +Formbricks is an open-source (AGPLv3) survey platform built to collect feedback from anyone—users, customers, or employees—on any platform. + +With Formbricks, you can replace many existing survey tools: + +- **Standalone surveys (share via link):** Replace Google Forms, Typeform or any other link survey tool [with Formbricks Form Builder](https://formbricks.com/open-source-form-builder). Use lots of question types and comprehensive customizations. + +- **Scalable website surveys:** Even if you have millions of website visitors, Formbricks lets you run well-timed and anonymously targeted [surveys on any public website.](https://formbricks.com/website-survey) + +- **Highly targeted app surveys:** Identify known users with Formbricks and enrich their profiles with attributes and specific actions. Build cohorts for [highly targeted in-app surveys.](https://formbricks.com/in-app-survey) + +The survey platform is **mostly free, even for commercial use**. Over time, all survey features will stay part of the free Community Edition. + +### Formbricks – The Experience Management (XM) Suite + +To support the development of our open-source platform, we’ve created a premium offering: the **Formbricks XM Suite**. + +- **What is XM?** + Experience Management (XM) involves collecting, analysing, and reporting feedback from stakeholders (like customers or employees) to understand and improve their experience with your organisation. + +- **Why XM Matters** + Helping businesses, governments, and nonprofits understand their users' experiences leads to better services and happier people. Formbricks XM provides the data needed to make decisions that put people first. + +- **How XM Works in Formbricks** + Formbricks XM simplifies experience management. It focuses only on what’s needed to measure specific experiences, with easy-to-use templates, reports, and best practices. + +We have spent a lot of time and energy building out the open-source survey platform which powers the above. Stick around to see how Formbricks XM Apps will empower everyone to think and work human-centric. + +[Try Formbricks Cloud ☁️ ](https://app.formbricks.com/) diff --git a/apps/docs/app/self-hosting/license/page.mdx b/docs/self-hosting/advanced/license.mdx similarity index 57% rename from apps/docs/app/self-hosting/license/page.mdx rename to docs/self-hosting/advanced/license.mdx index 651a8c5e30..f6a68f755f 100644 --- a/apps/docs/app/self-hosting/license/page.mdx +++ b/docs/self-hosting/advanced/license.mdx @@ -1,11 +1,8 @@ -export const metadata = { - title: "About the Formbricks Open-Source License", - description: "The Formbricks core is available under the AGPLv3 license", -}; - -#### Self-Hosting - -# License +--- +title: 'License' +description: "License for Formbricks" +icon: "file-certificate" +--- The Formbricks core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises under a separate, paid Enterprise License. This assures the long-term sustainability of the open source project. All free features are listed [below](#what-features-are-free). @@ -15,26 +12,26 @@ The Formbricks core source code is licensed under AGPLv3 and available on GitHub ## Enterprise Edition -Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository contains code licensed under our **[Enterprise License](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE)**. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing) or [get in touch](https://cal.com/johannes/license). +Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository contains code licensed under our [Enterprise License](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE). This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing) or [get in touch](https://cal.com/johannes/license). ## When do I need an Enterprise License? | | Community Edition | Enterprise License | | ------------------------------------------------------------- | ----------------- | ------------------ | -| Self-host for commercial purposes | ✅ | No license needed | -| Fork codebase, make changes, release under AGPLv3 | ✅ | No license needed | -| Fork codebase, make changes, **keep private** | ❌ | ✅ | -| Unlimited responses | ✅ | No license needed | -| Unlimited surveys | ✅ | No license needed | -| Unlimited users | ✅ | No license needed | +| Self-host for commercial purposes | ✅ | No license needed | +| Fork codebase, make changes, release under AGPLv3 | ✅ | No license needed | +| Fork codebase, make changes, **keep private** | ❌ | ✅ | +| Unlimited responses | ✅ | No license needed | +| Unlimited surveys | ✅ | No license needed | +| Unlimited users | ✅ | No license needed | | Projects | 3 | Unlimited | -| Use any of the other [free features](#what-features-are-free) | ✅ | No license needed | -| Remove branding ⚠️ Changed with 3.0 ⚠️ | ❌ | ✅ | -| SSO ⚠️ Changed with 3.0 ⚠️ | ❌ | ✅ | -| Contacts & Targeting ⚠️ Changed with 3.0 ⚠️ | ❌ | ✅ | -| Teams & access roles | ❌ | ✅ | -| Cluster support | ❌ | ✅ | -| Use any of the [paid features](#what-features-are-free) | ❌ | ✅ | +| Use any of the other [free features](#what-features-are-free) | ✅ | No license needed | +| Remove branding ⚠️ Changed with 3.0 ⚠️ | ❌ | ✅ | +| SSO ⚠️ Changed with 3.0 ⚠️ | ❌ | ✅ | +| Contacts & Targeting ⚠️ Changed with 3.0 ⚠️ | ❌ | ✅ | +| Teams & access roles | ❌ | ✅ | +| Cluster support | ❌ | ✅ | +| Use any of the [paid features](#what-features-are-free) | ❌ | ✅ | Ready to get started with the Enterprise Edition? Fill out our form below and we'll reach out to you. @@ -44,26 +41,25 @@ Many organisations want to do an internal test run with the Enterprise Edition.
- + position: "relative", + height: "100vh", + maxHeight: "100vh", + overflow: "auto", + borderRadius: "12px", +}} +> +
``` - - - ## Iframe Events The iframe fires a **formbricksSurveyCompleted** event when a user finishes a survey within the embedded iframe. This event can be captured through a message listener in your webpage's JavaScript @@ -67,29 +49,28 @@ The iframe fires a **formbricksSurveyCompleted** event when a user finishes a su 3. Check if the received message indicates that the survey is completed by comparing the `event.data` with the value `formbricksSurveyCompleted`. - It is important to verify the origin of the message to ensure it comes from the iframe containing your survey, enhancing the security of your event handling. + It is important to verify the origin of the message to ensure it comes from + the iframe containing your survey, enhancing the security of your event + handling. 4. Implement your custom actions within the callback function based on the survey completion. ### Example of Handling Survey Completion Events - - - -```javascript +```javascript Example Code for Event Listener window.addEventListener("message", (event) => { // Replace 'https://app.formbricks.com' with the actual web app url - if (event.origin === "https://app.formbricks.com" && event.data === "formbricksSurveyCompleted") { + if ( + event.origin === "https://app.formbricks.com" && + event.data === "formbricksSurveyCompleted" + ) { console.log("Survey completed!"); // Implement your custom actions here } }); ``` - - - ## Emebd Mode Embed your survey with a minimalist design, disregarding padding and background. @@ -102,30 +83,16 @@ It can be enabled by simply appending **?embed=true** to your survey link or fro 2. Toggle **Embed mode** - +![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp) ### With Embed mode enabled - +![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-enabled.webp) ### With Embed mode disabled - +![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-disabled.webp) + ## Embedding Surveys in Emails @@ -149,7 +116,8 @@ Different email clients have different support for HTML and CSS. We recommend te Below are some of the methods and services that we know that allows HTML embedding and how you can use them: - Please use the below methods at your own discretion. We do not officially endorse any of the services mentioned below. + Please use the below methods at your own discretion. We do not officially + endorse any of the services mentioned below. ### 1. Gmail @@ -160,48 +128,22 @@ Gmail does not support HTML embedding natively. It's a WYSIWYG (What You See Is - Open Gmail and compose a new email. - Write your email content after which you want to embed the survey. -{" "} - - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-without-survey.webp) - Right next to the Send button you will see a new button called **HTML Editor**. Click on it. - This will open a new window with the **Design** tab active. Switch to the **Source** tab. -{" "} +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-source-tab.webp) - - Now paste the copied HTML code from Formbricks into this window. On the right, you will see a preview of how the email will look. -{" "} - - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-add-survey.webp) - Click on the **Close Editor** button to save the changes & close the editor. -{" "} +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-with-survey.webp) - - Voila! You have successfully embedded the survey in your email. @@ -227,19 +169,15 @@ Nodemailer is a Node.js module that allows you to send emails with HTML content. - Reference: Take a look at Nodemailer's official message documentation [here](https://nodemailer.com/message/) - Please note that the above methods are not exhaustive and there are many other ways to embed HTML in emails. + Please note that the above methods are not exhaustive and there are many other + ways to embed HTML in emails. ## Example: Email Footer Survey Embed a survey link in your email signature to collect feedback subtly yet effectively. Here’s how: - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/jo-signature.webp) 1. Create a Survey: Adjust an existing survey or create a new one. 2. Scroll down & enable the **Hidden Fields** option. @@ -247,21 +185,16 @@ Embed a survey link in your email signature to collect feedback subtly yet effec 4. Now Publish the survey as a Link Survey & copy the link. 5. Embed in Your Signature: Add this HTML snippet to your email signature in your email client settings. - - - -```html -Was our conversation helpful? Yes 👍 | +```html Embed this in your Email +Was our conversation helpful? +Yes 👍 | No 👎 ``` - - - Replace `YOUR_SURVEY_LINK` with the actual survey link. PS: If you do not see any signature settings, just use one of the methods we've mentioned above to embed the HTML code in your email. --- -**Can’t figure it out?**: **[Get help in Github Discussions](https://github.com/formbricks/formbricks/discussions)** +**Can’t figure it out?**: [Get help in Github Discussions](https://github.com/formbricks/formbricks/discussions) diff --git a/apps/docs/app/link-surveys/market-research-panel/page.mdx b/docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx similarity index 53% rename from apps/docs/app/link-surveys/market-research-panel/page.mdx rename to docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx index 5bdf751fbe..6ec3ee3924 100644 --- a/apps/docs/app/link-surveys/market-research-panel/page.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx @@ -1,22 +1,9 @@ -import { MdxImage } from "@/components/mdx-image"; - -import CopySurveyLink from "./copy-survey-link.webp"; -import CreateStudy from "./create-study.webp"; -import HiddenFields from "./hidden-fields.webp"; -import PreviewComplete from "./preview-complete.webp"; -import PreviewStudy from "./preview-study.webp"; -import AddRedirectUrl from "./redirect-url-formbricks.webp"; -import RedirectUrl from "./redirect-url.webp"; -import ScreeningOut from "./screening-out.webp"; -import UrlParameters from "./url-parameters.webp"; - -export const metadata = { - title: "Creating a Research Panel with Prolific", - description: - "Formbricks surveys can be integrated with Prolifics participant panel easily. This tutorial walks you through the steps on how to access a pool of over 200.000 participants for your research.", -}; - -#### Research Panel +--- +title: "Market Research Panel" +description: + "Formbricks surveys can be integrated with Prolifics participant panel easily. This tutorial walks you through the steps on how to access a pool of over 200.000 participants for your research." +icon: "users" +--- # Creating a Research Panel with Prolific @@ -25,8 +12,10 @@ You need a lot of research participants that match your target audience fast? Formbricks integrates well with Prolific. Prolific provides a pool of over 200.000 research participants you can choose from. Run market research with Formbricks within hours, not days. - Prolific is a paid service. You need to fund your account to access the pool of participants. The cost - depends on the number of participants you want to reach and the demographics you're targeting. You can get an estimate of the cost with the [Prolific price calculator](https://www.prolific.com/calculator) + Prolific is a paid service. You need to fund your account to access the pool + of participants. The cost depends on the number of participants you want to + reach and the demographics you're targeting. You can get an estimate of the + cost with the [Prolific price calculator](https://www.prolific.com/calculator) ## Purpose @@ -45,12 +34,7 @@ To be able to attribute a completed answer to a research participant, you need t Add three fields with the IDs `PROLIFIC_PID`, `STUDY_ID`, and `SESSION_ID`. - +![Hidden fields added](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/hidden-fields.webp) ### Step 2: Create an account on Prolific @@ -60,34 +44,20 @@ Go to [Prolific](https://app.prolific.co/) and create an account. Once you're logged in to Prolific, create a new study. - +![Create a study on Prolific](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/create-study.webp) ### Step 4: Copy the Formbricks survey link to the Prolific study We connect the Formbricks survey with the Prolific study by copying the survey link from Formbricks and pasting it into the Prolific study: - +![Copy the survey link](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/copy-survey-link.webp) + ### Step 5: Choose URL parameters for attribution To attribute responses to the correct participant, you need to add URL parameters to the Formbricks survey link. The parameters are `PROLIFIC_PID`, `STUDY_ID`, and `SESSION_ID`, exactly like the hidden fields you added. - +![Adding URL parameters to the survey](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/url-parameters.webp) ### Step 6: Update the Formbricks Redirect URL @@ -95,47 +65,30 @@ To ensure that participants are redirected back to Prolific after completing the Copy from Prolific: - +![Copy redirect URL](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/redirect-url.webp) Set it up as Redirect URL in the Response Options in Formbricks: - +![Add redirect URL to Formbricks](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/redirect-url-formbricks.webp) ### Step 7: Preview the study Preview the study using Prolific's [Preview-functionality](https://researcher-help.prolific.com/hc/en-gb/articles/360009222853-Previewing-your-study) - +![Preview study](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/preview-study.webp) Got to the success screen? Then you're ready to publish your study! - +![Preview complete](/images/xm-and-surveys/surveys/link-surveys/market-research-panel/preview-complete.webp) ### Step 8: Publish the study After you've published the study, you'll get the first responses within a few hours. -Prolific is a paid service. You need to fund your account to publish your study. + + Prolific is a paid service. You need to fund your account to publish your + study. + ### That's it! 🎉 diff --git a/apps/docs/app/link-surveys/pin-protected-surveys/page.mdx b/docs/xm-and-surveys/surveys/link-surveys/pin-protected-surveys.mdx similarity index 55% rename from apps/docs/app/link-surveys/pin-protected-surveys/page.mdx rename to docs/xm-and-surveys/surveys/link-surveys/pin-protected-surveys.mdx index 43e5cb4d1f..43dd5c057f 100644 --- a/apps/docs/app/link-surveys/pin-protected-surveys/page.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/pin-protected-surveys.mdx @@ -1,20 +1,9 @@ -import { MdxImage } from "@/components/mdx-image"; - -import StepFive from "./images/step-five.webp"; -import StepFour from "./images/step-four.webp"; -import StepOne from "./images/step-one.webp"; -import StepThree from "./images/step-three.webp"; -import StepTwo from "./images/step-two.webp"; - -export const metadata = { - title: "Protect Surveys with a Secure PIN", - description: - "Enhance the security and exclusivity of your surveys by requiring respondents to enter a secure 4 digit PIN to access your survey. Learn how to enable PIN protection and the benefits it offers.", -}; - -# PIN Protected Surveys - -To enhance the security and exclusivity of your surveys, you can require respondents to enter a secure 4 digit PIN to access your survey. This feature ensures that only participants who know the PIN can view and respond to the survey, making it ideal for targeted or confidential surveys. +--- +title: "PIN Protected Surveys" +description: + "Enhance the security and exclusivity of your surveys by requiring respondents to enter a secure 4 digit PIN to access your survey. Learn how to enable PIN protection and the benefits it offers." +icon: "lock" +--- ## **Enabling PIN Protection** @@ -23,58 +12,45 @@ PIN protection can be applied to your surveys easily through the survey editor. ### **Steps to Set Up PIN Protection** 1. **Open Settings in Survey Editor**: Navigate to your survey in the survey editor where you wish to enable PIN protection & click on Settings Tab. + 2. **Select Response Options**: Find and select **`Response Options`** to access settings related to survey responses. - -3. **Enable PIN Protection**: Find the option for "Protect Survey with a PIN" and activate it. You will be -prompted to enter a PIN that respondents must use to access the survey. +![Select Response Options](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-one.webp) + +1. **Enable PIN Protection**: Find the option for "Protect Survey with a PIN" and + activate it. You will be prompted to enter a PIN that respondents must use to access + the survey. ### **Setting the PIN** - -Enter the PIN you wish to use for your survey. Once set, this PIN will need to be entered by participants to -access the survey. Note that this can be changed anytime from here in the future. +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-two.webp) + +Enter the PIN you wish to use for your survey. Once set, this PIN will need to be +entered by participants to access the survey. Note that this can be changed anytime +from here in the future. ### **User Experience Upon Accessing the Survey** When a respondent attempts to access the survey, they are prompted to enter the PIN: - **PIN Entry Prompt**: A screen will appear asking the respondent to enter the PIN to proceed. This acts as the first gatekeeping step before survey access is granted. - + +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-three.webp) + - **Incorrect PIN Handling**: If an incorrect PIN is entered, the respondent will be informed and asked to try again, ensuring secure access to the survey. - + +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-four.webp) + - **Correct PIN**: On entering the correct PIN, the user access the survey & can fill it accordingly. - + +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-five.webp) ### **Benefits of PIN Protection** - **Enhanced Security**: Protects the survey from unauthorized access, ensuring that only participants with the PIN can enter. + - **Controlled Participation**: Enables you to restrict survey participation to a specific group, such as during a closed testing phase or confidential feedback gathering. + - **Prevents Unwanted Access**: Deters casual browsing and unauthorized attempts to view or complete the survey. ## **Use Cases** @@ -82,7 +58,9 @@ When a respondent attempts to access the survey, they are prompted to enter the PIN protection is particularly useful in situations where: - Confidential surveys are being conducted, such as internal company feedback or sensitive research studies. + - Surveys are designed for a specific event or group, and access needs to be controlled. + - You want to limit survey responses to participants who have been explicitly invited or have registered in advance. ## **Conclusion** diff --git a/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx b/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx new file mode 100644 index 0000000000..c1e689523d --- /dev/null +++ b/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx @@ -0,0 +1,45 @@ +--- +title: "Quickstart" +description: "Create your first link survey in under 5 minutes." +icon: "rocket" +--- + +Link Surveys make it easy for your users to give you feedback. They are a great way to get feedback from your users, without interrupting their workflow. This quickstart guide will show you how to create your first link survey in under 5 minutes. + +## Create a free Formbricks Cloud account + +While you can [self-host](/self-hosting/overview) Formbricks, the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and click through the onboarding. + +Choose one of the pre-created templates to get started. We’ll choose the **Product Market Fit** template for this quickstart guide. + +## Create your first survey + +When you click the template, you'll be taken to the survey editor. Here, you can edit the survey questions and settings. To keep it simple, we'll leave the questions as they are and go to the survey settings. + +Click on the **Settings** tab to edit the survey settings. + +## Configure your survey settings + +Formbricks packs a lot of useful functionality out of the box. You can: + +- Close the survey on a specidic date + +- After a number of response + +- Redirect users to a URL after they completed the survey + +- Protect survey with a Pin + +- ... and much more! + +## Style your survey + +Style your survey to your need. You can keep it simplistic or use animated backgrounds. You can change the main color and soon you'll be able to fully control the appearance of the survey. + +## Publish your survey + +Once you’re happy with the survey settings, hit **Publish** and you’ll be forwarded to the Summary Page. This is where you’ll find the responses to this survey. + +## Share your survey + +Congratulations! Your survey is now published and ready to be shared with your users. You can share the survey link via email, SMS, or any other channel. diff --git a/docs/xm-and-surveys/surveys/link-surveys/single-use-links.mdx b/docs/xm-and-surveys/surveys/link-surveys/single-use-links.mdx new file mode 100644 index 0000000000..b37fefde57 --- /dev/null +++ b/docs/xm-and-surveys/surveys/link-surveys/single-use-links.mdx @@ -0,0 +1,53 @@ +--- +title: "Single Use Links" +description: + "Make sure that each respondent only replies once with single use links." +icon: "link" +--- + +This guide will help you understand how to generate and use single-use links within our application. + +## Purpose + +- Single-use links (or one-time / disposable links) are URLs that grant access to a survey only once. + +- The primary purpose of single-use links is to assure that no respondent submits a survey twice. + + + Want to create up to 5,000 single-use links? Use our [API endpoint for + that.](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#c49ef758-a78a-4ef4-a282-262621151f08) + + +## Using Single-Use Links with Formbricks + +Using single-use links with Formbricks is quite straight-forward: + +1. In the survey settings, toggle "Single Use Link" on: + +![Single use survey settings](/images/xm-and-surveys/surveys/link-surveys/single-use-links/single-use-setting.webp) + + +2. When you publish your survey, the following modal will open: + +![Share modal with 7 single use links which can be regenerated](/images/xm-and-surveys/surveys/link-surveys/single-use-links/share-modal.webp) + +Here, you can copy and generate as many single-use links as you need. + +## URL Encryption + +You can encrypt single use URLs to assure information to be protected. To enable it, you have to set the correct environment variable: + +![Set the right env var to be able to enable encryption.](/images/xm-and-surveys/surveys/link-surveys/single-use-links/env-variable.webp) + +## Check suId of a submission + +You can find the suId of each submission in the submission meta data. To view it, simple hover over the Avatar: + +![View suId in the submission meta data.](/images/xm-and-surveys/surveys/link-surveys/single-use-links/metadata.webp) + + +### 'Link used' message + +You can customize the 'link used' messaging in the Survey Editor settings: + +![Adjust the message shown to people if a link was already used.](/images/xm-and-surveys/surveys/link-surveys/single-use-links/used-message.webp) diff --git a/apps/docs/app/link-surveys/source-tracking/page.mdx b/docs/xm-and-surveys/surveys/link-surveys/source-tracking.mdx similarity index 60% rename from apps/docs/app/link-surveys/source-tracking/page.mdx rename to docs/xm-and-surveys/surveys/link-surveys/source-tracking.mdx index 8995b95c49..324428b5ba 100644 --- a/apps/docs/app/link-surveys/source-tracking/page.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/source-tracking.mdx @@ -1,28 +1,15 @@ -import { MdxImage } from "@/components/mdx-image"; -import { ResponsiveVideo } from "@/components/responsive-video"; +--- +title: "Source Tracking" +description: "Track the source of your users in an easy & compliant way!" +icon: "display-chart-up" +--- -import ShareLink from "./share-link.webp"; -import ViewResponse from "./view-response.webp"; - -export const metadata = { - title: "Source Tracking", - description: "Track the source of your users in an easy & compliant way!", -}; - -#### Link Surveys - -# Source Tracking - -Understand the source a survey respondent comes from when responding to your survey - all while keeping data privacy standards high! +Understand the source a survey respondent comes from when responding to your survey – all while keeping data privacy standards high! Check out this video to learn more about source tracking in link surveys: -{/* Replace link below with our new link on Source Tracking */} + - ## Purpose @@ -33,42 +20,26 @@ Source tracking for link surveys is essential when you: ## Code Example - - - -```sh +```sh Example Source as Google https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google ``` - - - ## How it Works To track the source of users in your link surveys effectively, follow these steps: 1. **Generate Survey URL**: Create a Link Survey and get the sharable link. Append `?source=YourSouce` to the link to reference it with your campaigns and sources. - - - -```sh +```sh Example Source as Google https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google ``` - - - 2. **Collect Data**: When users access the survey through these links, the URL parameters will capture the source information from which they were shared. 3. **View Responses**: Use the collected source data to analyze where your survey respondents are coming from. You can hover over the user icon in the responses tab to see the source of the user. - +![View Source in Response](/images/xm-and-surveys/surveys/link-surveys/source-tracking/view-response.webp) + 4. **Analyse Data**: Download all the responses as a CSV/Excel and get access to the source information. This can provide valuable insights into your audience. diff --git a/apps/docs/app/link-surveys/start-at-question/page.mdx b/docs/xm-and-surveys/surveys/link-surveys/start-at-question.mdx similarity index 78% rename from apps/docs/app/link-surveys/start-at-question/page.mdx rename to docs/xm-and-surveys/surveys/link-surveys/start-at-question.mdx index e87e011935..5044b9882d 100644 --- a/apps/docs/app/link-surveys/start-at-question/page.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/start-at-question.mdx @@ -1,11 +1,9 @@ -export const metadata = { - title: "Start At Specific Question", - description: "Start a survey at a specific question using the URL to skip the initial questions.", -}; - -#### Link Surveys - -# Start At Specific Question +--- +title: "Start At Specific Question" +description: + "Start a survey at a specific question using the URL to skip the initial questions." +icon: "arrow-right" +--- You can start a survey at a specific question from the survey using the URL to skip the initial questions. This is useful when you want to link to a specific question from an external source or want to use the same survey in different parts of the user journey. @@ -16,7 +14,9 @@ You can start a survey at a specific question from the survey using the URL to s 2. Find the question you want to start at, click on **Show Advanced Settings**, and copy the **Question ID**. - Each question has a unique Question ID, which is used to identify it in the survey. You can use different Question IDs for multiple **startAt** points in the URL. + Each question has a unique Question ID, which is used to identify it in the + survey. You can use different Question IDs for multiple **startAt** points in + the URL. 3. Append `?startAt=question_id` to your survey's URL, replacing `question_id` with the copied Question ID. @@ -25,16 +25,10 @@ You can start a survey at a specific question from the survey using the URL to s ### Sample Link Survey URL with `startAt` - - - -```sh +```sh Example Link Survey URL with startAt configured https://formbricks.com/clny997dj087ho30fdzyf4nkl?startAt=bqd29m94l9k0hnc3azbrexl8 ``` - - - ## Use Cases - **Link to a specific question from an external source:** Use this feature to direct users to a specific question in your survey from emails, chatbots, or web pages, providing a seamless experience. diff --git a/apps/docs/app/link-surveys/verify-email-before-survey/page.mdx b/docs/xm-and-surveys/surveys/link-surveys/verify-email-before-survey.mdx similarity index 64% rename from apps/docs/app/link-surveys/verify-email-before-survey/page.mdx rename to docs/xm-and-surveys/surveys/link-surveys/verify-email-before-survey.mdx index 9674e71e0d..2a7747431d 100644 --- a/apps/docs/app/link-surveys/verify-email-before-survey/page.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/verify-email-before-survey.mdx @@ -1,20 +1,9 @@ -import { MdxImage } from "@/components/mdx-image"; - -import StepFive from "./images/step-five.webp"; -import StepFour from "./images/step-four.webp"; -import StepOne from "./images/step-one.webp"; -import StepSeven from "./images/step-seven.webp"; -import StepSix from "./images/step-six.webp"; -import StepThree from "./images/step-three.webp"; -import StepTwo from "./images/step-two.webp"; - -export const metadata = { - title: "Verifying Email Before Accessing Surveys", - description: - "Ensure the credibility of your survey participants and maintain high-quality data by requiring respondents to verify their email before they can view and respond to your Formbricks link surveys. Learn how to enable email verification and the benefits it offers.", -}; - -# **Verifying Email Before Accessing Surveys** +--- +title: "Verify Email" +description: + "Verify email before accessing surveys" +icon: "envelope" +--- To ensure the credibility of your survey participants and maintain high-quality data, you can require respondents to verify their email before they can view and respond to your Formbricks link surveys. This verification process helps confirm that only participants with valid email addresses can access the survey, enhancing data integrity. @@ -28,21 +17,12 @@ This feature, designed for link surveys, can be enabled or disabled directly fro 2. **Access Settings**: Click on the **`Settings`** tab next to the Questions & Styling tab. 3. **Select Response Options**: Find and select **`Response Options`** to access settings related to survey responses. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-one.webp) 4. **Activate Email Verification**: Find the "Verify Email Before Accessing Survey" option and use the toggle to activate email verification. Specify what details should be visible to the public when they access the survey. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-two.webp) + ### **User Experience Upon Accessing the Survey** @@ -50,39 +30,20 @@ When email verification is enabled, the following process unfolds for the user: 1. **Email Entry Prompt**: Upon accessing the survey link, the user is prompted to verify their email before they can proceed. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-three.webp) 2. **Preview Option**: A "Preview survey questions" option is available for those who are just browsing or curious about the survey content without completing it. This allows a non-interactive view of the survey. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-four.webp) 3. **Verification Process**: After entering their email, respondents receive an email containing a survey link, which they can use to access the survey. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-five.webp) + **4. Survey Access**: After verifying their email, respondents can access and respond to the survey. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-six.webp) ### **Benefits of Email Verification** @@ -94,12 +55,7 @@ When email verification is enabled, the following process unfolds for the user: In the Formbricks dashboard, the survey response card displays the verified email along with the responses attached to it, ensuring traceability and authenticity of the data collected. - +![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-seven.webp) ## **Use Cases** diff --git a/apps/docs/app/app-surveys/actions/page.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/actions.mdx similarity index 76% rename from apps/docs/app/app-surveys/actions/page.mdx rename to docs/xm-and-surveys/surveys/website-app-surveys/actions.mdx index 69002b8aa9..d5a76f8cc1 100644 --- a/apps/docs/app/app-surveys/actions/page.mdx +++ b/docs/xm-and-surveys/surveys/website-app-surveys/actions.mdx @@ -1,17 +1,8 @@ -import { MdxImage } from "@/components/mdx-image"; - -import I1 from "./images/i1.webp"; -import I2 from "./images/i2.webp"; - -export const metadata = { - title: "Using Actions in Formbricks", - description: - "Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.", -}; - -# Actions - -Actions are predefined events within your app that prompt Formbricks to display a survey when triggered. These are detected by the Formbricks widget, which then presents the appropriate survey based on your predefined settings. +--- +title: "Actions" +description: "Actions are predefined events within your app that prompt Formbricks to display a survey when triggered. These are detected by the Formbricks widget, which then presents the appropriate survey based on your predefined settings." +icon: "code" +--- ## **How Do Actions Work?** @@ -22,7 +13,9 @@ Actions in Formbricks App Surveys are deeply integrated with user activities wit Actions are invaluable for enhancing survey relevance and effectiveness: - **Personalized Engagement**: Surveys triggered by user actions ensure content is highly relevant and engaging, matching each user’s current context. + - **User Attributes**: By tying surveys to specific user attributes, such as activity levels or feature usage, you can customize the survey experience to reflect individual user profiles. + - **User Targeting**: Precise targeting based on user attributes ensures that surveys are shown only to users who meet certain criteria, enhancing the relevance and effectiveness of each survey. ## **Setting Up No-Code Actions** @@ -33,21 +26,11 @@ To add a No-Code Action: 1. Visit the Formbricks Dashboard & switch to the Actions tab: - +![setup checklist ui of survey popup for app surveys](/images/xm-and-surveys/surveys/website-app-surveys/actions/i1.webp) -2. Now click on “Add Action” +1. Now click on “Add Action” - +![add action](/images/xm-and-surveys/surveys/website-app-surveys/actions/i2.webp) Here are four types of No-Code actions you can set up: @@ -73,19 +56,21 @@ This action is triggered when a user scrolls through 50% of a page within your a This action is triggered when a user visits a specific page within your application. You can define the URL match conditions as follows: - You can combine the url filters with any of the no-code actions to trigger the survey based on the URL match conditions. ### **URL Match Conditions** - **exactMatch**: Triggers the action when the URL exactly matches the specified string. -- **contains**: Activates when the URL contains the specified substring. -- **startsWith**: Fires when the URL starts with the specified string. -- **endsWith**: Executes when the URL ends with the specified string. -- **notMatch**: Triggers when the URL does not match the specified condition. -- **notContains**: Activates when the URL does not contain the specified substring. - +- **contains**: Activates when the URL contains the specified substring. + +- **startsWith**: Fires when the URL starts with the specified string. + +- **endsWith**: Executes when the URL ends with the specified string. + +- **notMatch**: Triggers when the URL does not match the specified condition. + +- **notContains**: Activates when the URL does not contain the specified substring. ## **Setting Up Code Actions** @@ -96,26 +81,16 @@ For more granular control, you can implement actions directly in your codebase: 2. **Track an Action**: Use formbricks.track() to send an action event to Formbricks. - - +```javascript Track an action formbricks.track("Action Name"); -```javascript -formbricks.track("Action Name"); ``` - - Here is an example of how to fire an action when a user clicks a button: - - -```javascript +```javascript Track Button Click const handleClick = () => { formbricks.track("Button Clicked"); }; return ; ``` - - - diff --git a/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting.mdx new file mode 100644 index 0000000000..46d6c35766 --- /dev/null +++ b/docs/xm-and-surveys/surveys/website-app-surveys/advanced-targeting.mdx @@ -0,0 +1,28 @@ +--- +title: "Advanced Targeting" +description: "Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, device type, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code." +icon: "bullseye" +--- + + +## How to setup Advanced Targeting + + + Advanced Targeting is only available on the Pro plan! + + +* On the Formbricks dashboard, click on **People** tab from the top navigation bar. + +* Switch to the **Segments** tab & click on **Create Segment**. + +* Give your segment a title & a description to help you remember what this segment is about. + +* Now click on the **Add Filter** button to add a filter. You can filter based on user attributes, other segments, devices, and more. + +* To group a set of filters together, click on the Three Dots icon on the right side of the filter and click on **Create Group**. + +* Try playing around with different filters & conditions that we have provided to see how the segment size changes. + +* Once you are happy with the segment, click on **Save Segment**. + +* Now, when you create a survey, you can select this segment to target your survey to. \ No newline at end of file diff --git a/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx new file mode 100644 index 0000000000..d08e0753b5 --- /dev/null +++ b/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx @@ -0,0 +1,378 @@ +--- +title: "Framework Guides" +icon: "book" +--- + +Integrate the **Formbricks App Survey SDK** into your app using multiple options. Explore the available choices below. If you need something else, open a [**GitHub Discussion**](https://github.com/formbricks/formbricks/discussions), and we’ll be happy to help! + + + + [All you need to do is add three lines of code to your HTML script, and that's it!](https://formbricks.com/docs/app-surveys/framework-guides#html) + + + + [Load our JavaScript library with your environment ID, and you're ready to + go!](https://formbricks.com/docs/app-surveys/framework-guides#react-js) + + + + [Natively add us to your Next.js project, with support for both App and Pages + project + structure.](https://formbricks.com/docs/app-surveys/framework-guides#next-js) + + + + Learn how to use Formbricks' React Native SDK to integrate your surveys into + React Native applications. + + + + [Easily integrate our SDK with your React Native app for seamless survey support.](https://formbricks.com/docs/app-surveys/framework-guides#react-native) + + + +## Prerequisites + +Before getting started, make sure you have: + +- A running web application with user authentication in your chosen framework. + +- A Formbricks account with your **environment ID** and **API host**, available in the **Setup Checklist** under **Settings**. + +## HTML + +All you need to do is copy a ` + +``` + +### Required Customizations + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------- | +| environment-id | string | Formbricks Environment ID. | +| api-host | string | URL of the hosted Formbricks instance. | + +Now, visit the [Validate Your Setup](#validate-your-setup) section to verify your setup! + +## React.js + +Install the Formbricks SDK using one of the following package managers: `npm`, `pnpm`, or `yarn`. +Note that **`zod`** is required as a peer dependency and must also be installed in your project. + +```javascript npm +npm install @formbricks/js zod +``` + +```bash pnpm +pnpm add @formbricks/js zod +``` + +```bash yarn +yarn add @formbricks/js zod +``` + +Update your `App.js/ts` file to initialize Formbricks. + +```javascript src/App.js +// other imports +import formbricks from "@formbricks/js"; + +if (typeof window !== "undefined") { + formbricks.init({ + environmentId: "", + apiHost: "", + userId: "", //optional + }); +} + +function App() { + // your own app +} + +export default App; +``` + +## Required Customizations + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------- | +| environment-id | string | Formbricks Environment ID. | +| api-host | string | URL of the hosted Formbricks instance. | + +Now, visit the [Validate Your Setup](#validate-your-setup) section to verify your setup! + +## Next.js + +Next.js projects use either the **App Directory** or the **Pages Directory**. Since the Formbricks SDK runs on the client side, follow these steps based on your setup: + +- **App Directory**: Create a new component in `app/formbricks.tsx` and call it in `app/layout.tsx`. + +- **Pages Directory**: Initialize Formbricks directly in `_app.tsx`. + +Code snippets for the integration for both conventions are provided to further assist you. + +```bash npm +npm install @formbricks/js zod +``` + +```bash pnpm +pnpm add @formbricks/js zod +``` + +```bash yarn +yarn add @formbricks/js zod +``` + +### App directory + +```typescript app/formbricks.tsx +"use client"; + +import { usePathname, useSearchParams } from "next/navigation"; +import { useEffect } from "react"; +import formbricks from "@formbricks/js"; + +export default function FormbricksProvider() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + formbricks.init({ + environmentId: "", + apiHost: "", + userId: "", //optional + }); + }, []); + + useEffect(() => { + formbricks?.registerRouteChange(); + }, [pathname, searchParams]); + + return null; +} +``` + +```typescript app/layout.tsx +// other imports +import FormbricksProvider from "./formbricks"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + ); +} +``` + +### Pages directory + +```javascript src/pages/_app.tsx +// other import +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import formbricks from "@formbricks/js"; + +if (typeof window !== "undefined") { + formbricks.init({ + environmentId: "", + apiHost: "", + userId: "", //optional + }); +} + +export default function App({ Component, pageProps }: AppProps) { + const router = useRouter(); + + useEffect(() => { + // Connect next.js router to Formbricks + const handleRouteChange = formbricks?.registerRouteChange; + router.events.on("routeChangeComplete", handleRouteChange); + + return () => { + router.events.off("routeChangeComplete", handleRouteChange); + }; + }, []); + return ; +} +``` + +### Required Customizations + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------- | +| environment-id | string | Formbricks Environment ID. | +| api-host | string | URL of the hosted Formbricks instance. | + +First, initialize the Formbricks SDK to run only on the client side. To track page changes, register the route change event with the Next.js router. + +Next, go to the [**Validate Your Setup**](#validate-your-setup) section to verify your setup! + +## Vue.js + +Integrating the Formbricks SDK with Vue.js is easy. We’ll ensure the SDK is only loaded and used on the client side, as it’s not meant for server-side use. + +```bash npm +npm install @formbricks/js +``` + +```bash pnpm +pnpm add @formbricks/js +``` + +```bash yarn +yarn add @formbricks/js +``` + +```javascript src/formbricks.js +import formbricks from "@formbricks/js"; + +if (typeof window !== "undefined") { + formbricks.init({ + environmentId: "", + apiHost: "", + userId: "", //optional + }); +} + +export default formbricks; +``` + +```javascript src/main.js +// other imports +import formbricks from "@/formbricks"; + +const app = createApp(App); + +app.use(router); + +app.mount("#app"); + +router.afterEach((to, from) => { + if (typeof formbricks !== "undefined") { + formbricks.registerRouteChange(); + } +}); +``` + +### Required Customizations + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------- | +| environment-id | string | Formbricks Environment ID. | +| api-host | string | URL of the hosted Formbricks instance. | + +Now, visit the [Validate Your Setup](#validate-your-setup) section to verify your setup! + +## React Native + +Install the Formbricks React Native SDK using one of the package managers, i.e., npm, pnpm, or yarn. + +```bash npm +npm install @formbricks/react-native +``` + +```bash pnpm +pnpm add @formbricks/react-native +``` + +```bash yarn +yarn add @formbricks/react-native +``` + +Now, update your `App.js/App.tsx` file to initialize Formbricks: + +```javascript src/App.js +// other imports +import Formbricks from "@formbricks/react-native"; + +const config = { + environmentId: "", + apiHost: "", + userId: "", // optional +}; + +export default function App() { + return ( + <> + {/* Your app content */} + + + ); +} +``` + +## Required Customizations + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------- | +| environment-id | string | Formbricks Environment ID. | +| api-host | string | URL of the hosted Formbricks instance. | + +## Validate your setup + +Once you’ve completed the steps above, validate your setup by checking the Setup Checklist in the Settings. The widget status indicator should change from this: + +![first validate](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738122589/image_ecaovs.jpg) +To this: + +![second validate](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738122750/image_ymaenn.jpg) + +## Debugging Formbricks Integration + +Enabling debug mode in your browser can help troubleshoot issues with Formbricks. Here’s how to activate it and what to look for in the logs. + +### Activate Debug Mode + +To enable debug mode, add `?formbricksDebug=true` to your app’s URL (e.g., [`https://example.com?formbricksDebug=true)`](). + +#### View Debug Logs + +1. Open your browser’s developer tools: + +- **Google Chrome/Edge**: Press `F12` or right-click and select "**Inspect" > "Console**". + +- **Firefox**: Press `F12` or right-click and select "**Inspect Element" > "Console**". + +- **Safari**: Press `Option + Command + C` to open developer tools and go to the "**Console**" tab. + +#### Common Use Cases + +Debug mode is helpful for: + +- Verifying Formbricks initialization. + +- Identifying issues with survey triggers. + +- Troubleshooting unexpected behavior. + +#### Debug Log Messages + +Logs provide insights into: + +- API calls and responses. + +- Survey triggers and form interactions. + +- Initialization errors. + +If you're stuck, visit [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions) for assistance. + +``` + +``` diff --git a/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx new file mode 100644 index 0000000000..09ba407551 --- /dev/null +++ b/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx @@ -0,0 +1,46 @@ +--- +title: "Quickstart" +description: "App surveys deliver 6–10x higher conversion rates compared to email surveys. If you are new to Formbricks, follow the steps in this guide to launch a survey in your web or mobile app (React Native) within 10–15 minutes." +icon: "rocket" +--- + +* **Create a Formbricks Cloud Account**: While you can [self-host](/self-hosting) Formbricks, the fastest and easiest way to get started is with the free Cloud plan. Simply [sign up](https://app.formbricks.com/auth/signup) here, follow the onboarding steps, and choose the "Formbricks Surveys” option.![what are you here for](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738113228/image_d11uiy.png) + +* **Choose your project channel**: Select the channel where you want to create your project. You can create both app and link surveys from any channel, but for onboarding, select between app surveys or the public website option. Once selected, you'll be prompted to connect your app/website to Formbricks. + +![three options](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738113543/image_ndinwq.png) + +* **Connect your App/Website**: During onboarding, you'll be prompted to connect your app or website. You'll receive a code snippet for HTML and an npm package to embed in your app. + +![connect product to formbricks](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738114328/image_tv0jmj.png) + +* Paste the code snippet in your app and reload the page. You should now see a message being displayed that your app or website is now connected with Formbricks. + +![congrats](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738114793/image_lij7kw.png) +Onboarding is complete! Now let’s create our first survey as you should see templates to choose from after clicking on **Next**: + +![first survey](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738115025/image_yyuuzn.jpg) + +* **Create your first survey**: To be able to see a survey in your app, you need to create one. We’ll choose one of the templates and head over to the survey settings: + +Pick the Survey Type as **App Survey**. + +![app survey](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738115200/image_zxi8lw.png) + +* **Set Trigger for the Survey**: Scroll to **Survey Trigger**, click **+ Add Action**, and select **New Session**. This ensures the survey appears when the Formbricks Widget detects a new user session. + +![add action](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738115544/image_hj6hbf.jpg) + +* **Set Recontact Options for Debugging**: Adjust the **Recontact Options** to test the survey more easily. By default, surveys appear only once per user to prevent fatigue. + + + Please change this setting after testing your survey to avoid user fatigue. + + +![debugging](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738115751/image_rizwle.jpg) + +* Click **Publish** to proceed to the **Summary Page**, where you can view survey responses. + +![published](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738116264/image_ktcgl7.jpg)We offer framework guides for various frontend technologies. Check out the [**App Survey Framework Guides**](/xm-and-surveys/surveys/website-app-surveys/framework-guides) to set up your app survey. + +Need help? Join us in [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions), and we’ll be happy to assist! \ No newline at end of file diff --git a/apps/docs/app/app-surveys/recontact/page.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/recontact.mdx similarity index 59% rename from apps/docs/app/app-surveys/recontact/page.mdx rename to docs/xm-and-surveys/surveys/website-app-surveys/recontact.mdx index 8bb45df6ca..197a6ac495 100644 --- a/apps/docs/app/app-surveys/recontact/page.mdx +++ b/docs/xm-and-surveys/surveys/website-app-surveys/recontact.mdx @@ -1,31 +1,25 @@ -import { MdxImage } from "@/components/mdx-image"; - -import AppSurvey from "./app-survey.webp"; -import GlobalWaitTime from "./global-wait-time.webp"; -import IgnoreWaitTime from "./ignore-wait-time.webp"; -import SurveyRecontact from "./survey-recontact.webp"; - -export const metadata = { - title: "Recontact Options in Formbricks: When to show a survey again to a user", - description: - "Explore how to configure Recontact options in Formbricks to control the frequency of survey exposure to users, ensuring effective feedback collection without compromising user experience.", -}; - -# Recontact Options - -Recontact options in Formbricks enable you to manage how often and under what conditions a survey is shown to a user. This feature is crucial for balancing effective feedback collection with a positive user experience by preventing survey fatigue. +--- +title: "Recontact Options" +description: "Recontact options in Formbricks enable you to manage how often and under what conditions a survey is shown to a user. This feature is crucial for balancing effective feedback collection with a positive user experience by preventing survey fatigue." +icon: "user-check" +--- ## When do Recontact Options come into play? Recontact options are the last layer of the logic that determines if a survey is shown to the current user. The logic goes as follows: 1. Targeting: Does the current user targeted to fill out this survey? If yes... + 2. Trigger: Is the survey triggered? If yes... + 3. **Recontact Options:** Should the survey be shown (again) to this user? That's dependent on: - Did the user see any survey recently (meaning, has Global Waiting Time passed)? + - Did the user see this specific survey already? + - How many times did the user see this specific survey already? + - Has the user already responded to this survey? As you can see, there are a lot of different cases to cover. Let's have a closer look 👇 @@ -37,30 +31,24 @@ As you can see, there are a lot of different cases to cover. Let's have a closer You can adjust the default behavior by modifying the Recontact Options for each survey in the settings: 1. Open the Survey Editor for the survey you want to see & modify the Recontact Options for. + 2. Select the Settings Tab. + 3. Ensure your Survey type is set to **App Survey**. - +![Choose Survey Type as App Survey](/images/xm-and-surveys/surveys/website-app-surveys/recontact/app-survey.webp) -4. Scroll down to the Recontact Options section. +1. Scroll down to the Recontact Options section. Available Recontact Options include: - **Show only once**: (default) Displays the survey a single time, regardless of whether it was completed. -- **Until they Submit a Response**: If tareting matches and trigger fires, Formbricks keeps showing the survey until the user submits a response. -- **Keep Showing while Conditions Match**: Always shows the survey while specific conditions are met. Useful for continuous feedback collection, such as in [Docs Feedback Survey](/best-practices/docs-feedback) or the [Feedback Box](/best-practices/feedback-box). - +- **Until they Submit a Response**: If tareting matches and trigger fires, Formbricks keeps showing the survey until the user submits a response. + +- **Keep Showing while Conditions Match**: Always shows the survey while specific conditions are met. Useful for continuous feedback collection, such as in [Docs Feedback Survey](/xm-and-surveys/xm/best-practices/docs-feedback) or the [Feedback Box](/xm-and-surveys/xm/best-practices/feedback-box). + +![Choose Recontanct Options for the Survey](/images/xm-and-surveys/surveys/website-app-surveys/recontact/survey-recontact.webp) ## Project-wide Global Waiting Time @@ -71,35 +59,33 @@ The Global Waiting Time is a universal blocker to make sure that no user sees to To adjust the Global Waiting Time: 1. Visit Formbricks Settings + 2. Go to Project Settings + 3. Find the **Recontact Waiting Time** section + 4. Modify the interval (in days) as needed. - +![Formbricks Project-Wide Wait Time](/images/xm-and-surveys/surveys/website-app-surveys/recontact/global-wait-time.webp) ## Overriding Global Waiting Time for a Specific Survey For specific surveys, you may need to override the default waiting time. Below is how you can do that: 1. In the Survey Editor, access the Settings Tab. + 2. Find the Ignore Waiting Time between Surveys toggle under Recontact Options. + 3. Enable this toggle to bypass the global setting. + 4. Set a custom recontact period: + - **Always Show Survey**: Displays the survey whenever triggered, ignoring the waiting time. + - **Wait `X` days before showing this survey again**: Sets a specific interval before the survey can be shown again. - +![Ignore Global Waiting Time for a Specific Survey](/images/xm-and-surveys/surveys/website-app-surveys/recontact/ignore-wait-time.webp) --- -Still struggling or something not working as expected? [Join us in Github Discussions](https://github.com/formbricks/formbricks/discussions) and we'd be glad to assist you! +Still struggling or is something not working as expected? [Join us in Github Discussions](https://github.com/formbricks/formbricks/discussions) and we'd be glad to assist you! diff --git a/apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/page.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx similarity index 72% rename from apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/page.mdx rename to docs/xm-and-surveys/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx index 0276f02361..bccc9bd615 100644 --- a/apps/docs/app/[survey-type]/global/show-survey-to-percent-of-users/page.mdx +++ b/docs/xm-and-surveys/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx @@ -1,20 +1,14 @@ -import { MdxImage } from "@/components/mdx-image"; - -import StepOne from "./images/step-one.webp"; -import StepTwo from "./images/step-two.webp"; - -export const metadata = { - title: "Show Survey to % of Users", - description: - "To target specific segments of your audience or manage survey exposure, Formbricks allows you to display surveys to only a percentage of your targeted users.", -}; - -# Show Survey to % of Users +--- +title: "Show Survey to % of Users" +description: "Formbricks allows you to display surveys to only a percentage of your targeted users." +icon: "percent" +--- To target specific segments of your audience or manage survey exposure, Formbricks allows you to display surveys to only a percentage of your targeted users. - This feature is applicable for website surveys and app surveys, where managing participant engagement and response volume is crucial. + This feature is applicable for website surveys and app surveys, where managing + participant engagement and response volume is crucial. ## **Enabling Percentage-Based Visibility** @@ -24,37 +18,29 @@ Set up this feature to control how many users see your survey, using a simple sl ### **Steps to Set Up Percentage-Based Visibility** 1. **Open Survey Editor**: Navigate to the survey where you wish to configure visibility settings & click on Edit. + 2. **Access Settings**: In the Survey Editor, click on the **`Settings`** tab + 3. **Navigate to Survey Trigger Options**: - {" "} - - +![Survey Trigger Options](/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp) - Select **`Survey Trigger`** from the menu options. + - Choose **`Survey Display Settings`** to access visibility controls. 4. **Adjust User Visibility Percentage**: - Find the **`Show Survey to % of Users`** toggle. Enable it. + - Enter the desired percentage (from 0.01% to 100%) of users to whom the survey will be shown. - {" "} - - +![Choose a link survey template](/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp) - Please note that this feature operates based on mathematical probabilistic functions, which may result in slight variations in the exact percentage of users who see the survey. + Please note that this feature operates based on mathematical probabilistic + functions, which may result in slight variations in the exact percentage of + users who see the survey. ### **Example Usage** @@ -66,7 +52,9 @@ For instance, if you want to expose your survey to only half of your targeted us It's effective for: - **A/B Testing**: Compare different user experiences by showing different surveys or survey variations to different segments of your audience. + - **Gradual Rollouts**: Gradually introduce a new survey to a portion of your users to monitor initial responses and adjust based on feedback before full deployment. + - **Load Management**: Manage server load and response processing by limiting the number of responses collected at any given time. ## **Conclusion** diff --git a/docs/xm-and-surveys/surveys/website-app-surveys/user-identification.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/user-identification.mdx new file mode 100644 index 0000000000..7c6bc0898f --- /dev/null +++ b/docs/xm-and-surveys/surveys/website-app-surveys/user-identification.mdx @@ -0,0 +1,83 @@ +--- +title: "User Identification" +description: "User Identification helps you to not only segment your users but also to see more information about the user who responded to a survey. This helps you to target surveys to specific user segments and see more information about the user who responded to a survey." +icon: "user" +--- + +### Understanding Identified vs Unidentified Users + +In Formbricks, understanding the difference between identified and unidentified users is crucial for effective survey segmentation and targeted feedback collection. + +| Feature | Unidentified Users | Identified Users | +| -------------------------------------------------------- | ------------------ | ---------------- | +| Show surveys based on **trigger** actions | ✅ | ✅ | +| Set **recontact details** to avoid survey fatique | ✅ | ✅ | +| Target a subset of users using **attributes & segments** | ❌ | ✅ | +| Collect **user information** in Formbricks | ❌ | ✅ | +| Track **custom attributes** | ❌ | ✅ | +| Counts towards your **monthly tacked user (MTU)** limit | ❌ | ✅ | +| Recommended for **public-facing websites** | ✅ | ❌ | +| Recommended for **web apps after login** | ❌ | ✅ | + +## Identified Users + +Identified users are those for whom specific information has been set, notably the userId. This identification allows for more precise targeting of surveys and a deeper understanding of the feedback provided. When enabled, all information specified by you and all actions are sent to Formbricks. + +This method is recommended for applications where users are required to log in and will often return. + +### Setting User ID + +To enable user identification, set the `userId` in the `init()` call of Formbricks. The user will show up in the Formbricks dashboard only if the `userId` is set. Use a unique string, like a database ID or a unique email address. You can also anonymize the identifier, as long as it is unique for each user. + +```javascript +formbricks.init({ + environmentId: "", + apiHost: "", + userId: "", +}); +``` + +### Enhanced Initialization with User Attributes + +Set user attributes in Formbricks during initialization along with the `userId`. + + ```javascript Enhanced Initialization with User Attributes + formbricks.init({ + environmentId: "", + apiHost: "", + userId: "", + attributes: { + // your custom attributes + Plan: "premium", + }, + }); + ``` + +## Setting Custom User Attributes + +Use the `setAttribute` function to set custom attributes for the user (e.g., name, plan). + + + **Note**: the number of different attribute classes (e.g., "Plan," + "First Name," etc.) is currently limited to 150 attributes per environment. + + +```javascript Setting Custom Attributes +formbricks.setAttribute("Plan","free"); + ``` + +The `setAttribute` function works like this: + + +```javascript Setting Custom Attributes +formbricks.setAttribute("attribute_key", "attribute_value"); +``` + + +### Logging Out Users + +When a user logs out of your webpage, also log them out of Formbricks to prevent activity from being linked to the wrong user. Use the logout function: + +```javascript Logging out User +formbricks.logout(); +``` diff --git a/docs/xm-and-surveys/xm/best-practices/cancel-subscription.mdx b/docs/xm-and-surveys/xm/best-practices/cancel-subscription.mdx new file mode 100644 index 0000000000..fc6ae68d10 --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/cancel-subscription.mdx @@ -0,0 +1,112 @@ +--- +title: "Learn from Churn" +description: "Mastering Churn Surveys with Formbricks | Essential Tips & Steps" +icon: "chart-line-down" +--- + +Churn is hard, but can teach you a lot. Whenever a user decides that your product isn’t worth it anymore, you have a unique opportunity to get deep insights. These insights are pure gold to reduce churn. + +## Purpose + +The Churn Survey is among the most effective ways to identify weaknesses in your offering. People were willing to pay but now are not anymore: What changed? Let’s find out! + +## Formbricks Approach + +* Ask at exactly the right point in time + +* Follow-up to prevent bad reviews + +* Coming soon: Make survey mandatory + +## Overview + +To run the Churn Survey in your app you want to proceed as follows: + +1. Create new Churn Survey at [app.formbricks.com](https://app.formbricks.com/) + +2. Set up the user action to display survey at right point in time + +3. Choose correct recontact options to never miss a feedback + +4. Prevent that churn! + + + ### Formbricks Widget running? We assume that you have already installed the + + Formbricks Widget in your web app. It’s required to display messages and + surveys in your app. If not, please follow the [Quick Start Guide (takes + 15mins max.)](/xm-and-surveys/surveys/website-app-surveys/quickstart) + + +### 1. Create new Churn Survey + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Click on "Create Survey" and choose the template “Churn Survey”: + +![Create churn survey by template](/images/xm-and-surveys/xm/best-practices/cancel-subscription/create-cancel-flow.webp) + +### 2. Update questions (if you like) + +You’re free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊 + +![Change text content](/images/xm-and-surveys/xm/best-practices/cancel-subscription/change-text.webp) + +*Want to change the button color? You can do so in the project settings.* + +Save, and move over to the “Audience” tab. + +### 3. Pre-segment your audience + +In this case, you don’t really need to pre-segment your audience. You likely want to ask everyone who hits the “Cancel subscription” button. + +### 4. Set up a trigger + +To create the trigger for your Churn Survey, you have three options to choose from: + +* **Trigger by Inner Text:** You likely have a “Cancel Subscription” button in your app. You can setup a user Action with the according `Inner Text` to trigger the survey, like so: + +![Set the trigger by inner Text](/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-inner-text.webp) + +* **Trigger by CSS Selector:** In case you have more than one button saying “Cancel Subscription” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“cancel-subscription”` and set your user action up like so: + +![Set the trigger by CSS Selector](/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-css-selector.webp) + +* **Trigger by page view filters:** Lastly, you could also display your survey on a subpage “/subscription-cancelled” where you forward users once they cancelled the trial subscription. You can then create a user Action with the type `Page View` and add select `Limit to specific pages` to add url filters, with the following settings: + +![Set the trigger by page URL](/images/xm-and-surveys/xm/best-practices/cancel-subscription/trigger-page-url.webp) + +Whenever a user visits this page, matches the filter conditions above and the recontact options (below) the survey will be displayed ✅ + +Here is our complete [Actions manual](/xm-and-surveys/surveys/website-app-surveys/actions/) covering [No-Code](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions) and [Code](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions) Actions. + + + Pre-churn flow coming soon We’re currently building full-screen survey + pop-ups. You’ll be able to prevent users from closing the survey unless they + respond to it. It’s certainly debatable if you want that but you could force + them to click through the survey before letting them cancel 🤷 + + +### 5. Select Action in the “When to ask” card + +![Select feedback button action](/images/xm-and-surveys/xm/best-practices/cancel-subscription/select-action.webp) + +### 6. Last step: Set Recontact Options correctly + +Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you milk these super valuable insights. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days: + +![Set recontact options](/images/xm-and-surveys/xm/best-practices/cancel-subscription/recontact-options.webp) + +These settings make sure the survey is always displayed, when a user wants to Cancel their subscription. + +### 7. Congrats! You’re ready to publish your survey 💃 + +![Publish survey](/images/xm-and-surveys/xm/best-practices/cancel-subscription/publish-survey.webp) + + + Formbricks Widget running? You need to have the Formbricks Widget installed + to display the Churn Survey in your app. Please follow [this tutorial (Step 4 + onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. + + +### **Get those insights! 🎉** \ No newline at end of file diff --git a/docs/xm-and-surveys/xm/best-practices/contact-form.mdx b/docs/xm-and-surveys/xm/best-practices/contact-form.mdx new file mode 100644 index 0000000000..88e8f5953a --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/contact-form.mdx @@ -0,0 +1,157 @@ +--- +title: "Contact form" +icon: "address-book" +description: "A step-by-step guide to creating a contact form using Formbricks." +--- + +Welcome to this comprehensive guide on creating a contact form using Formbricks. Whether you're just starting out or you're a seasoned developer, this tutorial will walk you through every step of building an engaging and effective contact form. + +## What We’ll Build + +By the end of this tutorial, you'll have created a simple contact form featuring: + +- A welcoming introduction. + +- Fields for collecting the user's name and email. + +- A question to find out why they’re contacting you. + +- A message field for users to share their queries. + +### Setting Up Your Form + +First, let's lay the groundwork for your form: + +- Head to the Surveys page and click on **New Survey**. + +- Select **Start from Scratch** to create a new form. + +- In the form editor, click the three dots next to a question, then select **Change Question Type** and choose **Statement (Call to Action)**. + +![Toggle button for Statement (Call to Action)](/images/xm-and-surveys/xm/best-practices/contact-form/welcome1.webp) + +- Add a welcoming statement to greet your users and explain the form's purpose. + +- Personalize the greeting to make it inviting and encourage engagement. + + + A warm welcome sets the tone for your form. Make it friendly to encourage + users to participate. + + +### Adding the Name Field + +Next, let's capture the user's name: + +- Click **Add Question**. + +![Adding a question in Formbricks](/images/xm-and-surveys/xm/best-practices/contact-form/add-question.webp) + +- Enter the prompts for the name field. + +- Turn off the **Long Answer** option at the bottom right. + +- Adjust any **settings**, such as making the field required. + +![Name field configuration](/images/xm-and-surveys/xm/best-practices/contact-form/name-field.webp) + +### Adding the Email Field + +Now, let’s add a field to collect the user's email address: + +- Click **Add Question** again. + +- Select Email as the input type. + +- Enter a prompt for the email field. + +![Email field configuration](/images/xm-and-surveys/xm/best-practices/contact-form/email-field.webp) + +### Adding a Reason for Contact + +Let’s now understand why the user is contacting you: + +- Click **Add Question** once again. + +- Select **Change Question** Type and choose **Single Select**. + +- Add the question "Why are you contacting us today?" + +![Single Select question configuration](/images/xm-and-surveys/xm/best-practices/contact-form/single-select-questionare.webp) + + + Predefined options help categorize inquiries, making it easier for you to + respond appropriately. + + +- Add options like "General Inquiry," "Support," and "Feedback." + +![Single Select question configuration](/images/xm-and-surveys/xm/best-practices/contact-form/query-form.webp) + +### Adding a Message Field + +Finally, let’s provide a space for the user’s message: + +- Click **Add Question** for the last time. + +- Add the question: "Your Message." + +- Set the placeholder text to something like "Please write your message here." + +![Message field configuration](/images/xm-and-surveys/xm/best-practices/contact-form/message-field.webp) + +- Consider setting a minimum character count to ensure detailed messages. + +### Finalizing Your Form + +Once your form is complete, follow these final steps: + +- Review and rearrange the questions if necessary. + +- Test the form by filling it out as a user. + +- Customize the **Thank You** message for submissions. + +- Publish the form to get a shareable link. + +- Enable submission notifications: + + * Go to your Formbricks account settings. + + * Verify your email address. + + * Ensure that **Survey** notifications are enabled. + +### Integrating the Contact Form into Your Website + +After publishing the form, follow these steps to integrate it into your site: + +- **Copy the Shareable Link** + +![Embed Image configuration](/images/xm-and-surveys/xm/best-practices/contact-form/embed.webp) + + * Find your form in the Formbricks dashboard, and click Share. + + * Select Embed in a Web Page. + +- **Embed the Code** + + * Copy the provided code and paste it into your website where you want the form to appear. + + + Note: There is an options toggle button called "Embed Mode." When enabled, it + updates the `src` to `"?embed=true"` and displays your survey in a minimalist + design, removing padding and background for a cleaner look. + + +- **Test the Integration** + + * Check if the form displays correctly on your site. + + * Submit a test entry to ensure everything works and notifications are received. + +## Conclusion + +Congratulations! You’ve successfully created and integrated a professional contact form using Formbricks. This form will help you collect valuable information from your visitors in an efficient, user-friendly way. + +A great contact form strikes the balance between collecting necessary details and being simple enough to encourage submissions. **You’ve achieved just that!** \ No newline at end of file diff --git a/apps/docs/app/best-practices/docs-feedback/page.mdx b/docs/xm-and-surveys/xm/best-practices/docs-feedback.mdx similarity index 67% rename from apps/docs/app/best-practices/docs-feedback/page.mdx rename to docs/xm-and-surveys/xm/best-practices/docs-feedback.mdx index 18bace87c1..0bb487a586 100644 --- a/apps/docs/app/best-practices/docs-feedback/page.mdx +++ b/docs/xm-and-surveys/xm/best-practices/docs-feedback.mdx @@ -1,135 +1,101 @@ -import { MdxImage } from "@/components/mdx-image"; +--- +title: "Docs Feedback" +icon: "comment" +description: "A step-by-step guide to getting feedback on your Documentation with Formbricks" +--- -import AddAction from "./add-action.webp"; -import ChangeId from "./change-id.webp"; -import CopyIds from "./copy-ids.webp"; -import DocsNavi from "./docs-navi.webp"; -import DocsTemplate from "./docs-template.webp"; -import SelectAction from "./select-action.webp"; -import SurveyTrigger from "./survey-trigger.webp"; -import SwitchToDev from "./switch-to-dev.webp"; - -export const metadata = { - title: - "Integrate Docs Feedback in Your Website: A Step-by-Step Guide on getting feedback on your Documentation with Formbricks", - description: - "Learn the step-by-step process to effectively measure the clarity of your documentation using Formbricks Cloud. Dive into best practices, setting up cloud environments, integrating feedback widgets on your frontend, and connecting to the Formbricks API for a seamless user experience.", -}; - -#### Best Practices - -# Docs Feedback Docs Feedback allows you to measure how clear your documentation is. ## Purpose -Unlike yourself, your users don't spend 5-7 days per week thinking about your product. To fight the "Curse of Knowledge" you have to measure how clear your docs are. +Your users don’t spend as much time thinking about your product as you do. To fight the "Curse of Knowledge" you have to measure how clear your docs are. ## Installation To get this running, you'll need a bit of time. Here are the steps we're going through: -1. Set up Formbricks Cloud -2. Build the frontend -3. Connect to API -4. Test +- Set up Formbricks Cloud + +- Build the frontend + +- Connect to API + +- Test ## 1. Setting up Formbricks Cloud -1. To get started, create an account for the [Formbricks Cloud](https://app.formbricks.com/auth/signup). +- Create a [Formbricks Cloud](https://app.formbricks.com/auth/signup) account. -2. In the Menu (top right) you see that you can toggle switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesn’t mess up the insights from prod. Switch to “Development”: +- In the top-right menu, you can switch between “Development” and “Production” environments. These are separate, so your test data won’t affect real insights. Switch to “Development”: - +![switch to dev environment](/images/xm-and-surveys/xm/best-practices/docs-feedback/switch-to-dev.webp) -3. Then, create a survey using the template “Docs Feedback”: +- Then, create a survey using the template “Docs Feedback”: - +![select docs template](/images/xm-and-surveys/xm/best-practices/docs-feedback/docs-template.webp) -4. Change the Internal Question ID of the first question to **“isHelpful”** to make your life easier 😉 +- Change the Internal Question ID of the first question to **“isHelpful”** to make your life easier 😉 - +![change id](/images/xm-and-surveys/xm/best-practices/docs-feedback/change-id.webp) -5. In the same way, you can change the Internal Question ID of the _Please elaborate_ question to **“additionalFeedback”** and the one of the _Page URL_ question to **“pageUrl”**. +- Similarly, you can change the Internal Question ID of the *Please elaborate* question to **“additionalFeedback”** and the one of the *Page URL* question to **“pageUrl”**. - Answers need to be identical If you want different answers than “Yes 👍” and “No 👎” you need to update the choices accordingly. They have to be identical to the frontend we're building in the next step. + The answers must be identical. If you want different options than "Yes 👍" and "No 👎", you need to update the choices accordingly. They must match the frontend we’re building in the next step. -6. Click on “Continue to Settings or select the audience tab manually. Scroll down to “Survey Trigger” and create a new Action: +- Click on “Continue to Settings or select the audience tab manually. Scroll down to “Survey Trigger” and create a new Action: - +![set up when to ask card](/images/xm-and-surveys/xm/best-practices/docs-feedback/survey-trigger.webp) -7. Our goal is to create an event that never fires. This is a bit nonsensical because it is a workaround. Stick with me 😃 Fill the action out like on the screenshot: +- Our goal is to create an event that never triggers. This might seem odd, but it's a necessary workaround. Fill out the action as shown in the screenshot: - +![add action](/images/xm-and-surveys/xm/best-practices/docs-feedback/add-action.webp) -8. Select the Non-Event in the dropdown. Now you see that the “Publish survey” button is active. Publish your survey 🤝 +- Select the Non-Event in the dropdown. Now you see that the “Publish survey” button is active. Publish your survey 🤝 - +![select nonevent](/images/xm-and-surveys/xm/best-practices/docs-feedback/select-action.webp) **You’re all setup in Formbricks Cloud for now 👍** ## 2. Build the frontend - Your frontend might work differently Your frontend likely looks and works differently. This is an example specific to our tech stack. We want to illustrate what you should consider building yours 😊 + Your frontend might work differently Your frontend likely looks and works + differently. This is an example specific to our tech stack. We want to + illustrate what you should consider building yours. Before we start, lets talk about the widget. It works like this: -- Once the user selects yes/no, a partial response is sent to the Formbricks API. It includes the feedback and the current page url. -- Then the user is presented with an additional open text field to further explain their choice. Once it's submitted, the previous response is updated with the additional feedback. +* Once the user selects yes/no, a partial response is sent to the Formbricks API. It includes the feedback and the current page url. + +* Then the user is presented with an additional open text field to further explain their choice. Once it's submitted, the previous response is updated with the additional feedback. This allows us to capture and analyze partial feedback where the user is not willing to provide additional information. **Let's do this 👇** -1. Open the code editor where you handle your docs page. +- Open the code editor where you handle your docs page. -2. Likely, you have a template file or similar which renders the navigation at the bottom of the page: +- Likely, you have a template file or similar which renders the navigation at the bottom of the page: - +![doc navigation](/images/xm-and-surveys/xm/best-practices/docs-feedback/docs-navi.webp) -Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) for our docs. +Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) in this case. -3. Write the frontend code for the widget. Here is the full component (we break it down right below): +- Write the frontend code for the widget. Here is the full component (we break it down right below): - - - -```tsx +```tsx Entire Widget import { Button } from "@/modules/ui/components/Button"; import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/popover"; import { useRouter } from "next/router"; import { useState } from "react"; -import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit"; +import { + handleFeedbackSubmit, + updateFeedback, +} from "../../lib/handleFeedbackSubmit"; export const DocsFeedback = () => { const router = useRouter(); @@ -157,9 +123,13 @@ export const DocsFeedback = () => { { - const id = await handleFeedbackSubmit(option, router.asPath); + const id = await handleFeedbackSubmit( + option, + router.asPath + ); setResponseId(id); - }}> + }} + > {option} ))} @@ -182,7 +152,8 @@ export const DocsFeedback = () => { setIsOpen(false); setFreeText(""); setSharedFeedback(true); - }}> + }} + > Send @@ -198,16 +169,11 @@ export const DocsFeedback = () => { }; ``` - - **Let’s break it down!** Setting the local states and getting the current URL: - - - -```tsx +```tsx State Management const router = useRouter(); // to get the URL of the current docs page const [isOpen, setIsOpen] = useState(false); // to close Popover after const [sharedFeedback, setSharedFeedback] = useState(false); // to display Thank You message @@ -215,13 +181,9 @@ const [responseId, setResponseId] = useState(null); // to store responseID (will const [freeText, setFreeText] = useState(""); // to locally store the additional info provided by user ``` - - Disabling feedback if config environment variables are not set properly: - - -```tsx +```tsx Disable feedback if incorrect config env vars if ( !process.env.NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID || !process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST || @@ -231,13 +193,9 @@ if ( } ``` - - The actual frontend (read comments): - - -```tsx +```tsx Actual Frontend return (
{!sharedFeedback ? ( // displays Feedback buttons or Thank You message @@ -290,18 +248,13 @@ return ( } ``` - - ## 3. Connecting to the Formbricks API -The last step is to hook up your sparkling new frontend to the Formbricks API. To do so, we followed the “[Create Response](/api/client/responses#create-a-response)” and “[Update Response](/api/client/responses#update-a-response)” pages in our docs. +The last step is to hook up your new frontend to the Formbricks API. To achieve that, we followed the “[Create Response](/api-reference/client-api->-response/create-response)” and “[Update Response](/api-reference/client-api->-response/update-response)” pages in our docs. Here is the code for the `handleFeedbackSubmit` function with comments: - - - -```tsx +```tsx handleFeedbackSubmit() function definition export const handleFeedbackSubmit = async (YesNo, pageUrl) => { const response_data = { data: { @@ -340,13 +293,9 @@ export const handleFeedbackSubmit = async (YesNo, pageUrl) => { }; ``` - - And this is the `updateFeedback` function with comments: - - -```tsx +```tsx updateFeedback() function definition export const updateFeedback = async (freeText, responseId) => { if (!responseId) { console.error("No response ID available"); // If there is not response ID, no response can be updated. @@ -383,27 +332,23 @@ export const updateFeedback = async (freeText, responseId) => { }; ``` - - -That’s almost it! 🤸 +We're almosr there! 🤸 ## 4. Setting it up for testing Before you roll it out in production, you want to test it. To do so, you need two things: -1. Environment ID (1) of the development environment on app.formbricks.com -2. Survey ID (2) of your test survey +- Environment ID (1) of the development environment on app.formbricks.com + +- Survey ID (2) of your test survey When you are on the survey detail page, you’ll find both of them in the URL: - +![copy IDs](/images/xm-and-surveys/xm/best-practices/docs-feedback/copy-ids.webp) Now, you have to replace the IDs and the API host accordingly in your `handleFeedbackSubmit`: - - - -```tsx +```tsx Replace the ID and API accordingly const payload = { response: response_data, surveyId: clgwfv4a7002el50ihyuss38x, // This is an example, replace with yours @@ -416,13 +361,9 @@ Now, you have to replace the IDs and the API host accordingly in your `handleFee }; ``` - - And lastly, in the `updateFeedback` function - - -```tsx +```tsx Replace the ID and API here as well try { const res = await fetch( // Note that we also updated the API host to 'https://app.formbricks.com/' @@ -430,10 +371,8 @@ And lastly, in the `updateFeedback` function } ``` - - ### You’re good to go! 🎉 Something doesn’t work? Check your browser console for the error. -**Can’t figure it out?**: **[Get help in Github Discussions](https://github.com/formbricks/formbricks/discussions)** +**Can’t figure it out?**: [Get help in GitHub Discussions](https://github.com/formbricks/formbricks/discussions) \ No newline at end of file diff --git a/docs/xm-and-surveys/xm/best-practices/feature-chaser.mdx b/docs/xm-and-surveys/xm/best-practices/feature-chaser.mdx new file mode 100644 index 0000000000..e53e5563cb --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/feature-chaser.mdx @@ -0,0 +1,86 @@ +--- +title: "Feature Chaser" +description: "Learn how to harness the power of Formbricks to gather targeted user feedback on specific features. Dive deep into creating, triggering, and publishing the Feature Chaser survey to enhance your product with actionable insights for specific users." +icon: "wrench" +--- + + +Following up on specific features only makes sense with very targeted surveys. Formbricks is built for that. + +## Purpose + +Product analytics never tell you why a feature is used - and why not. Following up on specific features with highly relevant questions is a great way to gather feedback and improve your product. + +## Formbricks Approach + +* Trigger survey at exactly the right point in the user journey + +* Never ask twice, keep your data clean + +* Prevent survey fatigue with global waiting period + +## Overview + +To run the Feature Chaser survey in your app you want to proceed as follows: + +- Create new Feature Chaser survey at [app.formbricks.com](https://app.formbricks.com/) + +- Setup a user action to display survey at the right point in time + + + Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/xm-and-surveys/surveys/website-app-surveys/quickstart) + + +### 1. Create new Feature Chaser + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Click on "Create Survey" and choose the template “Feature Chaser”: + +![Create survey by template](/images/xm-and-surveys/xm/best-practices/feature-chaser/create-survey.webp) + +### 2. Update questions + +The questions you want to ask are dependent on your feature and can be very specific. In the template, we suggest a high-level check on how easy it was for the user to achieve their goal. We also add an opportunity to provide context: + +![Change text content](/images/xm-and-surveys/xm/best-practices/feature-chaser/change-text.webp) + +Save, and move over to where the magic happens: The “Audience” tab. + +### 3. Set up a trigger for the Feature Chaser survey: + +Before setting the right trigger, you need to identify a user action in your app which signals, that they have just used the feature you want to understand better. In most cases, it is clicking a specific button in your product. + +You can create [Code Actions](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions) and [No Code Actions](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions) to follow users through your app. In this example, we will create a No Code Action. + +There are two ways to track a button: + +1. **Trigger by Inner Text:** You might have a button with a unique text at the end of your feature e.g. "Export Report". You can setup a user Action with the according `Inner Text` to trigger the survey, like so: + +![Set the trigger by inner Text](/images/xm-and-surveys/xm/best-practices/feature-chaser/action-innertext.webp) + +1. **Trigger by CSS Selector:** In case you have more than one button saying “Export Report” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“export-report-featurename”` and set your user action up like so: + +![Set the trigger by CSS Selector](/images/xm-and-surveys/xm/best-practices/feature-chaser/action-css.webp) + +Please follow our [Actions manual](/xm-and-surveys/surveys/website-app-surveys/actions) for an in-depth description of how Actions work. + +### 4. Select Action in the “When to ask” card + +![Select PMF trigger button action](/images/xm-and-surveys/xm/best-practices/feature-chaser/select-action.webp) + +### 5. Last step: Set Recontact Options correctly + +Lastly, scroll down to “Recontact Options”. Here you have full freedom to decide who you want to ask. Generally, you only want to ask every user once and prevent survey fatigue. It's up to you to decide if you want to ask again, when the user did not yet reply: + +![Set recontact options](/images/xm-and-surveys/xm/best-practices/feature-chaser/recontact-options.webp) + +### 7. Congrats! You’re ready to publish your survey 💃 + +![Publish survey](/images/xm-and-surveys/xm/best-practices/feature-chaser/publish.webp) + + + Formbricks Widget running? You need to have the Formbricks Widget installed + to display the Feature Chaser in your app. Please follow [this tutorial (Step + 4 onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. + diff --git a/docs/xm-and-surveys/xm/best-practices/feedback-box.mdx b/docs/xm-and-surveys/xm/best-practices/feedback-box.mdx new file mode 100644 index 0000000000..0356b3b6b8 --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/feedback-box.mdx @@ -0,0 +1,75 @@ +--- +title: "Feedback Box" +description: "A step-by-step guide to creating a feedback box using Formbricks." +icon: "box" +--- + +The Feedback Box gives your users a direct channel to share their feedback and feel heard. + +## Purpose + +A low friction way to gather feedback helps catching even the smallest points of frustration in user experiences. Use automations to react rapidly and make users feel heard. + +## Formbricks Approach + +* Make it easy: 2 clicks to share feedback + +* Pipe insights where team can see them and react quickly + +## Installation + +To add the Feedback Box to your app, you need to perform these steps: + +1. Create new Feedback Box at app.formbricks.com + +2. Add user action to trigger Feedback Box + +3. Update recontact settings to display correctly + +### 1. Create new Feedback Box + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Then, create a new survey and look for the "Feedback Box" template: + +![Create feedback box by template](/images/xm-and-surveys/xm/best-practices/feedback-box/create-feedback-box-by-template.webp) + +### 2. Update question content + +Change the questions and answer options according to your preference: + +![Change text content](/images/xm-and-surveys/xm/best-practices/feedback-box/change-text-content.webp) + +### 3. Create user action to trigger Feedback Box: + +Go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool User Action Tracker: + +![Add action](/images/xm-and-surveys/xm/best-practices/feedback-box/add-action.webp) + +We have two options to track the Feedback Button in your application: innerText and CSS-Selector: + +1. **Inner Text:** This means that whenever a user clicks any HTML item in your app which has an `Inner Text` of `Feedback` the Feedback Box will be displayed. + +![Add HTML action](/images/xm-and-surveys/xm/best-practices/feedback-box/action-innertext.webp) + +2. **CSS Selector:** This means that when an element with a specific CSS-Selector like `#feedback-button` is clicked, your Feedback Box is triggered. + +![Add CSS action](/images/xm-and-surveys/xm/best-practices/feedback-box/action-css.webp) + +### 4. Select action in the “When to ask” card + +![Select feedback button action](/images/xm-and-surveys/xm/best-practices/feedback-box/select-action.webp) + +### 5. Set Recontact Options correctly + +Scroll down to “Recontact Options”. Here you have to choose the right settings so that the Feedback Box pops up every time the user action is performed. (Our default is that every user sees every survey only once): + +![Set recontact options](/images/xm-and-surveys/xm/best-practices/feedback-box/set-recontact-options.webp) + +### 6. You’re ready publish your survey! + +![Publish survey](/images/xm-and-surveys/xm/best-practices/feedback-box/publish-survey.webp) + +## Setting up the Widget + +Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. \ No newline at end of file diff --git a/docs/xm-and-surveys/xm/best-practices/improve-email-content.mdx b/docs/xm-and-surveys/xm/best-practices/improve-email-content.mdx new file mode 100644 index 0000000000..99750428ac --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/improve-email-content.mdx @@ -0,0 +1,66 @@ +--- +title: "Improve Email Content" +description: "Measure email content quality with Formbricks" +icon: "envelope" +--- + + +Email remains the predominant way to communicate with your customers. Measure the effectiveness to improve your offering. + +## Purpose + +Measuring the content quality of both transactional and marketing emails is a key element for improving customer communication. + +## Formbricks Approach + +* Embed the survey into your email so it’s part of the newsletter. + +* Use link prefilling to store the answer users clicked on in the email. + +* Dynamic user identification to append reader's email for personalized profiles and follow ups. + +## Installation + +To embed the newsletter survey into your email, follow these steps: + +1. Create new 'Improve Newsletter Content' survey at [app.formbricks.com](https://app.formbricks.com/) + +2. Select how you where you want to display the survey. + +3. Copy the embed code anywhere you want in your newsletter. + +### 1. Create new 'Improve Newsletter Content' Survey + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Then, create a new survey and look for the "Improve Newsletter Content" template: + +![Create Improve Newsletter Content by template](/images/xm-and-surveys/xm/best-practices/improve-email-content/improve-newsletter-content-survey-location.webp) + +### 2. Customize Survey questions + +Customize survey questions, emojis or stars however you like: + +![Edit Improve Newsletter Content template](/images/xm-and-surveys/xm/best-practices/improve-email-content/improve-newsletter-content-editor-formbricks.webp) + +### 3. Configure Survey Settings + +Navigate to the **Settings** tab and choose the type of survey you want. After that, choose **Link Survey**: + +![Choose survey type](/images/xm-and-surveys/xm/best-practices/improve-email-content/choose-survey-type.webp) + +### 4. Choose how you want to embed your survey + +After publishing your survey, a modal that prompts you to embed your survey will pop up. + +![Embed newsletter survey](/images/xm-and-surveys/xm/best-practices/improve-email-content/embed-survey-prompt.webp) + +Select the Embed Survey card and you will be directed to another modal. + +### 5. Copy code to embed the survey in your newsletter + +Click the button with the “View Embed Code” text at the top right corner of the modal and simply paste the HTML code for your survey anywhere you want it in your newsletter: + +![Embed survey code](/images/xm-and-surveys/xm/best-practices/improve-email-content/embed-survey-code-in-your-email.webp) + +Send a test email to yourself and try it out 🤓 diff --git a/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx b/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx new file mode 100644 index 0000000000..c4cada2faa --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/improve-trial-cr.mdx @@ -0,0 +1,91 @@ +--- +title: "Improve Trial Conversion" +description: "Improve Trial Conversion with Formbricks" +icon: "chart-line-up" +--- + +When a user doesn't convert, you want to know why. A micro-survey displayed at exactly the right time gives you a window into understanding the most relevant question: To pay or not to pay? + +## Purpose + +Understanding why free users don’t convert to paid users helps boost revenue. This insight allows you to adjust your offering to encourage more users to upgrade. + +## Formbricks Approach + +- Ask at exactly the right point in time. +- Ask to understand the problem, don’t ask for solutions. + +## Installation + +To display the Trial Conversion Survey in your app you want to proceed as follows: + +- Create new Trial Conversion Survey at [app.formbricks.com](https://app.formbricks.com/). +- Set up the user action to display survey at right point in time. +- Print it. + + + ### Is the Formbricks Widget Running? + We assume you’ve already installed the Formbricks Widget in your web app, as it’s required to display messages and surveys. If not, follow the [Quick Start Guide (takes 15 mins max.)](/xm-and-surveys/surveys/website-app-surveys/quickstart). + + +### 1. Create new Trial Conversion Survey + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Click on **Create Survey** and select the template, **Improve Trial Conversion**: + +![Create survey by template](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/create-survey.webp) + +### 2. Optional: Update questions + +You can update the questions and answer options. + +![Change text content](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/change-text.webp) + +_Want to change the button color? Adjust it in the project settings!_ + +Save, and move over to the **Audience** tab. + +### 3. Pre-segment your audience (coming soon) + + +### Filter by Attribute Coming Soon + +We're working on pre-segmenting users by attributes. This manual will be updated in the coming days. + + +Pre-segmentation isn't needed for this survey since you likely want to target all users who cancel their trial. You can use a specific user action, like clicking **Cancel Trial**, to show the survey only to users trying your product. + +### 4. Set up a trigger for the Trial Conversion Survey: + +How you trigger your survey depends on your product. There are two options: + +- **Trigger by Page view:** If you have a page like `/trial-cancelled` for users who cancel their trial subscription, create a user action with the type "Page View." Select "Limit to specific pages" and apply URL filters with these settings: + +![Change text content](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-pageurl.webp) + +Whenever a user visits this page, the survey will be displayed ✅ + +- **Trigger by Button Click:** In a different case, you have a “Cancel Trial" button in your app. You can setup a user Action with the `Inner Text`: + +![Change text content](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/action-innertext.webp) + +Please have a look at our complete [Actions manual](/xm-and-surveys/surveys/website-app-surveys/actions) if you have questions. + +### 5. Select Action in the “When to ask” card + +![Select feedback button action](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/select-action.webp) + +### 6. Last step: Set Recontact Options correctly + +Finally, scroll down to "Recontact Options" and select the appropriate settings to gather the most insights. Ensure the survey is always displayed, even if the user has seen it in the past few days. + +![Set recontact options](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/recontact-options.webp) + +### 7. Congrats! You’re ready to publish your survey 🥳 + +![Publish survey](/images/xm-and-surveys/xm/best-practices/improve-trial-cr/publish.webp) + + + Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. + \ No newline at end of file diff --git a/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx b/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx new file mode 100644 index 0000000000..1ba5636010 --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/interview-prompt.mdx @@ -0,0 +1,105 @@ +--- +title: "App Interview Prompt" +description: "Maximize User Interview Participation with App Interview Prompts" +icon: "comment" +--- + +The Interview Prompt allows you to pick a specific user segment (e.g. Power Users) and invite them to a user interview. Bye, bye spammy email invites, benefit from up to 6x more respondents. + +## Purpose + +Product analytics and app surveys are incomplete without user interviews. Set the scheduling on autopilot for a continuous stream of interviews. + +## Formbricks Approach + +* Pre-segment users with custom attributes. Only invite highly relevant users. + +* In-app prompts (app surveys or website surveys) have a 6x higher conversion rate than email invites. + +* Set scheduling user interviews on auto pilot. + +* Soon: Integrate directly with your [Cal.com](http://Cal.com) account. + +## Installation + +To display an Interview Prompt in your app you want to proceed as follows: + +1. Create new Interview Prompt at [app.formbricks.com](https://app.formbricks.com/) + +2. Adjust content and settings + +3. That’s it! 🎉 + + + Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (15mins)](/xm-and-surveys/surveys/website-app-surveys/quickstart) + + +### 1. Create new Interview Prompt + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Click on "Create Survey" and choose the template “Interview Prompt”: + +![Create interview prompt by template](/images/xm-and-surveys/xm/best-practices/interview-prompt/create-prompt.webp) + +### 2. Update prompt and CTA + +Update the prompt, description and button text to match your products tonality. You can also update the button color in the Project Settings. + +![Change text content](/images/xm-and-surveys/xm/best-practices/interview-prompt/change-text.webp) + +In the button settings you have to make sure it is set to “External URL”. In the URL field, copy your booking link (e.g. [https://cal.com/company/user-interview).](https://cal.com/company/user-interview\).) If you don’t have a booking link yet, head over to [cal.com](http://cal.com) and get one - they have the best free plan out there! + +![Add CSS action](/images/xm-and-surveys/xm/best-practices/interview-prompt/interview-example.webp) + +Save, and move over to the “Audience” tab. + +### 3. Pre-segment your audience (coming soon) + + + ## Filter by attribute coming soon. We're working on pre-segmenting users by + + attributes. We will update this manual in the next few days. + + +Once you clicked over to the “Audience” tab you can change the settings. In the **Who To Send** card, select “Filter audience by attribute”. This allows you to only show the prompt to a specific segment of your user base. + +In our case, we want to select users who we have assigned the attribute “Power User”. To learn how to assign attributes to your users, please [follow this guide](/xm-and-surveys/surveys/website-app-surveys/user-identification). + +Great, now only the “Power User” segment will see our Interview Prompt. But when will they see it? + +### 4. Set up a trigger for the Interview Prompt: + +To create the trigger to show your Interview Prompt, go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool User Action Tracker: + +![Add action](/images/xm-and-surveys/xm/best-practices/interview-prompt/add-action.webp) + +Generally, we have two types of user actions: Page views and clicks. The Interview Prompt, you’ll likely want to display it on a page visit since you already filter who sees the prompt by attributes. + +1. **Page view:** Whenever a user visits a page the survey will be displayed, as long as the other conditions match. Other conditions are pre-segmentation, if this user has seen a survey in the past 2 weeks, etc. + +![Add page URL action](/images/xm-and-surveys/xm/best-practices/interview-prompt/action-pageurl.webp) + +1. **Click(Inner Text & CSS Selector):** When a user clicks an element (like a button) with a specific text content or CSS selector, the prompt will be displayed as long as the other conditions also match. + +![Add CSS action](/images/xm-and-surveys/xm/best-practices/interview-prompt/action-css.webp) + +![Add inner text action](/images/xm-and-surveys/xm/best-practices/interview-prompt/action-innertext.webp) + +### 5. Select action in the “When to ask” card + +![Select feedback button action](/images/xm-and-surveys/xm/best-practices/interview-prompt/select-action.webp) + +### 6. Set Recontact Options correctly + +Scroll down to “Recontact Options”. Here you have to choose the correct settings to strike the right balance between asking for user feedback and preventing survey fatigue. Your settings also depend on the size of your user base or segment. If you e.g. have thousands of “Power Users” you can easily afford to only display the prompt once. If you have a smaller user base you might want to ask twice to get a sufficient amount of bookings: + +![Set recontact options](/images/xm-and-surveys/xm/best-practices/interview-prompt/recontact-options.webp) + +### 7. Congrats! You’re ready to publish your survey 💃 🤸 + +![Publish survey](/images/xm-and-surveys/xm/best-practices/interview-prompt/publish-survey.webp) + + + Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. + \ No newline at end of file diff --git a/docs/xm-and-surveys/xm/best-practices/pmf-survey.mdx b/docs/xm-and-surveys/xm/best-practices/pmf-survey.mdx new file mode 100644 index 0000000000..164a483408 --- /dev/null +++ b/docs/xm-and-surveys/xm/best-practices/pmf-survey.mdx @@ -0,0 +1,80 @@ +--- +title: "Product-Market Fit" +description: "How to Set Up a Product-Market Fit Survey Using Formbricks - Step-by-Step Guide" +icon: "chart-line-up" +--- + +## Purpose + + +Measuring and understanding your PMF is crucial for building a successful business. It helps you identify what users like, what’s missing, and what to build next. This survey is ideal for measuring PMF, similar to [Superhuman](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit). + +## Formbricks Approach + +- Pre-segment users to only survey users who have experienced your products value. +- Never ask twice, keep your data clean. +- Run on autopilot: Set up once, keep surveying users continuously. + +## Overview + +To display the Product-Market Fit survey in your app you want to proceed as follows: + +1. Create new Product-Market Fit survey at [app.formbricks.com](https://app.formbricks.com/) +2. Setup pre-segmentation to assure high data quality +3. Setup the user action to display survey at good point in time + + + Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. + + +### 1. Create new PMF survey + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Click on "Create Survey" and choose a PMF survey template. The first one is short, while the second builds on the ["Product-Market Fit Engine"](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit) developed by Superhuman. + +![Create survey by template](/images/xm-and-surveys/xm/best-practices/pmf-survey/create-survey.webp) +/> + +### Optional: Update questions + +You can update the questions and answer options, but based on our experience, we recommend starting with the provided template. For a detailed guide on how to use the data you're collecting, check out this [guide](https://coda.io/@rahulvohra/superhuman-product-market-fit-engine). + +![Change text content](/images/xm-and-surveys/xm/best-practices/pmf-survey/change-text.webp) +/> + +_Want to change the button color? You can do so in the project settings!_ + +Save, and move over to where the magic happens: The “Audience” tab. + +### 3. Pre-segment your audience + +To run this survey effectively, pre-segment your user base. As mentioned earlier, asking all users can lead to misleading opinions. Focus on gathering feedback from users who have invested time in getting to know and use your product. + +**Filter by attribute**: You can keep the logic to decide if a user has (or has not) experienced value in your application. This makes most sense if you want to use historic usage data to decide if a user qualifies or not. Create your logic and if it applies, send an attribute to Formbricks by e.g. `formbricks.setAttribute("Loyalty", "Experienced Value");` Here is the full manual on how to [set attributes](/xm-and-surveys/surveys/website-app-surveys/user-identification). + +### 4. Set up a trigger for the Product-Market Fit survey: + +You need a trigger to display the survey but in this case, the filtering does all the work. It’s up to you to decide to display the survey after the user viewed a specific subpage (pageURL) or after clicking an element. Have a look at the [Actions manual](/xm-and-surveys/surveys/website-app-surveys/actions) if you are not sure how to set them up: + +![Add CSS action](/images/xm-and-surveys/xm/best-practices/pmf-survey/action-css.webp) + +![Add inner text action](/images/xm-and-surveys/xm/best-practices/pmf-survey/action-pageurl.webp) + +### 5. Select Action in the “When to ask” card + +![Select PMF trigger button action](/images/xm-and-surveys/xm/best-practices/pmf-survey/select-action.webp) + +### 6. Last step: Set Recontact Options correctly + +Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure your data remains of high quality. You want to make sure that this survey is only responded to once per user. It is up to you to decide if you want to display it several times until the user responds: + +![Set recontact options](/images/xm-and-surveys/xm/best-practices/pmf-survey/recontact-options.webp) + +### 7. Congrats! You’re ready to publish your survey 💃 + +![Publish survey](/images/xm-and-surveys/xm/best-practices/pmf-survey/publish.webp) + + + Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box in your app. Please follow [this tutorial (Step 4 onwards)](/xm-and-surveys/surveys/website-app-surveys/quickstart) to install the widget. + \ No newline at end of file diff --git a/apps/docs/app/best-practices/quiz-time/page.mdx b/docs/xm-and-surveys/xm/best-practices/quiz-time.mdx similarity index 70% rename from apps/docs/app/best-practices/quiz-time/page.mdx rename to docs/xm-and-surveys/xm/best-practices/quiz-time.mdx index aa1cf39163..ae1fd80bd7 100644 --- a/apps/docs/app/best-practices/quiz-time/page.mdx +++ b/docs/xm-and-surveys/xm/best-practices/quiz-time.mdx @@ -1,17 +1,8 @@ -import { MdxImage } from "@/components/mdx-image"; -import AddLogic from "./conditional-logic.webp"; -import EndingLogic from "./ending-logic.webp"; -import PassFail from "./pass-fail.webp"; -import Quiz from "./quiz.webp"; -import Score from "./score.webp"; -import SingleSelect from "./single-select.webp"; -import WhenThen from "./when-then.webp"; - -export const metadata = { - title: "How to Create a Quiz Using Formbricks - Step-by-Step Guide", - description: - "Learn to leverage Formbricks to create Quizzes. Follow our detailed step-by-step guide to build quizzes with custom logic and multiple endings.", -}; +--- +title: "Quiz Time" +icon: "brain" +description: "How to Create a Quiz Using Formbricks - Step-by-Step Guide" +--- # Creating a quiz with Formbricks - Step-by-step Guide @@ -33,12 +24,7 @@ First, make sure you have a Formbricks account. If not, you can create one [here 3. Go to the settings and select form type as **Link Survey** 4. In the form editor, click the three dots next to a question, then select Change Question Type and choose **Single-Select**. - +![Change Question type to Single-Select](/images/xm-and-surveys/xm/best-practices/quiz-time/single-select.webp) 5. Add a welcoming statement to greet your users and explain the Quiz's purpose. 6. Personalize the greeting to make it inviting and encourage engagement. @@ -55,7 +41,7 @@ B) Japan C) India D) Nepal - +![Sample Question](/images/xm-and-surveys/xm/best-practices/quiz-time/quiz.webp) ## Calculate Score @@ -64,28 +50,19 @@ Now that we have our question ready, let’s add the logic to calculate scores. 1. Scroll down in the editor and click on variables. 2. Create a new variable named `score` with a default value of 0 - +![Create Variable named Score image](/images/xm-and-surveys/xm/best-practices/quiz-time/score.webp) 3. Now go back to the question and expand the advanced settings by clicking on `> Show Advanced Settings`. 4. Under the conditional logic you should see the option to `Add Logic`. Click on that. - +![Add Logic Button](/images/xm-and-surveys/xm/best-practices/quiz-time/conditional-logic.webp) + 5. Now you should see conditional logic. Customize the logic to match your needs for example: **When** `question` equals `YOUR_ANSWER_HERE` **Then** `Calculate` `score` `Add +` `01`. So it should look something like this. - +![When-Then Conditional Logic](/images/xm-and-surveys/xm/best-practices/quiz-time/when-then.webp) 6. Let's duplicate and customize these questions. Click on the duplicate icon at the top of the question. 7. Now edit the questions, options, and answers in the **conditional logic** @@ -100,21 +77,11 @@ Once you have all the questions and the calculation logic in place, it’s time **When** `score` >= `03` **Then** `Jump to` `Pass`. So it should look something like this. - +![Conditional Logic for ending card](/images/xm-and-surveys/xm/best-practices/quiz-time/ending-logic.webp) 4. Ensure that the Fail card is positioned above the Pass card. This allows any condition that does not meet the criteria of being greater than or equal to 3 to jump to the Fail card. - +![Pass or Fail ending Cards](/images/xm-and-surveys/xm/best-practices/quiz-time/pass-fail.webp) 5. That's it! Now you can save and publish the quiz. diff --git a/package.json b/package.json index ef02f4683e..44ac9b69ba 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "test-e2e:azure": "pnpm test:e2e -c playwright.service.config.ts --workers=20", "prepare": "husky install", "storybook": "turbo run storybook", - "fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate" + "fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate", + "tolgee-pull": "tolgee pull && prettier --write ./packages/lib/messages/*.json" }, "devDependencies": { "@azure/microsoft-playwright-testing": "1.0.0-beta.6", diff --git a/packages/api/src/api/client/attribute.ts b/packages/api/src/api/client/attribute.ts index 0aee4cf625..338878108c 100644 --- a/packages/api/src/api/client/attribute.ts +++ b/packages/api/src/api/client/attribute.ts @@ -14,9 +14,7 @@ export class AttributeAPI { async update( attributeUpdateInput: Omit - ): Promise< - Result<{ changed: boolean; message: string; details?: Record }, ApiErrorResponse> - > { + ): Promise> { // transform all attributes to string if attributes are present into a new attributes copy const attributes: Record = {}; for (const key in attributeUpdateInput.attributes) { diff --git a/packages/api/src/api/client/environment.ts b/packages/api/src/api/client/environment.ts new file mode 100644 index 0000000000..8bc2114f1b --- /dev/null +++ b/packages/api/src/api/client/environment.ts @@ -0,0 +1,18 @@ +import { type Result } from "@formbricks/types/error-handlers"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; +import { type TJsEnvironmentState } from "@formbricks/types/js"; +import { makeRequest } from "../../utils/make-request"; + +export class EnvironmentAPI { + private apiHost: string; + private environmentId: string; + + constructor(apiHost: string, environmentId: string) { + this.apiHost = apiHost; + this.environmentId = environmentId; + } + + async getState(): Promise> { + return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/environment/`, "GET"); + } +} diff --git a/packages/api/src/api/client/index.ts b/packages/api/src/api/client/index.ts index e1a5704490..e1932e6b3e 100644 --- a/packages/api/src/api/client/index.ts +++ b/packages/api/src/api/client/index.ts @@ -1,14 +1,18 @@ import { type ApiConfig } from "../../types"; import { AttributeAPI } from "./attribute"; import { DisplayAPI } from "./display"; +import { EnvironmentAPI } from "./environment"; import { ResponseAPI } from "./response"; import { StorageAPI } from "./storage"; +import { UserAPI } from "./user"; export class Client { response: ResponseAPI; display: DisplayAPI; storage: StorageAPI; attribute: AttributeAPI; + user: UserAPI; + environment: EnvironmentAPI; constructor(options: ApiConfig) { const { apiHost, environmentId } = options; @@ -17,5 +21,7 @@ export class Client { this.display = new DisplayAPI(apiHost, environmentId); this.attribute = new AttributeAPI(apiHost, environmentId); this.storage = new StorageAPI(apiHost, environmentId); + this.user = new UserAPI(apiHost, environmentId); + this.environment = new EnvironmentAPI(apiHost, environmentId); } } diff --git a/packages/api/src/api/client/user.ts b/packages/api/src/api/client/user.ts new file mode 100644 index 0000000000..c86378903e --- /dev/null +++ b/packages/api/src/api/client/user.ts @@ -0,0 +1,44 @@ +import { type Result } from "@formbricks/types/error-handlers"; +import { type ApiErrorResponse } from "@formbricks/types/errors"; +import { makeRequest } from "../../utils/make-request"; + +export class UserAPI { + private apiHost: string; + private environmentId: string; + + constructor(apiHost: string, environmentId: string) { + this.apiHost = apiHost; + this.environmentId = environmentId; + } + + async createOrUpdate(userUpdateInput: { userId: string; attributes?: Record }): Promise< + Result< + { + state: { + expiresAt: Date | null; + data: { + userId: string | null; + segments: string[]; + displays: { surveyId: string; createdAt: Date }[]; + responses: string[]; + lastDisplayAt: Date | null; + language?: string; + }; + }; + messages?: string[]; + }, + ApiErrorResponse + > + > { + // transform all attributes to string if attributes are present into a new attributes copy + const attributes: Record = {}; + for (const key in userUpdateInput.attributes) { + attributes[key] = String(userUpdateInput.attributes[key]); + } + + return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/user`, "POST", { + userId: userUpdateInput.userId, + attributes, + }); + } +} diff --git a/packages/config-typescript/react-native-library.json b/packages/config-typescript/react-native-library.json index 9da691e72a..3c292f9a73 100644 --- a/packages/config-typescript/react-native-library.json +++ b/packages/config-typescript/react-native-library.json @@ -2,6 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "allowJs": true, + "esModuleInterop": true, "jsx": "react-native", "lib": ["dom", "dom.iterable", "ES2022"], "noEmit": true, diff --git a/packages/database/json-types.ts b/packages/database/json-types.ts index 147d36f0e4..6b33826c1f 100644 --- a/packages/database/json-types.ts +++ b/packages/database/json-types.ts @@ -1,5 +1,4 @@ /* eslint-disable import/no-relative-packages -- required for importing types */ - /* eslint-disable @typescript-eslint/no-namespace -- using namespaces is required for prisma-json-types-generator */ import { type TActionClassNoCodeConfig } from "../types/action-classes"; import { type TIntegrationConfig } from "../types/integration"; diff --git a/packages/database/migration/20250206093504_add_activepieces_to_webhook_source/migration.sql b/packages/database/migration/20250206093504_add_activepieces_to_webhook_source/migration.sql new file mode 100644 index 0000000000..026ea2fb84 --- /dev/null +++ b/packages/database/migration/20250206093504_add_activepieces_to_webhook_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "WebhookSource" ADD VALUE 'activepieces'; diff --git a/packages/database/migration/20250211050118_removed_new_session_event/migration.ts b/packages/database/migration/20250211050118_removed_new_session_event/migration.ts new file mode 100644 index 0000000000..ac9b4bb620 --- /dev/null +++ b/packages/database/migration/20250211050118_removed_new_session_event/migration.ts @@ -0,0 +1,34 @@ +import type { MigrationScript } from "../../src/scripts/migration-runner"; + +export const removedNewSessionEvent: MigrationScript = { + type: "data", + id: "dnh52k9vepinuhwuur8fclqf", + name: "20250211050118_removed_new_session_event", + run: async ({ tx }) => { + const updatedActions = await tx.$executeRaw` + UPDATE "ActionClass" + SET type = 'noCode', + "noCodeConfig" = '{"type":"pageView","urlFilters":[]}'::jsonb + WHERE type = 'automatic' + AND EXISTS ( + SELECT 1 + FROM "SurveyTrigger" + WHERE "actionClassId" = "ActionClass".id + ) + `; + + console.log(`Updated ${updatedActions.toString()} automatic actions`); + + // Delete actions that are not referenced in SurveyTrigger + const deletedActions = await tx.$executeRaw` + DELETE FROM "ActionClass" + WHERE type = 'automatic' + AND NOT EXISTS ( + SELECT 1 + FROM "SurveyTrigger" + WHERE "actionClassId" = "ActionClass".id + ) + `; + console.log(`Deleted ${deletedActions.toString()} automatic actions`); + }, +}; diff --git a/packages/database/migration/20250211071744_removed_automatic_action_type/migration.sql b/packages/database/migration/20250211071744_removed_automatic_action_type/migration.sql new file mode 100644 index 0000000000..f409d24608 --- /dev/null +++ b/packages/database/migration/20250211071744_removed_automatic_action_type/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - The values [automatic] on the enum `ActionType` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "ActionType_new" AS ENUM ('code', 'noCode'); +ALTER TABLE "ActionClass" ALTER COLUMN "type" TYPE "ActionType_new" USING ("type"::text::"ActionType_new"); +ALTER TYPE "ActionType" RENAME TO "ActionType_old"; +ALTER TYPE "ActionType_new" RENAME TO "ActionType"; +DROP TYPE "ActionType_old"; +COMMIT; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index db35503728..c534baeb71 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -31,6 +31,7 @@ enum WebhookSource { zapier make n8n + activepieces } model Webhook { @@ -332,7 +333,6 @@ model SurveyFollowUp { enum ActionType { code noCode - automatic } model ActionClass { diff --git a/packages/database/src/scripts/generate-data-migration.ts b/packages/database/src/scripts/generate-data-migration.ts index dfd923829b..a9ea29b00c 100644 --- a/packages/database/src/scripts/generate-data-migration.ts +++ b/packages/database/src/scripts/generate-data-migration.ts @@ -1,7 +1,7 @@ +import { createId } from "@paralleldrive/cuid2"; import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; -import { createId } from "@paralleldrive/cuid2"; const rl = readline.createInterface({ input: process.stdin, diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts index b0d020ec6b..5a48553c90 100644 --- a/packages/database/src/scripts/migration-runner.ts +++ b/packages/database/src/scripts/migration-runner.ts @@ -1,8 +1,8 @@ +import { type Prisma, PrismaClient } from "@prisma/client"; import { exec } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; -import { type Prisma, PrismaClient } from "@prisma/client"; const execAsync = promisify(exec); diff --git a/packages/database/zod/invites.ts b/packages/database/zod/invites.ts new file mode 100644 index 0000000000..f5adbb5e34 --- /dev/null +++ b/packages/database/zod/invites.ts @@ -0,0 +1,15 @@ +import { type Invite } from "@prisma/client"; +import { z } from "zod"; + +export const ZInvite = z.object({ + id: z.string(), + email: z.string().email(), + name: z.string().nullable(), + organizationId: z.string(), + creatorId: z.string(), + acceptorId: z.string().nullable(), + createdAt: z.date(), + expiresAt: z.date(), + role: z.enum(["owner", "manager", "member", "billing"]), + teamIds: z.array(z.string()), +}) satisfies z.ZodType>; diff --git a/packages/js-core/src/lib/attributes.ts b/packages/js-core/src/lib/attributes.ts index fbd2c27924..f62e3489e6 100644 --- a/packages/js-core/src/lib/attributes.ts +++ b/packages/js-core/src/lib/attributes.ts @@ -18,7 +18,7 @@ export const updateAttribute = async ( { changed: boolean; message: string; - details?: Record; + messages?: string[]; }, ApiErrorResponse > @@ -65,10 +65,12 @@ export const updateAttribute = async ( }); } - if (res.data.details) { - Object.entries(res.data.details).forEach(([detailsKey, detailsValue]) => { - logger.error(`${detailsKey}: ${detailsValue}`); - }); + const responseMessages = res.data.messages; + + if (responseMessages && responseMessages.length > 0) { + for (const message of responseMessages) { + logger.debug(message); + } } if (res.data.changed) { @@ -79,9 +81,7 @@ export const updateAttribute = async ( value: { changed: true, message: "Attribute updated in Formbricks", - ...(res.data.details && { - details: res.data.details, - }), + messages: responseMessages, }, }; } @@ -91,9 +91,7 @@ export const updateAttribute = async ( value: { changed: false, message: "Attribute not updated in Formbricks", - ...(res.data.details && { - details: res.data.details, - }), + messages: responseMessages, }, }; }; @@ -123,10 +121,10 @@ export const updateAttributes = async ( const res = await api.client.attribute.update({ userId, attributes: updatedAttributes }); if (res.ok) { - if (res.data.details) { - Object.entries(res.data.details).forEach(([key, value]) => { - logger.debug(`${key}: ${value}`); - }); + if (res.data.messages) { + for (const message of res.data.messages) { + logger.debug(message); + } } return ok(updatedAttributes); diff --git a/packages/js-core/src/lib/constants.ts b/packages/js-core/src/lib/constants.ts index bf19f961e6..7b57b3e576 100644 --- a/packages/js-core/src/lib/constants.ts +++ b/packages/js-core/src/lib/constants.ts @@ -1,4 +1,3 @@ -export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native"; export const JS_LOCAL_STORAGE_KEY = "formbricks-js"; export const LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY = "formbricks-js-website"; export const LEGACY_JS_APP_LOCAL_STORAGE_KEY = "formbricks-js-app"; diff --git a/packages/js-core/src/lib/environment-state.ts b/packages/js-core/src/lib/environment-state.ts index bf013b2a4f..c77799b6d9 100644 --- a/packages/js-core/src/lib/environment-state.ts +++ b/packages/js-core/src/lib/environment-state.ts @@ -48,13 +48,10 @@ export const fetchEnvironmentState = async ( throw error.error; } - const data = (await response.json()) as { data: TJsEnvironmentState["data"] }; + const data = (await response.json()) as { data: TJsEnvironmentState }; const { data: state } = data; - return { - data: { ...state }, - expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes - }; + return state; }; /** diff --git a/packages/js-core/src/lib/initialize.ts b/packages/js-core/src/lib/initialize.ts index ec2ed22500..453c8feece 100644 --- a/packages/js-core/src/lib/initialize.ts +++ b/packages/js-core/src/lib/initialize.ts @@ -2,7 +2,6 @@ import { type TAttributes } from "@formbricks/types/attributes"; import { type ApiErrorResponse } from "@formbricks/types/errors"; import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js"; -import { trackNoCodeAction } from "./actions"; import { updateAttributes } from "./attributes"; import { Config } from "./config"; import { @@ -359,9 +358,6 @@ export const initialize = async ( } catch (e) { handleErrorOnFirstInit(e); } - - // and track the new session event - await trackNoCodeAction("New Session"); } logger.debug("Adding event listeners"); diff --git a/packages/lib/actionClass/service.ts b/packages/lib/actionClass/service.ts index c87b09795c..ec2ed407cf 100644 --- a/packages/lib/actionClass/service.ts +++ b/packages/lib/actionClass/service.ts @@ -1,7 +1,7 @@ "use server"; import "server-only"; -import { Prisma } from "@prisma/client"; +import { ActionClass, Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/action-classes"; @@ -139,7 +139,7 @@ export const deleteActionClass = async (actionClassId: string): Promise => { +): Promise => { validateInputs([environmentId, ZId], [actionClass, ZActionClassInput]); const { environmentId: _, ...actionClassInput } = actionClass; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 8b1ca0e9e0..24c7dc7905 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -188,8 +188,9 @@ export const REDIS_URL = env.REDIS_URL; export const REDIS_HTTP_URL = env.REDIS_HTTP_URL; export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1"; -export const CUSTOMER_IO_SITE_ID = env.CUSTOMER_IO_SITE_ID; -export const CUSTOMER_IO_API_KEY = env.CUSTOMER_IO_API_KEY; +export const BREVO_API_KEY = env.BREVO_API_KEY; +export const BREVO_LIST_ID = env.BREVO_LIST_ID; + export const UNSPLASH_ACCESS_KEY = env.UNSPLASH_ACCESS_KEY; export const UNSPLASH_ALLOWED_DOMAINS = ["api.unsplash.com"]; diff --git a/packages/lib/customerio.ts b/packages/lib/customerio.ts deleted file mode 100644 index 9c7f5f5f8b..0000000000 --- a/packages/lib/customerio.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ZId } from "@formbricks/types/common"; -import { TUserEmail, ZUserEmail } from "@formbricks/types/user"; -import { CUSTOMER_IO_API_KEY, CUSTOMER_IO_SITE_ID } from "./constants"; -import { validateInputs } from "./utils/validate"; - -export const createCustomerIoCustomer = async ({ id, email }: { id: string; email: TUserEmail }) => { - if (!CUSTOMER_IO_SITE_ID || !CUSTOMER_IO_API_KEY) { - return; - } - - validateInputs([id, ZId], [email, ZUserEmail]); - - try { - const auth = Buffer.from(`${CUSTOMER_IO_SITE_ID}:${CUSTOMER_IO_API_KEY}`).toString("base64"); - const res = await fetch(`https://track-eu.customer.io/api/v1/customers/${id}`, { - method: "PUT", - headers: { - Authorization: `Basic ${auth}`, - }, - body: JSON.stringify({ - id: id, - email: email, - }), - }); - if (res.status !== 200) { - console.log("Error sending user to CustomerIO:", await res.text()); - } - } catch (error) { - console.log("error sending user to CustomerIO:", error); - } -}; diff --git a/packages/lib/env.ts b/packages/lib/env.ts index 879b5d5093..66e421f1ee 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -18,8 +18,8 @@ export const env = createEnv({ AZUREAD_CLIENT_SECRET: z.string().optional(), AZUREAD_TENANT_ID: z.string().optional(), CRON_SECRET: z.string().min(10), - CUSTOMER_IO_API_KEY: z.string().optional(), - CUSTOMER_IO_SITE_ID: z.string().optional(), + BREVO_API_KEY: z.string().optional(), + BREVO_LIST_ID: z.string().optional(), DATABASE_URL: z.string().url(), DEBUG: z.enum(["1", "0"]).optional(), DEFAULT_ORGANIZATION_ID: z.string().optional(), @@ -141,9 +141,9 @@ export const env = createEnv({ AZUREAD_CLIENT_ID: process.env.AZUREAD_CLIENT_ID, AZUREAD_CLIENT_SECRET: process.env.AZUREAD_CLIENT_SECRET, AZUREAD_TENANT_ID: process.env.AZUREAD_TENANT_ID, + BREVO_API_KEY: process.env.BREVO_API_KEY, + BREVO_LIST_ID: process.env.BREVO_LIST_ID, CRON_SECRET: process.env.CRON_SECRET, - CUSTOMER_IO_API_KEY: process.env.CUSTOMER_IO_API_KEY, - CUSTOMER_IO_SITE_ID: process.env.CUSTOMER_IO_SITE_ID, DATABASE_URL: process.env.DATABASE_URL, DEBUG: process.env.DEBUG, DEFAULT_ORGANIZATION_ID: process.env.DEFAULT_ORGANIZATION_ID, diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts index 0b111eaea1..b14b6ba22d 100644 --- a/packages/lib/environment/service.ts +++ b/packages/lib/environment/service.ts @@ -164,15 +164,6 @@ export const createEnvironment = async ( type: environmentInput.type || "development", project: { connect: { id: projectId } }, appSetupCompleted: environmentInput.appSetupCompleted || false, - actionClasses: { - create: [ - { - name: "New Session", - description: "Gets fired when a new session is created", - type: "automatic", - }, - ], - }, attributeKeys: { create: [ { diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 8285658624..61d1e5bce8 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -4,9 +4,9 @@ "continue_with_email": "Login mit E-Mail", "continue_with_github": "Login mit GitHub", "continue_with_google": "Login mit Google", - "continue_with_oidc": "Login mit {oidcDisplayName}", + "continue_with_oidc": "Weiter mit {oidcDisplayName}", + "continue_with_openid": "Login mit OpenID", "forgot-password": { - "an_error_occurred_when_logging": "Beim Anmelden ist ein Fehler aufgetreten", "back_to_login": "Zurück zum Login", "email-sent": { "heading": "Passwort erfolgreich angefordert", @@ -26,22 +26,21 @@ }, "invite": { "create_account": "Konto erstellen", - "email_does_not_match": "Ooops! Falsche E-Mail-Adresse 🤦", + "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 🤗", + "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 😥", + "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 😥", + "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 🎉", + "welcome_to_organization": "Du bist dabei \uD83C\uDF89", "welcome_to_organization_description": "Willkommen in der Organisation." }, "last_used": "Last used", "login": { - "an_error_occurred_when_logging_you_in": "Beim Anmelden ist ein Fehler aufgetreten", "backup_code": "Backup-Code", "create_an_account": "Konto erstellen", "enter_your_backup_code": "Gib deinen Backup-Code ein", @@ -51,13 +50,10 @@ "login_with_email": "Mit E-Mail einloggen", "lost_access": "Zugang verloren?", "new_to_formbricks": "Neu bei Formbricks?", - "too_many_requests_please_try_again_after_some_time": "Zu viele Anfragen, bitte versuche es später noch einmal!", - "two_factor_authentication_code": "Zwei-Faktor-Authentifizierungscode", "use_a_backup_code": "Einen Backup-Code verwenden" }, "signup": { "captcha_failed": "reCAPTCHA fehlgeschlagen", - "error": "Es ist ein Fehler aufgetreten, als Du Dich angemeldet hast", "have_an_account": "Hast Du ein Konto?", "log_in": "Einloggen", "password_validation_contain_at_least_1_number": "Enthält mindestens 1 Zahl", @@ -85,7 +81,7 @@ "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.", + "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": { @@ -116,7 +112,7 @@ "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": "Ein unbekannter Fehler ist beim Löschen von {type}s aufgetreten", + "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", @@ -124,10 +120,8 @@ "app": "App", "app_survey": "App-Umfrage", "apply_filters": "Filter anwenden", - "archive": "Archiv", "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.", - "attribute_type": "Attributtyp", "attributes": "Attribute", "automatic": "Automatisch", "avatar": "Avatar", @@ -143,7 +137,6 @@ "clear_filters": "Filter löschen", "clear_selection": "Auswahl aufheben", "click": "Klick", - "click_here_to_upload": "Klicke hier, um hochzuladen", "clicks": "Klicks", "close": "Schließen", "code": "Code", @@ -154,7 +147,6 @@ "connect": "Verbinden", "connect_formbricks": "Formbricks verbinden", "connected": "Verbunden", - "contact": "Kontakt", "contacts": "Kontakte", "copied_to_clipboard": "In die Zwischenablage kopiert", "copy": "Kopieren", @@ -191,14 +183,11 @@ "enable": "Aktivieren", "enterprise_license": "Enterprise Lizenz", "environment_not_found": "Umgebung nicht gefunden", - "environment_notice": "Du bist gerade in der {environment} Umgebung.", + "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", - "experience": "Experience", - "failed_to_get_first_environment_of_user": "Fehler beim Abrufen der ersten Umgebung des Benutzers", - "filters_reset_successfully": "Filter erfolgreich zurückgesetzt", "finish": "Fertigstellen", "follow_these": "Folge diesen", "formbricks_version": "Formbricks Version", @@ -213,7 +202,6 @@ "hidden_fields": "Versteckte Felder", "hide": "Verstecken", "hide_column": "Spalte ausblenden", - "hide_filters": "Filter ausblenden", "image": "Bild", "images": "Bilder", "import": "Importieren", @@ -231,7 +219,6 @@ "key": "Schlüssel", "label": "Bezeichnung", "language": "Sprache", - "languages": "Sprachen", "learn_more": "Mehr erfahren", "license": "Lizenz", "light_overlay": "Helle Überlagerung", @@ -251,8 +238,6 @@ "maximum": "Maximal", "member": "Mitglied", "members": "Mitglieder", - "membership_not_found": "Mitgliedschaft nicht gefunden", - "meta": "Meta", "metadata": "Metadaten", "minimum": "Minimum", "mobile_overlay_text": "Formbricks ist für Geräte mit kleineren Auflösungen nicht verfügbar.", @@ -264,7 +249,7 @@ "neutral": "Neutral", "new": "Neu", "new_survey": "Neue Umfrage", - "new_version_available": "Formbricks {version} ist da. Jetzt upgraden!", + "new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!", "next": "Weiter", "no_background_image_found": "Kein Hintergrundbild gefunden.", "no_code": "No Code", @@ -272,11 +257,9 @@ "no_result_found": "Kein Ergebnis gefunden", "no_results": "Keine Ergebnisse", "no_surveys_found": "Keine Umfragen gefunden.", - "none": "Keine", "not_authenticated": "Du bist nicht authentifiziert, um diese Aktion durchzuführen.", "not_authorized": "Nicht berechtigt", "not_connected": "Nicht verbunden", - "not_now": "Nicht jetzt", "note": "Notiz", "notes": "Notizen", "notifications": "Benachrichtigungen", @@ -284,21 +267,17 @@ "off": "Aus", "on": "An", "only_one_file_allowed": "Es ist nur eine Datei erlaubt", - "only_organization_owners_and_managers_can_access_this_setting": "Nur Organisationsbesitzer und Manager haben Zugriff auf diese Einstellung.", "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.", - "only_owners_managers_and_team_admins_can_perform_this_action": "Nur Eigentümer, Manager und Team-Admins können diese Aktion ausführen.", "or": "oder", "organization": "Organisation", "organization_not_found": "Organisation nicht gefunden", "organization_teams_not_found": "Organisations-Teams nicht gefunden", "other": "Andere", - "other_filters": "Weitere Filter", "others": "Andere", "overview": "Überblick", "password": "Passwort", "paused": "Pausiert", "pending_downgrade": "Herabstufung ausstehend", - "people": "Personen", "people_manager": "Mitarbeiterverwaltung", "person": "Person", "phone": "Handy", @@ -314,8 +293,10 @@ "privacy": "Datenschutz", "privacy_policy": "Datenschutzerklärung", "product_manager": "Produktmanager", + "product_not_found": "Produkt nicht gefunden", "profile": "Profil", "project": "Projekt", + "project_configuration": "Projektkonfiguration", "project_id": "Projekt-ID", "project_name": "Projektname", "project_not_found": "Projekt nicht gefunden", @@ -327,11 +308,8 @@ "questions": "Fragen", "read_docs": "Dokumentation lesen", "remove": "Entfernen", - "removed": "Entfernt", "reorder_and_hide_columns": "Spalten neu anordnen und ausblenden", "report_survey": "Umfrage melden", - "request_an_enterprise_license": "eine Enterprise Lizenz anfordern.", - "reset_all_filters": "Alle Filter zurücksetzen", "reset_to_default": "Auf Standard zurücksetzen", "response": "Antwort", "responses": "Antworten", @@ -342,7 +320,6 @@ "sales": "Vertrieb", "save": "Speichern", "save_changes": "Änderungen speichern", - "saved": "Gespeichert", "scheduled": "Geplant", "search": "Suchen", "security": "Sicherheit", @@ -357,7 +334,6 @@ "selections": "Auswahlen", "send": "Senden", "send_test_email": "Test-E-Mail senden", - "sent": "Gesendet", "session_not_found": "Sitzung nicht gefunden", "settings": "Einstellungen", "share_feedback": "Feedback geben", @@ -365,7 +341,6 @@ "show_response_count": "Antwortanzahl anzeigen", "shown": "Angezeigt", "size": "Größe", - "skip": "Überspringen", "skipped": "Übersprungen", "skips": "Übersprungen", "some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden", @@ -387,7 +362,7 @@ "survey_type": "Umfragetyp", "surveys": "Umfragen", "switch_organization": "Organisation wechseln", - "switch_to": "Wechsle zu {environment}", + "switch_to": "Wechseln zu {environment}", "table_items_deleted_successfully": "{type}s erfolgreich gelöscht", "table_settings": "Tabelleinstellungen", "tags": "Tags", @@ -395,7 +370,6 @@ "team": "Team", "team_access": "Teamzugriff", "team_name": "Teamname", - "team_not_found": "Team nicht gefunden", "teams": "Teams", "teams_not_found": "Teams nicht gefunden", "text": "Text", @@ -406,12 +380,10 @@ "top_right": "Oben rechts", "try_again": "Versuch's nochmal", "type": "Typ", - "unarchive": "Wiederherstellen", "unlock_more_projects_with_a_higher_plan": "Schalte mehr Projekte mit einem höheren Plan frei.", "update": "Aktualisierung", "updated": "Aktualisiert", "updated_at": "Aktualisiert am", - "upgrade_now": "Jetzt upgraden", "upload": "Hochladen", "upload_input_description": "Klicke oder ziehe, um Dateien hochzuladen.", "url": "URL", @@ -422,12 +394,10 @@ "variables": "Variablen", "verified_email": "Verifizierte E-Mail", "video": "Video", - "view_filters": "Filter anzeigen", "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": "Website", "website_and_app_connection": "Website & App Verbindung", "website_app_survey": "Website- & App-Umfrage", "website_survey": "Website-Umfrage", @@ -440,31 +410,43 @@ "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 {Datum} auf die Community Edition herabgestuft." + "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 🕵️", + "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", @@ -472,36 +454,45 @@ "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 🙏", + "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 🤍", + "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 👋", + "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", + "powered_by_formbricks": "Unterstützt von Formbricks", + "privacy_policy": "Datenschutzerklärung", + "reject": "Ablehnen", + "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 👋", + "survey_response_finished_email_hey": "Hey \uD83D\uDC4B", "survey_response_finished_email_this_form": "dieses Formular", "survey_response_finished_email_turn_off_notifications": "Benachrichtigungen ausschalten für", "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Benachrichtigungen für alle neu erstellten Formulare ausschalten", "survey_response_finished_email_view_more_responses": "Zeige {responseCount} weitere Antworten", - "survey_response_finished_email_view_responses": "Antworten anzeigen", "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 👋", + "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", @@ -512,12 +503,14 @@ "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_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": { @@ -527,7 +520,7 @@ "action_deleted_successfully": "Aktion erfolgreich gelöscht", "action_type": "Aktionstyp", "action_updated_successfully": "Aktion erfolgreich aktualisiert", - "action_with_key_already_exists": "Aktion mit Schlüssel {key} existiert bereits", + "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", @@ -568,7 +561,6 @@ "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.", - "this_is_a_code_action_you_can_only_change_the_description": "Das ist eine Code-Aktion. Du kannst nur die Beschreibung ändern.", "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", @@ -578,13 +570,6 @@ "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" }, - "attributes": { - "ex_user_property": "Benutzereigenschaft", - "how_to_add_attributes": "Wie man Attribute hinzufügt", - "show_archived": "Archivierte anzeigen", - "this_attribute_was_added_automatically_you_cannot_make_changes_to_it": "Dieses Attribut wurde automatisch hinzugefügt. Du kannst keine Änderungen daran vornehmen.", - "this_is_a_code_attribute_you_can_only_change_the_description": "Das ist ein Code-Attribut. Du kannst nur die Beschreibung ändern." - }, "connect": { "congrats": "Glückwunsch!", "connection_successful_message": "Gut gemacht! Wir sind verbunden.", @@ -602,19 +587,12 @@ "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", - "error_fetching_next_page_of_people_data": "Fehler beim Abrufen der nächsten Seite mit Kontaktdaten", - "error_fetching_people_data": "Fehler beim Abrufen der Kontaktdaten", - "fetching_user": "Benutzer wird geladen", "first_name": "Vorname", - "formbricks_id": "Formbricks-ID (intern)", - "how_to_add_contacts": "Wie man Kontakte hinzufügt", "last_name": "Nachname", - "loading_user_responses": "Benutzerantworten werden geladen", "no_responses_found": "Keine Antworten gefunden", "not_provided": "Nicht angegeben", "search_contact": "Kontakt suchen", "select_attribute": "Attribut auswählen", - "sessions": "Sitzungen", "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.", @@ -646,18 +624,15 @@ "did_you_find_this_insight_helpful": "War diese Erkenntnis hilfreich?", "failed_to_update_category": "Kategorie konnte nicht aktualisiert werden", "feature_request": "Anfrage", - "good_afternoon": "🌤️ Guten Nachmittag", - "good_evening": "🌙 Guten Abend", + "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}", - "negative": "Negativ", "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.", - "positive": "Positiv", "praise": "Lob", - "sentiment": "Stimmung", "sentiment_score": "Stimmungswert", "templates_card_description": "Wähle deine Vorlage oder starte von Grund auf neu", "templates_card_title": "Miss die Kundenerfahrung", @@ -668,6 +643,7 @@ }, "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", @@ -715,13 +691,12 @@ "integration_removed_successfully": "Integration erfolgreich entfernt", "integration_updated_successfully": "Integration erfolgreich aktualisiert", "make_integration_description": "Integriere Formbricks mit über 1000 Apps über Make", - "manage": "verwalten", "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} Arbeitsbereich", + "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.", @@ -742,7 +717,9 @@ "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" + "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", @@ -773,6 +750,7 @@ "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!", @@ -798,15 +776,13 @@ "project": { "api-keys": { "add_api_key": "API-Schlüssel hinzufügen", - "add_env_api_key": "API-Schlüssel für {environmentType} hinzufügen", + "add_env_api_key": "{environmentType} 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_keys": "API-Schlüssel", - "api_keys_description": "Verwalte deine API-Schlüssel.", "dev_api_keys": "API-Schlüssel (Dev)", "dev_api_keys_description": "API-Schlüssel für deine Entwicklungsumgebung hinzufügen und entfernen.", "no_api_keys_yet": "Du hast noch keine API-Schlüssel", @@ -824,7 +800,7 @@ "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": "Dient zur Identifizierung der richtigen Umgebung: {environmentId} gehört dir.", + "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.", @@ -840,8 +816,8 @@ "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.", - "open_the_browser_console_to_see_the_logs_2": "Öffne die Browser Konsole, um die Logs zu sehen.", - "receiving_data": "Daten werden empfangen 💃🕺", + "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", @@ -851,21 +827,19 @@ "tag_of_your_app": "Tag deiner App", "to_the": "zur", "to_the_url_where_you_load_the": "URL, wo Du die lädst", - "to_the_url_where_you_load_the_formbricks_sdk": "URL, wo Du das Formbricks SDK lädst.", "want_to_learn_how_to_add_user_attributes": "Willst Du lernen, wie man Attribute hinzufügt?", "you_also_need_to_pass_a": "du musst auch eine bestehen", - "you_are_done": "Du bist fertig 🎉", + "you_are_done": "Du bist fertig \uD83C\uDF89", "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": "Lösche {projectName} inkl. aller Umfragen, Antworten, Personen, Aktionen und Attribute.", + "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", - "organization_name": "Name der Organisation", "project_deleted_successfully": "Projekt erfolgreich gelöscht", "project_name_settings_description": "Ändere den Namen deines Projekts.", "project_name_updated_successfully": "Projektname erfolgreich aktualisiert", @@ -874,8 +848,7 @@ "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?", - "you_left_the_organization_successfully": "Du hast die Organisation erfolgreich verlassen" + "whats_your_project_called": "Wie heißt dein Projekt?" }, "languages": { "add_language": "Sprache hinzufügen", @@ -917,9 +890,6 @@ "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.", - "formbricks_branding_upgrade_message": "Um das Formbricks Branding von Link-Umfragen zu entfernen, bitte", - "formbricks_branding_upgrade_message_in_app": "Um das Formbricks Branding aus den In-App-Umfragen zu entfernen, bitte", - "formbricks_branding_upgrade_text": "aktualisiere deinen Plan.", "logo_removed_successfully": "Logo erfolgreich entfernt", "logo_settings_description": "Lade dein Firmenlogo hoch, um Umfragen zu branden.", "logo_updated_successfully": "Logo erfolgreich aktualisiert", @@ -931,7 +901,7 @@ "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_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", @@ -956,18 +926,10 @@ "unique_constraint_failed_on_the_fields": "Eindeutige Einschränkung für die Felder fehlgeschlagen" }, "teams": { - "add_existing_team": "Vorhandenes Team hinzufügen", - "create_new_team": "Neues Team erstellen", - "manage": "Verwalten", "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", - "read": "Lesen", - "read_write": "Lesen & Schreiben", - "remove_access": "Zugang entfernen", - "remove_access_confirmation": "Sind Sie sicher, dass Sie den Zugang für dieses Team entfernen möchten?", - "select_teams": "Teams auswählen", "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." } @@ -976,7 +938,6 @@ "segments": { "add_filter_below": "Filter unten hinzufügen", "add_your_first_filter_to_get_started": "Füge deinen ersten Filter hinzu, um loszulegen", - "advance_segment_cannot_be_edited_upgrade_your_plan": "Dies ist ein fortgeschrittener Abschnitt, Du kannst ihn nicht bearbeiten. Bitte upgrade deinen Plan, um diesen Abschnitt zu bearbeiten.", "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", @@ -989,13 +950,10 @@ "error_saving_segment": "Fehler beim Speichern des Segments", "ex_fully_activated_recurring_users": "Beispiel: Wiederkehrende Nutzer", "ex_power_users": "Ex-Power-User", - "failed_to_fetch_segments": "Fehler beim Abrufen der Segmente.", "filters_reset_successfully": "Filter erfolgreich zurückgesetzt", - "for_advanced_targeting_please": "Für fortgeschrittenes Targeting, bitte", "here": "hier", "hide_filters": "Filter ausblenden", "identifying_users": "Benutzer identifizieren", - "invalid_filters_please_check_the_filters_and_try_again": "Ungültige Filter. Bitte überprüfe die Filter und versuche es erneut.", "invalid_segment": "Ungültiges Segment", "invalid_segment_filters": "Ungültige Filter. Bitte überprüfe die Filter und versuche es erneut.", "load_segment": "Segment laden", @@ -1023,8 +981,6 @@ "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", - "upgrade_your_plan": "aktualisiere deinen Plan.", - "upgrade_your_plan_to_create_more_than_5_segments": "Upgrade deinen Plan, um mehr als 5 Segmente zu erstellen.", "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.", @@ -1060,9 +1016,9 @@ "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'", + "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", @@ -1084,7 +1040,7 @@ "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 wird {price} pro Monat berechnet.", + "switch_plan_confirmation_text": "Bist du sicher, dass du zum {plan}-Plan wechseln möchtest? Dir werden {price} pro Monat berechnet.", "team_access_roles": "Rollen für Teammitglieder", "technical_onboarding": "Technische Einführung", "unable_to_upgrade_plan": "Plan kann nicht aktualisiert werden", @@ -1121,7 +1077,6 @@ "your_enterprise_license_is_active_all_features_unlocked": "Deine Unternehmenslizenz ist aktiv. Alle Funktionen freigeschaltet." }, "general": { - "add_member": "Mitglied hinzufügen", "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.", @@ -1138,6 +1093,7 @@ "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", @@ -1147,7 +1103,6 @@ "from_your_organization": "von deiner Organisation", "invitation_sent_once_more": "Einladung nochmal gesendet.", "invite_deleted_successfully": "Einladung erfolgreich gelöscht", - "invite_organization_member": "Organisationsmitglied einladen", "invited_on": "Eingeladen am {date}", "invites_failed": "Einladungen fehlgeschlagen", "leave_organization": "Organisation verlassen", @@ -1169,36 +1124,24 @@ "organization_name": "Organisationsname", "organization_name_description": "Gib deiner Organisation einen Namen.", "organization_name_placeholder": "z. B. Powerpuff Girls", - "organization_name_required": "Der Name der Organisation ist erforderlich.", "organization_name_updated_successfully": "Organisationsname erfolgreich aktualisiert", "organization_settings": "Organisationseinstellungen", - "ownership_transferred_successfully": "Eigentumsrechte erfolgreich übertragen", "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", - "send_invitation": "Einladung 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", - "there_can_only_be_one_owner_of_each_organization": "Es kann nur einen Besitzer jeder Organisation geben. Wenn Du deine Eigentümerschaft überträgst an", - "to_confirm": "bestätigen:", - "type_in": "Tippe ein", - "upgrade_plan_notice_text_for_url_cloud": "aktualisiere deinen Plan.", - "upgrade_plan_notice_text_for_url_enterprise": "eine Enterprise-Lizenz besorgen.", - "when_you_transfer_the_ownership_you_will_remain_an_admin_of_the_organization": "Wenn Du die Eigentümerschaft überträgst, bleibst Du ein Admin der Organisation.", - "you_will_lose_all_of_your_ownership_rights": "du wirst alle deine Eigentumsrechte verlieren." + "test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet" }, "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.", - "manage_email_preferences": "E-Mail-Einstellungen verwalten", "need_slack_or_discord_notifications": "Brauchst Du Slack- oder Discord-Benachrichtigungen", - "notification_settings": "Benachrichtigungseinstellungen", "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", @@ -1226,7 +1169,6 @@ "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:", - "org_deletion_warning": "Wenn Du das einzige Mitglied einer Organisation bist oder kein anderer Admin anwesend ist, wird die Organisation zusammen mit allen zugehörigen Daten unwiderruflich gelöscht.", "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", @@ -1237,8 +1179,6 @@ "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.", - "the_2fa_otp_is_incorrect_please_try_again": "Der 2FA-OTP ist falsch. Bitte versuche es erneut.", - "to_enable_two_factor_authentication_you_need_an_active": "Um die Zwei-Faktor-Authentifizierung zu aktivieren, brauchst du eine aktive", "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.", @@ -1251,11 +1191,7 @@ "you_must_select_a_file": "Du musst eine Datei auswählen." }, "teams": { - "add_member": "Mitglied hinzufügen", - "add_members": "Mitglieder hinzufügen", "add_members_description": "Füge Mitglieder zum Team hinzu und bestimme ihre Rolle.", - "add_project": "Projekt hinzufügen", - "add_projects": "Projekte hinzufügen", "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.", @@ -1267,64 +1203,36 @@ "create_first_team_message": "Du musst zuerst ein Team erstellen.", "create_new_team": "Neues Team erstellen", "delete_team": "Team löschen", - "empty_project_message": "Du hast noch keine Projekte hinzugefügt. Weise dem Team ein Projekt zu, um den Mitgliedern Zugriff zu gewähren.", "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.", - "join_team": "Team beitreten", - "leave": "Verlassen", - "leave_team": "Team verlassen", - "leave_team_confirmation": "Bist Du sicher, dass Du dieses Team verlassen möchtest?", "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_removed_successfully": "Mitglied erfolgreich entfernt", "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.", - "members_added_successfully": "Mitglieder erfolgreich hinzugefügt", - "no_members_found": "Keine Mitglieder gefunden", - "no_other_teams_found": "Keine anderen Teams gefunden", - "org_owner_and_managers_can_only_be_team_admin": "Organisationsbesitzer und Manager können nur Team-Administratoren sein.", - "organization_members": "Organisationsmitglieder", - "organization_projects": "Organisationsprojekte", "owner_role_description": "Besitzer haben die volle Kontrolle über die Organisation.", - "permission": "Berechtigung", - "permission_updated_successfully": "Berechtigung erfolgreich aktualisiert.", "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.", - "project_name": "Projektname", - "project_removed_successfully": "Projekt erfolgreich entfernt.", "read": "Lesen", "read_write": "Lesen & Schreiben", - "remove": "Entfernen", - "remove_member_confirmation": "Sind Sie sicher, dass Sie dieses Mitglied entfernen möchten?", - "remove_project": "Projekt entfernen", - "remove_project_confirmation": "Sind Sie sicher, dass Sie dieses Projekt entfernen möchten?", - "role_updated_successfully": "Rolle erfolgreich aktualisiert", - "select_type": "Typ auswählen", "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_members": "Teammitglieder", "team_name": "Teamname", - "team_name_description": "Geben Sie Ihrem Team einen beschreibenden Namen.", "team_name_settings_title": "{teamName} Einstellungen", - "team_projects": "Teamprojekte", "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.", - "this_action_cannot_be_undone_if_it_s_gone_it_s_gone": "Diese Aktion kann nicht rückgängig gemacht werden. Wenn es weg ist, ist es weg.", "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.", - "upgrade_plan_notice_text_for_url_cloud": "aktualisiere deinen Plan.", - "upgrade_plan_notice_text_for_url_enterprise": "eine Enterprise-Lizenz besorgen.", "you_are_a_member": "Du bist Mitglied" } }, @@ -1340,7 +1248,7 @@ "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": "+ 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", @@ -1404,10 +1312,9 @@ "button_url": "URL", "cal_username": "Cal.com Benutzername oder Benutzername/Ereignis", "calculate": "Berechnen", - "cannot_add_question_with_empty_headline_as_recall": "Frage ohne Überschrift können nicht für Recall genutzt werden", "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_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", @@ -1435,7 +1342,6 @@ "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", - "clone_edit_segment": "Segment duplizieren & bearbeiten", "close_survey_on_date": "Umfrage am Datum schließen", "close_survey_on_response_limit": "Umfrage bei Erreichen des Antwortlimits schließen", "color": "Farbe", @@ -1445,6 +1351,7 @@ "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", @@ -1474,8 +1381,7 @@ "does_not_include_one_of": "Enthält nicht eines von", "does_not_start_with": "Fängt nicht an mit", "edit_recall": "Erinnerung bearbeiten", - "edit_segment": "Segment bearbeiten", - "edit_translations": "{language}-Übersetzungen 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", @@ -1485,9 +1391,7 @@ "equals": "Gleich", "equals_one_of": "Entspricht einem von", "error_publishing_survey": "Beim Veröffentlichen der Umfrage ist ein Fehler aufgetreten.", - "error_resetting_filters": "Fehler beim Zurücksetzen der Filter", "error_saving_changes": "Fehler beim Speichern der Änderungen", - "error_saving_segment": "Fehler beim Speichern des Segments", "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", @@ -1516,7 +1420,6 @@ "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_replyTo_placeholder": "E-Mail-Adresse eingeben & Leertaste drücken", "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", @@ -1537,7 +1440,6 @@ "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", - "for_advanced_targeting_please": "Für fortgeschrittenes Targeting, bitte", "form_styling": "Umfrage Styling", "formbricks_ai_description": "Beschreibe deine Umfrage und lass Formbricks KI die Umfrage für Dich erstellen", "formbricks_ai_generate": "erzeugen", @@ -1551,9 +1453,8 @@ "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_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", - "identifying_users": "Benutzer identifizieren", "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", @@ -1564,7 +1465,6 @@ "inner_text": "Innerer Text", "input_border_color": "Randfarbe des Eingabefelds", "input_color": "Farbe des Eingabefelds", - "invalid_segment": "Ungültiges Segment", "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", @@ -1594,7 +1494,6 @@ "long_answer": "Lange Antwort", "lower_label": "Unteres Label", "manage_languages": "Sprachen verwalten", - "manage_languages_to_get_started": "Sprachen verwalten, um loszulegen", "max_file_size": "Max. Dateigröße", "max_file_size_limit_is": "Max. Dateigröße ist", "multiply": "Multiplizieren *", @@ -1602,9 +1501,8 @@ "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 für '{query}' gefunden", + "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_option_found": "Keine Option gefunden.", "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.", @@ -1618,7 +1516,6 @@ "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": "Überschreibt die Wartezeit zwischen Umfragen auf {days} Tag(e).", "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}", @@ -1627,7 +1524,6 @@ "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", - "pre_segment_your_users_with_attributes_filters": "Segmentiere deine Nutzer im Voraus mit Attributfiltern.", "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", @@ -1647,35 +1543,29 @@ "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_all_filters": "Alle Filter entfernen", "remove_description": "Beschreibung entfernen", "remove_translations": "Übersetzungen entfernen", - "remove_translations_warning": "Bist Du sicher, dass Du alle Übersetzungen aus dieser Umfrage entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "require_answer": "Antwort erforderlich", "required": "Erforderlich", - "reset_all_filters": "Alle Filter zurücksetzen", "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": "Das Antwortlimit muss die Anzahl der erhaltenen Antworten ({responseCount}) überschreiten.", + "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", - "save_as_new_segment": "Als neues Segment speichern", "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.", - "segment_saved_successfully": "Segment erfolgreich gespeichert", "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_language_as_default_language": "Setze {language} als Standardsprache", "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", @@ -1685,7 +1575,7 @@ "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 Zielgruppe", + "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.", @@ -1708,19 +1598,15 @@ "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 👉", - "target_audience": "Zielgruppe", + "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_resets_all_filters_in_this_survey": "Diese Aktion setzt alle Filter in dieser Umfrage zurück.", "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_is_an_advanced_segment_please_upgrade_your_plan_to_edit_it": "Dies ist ein fortgeschrittenes Segment. Bitte upgrade deinen Plan, um es zu bearbeiten.", - "this_segment_is_used_in_other_surveys_make_changes": "Dieser Abschnitt wird in anderen Umfragen verwendet. Änderungen vornehmen", "this_setting_overwrites_your": "Diese Einstellung überschreibt deine", "three_points": "3 Punkte", "times": "Zeiten", @@ -1734,7 +1620,6 @@ "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", - "upgrade_to_the_scale_plan": "auf den Scale-Plan upgraden.", "upload": "Hochladen", "upload_at_least_2_images": "Lade mindestens 2 Bilder hoch", "upper_label": "Oberes Label", @@ -1742,7 +1627,6 @@ "url_filters": "URL-Filter", "url_not_supported": "URL nicht unterstützt", "use_with_caution": "Mit Vorsicht verwenden", - "user_targeting_is_currently_only_available_when": "Benutzerzielgruppen sind derzeit nur verfügbar, wenn", "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.", @@ -1754,7 +1638,6 @@ "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.", - "with_the_formbricks_sdk": "mit dem Formbricks SDK", "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.", @@ -1851,7 +1734,7 @@ "filter_added_successfully": "Filter erfolgreich hinzugefügt", "filter_updated_successfully": "Filter erfolgreich aktualisiert", "formbricks_email_survey_preview": "Formbricks E-Mail-Umfrage Vorschau", - "go_to_setup_checklist": "Gehe zur Einrichtungs-Checkliste 👉", + "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", @@ -1887,12 +1770,10 @@ "mobile_app": "Mobile App", "no_response_matches_filter": "Keine Antwort entspricht deinem Filter", "only_completed": "Nur vollständige Antworten", - "open_options": "Optionen öffnen", "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_confirmation": "Bist Du sicher, dass Du diese Umfrageergebnisse öffentlich zugänglich machen willst?", "publish_to_web_warning_description": "Deine Umfrageergebnisse werden öffentlich sein. Jeder außerhalb deiner Organisation kann darauf zugreifen, wenn er den Link hat.", "results_are_public": "Ergebnisse sind öffentlich", "send_preview": "Vorschau senden", @@ -1923,13 +1804,12 @@ "to_run_highly_targeted_surveys": "granular zielgerichtete Umfragen durchführen", "ttc_tooltip": "Durchschnittliche Zeit bis zum Abschluss der Umfrage.", "unknown_question_type": "Unbekannter Fragetyp", - "unpublish": "Veröffentlichung rückgangig machen", "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 🧘‍♂️", + "waiting_for_response": "Warte auf eine Antwort \uD83E\uDDD8‍♂️", "web_app": "Web-App", "were_working_on_sdks_for_flutter_swift_and_kotlin": "Wir arbeiten an SDKs für Flutter, Swift und Kotlin.", "what_is_a_panel": "Was ist ein Panel?", @@ -1939,7 +1819,7 @@ "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 💡", + "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!" }, @@ -1955,7 +1835,8 @@ "multiple_industries": "Mehrere Branchen", "use_this_template": "Vorlage verwenden", "uses_branching_logic": "Diese Umfrage verwendet Logik." - } + }, + "this_is_a_new_key": "Das ist ein neuer Schlüssel " }, "xm-templates": { "ces": "CES", @@ -1979,7 +1860,6 @@ }, "organizations": { "landing": { - "create_organization": "Organisation erstellen", "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." }, @@ -2001,18 +1881,15 @@ "what_are_you_here_for": "Warum bist Du hier?" }, "settings": { - "app_channel_headline": "Lass uns herausfinden, was deine Nutzer brauchen!", "brand_color": "Markenfarbe", "brand_color_description": "Passe die Hauptfarbe der Umfragen an deine Marke an.", "create_new_team": "Neues Team erstellen", - "link_channel_headline": "Du pflegst ein Produkt, wie aufregend!", "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?", - "website_channel_headline": "Lass uns Alles aus deinem Website-Traffic herausholen!" + "team_description": "Wer kann auf dieses Projekt zugreifen?" } } } @@ -2043,7 +1920,7 @@ "setup": { "intro": { "get_started": "Leg los", - "made_with_love_in_kiel": "Gebaut mit 🤍 in Deutschland", + "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.", @@ -2101,11 +1978,11 @@ "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_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_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.", @@ -2139,10 +2016,10 @@ "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_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_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.", @@ -2168,7 +2045,7 @@ "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_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?", @@ -2195,7 +2072,7 @@ "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_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!", @@ -2203,7 +2080,7 @@ "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 😔 Sprich direkt mit unserem CEO!", + "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", @@ -2236,7 +2113,7 @@ "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_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", @@ -2244,7 +2121,7 @@ "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_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", @@ -2256,28 +2133,28 @@ "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_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_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_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_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", @@ -2293,17 +2170,17 @@ "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_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_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_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?", @@ -2317,7 +2194,7 @@ "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_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?", @@ -2332,8 +2209,8 @@ "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 👍", - "docs_feedback_question_1_choice_2": "Nein 👎", + "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", @@ -2341,14 +2218,14 @@ "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_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_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.", @@ -2364,12 +2241,6 @@ "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_4_choice_1": "Extrem gut", - "employee_satisfaction_question_4_choice_2": "Sehr gut", - "employee_satisfaction_question_4_choice_3": "Mittelmäßig gut", - "employee_satisfaction_question_4_choice_4": "Etwas gut", - "employee_satisfaction_question_4_choice_5": "Überhaupt nicht gut", - "employee_satisfaction_question_4_headline": "Wie gut fühlst Du Dich in deiner Arbeit anerkannt?", "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", @@ -2404,8 +2275,8 @@ "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 🤸

", + "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", @@ -2433,14 +2304,6 @@ "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...", - "example_app_survey_name": "Beispiel-App-Umfrage", - "example_app_survey_question_1_button_label": "Los geht's!", - "example_app_survey_question_1_headline": "App erfolgreich verbunden", - "example_app_survey_question_1_html": "Du bist startklar. Erstelle eine Umfrage für deine App-Nutzer.", - "example_website_survey_name": "Beispiel Umfrage", - "example_website_survey_question_1_button_label": "Los geht's!", - "example_website_survey_question_1_headline": "Website erfolgreich verbunden 🎉", - "example_website_survey_question_1_html": "Du bist startklar. Erstelle deine eigene Umfrage für Website-Besucher 👇", "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?", @@ -2463,8 +2326,8 @@ "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 🐞", - "feedback_box_question_1_choice_2": "Anfrage 💡", + "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?", @@ -2480,7 +2343,7 @@ "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 👋

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

Hab noch einen schönen Tag!

", + "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...", @@ -2492,18 +2355,13 @@ "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_customer_goals_question_1_choice_1": "Meine Nutzerbasis besser verstehen", - "identify_customer_goals_question_1_choice_2": "Upsell-Möglichkeiten identifizieren", - "identify_customer_goals_question_1_choice_3": "Das bestmögliche Produkt bauen", - "identify_customer_goals_question_1_choice_4": "Die Welt beherrschen.", - "identify_customer_goals_question_1_headline": "Was ist dein Hauptziel bei der Nutzung von {{projectName}}?", "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_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", @@ -2511,8 +2369,8 @@ "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_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...", @@ -2525,15 +2383,15 @@ "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 🙏", - "identify_sign_up_barriers_with_project_name": "Anmeldebarrieren für {{projectName}}", + "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}}?", + "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.", @@ -2541,10 +2399,10 @@ "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_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_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...", @@ -2574,9 +2432,7 @@ "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_3_button_label": "Weiter", - "improve_trial_conversion_question_3_headline": "Was hast Du erwartet, dass {{projectName}} für Dich tun kann?", + "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.", @@ -2593,34 +2449,34 @@ "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_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? 🙏", + "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_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_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_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_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_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", @@ -2629,7 +2485,7 @@ "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_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", @@ -2648,8 +2504,8 @@ "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_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", @@ -2692,15 +2548,14 @@ "next": "Weiter", "nps": "Net Promoter Score (NPS)", "nps_description": "Net-Promoter-Score messen (0-10)", - "nps_headline": "Wie wahrscheinlich ist es, dass Du {{projectName}} einem Freund oder Kollegen weiterempfiehlst?", "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_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_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:", @@ -2755,28 +2610,27 @@ "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_headline": "Wie könnten wir sonst noch deine Erfahrung mit $[projectName] verbessern?", "prioritize_features_question_3_placeholder": "Tippe deine Antwort hier...", - "product_csat": "{{projectName}} Kundenzufriedenheit (CSAT)", "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_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_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_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_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", @@ -2785,9 +2639,9 @@ "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_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", @@ -2858,11 +2712,11 @@ "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_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 🙏 Bitte schreibe eine Bewertung für uns!", + "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?", @@ -2904,23 +2758,27 @@ "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_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 🙏 Bitte schreibe eine Bewertung für uns!", + "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_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 🙏 Bitte schreibe eine Bewertung für uns!", + "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", @@ -2942,7 +2800,7 @@ "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_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", @@ -2958,8 +2816,8 @@ "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_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...", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index c16e36672d..ac1a1d5602 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -5,8 +5,8 @@ "continue_with_github": "Continue with GitHub", "continue_with_google": "Continue with Google", "continue_with_oidc": "Continue with {oidcDisplayName}", + "continue_with_openid": "Continue with OpenID", "forgot-password": { - "an_error_occurred_when_logging": "An error occurred when logging you in", "back_to_login": "Back to login", "email-sent": { "heading": "Password reset successfully requested", @@ -26,22 +26,21 @@ }, "invite": { "create_account": "Create an account", - "email_does_not_match": "Ooops! Wrong email 🤦", + "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 🤗", + "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 😥", + "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 😥", + "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 🎉", + "welcome_to_organization": "You’re in \uD83C\uDF89", "welcome_to_organization_description": "Welcome to the organization." }, "last_used": "Last Used", "login": { - "an_error_occurred_when_logging_you_in": "An error occurred when logging you in", "backup_code": "Backup code", "create_an_account": "Create an account", "enter_your_backup_code": "Enter your backup code", @@ -51,13 +50,10 @@ "login_with_email": "Login with Email", "lost_access": "Lost Access?", "new_to_formbricks": "New to Formbricks?", - "too_many_requests_please_try_again_after_some_time": "Too many requests, please try again after some time!", - "two_factor_authentication_code": "Two-factor authentication code", "use_a_backup_code": "Use a backup code" }, "signup": { "captcha_failed": "Captcha failed", - "error": "An error occurred when signing you up", "have_an_account": "Have an account?", "log_in": "Log in", "password_validation_contain_at_least_1_number": "Contain at least 1 number", @@ -85,7 +81,7 @@ "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}. ", + "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": { @@ -124,10 +120,8 @@ "app": "App", "app_survey": "App Survey", "apply_filters": "Apply filters", - "archive": "Archive", "are_you_sure": "Are you sure?", "are_you_sure_this_action_cannot_be_undone": "Are you sure? This action cannot be undone.", - "attribute_type": "Attribute Type", "attributes": "Attributes", "automatic": "Automatic", "avatar": "Avatar", @@ -143,7 +137,6 @@ "clear_filters": "Clear filters", "clear_selection": "Clear selection", "click": "Click", - "click_here_to_upload": "Click here to upload", "clicks": "Clicks", "close": "Close", "code": "Code", @@ -154,7 +147,6 @@ "connect": "Connect", "connect_formbricks": "Connect Formbricks", "connected": "Connected", - "contact": "Contact", "contacts": "Contacts", "copied_to_clipboard": "Copied to clipboard", "copy": "Copy", @@ -196,9 +188,6 @@ "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", - "experience": "Experience", - "failed_to_get_first_environment_of_user": "Failed to get first environment of user", - "filters_reset_successfully": "Filters reset successfully", "finish": "Finish", "follow_these": "Follow these", "formbricks_version": "Formbricks Version", @@ -213,7 +202,6 @@ "hidden_fields": "Hidden fields", "hide": "Hide", "hide_column": "Hide column", - "hide_filters": "Hide filters", "image": "Image", "images": "Images", "import": "Import", @@ -231,7 +219,6 @@ "key": "Key", "label": "Label", "language": "Language", - "languages": "Languages", "learn_more": "Learn more", "license": "License", "light_overlay": "Light overlay", @@ -251,8 +238,6 @@ "maximum": "Maximum", "member": "Member", "members": "Members", - "membership_not_found": "Membership not found", - "meta": "Meta", "metadata": "Metadata", "minimum": "Minimum", "mobile_overlay_text": "Formbricks is not available for devices with smaller resolutions.", @@ -272,11 +257,9 @@ "no_result_found": "No result found", "no_results": "No results", "no_surveys_found": "No surveys found.", - "none": "None", "not_authenticated": "You are not authenticated to perform this action.", "not_authorized": "Not authorized", "not_connected": "Not Connected", - "not_now": "Not now", "note": "Note", "notes": "Notes", "notifications": "Notifications", @@ -284,21 +267,17 @@ "off": "Off", "on": "On", "only_one_file_allowed": "Only one file is allowed", - "only_organization_owners_and_managers_can_access_this_setting": "Only organization owners and managers can access this setting.", "only_owners_managers_and_manage_access_members_can_perform_this_action": "Only owners, managers and manage access members can perform this action.", - "only_owners_managers_and_team_admins_can_perform_this_action": "Only owners, managers and team admins can perform this action.", "or": "or", "organization": "Organization", "organization_not_found": "Organization not found", "organization_teams_not_found": "Organization teams not found", "other": "Other", - "other_filters": "Other filters", "others": "Others", "overview": "Overview", "password": "Password", "paused": "Paused", "pending_downgrade": "Pending Downgrade", - "people": "People", "people_manager": "People Manager", "person": "Person", "phone": "Phone", @@ -314,8 +293,10 @@ "privacy": "Privacy Policy", "privacy_policy": "Privacy Policy", "product_manager": "Product Manager", + "product_not_found": "Product not found", "profile": "Profile", "project": "Project", + "project_configuration": "Project's Configuration", "project_id": "Project ID", "project_name": "Project Name", "project_not_found": "Project not found", @@ -327,11 +308,8 @@ "questions": "Questions", "read_docs": "Read Docs", "remove": "Remove", - "removed": "Removed", "reorder_and_hide_columns": "Reorder and hide columns", "report_survey": "Report Survey", - "request_an_enterprise_license": "request an Enterprise license.", - "reset_all_filters": "Reset all filters", "reset_to_default": "Reset to default", "response": "Response", "responses": "Responses", @@ -342,7 +320,6 @@ "sales": "Sales", "save": "Save", "save_changes": "Save changes", - "saved": "Saved", "scheduled": "Scheduled", "search": "Search", "security": "Security", @@ -357,7 +334,6 @@ "selections": "Selections", "send": "Send", "send_test_email": "Send test email", - "sent": "Sent", "session_not_found": "Session not found", "settings": "Settings", "share_feedback": "Share feedback", @@ -365,7 +341,6 @@ "show_response_count": "Show response count", "shown": "Shown", "size": "Size", - "skip": "Skip", "skipped": "Skipped", "skips": "Skips", "some_files_failed_to_upload": "Some files failed to upload", @@ -395,7 +370,6 @@ "team": "Team", "team_access": "Team Access", "team_name": "Team name", - "team_not_found": "Team not found", "teams": "Teams", "teams_not_found": "Teams not found", "text": "Text", @@ -406,12 +380,10 @@ "top_right": "Top Right", "try_again": "Try again", "type": "Type", - "unarchive": "Unarchive", "unlock_more_projects_with_a_higher_plan": "Unlock more projects with a higher plan.", "update": "Update", "updated": "Updated", "updated_at": "Updated at", - "upgrade_now": "Upgrade now", "upload": "Upload", "upload_input_description": "Click or drag to upload files.", "url": "URL", @@ -422,12 +394,10 @@ "variables": "Variables", "verified_email": "Verified Email", "video": "Video", - "view_filters": "View filters", "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": "Website", "website_and_app_connection": "Website & App Connection", "website_app_survey": "Website & App Survey", "website_survey": "Website Survey", @@ -443,28 +413,40 @@ "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 🕵️", + "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", @@ -472,36 +454,45 @@ "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 🙏", + "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 🤍", + "notification_footer_the_formbricks_team": "The Formbricks Team \uD83E\uDD0D", "notification_footer_to_halt_weekly_updates": "To halt Weekly Updates,", - "notification_header_hey": "Hey 👋", + "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", + "powered_by_formbricks": "Powered by Formbricks", + "privacy_policy": "Privacy Policy", + "reject": "Reject", + "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 👋", + "survey_response_finished_email_hey": "Hey \uD83D\uDC4B", "survey_response_finished_email_this_form": "this form", "survey_response_finished_email_turn_off_notifications": "Turn off notifications for", "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "Turn off notifications for all newly created forms", "survey_response_finished_email_view_more_responses": "View {responseCount} more responses", - "survey_response_finished_email_view_responses": "View 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 👋", + "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", @@ -512,12 +503,14 @@ "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_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": { @@ -568,7 +561,6 @@ "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.", - "this_is_a_code_action_you_can_only_change_the_description": "This is a code action. You can only change the description.", "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", @@ -578,13 +570,6 @@ "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" }, - "attributes": { - "ex_user_property": "Ex. User property", - "how_to_add_attributes": "How to add Attributes", - "show_archived": "Show archived", - "this_attribute_was_added_automatically_you_cannot_make_changes_to_it": "This attribute was added automatically. You cannot make changes to it.", - "this_is_a_code_attribute_you_can_only_change_the_description": "This is a code attribute. You can only change the description." - }, "connect": { "congrats": "Congrats!", "connection_successful_message": "Well done! We're connected.", @@ -602,19 +587,12 @@ "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", - "error_fetching_next_page_of_people_data": "Error fetching next page of contacts data", - "error_fetching_people_data": "Error fetching contacts data", - "fetching_user": "Fetching user", "first_name": "First Name", - "formbricks_id": "Formbricks Id (internal)", - "how_to_add_contacts": "How to add contacts", "last_name": "Last Name", - "loading_user_responses": "Loading user responses", "no_responses_found": "No responses found", "not_provided": "Not provided", "search_contact": "Search contact", "select_attribute": "Select Attribute", - "sessions": "Sessions", "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.", @@ -646,18 +624,15 @@ "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": "🌤️ Good afternoon", - "good_evening": "🌙 Good evening", + "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}", - "negative": "Negative", "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.", - "positive": "Positive", "praise": "Praise", - "sentiment": "Sentiment", "sentiment_score": "Sentiment Score", "templates_card_description": "Choose a template or start from scratch", "templates_card_title": "Measure your customer experience", @@ -668,6 +643,7 @@ }, "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", @@ -715,7 +691,6 @@ "integration_removed_successfully": "Integration removed successfully", "integration_updated_successfully": "Integration updated successfully", "make_integration_description": "Integrate Formbricks with 1000+ apps via Make", - "manage": "Manage", "manage_webhooks": "Manage Webhooks", "n8n_integration_description": "Integrate Formbricks with 350+ apps via n8n", "notion": { @@ -742,7 +717,9 @@ "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" + "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", @@ -773,6 +750,7 @@ "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!", @@ -805,8 +783,6 @@ "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_keys": "API Keys", - "api_keys_description": "Manage your API keys.", "dev_api_keys": "Development Env Keys", "dev_api_keys_description": "Add and remove API keys for your Development environment.", "no_api_keys_yet": "You don't have any API keys yet", @@ -840,8 +816,8 @@ "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.", - "open_the_browser_console_to_see_the_logs_2": "Open the browser console to see the logs.", - "receiving_data": "Receiving data 💃🕺", + "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", @@ -851,10 +827,9 @@ "tag_of_your_app": "tag of your app", "to_the": "to the", "to_the_url_where_you_load_the": "to the URL where you load the", - "to_the_url_where_you_load_the_formbricks_sdk": "to the URL where you load the Formbricks SDK", "want_to_learn_how_to_add_user_attributes": "Want to learn how to add user attributes, custom events and more?", "you_also_need_to_pass_a": "you also need to pass a", - "you_are_done": "You're done 🎉", + "you_are_done": "You're done \uD83C\uDF89", "your_app_now_communicates_with_formbricks": "Your app now communicates with Formbricks - sending events, and loading surveys automatically!" }, "general": { @@ -865,7 +840,6 @@ "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", - "organization_name": "Organization Name", "project_deleted_successfully": "Project deleted successfully", "project_name_settings_description": "Change your projects name.", "project_name_updated_successfully": "Project name updated successfully", @@ -874,8 +848,7 @@ "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?", - "you_left_the_organization_successfully": "You left the organization successfully" + "whats_your_project_called": "What's your project called?" }, "languages": { "add_language": "Add language", @@ -917,9 +890,6 @@ "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.", - "formbricks_branding_upgrade_message": "To remove the Formbricks branding from Link Surveys, please", - "formbricks_branding_upgrade_message_in_app": "To remove the Formbricks branding from In-app Surveys, please", - "formbricks_branding_upgrade_text": "upgrade your plan.", "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", @@ -956,18 +926,10 @@ "unique_constraint_failed_on_the_fields": "Unique constraint failed on the fields" }, "teams": { - "add_existing_team": "Add existing team", - "create_new_team": "Create new team", - "manage": "Manage", "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", - "read": "Read", - "read_write": "Read & write", - "remove_access": "Remove Access", - "remove_access_confirmation": "Are you sure you want to remove access for this team?", - "select_teams": "Select teams", "team_name": "Team Name", "team_settings_description": "See which teams can access this project." } @@ -976,7 +938,6 @@ "segments": { "add_filter_below": "Add filter below", "add_your_first_filter_to_get_started": "Add your first filter to get started", - "advance_segment_cannot_be_edited_upgrade_your_plan": "This is an advanced segment, you cannot edit it. Please upgrade your plan to edit this segment.", "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", @@ -989,13 +950,10 @@ "error_saving_segment": "Error saving segment", "ex_fully_activated_recurring_users": "Ex. Fully activated recurring users", "ex_power_users": "Ex. Power Users", - "failed_to_fetch_segments": "Failed to fetch segments.", "filters_reset_successfully": "Filters reset successfully", - "for_advanced_targeting_please": "For advanced targeting, please", "here": "here", "hide_filters": "Hide filters", "identifying_users": "identifying users", - "invalid_filters_please_check_the_filters_and_try_again": "Invalid filters. Please check the filters and try again.", "invalid_segment": "Invalid segment", "invalid_segment_filters": "Invalid filters. Please check the filters and try again.", "load_segment": "Load Segment", @@ -1023,8 +981,6 @@ "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", - "upgrade_your_plan": "upgrade your plan.", - "upgrade_your_plan_to_create_more_than_5_segments": "Upgrade your plan to create more than 5 segments.", "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.", @@ -1121,7 +1077,6 @@ "your_enterprise_license_is_active_all_features_unlocked": "Your Enterprise License is active. All features unlocked." }, "general": { - "add_member": "Add member", "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.", @@ -1138,6 +1093,7 @@ "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", @@ -1147,7 +1103,6 @@ "from_your_organization": "from your organization", "invitation_sent_once_more": "Invitation sent once more.", "invite_deleted_successfully": "Invite deleted successfully", - "invite_organization_member": "Invite Organization Member", "invited_on": "Invited on {date}", "invites_failed": "Invites failed", "leave_organization": "Leave organization", @@ -1169,36 +1124,24 @@ "organization_name": "Organization Name", "organization_name_description": "Give your organization a descriptive name.", "organization_name_placeholder": "e.g. Power Puff Girls", - "organization_name_required": "Organization name is required.", "organization_name_updated_successfully": "Organization name updated successfully", "organization_settings": "Organization Settings", - "ownership_transferred_successfully": "Ownership transferred successfully", "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", - "send_invitation": "Send Invitation", "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", - "there_can_only_be_one_owner_of_each_organization": "There can only be one owner of each organization. If you transfer your ownership to", - "to_confirm": "to confirm:", - "type_in": "Type in", - "upgrade_plan_notice_text_for_url_cloud": "upgrade your plan.", - "upgrade_plan_notice_text_for_url_enterprise": "get an Enterprise License.", - "when_you_transfer_the_ownership_you_will_remain_an_admin_of_the_organization": "When you transfer the ownership, you will remain an Admin of the organization.", - "you_will_lose_all_of_your_ownership_rights": "you will lose all of your ownership rights." + "test_email_sent_successfully": "Test email sent successfully" }, "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.", - "manage_email_preferences": "Manage your email preferences", "need_slack_or_discord_notifications": "Need Slack or Discord notifications", - "notification_settings": "Notification Settings", "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", @@ -1226,7 +1169,6 @@ "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:", - "org_deletion_warning": "If you are the only member of an organization or there is no other admin present, the organization will be irreversibly deleted along with all associated data.", "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", @@ -1237,8 +1179,6 @@ "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.", - "the_2fa_otp_is_incorrect_please_try_again": "The 2FA OTP is incorrect. Please try again.", - "to_enable_two_factor_authentication_you_need_an_active": "To enable two-factor authentication, you need an active", "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.", @@ -1251,11 +1191,7 @@ "you_must_select_a_file": "You must select a file." }, "teams": { - "add_member": "Add Member", - "add_members": "Add members", "add_members_description": "Add members to the team and determine their role.", - "add_project": "Add Project", - "add_projects": "Add projects", "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.", @@ -1267,64 +1203,36 @@ "create_first_team_message": "You need to create a team first.", "create_new_team": "Create new team", "delete_team": "Delete team", - "empty_project_message": "You haven't added any projects yet. Assign a project to the team to grant access to its members.", "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.", - "join_team": "Join Team", - "leave": "Leave", - "leave_team": "Leave Team", - "leave_team_confirmation": "Are you sure you want to leave this team?", "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_removed_successfully": "Member removed successfully", "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.", - "members_added_successfully": "Members added successfully", - "no_members_found": "No members found", - "no_other_teams_found": "No other teams found", - "org_owner_and_managers_can_only_be_team_admin": "Org owner and managers can only be team admin.", - "organization_members": "Organization Members", - "organization_projects": "Organization Projects", "owner_role_description": "Owners have full control over the organization.", - "permission": "Permission", - "permission_updated_successfully": "Permission updated successfully.", "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.", - "project_name": "Project Name", - "project_removed_successfully": "Project removed successfully.", "read": "Read", "read_write": "Read & Write", - "remove": "Remove", - "remove_member_confirmation": "Are you sure you want to remove this member?", - "remove_project": "Remove Project", - "remove_project_confirmation": "Are you sure you want to remove this project?", - "role_updated_successfully": "Role updated successfully", - "select_type": "Select type", "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_members": "Team Members", "team_name": "Team Name", - "team_name_description": "Give your team a descriptive name.", "team_name_settings_title": "{teamName} Settings", - "team_projects": "Team Projects", "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.", - "this_action_cannot_be_undone_if_it_s_gone_it_s_gone": "This action cannot be undone. If it's gone, it's gone.", "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.", - "upgrade_plan_notice_text_for_url_cloud": "upgrade your plan.", - "upgrade_plan_notice_text_for_url_enterprise": "Get an Enterprise License.", "you_are_a_member": "You're a member" } }, @@ -1404,7 +1312,6 @@ "button_url": "Button URL", "cal_username": "Cal.com username or username/event", "calculate": "Calculate", - "cannot_add_question_with_empty_headline_as_recall": "Cannot add question with empty headline as recall", "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", @@ -1435,7 +1342,6 @@ "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", - "clone_edit_segment": "Clone & Edit Segment", "close_survey_on_date": "Close survey on date", "close_survey_on_response_limit": "Close survey on response limit", "color": "Color", @@ -1445,6 +1351,7 @@ "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", @@ -1474,8 +1381,7 @@ "does_not_include_one_of": "Does not include one of", "does_not_start_with": "Does not start with", "edit_recall": "Edit Recall", - "edit_segment": "Edit Segment", - "edit_translations": "Edit {language} translations", + "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", @@ -1485,9 +1391,7 @@ "equals": "Equals", "equals_one_of": "Equals one of", "error_publishing_survey": "An error occured while publishing the survey.", - "error_resetting_filters": "Error resetting filters", "error_saving_changes": "Error saving changes", - "error_saving_segment": "Error saving segment", "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", @@ -1516,7 +1420,6 @@ "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_replyTo_placeholder": "Write an email address & press space bar", "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", @@ -1537,7 +1440,6 @@ "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", - "for_advanced_targeting_please": "For advanced targeting, please", "form_styling": "Form styling", "formbricks_ai_description": "Describe your survey and let Formbricks AI create the survey for you", "formbricks_ai_generate": "Generate", @@ -1553,7 +1455,6 @@ "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", - "identifying_users": "identifying users", "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", @@ -1564,7 +1465,6 @@ "inner_text": "Inner Text", "input_border_color": "Input border color", "input_color": "Input color", - "invalid_segment": "Invalid segment", "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", @@ -1594,7 +1494,6 @@ "long_answer": "Long answer", "lower_label": "Lower Label", "manage_languages": "Manage Languages", - "manage_languages_to_get_started": "Manage Languages to get started", "max_file_size": "Max file size", "max_file_size_limit_is": "Max file size limit is", "multiply": "Multiply *", @@ -1602,9 +1501,8 @@ "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_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_option_found": "No option found.", "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.", @@ -1618,7 +1516,6 @@ "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": "Overwrites waiting period between surveys to {days} day(s).", "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}", @@ -1627,7 +1524,6 @@ "please_enter_a_file_extension": "Please enter a file extension.", "please_set_a_survey_trigger": "Please set a survey trigger", "please_specify": "Please specify", - "pre_segment_your_users_with_attributes_filters": "Pre-segment your users with attributes filters.", "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", @@ -1647,13 +1543,10 @@ "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_all_filters": "Remove all filters", "remove_description": "Remove description", "remove_translations": "Remove translations", - "remove_translations_warning": "Are you sure you want to remove all the translations from this survey? This action cannot be undone.", "require_answer": "Require Answer", "required": "Required", - "reset_all_filters": "Reset all filters", "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", @@ -1663,19 +1556,16 @@ "roundness": "Roundness", "rows": "Rows", "save_and_close": "Save & Close", - "save_as_new_segment": "Save as new Segment", "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.", - "segment_saved_successfully": "Segment saved successfully", "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_language_as_default_language": "Set {language} as default language", "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", @@ -1708,19 +1598,15 @@ "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 👉", - "target_audience": "Target Audience", + "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_resets_all_filters_in_this_survey": "This action resets all filters in this survey.", "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_is_an_advanced_segment_please_upgrade_your_plan_to_edit_it": "This is an advanced segment. Please upgrade your plan to edit it.", - "this_segment_is_used_in_other_surveys_make_changes": "This segment is used in other surveys. Make changes", "this_setting_overwrites_your": "This setting overwrites your", "three_points": "3 points", "times": "times", @@ -1734,7 +1620,6 @@ "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", - "upgrade_to_the_scale_plan": "upgrade to the Scale plan.", "upload": "Upload", "upload_at_least_2_images": "Upload at least 2 images", "upper_label": "Upper Label", @@ -1742,7 +1627,6 @@ "url_filters": "URL Filters", "url_not_supported": "URL not supported", "use_with_caution": "Use with caution", - "user_targeting_is_currently_only_available_when": "User targeting is currently only available when", "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.", @@ -1754,7 +1638,6 @@ "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.", - "with_the_formbricks_sdk": "with the Formbricks SDK", "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.", @@ -1809,7 +1692,7 @@ "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": "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)", @@ -1851,7 +1734,7 @@ "filter_added_successfully": "Filter added successfully", "filter_updated_successfully": "Filter updated successfully", "formbricks_email_survey_preview": "Formbricks Email Survey Preview", - "go_to_setup_checklist": "Go to Setup Checklist 👉", + "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", @@ -1887,12 +1770,10 @@ "mobile_app": "Mobile app", "no_response_matches_filter": "No response matches your filter", "only_completed": "Only completed", - "open_options": "Open options", "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_confirmation": "Are you sure you want to publish 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.", "results_are_public": "Results are public", "send_preview": "Send preview", @@ -1923,13 +1804,12 @@ "to_run_highly_targeted_surveys": "to run highly targeted surveys.", "ttc_tooltip": "Average time to complete the survey.", "unknown_question_type": "Unknown Question Type", - "unpublish": "Unpublish", "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 🧘‍♂️", + "waiting_for_response": "Waiting for a response \uD83E\uDDD8‍♂️", "web_app": "Web app", "were_working_on_sdks_for_flutter_swift_and_kotlin": "We're working on SDKs for Flutter, Swift and Kotlin.", "what_is_a_panel": "What is a panel?", @@ -1939,7 +1819,7 @@ "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 💡", + "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!" }, @@ -1955,7 +1835,8 @@ "multiple_industries": "Multiple industries", "use_this_template": "Use this template", "uses_branching_logic": "This survey uses branching logic." - } + }, + "this_is_a_new_key": "This is a updated key" }, "xm-templates": { "ces": "CES", @@ -1979,7 +1860,6 @@ }, "organizations": { "landing": { - "create_organization": "Create organization", "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." }, @@ -2001,18 +1881,15 @@ "what_are_you_here_for": "What are you here for?" }, "settings": { - "app_channel_headline": "Let's research what your users need!", "brand_color": "Brand color", "brand_color_description": "Match the main color of surveys with your brand.", "create_new_team": "Create new team", - "link_channel_headline": "You maintain a product, how exciting!", "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?", - "website_channel_headline": "Let's get the most out of your website traffic!" + "team_description": "Who all can access this project?" } } } @@ -2043,7 +1920,7 @@ "setup": { "intro": { "get_started": "Get started", - "made_with_love_in_kiel": "Made with 🤍 in Germany", + "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.", @@ -2101,11 +1978,11 @@ "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_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_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.", @@ -2139,10 +2016,10 @@ "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_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_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.", @@ -2168,7 +2045,7 @@ "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_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]?", @@ -2195,7 +2072,7 @@ "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_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!", @@ -2203,7 +2080,7 @@ "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 😔 Talk to our CEO directly!", + "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", @@ -2236,7 +2113,7 @@ "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_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", @@ -2244,7 +2121,7 @@ "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_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", @@ -2256,28 +2133,28 @@ "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_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_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_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_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", @@ -2293,17 +2170,17 @@ "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_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_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_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?", @@ -2317,7 +2194,7 @@ "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_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]?", @@ -2332,8 +2209,8 @@ "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 👍", - "docs_feedback_question_1_choice_2": "No 👎", + "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", @@ -2341,14 +2218,14 @@ "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_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_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.", @@ -2364,12 +2241,6 @@ "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_4_choice_1": "Extremely well", - "employee_satisfaction_question_4_choice_2": "Very well", - "employee_satisfaction_question_4_choice_3": "Moderately well", - "employee_satisfaction_question_4_choice_4": "Slightly well", - "employee_satisfaction_question_4_choice_5": "Not at all well", - "employee_satisfaction_question_4_headline": "How well do you feel your work is recognized?", "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", @@ -2404,8 +2275,8 @@ "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 🤸

", + "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", @@ -2433,14 +2304,6 @@ "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...", - "example_app_survey_name": "Example app survey", - "example_app_survey_question_1_button_label": "Let's do it!", - "example_app_survey_question_1_headline": "App successfully connected", - "example_app_survey_question_1_html": "You're all set up. Create your own survey for your app users.", - "example_website_survey_name": "Example website survey", - "example_website_survey_question_1_button_label": "Let's do it!", - "example_website_survey_question_1_headline": "Website successfully connected 🎉", - "example_website_survey_question_1_html": "You're all set up. Create your own survey for website visitors 👇", "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?", @@ -2463,8 +2326,8 @@ "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 🐞", - "feedback_box_question_1_choice_2": "Feature Request 💡", + "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?", @@ -2480,7 +2343,7 @@ "file_upload": "File Upload", "file_upload_description": "Enable respondents to upload documents, images, or other files", "finish": "Finish", - "follow_ups_modal_action_body": "

Hey 👋

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

Have a great day!

", + "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...", @@ -2492,18 +2355,13 @@ "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_customer_goals_question_1_choice_1": "Understand my user base deeply", - "identify_customer_goals_question_1_choice_2": "Identify upselling opportunities", - "identify_customer_goals_question_1_choice_3": "Build the best possible product", - "identify_customer_goals_question_1_choice_4": "Rule the world to make everyone breakfast brussels sprouts.", - "identify_customer_goals_question_1_headline": "What's your primary goal for using {{projectName}}?", "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_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", @@ -2511,8 +2369,8 @@ "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_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...", @@ -2525,15 +2383,15 @@ "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 🙏

", - "identify_sign_up_barriers_with_project_name": "{{projectName}} Sign Up Barriers", + "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}}?", + "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", @@ -2541,10 +2399,10 @@ "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_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_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...", @@ -2574,9 +2432,7 @@ "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_3_button_label": "Next", - "improve_trial_conversion_question_3_headline": "What did you expect {{projectName}} would do for you?", + "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.", @@ -2593,34 +2449,34 @@ "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_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? 🙏", + "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_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_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_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_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_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", @@ -2629,7 +2485,7 @@ "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_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", @@ -2648,8 +2504,8 @@ "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_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", @@ -2692,15 +2548,14 @@ "next": "Next", "nps": "Net Promoter Score (NPS)", "nps_description": "Measure Net-Promoter-Score (0-10)", - "nps_headline": "How likely are you to recommend {{projectName}} to a friend or colleague?", "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_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_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?", @@ -2755,17 +2610,16 @@ "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_headline": "How else could we improve you experience with $[projectName]?", "prioritize_features_question_3_placeholder": "Type your answer here...", - "product_csat": "{{projectName}} CSAT", "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_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_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.", @@ -2776,7 +2630,7 @@ "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_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", @@ -2785,9 +2639,9 @@ "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_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", @@ -2841,7 +2695,6 @@ "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.", @@ -2859,11 +2712,11 @@ "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_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 🙏 Please write a review for us!", + "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?", @@ -2871,7 +2724,6 @@ "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", @@ -2906,23 +2758,27 @@ "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_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 🙏 Please write a review for us!", + "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_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 🙏 Please write a review for us!", + "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", @@ -2944,7 +2800,7 @@ "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_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", @@ -2960,8 +2816,8 @@ "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_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...", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index 8bf6938fa5..dccc93e282 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -5,8 +5,8 @@ "continue_with_github": "Continuer avec GitHub", "continue_with_google": "Continuer avec Google", "continue_with_oidc": "Continuer avec {oidcDisplayName}", + "continue_with_openid": "Continuer avec OpenID", "forgot-password": { - "an_error_occurred_when_logging": "Une erreur est survenue lors de votre connexion.", "back_to_login": "Retour à la connexion", "email-sent": { "heading": "Demande de réinitialisation de mot de passe réussie.", @@ -26,22 +26,21 @@ }, "invite": { "create_account": "Créer un compte", - "email_does_not_match": "Oups ! Mauvais email 🤦", + "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 🤗", + "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 😥", + "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 😥", + "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 🎉", + "welcome_to_organization": "Vous êtes dedans \uD83C\uDF89", "welcome_to_organization_description": "Bienvenue dans l'organisation." }, "last_used": "Dernière utilisation", "login": { - "an_error_occurred_when_logging_you_in": "Une erreur est survenue lors de votre connexion.", "backup_code": "Code de sauvegarde", "create_an_account": "Créer un compte", "enter_your_backup_code": "Entrez votre code de sauvegarde", @@ -51,13 +50,10 @@ "login_with_email": "Se connecter avec un e-mail", "lost_access": "Accès perdu ?", "new_to_formbricks": "Nouveau sur Formbricks ?", - "too_many_requests_please_try_again_after_some_time": "Trop de demandes, veuillez réessayer après un certain temps !", - "two_factor_authentication_code": "Code d'authentification à deux facteurs", "use_a_backup_code": "Utiliser un code de secours" }, "signup": { "captcha_failed": "Captcha échoué", - "error": "Une erreur est survenue lors de votre inscription.", "have_an_account": "Avez-vous un compte ?", "log_in": "Se connecter", "password_validation_contain_at_least_1_number": "Contenir au moins 1 chiffre", @@ -79,13 +75,13 @@ "testimonial_title": "Transformez les insights clients en expériences irrésistibles.", "verification-requested": { "invalid_email_address": "Adresse e-mail invalide", - "invalid_token": "", + "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 e-mail à {email}.", + "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": { @@ -124,10 +120,8 @@ "app": "Application", "app_survey": "Sondage d'application", "apply_filters": "Appliquer des filtres", - "archive": "Archive", "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.", - "attribute_type": "Type d'attribut", "attributes": "Attributs", "automatic": "Automatique", "avatar": "Avatar", @@ -143,7 +137,6 @@ "clear_filters": "Effacer les filtres", "clear_selection": "Effacer la sélection", "click": "Cliquez", - "click_here_to_upload": "Cliquez ici pour télécharger", "clicks": "Clics", "close": "Fermer", "code": "Code", @@ -154,7 +147,6 @@ "connect": "Connecter", "connect_formbricks": "Connecter Formbricks", "connected": "Connecté", - "contact": "Contact", "contacts": "Contacts", "copied_to_clipboard": "Copié dans le presse-papiers", "copy": "Copier", @@ -177,7 +169,7 @@ "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": "", + "discard": "Annuler", "dismissed": "Rejeté", "docs": "documents", "documentation": "Documentation", @@ -196,9 +188,6 @@ "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", - "experience": "Expérience", - "failed_to_get_first_environment_of_user": "Échec de l'obtention du premier environnement de l'utilisateur.", - "filters_reset_successfully": "Filtres réinitialisés avec succès", "finish": "Terminer", "follow_these": "Suivez ceci", "formbricks_version": "Version de Formbricks", @@ -213,7 +202,6 @@ "hidden_fields": "Champs cachés", "hide": "Cacher", "hide_column": "Cacher la colonne", - "hide_filters": "Cacher les filtres", "image": "Image", "images": "Images", "import": "Importer", @@ -231,7 +219,6 @@ "key": "Clé", "label": "Étiquette", "language": "Langue", - "languages": "Langues", "learn_more": "En savoir plus", "license": "Licence", "light_overlay": "Superposition légère", @@ -251,13 +238,11 @@ "maximum": "Max", "member": "Membre", "members": "Membres", - "membership_not_found": "Adhésion non trouvée", - "meta": "Meta", "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": "Monter", + "move_up": "Déplacer vers le haut", "multiple_languages": "Plusieurs langues", "name": "Nom", "negative": "Négatif", @@ -272,11 +257,9 @@ "no_result_found": "Aucun résultat trouvé", "no_results": "Aucun résultat", "no_surveys_found": "Aucun sondage trouvé.", - "none": "Aucun", "not_authenticated": "Vous n'êtes pas authentifié pour effectuer cette action.", "not_authorized": "Non autorisé", "not_connected": "Non connecté", - "not_now": "Pas maintenant", "note": "Remarque", "notes": "Notes", "notifications": "Notifications", @@ -284,21 +267,17 @@ "off": "Éteint", "on": "Sur", "only_one_file_allowed": "Un seul fichier est autorisé", - "only_organization_owners_and_managers_can_access_this_setting": "Seules les propriétaires et les gestionnaires de l'organisation peuvent accéder à ce paramètre.", "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.", - "only_owners_managers_and_team_admins_can_perform_this_action": "Seules les propriétaires, les gestionnaires et les administrateurs d'équipe peuvent effectuer cette action.", "or": "ou", "organization": "Organisation", "organization_not_found": "Organisation non trouvée", "organization_teams_not_found": "Équipes d'organisation non trouvées", "other": "Autre", - "other_filters": "Autres filtres", "others": "Autres", "overview": "Aperçu", "password": "Mot de passe", "paused": "En pause", "pending_downgrade": "Downgrade en attente", - "people": "Personnes", "people_manager": "Responsable des personnes", "person": "Personne", "phone": "Téléphone", @@ -314,8 +293,10 @@ "privacy": "Politique de confidentialité", "privacy_policy": "Politique de confidentialité", "product_manager": "Chef de produit", + "product_not_found": "Produit non trouvé", "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é", @@ -327,11 +308,8 @@ "questions": "Questions", "read_docs": "Lire les documents", "remove": "Retirer", - "removed": "Retiré", "reorder_and_hide_columns": "Réorganiser et masquer des colonnes", "report_survey": "Rapport d'enquête", - "request_an_enterprise_license": "demander une licence Entreprise.", - "reset_all_filters": "Réinitialiser tous les filtres", "reset_to_default": "Réinitialiser par défaut", "response": "Réponse", "responses": "Réponses", @@ -342,7 +320,6 @@ "sales": "Ventes", "save": "Enregistrer", "save_changes": "Enregistrer les modifications", - "saved": "Enregistré", "scheduled": "Programmé", "search": "Recherche", "security": "Sécurité", @@ -357,7 +334,6 @@ "selections": "Sélections", "send": "Envoyer", "send_test_email": "Envoyer un e-mail de test", - "sent": "Envoyé", "session_not_found": "Session non trouvée", "settings": "Paramètres", "share_feedback": "Partager des retours", @@ -365,7 +341,6 @@ "show_response_count": "Afficher le nombre de réponses", "shown": "Montré", "size": "Taille", - "skip": "Sauter", "skipped": "Passé", "skips": "Sauter", "some_files_failed_to_upload": "Certains fichiers n'ont pas pu être téléchargés", @@ -395,7 +370,6 @@ "team": "Équipe", "team_access": "Accès Équipe", "team_name": "Nom de l'équipe", - "team_not_found": "Équipe non trouvée", "teams": "Équipes", "teams_not_found": "Équipes non trouvées", "text": "Texte", @@ -406,12 +380,10 @@ "top_right": "Haut Droit", "try_again": "Réessayer", "type": "Type", - "unarchive": "Désarchiver", "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 à", - "upgrade_now": "Mettre à niveau maintenant", "upload": "Télécharger", "upload_input_description": "Cliquez ou faites glisser pour télécharger des fichiers.", "url": "URL", @@ -422,12 +394,10 @@ "variables": "Variables", "verified_email": "Email vérifié", "video": "Vidéo", - "view_filters": "Filtres de vue", "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": "Site web", "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", @@ -437,34 +407,46 @@ "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} projet.", + "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 🕵️", + "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é", @@ -472,36 +454,45 @@ "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 🙏", + "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 🤍", + "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 👋", + "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é", + "powered_by_formbricks": "Propulsé par Formbricks", + "privacy_policy": "Politique de confidentialité", + "reject": "Rejeter", + "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 👋", + "survey_response_finished_email_hey": "Salut \uD83D\uDC4B", "survey_response_finished_email_this_form": "ce formulaire", "survey_response_finished_email_turn_off_notifications": "Désactiver les notifications pour", "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_view_more_responses": "Voir {responseCount} réponses supplémentaires", - "survey_response_finished_email_view_responses": "Voir les réponses", "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 👋", + "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", @@ -512,12 +503,14 @@ "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_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": { @@ -527,8 +520,8 @@ "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à", + "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", @@ -568,7 +561,6 @@ "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.", - "this_is_a_code_action_you_can_only_change_the_description": "Ceci est une action de code. Vous ne pouvez changer que la description.", "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", @@ -578,13 +570,6 @@ "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" }, - "attributes": { - "ex_user_property": "Propriété utilisateur ex.", - "how_to_add_attributes": "Comment ajouter des attributs", - "show_archived": "Afficher les archives", - "this_attribute_was_added_automatically_you_cannot_make_changes_to_it": "Cet attribut a été ajouté automatiquement. Vous ne pouvez pas le modifier.", - "this_is_a_code_attribute_you_can_only_change_the_description": "Ceci est un attribut de code. Vous ne pouvez changer que la description." - }, "connect": { "congrats": "Félicitations !", "connection_successful_message": "Bien joué ! Nous sommes connectés.", @@ -602,19 +587,12 @@ "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", - "error_fetching_next_page_of_people_data": "Erreur lors de la récupération de la page suivante des données de contacts", - "error_fetching_people_data": "Erreur lors de la récupération des données de contacts", - "fetching_user": "Récupération de l'utilisateur", "first_name": "Prénom", - "formbricks_id": "Identifiant Formbricks (interne)", - "how_to_add_contacts": "Comment ajouter des contacts", "last_name": "Nom de famille", - "loading_user_responses": "Chargement des réponses de l'utilisateur", "no_responses_found": "Aucune réponse trouvée", "not_provided": "Non fourni", "search_contact": "Rechercher un contact", "select_attribute": "Sélectionner un attribut", - "sessions": "Séances", "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.", @@ -646,21 +624,18 @@ "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": "🌤️ Bon après-midi", - "good_evening": "🌙 Bonsoir", + "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": "Informations pour {projectName}", - "negative": "Négatif", + "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.", - "positive": "Positif", "praise": "Éloge", - "sentiment": "Sentiment", "sentiment_score": "Score de sentiment", "templates_card_description": "Choisissez un modèle ou commencez à partir de zéro", - "templates_card_title": "", + "templates_card_title": "Mesurez l'expérience de vos clients", "this_month": "Ce mois-ci", "this_quarter": "Ce trimestre", "this_week": "Cette semaine", @@ -668,6 +643,7 @@ }, "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", @@ -715,13 +691,12 @@ "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": "Gérer", "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 Notion. Les données ne seront pas reflétées dans votre base de données 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}", + "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.", @@ -742,7 +717,9 @@ "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" + "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.", @@ -752,7 +729,7 @@ "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 {équipe}", + "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", @@ -773,6 +750,7 @@ "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 !", @@ -805,8 +783,6 @@ "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_keys": "Clés API", - "api_keys_description": "Gérez vos clés API.", "dev_api_keys": "Clés de l'environnement de développement", "dev_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de développement.", "no_api_keys_yet": "Vous n'avez pas encore de clés API.", @@ -840,8 +816,8 @@ "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.", - "open_the_browser_console_to_see_the_logs_2": "Ouvrez la console du navigateur pour voir les journaux.", - "receiving_data": "Réception des données 💃🕺", + "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", @@ -851,10 +827,9 @@ "tag_of_your_app": "étiquette de votre application", "to_the": "au", "to_the_url_where_you_load_the": "vers l'URL où vous chargez le", - "to_the_url_where_you_load_the_formbricks_sdk": "vers l'URL où vous chargez le SDK Formbricks", "want_to_learn_how_to_add_user_attributes": "Vous voulez apprendre à ajouter des attributs utilisateur, des événements personnalisés et plus encore ?", "you_also_need_to_pass_a": "vous devez également passer un", - "you_are_done": "Vous avez terminé 🎉", + "you_are_done": "Vous avez terminé \uD83C\uDF89", "your_app_now_communicates_with_formbricks": "Votre application communique désormais avec Formbricks - envoyant des événements et chargeant des enquêtes automatiquement !" }, "general": { @@ -865,7 +840,6 @@ "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.", - "organization_name": "Nom de l'organisation", "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.", @@ -874,8 +848,7 @@ "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 ?", - "you_left_the_organization_successfully": "Vous avez quitté l'organisation avec succès." + "whats_your_project_called": "Comment s'appelle votre projet ?" }, "languages": { "add_language": "Ajouter une langue", @@ -890,7 +863,7 @@ "identifier": "Identifiant (ISO)", "incomplete_translations": "Traductions incomplètes", "language": "Langue", - "language_deleted_successfully": "", + "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.", @@ -907,7 +880,6 @@ "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é", @@ -918,9 +890,6 @@ "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.", - "formbricks_branding_upgrade_message": "Pour supprimer la marque Formbricks des enquêtes Link, veuillez", - "formbricks_branding_upgrade_message_in_app": "Pour supprimer la marque Formbricks des enquêtes dans l'application, veuillez", - "formbricks_branding_upgrade_text": "mettez à niveau votre plan.", "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", @@ -957,18 +926,10 @@ "unique_constraint_failed_on_the_fields": "Échec de la contrainte unique sur les champs" }, "teams": { - "add_existing_team": "Ajouter une équipe existante", - "create_new_team": "Créer une nouvelle équipe", - "manage": "Gérer", "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", - "read": "Lire", - "read_write": "Lire et écrire", - "remove_access": "Retirer l'accès", - "remove_access_confirmation": "Êtes-vous sûr de vouloir supprimer l'accès pour cette équipe ?", - "select_teams": "Sélectionner des équipes", "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." } @@ -977,7 +938,6 @@ "segments": { "add_filter_below": "Ajouter un filtre ci-dessous", "add_your_first_filter_to_get_started": "Ajoutez votre premier filtre pour commencer", - "advance_segment_cannot_be_edited_upgrade_your_plan": "Ceci est un segment avancé, vous ne pouvez pas l'éditer. Veuillez mettre à niveau votre plan pour éditer ce segment.", "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", @@ -990,13 +950,10 @@ "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", - "failed_to_fetch_segments": "Échec de la récupération des segments.", "filters_reset_successfully": "Filtres réinitialisés avec succès", - "for_advanced_targeting_please": "Pour un ciblage avancé, s'il vous plaît", "here": "ici", "hide_filters": "Cacher les filtres", "identifying_users": "identification des utilisateurs", - "invalid_filters_please_check_the_filters_and_try_again": "Filtres invalides. Veuillez vérifier les filtres et réessayer.", "invalid_segment": "Segment invalide", "invalid_segment_filters": "Filtres invalides. Veuillez vérifier les filtres et réessayer.", "load_segment": "Charger le segment", @@ -1024,8 +981,6 @@ "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.", - "upgrade_your_plan": "mettez à niveau votre plan.", - "upgrade_your_plan_to_create_more_than_5_segments": "Améliorez votre plan pour créer plus de 5 segments.", "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.", @@ -1122,7 +1077,6 @@ "your_enterprise_license_is_active_all_features_unlocked": "Votre licence d'entreprise est active. Toutes les fonctionnalités sont déverrouillées." }, "general": { - "add_member": "Ajouter un membre", "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.", @@ -1137,8 +1091,9 @@ "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": "Hey {userName}", + "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.", @@ -1148,7 +1103,6 @@ "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", - "invite_organization_member": "Inviter un membre de l'organisation", "invited_on": "Invité le {date}", "invites_failed": "Invitations échouées", "leave_organization": "Quitter l'organisation", @@ -1170,36 +1124,24 @@ "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_required": "Le nom de l'organisation est requis.", "organization_name_updated_successfully": "Nom de l'organisation mis à jour avec succès", "organization_settings": "Paramètres de l'organisation", - "ownership_transferred_successfully": "Propriété transférée avec succès", "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", - "send_invitation": "Envoyer l'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", - "there_can_only_be_one_owner_of_each_organization": "Il ne peut y avoir qu'un seul propriétaire pour chaque organisation. Si vous transférez votre propriété à", - "to_confirm": "pour confirmer :", - "type_in": "Tapez", - "upgrade_plan_notice_text_for_url_cloud": "mettez à niveau votre plan.", - "upgrade_plan_notice_text_for_url_enterprise": "obtenir une licence d'entreprise.", - "when_you_transfer_the_ownership_you_will_remain_an_admin_of_the_organization": "Lorsque vous transférez la propriété, vous resterez un administrateur de l'organisation.", - "you_will_lose_all_of_your_ownership_rights": "vous perdrez tous vos droits de propriété." + "test_email_sent_successfully": "E-mail de test envoyé avec succès" }, "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.", - "manage_email_preferences": "Gérez vos préférences d'email", "need_slack_or_discord_notifications": "Besoin de notifications Slack ou Discord", - "notification_settings": "Paramètres de notification", "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.", @@ -1227,7 +1169,6 @@ "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 :", - "org_deletion_warning": "Si vous êtes le seul membre d'une organisation ou s'il n'y a pas d'autre administrateur présent, l'organisation sera définitivement supprimée avec toutes les données associées.", "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.", @@ -1238,8 +1179,6 @@ "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é.", - "the_2fa_otp_is_incorrect_please_try_again": "Le code OTP 2FA est incorrect. Veuillez réessayer.", - "to_enable_two_factor_authentication_you_need_an_active": "Pour activer l'authentification à deux facteurs, vous avez besoin d'un actif", "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.", @@ -1252,11 +1191,7 @@ "you_must_select_a_file": "Vous devez sélectionner un fichier." }, "teams": { - "add_member": "Ajouter un membre", - "add_members": "Ajouter des membres", "add_members_description": "Ajoutez des membres à l'équipe et déterminez leur rôle.", - "add_project": "Ajouter un projet", - "add_projects": "Ajouter des projets", "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.", @@ -1268,64 +1203,36 @@ "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_project_message": "Vous n'avez pas encore ajouté de projets. Assignez un projet à l'équipe pour accorder l'accès à ses membres.", "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.", - "join_team": "Rejoindre l'équipe", - "leave": "Quitter", - "leave_team": "Quitter l'équipe", - "leave_team_confirmation": "Es-tu sûr de vouloir quitter cette équipe ?", "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_removed_successfully": "Membre supprimé avec succès", "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.", - "members_added_successfully": "Membres ajoutés avec succès", - "no_members_found": "Aucun membre trouvé", - "no_other_teams_found": "Aucune autre équipe trouvée", - "org_owner_and_managers_can_only_be_team_admin": "Le propriétaire de l'organisation et les gestionnaires ne peuvent être que des administrateurs d'équipe.", - "organization_members": "Membres de l'organisation", - "organization_projects": "Projets d'organisation", "owner_role_description": "Les propriétaires ont un contrôle total sur l'organisation.", - "permission": "Permission", - "permission_updated_successfully": "Autorisation mise à jour avec succès.", "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.", - "project_name": "Nom du projet", - "project_removed_successfully": "Projet supprimé avec succès.", "read": "Lire", "read_write": "Lire et Écrire", - "remove": "Retirer", - "remove_member_confirmation": "Êtes-vous sûr de vouloir supprimer ce membre ?", - "remove_project": "Supprimer le projet", - "remove_project_confirmation": "Êtes-vous sûr de vouloir supprimer ce projet ?", - "role_updated_successfully": "Rôle mis à jour avec succès", - "select_type": "Choisir le type", "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_members": "Membres de l'équipe", "team_name": "Nom de l'équipe", - "team_name_description": "Donnez à votre équipe un nom descriptif.", "team_name_settings_title": "Paramètres de {teamName}", - "team_projects": "Projets d'équipe", "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.", - "this_action_cannot_be_undone_if_it_s_gone_it_s_gone": "Cette action ne peut pas être annulée. Si c'est parti, c'est parti.", "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.", - "upgrade_plan_notice_text_for_url_cloud": "mettez à niveau votre plan.", - "upgrade_plan_notice_text_for_url_enterprise": "obtenir une licence d'entreprise.", "you_are_a_member": "Vous êtes un membre" } }, @@ -1372,14 +1279,14 @@ "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": "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": "Tous les autres réponses continueront à", + "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 plus d'une image", + "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", @@ -1393,19 +1300,18 @@ "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'", + "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": "Étiquette du bouton", + "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", - "cannot_add_question_with_empty_headline_as_recall": "Impossible d'ajouter une question avec un titre vide en tant que rappel", "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}", @@ -1436,7 +1342,6 @@ "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", - "clone_edit_segment": "Cloner et modifier le segment", "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", @@ -1446,6 +1351,7 @@ "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", @@ -1475,20 +1381,17 @@ "does_not_include_one_of": "n'inclut pas un de", "does_not_start_with": "Ne commence pas par", "edit_recall": "Modifier le rappel", - "edit_segment": "Modifier le segment", - "edit_translations": "Modifier les traductions {language}", + "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}.", + "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_resetting_filters": "Erreur lors de la réinitialisation des filtres", "error_saving_changes": "Erreur lors de l'enregistrement des modifications", - "error_saving_segment": "Erreur lors de l'enregistrement du segment", "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", @@ -1517,7 +1420,6 @@ "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_replyTo_placeholder": "Écrivez une adresse e-mail et appuyez sur la barre d'espace", "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", @@ -1538,7 +1440,6 @@ "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", - "for_advanced_targeting_please": "Pour un ciblage avancé, s'il vous plaît", "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", @@ -1552,9 +1453,8 @@ "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_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", - "identifying_users": "identification des utilisateurs", "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", @@ -1565,7 +1465,6 @@ "inner_text": "Texte interne", "input_border_color": "Couleur de bordure d'entrée", "input_color": "Couleur d'entrée", - "invalid_segment": "Segment invalide", "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", @@ -1574,7 +1473,7 @@ "is_before": "Est avant", "is_booked": "Est réservé", "is_clicked": "Est cliqué", - "is_completely_submitted": "", + "is_completely_submitted": "Est complètement soumis", "is_partially_submitted": "Est partiellement soumis", "is_skipped": "Est ignoré", "is_submitted": "Est soumis", @@ -1595,32 +1494,29 @@ "long_answer": "Longue réponse", "lower_label": "Étiquette inférieure", "manage_languages": "Gérer les langues", - "manage_languages_to_get_started": "Gérer les langues pour commencer", "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": "Étiquette du bouton \"Suivant", + "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_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_option_found": "Aucune option trouvée.", "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 {indexChoix}", - "option_used_in_logic_error": "Cette option est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord l'enlever de la logique.", + "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": "Remplace la période d'attente entre les enquêtes par {jours} jour(s).", - "overwrites_waiting_period_between_surveys_to_x_days": "Remplace la période d'attente entre les enquêtes par {jours} jour(s).", + "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.", @@ -1628,8 +1524,7 @@ "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", - "pre_segment_your_users_with_attributes_filters": "Précisez vos utilisateurs à l'avance avec des filtres d'attributs.", - "prevent_double_submission": "", + "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.", @@ -1639,7 +1534,7 @@ "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}.", + "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", @@ -1648,35 +1543,29 @@ "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_all_filters": "Supprimer tous les filtres", "remove_description": "Supprimer la description", "remove_translations": "Supprimer les traductions", - "remove_translations_warning": "Êtes-vous sûr de vouloir supprimer toutes les traductions de cette enquête ? Cette action ne peut pas être annulée.", "require_answer": "Réponse requise", "required": "Requis", - "reset_all_filters": "Réinitialiser tous les filtres", "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éponse doit dépasser le nombre de réponses reçues ({responseCount}).", + "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", - "save_as_new_segment": "Enregistrer en tant que nouveau segment", "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.", - "segment_saved_successfully": "Segment enregistré avec succès", "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_language_as_default_language": "Définir {language} comme langue par défaut", "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", @@ -1709,19 +1598,15 @@ "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 👉", - "target_audience": "Public cible", + "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_resets_all_filters_in_this_survey": "Cette action réinitialise tous les filtres de cette enquête.", "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_is_an_advanced_segment_please_upgrade_your_plan_to_edit_it": "This is an advanced segment. Please upgrade your plan to edit it.", - "this_segment_is_used_in_other_surveys_make_changes": "Ce segment est utilisé dans d'autres enquêtes. Apportez des modifications.", "this_setting_overwrites_your": "Ce paramètre écrase votre", "three_points": "3 points", "times": "fois", @@ -1735,7 +1620,6 @@ "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", - "upgrade_to_the_scale_plan": "passer au plan Scale.", "upload": "Télécharger", "upload_at_least_2_images": "Téléchargez au moins 2 images", "upper_label": "Étiquette supérieure", @@ -1743,7 +1627,6 @@ "url_filters": "Filtres d'URL", "url_not_supported": "URL non supportée", "use_with_caution": "À utiliser avec précaution", - "user_targeting_is_currently_only_available_when": "La ciblage des utilisateurs est actuellement disponible uniquement lorsque", "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.", @@ -1755,7 +1638,6 @@ "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.", - "with_the_formbricks_sdk": "avec le SDK Formbricks", "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.", @@ -1810,8 +1692,8 @@ "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", + "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", @@ -1852,13 +1734,13 @@ "filter_added_successfully": "Filtre ajouté avec succès", "filter_updated_successfully": "Filtre mis à jour avec succès", "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 👉", + "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": "", + "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", @@ -1888,12 +1770,10 @@ "mobile_app": "Application mobile", "no_response_matches_filter": "Aucune réponse ne correspond à votre filtre", "only_completed": "Uniquement terminé", - "open_options": "Ouvrir les options", "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_confirmation": "Êtes-vous sûr de vouloir publier ces résultats d'enquête au public ?", "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.", "results_are_public": "Les résultats sont publics.", "send_preview": "Envoyer un aperçu", @@ -1924,13 +1804,12 @@ "to_run_highly_targeted_surveys": "réaliser des enquêtes très ciblées.", "ttc_tooltip": "Temps moyen pour compléter l'enquête.", "unknown_question_type": "Type de question inconnu", - "unpublish": "Désactiver la publication", "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 🧘‍♂️", + "waiting_for_response": "En attente d'une réponse \uD83E\uDDD8‍♂️", "web_app": "application web", "were_working_on_sdks_for_flutter_swift_and_kotlin": "Nous travaillons sur des SDK pour Flutter, Swift et Kotlin.", "what_is_a_panel": "Qu'est-ce qu'un panneau ?", @@ -1940,7 +1819,7 @@ "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 💡", + "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é !" }, @@ -1956,7 +1835,8 @@ "multiple_industries": "Plusieurs secteurs", "use_this_template": "Utilisez ce modèle", "uses_branching_logic": "Cette enquête utilise une logique de branchement." - } + }, + "this_is_a_new_key": "Ceci est une clé mise à jour" }, "xm-templates": { "ces": "CES", @@ -1980,7 +1860,6 @@ }, "organizations": { "landing": { - "create_organization": "Créer une organisation", "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." }, @@ -2002,18 +1881,15 @@ "what_are_you_here_for": "Pourquoi êtes-vous ici ?" }, "settings": { - "app_channel_headline": "Faisons des recherches sur ce dont vos utilisateurs ont besoin !", "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", - "link_channel_headline": "", "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 ?", - "website_channel_headline": "Maximisons le potentiel de votre trafic web !" + "team_description": "Qui peut accéder à ce projet ?" } } } @@ -2044,7 +1920,7 @@ "setup": { "intro": { "get_started": "Commencer", - "made_with_love_in_kiel": "Fabriqué avec 🤍 en Allemagne", + "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.", @@ -2102,11 +1978,11 @@ "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_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_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.", @@ -2140,10 +2016,10 @@ "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_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_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.", @@ -2169,7 +2045,7 @@ "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_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] ?", @@ -2179,7 +2055,7 @@ "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": "", + "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.", @@ -2196,7 +2072,7 @@ "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_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 !", @@ -2204,7 +2080,7 @@ "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 😔 Parlez directement à notre PDG !", + "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", @@ -2237,15 +2113,15 @@ "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_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": "", + "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_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", @@ -2257,28 +2133,28 @@ "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_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": "", + "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_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_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_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", @@ -2294,17 +2170,17 @@ "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_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_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_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 ?", @@ -2318,7 +2194,7 @@ "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_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] ?", @@ -2333,8 +2209,8 @@ "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 👍", - "docs_feedback_question_1_choice_2": "Non 👎", + "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", @@ -2342,14 +2218,14 @@ "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_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_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.", @@ -2365,12 +2241,6 @@ "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_4_choice_1": "Extrêmement bien", - "employee_satisfaction_question_4_choice_2": "Très bien", - "employee_satisfaction_question_4_choice_3": "Modérément bien", - "employee_satisfaction_question_4_choice_4": "Légèrement bien", - "employee_satisfaction_question_4_choice_5": "Pas du tout bien", - "employee_satisfaction_question_4_headline": "Dans quelle mesure pensez-vous que votre travail est reconnu ?", "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", @@ -2405,8 +2275,8 @@ "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 🤸

", + "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", @@ -2434,14 +2304,6 @@ "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...", - "example_app_survey_name": "Enquête sur l'application exemple", - "example_app_survey_question_1_button_label": "Faisons-le !", - "example_app_survey_question_1_headline": "L'application est connectée avec succès", - "example_app_survey_question_1_html": "Vous êtes prêt. Créez votre propre enquête pour vos utilisateurs d'application.", - "example_website_survey_name": "Enquête sur le site web exemple", - "example_website_survey_question_1_button_label": "Faisons-le !", - "example_website_survey_question_1_headline": "Site web connecté avec succès 🎉", - "example_website_survey_question_1_html": "Vous êtes prêt. Créez votre propre enquête pour les visiteurs du site web 👇", "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 ?", @@ -2464,8 +2326,8 @@ "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 🐞", - "feedback_box_question_1_choice_2": "Demande de fonctionnalité 💡", + "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é ?", @@ -2480,9 +2342,8 @@ "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 👋

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

Passez une excellente journée !

", + "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...", @@ -2494,18 +2355,13 @@ "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_customer_goals_question_1_choice_1": "Comprendre profondément ma base d'utilisateurs.", - "identify_customer_goals_question_1_choice_2": "Identifier les opportunités de vente incitative", - "identify_customer_goals_question_1_choice_3": "Construire le meilleur produit possible", - "identify_customer_goals_question_1_choice_4": "Règle le monde pour faire des choux de Bruxelles au petit-déjeuner pour tout le monde.", - "identify_customer_goals_question_1_headline": "Quel est votre objectif principal en utilisant {{projectName}} ?", "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_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", @@ -2513,8 +2369,8 @@ "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_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...", @@ -2525,17 +2381,17 @@ "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": "", + "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 🙏

", - "identify_sign_up_barriers_with_project_name": "Barrières d'inscription {{projectName}}", + "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}} ?", + "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.", @@ -2543,10 +2399,10 @@ "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_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_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...", @@ -2576,9 +2432,7 @@ "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_3_button_label": "Suivant", - "improve_trial_conversion_question_3_headline": "Qu'attendiez-vous que {{projectName}} fasse pour vous ?", + "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.", @@ -2595,34 +2449,34 @@ "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_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": "", - "interview_prompt_question_1_headline": "Avez-vous 15 minutes pour nous parler ? 🙏", + "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_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_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_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_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_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", @@ -2631,7 +2485,7 @@ "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_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", @@ -2650,8 +2504,8 @@ "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_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", @@ -2694,15 +2548,14 @@ "next": "Suivant", "nps": "Score de Promoteur Net (NPS)", "nps_description": "Mesurer le Net Promoter Score (0-10)", - "nps_headline": "Quelle est la probabilité que vous recommandiez {{projectName}} à un ami ou un collègue ?", "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_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_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 ?", @@ -2757,17 +2610,16 @@ "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_headline": "Comment pourrions-nous améliorer votre expérience avec $[projectName] ?", "prioritize_features_question_3_placeholder": "Entrez votre réponse ici...", - "product_csat": "CSAT {{projectName}}", "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_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_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.", @@ -2778,7 +2630,7 @@ "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_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", @@ -2787,9 +2639,9 @@ "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_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", @@ -2860,11 +2712,11 @@ "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_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 🙏 Veuillez écrire un avis pour nous !", + "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 ?", @@ -2906,23 +2758,27 @@ "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_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 🙏 Veuillez écrire un avis pour nous !", + "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_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 🙏 Veuillez écrire un avis pour nous !", + "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", @@ -2944,7 +2800,7 @@ "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_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", @@ -2960,8 +2816,8 @@ "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_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...", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index 40171fe4c0..3fcc70e46b 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -5,8 +5,8 @@ "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", "forgot-password": { - "an_error_occurred_when_logging": "Ocorreu um erro ao fazer login", "back_to_login": "Voltar para o login", "email-sent": { "heading": "Pedido de redefinição de senha feito com sucesso", @@ -26,22 +26,21 @@ }, "invite": { "create_account": "Cria uma conta", - "email_does_not_match": "Opa! Email errado 🤦", + "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 🤗", + "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 😥", + "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 😥", + "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 🎉", + "welcome_to_organization": "Você tá dentro \uD83C\uDF89", "welcome_to_organization_description": "Bem-vindo à organização." }, "last_used": "Usado por último", "login": { - "an_error_occurred_when_logging_you_in": "Ocorreu um erro ao fazer login", "backup_code": "Código de backup", "create_an_account": "Cria uma conta", "enter_your_backup_code": "Digite seu código de backup", @@ -51,13 +50,10 @@ "login_with_email": "Entrar com Email", "lost_access": "Perdeu o acesso?", "new_to_formbricks": "Novo no Formbricks?", - "too_many_requests_please_try_again_after_some_time": "Muitas solicitações, por favor tente novamente mais tarde!", - "two_factor_authentication_code": "código de autenticação de dois fatores", "use_a_backup_code": "Usar um código de backup" }, "signup": { "captcha_failed": "reCAPTCHA falhou", - "error": "Ocorreu um erro ao te cadastrar", "have_an_account": "Já tem uma conta?", "log_in": "Fazer login", "password_validation_contain_at_least_1_number": "Conter pelo menos 1 número", @@ -85,7 +81,7 @@ "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 e-mail para {email}.", + "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": { @@ -124,10 +120,8 @@ "app": "app", "app_survey": "Pesquisa de App", "apply_filters": "Aplicar filtros", - "archive": "Arquivo", "are_you_sure": "Certeza?", "are_you_sure_this_action_cannot_be_undone": "Tem certeza? Essa ação não pode ser desfeita.", - "attribute_type": "Tipo de Atributo", "attributes": "atributos", "automatic": "Automático", "avatar": "Avatar", @@ -143,7 +137,6 @@ "clear_filters": "Limpar filtros", "clear_selection": "Limpar seleção", "click": "Clica", - "click_here_to_upload": "Clique aqui para enviar", "clicks": "cliques", "close": "Fechar", "code": "Código", @@ -154,7 +147,6 @@ "connect": "Conectar", "connect_formbricks": "Conectar Formbricks", "connected": "conectado", - "contact": "Contato", "contacts": "Contatos", "copied_to_clipboard": "Copiado para a área de transferência", "copy": "Copiar", @@ -196,9 +188,6 @@ "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", - "experience": "experiência", - "failed_to_get_first_environment_of_user": "Falha ao obter a primeira ambiente do usuário", - "filters_reset_successfully": "Filtros redefinidos com sucesso", "finish": "Terminar", "follow_these": "Siga esses", "formbricks_version": "Versão do Formbricks", @@ -213,7 +202,6 @@ "hidden_fields": "Campos ocultos", "hide": "esconder", "hide_column": "Ocultar coluna", - "hide_filters": "Esconder filtros", "image": "imagem", "images": "Imagens", "import": "importar", @@ -231,7 +219,6 @@ "key": "Chave", "label": "Etiqueta", "language": "Língua", - "languages": "Idiomas", "learn_more": "Saiba mais", "license": "Licença", "light_overlay": "sobreposição leve", @@ -251,8 +238,6 @@ "maximum": "Máximo", "member": "Membros", "members": "Membros", - "membership_not_found": "Assinatura não encontrada", - "meta": "Meta", "metadata": "metadados", "minimum": "Mínimo", "mobile_overlay_text": "O Formbricks não está disponível para dispositivos com resoluções menores.", @@ -264,7 +249,7 @@ "neutral": "Neutro", "new": "Novo", "new_survey": "Nova Pesquisa", - "new_version_available": "O Formbricks {version} chegou. Atualize agora!", + "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", @@ -272,11 +257,9 @@ "no_result_found": "Nenhum resultado encontrado", "no_results": "Nenhum resultado", "no_surveys_found": "Não foram encontradas pesquisas.", - "none": "Nenhum", "not_authenticated": "Você não está autenticado para realizar essa ação.", "not_authorized": "Não autorizado", "not_connected": "Desconectado", - "not_now": "Agora não", "note": "Nota", "notes": "Anotações", "notifications": "Notificações", @@ -284,21 +267,17 @@ "off": "desligado", "on": "ligado", "only_one_file_allowed": "É permitido apenas um arquivo", - "only_organization_owners_and_managers_can_access_this_setting": "Apenas proprietários e gerentes da organização podem acessar essa configuração.", "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.", - "only_owners_managers_and_team_admins_can_perform_this_action": "Apenas proprietários, gerentes e administradores de equipe podem realizar essa ação.", "or": "ou", "organization": "organização", "organization_not_found": "Organização não encontrada", "organization_teams_not_found": "Equipes da organização não encontradas", "other": "outro", - "other_filters": "Outros filtros", "others": "Outros", "overview": "Visão Geral", "password": "Senha", "paused": "Pausado", "pending_downgrade": "Rebaixamento Pendente", - "people": "Pessoas", "people_manager": "Gerente de Pessoas", "person": "Pessoa", "phone": "Celular", @@ -314,8 +293,10 @@ "privacy": "Política de Privacidade", "privacy_policy": "Política de Privacidade", "product_manager": "Gerente de Produto", + "product_not_found": "Produto não encontrado", "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", @@ -327,11 +308,8 @@ "questions": "Perguntas", "read_docs": "Ler Documentos", "remove": "remover", - "removed": "Removido", "reorder_and_hide_columns": "Reordenar e ocultar colunas", "report_survey": "Relatório de Pesquisa", - "request_an_enterprise_license": "pedir uma licença Enterprise", - "reset_all_filters": "Redefinir todos os filtros", "reset_to_default": "Restaurar para o padrão", "response": "Resposta", "responses": "Respostas", @@ -342,7 +320,6 @@ "sales": "vendas", "save": "Salvar", "save_changes": "Salvar alterações", - "saved": "Salvo", "scheduled": "agendado", "search": "Buscar", "security": "Segurança", @@ -357,7 +334,6 @@ "selections": "seleções", "send": "Enviar", "send_test_email": "Enviar e-mail de teste", - "sent": "Enviado", "session_not_found": "Sessão não encontrada", "settings": "Configurações", "share_feedback": "Compartilhar feedback", @@ -365,7 +341,6 @@ "show_response_count": "Mostrar contagem de respostas", "shown": "mostrado", "size": "Tamanho", - "skip": "Pular", "skipped": "Pulou", "skips": "Pula", "some_files_failed_to_upload": "Alguns arquivos falharam ao enviar", @@ -387,7 +362,7 @@ "survey_type": "Tipo de Pesquisa", "surveys": "pesquisas", "switch_organization": "Mudar organização", - "switch_to": "Mudar para {ambiente}", + "switch_to": "Mudar para {environment}", "table_items_deleted_successfully": "{type}s deletados com sucesso", "table_settings": "Arrumação da mesa", "tags": "etiquetas", @@ -395,7 +370,6 @@ "team": "Time", "team_access": "Acesso da equipe", "team_name": "Nome da equipe", - "team_not_found": "Equipe não encontrada", "teams": "Times", "teams_not_found": "Equipes não encontradas", "text": "Texto", @@ -406,12 +380,10 @@ "top_right": "Canto Superior Direito", "try_again": "Tenta de novo", "type": "Tipo", - "unarchive": "Desarquivar", "unlock_more_projects_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.", "update": "atualizar", "updated": "atualizado", "updated_at": "Atualizado em", - "upgrade_now": "Atualize agora", "upload": "Enviar", "upload_input_description": "Clique ou arraste para fazer o upload de arquivos.", "url": "URL", @@ -422,12 +394,10 @@ "variables": "Variáveis", "verified_email": "Email Verificado", "video": "vídeo", - "view_filters": "Ver filtros", "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": "site", "website_and_app_connection": "Conexão de Site e App", "website_app_survey": "Pesquisa de Site e App", "website_survey": "Pesquisa de Site", @@ -437,34 +407,46 @@ "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 seu limite de {projectLimit} projeto.", + "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 {data}." + "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 🕵️", + "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", @@ -472,36 +454,45 @@ "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 🙏", + "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 🤍", + "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 👋", + "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", + "powered_by_formbricks": "Desenvolvido por Formbricks", + "privacy_policy": "Política de Privacidade", + "reject": "Rejeitar", + "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í 👋", + "survey_response_finished_email_hey": "E aí \uD83D\uDC4B", "survey_response_finished_email_this_form": "esse formulário", "survey_response_finished_email_turn_off_notifications": "Desativar notificações para", "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_view_more_responses": "Ver mais {responseCount} respostas", - "survey_response_finished_email_view_responses": "Ver 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 👋", + "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", @@ -512,12 +503,14 @@ "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_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": { @@ -568,7 +561,6 @@ "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.", - "this_is_a_code_action_you_can_only_change_the_description": "Essa é uma ação de código. Você só pode mudar a descrição.", "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", @@ -578,13 +570,6 @@ "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" }, - "attributes": { - "ex_user_property": "Ex. Propriedade do usuário", - "how_to_add_attributes": "Como adicionar Atributos", - "show_archived": "Mostrar arquivados", - "this_attribute_was_added_automatically_you_cannot_make_changes_to_it": "Esse atributo foi adicionado automaticamente. Você não pode fazer alterações nele.", - "this_is_a_code_attribute_you_can_only_change_the_description": "Esse é um atributo de código. Você só pode mudar a descrição." - }, "connect": { "congrats": "Parabéns!", "connection_successful_message": "Mandou bem! Estamos conectados.", @@ -602,19 +587,12 @@ "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", - "error_fetching_next_page_of_people_data": "Erro ao buscar a próxima página de dados de contatos", - "error_fetching_people_data": "Erro ao buscar os dados de contatos", - "fetching_user": "Carregando usuário", "first_name": "Primeiro Nome", - "formbricks_id": "Formbricks Id (interno)", - "how_to_add_contacts": "Como adicionar contatos", "last_name": "Sobrenome", - "loading_user_responses": "Carregando respostas do usuário", "no_responses_found": "Nenhuma resposta encontrada", "not_provided": "Não fornecido", "search_contact": "Buscar contato", "select_attribute": "Selecionar Atributo", - "sessions": "Sessões", "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.", @@ -646,18 +624,15 @@ "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": "🌤️ Boa tarde", - "good_evening": "🌙 Boa noite", + "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}", - "negative": "Negativo", "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.", - "positive": "positivo", "praise": "elogio", - "sentiment": "sentimento", "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", @@ -668,6 +643,7 @@ }, "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", @@ -715,13 +691,12 @@ "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": "gerenciar", "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 workspace {workspace}", + "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.", @@ -738,11 +713,13 @@ "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}. Use uma coluna do tipo {mapped_type}.", + "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" + "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", @@ -752,7 +729,7 @@ "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 {equipe}", + "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", @@ -773,6 +750,7 @@ "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!", @@ -805,8 +783,6 @@ "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_keys": "Chaves de API", - "api_keys_description": "Gerencie suas chaves de API.", "dev_api_keys": "Chaves do Ambiente de Desenvolvimento", "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", "no_api_keys_yet": "Você ainda não tem nenhuma chave de API", @@ -824,7 +800,7 @@ "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} é seu.", + "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.", @@ -840,8 +816,8 @@ "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.", - "open_the_browser_console_to_see_the_logs_2": "Abre o console do navegador pra ver os logs.", - "receiving_data": "Recebendo dados 💃🕺", + "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", @@ -851,21 +827,19 @@ "tag_of_your_app": "etiqueta do seu app", "to_the": "pro", "to_the_url_where_you_load_the": "para a URL onde você carrega o", - "to_the_url_where_you_load_the_formbricks_sdk": "para a URL onde você carrega o SDK do Formbricks.", "want_to_learn_how_to_add_user_attributes": "Quer aprender como adicionar atributos de usuário, eventos personalizados e mais?", "you_also_need_to_pass_a": "você também precisa passar um", - "you_are_done": "Você terminou 🎉", + "you_are_done": "Você terminou \uD83C\uDF89", "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} incluindo todas as pesquisas, respostas, pessoas, ações e atributos.", + "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", - "organization_name": "Nome da Organização", "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", @@ -874,8 +848,7 @@ "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?", - "you_left_the_organization_successfully": "Você saiu da organização com sucesso" + "whats_your_project_called": "Como é o nome do seu projeto?" }, "languages": { "add_language": "Adicionar idioma", @@ -917,9 +890,6 @@ "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.", - "formbricks_branding_upgrade_message": "Para remover a marca Formbricks dos Link Surveys, por favor", - "formbricks_branding_upgrade_message_in_app": "Para remover a marca do Formbricks das Pesquisas In-app, por favor", - "formbricks_branding_upgrade_text": "atualize seu plano.", "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", @@ -931,7 +901,7 @@ "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 de {tipo}", + "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", @@ -956,18 +926,10 @@ "unique_constraint_failed_on_the_fields": "Falha na restrição única nos campos" }, "teams": { - "add_existing_team": "Adicionar equipe existente", - "create_new_team": "Criar nova equipe", - "manage": "Gerenciar", "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", - "read": "Leitura", - "read_write": "Leitura & Escrita", - "remove_access": "Remover acesso", - "remove_access_confirmation": "Tem certeza de que deseja remover o acesso para esta equipe?", - "select_teams": "Selecionar equipes", "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." } @@ -976,7 +938,6 @@ "segments": { "add_filter_below": "Adicionar filtro abaixo", "add_your_first_filter_to_get_started": "Adicione seu primeiro filtro para começar", - "advance_segment_cannot_be_edited_upgrade_your_plan": "Este é um segmento avançado, você não pode editá-lo. Por favor, faça um upgrade no seu plano para editar este segmento.", "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", @@ -989,13 +950,10 @@ "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", - "failed_to_fetch_segments": "Falha ao buscar segmentos.", "filters_reset_successfully": "Filtros redefinidos com sucesso", - "for_advanced_targeting_please": "Para uma segmentação avançada, por favor", "here": "aqui", "hide_filters": "Esconder filtros", "identifying_users": "identificando usuários", - "invalid_filters_please_check_the_filters_and_try_again": "Filtros inválidos. Por favor, verifique os filtros e tente novamente.", "invalid_segment": "Segmento inválido", "invalid_segment_filters": "Filtros inválidos. Por favor, verifique os filtros e tente novamente.", "load_segment": "Segmento de Carga", @@ -1023,8 +981,6 @@ "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", - "upgrade_your_plan": "atualize seu plano.", - "upgrade_your_plan_to_create_more_than_5_segments": "Faça um upgrade no seu plano para criar mais de 5 segmentos.", "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.", @@ -1084,7 +1040,7 @@ "startup": "startup", "startup_description": "Tudo no Grátis com recursos adicionais.", "switch_plan": "Mudar Plano", - "switch_plan_confirmation_text": "Tem certeza de que quer mudar para o plano {plan}? Você vai ser cobrado {price} por mês.", + "switch_plan_confirmation_text": "Tem certeza de que deseja mudar para o plano {plan}? Você será cobrado {price} por mês.", "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", @@ -1121,7 +1077,6 @@ "your_enterprise_license_is_active_all_features_unlocked": "Sua licença empresarial está ativa. Todos os recursos estão desbloqueados." }, "general": { - "add_member": "Adicionar membro", "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.", @@ -1134,10 +1089,11 @@ "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, digite {organizationName} no campo abaixo para confirmar a exclusão definitiva desta organização:", + "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": "Olá {userName}", + "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", @@ -1147,7 +1103,6 @@ "from_your_organization": "da sua organização", "invitation_sent_once_more": "Convite enviado de novo.", "invite_deleted_successfully": "Convite deletado com sucesso", - "invite_organization_member": "Convidar Membro da Organização", "invited_on": "Convidado em {date}", "invites_failed": "Convites falharam", "leave_organization": "Sair da organização", @@ -1169,36 +1124,24 @@ "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_required": "Nome da organização é obrigatório.", "organization_name_updated_successfully": "Nome da organização atualizado com sucesso", "organization_settings": "Configurações da Organização", - "ownership_transferred_successfully": "Propriedade transferida com sucesso", "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", - "send_invitation": "Enviar 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", - "there_can_only_be_one_owner_of_each_organization": "Só pode ter um dono de cada organização. Se você transferir sua propriedade para", - "to_confirm": "pra confirmar:", - "type_in": "Digita aí", - "upgrade_plan_notice_text_for_url_cloud": "atualize seu plano.", - "upgrade_plan_notice_text_for_url_enterprise": "conseguir uma Licença Empresarial.", - "when_you_transfer_the_ownership_you_will_remain_an_admin_of_the_organization": "Quando você transferir a propriedade, você vai continuar sendo um Admin da organização.", - "you_will_lose_all_of_your_ownership_rights": "você vai perder todos os seus direitos de propriedade." + "test_email_sent_successfully": "E-mail de teste enviado com sucesso" }, "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.", - "manage_email_preferences": "Gerencie suas preferências de e-mail", "need_slack_or_discord_notifications": "Preciso de notificações no Slack ou Discord", - "notification_settings": "Configurações de Notificação", "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", @@ -1226,7 +1169,6 @@ "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:", - "org_deletion_warning": "Se você for o único membro de uma organização ou não houver outro administrador presente, a organização será deletada de forma irreversível junto com todos os dados associados.", "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", @@ -1237,8 +1179,6 @@ "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.", - "the_2fa_otp_is_incorrect_please_try_again": "O código OTP de 2FA está incorreto. Por favor, tente novamente.", - "to_enable_two_factor_authentication_you_need_an_active": "Pra ativar a autenticação de dois fatores, você precisa de um ativo", "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.", @@ -1251,11 +1191,7 @@ "you_must_select_a_file": "Você tem que selecionar um arquivo." }, "teams": { - "add_member": "Adicionar membro", - "add_members": "Adicionar membros", "add_members_description": "Adicione membros à equipe e determine sua função.", - "add_project": "Adicionar projeto", - "add_projects": "Adicionar projetos", "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.", @@ -1267,64 +1203,36 @@ "create_first_team_message": "Você precisa criar uma equipe primeiro.", "create_new_team": "Criar nova equipe", "delete_team": "Excluir equipe", - "empty_project_message": "Você ainda não adicionou nenhum projetos. Atribua um projeto à equipe para conceder acesso aos membros.", "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.", - "join_team": "Entrar na equipe", - "leave": "Sair", - "leave_team": "Sair da equipe", - "leave_team_confirmation": "Tem certeza de que deseja sair desta equipe?", "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_removed_successfully": "Membro removido com sucesso", "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.", - "members_added_successfully": "Membros adicionados com sucesso", - "no_members_found": "Nenhum membro encontrado", - "no_other_teams_found": "Nenhuma outra equipe encontrada", - "org_owner_and_managers_can_only_be_team_admin": "Proprietários e gerentes da organização só podem ser administradores da equipe.", - "organization_members": "Membros da organização", - "organization_projects": "Projetos da organização", "owner_role_description": "Os proprietários têm controle total sobre a organização.", - "permission": "Permissão", - "permission_updated_successfully": "Permissão atualizada com sucesso.", "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.", - "project_name": "Nome do projeto", - "project_removed_successfully": "Projeto removido com sucesso.", "read": "Leitura", "read_write": "Leitura & Escrita", - "remove": "Remover", - "remove_member_confirmation": "Tem certeza de que deseja remover este membro?", - "remove_project": "Remover projeto", - "remove_project_confirmation": "Tem certeza de que deseja remover este projeto?", - "role_updated_successfully": "Função atualizada com sucesso", - "select_type": "Selecionar tipo", "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_members": "Membros da equipe", "team_name": "Nome da equipe", - "team_name_description": "Dê um nome descritivo à sua equipe.", "team_name_settings_title": "Configurações de {teamName}", - "team_projects": "Projetos da equipe", "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.", - "this_action_cannot_be_undone_if_it_s_gone_it_s_gone": "Esta ação não pode ser desfeita. Se foi, foi.", "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.", - "upgrade_plan_notice_text_for_url_cloud": "atualize seu plano.", - "upgrade_plan_notice_text_for_url_enterprise": "conseguir uma Licença Empresarial.", "you_are_a_member": "Você é um membro" } }, @@ -1371,7 +1279,7 @@ "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": "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", @@ -1404,7 +1312,6 @@ "button_url": "URL do Botão", "cal_username": "Nome de usuário do Cal.com ou nome de usuário/evento", "calculate": "Calcular", - "cannot_add_question_with_empty_headline_as_recall": "Não dá pra adicionar pergunta com título vazio como lembrete", "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}", @@ -1435,7 +1342,6 @@ "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", - "clone_edit_segment": "Clonar e Editar Segmento", "close_survey_on_date": "Fechar pesquisa na data", "close_survey_on_response_limit": "Fechar pesquisa ao atingir limite de respostas", "color": "cor", @@ -1445,6 +1351,7 @@ "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", @@ -1474,8 +1381,7 @@ "does_not_include_one_of": "Não inclui um de", "does_not_start_with": "Não começa com", "edit_recall": "Editar Lembrete", - "edit_segment": "Editar Segmento", - "edit_translations": "Editar traduções de {language}", + "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", @@ -1485,9 +1391,7 @@ "equals": "Igual", "equals_one_of": "É igual a um de", "error_publishing_survey": "Ocorreu um erro ao publicar a pesquisa.", - "error_resetting_filters": "Erro ao redefinir filtros", "error_saving_changes": "Erro ao salvar alterações", - "error_saving_segment": "Erro ao salvar segmento", "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", @@ -1516,7 +1420,6 @@ "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_replyTo_placeholder": "Escreva um endereço de e-mail e pressione a barra de espaço", "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", @@ -1537,7 +1440,6 @@ "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", - "for_advanced_targeting_please": "Para uma segmentação avançada, por favor", "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", @@ -1551,9 +1453,8 @@ "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 nas Pesquisas {surveyTypeDerived}", + "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", - "identifying_users": "identificando usuários", "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", @@ -1564,12 +1465,10 @@ "inner_text": "Texto Interno", "input_border_color": "Cor da borda de entrada", "input_color": "Cor de entrada", - "invalid_segment": "Segmento inválido", "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", @@ -1595,7 +1494,6 @@ "long_answer": "resposta longa", "lower_label": "Etiqueta Inferior", "manage_languages": "Gerenciar Idiomas", - "manage_languages_to_get_started": "Gerencie Idiomas para começar", "max_file_size": "Tamanho máximo do arquivo", "max_file_size_limit_is": "Tamanho máximo do arquivo é", "multiply": "Multiplicar *", @@ -1603,9 +1501,8 @@ "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": "Não foram encontradas imagens para '{query}'", + "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_option_found": "Não foi encontrada nenhuma opção.", "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.", @@ -1613,22 +1510,20 @@ "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": "Essa opção é usada na lógica da pergunta {questionIndex}. Por favor, remova ela da lógica primeiro.", + "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": "Substitui o período de espera entre pesquisas para {days} dia(s).", "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": "Foto {idx}", + "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", - "pre_segment_your_users_with_attributes_filters": "Pré-segmente seus usuários com filtros de atributos.", "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", @@ -1648,35 +1543,29 @@ "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_all_filters": "Remover todos os filtros", "remove_description": "Remover descrição", "remove_translations": "Remover traduções", - "remove_translations_warning": "Tem certeza de que quer remover todas as traduções desta pesquisa? Essa ação não pode ser desfeita.", "require_answer": "Preciso de Resposta", "required": "Obrigatório", - "reset_all_filters": "Redefinir todos os filtros", "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 ser maior que o número de respostas recebidas ({responseCount}).", + "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", - "save_as_new_segment": "Salvar como novo Segmento", "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.", - "segment_saved_successfully": "Segmento salvo com sucesso", "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_language_as_default_language": "Definir {idioma} como idioma padrão", "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", @@ -1686,7 +1575,7 @@ "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 {percentual}% dos usuários-alvo", + "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.", @@ -1709,19 +1598,15 @@ "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 👉", - "target_audience": "Público-alvo", + "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_resets_all_filters_in_this_survey": "Essa ação reseta todos os filtros dessa pesquisa.", "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_is_an_advanced_segment_please_upgrade_your_plan_to_edit_it": "Esse é um segmento avançado. Por favor, faça um upgrade no seu plano para editá-lo.", - "this_segment_is_used_in_other_surveys_make_changes": "Esse segmento é usado em outras pesquisas. Faça mudanças", "this_setting_overwrites_your": "Essa configuração sobrescreve seu", "three_points": "3 pontos", "times": "times", @@ -1735,7 +1620,6 @@ "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", - "upgrade_to_the_scale_plan": "faça um upgrade para o plano Scale.", "upload": "Enviar", "upload_at_least_2_images": "Faz o upload de pelo menos 2 imagens", "upper_label": "Etiqueta Superior", @@ -1743,7 +1627,6 @@ "url_filters": "Filtros de URL", "url_not_supported": "URL não suportada", "use_with_caution": "Use com cuidado", - "user_targeting_is_currently_only_available_when": "A segmentação de usuários está disponível atualmente apenas quando", "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.", @@ -1755,7 +1638,6 @@ "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.", - "with_the_formbricks_sdk": "com o SDK do Formbricks.", "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.", @@ -1810,7 +1692,7 @@ "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": "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)", @@ -1852,7 +1734,7 @@ "filter_added_successfully": "Filtro adicionado com sucesso", "filter_updated_successfully": "Filtro atualizado com sucesso", "formbricks_email_survey_preview": "Prévia da Pesquisa por E-mail do Formbricks", - "go_to_setup_checklist": "Vai para a Lista de Configuração 👉", + "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", @@ -1888,12 +1770,10 @@ "mobile_app": "app de celular", "no_response_matches_filter": "Nenhuma resposta corresponde ao seu filtro", "only_completed": "Somente concluído", - "open_options": "Abre opções", "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_confirmation": "Você tem certeza de que quer publicar 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.", "results_are_public": "Os resultados são públicos", "send_preview": "Enviar prévia", @@ -1924,13 +1804,12 @@ "to_run_highly_targeted_surveys": "fazer pesquisas altamente direcionadas.", "ttc_tooltip": "Tempo médio para completar a pesquisa.", "unknown_question_type": "Tipo de pergunta desconhecido", - "unpublish": "Despublicar", "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 🧘‍♂️", + "waiting_for_response": "Aguardando uma resposta \uD83E\uDDD8‍♂️", "web_app": "aplicativo web", "were_working_on_sdks_for_flutter_swift_and_kotlin": "Estamos trabalhando em SDKs para Flutter, Swift e Kotlin.", "what_is_a_panel": "O que é um painel?", @@ -1940,7 +1819,7 @@ "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 💡", + "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!" }, @@ -1956,7 +1835,8 @@ "multiple_industries": "várias indústrias", "use_this_template": "Use esse modelo", "uses_branching_logic": "Essa pesquisa usa lógica de ramificação." - } + }, + "this_is_a_new_key": "Esta é uma chave atualizada" }, "xm-templates": { "ces": "CES", @@ -1980,7 +1860,6 @@ }, "organizations": { "landing": { - "create_organization": "Criar organização", "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." }, @@ -2002,18 +1881,15 @@ "what_are_you_here_for": "O que você tá fazendo aqui?" }, "settings": { - "app_channel_headline": "Vamos pesquisar o que seus usuários precisam!", "brand_color": "cor da marca", "brand_color_description": "Combine a cor principal das pesquisas com a sua marca.", "create_new_team": "Criar nova equipe", - "link_channel_headline": "Você mantém um produto, que empolgante!", "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?", - "website_channel_headline": "Vamos aproveitar ao máximo o tráfego do seu site!" + "team_description": "Quem pode acessar este projeto?" } } } @@ -2044,7 +1920,7 @@ "setup": { "intro": { "get_started": "Começar", - "made_with_love_in_kiel": "Feito com 🤍 em Alemanha", + "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", @@ -2102,11 +1978,11 @@ "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_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_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.", @@ -2140,10 +2016,10 @@ "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_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_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.", @@ -2169,7 +2045,7 @@ "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_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]?", @@ -2196,7 +2072,7 @@ "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_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!", @@ -2204,7 +2080,7 @@ "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 😔 Fala direto com nosso CEO!", + "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", @@ -2237,7 +2113,7 @@ "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_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", @@ -2245,7 +2121,7 @@ "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_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", @@ -2257,28 +2133,28 @@ "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_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_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_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_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", @@ -2294,17 +2170,17 @@ "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_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_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_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?", @@ -2318,7 +2194,7 @@ "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_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]?", @@ -2333,8 +2209,8 @@ "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 👍", - "docs_feedback_question_1_choice_2": "Não 👎", + "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", @@ -2342,14 +2218,14 @@ "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_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_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.", @@ -2365,12 +2241,6 @@ "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_4_choice_1": "Muito bem mesmo", - "employee_satisfaction_question_4_choice_2": "Muito bem", - "employee_satisfaction_question_4_choice_3": "Mais ou menos bem", - "employee_satisfaction_question_4_choice_4": "Mais ou menos bem", - "employee_satisfaction_question_4_choice_5": "Nada bem", - "employee_satisfaction_question_4_headline": "Quão bem você sente que seu trabalho é reconhecido?", "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", @@ -2405,8 +2275,8 @@ "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 🤸", + "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", @@ -2434,14 +2304,6 @@ "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...", - "example_app_survey_name": "Pesquisa de app de exemplo", - "example_app_survey_question_1_button_label": "Bora fazer isso!", - "example_app_survey_question_1_headline": "App conectado com sucesso", - "example_app_survey_question_1_html": "Tá tudo pronto. Crie sua própria pesquisa pros usuários do seu app.", - "example_website_survey_name": "Pesquisa de exemplo de site", - "example_website_survey_question_1_button_label": "Bora fazer isso!", - "example_website_survey_question_1_headline": "Site conectado com sucesso 🎉", - "example_website_survey_question_1_html": "Você está pronto. Crie sua própria pesquisa para os visitantes do site 👇", "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ê?", @@ -2464,8 +2326,8 @@ "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 🐞", - "feedback_box_question_1_choice_2": "Pedido de Recurso 💡", + "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?", @@ -2480,9 +2342,8 @@ "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 👋

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

Tenha um ótimo dia!

", + "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...", @@ -2494,18 +2355,13 @@ "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_customer_goals_question_1_choice_1": "Entender profundamente minha base de usuários", - "identify_customer_goals_question_1_choice_2": "Identificar oportunidades de upsell", - "identify_customer_goals_question_1_choice_3": "Construa o melhor produto possível", - "identify_customer_goals_question_1_choice_4": "Dominar o mundo pra fazer café da manhã de couve de bruxelas pra todo mundo.", - "identify_customer_goals_question_1_headline": "Qual é o seu principal objetivo ao usar {{projectName}}?", "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_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", @@ -2513,8 +2369,8 @@ "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_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...", @@ -2527,15 +2383,15 @@ "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 🙏", - "identify_sign_up_barriers_with_project_name": "Barreiras de Cadastro do {{projectName}}", + "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}}?", + "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", @@ -2543,10 +2399,10 @@ "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_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_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...", @@ -2576,9 +2432,7 @@ "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_3_button_label": "Próximo", - "improve_trial_conversion_question_3_headline": "O que você esperava que {{projectName}} fizesse por você?", + "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.", @@ -2595,34 +2449,34 @@ "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_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? 🙏", + "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_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_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_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_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_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", @@ -2631,7 +2485,7 @@ "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_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", @@ -2650,8 +2504,8 @@ "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_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", @@ -2694,15 +2548,14 @@ "next": "Próximo", "nps": "Pontuação de Promotores Líquidos (NPS)", "nps_description": "Medir o Net-Promoter-Score (0-10)", - "nps_headline": "Qual a chance de você recomendar {{projectName}} para um amigo ou colega?", "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_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_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?", @@ -2757,17 +2610,16 @@ "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_headline": "De que outra forma poderíamos melhorar sua experiência com $[projectName]?", "prioritize_features_question_3_placeholder": "Digite sua resposta aqui...", - "product_csat": "{{projectName}} Satisfação do Cliente", "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_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_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.", @@ -2778,7 +2630,7 @@ "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_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", @@ -2787,9 +2639,9 @@ "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_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", @@ -2860,11 +2712,11 @@ "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_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 🙏 Por favor, escreva uma avaliação pra gente!", + "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?", @@ -2906,23 +2758,27 @@ "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_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 🙏 Por favor, escreva uma avaliação pra gente!", + "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_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 🙏 Por favor, escreva uma avaliação pra gente!", + "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", @@ -2944,7 +2800,7 @@ "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_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", @@ -2960,8 +2816,8 @@ "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_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...", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json new file mode 100644 index 0000000000..083647fa52 --- /dev/null +++ b/packages/lib/messages/zh-Hant-TW.json @@ -0,0 +1,2840 @@ +{ + "auth": { + "continue_with_azure": "使用 Azure 繼續", + "continue_with_email": "使用電子郵件繼續", + "continue_with_github": "使用 GitHub 繼續", + "continue_with_google": "使用 Google 繼續", + "continue_with_oidc": "使用 '{'oidcDisplayName'}' 繼續", + "continue_with_openid": "使用 OpenID 繼續", + "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": "使用備份碼" + }, + "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": "屬性", + "automatic": "自動", + "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": "嵌入", + "enable": "啟用", + "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": "一般", + "get_started": "開始使用", + "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_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": "成員", + "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_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": "產品經理", + "product_not_found": "找不到產品", + "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": "報告問卷", + "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_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 密碼已變更", + "powered_by_formbricks": "由 Formbricks 提供技術支援", + "privacy_policy": "隱私權政策", + "reject": "拒絕", + "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_this_form": "這個表單", + "survey_response_finished_email_turn_off_notifications": "關閉通知,適用於", + "survey_response_finished_email_turn_off_notifications_for_all_new_forms": "關閉所有新建立表單的通知", + "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 金鑰", + "add_env_api_key": "新增 '{'environmentType'}' 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 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。", + "dev_api_keys": "開發環境金鑰", + "dev_api_keys_description": "為您的開發環境新增和移除 API 金鑰。", + "no_api_keys_yet": "您還沒有任何 API 金鑰", + "prod_api_keys": "生產環境金鑰", + "prod_api_keys_description": "為您的生產環境新增和移除 API 金鑰。", + "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 連線", + "function": "函式", + "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": "到", + "to_the_url_where_you_load_the": "到您載入", + "want_to_learn_how_to_add_user_attributes": "想瞭解如何新增使用者屬性、自訂事件等嗎?", + "you_also_need_to_pass_a": "您還需要傳遞", + "you_are_done": "您已完成 \uD83C\uDF89", + "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_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": { + "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": "多語言問卷", + "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'}'。", + "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": "測試電子郵件已成功發送" + }, + "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": "管理您的密碼和其他安全性設定。", + "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_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_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_partially_submitted": "已部分提交", + "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": "每個連結只接受一次提交。以下是如何操作。", + "current_selection_csv": "目前選取 (CSV)", + "current_selection_excel": "目前選取 (Excel)", + "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": "篩選器已成功更新", + "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": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。", + "how_to_embed_a_survey_on_your_react_native_app": "如何在您的 React Native 應用程式中嵌入問卷", + "how_to_embed_a_survey_on_your_web_app": "如何在您的 Web 應用程式中嵌入問卷", + "identify_users": "識別使用者", + "identify_users_and_set_attributes": "識別使用者並設定屬性", + "identify_users_description": "您有電子郵件地址或使用者 ID 嗎?將其附加到網址。", + "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": "去年", + "learn_how_to": "瞭解如何", + "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": "您的問卷結果將會是公開的。任何組織外的人員都可以存取這些結果(如果他們有連結)。", + "results_are_public": "結果是公開的", + "send_preview": "發送預覽", + "send_to_panel": "發送到小組", + "setup_instructions": "設定說明", + "setup_instructions_for_react_native_apps": "React Native 應用程式的設定說明", + "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_app_with_formbricks": "以將您的應用程式與 Formbricks 連線", + "to_connect_your_web_app_with_formbricks": "以將您的 Web 應用程式與 Formbricks 連線", + "to_connect_your_website_with_formbricks": "以將您的網站與 Formbricks 連線", + "to_run_highly_targeted_surveys": "以執行高度目標化的問卷。", + "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 應用程式", + "were_working_on_sdks_for_flutter_swift_and_kotlin": "我們正在開發適用於 Flutter、Swift 和 Kotlin 的 SDK。", + "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": "此問卷使用分支邏輯。" + }, + "this_is_a_new_key": "這是一個更新的 key" + }, + "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": "使用視覺指標來擷取客戶接觸點的回饋。" + } + }, + "health": { + "degraded": "您的 Formbricks 運作狀況已降低", + "healthy": "所有系統都已啟動並執行" + }, + "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": "此免費且開源的問卷已關閉。", + "could_not_create_display": "無法建立顯示", + "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": "我們已收到此電子郵件地址的回應。", + "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/organization/service.ts b/packages/lib/organization/service.ts index 0516a70387..4b07687f02 100644 --- a/packages/lib/organization/service.ts +++ b/packages/lib/organization/service.ts @@ -380,7 +380,8 @@ export const getMonthlyOrganizationResponseCount = reactCache( export const subscribeOrganizationMembersToSurveyResponses = async ( surveyId: string, - createdBy: string + createdBy: string, + organizationId: string ): Promise => { try { const surveyCreator = await prisma.user.findUnique({ @@ -393,6 +394,10 @@ export const subscribeOrganizationMembersToSurveyResponses = async ( throw new ResourceNotFoundError("User", createdBy); } + if (surveyCreator.notificationSettings?.unsubscribedOrganizationIds?.includes(organizationId)) { + return; + } + const defaultSettings = { alert: {}, weeklySummary: {} }; const updatedNotificationSettings: TUserNotificationSettings = { ...defaultSettings, diff --git a/packages/lib/package.json b/packages/lib/package.json index 3e3e6a110c..9f23a0abb0 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -15,38 +15,38 @@ "test": "dotenv -e ../../.env -- vitest run" }, "dependencies": { - "@ai-sdk/azure": "1.0.10", - "@aws-sdk/client-s3": "3.712.0", - "@aws-sdk/s3-presigned-post": "3.712.0", - "@aws-sdk/s3-request-presigner": "3.712.0", + "@ai-sdk/azure": "1.1.9", + "@aws-sdk/client-s3": "3.741.0", + "@aws-sdk/s3-presigned-post": "3.741.0", + "@aws-sdk/s3-request-presigner": "3.741.0", "@formbricks/api": "workspace:*", "@formbricks/database": "workspace:*", "@formbricks/types": "workspace:*", "@paralleldrive/cuid2": "2.2.2", - "@t3-oss/env-nextjs": "0.11.1", - "@ungap/structured-clone": "1.2.1", - "aws-crt": "1.24.0", + "@t3-oss/env-nextjs": "0.12.0", + "@ungap/structured-clone": "1.3.0", + "aws-crt": "1.25.3", "date-fns": "4.1.0", "jsonwebtoken": "9.0.2", "markdown-it": "14.1.0", "mime-types": "2.1.35", "nanoid": "5.0.9", "next-auth": "4.24.11", - "posthog-node": "4.3.2", + "posthog-node": "4.4.1", "qrcode": "1.5.4", "server-only": "0.0.1", "superjson": "2.2.2", - "tailwind-merge": "2.5.5" + "tailwind-merge": "3.0.1" }, "devDependencies": { "@formbricks/config-typescript": "workspace:*", "@formbricks/eslint-config": "workspace:*", - "@types/jsonwebtoken": "9.0.7", + "@types/jsonwebtoken": "9.0.8", "@types/mime-types": "2.1.4", "@types/ungap__structured-clone": "1.2.0", "dotenv": "16.4.7", "ts-node": "10.9.2", - "vitest": "2.1.8", + "vitest": "3.0.5", "vitest-mock-extended": "2.0.2" } } diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts index 5a1b0532c4..5102986003 100644 --- a/packages/lib/response/service.ts +++ b/packages/lib/response/service.ts @@ -663,37 +663,3 @@ export const getResponseCountBySurveyId = reactCache( } )() ); - -export const getIfResponseWithSurveyIdAndEmailExist = reactCache( - async (surveyId: string, email: string): Promise => - cache( - async () => { - validateInputs([surveyId, ZId], [email, ZString]); - - try { - const response = await prisma.response.findFirst({ - where: { - surveyId, - data: { - path: ["verifiedEmail"], - equals: email, - }, - }, - select: { id: true }, - }); - - return !!response; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getIfResponseWithSurveyIdAndEmailExist-${surveyId}-${email}`], - { - tags: [responseCache.tag.bySurveyId(surveyId)], - } - )() -); diff --git a/packages/lib/response/tests/response.test.ts b/packages/lib/response/tests/response.test.ts index 3959ef74d1..492dd6e74d 100644 --- a/packages/lib/response/tests/response.test.ts +++ b/packages/lib/response/tests/response.test.ts @@ -8,8 +8,10 @@ import { mockMeta, mockResponse, mockResponseData, - mockResponseNote, // mockResponseWithMockPerson, - mockSingleUseId, // mockSurvey, + mockResponseNote, + // mockResponseWithMockPerson, + mockSingleUseId, + // mockSurvey, mockSurveyId, mockSurveySummaryOutput, mockTags, diff --git a/packages/lib/styling/constants.ts b/packages/lib/styling/constants.ts index c9a7758d0c..83956b9c01 100644 --- a/packages/lib/styling/constants.ts +++ b/packages/lib/styling/constants.ts @@ -1,7 +1,5 @@ // https://github.com/airbnb/javascript/#naming--uppercase import { TProjectStyling } from "@formbricks/types/project"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { translate } from "../templates"; export const COLOR_DEFAULTS = { brandColor: "#64748b", @@ -50,115 +48,3 @@ export const defaultStyling: TProjectStyling = { appSurveys: "straight", }, }; - -export const getPreviewSurvey = (locale: string, projectName: string) => { - return { - id: "cltxxaa6x0000g8hacxdxejeu", - createdAt: new Date(), - updatedAt: new Date(), - name: translate("preview_survey_name", locale), - type: "link", - environmentId: "cltwumfcz0009echxg02fh7oa", - createdBy: "cltwumfbz0000echxysz6ptvq", - status: "inProgress", - welcomeCard: { - html: { - default: translate("preview_survey_welcome_card_html", locale), - }, - enabled: false, - headline: { - default: translate("preview_survey_welcome_card_headline", locale), - }, - timeToFinish: false, - showResponseCount: false, - }, - styling: null, - segment: null, - questions: [ - { - id: "lbdxozwikh838yc6a8vbwuju", - type: "rating", - range: 5, - scale: "star", - isDraft: true, - headline: { - default: translate("preview_survey_question_1_headline", locale, { projectName }), - }, - required: true, - subheader: { - default: translate("preview_survey_question_1_subheader", locale), - }, - lowerLabel: { - default: translate("preview_survey_question_1_lower_label", locale), - }, - upperLabel: { - default: translate("preview_survey_question_1_upper_label", locale), - }, - }, - { - id: "rjpu42ps6dzirsn9ds6eydgt", - type: "multipleChoiceSingle", - choices: [ - { - id: "x6wty2s72v7vd538aadpurqx", - label: { - default: translate("preview_survey_question_2_choice_1_label", locale), - }, - }, - { - id: "fbcj4530t2n357ymjp2h28d6", - label: { - default: translate("preview_survey_question_2_choice_2_label", locale), - }, - }, - ], - isDraft: true, - headline: { - default: translate("preview_survey_question_2_headline", locale), - }, - backButtonLabel: { - default: translate("preview_survey_question_2_back_button_label", locale), - }, - required: true, - shuffleOption: "none", - }, - ], - endings: [ - { - id: "cltyqp5ng000108l9dmxw6nde", - type: "endScreen", - headline: { default: translate("preview_survey_ending_card_headline", locale) }, - subheader: { default: translate("preview_survey_ending_card_description", locale) }, - }, - ], - hiddenFields: { - enabled: true, - fieldIds: [], - }, - variables: [], - displayOption: "displayOnce", - recontactDays: null, - displayLimit: null, - autoClose: null, - runOnDate: null, - closeOnDate: null, - delay: 0, - displayPercentage: null, - autoComplete: 50, - isVerifyEmailEnabled: false, - isSingleResponsePerEmailEnabled: false, - redirectUrl: null, - projectOverwrites: null, - surveyClosedMessage: null, - singleUse: { - enabled: false, - isEncrypted: true, - }, - pin: null, - resultShareKey: null, - languages: [], - triggers: [], - showLanguageSwitch: false, - followUps: [], - } as TSurvey; -}; diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index e0ccedb55a..6aa137c92f 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -1,14 +1,10 @@ import "server-only"; -import { createId } from "@paralleldrive/cuid2"; -import { Prisma } from "@prisma/client"; +import { ActionClass, Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { TActionClass } from "@formbricks/types/action-classes"; import { ZOptionalNumber } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/common"; -import { TEnvironment } from "@formbricks/types/environment"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { TProject } from "@formbricks/types/project"; import { TSegment, ZSegmentFilters } from "@formbricks/types/segment"; import { TSurvey, @@ -19,21 +15,15 @@ import { ZSurvey, ZSurveyCreateInput, } from "@formbricks/types/surveys/types"; -import { actionClassCache } from "../actionClass/cache"; import { getActionClasses } from "../actionClass/service"; import { cache } from "../cache"; import { segmentCache } from "../cache/segment"; import { ITEMS_PER_PAGE } from "../constants"; -import { getEnvironment } from "../environment/service"; import { getOrganizationByEnvironmentId, subscribeOrganizationMembersToSurveyResponses, } from "../organization/service"; -import { structuredClone } from "../pollyfills/structuredClone"; import { capturePosthogEnvironmentEvent } from "../posthogServer"; -import { projectCache } from "../project/cache"; -import { getProjectByEnvironmentId } from "../project/service"; -import { responseCache } from "../response/cache"; import { getIsAIEnabled } from "../utils/ai"; import { validateInputs } from "../utils/validate"; import { surveyCache } from "./cache"; @@ -132,7 +122,7 @@ export const selectSurvey = { followUps: true, } satisfies Prisma.SurveySelect; -const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: TActionClass[]) => { +const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => { if (!triggers) return; // check if all the triggers are valid @@ -153,7 +143,7 @@ const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: TAc const handleTriggerUpdates = ( updatedTriggers: TSurvey["triggers"], currentTriggers: TSurvey["triggers"], - actionClasses: TActionClass[] + actionClasses: ActionClass[] ) => { if (!updatedTriggers) return {}; checkTriggersValidity(updatedTriggers, actionClasses); @@ -348,37 +338,6 @@ export const getSurveyCount = reactCache( )() ); -export const getInProgressSurveyCount = reactCache( - async (environmentId: string, filterCriteria?: TSurveyFilterCriteria): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); - try { - const surveyCount = await prisma.survey.count({ - where: { - environmentId: environmentId, - status: "inProgress", - ...buildWhereClause(filterCriteria), - }, - }); - - return surveyCount; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getInProgressSurveyCount-${environmentId}-${JSON.stringify(filterCriteria)}`], - { - tags: [surveyCache.tag.byEnvironmentId(environmentId)], - } - )() -); - export const updateSurvey = async (updatedSurvey: TSurvey): Promise => { validateInputs([updatedSurvey, ZSurvey]); @@ -747,67 +706,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => } }; -export const deleteSurvey = async (surveyId: string) => { - validateInputs([surveyId, ZId]); - - try { - const deletedSurvey = await prisma.survey.delete({ - where: { - id: surveyId, - }, - select: selectSurvey, - }); - - if (deletedSurvey.type === "app" && deletedSurvey.segment?.isPrivate) { - const deletedSegment = await prisma.segment.delete({ - where: { - id: deletedSurvey.segment.id, - }, - }); - - if (deletedSegment) { - segmentCache.revalidate({ - id: deletedSegment.id, - environmentId: deletedSurvey.environmentId, - }); - } - } - - responseCache.revalidate({ - surveyId, - environmentId: deletedSurvey.environmentId, - }); - surveyCache.revalidate({ - id: deletedSurvey.id, - environmentId: deletedSurvey.environmentId, - resultShareKey: deletedSurvey.resultShareKey ?? undefined, - }); - - if (deletedSurvey.segment?.id) { - segmentCache.revalidate({ - id: deletedSurvey.segment.id, - environmentId: deletedSurvey.environmentId, - }); - } - - // Revalidate public triggers by actionClassId - deletedSurvey.triggers.forEach((trigger) => { - surveyCache.revalidate({ - actionClassId: trigger.actionClass.id, - }); - }); - - return deletedSurvey; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); - } - - throw error; - } -}; - export const createSurvey = async ( environmentId: string, surveyBody: TSurveyCreateInput @@ -963,7 +861,7 @@ export const createSurvey = async ( }); if (createdBy) { - await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy); + await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy, organization.id); } await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", { @@ -981,258 +879,6 @@ export const createSurvey = async ( } }; -export const copySurveyToOtherEnvironment = async ( - environmentId: string, - surveyId: string, - targetEnvironmentId: string, - userId: string -) => { - validateInputs([environmentId, ZId], [surveyId, ZId], [targetEnvironmentId, ZId], [userId, ZId]); - - try { - const isSameEnvironment = environmentId === targetEnvironmentId; - - // Fetch required resources - const [existingEnvironment, existingProject, existingSurvey] = await Promise.all([ - getEnvironment(environmentId), - getProjectByEnvironmentId(environmentId), - getSurvey(surveyId), - ]); - - if (!existingEnvironment) throw new ResourceNotFoundError("Environment", environmentId); - if (!existingProject) throw new ResourceNotFoundError("Project", environmentId); - if (!existingSurvey) throw new ResourceNotFoundError("Survey", surveyId); - - let targetEnvironment: TEnvironment | null = null; - let targetProject: TProject | null = null; - - if (isSameEnvironment) { - targetEnvironment = existingEnvironment; - targetProject = existingProject; - } else { - [targetEnvironment, targetProject] = await Promise.all([ - getEnvironment(targetEnvironmentId), - getProjectByEnvironmentId(targetEnvironmentId), - ]); - - if (!targetEnvironment) throw new ResourceNotFoundError("Environment", targetEnvironmentId); - if (!targetProject) throw new ResourceNotFoundError("Project", targetEnvironmentId); - } - - const { - environmentId: _, - createdBy, - id: existingSurveyId, - createdAt, - updatedAt, - ...restExistingSurvey - } = existingSurvey; - const hasLanguages = existingSurvey.languages && existingSurvey.languages.length > 0; - - // Prepare survey data - const surveyData: Prisma.SurveyCreateInput = { - ...restExistingSurvey, - id: createId(), - name: `${existingSurvey.name} (copy)`, - type: existingSurvey.type, - status: "draft", - welcomeCard: structuredClone(existingSurvey.welcomeCard), - questions: structuredClone(existingSurvey.questions), - endings: structuredClone(existingSurvey.endings), - variables: structuredClone(existingSurvey.variables), - hiddenFields: structuredClone(existingSurvey.hiddenFields), - languages: hasLanguages - ? { - create: existingSurvey.languages.map((surveyLanguage) => ({ - language: { - connectOrCreate: { - where: { - projectId_code: { code: surveyLanguage.language.code, projectId: targetProject.id }, - }, - create: { - code: surveyLanguage.language.code, - alias: surveyLanguage.language.alias, - projectId: targetProject.id, - }, - }, - }, - default: surveyLanguage.default, - enabled: surveyLanguage.enabled, - })), - } - : undefined, - triggers: { - create: existingSurvey.triggers.map((trigger): Prisma.SurveyTriggerCreateWithoutSurveyInput => { - const baseActionClassData = { - name: trigger.actionClass.name, - environment: { connect: { id: targetEnvironmentId } }, - description: trigger.actionClass.description, - type: trigger.actionClass.type, - }; - - if (isSameEnvironment) { - return { - actionClass: { connect: { id: trigger.actionClass.id } }, - }; - } else if (trigger.actionClass.type === "code") { - return { - actionClass: { - connectOrCreate: { - where: { - key_environmentId: { key: trigger.actionClass.key!, environmentId: targetEnvironmentId }, - }, - create: { - ...baseActionClassData, - key: trigger.actionClass.key, - }, - }, - }, - }; - } else { - return { - actionClass: { - connectOrCreate: { - where: { - name_environmentId: { - name: trigger.actionClass.name, - environmentId: targetEnvironmentId, - }, - }, - create: { - ...baseActionClassData, - noCodeConfig: trigger.actionClass.noCodeConfig - ? structuredClone(trigger.actionClass.noCodeConfig) - : undefined, - }, - }, - }, - }; - } - }), - }, - environment: { - connect: { - id: targetEnvironmentId, - }, - }, - creator: { - connect: { - id: userId, - }, - }, - surveyClosedMessage: existingSurvey.surveyClosedMessage - ? structuredClone(existingSurvey.surveyClosedMessage) - : Prisma.JsonNull, - singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull, - projectOverwrites: existingSurvey.projectOverwrites - ? structuredClone(existingSurvey.projectOverwrites) - : Prisma.JsonNull, - styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull, - segment: undefined, - followUps: { - createMany: { - data: existingSurvey.followUps.map((followUp) => ({ - name: followUp.name, - trigger: followUp.trigger, - action: followUp.action, - })), - }, - }, - }; - - // Handle segment - if (existingSurvey.segment) { - if (existingSurvey.segment.isPrivate) { - surveyData.segment = { - create: { - title: surveyData.id!, - isPrivate: true, - filters: existingSurvey.segment.filters, - environment: { connect: { id: targetEnvironmentId } }, - }, - }; - } else if (isSameEnvironment) { - surveyData.segment = { connect: { id: existingSurvey.segment.id } }; - } else { - const existingSegmentInTargetEnvironment = await prisma.segment.findFirst({ - where: { - title: existingSurvey.segment.title, - isPrivate: false, - environmentId: targetEnvironmentId, - }, - }); - - surveyData.segment = { - create: { - title: existingSegmentInTargetEnvironment - ? `${existingSurvey.segment.title}-${Date.now()}` - : existingSurvey.segment.title, - isPrivate: false, - filters: existingSurvey.segment.filters, - environment: { connect: { id: targetEnvironmentId } }, - }, - }; - } - } - - const targetProjectLanguageCodes = targetProject.languages.map((language) => language.code); - const newSurvey = await prisma.survey.create({ - data: surveyData, - select: selectSurvey, - }); - - // Identify newly created action classes - const newActionClasses = newSurvey.triggers.map((trigger) => trigger.actionClass); - - // Revalidate cache only for newly created action classes - for (const actionClass of newActionClasses) { - actionClassCache.revalidate({ - environmentId: actionClass.environmentId, - name: actionClass.name, - id: actionClass.id, - }); - } - - let newLanguageCreated = false; - if (existingSurvey.languages && existingSurvey.languages.length > 0) { - const targetLanguageCodes = newSurvey.languages.map((lang) => lang.language.code); - newLanguageCreated = targetLanguageCodes.length > targetProjectLanguageCodes.length; - } - - // Invalidate caches - if (newLanguageCreated) { - projectCache.revalidate({ id: targetProject.id, environmentId: targetEnvironmentId }); - } - - surveyCache.revalidate({ - id: newSurvey.id, - environmentId: newSurvey.environmentId, - resultShareKey: newSurvey.resultShareKey ?? undefined, - }); - - existingSurvey.triggers.forEach((trigger) => { - surveyCache.revalidate({ - actionClassId: trigger.actionClass.id, - }); - }); - - if (newSurvey.segment) { - segmentCache.revalidate({ - id: newSurvey.segment.id, - environmentId: newSurvey.environmentId, - }); - } - - return newSurvey; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); - } - throw error; - } -}; - export const getSurveyIdByResultShareKey = reactCache( async (resultShareKey: string): Promise => cache( diff --git a/packages/lib/survey/tests/survey.test.ts b/packages/lib/survey/tests/survey.test.ts index 05b26dca87..95555ca362 100644 --- a/packages/lib/survey/tests/survey.test.ts +++ b/packages/lib/survey/tests/survey.test.ts @@ -1,35 +1,17 @@ import { prisma } from "../../__mocks__/database"; -import { mockResponseNote, mockResponseWithMockPerson } from "../../response/tests/__mocks__/data.mock"; import { Prisma } from "@prisma/client"; import { evaluateLogic } from "surveyLogic/utils"; import { beforeEach, describe, expect, it } from "vitest"; import { testInputValidation } from "vitestSetup"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { getSurvey, getSurveyCount, getSurveys, getSurveysByActionClassId, updateSurvey } from "../service"; import { - copySurveyToOtherEnvironment, - createSurvey, - deleteSurvey, - getSurvey, - getSurveyCount, - getSurveys, - getSurveysByActionClassId, - updateSurvey, -} from "../service"; -import { - createSurveyInput, mockActionClass, - mockContactAttributeKey, - mockDisplay, - mockEnvironment, mockId, mockOrganizationOutput, - mockPrismaPerson, - mockProject, mockSurveyOutput, mockSurveyWithLogic, - mockSyncSurveyOutput, mockTransformedSurveyOutput, - mockTransformedSyncSurveyOutput, mockUser, updateSurveyInput, } from "./__mock__/survey.mock"; @@ -325,111 +307,90 @@ describe("Tests for updateSurvey", () => { }); }); -describe("Tests for deleteSurvey", () => { - describe("Happy Path", () => { - it("Deletes a survey successfully", async () => { - prisma.survey.delete.mockResolvedValueOnce(mockSurveyOutput); - const deletedSurvey = await deleteSurvey(mockId); - expect(deletedSurvey).toEqual(mockSurveyOutput); - }); - }); +// describe("Tests for createSurvey", () => { +// beforeEach(() => { +// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]); +// }); - describe("Sad Path", () => { - testInputValidation(deleteSurvey, "123#"); +// describe("Happy Path", () => { +// it("Creates a survey successfully", async () => { +// prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput); +// prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput); +// prisma.actionClass.findMany.mockResolvedValue([mockActionClass]); +// prisma.user.findMany.mockResolvedValueOnce([ +// { +// ...mockUser, +// twoFactorSecret: null, +// backupCodes: null, +// password: null, +// identityProviderAccountId: null, +// groupId: null, +// role: "engineer", +// }, +// ]); +// prisma.user.update.mockResolvedValueOnce({ +// ...mockUser, +// twoFactorSecret: null, +// backupCodes: null, +// password: null, +// identityProviderAccountId: null, +// groupId: null, +// role: "engineer", +// }); +// const createdSurvey = await createSurvey(mockId, createSurveyInput); +// expect(createdSurvey).toEqual(mockTransformedSurveyOutput); +// }); +// }); - it("should throw an error if there is an unknown error", async () => { - const mockErrorMessage = "Unknown error occurred"; - prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); - prisma.survey.delete.mockRejectedValue(new Error(mockErrorMessage)); - await expect(deleteSurvey(mockId)).rejects.toThrow(Error); - }); - }); -}); +// describe("Sad Path", () => { +// testInputValidation(createSurvey, "123#", createSurveyInput); -describe("Tests for createSurvey", () => { - beforeEach(() => { - prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]); - }); +// it("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); +// }); +// }); +// }); - describe("Happy Path", () => { - it("Creates a survey successfully", async () => { - prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput); - prisma.organization.findFirst.mockResolvedValueOnce(mockOrganizationOutput); - prisma.actionClass.findMany.mockResolvedValue([mockActionClass]); - prisma.user.findMany.mockResolvedValueOnce([ - { - ...mockUser, - twoFactorSecret: null, - backupCodes: null, - password: null, - identityProviderAccountId: null, - groupId: null, - role: "engineer", - }, - ]); - prisma.user.update.mockResolvedValueOnce({ - ...mockUser, - twoFactorSecret: null, - backupCodes: null, - password: null, - identityProviderAccountId: null, - groupId: null, - role: "engineer", - }); - const createdSurvey = await createSurvey(mockId, createSurveyInput); - expect(createdSurvey).toEqual(mockTransformedSurveyOutput); - }); - }); +// describe("Tests for duplicateSurvey", () => { +// beforeEach(() => { +// prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]); +// }); - describe("Sad Path", () => { - testInputValidation(createSurvey, "123#", createSurveyInput); +// describe("Happy Path", () => { +// it("Duplicates a survey successfully", async () => { +// prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); +// prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput); +// // @ts-expect-error +// prisma.environment.findUnique.mockResolvedValueOnce(mockEnvironment); +// // @ts-expect-error +// prisma.project.findFirst.mockResolvedValueOnce(mockProject); +// prisma.actionClass.findFirst.mockResolvedValueOnce(mockActionClass); +// prisma.actionClass.create.mockResolvedValueOnce(mockActionClass); - it("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); - }); - }); -}); +// const createdSurvey = await copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId); +// expect(createdSurvey).toEqual(mockSurveyOutput); +// }); +// }); -describe("Tests for duplicateSurvey", () => { - beforeEach(() => { - prisma.actionClass.findMany.mockResolvedValueOnce([mockActionClass]); - }); +// describe("Sad Path", () => { +// testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#"); - describe("Happy Path", () => { - it("Duplicates a survey successfully", async () => { - prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput); - prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput); - // @ts-expect-error - prisma.environment.findUnique.mockResolvedValueOnce(mockEnvironment); - // @ts-expect-error - prisma.project.findFirst.mockResolvedValueOnce(mockProject); - prisma.actionClass.findFirst.mockResolvedValueOnce(mockActionClass); - prisma.actionClass.create.mockResolvedValueOnce(mockActionClass); +// it("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 +// ); +// }); - const createdSurvey = await copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId); - expect(createdSurvey).toEqual(mockSurveyOutput); - }); - }); - - describe("Sad Path", () => { - testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#"); - - it("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 () => { - const mockErrorMessage = "Unknown error occurred"; - prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage)); - await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId)).rejects.toThrow(Error); - }); - }); -}); +// it("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); +// }); +// }); +// }); // describe("Tests for getSyncSurveys", () => { // describe("Happy Path", () => { diff --git a/packages/lib/utils/questions.tsx b/packages/lib/utils/questions.tsx index 10227f0a8b..490b97faf7 100644 --- a/packages/lib/utils/questions.tsx +++ b/packages/lib/utils/questions.tsx @@ -1,4 +1,5 @@ import { createId } from "@paralleldrive/cuid2"; +import { TFnType } from "@tolgee/react"; import { ArrowUpFromLineIcon, CalendarDaysIcon, @@ -37,7 +38,6 @@ import { TSurveyRankingQuestion, TSurveyRatingQuestion, } from "@formbricks/types/surveys/types"; -import { translate } from "../templates"; import { replaceQuestionPresetPlaceholders } from "./templates"; export type TQuestion = { @@ -48,25 +48,25 @@ export type TQuestion = { preset: any; }; -export const getQuestionTypes = (locale: string): TQuestion[] => [ +export const getQuestionTypes = (t: TFnType): TQuestion[] => [ { id: QuestionId.OpenText, - label: translate("free_text", locale), - description: translate("free_text_description", locale), + label: t("templates.free_text"), + description: t("templates.free_text_description"), icon: MessageSquareTextIcon, preset: { headline: { default: "" }, - placeholder: { default: translate("free_text_placeholder", locale) }, + placeholder: { default: t("templates.free_text_placeholder") }, longAnswer: true, inputType: "text", - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.MultipleChoiceSingle, - label: translate("single_select", locale), - description: translate("single_select_description", locale), + label: t("templates.single_select"), + description: t("templates.single_select_description"), icon: Rows3Icon, preset: { headline: { default: "" }, @@ -81,14 +81,14 @@ export const getQuestionTypes = (locale: string): TQuestion[] => [ }, ], shuffleOption: "none", - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.MultipleChoiceMulti, - label: translate("multi_select", locale), - description: translate("multi_select_description", locale), + label: t("templates.multi_select"), + description: t("templates.multi_select_description"), icon: ListIcon, preset: { headline: { default: "" }, @@ -107,55 +107,55 @@ export const getQuestionTypes = (locale: string): TQuestion[] => [ }, ], shuffleOption: "none", - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.PictureSelection, - label: translate("picture_selection", locale), - description: translate("picture_selection_description", locale), + label: t("templates.picture_selection"), + description: t("templates.picture_selection_description"), icon: ImageIcon, preset: { headline: { default: "" }, allowMulti: true, choices: [], - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Rating, - label: translate("rating", locale), - description: translate("rating_description", locale), + label: t("templates.rating"), + description: t("templates.rating_description"), icon: StarIcon, preset: { headline: { default: "" }, scale: "star", range: 5, - lowerLabel: { default: translate("rating_lower_label", locale) }, - upperLabel: { default: translate("rating_upper_label", locale) }, - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + lowerLabel: { default: t("templates.rating_lower_label") }, + upperLabel: { default: t("templates.rating_upper_label") }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.NPS, - label: translate("nps", locale), - description: translate("nps_description", locale), + label: t("templates.nps"), + description: t("templates.nps_description"), icon: PresentationIcon, preset: { headline: { default: "" }, - lowerLabel: { default: translate("nps_lower_label", locale) }, - upperLabel: { default: translate("nps_upper_label", locale) }, - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + lowerLabel: { default: t("templates.nps_lower_label") }, + upperLabel: { default: t("templates.nps_upper_label") }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Ranking, - label: translate("ranking", locale), - description: translate("ranking_description", locale), + label: t("templates.ranking"), + description: t("templates.ranking_description"), icon: ListOrderedIcon, preset: { headline: { @@ -171,14 +171,14 @@ export const getQuestionTypes = (locale: string): TQuestion[] => [ label: { default: "" }, }, ], - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Matrix, - label: translate("matrix", locale), - description: translate("matrix_description", locale), + label: t("templates.matrix"), + description: t("templates.matrix_description"), icon: Grid3X3Icon, preset: { headline: { @@ -186,79 +186,79 @@ export const getQuestionTypes = (locale: string): TQuestion[] => [ }, rows: [{ default: "" }, { default: "" }], columns: [{ default: "" }, { default: "" }], - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.CTA, - label: translate("statement_call_to_action", locale), - description: translate("cta_description", locale), + label: t("templates.statement_call_to_action"), + description: t("templates.cta_description"), icon: MousePointerClickIcon, preset: { headline: { default: "" }, html: { default: "", }, - buttonLabel: { default: translate("book_interview", locale) }, + buttonLabel: { default: t("templates.book_interview") }, buttonExternal: false, - dismissButtonLabel: { default: translate("skip", locale) }, - backButtonLabel: { default: translate("back", locale) }, + dismissButtonLabel: { default: t("templates.skip") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Consent, - label: translate("consent", locale), - description: translate("consent_description", locale), + label: t("templates.consent"), + description: t("templates.consent_description"), icon: CheckIcon, preset: { headline: { default: "" }, html: { default: "" }, label: { default: "" }, - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.FileUpload, - label: translate("file_upload", locale), - description: translate("file_upload_description", locale), + label: t("templates.file_upload"), + description: t("templates.file_upload_description"), icon: ArrowUpFromLineIcon, preset: { headline: { default: "" }, allowMultipleFiles: false, - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Date, - label: translate("date", locale), - description: translate("date_description", locale), + label: t("templates.date"), + description: t("templates.date_description"), icon: CalendarDaysIcon, preset: { headline: { default: "" }, format: "M-d-y", - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Cal, - label: translate("schedule_a_meeting", locale), - description: translate("schedule_a_meeting_description", locale), + label: t("templates.schedule_a_meeting"), + description: t("templates.schedule_a_meeting_description"), icon: PhoneIcon, preset: { headline: { default: "" }, calUserName: "rick/get-rick-rolled", - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.Address, - label: translate("address", locale), - description: translate("address_description", locale), + label: t("templates.address"), + description: t("templates.address_description"), icon: HomeIcon, preset: { headline: { default: "" }, @@ -268,14 +268,14 @@ export const getQuestionTypes = (locale: string): TQuestion[] => [ state: { show: true, required: true, placeholder: { default: "State" } }, zip: { show: true, required: true, placeholder: { default: "Zip" } }, country: { show: true, required: true, placeholder: { default: "Country" } }, - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, { id: QuestionId.ContactInfo, - label: translate("contact_info", locale), - description: translate("contact_info_description", locale), + label: t("templates.contact_info"), + description: t("templates.contact_info_description"), icon: ContactIcon, preset: { headline: { default: "" }, @@ -284,14 +284,14 @@ export const getQuestionTypes = (locale: string): TQuestion[] => [ email: { show: true, required: true, placeholder: { default: "Email" } }, phone: { show: true, required: true, placeholder: { default: "Phone" } }, company: { show: true, required: true, placeholder: { default: "Company" } }, - buttonLabel: { default: translate("next", locale) }, - backButtonLabel: { default: translate("back", locale) }, + buttonLabel: { default: t("templates.next") }, + backButtonLabel: { default: t("templates.back") }, } as Partial, }, ]; -export const getCXQuestionTypes = (locale: string) => - getQuestionTypes(locale).filter((questionType) => { +export const getCXQuestionTypes = (t: TFnType) => + getQuestionTypes(t).filter((questionType) => { return [ TSurveyQuestionTypeEnum.OpenText, TSurveyQuestionTypeEnum.MultipleChoiceSingle, @@ -303,18 +303,17 @@ export const getCXQuestionTypes = (locale: string) => ].includes(questionType.id as TSurveyQuestionTypeEnum); }); -export const QUESTIONS_ICON_MAP: Record = getQuestionTypes( - "en-US" -).reduce( - (prev, curr) => ({ - ...prev, - [curr.id]: , - }), - {} as Record -); +export const getQuestionIconMap = (t: TFnType): Record => + getQuestionTypes(t).reduce( + (prev, curr) => ({ + ...prev, + [curr.id]: , + }), + {} as Record + ); -export const getQuestionNameMap = (locale: string) => - getQuestionTypes(locale).reduce( +export const getQuestionNameMap = (t: TFnType) => + getQuestionTypes(t).reduce( (prev, curr) => ({ ...prev, [curr.id]: curr.label, @@ -322,8 +321,8 @@ export const getQuestionNameMap = (locale: string) => {} ) as Record; -export const getQuestionIcon = (type: TSurveyQuestionTypeEnum) => { - return getQuestionTypes("en-US").find((questionType) => questionType.id === type)?.icon; +export const getQuestionIcon = (type: TSurveyQuestionTypeEnum, t: TFnType) => { + return getQuestionTypes(t).find((questionType) => questionType.id === type)?.icon; }; export const VARIABLES_ICON_MAP = { @@ -331,8 +330,8 @@ export const VARIABLES_ICON_MAP = { number: , }; -export const getCXQuestionNameMap = (locale: string) => - getCXQuestionTypes(locale).reduce( +export const getCXQuestionNameMap = (t: TFnType) => + getCXQuestionTypes(t).reduce( (prev, curr) => ({ ...prev, [curr.id]: curr.label, @@ -344,12 +343,12 @@ export const universalQuestionPresets = { required: true, }; -export const getQuestionDefaults = (id: string, project: any, locale: string) => { - const questionType = getQuestionTypes(locale).find((questionType) => questionType.id === id); +export const getQuestionDefaults = (id: string, project: any, t: TFnType) => { + const questionType = getQuestionTypes(t).find((questionType) => questionType.id === id); return replaceQuestionPresetPlaceholders(questionType?.preset, project); }; -export const getTSurveyQuestionTypeEnumName = (id: string, locale: string) => { - const questionType = getQuestionTypes(locale).find((questionType) => questionType.id === id); +export const getTSurveyQuestionTypeEnumName = (id: string, t: TFnType) => { + const questionType = getQuestionTypes(t).find((questionType) => questionType.id === id); return questionType?.label; }; diff --git a/packages/lib/utils/templates.ts b/packages/lib/utils/templates.ts index 8371346f58..cd4763e156 100644 --- a/packages/lib/utils/templates.ts +++ b/packages/lib/utils/templates.ts @@ -15,13 +15,13 @@ export const replaceQuestionPresetPlaceholders = ( newQuestion.headline[defaultLanguageCode] = getLocalizedValue( newQuestion.headline, defaultLanguageCode - ).replace("{{projectName}}", project.name); + ).replace("$[projectName]", project.name); } if (newQuestion.subheader) { newQuestion.subheader[defaultLanguageCode] = getLocalizedValue( newQuestion.subheader, defaultLanguageCode - )?.replace("{{projectName}}", project.name); + )?.replace("$[projectName]", project.name); } return newQuestion; }; @@ -29,7 +29,7 @@ export const replaceQuestionPresetPlaceholders = ( // replace all occurences of projectName with the actual project name in the current template export const replacePresetPlaceholders = (template: TTemplate, project: any) => { const preset = structuredClone(template.preset); - preset.name = preset.name.replace("{{projectName}}", project.name); + preset.name = preset.name.replace("$[projectName]", project.name); preset.questions = preset.questions.map((question) => { return replaceQuestionPresetPlaceholders(question, project); }); diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 5eae587155..81f446fb41 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -24,20 +24,10 @@ npm install @formbricks/react-native import Formbricks, { track } from "@formbricks/react-native"; export default function App() { - const config = { - environmentId: "your-environment-id", - apiHost: "https://app.formbricks.com", - userId: "hello-user", // optional - attributes: { - // optional - plan: "free", - }, - }; - return ( {/* Your app code */} - + ); } diff --git a/packages/react-native/package.json b/packages/react-native/package.json index b12f772bf2..fbc7639d5c 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/react-native", - "version": "1.3.1", + "version": "2.0.0", "license": "MIT", "description": "Formbricks React Native SDK allows you to connect your app to Formbricks, display surveys and trigger events.", "homepage": "https://formbricks.com", @@ -36,24 +36,29 @@ "build:dev": "tsc && vite build --mode dev", "lint": "eslint src --ext .ts,.js,.tsx,.jsx", "dev": "vite build --watch --mode dev", - "clean": "rimraf .turbo node_modules dist .turbo" + "clean": "rimraf .turbo node_modules dist .turbo", + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "dependencies": { + "zod": "3.24.1" }, "devDependencies": { "@formbricks/api": "workspace:*", "@formbricks/config-typescript": "workspace:*", - "@formbricks/lib": "workspace:*", - "@formbricks/types": "workspace:*", - "@react-native-async-storage/async-storage": "2.1.0", "@types/react": "18.3.11", + "@vitest/coverage-v8": "3.0.4", "react": "18.3.1", "react-native": "0.74.5", "terser": "5.37.0", "vite": "6.0.9", - "vite-plugin-dts": "4.3.0" + "vite-plugin-dts": "4.3.0", + "vitest": "3.0.4" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.60.0", - "react-native-webview": ">=13.0.0" + "react-native-webview": ">=13.0.0", + "@react-native-async-storage/async-storage": ">=2.1.0" } } diff --git a/packages/react-native/src/formbricks.tsx b/packages/react-native/src/components/formbricks.tsx similarity index 52% rename from packages/react-native/src/formbricks.tsx rename to packages/react-native/src/components/formbricks.tsx index a23b7a18bf..b514ea479e 100644 --- a/packages/react-native/src/formbricks.tsx +++ b/packages/react-native/src/components/formbricks.tsx @@ -1,36 +1,35 @@ import React, { useCallback, useEffect, useSyncExternalStore } from "react"; -import { type TJsConfigInput } from "@formbricks/types/js"; -import { Logger } from "../../js-core/src/lib/logger"; -import { init } from "./lib"; -import { SurveyStore } from "./lib/survey-store"; -import { SurveyWebView } from "./survey-web-view"; +import { SurveyWebView } from "@/components/survey-web-view"; +import { Logger } from "@/lib/common/logger"; +import { setup } from "@/lib/common/setup"; +import { SurveyStore } from "@/lib/survey/store"; interface FormbricksProps { - initConfig: TJsConfigInput; + appUrl: string; + environmentId: string; } + const surveyStore = SurveyStore.getInstance(); const logger = Logger.getInstance(); -export function Formbricks({ initConfig }: FormbricksProps): React.JSX.Element | null { +export function Formbricks({ appUrl, environmentId }: FormbricksProps): React.JSX.Element | null { // initializes sdk useEffect(() => { - const initialize = async (): Promise => { + const setupFormbricks = async (): Promise => { try { - await init({ - environmentId: initConfig.environmentId, - apiHost: initConfig.apiHost, - userId: initConfig.userId, - attributes: initConfig.attributes, + await setup({ + environmentId, + appUrl, }); } catch { logger.debug("Initialization failed"); } }; - initialize().catch(() => { + setupFormbricks().catch(() => { logger.debug("Initialization error"); }); - }, [initConfig]); + }, [environmentId, appUrl]); const subscribe = useCallback((callback: () => void) => { const unsubscribe = surveyStore.subscribe(callback); diff --git a/packages/react-native/src/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx similarity index 77% rename from packages/react-native/src/survey-web-view.tsx rename to packages/react-native/src/components/survey-web-view.tsx index 7031afa565..3aaf684169 100644 --- a/packages/react-native/src/survey-web-view.tsx +++ b/packages/react-native/src/components/survey-web-view.tsx @@ -4,19 +4,17 @@ import React, { type JSX, useEffect, useMemo, useRef, useState } from "react"; import { Modal } from "react-native"; import { WebView, type WebViewMessageEvent } from "react-native-webview"; import { FormbricksAPI } from "@formbricks/api"; -import { ResponseQueue } from "@formbricks/lib/responseQueue"; -import { SurveyState } from "@formbricks/lib/surveyState"; -import { getStyling } from "@formbricks/lib/utils/styling"; -import type { SurveyInlineProps } from "@formbricks/types/formbricks-surveys"; -import { ZJsRNWebViewOnMessageData } from "@formbricks/types/js"; -import type { TJsEnvironmentStateSurvey, TJsFileUploadParams, TJsPersonState } from "@formbricks/types/js"; -import type { TResponseUpdate } from "@formbricks/types/responses"; -import type { TUploadFileConfig } from "@formbricks/types/storage"; -import { Logger } from "../../js-core/src/lib/logger"; -import { filterSurveys, getDefaultLanguageCode, getLanguageCode } from "../../js-core/src/lib/utils"; -import { RNConfig } from "./lib/config"; -import { StorageAPI } from "./lib/storage"; -import { SurveyStore } from "./lib/survey-store"; +import { RNConfig } from "@/lib/common/config"; +import { StorageAPI } from "@/lib/common/file-upload"; +import { Logger } from "@/lib/common/logger"; +import { ResponseQueue } from "@/lib/common/response-queue"; +import { filterSurveys, getDefaultLanguageCode, getLanguageCode, getStyling } from "@/lib/common/utils"; +import { SurveyState } from "@/lib/survey/state"; +import { SurveyStore } from "@/lib/survey/store"; +import { type TEnvironmentStateSurvey, type TUserState, ZJsRNWebViewOnMessageData } from "@/types/config"; +import type { TResponseUpdate } from "@/types/response"; +import type { TFileUploadParams, TUploadFileConfig } from "@/types/storage"; +import type { SurveyInlineProps } from "@/types/survey"; const appConfig = RNConfig.getInstance(); const logger = Logger.getInstance(); @@ -25,7 +23,7 @@ logger.configure({ logLevel: "debug" }); const surveyStore = SurveyStore.getInstance(); interface SurveyWebViewProps { - survey: TJsEnvironmentStateSurvey; + survey: TEnvironmentStateSurvey; } export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | undefined { @@ -33,22 +31,23 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und const [isSurveyRunning, setIsSurveyRunning] = useState(false); const [showSurvey, setShowSurvey] = useState(false); - const project = appConfig.get().environmentState.data.project; - const attributes = appConfig.get().attributes; + const project = appConfig.get().environment.data.project; + const language = appConfig.get().user.data.language; const styling = getStyling(project, survey); const isBrandingEnabled = project.inAppSurveyBranding; const isMultiLanguageSurvey = survey.languages.length > 1; + const [languageCode, setLanguageCode] = useState("default"); const [surveyState, setSurveyState] = useState( - new SurveyState(survey.id, null, null, appConfig.get().personState.data.userId) + new SurveyState(survey.id, null, null, appConfig.get().user.data.userId) ); const responseQueue = useMemo( () => new ResponseQueue( { - apiHost: appConfig.get().apiHost, + appUrl: appConfig.get().appUrl, environmentId: appConfig.get().environmentId, retryAttempts: 2, setSurveyState, @@ -59,7 +58,30 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und ); useEffect(() => { - if (!isSurveyRunning && survey.delay) { + if (isMultiLanguageSurvey) { + const displayLanguage = getLanguageCode(survey, language); + if (!displayLanguage) { + logger.debug(`Survey "${survey.name}" is not available in specified language.`); + setIsSurveyRunning(false); + setShowSurvey(false); + surveyStore.resetSurvey(); + return; + } + setLanguageCode(displayLanguage); + setIsSurveyRunning(true); + } else { + setIsSurveyRunning(true); + } + }, [isMultiLanguageSurvey, language, survey]); + + useEffect(() => { + if (!isSurveyRunning) { + setShowSurvey(false); + return; + } + + if (survey.delay) { + logger.debug(`Delaying survey "${survey.name}" by ${String(survey.delay)} seconds`); const timerId = setTimeout(() => { setShowSurvey(true); }, survey.delay * 1000); @@ -67,26 +89,13 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und return () => { clearTimeout(timerId); }; - } else if (!survey.delay) { - setShowSurvey(true); } - }, [survey.delay, isSurveyRunning]); - let languageCode = "default"; - - if (isMultiLanguageSurvey) { - const displayLanguage = getLanguageCode(survey, attributes); - //if survey is not available in selected language, survey wont be shown - if (!displayLanguage) { - logger.debug(`Survey "${survey.name}" is not available in specified language.`); - setIsSurveyRunning(true); - return; - } - languageCode = displayLanguage; - } + setShowSurvey(true); + }, [survey.delay, isSurveyRunning, survey.name]); const addResponseToQueue = (responseUpdate: TResponseUpdate): void => { - const { userId } = appConfig.get().personState.data; + const { userId } = appConfig.get().user.data; if (userId) surveyState.updateUserId(userId); responseQueue.updateSurveyState(surveyState); @@ -101,13 +110,13 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und }; const onCloseSurvey = (): void => { - const { environmentState, personState } = appConfig.get(); + const { environment: environmentState, user: personState } = appConfig.get(); const filteredSurveys = filterSurveys(environmentState, personState); appConfig.update({ ...appConfig.get(), - environmentState, - personState, + environment: environmentState, + user: personState, filteredSurveys, }); @@ -116,10 +125,10 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und }; const createDisplay = async (surveyId: string): Promise<{ id: string }> => { - const { userId } = appConfig.get().personState.data; + const { userId } = appConfig.get().user.data; const api = new FormbricksAPI({ - apiHost: appConfig.get().apiHost, + apiHost: appConfig.get().appUrl, environmentId: appConfig.get().environmentId, }); @@ -135,21 +144,19 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und return res.data; }; - const uploadFile = async ( - file: TJsFileUploadParams["file"], - params?: TUploadFileConfig - ): Promise => { - const storage = new StorageAPI(appConfig.get().apiHost, appConfig.get().environmentId); + const uploadFile = async (file: TFileUploadParams["file"], params?: TUploadFileConfig): Promise => { + const storage = new StorageAPI(appConfig.get().appUrl, appConfig.get().environmentId); return await storage.uploadFile(file, params); }; return ( { setShowSurvey(false); + setIsSurveyRunning(false); }}> & { apiHost?: string }): string => { +const renderHtml = (options: Partial & { appUrl?: string }): string => { return ` @@ -368,7 +375,6 @@ const renderHtml = (options: Partial & { apiHost?: string }): window.ReactNativeWebView.postMessage(JSON.stringify({ onRetry: true })); }; - window.fileUploadPromiseCallbacks = new Map(); function onFileUpload(file, params) { @@ -422,7 +428,7 @@ const renderHtml = (options: Partial & { apiHost?: string }): } const script = document.createElement("script"); - script.src = "${options.apiHost ?? "http://localhost:3000"}/js/surveys.umd.cjs"; + script.src = "${options.appUrl ?? "http://localhost:3000"}/js/surveys.umd.cjs"; script.async = true; script.onload = () => loadSurvey(); script.onerror = (error) => { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 19f187dadf..888a454cb4 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1,6 +1,42 @@ -import { Formbricks } from "./formbricks"; -import { track } from "./lib"; +import { Formbricks } from "@/components/formbricks"; +import { CommandQueue } from "@/lib/common/command-queue"; +import { Logger } from "@/lib/common/logger"; +import * as Actions from "@/lib/survey/action"; +import * as Attributes from "@/lib/user/attribute"; +import * as User from "@/lib/user/user"; + +const logger = Logger.getInstance(); +logger.debug("Create command queue"); +const queue = new CommandQueue(); + +export const track = async (name: string): Promise => { + queue.add(Actions.track, true, name); + await queue.wait(); +}; + +export const setUserId = async (userId: string): Promise => { + queue.add(User.setUserId, true, userId); + await queue.wait(); +}; + +export const setAttribute = async (key: string, value: string): Promise => { + queue.add(Attributes.setAttributes, true, { [key]: value }); + await queue.wait(); +}; + +export const setAttributes = async (attributes: Record): Promise => { + queue.add(Attributes.setAttributes, true, attributes); + await queue.wait(); +}; + +export const setLanguage = async (language: string): Promise => { + queue.add(Attributes.setAttributes, true, { language }); + await queue.wait(); +}; + +export const logout = async (): Promise => { + queue.add(User.logout, true); + await queue.wait(); +}; export default Formbricks; - -export { track }; diff --git a/packages/react-native/src/lib/attributes.ts b/packages/react-native/src/lib/attributes.ts deleted file mode 100644 index 2f861a8bf2..0000000000 --- a/packages/react-native/src/lib/attributes.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { FormbricksAPI } from "@formbricks/api"; -import type { TAttributes } from "@formbricks/types/attributes"; -import type { ApiErrorResponse } from "@formbricks/types/errors"; -import { type Result, err, ok } from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; - -const logger = Logger.getInstance(); - -export const updateAttributes = async ( - apiHost: string, - environmentId: string, - userId: string, - attributes: TAttributes -): Promise> => { - // clean attributes and remove existing attributes if config already exists - const updatedAttributes = { ...attributes }; - - // send to backend if updatedAttributes is not empty - if (Object.keys(updatedAttributes).length === 0) { - logger.debug("No attributes to update. Skipping update."); - return ok(updatedAttributes); - } - - logger.debug(`Updating attributes: ${JSON.stringify(updatedAttributes)}`); - - const api = new FormbricksAPI({ - apiHost, - environmentId, - }); - - const res = await api.client.attribute.update({ userId, attributes: updatedAttributes }); - - if (res.ok) { - if (res.data.details) { - Object.entries(res.data.details).forEach(([key, value]) => { - logger.debug(`${key}: ${value}`); - }); - } - - return ok(updatedAttributes); - } - - const responseError = res.error; - - if (responseError.details?.ignore) { - logger.error(responseError.message); - return ok(updatedAttributes); - } - - return err({ - code: responseError.code, - status: responseError.status, - message: `Error updating person with userId ${userId}`, - url: new URL(`${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`), - responseMessage: responseError.responseMessage, - }); -}; diff --git a/packages/react-native/src/lib/command-queue.ts b/packages/react-native/src/lib/common/command-queue.ts similarity index 68% rename from packages/react-native/src/lib/command-queue.ts rename to packages/react-native/src/lib/common/command-queue.ts index 717b1dda2e..a0ce884fdf 100644 --- a/packages/react-native/src/lib/command-queue.ts +++ b/packages/react-native/src/lib/common/command-queue.ts @@ -1,11 +1,12 @@ -import { wrapThrowsAsync } from "@formbricks/types/error-handlers"; -import { ErrorHandler, type Result } from "../../../js-core/src/lib/errors"; -import { checkInitialized } from "./initialize"; +/* eslint-disable no-console -- we need to log global errors */ +import { checkSetup } from "@/lib/common/setup"; +import { wrapThrowsAsync } from "@/lib/common/utils"; +import type { Result } from "@/types/error"; export class CommandQueue { private queue: { command: (...args: any[]) => Promise> | Result | Promise; - checkInitialized: boolean; + checkSetup: boolean; commandArgs: any[]; }[] = []; private running = false; @@ -14,10 +15,10 @@ export class CommandQueue { public add( command: (...args: A[]) => Promise> | Result | Promise, - shouldCheckInitialized = true, + shouldCheckSetup = true, ...args: A[] ): void { - this.queue.push({ command, checkInitialized: shouldCheckInitialized, commandArgs: args }); + this.queue.push({ command, checkSetup: shouldCheckSetup, commandArgs: args }); if (!this.running) { this.commandPromise = new Promise((resolve) => { @@ -36,18 +37,16 @@ export class CommandQueue { private async run(): Promise { this.running = true; while (this.queue.length > 0) { - const errorHandler = ErrorHandler.getInstance(); const currentItem = this.queue.shift(); if (!currentItem) continue; - // make sure formbricks is initialized - if (currentItem.checkInitialized) { + // make sure formbricks is setup + if (currentItem.checkSetup) { // call different function based on package type - const initResult = checkInitialized(); + const setupResult = checkSetup(); - if (!initResult.ok) { - errorHandler.handle(initResult.error); + if (!setupResult.ok) { continue; } } @@ -59,9 +58,9 @@ export class CommandQueue { const result = await wrapThrowsAsync(executeCommand)(); if (!result.ok) { - errorHandler.handle(result.error); + console.error("🧱 Formbricks - Global error: ", result.error); } else if (!result.data.ok) { - errorHandler.handle(result.data.error); + console.error("🧱 Formbricks - Global error: ", result.data.error); } } this.running = false; diff --git a/packages/react-native/src/lib/config.ts b/packages/react-native/src/lib/common/config.ts similarity index 73% rename from packages/react-native/src/lib/config.ts rename to packages/react-native/src/lib/common/config.ts index 085dbdb382..6da3f0e827 100644 --- a/packages/react-native/src/lib/config.ts +++ b/packages/react-native/src/lib/common/config.ts @@ -1,13 +1,15 @@ /* eslint-disable no-console -- Required for error logging */ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers"; -import type { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js"; -import { RN_ASYNC_STORAGE_KEY } from "../../../js-core/src/lib/constants"; +import { AsyncStorage } from "@/lib/common/storage"; +import { wrapThrowsAsync } from "@/lib/common/utils"; +import type { TConfig, TConfigUpdateInput } from "@/types/config"; +import { type Result, err, ok } from "@/types/error"; + +export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native"; export class RNConfig { private static instance: RNConfig | null = null; - private config: TJsConfig | null = null; + private config: TConfig | null = null; private constructor() { this.loadFromStorage() @@ -29,7 +31,7 @@ export class RNConfig { return RNConfig.instance; } - public update(newConfig: TJsConfigUpdateInput): void { + public update(newConfig: TConfigUpdateInput): void { this.config = { ...this.config, ...newConfig, @@ -42,24 +44,24 @@ export class RNConfig { void this.saveToStorage(); } - public get(): TJsConfig { + public get(): TConfig { if (!this.config) { throw new Error("config is null, maybe the init function was not called?"); } return this.config; } - public async loadFromStorage(): Promise> { + public async loadFromStorage(): Promise> { try { const savedConfig = await AsyncStorage.getItem(RN_ASYNC_STORAGE_KEY); if (savedConfig) { - const parsedConfig = JSON.parse(savedConfig) as TJsConfig; + const parsedConfig = JSON.parse(savedConfig) as TConfig; // check if the config has expired if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- need to check if expiresAt is set - parsedConfig.environmentState.expiresAt && - new Date(parsedConfig.environmentState.expiresAt) <= new Date() + parsedConfig.environment.expiresAt && + new Date(parsedConfig.environment.expiresAt) <= new Date() ) { return err(new Error("Config in local storage has expired")); } diff --git a/packages/react-native/src/lib/event-listeners.ts b/packages/react-native/src/lib/common/event-listeners.ts similarity index 71% rename from packages/react-native/src/lib/event-listeners.ts rename to packages/react-native/src/lib/common/event-listeners.ts index ecd46179cc..f2fea20f12 100644 --- a/packages/react-native/src/lib/event-listeners.ts +++ b/packages/react-native/src/lib/common/event-listeners.ts @@ -1,32 +1,32 @@ import { addEnvironmentStateExpiryCheckListener, clearEnvironmentStateExpiryCheckListener, -} from "./environment-state"; -import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./person-state"; +} from "@/lib/environment/state"; +import { addUserStateExpiryCheckListener, clearUserStateExpiryCheckListener } from "@/lib/user/state"; let areRemoveEventListenersAdded = false; export const addEventListeners = (): void => { addEnvironmentStateExpiryCheckListener(); - addPersonStateExpiryCheckListener(); + addUserStateExpiryCheckListener(); }; export const addCleanupEventListeners = (): void => { if (areRemoveEventListenersAdded) return; clearEnvironmentStateExpiryCheckListener(); - clearPersonStateExpiryCheckListener(); + clearUserStateExpiryCheckListener(); areRemoveEventListenersAdded = true; }; export const removeCleanupEventListeners = (): void => { if (!areRemoveEventListenersAdded) return; clearEnvironmentStateExpiryCheckListener(); - clearPersonStateExpiryCheckListener(); + clearUserStateExpiryCheckListener(); areRemoveEventListenersAdded = false; }; export const removeAllEventListeners = (): void => { clearEnvironmentStateExpiryCheckListener(); - clearPersonStateExpiryCheckListener(); + clearUserStateExpiryCheckListener(); removeCleanupEventListeners(); }; diff --git a/packages/react-native/src/lib/storage.ts b/packages/react-native/src/lib/common/file-upload.ts similarity index 91% rename from packages/react-native/src/lib/storage.ts rename to packages/react-native/src/lib/common/file-upload.ts index ff956c72b7..a4fb21d703 100644 --- a/packages/react-native/src/lib/storage.ts +++ b/packages/react-native/src/lib/common/file-upload.ts @@ -1,12 +1,12 @@ /* eslint-disable no-console -- used for error logging */ -import type { TUploadFileConfig, TUploadFileResponse } from "@formbricks/types/storage"; +import { type TUploadFileConfig, type TUploadFileResponse } from "@/types/storage"; export class StorageAPI { - private apiHost: string; + private appUrl: string; private environmentId: string; - constructor(apiHost: string, environmentId: string) { - this.apiHost = apiHost; + constructor(appUrl: string, environmentId: string) { + this.appUrl = appUrl; this.environmentId = environmentId; } @@ -29,7 +29,7 @@ export class StorageAPI { surveyId, }; - const response = await fetch(`${this.apiHost}/api/v1/client/${this.environmentId}/storage`, { + const response = await fetch(`${this.appUrl}/api/v1/client/${this.environmentId}/storage`, { method: "POST", headers: { "Content-Type": "application/json", @@ -86,7 +86,7 @@ export class StorageAPI { let uploadResponse: Response = {} as Response; - const signedUrlCopy = signedUrl.replace("http://localhost:3000", this.apiHost); + const signedUrlCopy = signedUrl.replace("http://localhost:3000", this.appUrl); try { uploadResponse = await fetch(signedUrlCopy, { diff --git a/packages/react-native/src/lib/common/logger.ts b/packages/react-native/src/lib/common/logger.ts new file mode 100644 index 0000000000..5d45ffe90e --- /dev/null +++ b/packages/react-native/src/lib/common/logger.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-console -- Required for logging */ +type LogLevel = "debug" | "error"; + +interface LoggerConfig { + logLevel?: LogLevel; +} + +export class Logger { + private static instance: Logger | undefined; + private logLevel: LogLevel = "error"; + + static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + configure(config: LoggerConfig): void { + if (config.logLevel !== undefined) { + this.logLevel = config.logLevel; + } + } + + private logger(message: string, level: LogLevel): void { + if (level === "debug" && this.logLevel !== "debug") { + return; + } + + const timestamp = new Date().toISOString(); + const logMessage = `🧱 Formbricks - ${timestamp} [${level.toUpperCase()}] - ${message}`; + if (level === "error") { + console.error(logMessage); + } else { + console.log(logMessage); + } + } + + debug(message: string): void { + this.logger(message, "debug"); + } + + error(message: string): void { + this.logger(message, "error"); + } + + public resetInstance(): void { + Logger.instance = undefined; + } +} diff --git a/packages/react-native/src/lib/common/response-queue.ts b/packages/react-native/src/lib/common/response-queue.ts new file mode 100644 index 0000000000..35c0d8524c --- /dev/null +++ b/packages/react-native/src/lib/common/response-queue.ts @@ -0,0 +1,119 @@ +/* eslint-disable no-console -- required for logging errors */ +import { FormbricksAPI } from "@formbricks/api"; +import { type SurveyState } from "@/lib/survey/state"; +import { type TResponseUpdate } from "@/types/response"; + +interface QueueConfig { + appUrl: string; + environmentId: string; + retryAttempts: number; + onResponseSendingFailed?: (responseUpdate: TResponseUpdate) => void; + onResponseSendingFinished?: () => void; + setSurveyState?: (state: SurveyState) => void; +} + +const delay = (ms: number): Promise => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +export class ResponseQueue { + private queue: TResponseUpdate[] = []; + private config: QueueConfig; + private surveyState: SurveyState; + private isRequestInProgress = false; + private api: FormbricksAPI; + + constructor(config: QueueConfig, surveyState: SurveyState, apiInstance?: FormbricksAPI) { + this.config = config; + this.surveyState = surveyState; + this.api = + apiInstance ?? + new FormbricksAPI({ + apiHost: config.appUrl, + environmentId: config.environmentId, + }); + } + + add(responseUpdate: TResponseUpdate): void { + // update survey state + this.surveyState.accumulateResponse(responseUpdate); + if (this.config.setSurveyState) { + this.config.setSurveyState(this.surveyState); + } + // add response to queue + this.queue.push(responseUpdate); + void this.processQueue(); + } + + async processQueue(): Promise { + if (this.isRequestInProgress) return; + if (this.queue.length === 0) return; + + this.isRequestInProgress = true; + + const responseUpdate = this.queue[0]; + let attempts = 0; + + while (attempts < this.config.retryAttempts) { + const success = await this.sendResponse(responseUpdate); + if (success) { + this.queue.shift(); // remove the successfully sent response from the queue + break; // exit the retry loop + } + console.error(`Formbricks: Failed to send response. Retrying... ${attempts.toString()}`); + await delay(1000); // wait for 1 second before retrying + attempts++; + } + + if (attempts >= this.config.retryAttempts) { + // Inform the user after 2 failed attempts + console.error("Failed to send response after 2 attempts."); + // If the response fails finally, inform the user + if (this.config.onResponseSendingFailed) { + this.config.onResponseSendingFailed(responseUpdate); + } + this.isRequestInProgress = false; + } else { + if (responseUpdate.finished && this.config.onResponseSendingFinished) { + this.config.onResponseSendingFinished(); + } + this.isRequestInProgress = false; + void this.processQueue(); // process the next item in the queue if any + } + } + + async sendResponse(responseUpdate: TResponseUpdate): Promise { + try { + if (this.surveyState.responseId !== null) { + await this.api.client.response.update({ ...responseUpdate, responseId: this.surveyState.responseId }); + } else { + const response = await this.api.client.response.create({ + ...responseUpdate, + surveyId: this.surveyState.surveyId, + userId: this.surveyState.userId ?? null, + singleUseId: this.surveyState.singleUseId ?? null, + data: { ...responseUpdate.data, ...responseUpdate.hiddenFields }, + displayId: this.surveyState.displayId, + }); + if (!response.ok) { + throw new Error("Could not create response"); + } + this.surveyState.updateResponseId(response.data.id); + if (this.config.setSurveyState) { + this.config.setSurveyState(this.surveyState); + } + } + return true; + } catch (error) { + console.error(error); + return false; + } + } + + // update surveyState + updateSurveyState(surveyState: SurveyState): void { + this.surveyState = surveyState; + } +} diff --git a/packages/react-native/src/lib/common/setup.ts b/packages/react-native/src/lib/common/setup.ts new file mode 100644 index 0000000000..45cd25b78d --- /dev/null +++ b/packages/react-native/src/lib/common/setup.ts @@ -0,0 +1,280 @@ +import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config"; +import { + addCleanupEventListeners, + addEventListeners, + removeAllEventListeners, +} from "@/lib/common/event-listeners"; +import { Logger } from "@/lib/common/logger"; +import { AsyncStorage } from "@/lib/common/storage"; +import { filterSurveys, isNowExpired, wrapThrowsAsync } from "@/lib/common/utils"; +import { fetchEnvironmentState } from "@/lib/environment/state"; +import { DEFAULT_USER_STATE_NO_USER_ID } from "@/lib/user/state"; +import { sendUpdatesToBackend } from "@/lib/user/update"; +import { type TConfig, type TConfigInput, type TEnvironmentState, type TUserState } from "@/types/config"; +import { + type MissingFieldError, + type MissingPersonError, + type NetworkError, + type NotSetupError, + type Result, + err, + okVoid, +} from "@/types/error"; + +let isSetup = false; + +export const setIsSetup = (state: boolean): void => { + isSetup = state; +}; + +export const setup = async ( + configInput: TConfigInput +): Promise> => { + const appConfig = RNConfig.getInstance(); + const logger = Logger.getInstance(); + + if (isSetup) { + logger.debug("Already set up, skipping setup."); + return okVoid(); + } + + let existingConfig: TConfig | undefined; + try { + existingConfig = appConfig.get(); + logger.debug("Found existing configuration."); + } catch { + logger.debug("No existing configuration found."); + } + + // formbricks is in error state, skip setup + if (existingConfig?.status.value === "error") { + logger.debug("Formbricks was set to an error state."); + + const expiresAt = existingConfig.status.expiresAt; + + if (expiresAt && isNowExpired(expiresAt)) { + logger.debug("Error state is not expired, skipping setup"); + return okVoid(); + } + logger.debug("Error state is expired. Continue with setup."); + } + + logger.debug("Start setup"); + + if (!configInput.environmentId) { + logger.debug("No environmentId provided"); + return err({ + code: "missing_field", + field: "environmentId", + }); + } + + if (!configInput.appUrl) { + logger.debug("No appUrl provided"); + + return err({ + code: "missing_field", + field: "appUrl", + }); + } + + if ( + existingConfig?.environment && + existingConfig.environmentId === configInput.environmentId && + existingConfig.appUrl === configInput.appUrl + ) { + logger.debug("Configuration fits setup parameters."); + let isEnvironmentStateExpired = false; + let isUserStateExpired = false; + + if (isNowExpired(existingConfig.environment.expiresAt)) { + logger.debug("Environment state expired. Syncing."); + isEnvironmentStateExpired = true; + } + + if (existingConfig.user.expiresAt && isNowExpired(existingConfig.user.expiresAt)) { + logger.debug("Person state expired. Syncing."); + isUserStateExpired = true; + } + + try { + // fetch the environment state (if expired) + let environmentState: TEnvironmentState = existingConfig.environment; + let userState: TUserState = existingConfig.user; + + if (isEnvironmentStateExpired) { + const environmentStateResponse = await fetchEnvironmentState({ + appUrl: configInput.appUrl, + environmentId: configInput.environmentId, + }); + + if (environmentStateResponse.ok) { + environmentState = environmentStateResponse.data; + } else { + logger.error( + `Error fetching environment state: ${environmentStateResponse.error.code} - ${environmentStateResponse.error.responseMessage ?? ""}` + ); + return err({ + code: "network_error", + message: "Error fetching environment state", + status: 500, + url: new URL(`${configInput.appUrl}/api/v1/client/${configInput.environmentId}/environment`), + responseMessage: environmentStateResponse.error.message, + }); + } + } + + if (isUserStateExpired) { + // If the existing person state (expired) has a userId, we need to fetch the person state + // If the existing person state (expired) has no userId, we need to set the person state to the default + + if (userState.data.userId) { + const updatesResponse = await sendUpdatesToBackend({ + appUrl: configInput.appUrl, + environmentId: configInput.environmentId, + updates: { + userId: userState.data.userId, + }, + }); + + if (updatesResponse.ok) { + userState = updatesResponse.data.state; + } else { + logger.error( + `Error updating user state: ${updatesResponse.error.code} - ${updatesResponse.error.responseMessage ?? ""}` + ); + return err({ + code: "network_error", + message: "Error updating user state", + status: 500, + url: new URL( + `${configInput.appUrl}/api/v1/client/${configInput.environmentId}/update/contacts/${userState.data.userId}` + ), + responseMessage: "Unknown error", + }); + } + } else { + userState = DEFAULT_USER_STATE_NO_USER_ID; + } + } + + // filter the environment state wrt the person state + const filteredSurveys = filterSurveys(environmentState, userState); + + // update the appConfig with the new filtered surveys and person state + appConfig.update({ + ...existingConfig, + environment: environmentState, + user: userState, + filteredSurveys, + }); + + const surveyNames = filteredSurveys.map((s) => s.name); + logger.debug(`Fetched ${surveyNames.length.toString()} surveys during sync: ${surveyNames.join(", ")}`); + } catch { + logger.debug("Error during sync. Please try again."); + } + } else { + logger.debug("No valid configuration found. Resetting config and creating new one."); + void appConfig.resetConfig(); + logger.debug("Syncing."); + + // During setup, if we don't have a valid config, we need to fetch the environment state + // but not the person state, we can set it to the default value. + // The person state will be fetched when the `setUserId` method is called. + + try { + const environmentStateResponse = await fetchEnvironmentState({ + appUrl: configInput.appUrl, + environmentId: configInput.environmentId, + }); + + if (!environmentStateResponse.ok) { + // eslint-disable-next-line @typescript-eslint/only-throw-error -- error is ApiErrorResponse + throw environmentStateResponse.error; + } + + const personState = DEFAULT_USER_STATE_NO_USER_ID; + const environmentState = environmentStateResponse.data; + + const filteredSurveys = filterSurveys(environmentState, personState); + + appConfig.update({ + appUrl: configInput.appUrl, + environmentId: configInput.environmentId, + user: personState, + environment: environmentState, + filteredSurveys, + }); + } catch (e) { + await handleErrorOnFirstSetup(e as { code: string; responseMessage: string }); + } + } + + logger.debug("Adding event listeners"); + addEventListeners(); + addCleanupEventListeners(); + + setIsSetup(true); + logger.debug("Set up complete"); + + // check page url if set up after page load + return okVoid(); +}; + +export const checkSetup = (): Result => { + const logger = Logger.getInstance(); + logger.debug("Check if set up"); + + if (!isSetup) { + return err({ + code: "not_setup", + message: "Formbricks is not set up. Call setup() first.", + }); + } + + return okVoid(); +}; + +// eslint-disable-next-line @typescript-eslint/require-await -- disabled for now +export const tearDown = async (): Promise => { + const logger = Logger.getInstance(); + const appConfig = RNConfig.getInstance(); + + logger.debug("Setting user state to default"); + // clear the user state and set it to the default value + appConfig.update({ + ...appConfig.get(), + user: DEFAULT_USER_STATE_NO_USER_ID, + }); + + setIsSetup(false); + removeAllEventListeners(); +}; + +export const handleErrorOnFirstSetup = async (e: { + code: string; + responseMessage: string; +}): Promise => { + const logger = Logger.getInstance(); + + if (e.code === "forbidden") { + logger.error(`Authorization error: ${e.responseMessage}`); + } else { + logger.error(`Error during first setup: ${e.code} - ${e.responseMessage}. Please try again later.`); + } + + // put formbricks in error state (by creating a new config) and throw error + const initialErrorConfig: Partial = { + status: { + value: "error", + expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future + }, + }; + + await wrapThrowsAsync(async () => { + await AsyncStorage.setItem(RN_ASYNC_STORAGE_KEY, JSON.stringify(initialErrorConfig)); + })(); + + throw new Error("Could not set up formbricks"); +}; diff --git a/packages/react-native/src/lib/common/storage.ts b/packages/react-native/src/lib/common/storage.ts new file mode 100644 index 0000000000..a3a4ecacff --- /dev/null +++ b/packages/react-native/src/lib/common/storage.ts @@ -0,0 +1,9 @@ +import AsyncStorageModule from "@react-native-async-storage/async-storage"; + +const AsyncStorageWithDefault = AsyncStorageModule as typeof AsyncStorageModule & { + default?: typeof AsyncStorageModule; +}; + +const AsyncStorage = AsyncStorageWithDefault.default ?? AsyncStorageModule; + +export { AsyncStorage }; diff --git a/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts b/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts new file mode 100644 index 0000000000..30b098d5dd --- /dev/null +++ b/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts @@ -0,0 +1,125 @@ +import type { TConfig } from "@/types/config"; + +// ids +export const mockEnvironmentId = "ggskhsue85p2xrxrc7x3qagg"; +export const mockProjectId = "f5kptre0saxmltl7ram364qt"; +export const mockLanguageId = "n4ts6u7wy5lbn4q3jovikqot"; +export const mockSurveyId = "lz5m554yqh1i3moa3y230wei"; +export const mockActionClassId = "wypzu5qw7adgy66vq8s77tso"; + +export const mockConfig: TConfig = { + environmentId: mockEnvironmentId, + appUrl: "https://myapp.example", + environment: { + expiresAt: "2999-12-31T23:59:59Z", + data: { + surveys: [ + { + id: mockSurveyId, + name: "Onboarding Survey", + welcomeCard: null, + questions: [], + variables: [], + type: "app", // "link" or "app" + showLanguageSwitch: true, + endings: [], + autoClose: 5, + status: "inProgress", // whatever statuses you use + recontactDays: 7, + displayLimit: 1, + displayOption: "displayMultiple", + hiddenFields: [], + delay: 5, // e.g. 5s + projectOverwrites: {}, + languages: [ + { + // SurveyLanguage fields + surveyId: mockSurveyId, + default: true, + enabled: true, + languageId: mockLanguageId, + language: { + // Language fields + id: mockLanguageId, + code: "en", + alias: "en", + createdAt: "2025-01-01T10:00:00Z", + updatedAt: "2025-01-01T10:00:00Z", + projectId: mockProjectId, + }, + }, + ], + triggers: [ + { + actionClass: { + id: mockActionClassId, + key: "onboardingTrigger", + type: "code", + name: "Manual Trigger", + createdAt: "2025-01-01T10:00:00Z", + updatedAt: "2025-01-01T10:00:00Z", + environmentId: mockEnvironmentId, + description: "Manual Trigger", + noCodeConfig: {}, + }, + }, + ], + segment: undefined, // or mock your Segment if needed + displayPercentage: 100, + styling: { + // TSurveyStyling + overwriteThemeStyling: false, + brandColor: { light: "#2B6CB0" }, + }, + }, + ], + actionClasses: [ + { + id: mockActionClassId, + key: "onboardingTrigger", + type: "code", + name: "Manual Trigger", + noCodeConfig: {}, + }, + ], + project: { + id: mockProjectId, + recontactDays: 14, + clickOutsideClose: true, + darkOverlay: false, + placement: "bottomRight", + inAppSurveyBranding: true, + styling: { + // TProjectStyling + allowStyleOverwrite: true, + brandColor: { light: "#319795" }, + }, + }, + }, + }, + user: { + expiresAt: null, + data: { + userId: "user_abc", + segments: ["beta-testers"], + displays: [ + { + surveyId: mockSurveyId, + createdAt: "2025-01-01T10:00:00Z", + }, + ], + responses: [mockSurveyId], + lastDisplayAt: "2025-01-02T15:00:00Z", + language: "en", + }, + }, + filteredSurveys: [], // fill if you'd like to pre-filter any surveys + attributes: { + plan: "premium", + region: "US", + }, + status: { + value: "success", + expiresAt: null, + }, +} as unknown as TConfig; diff --git a/packages/react-native/src/lib/common/tests/__mocks__/response-queue.mock.ts b/packages/react-native/src/lib/common/tests/__mocks__/response-queue.mock.ts new file mode 100644 index 0000000000..0238837726 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/__mocks__/response-queue.mock.ts @@ -0,0 +1,10 @@ +export const mockSurveyId = "v6ym64bkch8o2spfajqt95u4"; +export const mockDisplayId = "athxslnasdkkoo18dyjs8y7e"; +export const mockEnvironmentId = "t49wnfhgq9cvarkvdq4316fd"; +export const mockUserId = "user_abc"; +export const mockAppUrl = "https://app.formbricks.com"; +export const mockResponseId = "d5596l4pynnqv03dokbnril4"; +export const mockQuestionId = "u9cnqpimopizdukyry8ye5us"; +export const mockResponseData = { + [mockQuestionId]: "test", +}; diff --git a/packages/react-native/src/lib/common/tests/command-queue.test.ts b/packages/react-native/src/lib/common/tests/command-queue.test.ts new file mode 100644 index 0000000000..89b273f7a7 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/command-queue.test.ts @@ -0,0 +1,165 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { CommandQueue } from "@/lib/common/command-queue"; +import { checkSetup } from "@/lib/common/setup"; +import { type Result } from "@/types/error"; + +// Mock the setup module so we can control checkSetup() +vi.mock("@/lib/common/setup", () => ({ + checkSetup: vi.fn(), +})); + +describe("CommandQueue", () => { + let queue: CommandQueue; + + beforeEach(() => { + // Clear all mocks before each test + vi.clearAllMocks(); + // Create a fresh CommandQueue instance + queue = new CommandQueue(); + }); + + test("executes commands in FIFO order", async () => { + const executionOrder: string[] = []; + + // Mock commands with proper Result returns + const cmdA = vi.fn(async (): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + executionOrder.push("A"); + resolve({ ok: true, data: undefined }); + }, 10); + }); + }); + const cmdB = vi.fn(async (): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + executionOrder.push("B"); + resolve({ ok: true, data: undefined }); + }, 10); + }); + }); + const cmdC = vi.fn(async (): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + executionOrder.push("C"); + resolve({ ok: true, data: undefined }); + }, 10); + }); + }); + + // We'll assume checkSetup always ok for this test + vi.mocked(checkSetup).mockReturnValue({ ok: true, data: undefined }); + + // Enqueue commands + queue.add(cmdA, true); + queue.add(cmdB, true); + queue.add(cmdC, true); + + // Wait for them to finish + await queue.wait(); + + expect(executionOrder).toEqual(["A", "B", "C"]); + }); + + test("skips execution if checkSetup() fails", async () => { + const cmd = vi.fn(async (): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 10); + }); + }); + + // Force checkSetup to fail + vi.mocked(checkSetup).mockReturnValue({ + ok: false, + error: { + code: "not_setup", + message: "Not setup", + }, + }); + + queue.add(cmd, true); + await queue.wait(); + + // Command should never have been called + expect(cmd).not.toHaveBeenCalled(); + }); + + test("executes command if checkSetup is false (no check)", async () => { + const cmd = vi.fn(async (): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ ok: true, data: undefined }); + }, 10); + }); + }); + + // checkSetup is irrelevant in this scenario, but let's mock it anyway + vi.mocked(checkSetup).mockReturnValue({ ok: true, data: undefined }); + + // Here we pass 'false' for the second argument, so no check is performed + queue.add(cmd, false); + await queue.wait(); + + expect(cmd).toHaveBeenCalledTimes(1); + }); + + test("logs errors if a command throws or returns error", async () => { + // Spy on console.error to see if it's called + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { + return { + ok: true, + data: undefined, + }; + }); + + // Force checkSetup to succeed + vi.mocked(checkSetup).mockReturnValue({ ok: true, data: undefined }); + + // Mock command that fails + const failingCmd = vi.fn(async () => { + await new Promise((resolve) => { + setTimeout(() => { + resolve("some error"); + }, 10); + }); + + throw new Error("some error"); + }); + + queue.add(failingCmd, true); + await queue.wait(); + + expect(consoleErrorSpy).toHaveBeenCalledWith("🧱 Formbricks - Global error: ", expect.any(Error)); + consoleErrorSpy.mockRestore(); + }); + + test("resolves wait() after all commands complete", async () => { + const cmd1 = vi.fn(async (): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ ok: true, data: undefined }); + }, 10); + }); + }); + const cmd2 = vi.fn(async (): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ ok: true, data: undefined }); + }, 10); + }); + }); + + vi.mocked(checkSetup).mockReturnValue({ ok: true, data: undefined }); + + queue.add(cmd1, true); + queue.add(cmd2, true); + + await queue.wait(); + + // By the time `await queue.wait()` resolves, both commands should be done + expect(cmd1).toHaveBeenCalled(); + expect(cmd2).toHaveBeenCalled(); + }); +}); diff --git a/packages/react-native/src/lib/common/tests/config.test.ts b/packages/react-native/src/lib/common/tests/config.test.ts new file mode 100644 index 0000000000..db63fd5633 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/config.test.ts @@ -0,0 +1,127 @@ +// config.test.ts +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { mockConfig } from "./__mocks__/config.mock"; +import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config"; +import type { TConfig, TConfigUpdateInput } from "@/types/config"; + +// Define mocks outside of any describe block + +describe("RNConfig", () => { + let configInstance: RNConfig; + + beforeEach(async () => { + // Clear mocks between tests + vi.clearAllMocks(); + + // get the config instance + configInstance = RNConfig.getInstance(); + + // reset the config + await configInstance.resetConfig(); + + // get the config instance again + configInstance = RNConfig.getInstance(); + }); + + afterEach(() => { + // In case we want to restore them after all tests + vi.restoreAllMocks(); + }); + + test("getInstance() returns a singleton", () => { + const secondInstance = RNConfig.getInstance(); + expect(configInstance).toBe(secondInstance); + }); + + test("get() throws if config is null", () => { + // constructor didn't load anything successfully + // so config is still null + expect(() => configInstance.get()).toThrow("config is null, maybe the init function was not called?"); + }); + + test("loadFromStorage() returns ok if valid config is found", async () => { + vi.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(JSON.stringify(mockConfig)); + + const result = await configInstance.loadFromStorage(); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data).toEqual(mockConfig); + } + }); + + test("loadFromStorage() returns err if config is expired", async () => { + const expiredConfig = { + ...mockConfig, + environment: { + ...mockConfig.environment, + expiresAt: new Date("2000-01-01T00:00:00Z"), + }, + }; + + vi.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(JSON.stringify(expiredConfig)); + + const result = await configInstance.loadFromStorage(); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.message).toBe("Config in local storage has expired"); + } + }); + + test("loadFromStorage() returns err if no or invalid config in storage", async () => { + // Simulate no data + vi.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(null); + + const result = await configInstance.loadFromStorage(); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.message).toBe("No or invalid config in local storage"); + } + }); + + test("update() merges new config, calls saveToStorage()", async () => { + vi.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(JSON.stringify(mockConfig)); + + // Wait for the constructor's async load + await new Promise(setImmediate); + + // Now we call update() + const newStatus = { value: "error", expiresAt: "2100-01-01T00:00:00Z" } as unknown as TConfig["status"]; + + configInstance.update({ ...mockConfig, status: newStatus } as unknown as TConfigUpdateInput); + + // The update call should eventually call setItem on AsyncStorage + expect(AsyncStorage.setItem).toHaveBeenCalled(); + // Let’s check if we can read the updated config: + const updatedConfig = configInstance.get(); + expect(updatedConfig.status.value).toBe("error"); + expect(updatedConfig.status.expiresAt).toBe("2100-01-01T00:00:00Z"); + }); + + test("saveToStorage() is invoked internally on update()", async () => { + vi.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(JSON.stringify(mockConfig)); + + await new Promise(setImmediate); + + configInstance.update({ status: { value: "success", expiresAt: null } } as unknown as TConfigUpdateInput); + expect(AsyncStorage.setItem).toHaveBeenCalledWith( + RN_ASYNC_STORAGE_KEY, + expect.any(String) // the JSON string + ); + }); + + test("resetConfig() clears config and AsyncStorage", async () => { + vi.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(JSON.stringify(mockConfig)); + await new Promise(setImmediate); + + // Now reset + const result = await configInstance.resetConfig(); + + expect(result.ok).toBe(true); + // config is now null + expect(() => configInstance.get()).toThrow("config is null"); + // removeItem should be called + expect(AsyncStorage.removeItem).toHaveBeenCalledWith(RN_ASYNC_STORAGE_KEY); + }); +}); diff --git a/packages/react-native/src/lib/common/tests/file-upload.test.ts b/packages/react-native/src/lib/common/tests/file-upload.test.ts new file mode 100644 index 0000000000..4000c69971 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/file-upload.test.ts @@ -0,0 +1,186 @@ +// file-upload.test.ts +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { StorageAPI } from "@/lib/common/file-upload"; +import type { TUploadFileConfig } from "@/types/storage"; + +// A global fetch mock so we can capture fetch calls. +// Alternatively, use `vi.stubGlobal("fetch", ...)`. +const fetchMock = vi.fn(); +global.fetch = fetchMock; + +const mockEnvironmentId = "dv46cywjt1fxkkempq7vwued"; + +describe("StorageAPI", () => { + const APP_URL = "https://myapp.example"; + const ENV_ID = mockEnvironmentId; + + let storage: StorageAPI; + + beforeEach(() => { + vi.clearAllMocks(); + storage = new StorageAPI(APP_URL, ENV_ID); + }); + + test("throws an error if file object is invalid", async () => { + // File missing "name", "type", or "base64" + await expect(storage.uploadFile({ type: "", name: "", base64: "" }, {})).rejects.toThrow( + "Invalid file object" + ); + }); + + test("throws if first fetch (storage route) returns non-OK", async () => { + // We provide a valid file object + const file = { type: "image/png", name: "test.png", base64: "data:image/png;base64,abc" }; + + // First fetch returns not ok + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 400, + } as Response); + + await expect(storage.uploadFile(file)).rejects.toThrow("Upload failed with status: 400"); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + `${APP_URL}/api/v1/client/${ENV_ID}/storage`, + expect.objectContaining({ + method: "POST", + }) + ); + }); + + test("throws if second fetch returns non-OK (local storage w/ signingData)", async () => { + // Suppose the first fetch is OK and returns JSON with signingData + const file = { type: "image/png", name: "test.png", base64: "data:image/png;base64,abc" }; + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: async () => { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + return { + data: { + signedUrl: "https://myapp.example/uploadLocal", + fileUrl: "https://myapp.example/files/test.png", + signingData: { signature: "xxx", timestamp: 1234, uuid: "abc" }, + presignedFields: null, + updatedFileName: "test.png", + }, + }; + }, + } as Response) + // second fetch fails + .mockResolvedValueOnce({ + ok: false, + json: async () => { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + return { message: "File size exceeded your plan limit" }; + }, + } as Response); + + await expect(storage.uploadFile(file)).rejects.toThrow("File size exceeded your plan limit"); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + test("throws if second fetch returns non-OK (S3) containing 'EntityTooLarge'", async () => { + const file = { type: "image/png", name: "test.png", base64: "data:image/png;base64,abc" }; + + // First fetch response includes presignedFields => indicates S3 scenario + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: async () => { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + return { + data: { + signedUrl: "https://some-s3-bucket/presigned", + fileUrl: "https://some-s3-bucket/test.png", + signingData: null, // means not local + presignedFields: { + key: "some-key", + policy: "base64policy", + }, + updatedFileName: "test.png", + }, + }; + }, + } as Response) + // second fetch fails with "EntityTooLarge" + .mockResolvedValueOnce({ + ok: false, + text: async () => { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + return "Some error with EntityTooLarge text in it"; + }, + } as Response); + + await expect(storage.uploadFile(file)).rejects.toThrow("File size exceeds the size limit for your plan"); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + test("successful upload returns fileUrl", async () => { + const file = { type: "image/png", name: "test.png", base64: "data:image/png;base64,abc" }; + const mockFileUrl = "https://myapp.example/files/test.png"; + + // First fetch => OK, returns JSON with 'signedUrl', 'fileUrl', etc. + fetchMock + .mockResolvedValueOnce({ + ok: true, + json: async () => { + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + return { + data: { + signedUrl: "https://myapp.example/uploadLocal", + fileUrl: mockFileUrl, + signingData: { + signature: "xxx", + timestamp: 1234, + uuid: "abc", + }, + presignedFields: null, + updatedFileName: "test.png", + }, + }; + }, + } as Response) + // second fetch => also OK + .mockResolvedValueOnce({ + ok: true, + } as Response); + + const url = await storage.uploadFile(file, { + allowedFileExtensions: [".png", ".jpg"], + surveyId: "survey_123", + } as TUploadFileConfig); + + expect(url).toBe(mockFileUrl); + expect(fetchMock).toHaveBeenCalledTimes(2); + + // We can also check the first fetch request body + const firstCall = fetchMock.mock.calls[0]; + expect(firstCall[0]).toBe(`${APP_URL}/api/v1/client/${ENV_ID}/storage`); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- we know it's a string + const bodyPayload = JSON.parse(firstCall[1].body as string); + + expect(bodyPayload).toMatchObject({ + fileName: "test.png", + fileType: "image/png", + allowedFileExtensions: [".png", ".jpg"], + surveyId: "survey_123", + }); + }); +}); diff --git a/packages/react-native/src/lib/common/tests/logger.test.ts b/packages/react-native/src/lib/common/tests/logger.test.ts new file mode 100644 index 0000000000..abedede805 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/logger.test.ts @@ -0,0 +1,82 @@ +// logger.test.ts +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { Logger } from "@/lib/common/logger"; + +// adjust import path as needed + +describe("Logger", () => { + let logger: Logger; + let consoleLogSpy: ReturnType; + let consoleErrorSpy: ReturnType; + + beforeEach(() => { + logger = Logger.getInstance(); + + // Reset any existing singleton + logger.resetInstance(); + + logger = Logger.getInstance(); + + // Mock console so we don't actually log in test output + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { + return { + ok: true, + data: undefined, + }; + }); + + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { + return { + ok: true, + data: undefined, + }; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("getInstance() returns a singleton", () => { + const anotherLogger = Logger.getInstance(); + expect(logger).toBe(anotherLogger); + }); + + test("default logLevel is 'error', so debug messages shouldn't appear", () => { + logger.debug("This is a debug log"); + logger.error("This is an error log"); + + // debug should NOT be logged by default + expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining("This is a debug log")); + // error should be logged + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR] - This is an error log")); + }); + + test("configure to logLevel=debug => debug messages appear", () => { + logger.configure({ logLevel: "debug" }); + + logger.debug("Debug log after config"); + logger.error("Error log after config"); + + // debug should now appear + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringMatching(/🧱 Formbricks.*\[DEBUG\].*Debug log after config/) + ); + // error should appear as well + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringMatching(/🧱 Formbricks.*\[ERROR\].*Error log after config/) + ); + }); + + test("logs have correct format including timestamp prefix", () => { + logger.configure({ logLevel: "debug" }); + logger.debug("Some message"); + + // Check that the log includes 🧱 Formbricks, timestamp, [DEBUG], and the message + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringMatching( + /^🧱 Formbricks - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z \[DEBUG\] - Some message$/ + ) + ); + }); +}); diff --git a/packages/react-native/src/lib/common/tests/response-queue.test.ts b/packages/react-native/src/lib/common/tests/response-queue.test.ts new file mode 100644 index 0000000000..63645bfdff --- /dev/null +++ b/packages/react-native/src/lib/common/tests/response-queue.test.ts @@ -0,0 +1,224 @@ +// response-queue.test.ts +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { type FormbricksAPI } from "@formbricks/api"; +import { + mockAppUrl, + mockDisplayId, + mockEnvironmentId, + mockQuestionId, + mockResponseId, + mockSurveyId, + mockUserId, +} from "./__mocks__/response-queue.mock"; +import { ResponseQueue } from "@/lib/common/response-queue"; +import type { SurveyState } from "@/lib/survey/state"; +import type { TResponseUpdate } from "@/types/response"; + +describe("ResponseQueue", () => { + let responseQueue: ResponseQueue; + let mockSurveyState: Partial; + let mockConfig: { + appUrl: string; + environmentId: string; + retryAttempts: number; + onResponseSendingFailed?: (resp: TResponseUpdate) => void; + onResponseSendingFinished?: () => void; + setSurveyState?: (state: SurveyState) => void; + }; + let mockApi: FormbricksAPI; + + beforeEach(() => { + vi.clearAllMocks(); + + // 2) Setup a "fake" surveyState + mockSurveyState = { + responseId: null, + surveyId: mockSurveyId, + userId: mockUserId, + singleUseId: null, + displayId: mockDisplayId, + accumulateResponse: vi.fn(), + updateResponseId: vi.fn(), + }; + + // 3) Setup a config object + mockConfig = { + appUrl: mockAppUrl, + environmentId: mockEnvironmentId, + retryAttempts: 2, + onResponseSendingFailed: vi.fn(), + onResponseSendingFinished: vi.fn(), + setSurveyState: vi.fn(), + }; + + // Create the queue + mockApi = { + client: { + response: { + create: vi.fn(), + update: vi.fn(), + }, + }, + } as unknown as FormbricksAPI; + + responseQueue = new ResponseQueue(mockConfig, mockSurveyState as SurveyState, mockApi); + }); + + test("add() accumulates response, updates setSurveyState, and calls processQueue()", () => { + // Spy on processQueue + const processQueueMock = vi.spyOn(responseQueue, "processQueue"); + + const update: TResponseUpdate = { + data: { + [mockQuestionId]: "test", + }, + ttc: { + [mockQuestionId]: 1000, + }, + finished: false, + }; + + // Call queue.add + responseQueue.add(update); + + expect(mockSurveyState.accumulateResponse).toHaveBeenCalledWith(update); + expect(mockConfig.setSurveyState).toHaveBeenCalledTimes(1); + expect(processQueueMock).toHaveBeenCalledTimes(1); + }); + + test("processQueue does nothing if already in progress or queue is empty", async () => { + // Because processQueue is called in add() + // Let's set isRequestInProgress artificially and call processQueue directly: + const responseQueueWithIsRequestInProgress = responseQueue as unknown as { + isRequestInProgress: boolean; + }; + responseQueueWithIsRequestInProgress.isRequestInProgress = true; + await responseQueue.processQueue(); + + // No changes, no error + expect(responseQueueWithIsRequestInProgress.isRequestInProgress).toBe(true); + + // Now set queue empty, isRequestInProgress false + responseQueueWithIsRequestInProgress.isRequestInProgress = false; + await responseQueue.processQueue(); + // still no error, but no action + // This just ensures we handle those conditions gracefully + }); + + test("when surveyState has no responseId, it calls create(...) and sets responseId on success", async () => { + const formbricksApiMock = vi.spyOn(mockApi.client.response, "create"); + + formbricksApiMock.mockResolvedValueOnce({ + ok: true, + data: { id: mockResponseId }, + }); + + // Add an item + const update: TResponseUpdate = { + data: { [mockQuestionId]: "test" }, + ttc: { [mockQuestionId]: 1000 }, + finished: false, + }; + + responseQueue.add(update); + + // We need to wait for the queue to process + await responseQueue.processQueue(); + + // fake delay for the queue to process and get empty + await new Promise((r) => { + setTimeout(r, 100); + }); + + // Check create call + expect(formbricksApiMock).toHaveBeenCalledWith({ + ...update, + surveyId: mockSurveyId, + userId: mockUserId, + singleUseId: null, + displayId: mockDisplayId, + data: { + [mockQuestionId]: "test", + }, + }); + + // responseId is updated + expect(mockSurveyState.updateResponseId).toHaveBeenCalledWith(mockResponseId); + + const responseQueueWithQueueArr = responseQueue as unknown as { queue: TResponseUpdate[] }; + expect(responseQueueWithQueueArr.queue).toHaveLength(0); + }); + + test("when surveyState has a responseId, it calls update(...) and empties the queue", async () => { + mockSurveyState.responseId = mockResponseId; + + const formbricksApiMock = vi.spyOn(mockApi.client.response, "update"); + + // Mock update => success + formbricksApiMock.mockResolvedValueOnce({ + ok: true, + data: { id: mockResponseId }, + }); + + const update: TResponseUpdate = { + data: { [mockQuestionId]: "test" }, + ttc: { [mockQuestionId]: 1000 }, + finished: false, + }; + + responseQueue.add(update); + + await responseQueue.processQueue(); + + // fake delay for the queue to process and get empty + await new Promise((r) => { + setTimeout(r, 100); + }); + + expect(formbricksApiMock).toHaveBeenCalledWith({ + ...update, + responseId: mockResponseId, + }); + + const responseQueueWithQueueArr = responseQueue as unknown as { queue: TResponseUpdate[] }; + expect(responseQueueWithQueueArr.queue).toHaveLength(0); + }); + + test("retries up to retryAttempts if sendResponse fails", async () => { + // Force create to fail + const formbricksApiMock = vi.spyOn(mockApi.client.response, "create"); + formbricksApiMock.mockRejectedValueOnce(new Error("Network error")); + + const update: TResponseUpdate = { + data: { [mockQuestionId]: "fail" }, + ttc: { [mockQuestionId]: 0 }, + finished: false, + }; + + responseQueue.add(update); + + await new Promise((r) => { + setTimeout(r, 1000); + }); + + await responseQueue.processQueue(); + + await new Promise((r) => { + setTimeout(r, 1000); + }); + + // It tries 2 times + expect(formbricksApiMock).toHaveBeenCalledTimes(2); + // Ultimately fails => item remains in queue + const responseQueueWithQueueArr = responseQueue as unknown as { queue: TResponseUpdate[] }; + expect(responseQueueWithQueueArr.queue).toHaveLength(1); + }); + + test("updateSurveyState updates the surveyState reference", () => { + const newState = { responseId: mockResponseId } as SurveyState; + responseQueue.updateSurveyState(newState); + + const responseQueueWithSurveyState = responseQueue as unknown as { surveyState: SurveyState }; + expect(responseQueueWithSurveyState.surveyState).toBe(newState); + }); +}); diff --git a/packages/react-native/src/lib/common/tests/setup.test.ts b/packages/react-native/src/lib/common/tests/setup.test.ts new file mode 100644 index 0000000000..63759d62f7 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/setup.test.ts @@ -0,0 +1,359 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config"; +import { + addCleanupEventListeners, + addEventListeners, + removeAllEventListeners, +} from "@/lib/common/event-listeners"; +import { Logger } from "@/lib/common/logger"; +import { checkSetup, handleErrorOnFirstSetup, setIsSetup, setup, tearDown } from "@/lib/common/setup"; +import { filterSurveys, isNowExpired } from "@/lib/common/utils"; +import { fetchEnvironmentState } from "@/lib/environment/state"; +import { DEFAULT_USER_STATE_NO_USER_ID } from "@/lib/user/state"; +import { sendUpdatesToBackend } from "@/lib/user/update"; + +// 1) Mock AsyncStorage +vi.mock("@react-native-async-storage/async-storage", () => ({ + default: { + setItem: vi.fn(), + getItem: vi.fn(), + removeItem: vi.fn(), + }, +})); + +// 2) Mock RNConfig +vi.mock("@/lib/common/config", () => ({ + RN_ASYNC_STORAGE_KEY: "formbricks-react-native", + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + resetConfig: vi.fn(), + })), + }, +})); + +// 3) Mock logger +vi.mock("@/lib/common/logger", () => ({ + Logger: { + getInstance: vi.fn(() => ({ + debug: vi.fn(), + error: vi.fn(), + })), + }, +})); + +// 4) Mock event-listeners +vi.mock("@/lib/common/event-listeners", () => ({ + addEventListeners: vi.fn(), + addCleanupEventListeners: vi.fn(), + removeAllEventListeners: vi.fn(), +})); + +// 5) Mock fetchEnvironmentState +vi.mock("@/lib/environment/state", () => ({ + fetchEnvironmentState: vi.fn(), +})); + +// 6) Mock filterSurveys +vi.mock("@/lib/common/utils", async (importOriginal) => { + return { + ...(await importOriginal()), + filterSurveys: vi.fn(), + isNowExpired: vi.fn(), + }; +}); + +// 7) Mock user/update +vi.mock("@/lib/user/update", () => ({ + sendUpdatesToBackend: vi.fn(), +})); + +describe("setup.ts", () => { + let getInstanceConfigMock: MockInstance<() => RNConfig>; + let getInstanceLoggerMock: MockInstance<() => Logger>; + + const mockLogger = { + debug: vi.fn(), + error: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + // By default, set isSetup to false so we can test setup logic from scratch + setIsSetup(false); + + getInstanceConfigMock = vi.spyOn(RNConfig, "getInstance"); + getInstanceLoggerMock = vi.spyOn(Logger, "getInstance").mockReturnValue(mockLogger as unknown as Logger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("setup()", () => { + test("returns ok if already setup", async () => { + getInstanceLoggerMock.mockReturnValue(mockLogger as unknown as Logger); + setIsSetup(true); + const result = await setup({ environmentId: "env_id", appUrl: "https://my.url" }); + expect(result.ok).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith("Already set up, skipping setup."); + }); + + test("fails if no environmentId is provided", async () => { + const result = await setup({ environmentId: "", appUrl: "https://my.url" }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("missing_field"); + } + }); + + test("fails if no appUrl is provided", async () => { + const result = await setup({ environmentId: "env_123", appUrl: "" }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("missing_field"); + } + }); + + test("skips setup if existing config is in error state and not expired", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environmentId: "env_123", + appUrl: "https://my.url", + environment: {}, + user: { data: {}, expiresAt: null }, + status: { value: "error", expiresAt: new Date(Date.now() + 10000) }, + }), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + + (isNowExpired as unknown as Mock).mockReturnValue(true); + + const result = await setup({ environmentId: "env_123", appUrl: "https://my.url" }); + expect(result.ok).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith("Formbricks was set to an error state."); + expect(mockLogger.debug).toHaveBeenCalledWith("Error state is not expired, skipping setup"); + }); + + test("proceeds if error state is expired", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environmentId: "env_123", + appUrl: "https://my.url", + environment: {}, + user: { data: {}, expiresAt: null }, + status: { value: "error", expiresAt: new Date(Date.now() - 10000) }, // expired + }), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + + const result = await setup({ environmentId: "env_123", appUrl: "https://my.url" }); + expect(result.ok).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith("Formbricks was set to an error state."); + expect(mockLogger.debug).toHaveBeenCalledWith("Error state is expired. Continue with setup."); + }); + + test("uses existing config if environmentId/appUrl match, checks for expiration sync", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environmentId: "env_123", + appUrl: "https://my.url", + environment: { expiresAt: new Date(Date.now() - 5000) }, // environment expired + user: { + data: { userId: "user_abc" }, + expiresAt: new Date(Date.now() - 5000), // also expired + }, + status: { value: "success", expiresAt: null }, + }), + update: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + + (isNowExpired as unknown as Mock).mockReturnValue(true); + + // Mock environment fetch success + (fetchEnvironmentState as unknown as Mock).mockResolvedValueOnce({ + ok: true, + data: { data: { surveys: [] }, expiresAt: new Date(Date.now() + 60_000) }, + }); + + // Mock sendUpdatesToBackend success + (sendUpdatesToBackend as unknown as Mock).mockResolvedValueOnce({ + ok: true, + data: { + state: { + expiresAt: new Date(), + data: { userId: "user_abc", segments: [] }, + }, + }, + }); + + (filterSurveys as unknown as Mock).mockReturnValueOnce([{ name: "S1" }, { name: "S2" }]); + + const result = await setup({ environmentId: "env_123", appUrl: "https://my.url" }); + expect(result.ok).toBe(true); + + // environmentState was fetched + expect(fetchEnvironmentState).toHaveBeenCalled(); + // user state was updated + expect(sendUpdatesToBackend).toHaveBeenCalled(); + // filterSurveys called + expect(filterSurveys).toHaveBeenCalled(); + // config updated + expect(mockConfig.update).toHaveBeenCalledWith( + expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- required for testing this object + user: expect.objectContaining({ + data: { userId: "user_abc", segments: [] }, + }), + filteredSurveys: [{ name: "S1" }, { name: "S2" }], + }) + ); + }); + + test("resets config if no valid config found, fetches environment, sets default user", async () => { + const mockConfig = { + get: () => { + throw new Error("no config found"); + }, + resetConfig: vi.fn(), + update: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + + (fetchEnvironmentState as unknown as Mock).mockResolvedValueOnce({ + ok: true, + data: { + data: { + surveys: [{ name: "SurveyA" }], + expiresAt: new Date(Date.now() + 60000), + }, + }, + }); + + (filterSurveys as unknown as Mock).mockReturnValueOnce([{ name: "SurveyA" }]); + + const result = await setup({ environmentId: "envX", appUrl: "https://urlX" }); + expect(result.ok).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledWith("No existing configuration found."); + expect(mockLogger.debug).toHaveBeenCalledWith( + "No valid configuration found. Resetting config and creating new one." + ); + expect(mockConfig.resetConfig).toHaveBeenCalled(); + expect(fetchEnvironmentState).toHaveBeenCalled(); + expect(mockConfig.update).toHaveBeenCalledWith({ + appUrl: "https://urlX", + environmentId: "envX", + user: DEFAULT_USER_STATE_NO_USER_ID, + environment: { + data: { + surveys: [{ name: "SurveyA" }], + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- required for testing this object + expiresAt: expect.any(Date), + }, + }, + filteredSurveys: [{ name: "SurveyA" }], + }); + }); + + test("calls handleErrorOnFirstSetup if environment fetch fails initially", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn(), + resetConfig: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValueOnce(mockConfig as unknown as RNConfig); + + (fetchEnvironmentState as unknown as Mock).mockResolvedValueOnce({ + ok: false, + error: { code: "forbidden", responseMessage: "No access" }, + }); + + await expect(setup({ environmentId: "envX", appUrl: "https://urlX" })).rejects.toThrow( + "Could not set up formbricks" + ); + }); + + test("adds event listeners and sets isSetup", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environmentId: "env_abc", + appUrl: "https://test.app", + environment: {}, + user: { data: {}, expiresAt: null }, + status: { value: "success", expiresAt: null }, + }), + update: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValueOnce(mockConfig as unknown as RNConfig); + + const result = await setup({ environmentId: "env_abc", appUrl: "https://test.app" }); + expect(result.ok).toBe(true); + expect(addEventListeners).toHaveBeenCalled(); + expect(addCleanupEventListeners).toHaveBeenCalled(); + }); + }); + + describe("checkSetup()", () => { + test("returns err if not setup", () => { + const res = checkSetup(); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error.code).toBe("not_setup"); + } + }); + + test("returns ok if setup", () => { + setIsSetup(true); + const res = checkSetup(); + expect(res.ok).toBe(true); + }); + }); + + describe("tearDown()", () => { + test("resets user state to default and removes event listeners", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + user: { data: { userId: "XYZ" } }, + }), + update: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValueOnce(mockConfig as unknown as RNConfig); + + await tearDown(); + + expect(mockConfig.update).toHaveBeenCalledWith( + expect.objectContaining({ + user: DEFAULT_USER_STATE_NO_USER_ID, + }) + ); + expect(removeAllEventListeners).toHaveBeenCalled(); + }); + }); + + describe("handleErrorOnFirstSetup()", () => { + test("stores error state in AsyncStorage, throws error", async () => { + // We import the function directly + const errorObj = { code: "forbidden", responseMessage: "No access" }; + + await expect(async () => { + await handleErrorOnFirstSetup(errorObj); + }).rejects.toThrow("Could not set up formbricks"); + + // AsyncStorage setItem should be called with the error config + expect(AsyncStorage.setItem).toHaveBeenCalledWith( + RN_ASYNC_STORAGE_KEY, + expect.stringContaining('"value":"error"') + ); + }); + }); +}); diff --git a/packages/react-native/src/lib/common/tests/utils.test.ts b/packages/react-native/src/lib/common/tests/utils.test.ts new file mode 100644 index 0000000000..36f87d37a7 --- /dev/null +++ b/packages/react-native/src/lib/common/tests/utils.test.ts @@ -0,0 +1,394 @@ +// utils.test.ts +import { mockProjectId, mockSurveyId } from "@/lib/common/tests/__mocks__/config.mock"; +import { + diffInDays, + filterSurveys, + getDefaultLanguageCode, + getLanguageCode, + getStyling, + shouldDisplayBasedOnPercentage, + wrapThrowsAsync, +} from "@/lib/common/utils"; +import type { + TEnvironmentState, + TEnvironmentStateProject, + TEnvironmentStateSurvey, + TSurveyStyling, + TUserState, +} from "@/types/config"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const mockSurveyId1 = "e3kxlpnzmdp84op9qzxl9olj"; +const mockSurveyId2 = "qo9rwjmms42hoy3k85fp8vgu"; +const mockSegmentId1 = "p6yrnz3s2tvoe5r0l28unq7k"; +const mockSegmentId2 = "wz43zrxeddhb1uo9cicustar"; + +describe("utils.ts", () => { + // --------------------------------------------------------------------------------- + // diffInDays + // --------------------------------------------------------------------------------- + describe("diffInDays()", () => { + test("calculates correct day difference", () => { + const date1 = new Date("2023-01-01"); + const date2 = new Date("2023-01-05"); + expect(diffInDays(date1, date2)).toBe(4); // four days apart + }); + + test("handles negative differences (abs)", () => { + const date1 = new Date("2023-01-10"); + const date2 = new Date("2023-01-05"); + expect(diffInDays(date1, date2)).toBe(5); + }); + + test("0 if same day", () => { + const date = new Date("2023-01-01"); + expect(diffInDays(date, date)).toBe(0); + }); + }); + + // --------------------------------------------------------------------------------- + // wrapThrowsAsync + // --------------------------------------------------------------------------------- + describe("wrapThrowsAsync()", () => { + test("returns ok on success", async () => { + const fn = vi.fn(async (x: number) => { + await new Promise((r) => { + setTimeout(r, 10); + }); + return x * 2; + }); + + const wrapped = wrapThrowsAsync(fn); + + const result = await wrapped(5); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toBe(10); + } + }); + + test("returns err on error", async () => { + const fn = vi.fn(async () => { + await new Promise((r) => { + setTimeout(r, 10); + }); + throw new Error("Something broke"); + }); + const wrapped = wrapThrowsAsync(fn); + + const result = await wrapped(); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.message).toBe("Something broke"); + } + }); + }); + + // --------------------------------------------------------------------------------- + // filterSurveys + // --------------------------------------------------------------------------------- + describe("filterSurveys()", () => { + // We'll create a minimal environment state + let environment: TEnvironmentState; + let user: TUserState; + const baseSurvey: Partial = { + id: mockSurveyId, + displayOption: "displayOnce", + displayLimit: 1, + recontactDays: null, + languages: [], + }; + + beforeEach(() => { + environment = { + expiresAt: new Date(), + data: { + project: { + id: mockProjectId, + recontactDays: 7, // fallback if survey doesn't have it + clickOutsideClose: false, + darkOverlay: false, + placement: "bottomRight", + inAppSurveyBranding: true, + styling: { allowStyleOverwrite: false }, + } as TEnvironmentStateProject, + surveys: [], + actionClasses: [], + }, + }; + user = { + expiresAt: null, + data: { + userId: null, + segments: [], + displays: [], + responses: [], + lastDisplayAt: null, + }, + }; + }); + + test("returns no surveys if user has no segments and userId is set", () => { + user.data.userId = "user_abc"; + // environment has a single survey + environment.data.surveys = [ + { ...baseSurvey, id: mockSurveyId1, segment: { id: mockSegmentId1 } } as TEnvironmentStateSurvey, + ]; + + const result = filterSurveys(environment, user); + expect(result).toEqual([]); // no segments => none pass + }); + + test("returns surveys if user has no userId but displayOnce and no displays yet", () => { + // userId is null => it won't segment filter + environment.data.surveys = [ + { ...baseSurvey, id: mockSurveyId1, displayOption: "displayOnce" } as TEnvironmentStateSurvey, + ]; + + const result = filterSurveys(environment, user); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(mockSurveyId1); + }); + + test("skips surveys that already displayed if displayOnce is used", () => { + environment.data.surveys = [ + { ...baseSurvey, id: mockSurveyId1, displayOption: "displayOnce" } as TEnvironmentStateSurvey, + ]; + user.data.displays = [{ surveyId: mockSurveyId1, createdAt: new Date() }]; + + const result = filterSurveys(environment, user); + expect(result).toEqual([]); + }); + + test("skips surveys if user responded to them and displayOption=displayMultiple", () => { + environment.data.surveys = [ + { ...baseSurvey, id: mockSurveyId1, displayOption: "displayMultiple" } as TEnvironmentStateSurvey, + ]; + user.data.responses = [mockSurveyId1]; + + const result = filterSurveys(environment, user); + expect(result).toEqual([]); + }); + + test("handles displaySome logic with displayLimit", () => { + environment.data.surveys = [ + { + ...baseSurvey, + id: mockSurveyId1, + displayOption: "displaySome", + displayLimit: 2, + } as TEnvironmentStateSurvey, + ]; + // user has 1 display of s1 + user.data.displays = [{ surveyId: mockSurveyId1, createdAt: new Date() }]; + + // No responses => so it's still allowed + const result = filterSurveys(environment, user); + expect(result).toHaveLength(1); + }); + + test("filters out surveys if recontactDays not met", () => { + // Suppose survey uses project fallback (7 days) + environment.data.surveys = [ + { ...baseSurvey, id: mockSurveyId1, displayOption: "displayOnce" } as TEnvironmentStateSurvey, + ]; + // user last displayAt is only 3 days ago + user.data.lastDisplayAt = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); + + const result = filterSurveys(environment, user); + expect(result).toHaveLength(0); + }); + + test("passes surveys if enough days have passed since lastDisplayAt", () => { + // user last displayAt is 8 days ago + user.data.lastDisplayAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000); + + environment.data.surveys = [ + { + ...baseSurvey, + id: mockSurveyId1, + displayOption: "respondMultiple", + recontactDays: null, + } as TEnvironmentStateSurvey, + ]; + const result = filterSurveys(environment, user); + expect(result).toHaveLength(1); + }); + + test("filters by segment if userId is set and user has segments", () => { + user.data.userId = "user_abc"; + user.data.segments = [mockSegmentId1]; + environment.data.surveys = [ + { + ...baseSurvey, + id: mockSurveyId1, + segment: { id: mockSegmentId1 }, + displayOption: "respondMultiple", + } as TEnvironmentStateSurvey, + { + ...baseSurvey, + id: mockSurveyId2, + segment: { id: mockSegmentId2 }, + displayOption: "respondMultiple", + } as TEnvironmentStateSurvey, + ]; + + const result = filterSurveys(environment, user); + // only the one that matches user's segment + expect(result).toHaveLength(1); + expect(result[0].id).toBe(mockSurveyId1); + }); + }); + + // --------------------------------------------------------------------------------- + // getStyling + // --------------------------------------------------------------------------------- + describe("getStyling()", () => { + test("returns project styling if allowStyleOverwrite=false", () => { + const project = { + id: "p1", + styling: { allowStyleOverwrite: false, brandColor: { light: "#fff" } }, + } as TEnvironmentStateProject; + const survey = { + styling: { + overwriteThemeStyling: true, + brandColor: { light: "#000" }, + } as TSurveyStyling, + } as TEnvironmentStateSurvey; + + const result = getStyling(project, survey); + // should get project styling + expect(result).toEqual(project.styling); + }); + + test("returns project styling if allowStyleOverwrite=true but survey overwriteThemeStyling=false", () => { + const project = { + id: "p1", + styling: { allowStyleOverwrite: true, brandColor: { light: "#fff" } }, + } as TEnvironmentStateProject; + const survey = { + styling: { + overwriteThemeStyling: false, + brandColor: { light: "#000" }, + } as TSurveyStyling, + } as TEnvironmentStateSurvey; + + const result = getStyling(project, survey); + // should get project styling still + expect(result).toEqual(project.styling); + }); + + test("returns survey styling if allowStyleOverwrite=true and survey overwriteThemeStyling=true", () => { + const project = { + id: "p1", + styling: { allowStyleOverwrite: true, brandColor: { light: "#fff" } }, + } as TEnvironmentStateProject; + const survey = { + styling: { + overwriteThemeStyling: true, + brandColor: { light: "#000" }, + } as TSurveyStyling, + } as TEnvironmentStateSurvey; + + const result = getStyling(project, survey); + expect(result).toEqual(survey.styling); + }); + }); + + // --------------------------------------------------------------------------------- + // getDefaultLanguageCode + // --------------------------------------------------------------------------------- + describe("getDefaultLanguageCode()", () => { + test("returns code of the language if it is flagged default", () => { + const survey = { + languages: [ + { + language: { code: "en" }, + default: false, + enabled: true, + }, + { + language: { code: "fr" }, + default: true, + enabled: true, + }, + ], + } as unknown as TEnvironmentStateSurvey; + expect(getDefaultLanguageCode(survey)).toBe("fr"); + }); + + test("returns undefined if no default language found", () => { + const survey = { + languages: [ + { language: { code: "en" }, default: false, enabled: true }, + { language: { code: "fr" }, default: false, enabled: true }, + ], + } as unknown as TEnvironmentStateSurvey; + expect(getDefaultLanguageCode(survey)).toBeUndefined(); + }); + }); + + // --------------------------------------------------------------------------------- + // getLanguageCode + // --------------------------------------------------------------------------------- + describe("getLanguageCode()", () => { + test("returns 'default' if no language param is passed", () => { + const survey = { + languages: [{ language: { code: "en" }, default: true, enabled: true }], + } as unknown as TEnvironmentStateSurvey; + const code = getLanguageCode(survey, undefined); + expect(code).toBe("default"); + }); + + test("returns 'default' if the chosen language is the default one", () => { + const survey = { + languages: [ + { language: { code: "en" }, default: true, enabled: true }, + { language: { code: "fr" }, default: false, enabled: true }, + ], + } as unknown as TEnvironmentStateSurvey; + const code = getLanguageCode(survey, "en"); + expect(code).toBe("default"); + }); + + test("returns undefined if language not found or disabled", () => { + const survey = { + languages: [ + { language: { code: "en" }, default: true, enabled: true }, + { language: { code: "fr" }, default: false, enabled: false }, + ], + } as unknown as TEnvironmentStateSurvey; + const code = getLanguageCode(survey, "fr"); + expect(code).toBeUndefined(); + }); + + test("returns the language code if found and enabled", () => { + const survey = { + languages: [ + { language: { code: "en", alias: "English" }, default: true, enabled: true }, + { language: { code: "fr", alias: "fr-FR" }, default: false, enabled: true }, + ], + } as unknown as TEnvironmentStateSurvey; + expect(getLanguageCode(survey, "fr")).toBe("fr"); + expect(getLanguageCode(survey, "fr-FR")).toBe("fr"); + }); + }); + + // --------------------------------------------------------------------------------- + // shouldDisplayBasedOnPercentage + // --------------------------------------------------------------------------------- + describe("shouldDisplayBasedOnPercentage()", () => { + test("returns true if random number <= displayPercentage", () => { + // We'll mock Math.random to return something + const mockedRandom = vi.spyOn(Math, "random").mockReturnValue(0.2); // 0.2 => 20% + // displayPercentage = 30 => 30% => we should display + expect(shouldDisplayBasedOnPercentage(30)).toBe(true); + + mockedRandom.mockReturnValue(0.5); // 50% + expect(shouldDisplayBasedOnPercentage(30)).toBe(false); + + // restore + mockedRandom.mockRestore(); + }); + }); +}); diff --git a/packages/react-native/src/lib/common/utils.ts b/packages/react-native/src/lib/common/utils.ts new file mode 100644 index 0000000000..912487d6ae --- /dev/null +++ b/packages/react-native/src/lib/common/utils.ts @@ -0,0 +1,170 @@ +import type { + TEnvironmentState, + TEnvironmentStateProject, + TEnvironmentStateSurvey, + TProjectStyling, + TSurveyStyling, + TUserState, +} from "@/types/config"; +import type { Result } from "@/types/error"; + +// Helper function to calculate difference in days between two dates +export const diffInDays = (date1: Date, date2: Date): number => { + const diffTime = Math.abs(date2.getTime() - date1.getTime()); + return Math.floor(diffTime / (1000 * 60 * 60 * 24)); +}; + +export const wrapThrowsAsync = + (fn: (...args: A) => Promise) => + async (...args: A): Promise> => { + try { + return { + ok: true, + data: await fn(...args), + }; + } catch (error) { + return { + ok: false, + error: error as Error, + }; + } + }; + +/** + * Filters surveys based on the displayOption, recontactDays, and segments + * @param environmentSate - The environment state + * @param userState - The user state + * @returns The filtered surveys + */ + +// takes the environment and user state and returns the filtered surveys +export const filterSurveys = ( + environmentState: TEnvironmentState, + userState: TUserState +): TEnvironmentStateSurvey[] => { + const { project, surveys } = environmentState.data; + const { displays, responses, lastDisplayAt, segments, userId } = userState.data; + + // Function to filter surveys based on displayOption criteria + let filteredSurveys = surveys.filter((survey: TEnvironmentStateSurvey) => { + switch (survey.displayOption) { + case "respondMultiple": + return true; + case "displayOnce": + return displays.filter((display) => display.surveyId === survey.id).length === 0; + case "displayMultiple": + return responses.filter((surveyId) => surveyId === survey.id).length === 0; + + case "displaySome": + if (survey.displayLimit === null) { + return true; + } + + // Check if survey response exists, if so, stop here + if (responses.filter((surveyId) => surveyId === survey.id).length) { + return false; + } + + // Otherwise, check if displays length is less than displayLimit + return displays.filter((display) => display.surveyId === survey.id).length < survey.displayLimit; + + default: + throw Error("Invalid displayOption"); + } + }); + + // filter surveys that meet the recontactDays criteria + filteredSurveys = filteredSurveys.filter((survey) => { + // if no survey was displayed yet, show the survey + if (!lastDisplayAt) { + return true; + } + + // if survey has recontactDays, check if the last display was more than recontactDays ago + // The previous approach checked the last display for each survey which is why we still have a surveyId in the displays array. + // TODO: Remove the surveyId from the displays array + if (survey.recontactDays !== null) { + return diffInDays(new Date(), new Date(lastDisplayAt)) >= survey.recontactDays; + } + + // use recontactDays of the project if survey does not have recontactDays + if (project.recontactDays) { + return diffInDays(new Date(), new Date(lastDisplayAt)) >= project.recontactDays; + } + + // if no recontactDays is set, show the survey + + return true; + }); + + if (!userId) { + return filteredSurveys; + } + + if (!segments.length) { + return []; + } + + // filter surveys based on segments + return filteredSurveys.filter((survey) => { + return survey.segment?.id && segments.includes(survey.segment.id); + }); +}; + +export const getStyling = ( + project: TEnvironmentStateProject, + survey: TEnvironmentStateSurvey +): TProjectStyling | TSurveyStyling => { + // allow style overwrite is enabled from the project + if (project.styling.allowStyleOverwrite) { + // survey style overwrite is disabled + if (!survey.styling?.overwriteThemeStyling) { + return project.styling; + } + + // survey style overwrite is enabled + return survey.styling; + } + + // allow style overwrite is disabled from the project + return project.styling; +}; + +export const getDefaultLanguageCode = (survey: TEnvironmentStateSurvey): string | undefined => { + const defaultSurveyLanguage = survey.languages.find((surveyLanguage) => { + return surveyLanguage.default; + }); + if (defaultSurveyLanguage) return defaultSurveyLanguage.language.code; +}; + +export const getLanguageCode = (survey: TEnvironmentStateSurvey, language?: string): string | undefined => { + const availableLanguageCodes = survey.languages.map((surveyLanguage) => surveyLanguage.language.code); + if (!language) return "default"; + + const selectedLanguage = survey.languages.find((surveyLanguage) => { + return ( + surveyLanguage.language.code === language.toLowerCase() || + surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase() + ); + }); + if (selectedLanguage?.default) { + return "default"; + } + if ( + !selectedLanguage || + !selectedLanguage.enabled || + !availableLanguageCodes.includes(selectedLanguage.language.code) + ) { + return undefined; + } + return selectedLanguage.language.code; +}; + +export const shouldDisplayBasedOnPercentage = (displayPercentage: number): boolean => { + const randomNum = Math.floor(Math.random() * 10000) / 100; + return randomNum <= displayPercentage; +}; + +export const isNowExpired = (expirationDate: Date): boolean => { + return new Date() >= expirationDate; +}; diff --git a/packages/react-native/src/lib/environment-state.ts b/packages/react-native/src/lib/environment-state.ts deleted file mode 100644 index cdb8887661..0000000000 --- a/packages/react-native/src/lib/environment-state.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable no-console -- logging required for error logging */ -// shared functions for environment and person state(s) -import { type TJsEnvironmentState, type TJsEnvironmentSyncParams } from "@formbricks/types/js"; -import { err } from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; -import { filterSurveys } from "../../../js-core/src/lib/utils"; -import { RNConfig } from "./config"; - -const appConfig = RNConfig.getInstance(); -const logger = Logger.getInstance(); -let environmentStateSyncIntervalId: number | null = null; - -/** - * Fetch the environment state from the backend - * @param apiHost - The API host - * @param environmentId - The environment ID - * @param noCache - Whether to skip the cache - * @returns The environment state - * @throws NetworkError - */ -export const fetchEnvironmentState = async ( - { apiHost, environmentId }: TJsEnvironmentSyncParams, - noCache = false -): Promise => { - const url = `${apiHost}/api/v1/client/${environmentId}/environment`; - - try { - const fetchOptions: RequestInit = {}; - - if (noCache) { - fetchOptions.cache = "no-cache"; - logger.debug("No cache option set for sync"); - } - - const response = await fetch(url, fetchOptions); - - if (!response.ok) { - const jsonRes = (await response.json()) as { message: string }; - - const error = err({ - code: "network_error", - status: response.status, - message: "Error syncing with backend", - url: new URL(url), - responseMessage: jsonRes.message, - }); - - // eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error is an Error object - throw error.error; - } - - const data = (await response.json()) as { data: TJsEnvironmentState["data"] }; - const { data: state } = data; - - return { - data: { ...state }, - expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes - }; - } catch (e: unknown) { - const errorTyped = e as { message?: string }; - - const error = err({ - code: "network_error", - message: errorTyped.message ?? "Error fetching the environment state", - status: 500, - url: new URL(url), - responseMessage: errorTyped.message ?? "Unknown error", - }); - - // eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error is an Error object - throw error.error; - } -}; - -/** - * Add a listener to check if the environment state has expired with a certain interval - */ -export const addEnvironmentStateExpiryCheckListener = (): void => { - const updateInterval = 1000 * 60; // every minute - - if (environmentStateSyncIntervalId === null) { - const intervalHandler = async (): Promise => { - const expiresAt = appConfig.get().environmentState.expiresAt; - - try { - // check if the environmentState has not expired yet - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- expiresAt is checked for null - if (expiresAt && new Date(expiresAt) >= new Date()) { - return; - } - - logger.debug("Environment State has expired. Starting sync."); - - const personState = appConfig.get().personState; - const environmentState = await fetchEnvironmentState( - { - apiHost: appConfig.get().apiHost, - environmentId: appConfig.get().environmentId, - }, - true - ); - - const filteredSurveys = filterSurveys(environmentState, personState); - - appConfig.update({ - ...appConfig.get(), - environmentState, - filteredSurveys, - }); - } catch (e) { - console.error(`Error during expiry check: ${e as string}`); - logger.debug("Extending config and try again later."); - const existingConfig = appConfig.get(); - appConfig.update(existingConfig); - } - }; - - environmentStateSyncIntervalId = setInterval( - () => void intervalHandler(), - updateInterval - ) as unknown as number; - } -}; - -export const clearEnvironmentStateExpiryCheckListener = (): void => { - if (environmentStateSyncIntervalId) { - clearInterval(environmentStateSyncIntervalId); - environmentStateSyncIntervalId = null; - } -}; diff --git a/packages/react-native/src/lib/environment/state.ts b/packages/react-native/src/lib/environment/state.ts new file mode 100644 index 0000000000..f62349df67 --- /dev/null +++ b/packages/react-native/src/lib/environment/state.ts @@ -0,0 +1,118 @@ +/* eslint-disable no-console -- logging required for error logging */ +import { FormbricksAPI } from "@formbricks/api"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { filterSurveys } from "@/lib/common/utils"; +import type { TConfigInput, TEnvironmentState } from "@/types/config"; +import { type ApiErrorResponse, type Result, err, ok } from "@/types/error"; + +let environmentStateSyncIntervalId: number | null = null; + +/** + * Fetch the environment state from the backend + * @param appUrl - The app URL + * @param environmentId - The environment ID + * @returns The environment state + * @throws NetworkError + */ +export const fetchEnvironmentState = async ({ + appUrl, + environmentId, +}: TConfigInput): Promise> => { + const url = `${appUrl}/api/v1/client/${environmentId}/environment`; + const api = new FormbricksAPI({ apiHost: appUrl, environmentId }); + + try { + const response = await api.client.environment.getState(); + + if (!response.ok) { + return err({ + code: response.error.code, + status: response.error.status, + message: "Error syncing with backend", + url: new URL(url), + responseMessage: response.error.message, + }); + } + + return ok(response.data) as Result; + } catch (e: unknown) { + const errorTyped = e as ApiErrorResponse; + return err({ + code: "network_error", + message: errorTyped.message, + status: 500, + url: new URL(url), + responseMessage: errorTyped.responseMessage ?? "Network error", + }); + } +}; + +/** + * Add a listener to check if the environment state has expired with a certain interval + */ +export const addEnvironmentStateExpiryCheckListener = (): void => { + const appConfig = RNConfig.getInstance(); + const logger = Logger.getInstance(); + + const updateInterval = 1000 * 60; // every minute + + if (environmentStateSyncIntervalId === null) { + const intervalHandler = async (): Promise => { + const expiresAt = appConfig.get().environment.expiresAt; + + try { + // check if the environmentState has not expired yet + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- expiresAt is checked for null + if (expiresAt && new Date(expiresAt) >= new Date()) { + return; + } + + logger.debug("Environment State has expired. Starting sync."); + + const personState = appConfig.get().user; + const environmentState = await fetchEnvironmentState({ + appUrl: appConfig.get().appUrl, + environmentId: appConfig.get().environmentId, + }); + + if (environmentState.ok) { + const { data: state } = environmentState; + const filteredSurveys = filterSurveys(state, personState); + + appConfig.update({ + ...appConfig.get(), + environment: state, + filteredSurveys, + }); + } else { + // eslint-disable-next-line @typescript-eslint/only-throw-error -- error is an ApiErrorResponse + throw environmentState.error; + } + } catch (e) { + console.error(`Error during expiry check: `, e); + logger.debug("Extending config and try again later."); + const existingConfig = appConfig.get(); + appConfig.update({ + ...existingConfig, + environment: { + ...existingConfig.environment, + expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes + }, + }); + } + }; + + environmentStateSyncIntervalId = setInterval( + () => void intervalHandler(), + updateInterval + ) as unknown as number; + } +}; + +export const clearEnvironmentStateExpiryCheckListener = (): void => { + if (environmentStateSyncIntervalId) { + clearInterval(environmentStateSyncIntervalId); + environmentStateSyncIntervalId = null; + } +}; diff --git a/packages/react-native/src/lib/environment/tests/state.test.ts b/packages/react-native/src/lib/environment/tests/state.test.ts new file mode 100644 index 0000000000..04807d866c --- /dev/null +++ b/packages/react-native/src/lib/environment/tests/state.test.ts @@ -0,0 +1,306 @@ +// state.test.ts +import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { FormbricksAPI } from "@formbricks/api"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { filterSurveys } from "@/lib/common/utils"; +import { + addEnvironmentStateExpiryCheckListener, + clearEnvironmentStateExpiryCheckListener, + fetchEnvironmentState, +} from "@/lib/environment/state"; +import type { TEnvironmentState } from "@/types/config"; + +// Mock the FormbricksAPI so we can control environment.getState +vi.mock("@formbricks/api", () => ({ + FormbricksAPI: vi.fn().mockImplementation(() => ({ + client: { + environment: { + getState: vi.fn(), + }, + }, + })), +})); + +// Mock logger (so we don’t spam console) +vi.mock("@/lib/common/logger", () => ({ + Logger: { + getInstance: vi.fn(() => { + return { + debug: vi.fn(), + error: vi.fn(), + }; + }), + }, +})); + +// Mock filterSurveys +vi.mock("@/lib/common/utils", () => ({ + filterSurveys: vi.fn(), +})); + +// Mock RNConfig +vi.mock("@/lib/common/config", () => { + return { + RN_ASYNC_STORAGE_KEY: "formbricks-react-native", + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, + }; +}); + +describe("environment/state.ts", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + // Use real timers so we don't pollute subsequent test code + vi.useRealTimers(); + }); + + describe("fetchEnvironmentState()", () => { + test("returns ok(...) with environment state", async () => { + // Setup mock + (FormbricksAPI as unknown as Mock).mockImplementationOnce(() => { + return { + client: { + environment: { + getState: vi.fn().mockResolvedValue({ + ok: true, + data: { data: { foo: "bar" }, expiresAt: new Date(Date.now() + 1000 * 60 * 30) }, + }), + }, + }, + }; + }); + + const result = await fetchEnvironmentState({ + appUrl: "https://fake.host", + environmentId: "env_123", + }); + + expect(result.ok).toBe(true); + + if (result.ok) { + const val: TEnvironmentState = result.data; + expect(val.data).toEqual({ foo: "bar" }); + expect(val.expiresAt).toBeInstanceOf(Date); + } + }); + + test("returns err(...) if environment.getState is not ok", async () => { + const mockError = { code: "forbidden", status: 403, message: "Access denied" }; + + (FormbricksAPI as unknown as Mock).mockImplementationOnce(() => { + return { + client: { + environment: { + getState: vi.fn().mockResolvedValue({ + ok: false, + error: mockError, + }), + }, + }, + }; + }); + + const result = await fetchEnvironmentState({ + appUrl: "https://fake.host", + environmentId: "env_123", + }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe(mockError.code); + expect(result.error.status).toBe(mockError.status); + expect(result.error.responseMessage).toBe(mockError.message); + } + }); + + test("returns err(...) on network error catch", async () => { + const mockNetworkError = { + code: "network_error", + message: "Timeout", + responseMessage: "Network fail", + }; + + (FormbricksAPI as unknown as Mock).mockImplementationOnce(() => { + return { + client: { + environment: { + getState: vi.fn().mockRejectedValue(mockNetworkError), + }, + }, + }; + }); + + const result = await fetchEnvironmentState({ + appUrl: "https://fake.host", + environmentId: "env_123", + }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe(mockNetworkError.code); + expect(result.error.message).toBe(mockNetworkError.message); + expect(result.error.responseMessage).toBe(mockNetworkError.responseMessage); + } + }); + }); + + describe("addEnvironmentStateExpiryCheckListener()", () => { + let mockRNConfig: MockInstance<() => RNConfig>; + let mockLoggerInstance: MockInstance<() => Logger>; + + const mockLogger = { + debug: vi.fn(), + error: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + + mockRNConfig = vi.spyOn(RNConfig, "getInstance"); + const mockConfig = { + get: vi.fn().mockReturnValue({ + environment: { + expiresAt: new Date(Date.now() + 60_000), // Not expired for now + }, + user: {}, + environmentId: "env_123", + appUrl: "https://fake.host", + }), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + mockLoggerInstance = vi.spyOn(Logger, "getInstance"); + mockLoggerInstance.mockReturnValue(mockLogger as unknown as Logger); + }); + + afterEach(() => { + clearEnvironmentStateExpiryCheckListener(); // clear after each test + }); + + test("starts interval check and updates state when expired", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environment: { + expiresAt: new Date(Date.now() - 1000).toISOString(), // expired + }, + appUrl: "https://test.com", + environmentId: "env_123", + user: { data: {} }, + }), + update: vi.fn(), + }; + + const mockNewState = { + data: { + expiresAt: new Date(Date.now() + 1000 * 60 * 30).toISOString(), + }, + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + environment: { + getState: vi.fn().mockResolvedValue({ + ok: true, + data: mockNewState, + }), + }, + }, + })); + + (filterSurveys as Mock).mockReturnValue([]); + + // Add listener + addEnvironmentStateExpiryCheckListener(); + + // Fast-forward time + await vi.advanceTimersByTimeAsync(1000 * 60); + + // Verify the update was called + expect(mockConfig.update).toHaveBeenCalled(); + }); + + test("extends expiry on error", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environment: { + expiresAt: new Date(Date.now() - 1000).toISOString(), + }, + appUrl: "https://test.com", + environmentId: "env_123", + }), + update: vi.fn(), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + // Mock API to throw an error + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + environment: { + getState: vi.fn().mockRejectedValue(new Error("Network error")), + }, + }, + })); + + addEnvironmentStateExpiryCheckListener(); + + // Fast-forward time + await vi.advanceTimersByTimeAsync(1000 * 60); + + // Verify the config was updated with extended expiry + expect(mockConfig.update).toHaveBeenCalled(); + }); + + test("does not fetch new state if not expired", async () => { + const futureDate = new Date(Date.now() + 1000 * 60 * 60); // 1 hour in future + const mockConfig = { + get: vi.fn().mockReturnValue({ + environment: { + expiresAt: futureDate.toISOString(), + }, + appUrl: "https://test.com", + environmentId: "env_123", + }), + update: vi.fn(), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + const apiMock = vi.fn().mockImplementation(() => ({ + client: { + environment: { + getState: vi.fn(), + }, + }, + })); + + (FormbricksAPI as Mock).mockImplementation(apiMock); + + addEnvironmentStateExpiryCheckListener(); + + // Fast-forward time by less than expiry + await vi.advanceTimersByTimeAsync(1000 * 60); + + expect(mockConfig.update).not.toHaveBeenCalled(); + }); + + test("clears interval when clearEnvironmentStateExpiryCheckListener is called", () => { + const clearIntervalSpy = vi.spyOn(global, "clearInterval"); + + addEnvironmentStateExpiryCheckListener(); + clearEnvironmentStateExpiryCheckListener(); + + expect(clearIntervalSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/react-native/src/lib/index.ts b/packages/react-native/src/lib/index.ts deleted file mode 100644 index 096be58cea..0000000000 --- a/packages/react-native/src/lib/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type TJsConfigInput } from "@formbricks/types/js"; -import { ErrorHandler } from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; -import { trackCodeAction } from "./actions"; -import { CommandQueue } from "./command-queue"; -import { initialize } from "./initialize"; - -const logger = Logger.getInstance(); -logger.debug("Create command queue"); -const queue = new CommandQueue(); - -export const init = async (initConfig: TJsConfigInput): Promise => { - ErrorHandler.init(initConfig.errorHandler); - queue.add(initialize, false, initConfig); - await queue.wait(); -}; - -export const track = async (name: string, properties = {}): Promise => { - queue.add(trackCodeAction, true, name, properties); - await queue.wait(); -}; diff --git a/packages/react-native/src/lib/initialize.ts b/packages/react-native/src/lib/initialize.ts deleted file mode 100644 index c7d8142b2c..0000000000 --- a/packages/react-native/src/lib/initialize.ts +++ /dev/null @@ -1,274 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { type TAttributes } from "@formbricks/types/attributes"; -import { wrapThrowsAsync } from "@formbricks/types/error-handlers"; -import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js"; -import { RN_ASYNC_STORAGE_KEY } from "../../../js-core/src/lib/constants"; -import { - ErrorHandler, - type MissingFieldError, - type MissingPersonError, - type NetworkError, - type NotInitializedError, - type Result, - err, - okVoid, -} from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; -import { filterSurveys } from "../../../js-core/src/lib/utils"; -import { trackAction } from "./actions"; -import { updateAttributes } from "./attributes"; -import { RNConfig } from "./config"; -import { fetchEnvironmentState } from "./environment-state"; -import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./event-listeners"; -import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./person-state"; - -let isInitialized = false; -const appConfig = RNConfig.getInstance(); -const logger = Logger.getInstance(); - -export const setIsInitialize = (state: boolean): void => { - isInitialized = state; -}; - -export const initialize = async ( - configInput: TJsConfigInput -): Promise> => { - if (isInitialized) { - logger.debug("Already initialized, skipping initialization."); - return okVoid(); - } - - let existingConfig: TJsConfig | undefined; - try { - existingConfig = appConfig.get(); - logger.debug("Found existing configuration."); - } catch { - logger.debug("No existing configuration found."); - } - - // formbricks is in error state, skip initialization - if (existingConfig?.status.value === "error") { - logger.debug("Formbricks was set to an error state."); - - const expiresAt = existingConfig.status.expiresAt; - - if (expiresAt && new Date(expiresAt) > new Date()) { - logger.debug("Error state is not expired, skipping initialization"); - return okVoid(); - } - logger.debug("Error state is expired. Continue with initialization."); - } - - ErrorHandler.getInstance().printStatus(); - - logger.debug("Start initialize"); - - if (!configInput.environmentId) { - logger.debug("No environmentId provided"); - return err({ - code: "missing_field", - field: "environmentId", - }); - } - - if (!configInput.apiHost) { - logger.debug("No apiHost provided"); - - return err({ - code: "missing_field", - field: "apiHost", - }); - } - - if ( - existingConfig?.environmentState && - existingConfig.environmentId === configInput.environmentId && - existingConfig.apiHost === configInput.apiHost - ) { - logger.debug("Configuration fits init parameters."); - let isEnvironmentStateExpired = false; - let isPersonStateExpired = false; - - if (new Date(existingConfig.environmentState.expiresAt) < new Date()) { - logger.debug("Environment state expired. Syncing."); - isEnvironmentStateExpired = true; - } - - if ( - configInput.userId && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- personState could be null - (existingConfig.personState === null || - (existingConfig.personState.expiresAt && new Date(existingConfig.personState.expiresAt) < new Date())) - ) { - logger.debug("Person state needs syncing - either null or expired"); - isPersonStateExpired = true; - } - - try { - // fetch the environment state (if expired) - const environmentState = isEnvironmentStateExpired - ? await fetchEnvironmentState({ - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - }) - : existingConfig.environmentState; - - // fetch the person state (if expired) - - let { personState } = existingConfig; - - if (isPersonStateExpired) { - if (configInput.userId) { - personState = await fetchPersonState({ - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - userId: configInput.userId, - }); - } else { - personState = DEFAULT_PERSON_STATE_NO_USER_ID; - } - } - - // filter the environment state wrt the person state - const filteredSurveys = filterSurveys(environmentState, personState); - - // update the appConfig with the new filtered surveys - appConfig.update({ - ...existingConfig, - environmentState, - personState, - filteredSurveys, - attributes: configInput.attributes ?? {}, - }); - - const surveyNames = filteredSurveys.map((s) => s.name); - logger.debug(`Fetched ${surveyNames.length.toString()} surveys during sync: ${surveyNames.join(", ")}`); - } catch { - logger.debug("Error during sync. Please try again."); - } - } else { - logger.debug("No valid configuration found. Resetting config and creating new one."); - void appConfig.resetConfig(); - logger.debug("Syncing."); - - try { - const environmentState = await fetchEnvironmentState( - { - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - }, - false - ); - - const personState = configInput.userId - ? await fetchPersonState( - { - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - userId: configInput.userId, - }, - false - ) - : DEFAULT_PERSON_STATE_NO_USER_ID; - - const filteredSurveys = filterSurveys(environmentState, personState); - - let updatedAttributes: TAttributes | null = null; - if (configInput.attributes) { - if (configInput.userId) { - const res = await updateAttributes( - configInput.apiHost, - configInput.environmentId, - configInput.userId, - configInput.attributes - ); - - if (!res.ok) { - if (res.error.code === "forbidden") { - logger.error(`Authorization error: ${res.error.responseMessage ?? ""}`); - } - return err(res.error) as unknown as Result< - void, - MissingFieldError | NetworkError | MissingPersonError - >; - } - - updatedAttributes = res.value; - } else { - updatedAttributes = { ...configInput.attributes }; - } - } - - appConfig.update({ - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - personState, - environmentState, - filteredSurveys, - attributes: updatedAttributes ?? {}, - }); - } catch (e) { - await handleErrorOnFirstInit(e as { code: string; responseMessage: string }); - } - - // and track the new session event - trackAction("New Session"); - } - - logger.debug("Adding event listeners"); - addEventListeners(); - addCleanupEventListeners(); - - setIsInitialize(true); - logger.debug("Initialized"); - - // check page url if initialized after page load - return okVoid(); -}; - -export const checkInitialized = (): Result => { - logger.debug("Check if initialized"); - - if (!isInitialized || !ErrorHandler.initialized) { - return err({ - code: "not_initialized", - message: "Formbricks not initialized. Call initialize() first.", - }); - } - - return okVoid(); -}; - -export const deinitalize = async (): Promise => { - logger.debug("Deinitializing"); - await appConfig.resetConfig(); - setIsInitialize(false); - removeAllEventListeners(); -}; - -export const handleErrorOnFirstInit = async (e: { - code: string; - responseMessage: string; -}): Promise => { - if (e.code === "forbidden") { - logger.error(`Authorization error: ${e.responseMessage}`); - } else { - logger.error( - `Error during first initialization: ${e.code} - ${e.responseMessage}. Please try again later.` - ); - } - - // put formbricks in error state (by creating a new config) and throw error - const initialErrorConfig: Partial = { - status: { - value: "error", - expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future - }, - }; - - await wrapThrowsAsync(async () => { - await AsyncStorage.setItem(RN_ASYNC_STORAGE_KEY, JSON.stringify(initialErrorConfig)); - })(); - - throw new Error("Could not initialize formbricks"); -}; diff --git a/packages/react-native/src/lib/person-state.ts b/packages/react-native/src/lib/person-state.ts deleted file mode 100644 index a890dc98e6..0000000000 --- a/packages/react-native/src/lib/person-state.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { type TJsPersonState, type TJsPersonSyncParams } from "@formbricks/types/js"; -import { err } from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; -import { RNConfig } from "./config"; - -const config = RNConfig.getInstance(); -const logger = Logger.getInstance(); -let personStateSyncIntervalId: number | null = null; - -export const DEFAULT_PERSON_STATE_NO_USER_ID: TJsPersonState = { - expiresAt: null, - data: { - userId: null, - segments: [], - displays: [], - responses: [], - lastDisplayAt: null, - }, -} as const; - -/** - * Fetch the person state from the backend - * @param apiHost - The API host - * @param environmentId - The environment ID - * @param userId - The user ID - * @param noCache - Whether to skip the cache - * @returns The person state - * @throws NetworkError - */ -export const fetchPersonState = async ( - { apiHost, environmentId, userId }: TJsPersonSyncParams, - noCache = false -): Promise => { - const url = `${apiHost}/api/v1/client/${environmentId}/identify/contacts/${userId}`; - - try { - const fetchOptions: RequestInit = {}; - - if (noCache) { - fetchOptions.cache = "no-cache"; - logger.debug("No cache option set for sync"); - } - - const response = await fetch(url, fetchOptions); - - if (!response.ok) { - const jsonRes = (await response.json()) as { code: string; message: string }; - - const error = err({ - code: jsonRes.code === "forbidden" ? "forbidden" : "network_error", - status: response.status, - message: "Error syncing with backend", - url: new URL(url), - responseMessage: jsonRes.message, - }); - - // eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error is an Error object - throw error.error; - } - - const data = (await response.json()) as { data: TJsPersonState["data"] }; - const { data: state } = data; - - const defaultPersonState: TJsPersonState = { - expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes - data: { - userId, - segments: [], - displays: [], - responses: [], - lastDisplayAt: null, - }, - }; - - if (!Object.keys(state).length) { - return defaultPersonState; - } - - return { - data: { ...state }, - expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes - }; - } catch (e: unknown) { - const errorTyped = e as { message?: string }; - - const error = err({ - code: "network_error", - message: errorTyped.message ?? "Error fetching the person state", - status: 500, - url: new URL(url), - responseMessage: errorTyped.message ?? "Unknown error", - }); - - // eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error is an Error object - throw error.error; - } -}; - -/** - * Add a listener to check if the person state has expired with a certain interval - */ -export const addPersonStateExpiryCheckListener = (): void => { - const updateInterval = 1000 * 60; // every 60 seconds - - if (personStateSyncIntervalId === null) { - const intervalHandler = (): void => { - const userId = config.get().personState.data.userId; - - if (!userId) { - return; - } - - // extend the personState validity by 30 minutes: - config.update({ - ...config.get(), - personState: { - ...config.get().personState, - expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes - }, - }); - }; - - personStateSyncIntervalId = setInterval(intervalHandler, updateInterval) as unknown as number; - } -}; - -/** - * Clear the person state expiry check listener - */ -export const clearPersonStateExpiryCheckListener = (): void => { - if (personStateSyncIntervalId) { - clearInterval(personStateSyncIntervalId); - personStateSyncIntervalId = null; - } -}; diff --git a/packages/react-native/src/lib/person.ts b/packages/react-native/src/lib/person.ts deleted file mode 100644 index 1a81da368e..0000000000 --- a/packages/react-native/src/lib/person.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { type NetworkError, type Result, err, okVoid } from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; -import { RNConfig } from "./config"; -import { deinitalize, initialize } from "./initialize"; - -const appConfig = RNConfig.getInstance(); -const logger = Logger.getInstance(); - -export const logoutPerson = async (): Promise => { - await deinitalize(); - await appConfig.resetConfig(); -}; - -export const resetPerson = async (): Promise> => { - logger.debug("Resetting state & getting new state from backend"); - const userId = appConfig.get().personState.data.userId; - const syncParams = { - environmentId: appConfig.get().environmentId, - apiHost: appConfig.get().apiHost, - ...(userId && { userId }), - attributes: appConfig.get().attributes, - }; - await logoutPerson(); - try { - await initialize(syncParams); - return okVoid(); - } catch (e) { - return err(e as NetworkError); - } -}; diff --git a/packages/react-native/src/lib/actions.ts b/packages/react-native/src/lib/survey/action.ts similarity index 52% rename from packages/react-native/src/lib/actions.ts rename to packages/react-native/src/lib/survey/action.ts index 14ca81292f..e175e88e23 100644 --- a/packages/react-native/src/lib/actions.ts +++ b/packages/react-native/src/lib/survey/action.ts @@ -1,21 +1,18 @@ -import type { TJsEnvironmentStateSurvey } from "@formbricks/types/js"; -import { - type InvalidCodeError, - type NetworkError, - type Result, - err, - okVoid, -} from "../../../js-core/src/lib/errors"; -import { Logger } from "../../../js-core/src/lib/logger"; -import { shouldDisplayBasedOnPercentage } from "../../../js-core/src/lib/utils"; -import { RNConfig } from "./config"; -import { SurveyStore } from "./survey-store"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; +import { SurveyStore } from "@/lib/survey/store"; +import type { TEnvironmentStateSurvey } from "@/types/config"; +import { type InvalidCodeError, type NetworkError, type Result, err, okVoid } from "@/types/error"; -const appConfig = RNConfig.getInstance(); -const logger = Logger.getInstance(); -const surveyStore = SurveyStore.getInstance(); +/** + * Triggers the display of a survey if it meets the display percentage criteria + * @param survey - The survey configuration to potentially display + */ +export const triggerSurvey = (survey: TEnvironmentStateSurvey): void => { + const surveyStore = SurveyStore.getInstance(); + const logger = Logger.getInstance(); -export const triggerSurvey = (survey: TJsEnvironmentStateSurvey): void => { // Check if the survey should be displayed based on displayPercentage if (survey.displayPercentage) { const shouldDisplaySurvey = shouldDisplayBasedOnPercentage(survey.displayPercentage); @@ -28,7 +25,16 @@ export const triggerSurvey = (survey: TJsEnvironmentStateSurvey): void => { surveyStore.setSurvey(survey); }; +/** + * Tracks an action name and triggers associated surveys + * @param name - The name of the action to track + * @param alias - Optional alias for the action name + * @returns Result indicating success or network error + */ export const trackAction = (name: string, alias?: string): Result => { + const logger = Logger.getInstance(); + const appConfig = RNConfig.getInstance(); + const aliasName = alias ?? name; logger.debug(`Formbricks: Action "${aliasName}" tracked`); @@ -51,11 +57,16 @@ export const trackAction = (name: string, alias?: string): Result | Result => { +/** + * Tracks an action by its code and triggers associated surveys (used for code actions only) + * @param code - The action code to track + * @returns Result indicating success, network error, or invalid code error + */ +export const track = (code: string): Result | Result => { + const appConfig = RNConfig.getInstance(); + const { - environmentState: { + environment: { data: { actionClasses = [] }, }, } = appConfig.get(); diff --git a/packages/react-native/src/lib/survey/state.ts b/packages/react-native/src/lib/survey/state.ts new file mode 100644 index 0000000000..c7af6974a8 --- /dev/null +++ b/packages/react-native/src/lib/survey/state.ts @@ -0,0 +1,98 @@ +import { type TResponseUpdate } from "@/types/response"; + +export class SurveyState { + responseId: string | null = null; + displayId: string | null = null; + userId: string | null = null; + surveyId: string; + responseAcc: TResponseUpdate = { finished: false, data: {}, ttc: {}, variables: {} }; + singleUseId: string | null; + + constructor( + surveyId: string, + singleUseId?: string | null, + responseId?: string | null, + userId?: string | null + ) { + this.surveyId = surveyId; + this.userId = userId ?? null; + this.singleUseId = singleUseId ?? null; + this.responseId = responseId ?? null; + } + + /** + * Set the current survey ID + * @param id - The survey ID + */ + setSurveyId(id: string): void { + this.surveyId = id; + this.clear(); // Reset the state when setting a new surveyId + } + /** + * Get a copy of the current state + */ + copy(): SurveyState { + const copyInstance = new SurveyState( + this.surveyId, + this.singleUseId ?? undefined, + this.responseId ?? undefined, + this.userId ?? undefined + ); + copyInstance.responseId = this.responseId; + copyInstance.responseAcc = this.responseAcc; + return copyInstance; + } + + /** + * Update the response ID after a successful response creation + * @param id - The response ID + */ + updateResponseId(id: string): void { + this.responseId = id; + } + + /** + * Update the display ID after a successful display creation + * @param id - The display ID + */ + updateDisplayId(id: string): void { + this.displayId = id; + } + + /** + * Update the user ID + * @param id - The user ID + */ + updateUserId(id: string): void { + this.userId = id; + } + + /** + * Accumulate the responses + * @param responseUpdate - The new response data to add + */ + accumulateResponse(responseUpdate: TResponseUpdate): void { + this.responseAcc = { + finished: responseUpdate.finished, + ttc: responseUpdate.ttc, + data: { ...this.responseAcc.data, ...responseUpdate.data }, + variables: responseUpdate.variables, + displayId: responseUpdate.displayId, + }; + } + + /** + * Check if the current accumulated response is finished + */ + isResponseFinished(): boolean { + return this.responseAcc.finished; + } + + /** + * Clear the current state + */ + clear(): void { + this.responseId = null; + this.responseAcc = { finished: false, data: {}, ttc: {}, variables: {} }; + } +} diff --git a/packages/react-native/src/lib/survey-store.ts b/packages/react-native/src/lib/survey/store.ts similarity index 68% rename from packages/react-native/src/lib/survey-store.ts rename to packages/react-native/src/lib/survey/store.ts index c67e58be96..92de2fae45 100644 --- a/packages/react-native/src/lib/survey-store.ts +++ b/packages/react-native/src/lib/survey/store.ts @@ -1,13 +1,10 @@ -import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js"; +import type { TEnvironmentStateSurvey } from "@/types/config"; -type Listener = ( - state: TJsEnvironmentStateSurvey | null, - prevSurvey: TJsEnvironmentStateSurvey | null -) => void; +type Listener = (state: TEnvironmentStateSurvey | null, prevSurvey: TEnvironmentStateSurvey | null) => void; export class SurveyStore { private static instance: SurveyStore | undefined; - private survey: TJsEnvironmentStateSurvey | null = null; + private survey: TEnvironmentStateSurvey | null = null; private listeners = new Set(); static getInstance(): SurveyStore { @@ -17,13 +14,13 @@ export class SurveyStore { return SurveyStore.instance; } - public getSurvey(): TJsEnvironmentStateSurvey | null { + public getSurvey(): TEnvironmentStateSurvey | null { return this.survey; } - public setSurvey(survey: TJsEnvironmentStateSurvey): void { + public setSurvey(survey: TEnvironmentStateSurvey): void { const prevSurvey = this.survey; - if (prevSurvey !== survey) { + if (prevSurvey?.id !== survey.id) { this.survey = survey; this.listeners.forEach((listener) => { listener(this.survey, prevSurvey); diff --git a/packages/react-native/src/lib/survey/tests/__mocks__/state.mock.ts b/packages/react-native/src/lib/survey/tests/__mocks__/state.mock.ts new file mode 100644 index 0000000000..8ce2a56637 --- /dev/null +++ b/packages/react-native/src/lib/survey/tests/__mocks__/state.mock.ts @@ -0,0 +1,8 @@ +export const mockSurveyId = "nrlq3epdlgc9zuccy8ngvgca"; +export const mockSingleUseId = "oh40c6hjlbm2lt5gn1bzkhnn"; +export const mockResponseId = "qru1wamotsu8reijra9haej8"; +export const mockUserId = "li7yxnzfgvlnvkp4q7434dpo"; +export const mockDisplayId = "raxvjd8rp35vi0pjt3tg45am"; +export const mockQuestionId = "a3815gq6cld6nrjcmp7keut4"; +export const mockQuestionId2 = "n0u9zu7m8nc3net6x5dpwdl8"; +export const mockSurveyId2 = "rdt38nqnff9xbyrfbtpesmkm"; diff --git a/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts b/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts new file mode 100644 index 0000000000..8b0aefcdb0 --- /dev/null +++ b/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts @@ -0,0 +1,2 @@ +export const mockSurveyId = "jgocyoxk9uifo6u381qahmes"; +export const mockSurveyName = "Test Survey"; diff --git a/packages/react-native/src/lib/survey/tests/action.test.ts b/packages/react-native/src/lib/survey/tests/action.test.ts new file mode 100644 index 0000000000..76e93a36d8 --- /dev/null +++ b/packages/react-native/src/lib/survey/tests/action.test.ts @@ -0,0 +1,176 @@ +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; +import { track, trackAction, triggerSurvey } from "@/lib/survey/action"; +import { SurveyStore } from "@/lib/survey/store"; +import { type TEnvironmentStateSurvey } from "@/types/config"; + +vi.mock("@/lib/common/config", () => ({ + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/survey/store", () => ({ + SurveyStore: { + getInstance: vi.fn(() => ({ + setSurvey: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/common/logger", () => ({ + Logger: { + getInstance: vi.fn(() => { + return { + debug: vi.fn(), + }; + }), + }, +})); + +vi.mock("@/lib/common/utils", () => ({ + shouldDisplayBasedOnPercentage: vi.fn(), +})); + +describe("survey/action.ts", () => { + const mockSurvey = { + id: "survey_001", + name: "Test Survey", + displayPercentage: 50, + triggers: [ + { + actionClass: { name: "testAction" }, + }, + ], + }; + + const mockAppConfig = { + get: vi.fn(), + }; + + const mockSurveyStore = { + setSurvey: vi.fn(), + }; + + const mockLogger = { + debug: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + + const getInstanceRn = vi.spyOn(RNConfig, "getInstance"); + const getInstanceSurveyStore = vi.spyOn(SurveyStore, "getInstance"); + const getInstanceLogger = vi.spyOn(Logger, "getInstance"); + + // Mock instances + getInstanceRn.mockReturnValue(mockAppConfig as unknown as RNConfig); + getInstanceSurveyStore.mockReturnValue(mockSurveyStore as unknown as SurveyStore); + getInstanceLogger.mockReturnValue(mockLogger as unknown as Logger); + }); + + describe("triggerSurvey", () => { + test("does not trigger survey if displayPercentage criteria is not met", () => { + const shouldDisplayBasedOnPercentageMock = vi.mocked(shouldDisplayBasedOnPercentage); + shouldDisplayBasedOnPercentageMock.mockReturnValueOnce(false); + + triggerSurvey(mockSurvey as unknown as TEnvironmentStateSurvey); + + // Ensure survey is not set + expect(mockSurveyStore.setSurvey).not.toHaveBeenCalled(); + expect(mockLogger.debug).toHaveBeenCalledWith( + 'Survey display of "Test Survey" skipped based on displayPercentage.' + ); + }); + + test("triggers survey if displayPercentage criteria is met", () => { + // Mock `shouldDisplayBasedOnPercentage` to return true + const shouldDisplayBasedOnPercentageMock = vi.mocked(shouldDisplayBasedOnPercentage); + shouldDisplayBasedOnPercentageMock.mockReturnValueOnce(true); + + triggerSurvey(mockSurvey as unknown as TEnvironmentStateSurvey); + + // Ensure survey is set + expect(mockSurveyStore.setSurvey).toHaveBeenCalledWith(mockSurvey); + }); + }); + + describe("trackAction", () => { + const mockActiveSurveys = [mockSurvey]; + + beforeEach(() => { + mockAppConfig.get.mockReturnValue({ + filteredSurveys: mockActiveSurveys, + }); + }); + + test("triggers survey associated with action name", () => { + (shouldDisplayBasedOnPercentage as unknown as Mock).mockReturnValue(true); + + trackAction("testAction"); + + // Ensure triggerSurvey is called for the matching survey + expect(mockSurveyStore.setSurvey).toHaveBeenCalledWith(mockSurvey); + }); + + test("does not trigger survey if no active surveys are found", () => { + mockAppConfig.get.mockReturnValue({ + filteredSurveys: [], + }); + + trackAction("testAction"); + + // Ensure no surveys are triggered + expect(mockSurveyStore.setSurvey).not.toHaveBeenCalled(); + expect(mockLogger.debug).toHaveBeenCalledWith("No active surveys to display"); + }); + + test("logs tracked action name", () => { + trackAction("testAction"); + + expect(mockLogger.debug).toHaveBeenCalledWith('Formbricks: Action "testAction" tracked'); + }); + }); + + describe("track", () => { + const mockActionClasses = [ + { + key: "testCode", + type: "code", + name: "testAction", + }, + ]; + + beforeEach(() => { + mockAppConfig.get.mockReturnValue({ + environment: { + data: { actionClasses: mockActionClasses }, + }, + }); + }); + + test("tracks a valid action by code", () => { + const result = track("testCode"); + + expect(result.ok).toBe(true); + // expect(mockLogger.debug).toHaveBeenCalledWith('Formbricks: Action "testAction" tracked'); + }); + + test("returns error for invalid action code", () => { + const result = track("invalidCode"); + + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error.code).toBe("invalid_code"); + expect(result.error.message).toBe( + "invalidCode action unknown. Please add this action in Formbricks first in order to use it in your code." + ); + } + }); + }); +}); diff --git a/packages/react-native/src/lib/survey/tests/state.test.ts b/packages/react-native/src/lib/survey/tests/state.test.ts new file mode 100644 index 0000000000..5619ef92fa --- /dev/null +++ b/packages/react-native/src/lib/survey/tests/state.test.ts @@ -0,0 +1,158 @@ +import { + mockDisplayId, + mockQuestionId, + mockQuestionId2, + mockResponseId, + mockSingleUseId, + mockSurveyId, + mockSurveyId2, + mockUserId, +} from "./__mocks__/state.mock"; +import { SurveyState } from "@/lib/survey/state"; +import { beforeEach, describe, expect, test } from "vitest"; + +describe("SurveyState", () => { + let surveyState: SurveyState; + + beforeEach(() => { + surveyState = new SurveyState(mockSurveyId); + }); + + describe("constructor", () => { + test("initializes with required surveyId", () => { + expect(surveyState.surveyId).toBe(mockSurveyId); + expect(surveyState.responseId).toBeNull(); + expect(surveyState.userId).toBeNull(); + expect(surveyState.singleUseId).toBeNull(); + }); + + test("initializes with all optional parameters", () => { + const state = new SurveyState(mockSurveyId, mockSingleUseId, mockResponseId, mockUserId); + expect(state.surveyId).toBe(mockSurveyId); + expect(state.singleUseId).toBe(mockSingleUseId); + expect(state.responseId).toBe(mockResponseId); + expect(state.userId).toBe(mockUserId); + }); + }); + + describe("setSurveyId", () => { + test("updates surveyId and clears state", () => { + // First set some data + surveyState.responseId = mockResponseId; + surveyState.responseAcc = { + finished: true, + data: { [mockQuestionId]: "test" }, + ttc: { [mockQuestionId]: 5000 }, + variables: {}, + }; + + // Then update survey ID + surveyState.setSurveyId(mockSurveyId2); + + expect(surveyState.surveyId).toBe(mockSurveyId2); + expect(surveyState.responseId).toBeNull(); + expect(surveyState.responseAcc).toEqual({ finished: false, data: {}, ttc: {}, variables: {} }); + }); + }); + + describe("copy", () => { + test("creates an exact copy of the state", () => { + surveyState.responseId = mockResponseId; + surveyState.userId = mockUserId; + surveyState.singleUseId = mockSingleUseId; + surveyState.responseAcc = { + finished: true, + data: { [mockQuestionId]: "answer1" }, + ttc: { [mockQuestionId]: 3000 }, + variables: { var1: "value1" }, + }; + + const copy = surveyState.copy(); + + expect(copy).toBeInstanceOf(SurveyState); + expect(copy).not.toBe(surveyState); // Different instance + expect(copy.surveyId).toBe(surveyState.surveyId); + expect(copy.responseId).toBe(surveyState.responseId); + expect(copy.userId).toBe(surveyState.userId); + expect(copy.singleUseId).toBe(surveyState.singleUseId); + expect(copy.responseAcc).toEqual(surveyState.responseAcc); + }); + }); + + describe("accumulateResponse", () => { + test("accumulates response data correctly", () => { + const firstUpdate = { + finished: false, + data: { [mockQuestionId]: "answer1" }, + ttc: { [mockQuestionId]: 3000 }, + variables: { var1: "value1" }, + displayId: mockDisplayId, + }; + + const secondUpdate = { + finished: true, + data: { [mockQuestionId2]: "answer2" }, + ttc: { [mockQuestionId2]: 2000 }, + variables: { var2: "value2" }, + displayId: mockDisplayId, + }; + + surveyState.accumulateResponse(firstUpdate); + expect(surveyState.responseAcc.data).toEqual({ [mockQuestionId]: "answer1" }); + expect(surveyState.responseAcc.ttc).toEqual({ [mockQuestionId]: 3000 }); + expect(surveyState.responseAcc.variables).toEqual({ var1: "value1" }); + + surveyState.accumulateResponse(secondUpdate); + expect(surveyState.responseAcc.data).toEqual({ + [mockQuestionId]: "answer1", + [mockQuestionId2]: "answer2", + }); + expect(surveyState.responseAcc.ttc).toEqual({ [mockQuestionId2]: 2000 }); + expect(surveyState.responseAcc.variables).toEqual({ var2: "value2" }); + expect(surveyState.responseAcc.finished).toBe(true); + }); + }); + + describe("state management methods", () => { + test("updateResponseId sets response ID", () => { + surveyState.updateResponseId(mockResponseId); + expect(surveyState.responseId).toBe(mockResponseId); + }); + + test("updateDisplayId sets display ID", () => { + surveyState.updateDisplayId(mockDisplayId); + expect(surveyState.displayId).toBe(mockDisplayId); + }); + + test("updateUserId sets user ID", () => { + surveyState.updateUserId(mockUserId); + expect(surveyState.userId).toBe(mockUserId); + }); + + test("isResponseFinished returns correct state", () => { + expect(surveyState.isResponseFinished()).toBe(false); + surveyState.responseAcc.finished = true; + expect(surveyState.isResponseFinished()).toBe(true); + }); + + test("clear resets response state", () => { + surveyState.responseId = mockResponseId; + surveyState.responseAcc = { + finished: true, + data: { [mockQuestionId]: "test" }, + ttc: { [mockQuestionId]: 5000 }, + variables: { var1: "test" }, + }; + + surveyState.clear(); + + expect(surveyState.responseId).toBeNull(); + expect(surveyState.responseAcc).toEqual({ + finished: false, + data: {}, + ttc: {}, + variables: {}, + }); + }); + }); +}); diff --git a/packages/react-native/src/lib/survey/tests/store.test.ts b/packages/react-native/src/lib/survey/tests/store.test.ts new file mode 100644 index 0000000000..b11f663817 --- /dev/null +++ b/packages/react-native/src/lib/survey/tests/store.test.ts @@ -0,0 +1,129 @@ +import { mockSurveyId, mockSurveyName } from "@/lib/survey/tests/__mocks__/store.mock"; +import { SurveyStore } from "@/lib/survey/store"; +import type { TEnvironmentStateSurvey } from "@/types/config"; +import { beforeEach, describe, expect, test, vi } from "vitest"; + +describe("SurveyStore", () => { + let store: SurveyStore; + + beforeEach(() => { + // Reset the singleton instance before each test + // @ts-expect-error accessing private static property + SurveyStore.instance = undefined; + store = SurveyStore.getInstance(); + }); + + describe("getInstance", () => { + test("returns singleton instance", () => { + const instance1 = SurveyStore.getInstance(); + const instance2 = SurveyStore.getInstance(); + expect(instance1).toBe(instance2); + }); + }); + + describe("getSurvey", () => { + test("returns null when no survey is set", () => { + expect(store.getSurvey()).toBeNull(); + }); + + test("returns current survey when set", () => { + const mockSurvey: TEnvironmentStateSurvey = { + id: mockSurveyId, + name: mockSurveyName, + } as TEnvironmentStateSurvey; + + store.setSurvey(mockSurvey); + expect(store.getSurvey()).toBe(mockSurvey); + }); + }); + + describe("setSurvey", () => { + test("updates survey and notifies listeners when survey changes", () => { + const listener = vi.fn(); + const mockSurvey: TEnvironmentStateSurvey = { + id: mockSurveyId, + name: mockSurveyName, + } as TEnvironmentStateSurvey; + + store.subscribe(listener); + store.setSurvey(mockSurvey); + + expect(listener).toHaveBeenCalledWith(mockSurvey, null); + expect(store.getSurvey()).toBe(mockSurvey); + }); + + test("does not notify listeners when setting same survey", () => { + const listener = vi.fn(); + const mockSurvey: TEnvironmentStateSurvey = { + id: mockSurveyId, + name: mockSurveyName, + } as TEnvironmentStateSurvey; + + store.setSurvey(mockSurvey); + store.subscribe(listener); + store.setSurvey(mockSurvey); + + expect(listener).not.toHaveBeenCalled(); + }); + }); + + describe("resetSurvey", () => { + test("resets survey to null and notifies listeners", () => { + const listener = vi.fn(); + const mockSurvey: TEnvironmentStateSurvey = { + id: mockSurveyId, + name: mockSurveyName, + } as TEnvironmentStateSurvey; + + store.setSurvey(mockSurvey); + store.subscribe(listener); + store.resetSurvey(); + + expect(listener).toHaveBeenCalledWith(null, mockSurvey); + expect(store.getSurvey()).toBeNull(); + }); + + test("does not notify listeners when already null", () => { + const listener = vi.fn(); + store.subscribe(listener); + store.resetSurvey(); + + expect(listener).not.toHaveBeenCalled(); + expect(store.getSurvey()).toBeNull(); + }); + }); + + describe("subscribe", () => { + test("adds listener and returns unsubscribe function", () => { + const listener = vi.fn(); + const mockSurvey: TEnvironmentStateSurvey = { + id: mockSurveyId, + name: mockSurveyName, + } as TEnvironmentStateSurvey; + + const unsubscribe = store.subscribe(listener); + store.setSurvey(mockSurvey); + expect(listener).toHaveBeenCalledTimes(1); + + unsubscribe(); + store.setSurvey({ ...mockSurvey, name: "Updated Survey" } as TEnvironmentStateSurvey); + expect(listener).toHaveBeenCalledTimes(1); // Still 1, not called after unsubscribe + }); + + test("multiple listeners receive updates", () => { + const listener1 = vi.fn(); + const listener2 = vi.fn(); + const mockSurvey: TEnvironmentStateSurvey = { + id: mockSurveyId, + name: mockSurveyName, + } as TEnvironmentStateSurvey; + + store.subscribe(listener1); + store.subscribe(listener2); + store.setSurvey(mockSurvey); + + expect(listener1).toHaveBeenCalledWith(mockSurvey, null); + expect(listener2).toHaveBeenCalledWith(mockSurvey, null); + }); + }); +}); diff --git a/packages/react-native/src/lib/user/attribute.ts b/packages/react-native/src/lib/user/attribute.ts new file mode 100644 index 0000000000..cdb3c67e2f --- /dev/null +++ b/packages/react-native/src/lib/user/attribute.ts @@ -0,0 +1,12 @@ +import { UpdateQueue } from "@/lib/user/update-queue"; +import { type NetworkError, type Result, okVoid } from "@/types/error"; + +export const setAttributes = async ( + attributes: Record + // eslint-disable-next-line @typescript-eslint/require-await -- we want to use promises here +): Promise> => { + const updateQueue = UpdateQueue.getInstance(); + updateQueue.updateAttributes(attributes); + void updateQueue.processUpdates(); + return okVoid(); +}; diff --git a/packages/react-native/src/lib/user/state.ts b/packages/react-native/src/lib/user/state.ts new file mode 100644 index 0000000000..99497f0ad2 --- /dev/null +++ b/packages/react-native/src/lib/user/state.ts @@ -0,0 +1,54 @@ +import { RNConfig } from "@/lib/common/config"; +import type { TUserState } from "@/types/config"; + +let userStateSyncIntervalId: number | null = null; + +export const DEFAULT_USER_STATE_NO_USER_ID: TUserState = { + expiresAt: null, + data: { + userId: null, + segments: [], + displays: [], + responses: [], + lastDisplayAt: null, + }, +} as const; + +/** + * Add a listener to check if the user state has expired with a certain interval + */ +export const addUserStateExpiryCheckListener = (): void => { + const config = RNConfig.getInstance(); + const updateInterval = 1000 * 60; // every 60 seconds + + if (userStateSyncIntervalId === null) { + const intervalHandler = (): void => { + const userId = config.get().user.data.userId; + + if (!userId) { + return; + } + + // extend the personState validity by 30 minutes: + config.update({ + ...config.get(), + user: { + ...config.get().user, + expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes + }, + }); + }; + + userStateSyncIntervalId = setInterval(intervalHandler, updateInterval) as unknown as number; + } +}; + +/** + * Clear the person state expiry check listener + */ +export const clearUserStateExpiryCheckListener = (): void => { + if (userStateSyncIntervalId) { + clearInterval(userStateSyncIntervalId); + userStateSyncIntervalId = null; + } +}; diff --git a/packages/react-native/src/lib/user/tests/__mocks__/update-queue.mock.ts b/packages/react-native/src/lib/user/tests/__mocks__/update-queue.mock.ts new file mode 100644 index 0000000000..0402553306 --- /dev/null +++ b/packages/react-native/src/lib/user/tests/__mocks__/update-queue.mock.ts @@ -0,0 +1,6 @@ +export const mockUserId1 = "user_123"; +export const mockUserId2 = "user_456"; +export const mockAttributes = { + name: "John Doe", + email: "john@example.com", +}; diff --git a/packages/react-native/src/lib/user/tests/__mocks__/update.mock.ts b/packages/react-native/src/lib/user/tests/__mocks__/update.mock.ts new file mode 100644 index 0000000000..32c2d6452d --- /dev/null +++ b/packages/react-native/src/lib/user/tests/__mocks__/update.mock.ts @@ -0,0 +1,7 @@ +export const mockUserId = "user_123"; +export const mockEnvironmentId = "ew9ba7urnv7u3eo11k5c1z0r"; +export const mockAppUrl = "https://app.formbricks.com"; +export const mockAttributes = { + name: "John Doe", + email: "john@example.com", +}; diff --git a/packages/react-native/src/lib/user/tests/attribute.test.ts b/packages/react-native/src/lib/user/tests/attribute.test.ts new file mode 100644 index 0000000000..b827c71803 --- /dev/null +++ b/packages/react-native/src/lib/user/tests/attribute.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { setAttributes } from "@/lib/user/attribute"; +import { UpdateQueue } from "@/lib/user/update-queue"; + +export const mockAttributes = { + name: "John Doe", + email: "john@example.com", +}; + +// Mock the UpdateQueue +vi.mock("@/lib/user/update-queue", () => ({ + UpdateQueue: { + getInstance: vi.fn(() => ({ + updateAttributes: vi.fn(), + processUpdates: vi.fn(), + })), + }, +})); + +describe("User Attributes", () => { + const mockUpdateQueue = { + updateAttributes: vi.fn(), + processUpdates: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + + const getInstanceUpdateQueue = vi.spyOn(UpdateQueue, "getInstance"); + getInstanceUpdateQueue.mockReturnValue(mockUpdateQueue as unknown as UpdateQueue); + }); + + describe("setAttributes", () => { + test("successfully updates attributes and triggers processing", async () => { + const result = await setAttributes(mockAttributes); + + // Verify UpdateQueue methods were called correctly + expect(mockUpdateQueue.updateAttributes).toHaveBeenCalledWith(mockAttributes); + expect(mockUpdateQueue.processUpdates).toHaveBeenCalled(); + + // Verify result is ok + expect(result.ok).toBe(true); + }); + + test("processes multiple attribute updates", async () => { + const firstAttributes = { name: mockAttributes.name }; + const secondAttributes = { email: mockAttributes.email }; + + await setAttributes(firstAttributes); + await setAttributes(secondAttributes); + + expect(mockUpdateQueue.updateAttributes).toHaveBeenCalledTimes(2); + expect(mockUpdateQueue.updateAttributes).toHaveBeenNthCalledWith(1, firstAttributes); + expect(mockUpdateQueue.updateAttributes).toHaveBeenNthCalledWith(2, secondAttributes); + expect(mockUpdateQueue.processUpdates).toHaveBeenCalledTimes(2); + }); + + test("processes updates asynchronously", async () => { + const attributes = { name: mockAttributes.name }; + + // Mock processUpdates to be async + mockUpdateQueue.processUpdates.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(resolve, 100); + }) + ); + + const result = await setAttributes(attributes); + + expect(result.ok).toBe(true); + expect(mockUpdateQueue.processUpdates).toHaveBeenCalled(); + // The function returns before processUpdates completes due to void operator + }); + }); +}); diff --git a/packages/react-native/src/lib/user/tests/state.test.ts b/packages/react-native/src/lib/user/tests/state.test.ts new file mode 100644 index 0000000000..decc0d3adf --- /dev/null +++ b/packages/react-native/src/lib/user/tests/state.test.ts @@ -0,0 +1,103 @@ +import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { RNConfig } from "@/lib/common/config"; +import { addUserStateExpiryCheckListener, clearUserStateExpiryCheckListener } from "@/lib/user/state"; + +const mockUserId = "user_123"; + +vi.mock("@/lib/common/config", () => ({ + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, +})); + +describe("User State Expiry Check Listener", () => { + let mockRNConfig: MockInstance<() => RNConfig>; + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); // Simulate timers + + mockRNConfig = vi.spyOn(RNConfig, "getInstance"); + }); + + afterEach(() => { + clearUserStateExpiryCheckListener(); // Ensure cleanup after each test + }); + + test("should set an interval if not already set and update user state expiry when userId exists", () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + user: { data: { userId: mockUserId } }, + }), + update: vi.fn(), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + addUserStateExpiryCheckListener(); + + // Fast-forward time by 1 minute (60,000 ms) + vi.advanceTimersByTime(60_000); + + // Ensure config.update was called with extended expiry time + expect(mockConfig.update).toHaveBeenCalledWith({ + user: { + data: { userId: mockUserId }, + expiresAt: expect.any(Date) as Date, + }, + }); + }); + + test("should not update user state expiry if userId does not exist", () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + user: { data: { userId: null } }, + }), + update: vi.fn(), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + addUserStateExpiryCheckListener(); + vi.advanceTimersByTime(60_000); // Fast-forward 1 minute + + expect(mockConfig.update).not.toHaveBeenCalled(); // Ensures no update when no userId + }); + + test("should not set multiple intervals if already set", () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + user: { data: { userId: mockUserId } }, + }), + update: vi.fn(), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + addUserStateExpiryCheckListener(); + addUserStateExpiryCheckListener(); // Call again to check if it prevents multiple intervals + + vi.advanceTimersByTime(60_000); // Fast-forward 1 minute + + expect(mockConfig.update).toHaveBeenCalledTimes(1); + }); + + test("should clear interval when clearUserStateExpiryCheckListener is called", () => { + const mockConfig = { + get: vi.fn(), + update: vi.fn(), + }; + + mockRNConfig.mockReturnValue(mockConfig as unknown as RNConfig); + + addUserStateExpiryCheckListener(); + clearUserStateExpiryCheckListener(); + + vi.advanceTimersByTime(60_000); // Fast-forward 1 minute + + expect(mockConfig.update).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/react-native/src/lib/user/tests/update-queue.test.ts b/packages/react-native/src/lib/user/tests/update-queue.test.ts new file mode 100644 index 0000000000..8dfe6742e2 --- /dev/null +++ b/packages/react-native/src/lib/user/tests/update-queue.test.ts @@ -0,0 +1,161 @@ +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; +import { mockAttributes, mockUserId1, mockUserId2 } from "@/lib/user/tests/__mocks__/update-queue.mock"; +import { RNConfig } from "@/lib/common/config"; +import { sendUpdates } from "@/lib/user/update"; +import { UpdateQueue } from "@/lib/user/update-queue"; + +// Mock dependencies +vi.mock("@/lib/common/config", () => ({ + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(() => ({ + user: { + data: { + userId: "mock-user-id", + }, + }, + })), + update: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/common/logger", () => ({ + Logger: { + getInstance: vi.fn(() => ({ + debug: vi.fn(), + error: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/user/update", () => ({ + sendUpdates: vi.fn(), +})); + +describe("UpdateQueue", () => { + let updateQueue: UpdateQueue; + + beforeEach(() => { + vi.clearAllMocks(); + // Reset singleton instance + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- accessing private static property + (UpdateQueue as any).instance = null; + updateQueue = UpdateQueue.getInstance(); + }); + + test("getInstance returns singleton instance", () => { + const instance1 = UpdateQueue.getInstance(); + const instance2 = UpdateQueue.getInstance(); + expect(instance1).toBe(instance2); + }); + + test("updateUserId sets userId correctly when updates is null", () => { + const userId = mockUserId1; + updateQueue.updateUserId(userId); + expect(updateQueue.getUpdates()).toEqual({ + userId, + attributes: {}, + }); + }); + + test("updateUserId updates existing userId correctly", () => { + const userId1 = mockUserId1; + const userId2 = mockUserId2; + + updateQueue.updateUserId(userId1); + updateQueue.updateUserId(userId2); + + expect(updateQueue.getUpdates()).toEqual({ + userId: userId2, + attributes: {}, + }); + }); + + test("updateAttributes sets attributes correctly when updates is null", () => { + const attributes = mockAttributes; + updateQueue.updateAttributes(attributes); + + expect(updateQueue.getUpdates()).toEqual({ + userId: "mock-user-id", // from mocked config + attributes, + }); + }); + + test("updateAttributes merges with existing attributes", () => { + updateQueue.updateAttributes({ name: mockAttributes.name }); + updateQueue.updateAttributes({ email: mockAttributes.email }); + + expect(updateQueue.getUpdates()).toEqual({ + userId: "mock-user-id", + attributes: { + name: mockAttributes.name, + email: mockAttributes.email, + }, + }); + }); + + test("clearUpdates resets updates to null", () => { + updateQueue.updateAttributes({ name: mockAttributes.name }); + updateQueue.clearUpdates(); + expect(updateQueue.getUpdates()).toBeNull(); + }); + + test("isEmpty returns true when updates is null", () => { + expect(updateQueue.isEmpty()).toBe(true); + }); + + test("isEmpty returns false when updates exist", () => { + updateQueue.updateAttributes({ name: mockAttributes.name }); + expect(updateQueue.isEmpty()).toBe(false); + }); + + test("processUpdates debounces multiple calls", async () => { + // Call processUpdates multiple times in quick succession + + (sendUpdates as Mock).mockReturnValue({ + ok: true, + }); + + updateQueue.updateAttributes({ name: mockAttributes.name }); + updateQueue.updateAttributes({ email: mockAttributes.email }); + + // Wait for debounce timeout + await new Promise((resolve) => { + setTimeout(resolve, 600); + }); + + await updateQueue.processUpdates(); + + // Should only be called once with the merged updates + expect(sendUpdates).toHaveBeenCalledTimes(1); + }); + + test("processUpdates handles language attribute specially when no userId", async () => { + const configUpdateMock = vi.fn(); + (RNConfig.getInstance as Mock).mockImplementation(() => ({ + get: vi.fn(() => ({ + user: { data: { userId: "" } }, + })), + update: configUpdateMock, + })); + + updateQueue.updateAttributes({ language: "en" }); + await updateQueue.processUpdates(); + + expect(configUpdateMock).toHaveBeenCalled(); + }); + + test("processUpdates throws error when setting attributes without userId", async () => { + (RNConfig.getInstance as Mock).mockImplementation(() => ({ + get: vi.fn(() => ({ + user: { data: { userId: "" } }, + })), + })); + + updateQueue.updateAttributes({ name: mockAttributes.name }); + await expect(updateQueue.processUpdates()).rejects.toThrow( + "Formbricks can't set attributes without a userId!" + ); + }); +}); diff --git a/packages/react-native/src/lib/user/tests/update.test.ts b/packages/react-native/src/lib/user/tests/update.test.ts new file mode 100644 index 0000000000..14c42f1f60 --- /dev/null +++ b/packages/react-native/src/lib/user/tests/update.test.ts @@ -0,0 +1,221 @@ +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; +import { FormbricksAPI } from "@formbricks/api"; +import { + mockAppUrl, + mockAttributes, + mockEnvironmentId, + mockUserId, +} from "@/lib/user/tests/__mocks__/update.mock"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { sendUpdates, sendUpdatesToBackend } from "@/lib/user/update"; +import { type TUpdates } from "@/types/config"; + +vi.mock("@/lib/common/config", () => ({ + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/common/logger", () => ({ + Logger: { + getInstance: vi.fn(() => ({ + debug: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/common/utils", () => ({ + filterSurveys: vi.fn(), +})); + +vi.mock("@formbricks/api", () => ({ + FormbricksAPI: vi.fn().mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn(), + }, + }, + })), +})); + +describe("sendUpdatesToBackend", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("sends user updates to backend and returns updated state", async () => { + const mockResponse = { + ok: true, + data: { + state: { + data: { + userId: mockUserId, + attributes: mockAttributes, + }, + }, + }, + }; + + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn().mockResolvedValue(mockResponse), + }, + }, + })); + + const result = await sendUpdatesToBackend({ + appUrl: mockAppUrl, + environmentId: mockEnvironmentId, + updates: { userId: mockUserId, attributes: mockAttributes }, + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.state.data).toEqual({ userId: mockUserId, attributes: mockAttributes }); + } + }); + + test("returns network error if API call fails", async () => { + const mockUpdates: TUpdates = { userId: mockUserId, attributes: mockAttributes }; + + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn().mockResolvedValue({ + ok: false, + error: { code: "network_error", message: "Request failed", status: 500 }, + }), + }, + }, + })); + + const result = await sendUpdatesToBackend({ + appUrl: mockAppUrl, + environmentId: mockEnvironmentId, + updates: mockUpdates, + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("network_error"); + expect(result.error.message).toBe("Error updating user with userId user_123"); + } + }); + + test("throws error if network request fails", async () => { + const mockUpdates: TUpdates = { userId: mockUserId, attributes: { plan: "premium" } }; + + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn().mockRejectedValue(new Error("Network error")), + }, + }, + })); + + await expect( + sendUpdatesToBackend({ + appUrl: mockAppUrl, + environmentId: mockEnvironmentId, + updates: mockUpdates, + }) + ).rejects.toThrow("Network error"); + }); +}); + +describe("sendUpdates", () => { + beforeEach(() => { + (RNConfig.getInstance as Mock).mockImplementation(() => ({ + get: vi.fn().mockReturnValue({ + appUrl: mockAppUrl, + environmentId: mockEnvironmentId, + environment: { + data: { + surveys: [], + }, + }, + }), + update: vi.fn(), + })); + + (Logger.getInstance as Mock).mockImplementation(() => ({ + debug: vi.fn(), + })); + }); + + test("successfully processes updates", async () => { + const mockResponse = { + ok: true, + data: { + state: { + data: { + userId: mockUserId, + attributes: mockAttributes, + }, + expiresAt: new Date(Date.now() + 1000 * 60 * 30), + }, + }, + }; + + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn().mockResolvedValue(mockResponse), + }, + }, + })); + + const result = await sendUpdates({ updates: { userId: mockUserId, attributes: mockAttributes } }); + + expect(result.ok).toBe(true); + }); + + test("handles backend errors", async () => { + const mockErrorResponse = { + ok: false, + error: { + code: "invalid_request", + status: 400, + message: "Invalid request", + }, + }; + + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn().mockResolvedValue(mockErrorResponse), + }, + }, + })); + + const result = await sendUpdates({ updates: { userId: mockUserId, attributes: mockAttributes } }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("invalid_request"); + } + }); + + test("handles unexpected errors", async () => { + (FormbricksAPI as Mock).mockImplementation(() => ({ + client: { + user: { + createOrUpdate: vi.fn().mockRejectedValue(new Error("Unexpected error")), + }, + }, + })); + + const result = await sendUpdates({ updates: { userId: mockUserId, attributes: mockAttributes } }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("network_error"); + expect(result.error.status).toBe(500); + } + }); +}); diff --git a/packages/react-native/src/lib/user/tests/user.test.ts b/packages/react-native/src/lib/user/tests/user.test.ts new file mode 100644 index 0000000000..27ccdf2f86 --- /dev/null +++ b/packages/react-native/src/lib/user/tests/user.test.ts @@ -0,0 +1,169 @@ +import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { setup, tearDown } from "@/lib/common/setup"; +import { UpdateQueue } from "@/lib/user/update-queue"; +import { logout, setUserId } from "@/lib/user/user"; + +// Mock dependencies +vi.mock("@/lib/common/config", () => ({ + RNConfig: { + getInstance: vi.fn(() => ({ + get: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/common/logger", () => ({ + Logger: { + getInstance: vi.fn(() => ({ + error: vi.fn(), + debug: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/user/update-queue", () => ({ + UpdateQueue: { + getInstance: vi.fn(() => ({ + updateUserId: vi.fn(), + processUpdates: vi.fn(), + })), + }, +})); + +vi.mock("@/lib/common/setup", () => ({ + tearDown: vi.fn(), + setup: vi.fn(), +})); + +describe("user.ts", () => { + const mockUserId = "test-user-123"; + const mockEnvironmentId = "env-123"; + const mockAppUrl = "https://test.com"; + + let getInstanceConfigMock: MockInstance<() => RNConfig>; + let getInstanceLoggerMock: MockInstance<() => Logger>; + let getInstanceUpdateQueueMock: MockInstance<() => UpdateQueue>; + + beforeEach(() => { + vi.clearAllMocks(); + getInstanceConfigMock = vi.spyOn(RNConfig, "getInstance"); + getInstanceLoggerMock = vi.spyOn(Logger, "getInstance"); + getInstanceUpdateQueueMock = vi.spyOn(UpdateQueue, "getInstance"); + }); + + describe("setUserId", () => { + test("returns error if userId is already set", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + user: { + data: { + userId: "existing-user", + }, + }, + }), + }; + + const mockLogger = { + debug: vi.fn(), + error: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + getInstanceLoggerMock.mockReturnValue(mockLogger as unknown as Logger); + + const result = await setUserId(mockUserId); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("forbidden"); + expect(result.error.status).toBe(403); + } + expect(mockLogger.error).toHaveBeenCalled(); + }); + + test("successfully sets userId when none exists", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + user: { + data: { + userId: null, + }, + }, + }), + }; + + const mockLogger = { + debug: vi.fn(), + error: vi.fn(), + }; + + const mockUpdateQueue = { + updateUserId: vi.fn(), + processUpdates: vi.fn(), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + getInstanceLoggerMock.mockReturnValue(mockLogger as unknown as Logger); + getInstanceUpdateQueueMock.mockReturnValue(mockUpdateQueue as unknown as UpdateQueue); + const result = await setUserId(mockUserId); + + expect(result.ok).toBe(true); + expect(mockUpdateQueue.updateUserId).toHaveBeenCalledWith(mockUserId); + expect(mockUpdateQueue.processUpdates).toHaveBeenCalled(); + }); + }); + + describe("logout", () => { + test("successfully sets up formbricks after logout", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environmentId: mockEnvironmentId, + appUrl: mockAppUrl, + user: { data: { userId: mockUserId } }, + }), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + + (setup as Mock).mockResolvedValue(undefined); + + const result = await logout(); + + expect(tearDown).toHaveBeenCalled(); + expect(setup).toHaveBeenCalledWith({ + environmentId: mockEnvironmentId, + appUrl: mockAppUrl, + }); + expect(result.ok).toBe(true); + }); + + test("returns error if setup fails", async () => { + const mockConfig = { + get: vi.fn().mockReturnValue({ + environmentId: mockEnvironmentId, + appUrl: mockAppUrl, + user: { data: { userId: mockUserId } }, + }), + }; + + getInstanceConfigMock.mockReturnValue(mockConfig as unknown as RNConfig); + + const mockError = { code: "network_error", message: "Failed to connect" }; + (setup as Mock).mockRejectedValue(mockError); + + const result = await logout(); + + expect(tearDown).toHaveBeenCalled(); + expect(setup).toHaveBeenCalledWith({ + environmentId: mockEnvironmentId, + appUrl: mockAppUrl, + }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toEqual(mockError); + } + }); + }); +}); diff --git a/packages/react-native/src/lib/user/update-queue.ts b/packages/react-native/src/lib/user/update-queue.ts new file mode 100644 index 0000000000..9edab77ef0 --- /dev/null +++ b/packages/react-native/src/lib/user/update-queue.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-empty-function -- required for singleton pattern */ +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { sendUpdates } from "@/lib/user/update"; +import type { TAttributes, TUpdates } from "@/types/config"; + +const logger = Logger.getInstance(); + +export class UpdateQueue { + private static instance: UpdateQueue | null = null; + private updates: TUpdates | null = null; + private debounceTimeout: NodeJS.Timeout | null = null; + private readonly DEBOUNCE_DELAY = 500; + + private constructor() {} + + public static getInstance(): UpdateQueue { + if (!UpdateQueue.instance) { + UpdateQueue.instance = new UpdateQueue(); + } + + return UpdateQueue.instance; + } + + public updateUserId(userId: string): void { + if (!this.updates) { + this.updates = { + userId, + attributes: {}, + }; + } else { + this.updates = { + ...this.updates, + userId, + }; + } + } + + public updateAttributes(attributes: TAttributes): void { + const config = RNConfig.getInstance(); + // Get userId from updates first, then fallback to config + const userId = this.updates?.userId ?? config.get().user.data.userId ?? ""; + + if (!this.updates) { + this.updates = { + userId, + attributes, + }; + } else { + this.updates = { + ...this.updates, + userId, + attributes: { ...this.updates.attributes, ...attributes }, + }; + } + } + + public getUpdates(): TUpdates | null { + return this.updates; + } + + public clearUpdates(): void { + this.updates = null; + } + + public isEmpty(): boolean { + return !this.updates; + } + + public async processUpdates(): Promise { + if (!this.updates) { + return; + } + + if (this.debounceTimeout) { + clearTimeout(this.debounceTimeout); + } + + return new Promise((resolve, reject) => { + const handler = async (): Promise => { + try { + let currentUpdates = { ...this.updates }; + const config = RNConfig.getInstance(); + + if (Object.keys(currentUpdates).length > 0) { + // Get userId from either updates or config + const effectiveUserId = currentUpdates.userId ?? config.get().user.data.userId; + const isLanguageInUpdates = currentUpdates.attributes?.language; + + if (!effectiveUserId && isLanguageInUpdates) { + // no user id set but the updates contain a language + // we need to set this language in the local config: + config.update({ + ...config.get(), + user: { + ...config.get().user, + data: { + ...config.get().user.data, + language: currentUpdates.attributes?.language, + }, + }, + }); + + logger.debug("Updated language successfully"); + + const { language: _, ...remainingAttributes } = currentUpdates.attributes ?? {}; + + // remove language from attributes + currentUpdates = { + ...currentUpdates, + attributes: remainingAttributes, + }; + } + + if (Object.keys(currentUpdates.attributes ?? {}).length > 0 && !effectiveUserId) { + const errorMessage = + "Formbricks can't set attributes without a userId! Please set a userId first with the setUserId function"; + logger.error(errorMessage); + this.clearUpdates(); + throw new Error(errorMessage); + } + + // Only send updates if we have a userId (either from updates or local storage) + if (effectiveUserId) { + const result = await sendUpdates({ + updates: { + userId: effectiveUserId, + attributes: currentUpdates.attributes ?? {}, + }, + }); + + if (result.ok) { + logger.debug("Updates sent successfully"); + } else { + logger.error("Failed to send updates"); + } + } + } + + this.clearUpdates(); + resolve(); + } catch (error: unknown) { + logger.error( + `Failed to process updates: ${error instanceof Error ? error.message : "Unknown error"}` + ); + reject(error as Error); + } + }; + + this.debounceTimeout = setTimeout(() => void handler(), this.DEBOUNCE_DELAY); + }); + } +} diff --git a/packages/react-native/src/lib/user/update.ts b/packages/react-native/src/lib/user/update.ts new file mode 100644 index 0000000000..2237a945e0 --- /dev/null +++ b/packages/react-native/src/lib/user/update.ts @@ -0,0 +1,114 @@ +/* eslint-disable no-console -- required for logging errors */ +import { FormbricksAPI } from "@formbricks/api"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { filterSurveys } from "@/lib/common/utils"; +import { type TUpdates, type TUserState } from "@/types/config"; +import { type ApiErrorResponse, type Result, err, ok, okVoid } from "@/types/error"; + +export const sendUpdatesToBackend = async ({ + appUrl, + environmentId, + updates, +}: { + appUrl: string; + environmentId: string; + updates: TUpdates; +}): Promise< + Result< + { + state: TUserState; + messages?: string[]; + }, + ApiErrorResponse + > +> => { + const url = `${appUrl}/api/v1/client/${environmentId}/user`; + const api = new FormbricksAPI({ apiHost: appUrl, environmentId }); + + try { + const response = await api.client.user.createOrUpdate({ + userId: updates.userId, + attributes: updates.attributes, + }); + + if (!response.ok) { + return err({ + code: response.error.code, + status: response.error.status, + message: `Error updating user with userId ${updates.userId}`, + url: new URL(url), + responseMessage: response.error.message, + }); + } + + return ok(response.data); + } catch (e: unknown) { + const errorTyped = e as { message?: string }; + + const error = err({ + code: "network_error", + message: errorTyped.message ?? "Error fetching the person state", + status: 500, + url: new URL(url), + responseMessage: errorTyped.message ?? "Unknown error", + }); + + // eslint-disable-next-line @typescript-eslint/only-throw-error -- error.error is an Error object + throw error.error; + } +}; + +export const sendUpdates = async ({ + updates, +}: { + updates: TUpdates; +}): Promise> => { + const config = RNConfig.getInstance(); + const logger = Logger.getInstance(); + + const { appUrl, environmentId } = config.get(); + // update endpoint call + const url = `${appUrl}/api/v1/client/${environmentId}/user`; + + try { + const updatesResponse = await sendUpdatesToBackend({ appUrl, environmentId, updates }); + + if (updatesResponse.ok) { + const userState = updatesResponse.data.state; + const filteredSurveys = filterSurveys(config.get().environment, userState); + + // messages => string[] - contains the details of the attributes update + // for example, if the attribute "email" was being used for some user or not + const messages = updatesResponse.data.messages; + + if (messages && messages.length > 0) { + for (const message of messages) { + logger.debug(`User update message: ${message}`); + } + } + + config.update({ + ...config.get(), + user: { + ...userState, + }, + filteredSurveys, + }); + + return okVoid(); + } + + return err(updatesResponse.error); + } catch (e) { + console.error("error in sending updates: ", e); + + return err({ + code: "network_error", + message: "Error sending updates", + status: 500, + url: new URL(url), + responseMessage: "Unknown error", + }); + } +}; diff --git a/packages/react-native/src/lib/user/user.ts b/packages/react-native/src/lib/user/user.ts new file mode 100644 index 0000000000..5c44ad7dae --- /dev/null +++ b/packages/react-native/src/lib/user/user.ts @@ -0,0 +1,60 @@ +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { setup, tearDown } from "@/lib/common/setup"; +import { UpdateQueue } from "@/lib/user/update-queue"; +import { type ApiErrorResponse, type NetworkError, type Result, err, okVoid } from "@/types/error"; + +// eslint-disable-next-line @typescript-eslint/require-await -- we want to use promises here +export const setUserId = async (userId: string): Promise> => { + const appConfig = RNConfig.getInstance(); + const logger = Logger.getInstance(); + const updateQueue = UpdateQueue.getInstance(); + + const { + data: { userId: currentUserId }, + } = appConfig.get().user; + + if (currentUserId) { + logger.error( + "A userId is already set in formbricks, please first call the logout function and then set a new userId" + ); + return err({ + code: "forbidden", + message: "User already set", + responseMessage: "User already set", + status: 403, + }); + } + + updateQueue.updateUserId(userId); + void updateQueue.processUpdates(); + return okVoid(); +}; + +export const logout = async (): Promise> => { + const logger = Logger.getInstance(); + const appConfig = RNConfig.getInstance(); + + const { userId } = appConfig.get().user.data; + + if (!userId) { + logger.debug("No userId is set, please use the setUserId function to set a userId first"); + return okVoid(); + } + + logger.debug("Resetting state & getting new state from backend"); + const initParams = { + environmentId: appConfig.get().environmentId, + appUrl: appConfig.get().appUrl, + }; + + // logout the user, remove user state and setup formbricks again + await tearDown(); + + try { + await setup(initParams); + return okVoid(); + } catch (e) { + return err(e as NetworkError); + } +}; diff --git a/packages/react-native/src/types/config.ts b/packages/react-native/src/types/config.ts new file mode 100644 index 0000000000..ddb6826cb2 --- /dev/null +++ b/packages/react-native/src/types/config.ts @@ -0,0 +1,154 @@ +/* eslint-disable import/no-extraneous-dependencies -- required for Prisma types */ +import type { ActionClass, Language, Project, Segment, Survey, SurveyLanguage } from "@prisma/client"; +import { z } from "zod"; +import { type TResponseUpdate, ZResponseUpdate } from "@/types/response"; +import { type TFileUploadParams, ZFileUploadParams } from "@/types/storage"; + +export type TEnvironmentStateSurvey = Pick< + Survey, + | "id" + | "name" + | "welcomeCard" + | "questions" + | "variables" + | "type" + | "showLanguageSwitch" + | "endings" + | "autoClose" + | "status" + | "recontactDays" + | "displayLimit" + | "displayOption" + | "hiddenFields" + | "delay" + | "projectOverwrites" +> & { + languages: (SurveyLanguage & { language: Language })[]; + triggers: { actionClass: ActionClass }[]; + segment?: Segment; + displayPercentage: number; + type: "link" | "app"; + styling?: TSurveyStyling; +}; + +export type TEnvironmentStateProject = Pick< + Project, + "id" | "recontactDays" | "clickOutsideClose" | "darkOverlay" | "placement" | "inAppSurveyBranding" +> & { + styling: TProjectStyling; +}; + +export type TEnvironmentStateActionClass = Pick; + +export interface TEnvironmentState { + expiresAt: Date; + data: { + surveys: TEnvironmentStateSurvey[]; + actionClasses: TEnvironmentStateActionClass[]; + project: TEnvironmentStateProject; + }; +} + +export interface TUserState { + expiresAt: Date | null; + data: { + userId: string | null; + segments: string[]; + displays: { surveyId: string; createdAt: Date }[]; + responses: string[]; + lastDisplayAt: Date | null; + language?: string; + }; +} + +export interface TConfig { + environmentId: string; + appUrl: string; + environment: TEnvironmentState; + user: TUserState; + filteredSurveys: TEnvironmentStateSurvey[]; + status: { + value: "success" | "error"; + expiresAt: Date | null; + }; +} + +export type TConfigUpdateInput = Omit & { + status?: { + value: "success" | "error"; + expiresAt: Date | null; + }; +}; + +export type TAttributes = Record; + +export interface TConfigInput { + environmentId: string; + appUrl: string; +} + +export interface TStylingColor { + light: string; + dark?: string | null | undefined; +} + +export interface TBaseStyling { + brandColor?: TStylingColor | null; + questionColor?: TStylingColor | null; + inputColor?: TStylingColor | null; + inputBorderColor?: TStylingColor | null; + cardBackgroundColor?: TStylingColor | null; + cardBorderColor?: TStylingColor | null; + cardShadowColor?: TStylingColor | null; + highlightBorderColor?: TStylingColor | null; + isDarkModeEnabled?: boolean | null; + roundness?: number | null; + cardArrangement?: { + linkSurveys: "casual" | "straight" | "simple"; + appSurveys: "casual" | "straight" | "simple"; + } | null; + background?: { + bg?: string | null; + bgType?: "animation" | "color" | "image" | "upload" | null; + brightness?: number | null; + } | null; + hideProgressBar?: boolean | null; + isLogoHidden?: boolean | null; +} + +export interface TProjectStyling extends TBaseStyling { + allowStyleOverwrite: boolean; +} + +export interface TSurveyStyling extends TBaseStyling { + overwriteThemeStyling?: boolean | null; +} + +export interface TWebViewOnMessageData { + onFinished?: boolean | null; + onDisplay?: boolean | null; + onResponse?: boolean | null; + responseUpdate?: TResponseUpdate | null; + onRetry?: boolean | null; + onClose?: boolean | null; + onFileUpload?: boolean | null; + fileUploadParams?: TFileUploadParams | null; + uploadId?: string | null; +} + +export const ZJsRNWebViewOnMessageData = z.object({ + onFinished: z.boolean().nullish(), + onDisplay: z.boolean().nullish(), + onResponse: z.boolean().nullish(), + responseUpdate: ZResponseUpdate.nullish(), + onRetry: z.boolean().nullish(), + onClose: z.boolean().nullish(), + onFileUpload: z.boolean().nullish(), + fileUploadParams: ZFileUploadParams.nullish(), + uploadId: z.string().nullish(), +}); + +export interface TUpdates { + userId: string; + attributes?: TAttributes; +} diff --git a/packages/react-native/src/types/error.ts b/packages/react-native/src/types/error.ts new file mode 100644 index 0000000000..ea78317c81 --- /dev/null +++ b/packages/react-native/src/types/error.ts @@ -0,0 +1,65 @@ +export interface ResultError { + ok: false; + error: T; +} + +export interface ResultOk { + ok: true; + value: T; +} + +export type Result = { ok: true; data: T } | { ok: false; error: E }; + +export const ok = (data: T): Result => ({ ok: true, data }); + +export const okVoid = (): Result => ({ ok: true, data: undefined }); + +export const err = (error: E): ResultError => ({ + ok: false, + error, +}); + +export interface ApiErrorResponse { + code: + | "not_found" + | "gone" + | "bad_request" + | "internal_server_error" + | "unauthorized" + | "method_not_allowed" + | "not_authenticated" + | "forbidden" + | "network_error"; + message: string; + status: number; + url?: URL; + details?: Record; + responseMessage?: string; +} + +export interface MissingFieldError { + code: "missing_field"; + field: string; +} + +export interface MissingPersonError { + code: "missing_person"; + message: string; +} + +export interface NetworkError { + code: "network_error"; + status: number; + message: string; + url: URL; + responseMessage: string; +} +export interface NotSetupError { + code: "not_setup"; + message: string; +} + +export interface InvalidCodeError { + code: "invalid_code"; + message: string; +} diff --git a/packages/react-native/src/types/response.ts b/packages/react-native/src/types/response.ts new file mode 100644 index 0000000000..c912262e12 --- /dev/null +++ b/packages/react-native/src/types/response.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +export type TResponseData = Record>; + +export type TResponseTtc = Record; + +export type TResponseVariables = Record; + +export type TResponseHiddenFieldValue = Record; + +export interface TResponseUpdate { + finished: boolean; + data: TResponseData; + language?: string; + variables?: TResponseVariables; + ttc?: TResponseTtc; + meta?: { url?: string; source?: string; action?: string }; + hiddenFields?: TResponseHiddenFieldValue; + displayId?: string | null; + endingId?: string | null; +} + +export const ZResponseData = z.record(z.union([z.string(), z.number(), z.array(z.string())])); +export const ZResponseVariables = z.record(z.union([z.string(), z.number()])); +export const ZResponseTtc = z.record(z.number()); +export const ZResponseHiddenFieldValue = z.record(z.union([z.string(), z.number(), z.array(z.string())])); + +export const ZResponseUpdate = z.object({ + finished: z.boolean(), + data: ZResponseData, + language: z.string().optional(), + variables: ZResponseVariables.optional(), + ttc: ZResponseTtc.optional(), + meta: z + .object({ + url: z.string().optional(), + source: z.string().optional(), + action: z.string().optional(), + }) + .optional(), + hiddenFields: ZResponseHiddenFieldValue.optional(), + displayId: z.string().nullish(), + endingId: z.string().nullish(), +}); diff --git a/packages/react-native/src/types/storage.ts b/packages/react-native/src/types/storage.ts new file mode 100644 index 0000000000..a6099a1577 --- /dev/null +++ b/packages/react-native/src/types/storage.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +export interface TUploadFileConfig { + allowedFileExtensions?: string[] | undefined; + surveyId?: string | undefined; +} + +export interface TUploadFileResponse { + data: { + signedUrl: string; + fileUrl: string; + signingData: { + signature: string; + timestamp: number; + uuid: string; + } | null; + updatedFileName: string; + presignedFields?: Record | undefined; + }; +} + +export interface TFileUploadParams { + file: { type: string; name: string; base64: string }; + params: TUploadFileConfig; +} + +export const ZUploadFileConfig = z.object({ + allowedFileExtensions: z.array(z.string()).optional(), + surveyId: z.string().optional(), +}); + +export const ZFileUploadParams = z.object({ + file: z.object({ type: z.string(), name: z.string(), base64: z.string() }), + params: ZUploadFileConfig, +}); diff --git a/packages/react-native/src/types/survey.ts b/packages/react-native/src/types/survey.ts new file mode 100644 index 0000000000..40e2be1728 --- /dev/null +++ b/packages/react-native/src/types/survey.ts @@ -0,0 +1,35 @@ +import type { TEnvironmentStateSurvey, TProjectStyling, TSurveyStyling } from "@/types/config"; +import type { TResponseData, TResponseUpdate } from "@/types/response"; +import type { TFileUploadParams, TUploadFileConfig } from "@/types/storage"; + +export interface SurveyBaseProps { + survey: TEnvironmentStateSurvey; + styling: TSurveyStyling | TProjectStyling; + isBrandingEnabled: boolean; + getSetIsError?: (getSetError: (value: boolean) => void) => void; + getSetIsResponseSendingFinished?: (getSetIsResponseSendingFinished: (value: boolean) => void) => void; + getSetQuestionId?: (getSetQuestionId: (value: string) => void) => void; + getSetResponseData?: (getSetResponseData: (value: TResponseData) => void) => void; + onDisplay?: () => void; + onResponse?: (response: TResponseUpdate) => void; + onFinished?: () => void; + onClose?: () => void; + onRetry?: () => void; + autoFocus?: boolean; + isRedirectDisabled?: boolean; + prefillResponseData?: TResponseData; + skipPrefilled?: boolean; + languageCode: string; + onFileUpload: (file: TFileUploadParams["file"], config?: TUploadFileConfig) => Promise; + responseCount?: number; + isCardBorderVisible?: boolean; + startAtQuestionId?: string; + clickOutside?: boolean; + hiddenFieldsRecord?: TResponseData; + shouldResetQuestionId?: boolean; + fullSizeCards?: boolean; +} + +export interface SurveyInlineProps extends SurveyBaseProps { + containerId: string; +} diff --git a/packages/react-native/tsconfig.json b/packages/react-native/tsconfig.json index f3d5c3dc3c..77603fb57e 100644 --- a/packages/react-native/tsconfig.json +++ b/packages/react-native/tsconfig.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, "strict": true }, "exclude": ["dist", "build", "node_modules"], diff --git a/packages/react-native/vite.config.ts b/packages/react-native/vite.config.ts index fa184e9a70..b0fc7f72cf 100644 --- a/packages/react-native/vite.config.ts +++ b/packages/react-native/vite.config.ts @@ -1,9 +1,14 @@ import { resolve } from "node:path"; -import { defineConfig } from "vite"; +import { type UserConfig, defineConfig } from "vite"; import dts from "vite-plugin-dts"; -const config = () => { +const config = (): UserConfig => { return defineConfig({ + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, optimizeDeps: { exclude: ["react-native"], }, @@ -12,7 +17,13 @@ const config = () => { minify: "terser", sourcemap: true, rollupOptions: { - external: ["react", "react-native", "react-dom", "react-native-webview"], + external: [ + "react", + "react-native", + "react-dom", + "react-native-webview", + "@react-native-async-storage/async-storage", + ], }, lib: { entry: resolve(__dirname, "src/index.ts"), @@ -22,6 +33,14 @@ const config = () => { }, }, plugins: [dts({ rollupTypes: true, bundledPackages: ["@formbricks/api", "@formbricks/types"] })], + test: { + setupFiles: ["./vitest.setup.ts"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/lib/**/*.ts"], + }, + }, }); }; diff --git a/packages/react-native/vitest.setup.ts b/packages/react-native/vitest.setup.ts new file mode 100644 index 0000000000..4761029a38 --- /dev/null +++ b/packages/react-native/vitest.setup.ts @@ -0,0 +1,28 @@ +import { afterEach, beforeEach, vi } from "vitest"; + +beforeEach(() => { + vi.resetModules(); + vi.resetAllMocks(); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +// Mock react-native +vi.mock("react-native", () => ({ + Platform: { OS: "ios" }, +})); + +// Mock react-native-webview +vi.mock("react-native-webview", () => ({ + WebView: vi.fn(), +})); + +vi.mock("@react-native-async-storage/async-storage", () => ({ + default: { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }, +})); diff --git a/packages/surveys/src/components/general/subheader.tsx b/packages/surveys/src/components/general/subheader.tsx index 705886bbf2..66443fe75c 100644 --- a/packages/surveys/src/components/general/subheader.tsx +++ b/packages/surveys/src/components/general/subheader.tsx @@ -9,7 +9,7 @@ export function Subheader({ subheader, questionId }: SubheaderProps) { return ( diff --git a/packages/surveys/src/components/questions/date-question.tsx b/packages/surveys/src/components/questions/date-question.tsx index 48a636c029..543f893563 100644 --- a/packages/surveys/src/components/questions/date-question.tsx +++ b/packages/surveys/src/components/questions/date-question.tsx @@ -158,7 +158,7 @@ export function DateQuestion({ subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""} questionId={question.id} /> -
+
{errorMessage}
{!datePickerOpen && ( -
{ setDatePickerOpen(true); }} @@ -174,6 +174,8 @@ export function DateQuestion({ onKeyDown={(e) => { if (e.key === " ") setDatePickerOpen(true); }} + aria-label={selectedDate ? `You have selected ${formattedDate}` : "Select a date"} + aria-describedby={errorMessage ? "error-message" : undefined} className="focus:fb-outline-brand fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-text-heading fb-rounded-custom fb-relative fb-flex fb-h-[12dvh] fb-w-full fb-cursor-pointer fb-appearance-none fb-items-center fb-justify-center fb-border fb-text-left fb-text-base fb-font-normal">
{selectedDate ? ( @@ -186,7 +188,7 @@ export function DateQuestion({
)}
-
+ )} { const baseClass = - "hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal fb-text-heading aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200"; + "hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200"; // today's date class if ( date.getDate() === new Date().getDate() && date.getMonth() === new Date().getMonth() && date.getFullYear() === new Date().getFullYear() ) { - return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading focus:fb-ring-2 focus:fb-bg-slate-200`; + return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-calendar-tile focus:fb-ring-2 focus:fb-bg-slate-200`; } // active date class if ( @@ -238,10 +240,10 @@ export function DateQuestion({ date.getMonth() === selectedDate.getMonth() && date.getFullYear() === selectedDate.getFullYear() ) { - return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading`; + return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-calendar-tile`; } - return baseClass; + return `${baseClass} !fb-text-heading`; }, formatShortWeekday: (_: any, date: Date) => { return date.toLocaleDateString("en-US", { weekday: "short" }).slice(0, 2); diff --git a/packages/surveys/src/components/questions/open-text-question.tsx b/packages/surveys/src/components/questions/open-text-question.tsx index 592e2a737a..9a3e690662 100644 --- a/packages/surveys/src/components/questions/open-text-question.tsx +++ b/packages/surveys/src/components/questions/open-text-question.tsx @@ -111,10 +111,16 @@ export function OpenTextQuestion({ handleInputChange(e.currentTarget.value); }} className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm" - pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"} + pattern={question.inputType === "phone" ? "^[0-9+][0-9+\\- ]*[0-9]$" : ".*"} title={question.inputType === "phone" ? "Enter a valid phone number" : undefined} minlength={question.inputType === "text" ? question.charLimit?.min : undefined} - maxlength={question.inputType === "text" ? question.charLimit?.max : undefined} + maxlength={ + question.inputType === "text" + ? question.charLimit?.max + : question.inputType === "phone" + ? 30 + : undefined + } /> ) : (