mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
54 Commits
4.1.0
...
simplify-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b81235ac71 | ||
|
|
f57497d8b3 | ||
|
|
aab6798b29 | ||
|
|
f07092595f | ||
|
|
c03c7ec1ed | ||
|
|
628de8e6ae | ||
|
|
be4b54a827 | ||
|
|
e03df83e88 | ||
|
|
ed26427302 | ||
|
|
554809742b | ||
|
|
28adfb905c | ||
|
|
05c455ed62 | ||
|
|
f7687bc0ea | ||
|
|
af34391309 | ||
|
|
70978fbbdf | ||
|
|
f6683d1165 | ||
|
|
13be7a8970 | ||
|
|
0472d5e8f0 | ||
|
|
00a61f7abe | ||
|
|
6999abba3b | ||
|
|
9ae66f44ae | ||
|
|
7933d0077a | ||
|
|
cc8289fa33 | ||
|
|
c458051839 | ||
|
|
718a199d5b | ||
|
|
5ab9fdf1e3 | ||
|
|
5741209aa9 | ||
|
|
35d0d8ed54 | ||
|
|
5bce5c0a3b | ||
|
|
c61212964c | ||
|
|
b8d41a6e9b | ||
|
|
eedd5200a4 | ||
|
|
71a85c7126 | ||
|
|
341e2639e1 | ||
|
|
056470e6f0 | ||
|
|
e965ad4b97 | ||
|
|
12e703c02b | ||
|
|
07065f2675 | ||
|
|
7ca45cefeb | ||
|
|
4df28878db | ||
|
|
b355d05b25 | ||
|
|
e757e9aec9 | ||
|
|
cf4119baf6 | ||
|
|
6be2ae3071 | ||
|
|
600b793641 | ||
|
|
cde03b6997 | ||
|
|
00371bfb01 | ||
|
|
6be6782531 | ||
|
|
3ae4f8aa68 | ||
|
|
3d3c69a92b | ||
|
|
b1b94eaa66 | ||
|
|
67cc96449d | ||
|
|
bf41a53b86 | ||
|
|
26292ecf39 |
@@ -179,14 +179,14 @@ For endpoints serving client SDKs, coordinate TTLs across layers:
|
||||
|
||||
```typescript
|
||||
// Client SDK cache (expiresAt) - longest TTL for fewer requests
|
||||
const CLIENT_TTL = 60 * 60; // 1 hour (seconds for client)
|
||||
const CLIENT_TTL = 60; // 1 minute (seconds for client)
|
||||
|
||||
// Server Redis cache - shorter TTL ensures fresh data for clients
|
||||
const SERVER_TTL = 60 * 30 * 1000; // 30 minutes in milliseconds
|
||||
const SERVER_TTL = 60 * 1000; // 1 minutes in milliseconds
|
||||
|
||||
// HTTP cache headers (seconds)
|
||||
const BROWSER_TTL = 60 * 60; // 1 hour (max-age)
|
||||
const CDN_TTL = 60 * 30; // 30 minutes (s-maxage)
|
||||
const BROWSER_TTL = 60; // 1 minute (max-age)
|
||||
const CDN_TTL = 60; // 1 minute (s-maxage)
|
||||
const CORS_TTL = 60 * 60; // 1 hour (balanced approach)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
---
|
||||
description: >
|
||||
This rule provides comprehensive knowledge about the Formbricks database structure, relationships,
|
||||
and data patterns. It should be used **only when the agent explicitly requests database schema-level
|
||||
details** to support tasks such as: writing/debugging Prisma queries, designing/reviewing data models,
|
||||
investigating multi-tenancy behavior, creating API endpoints, or understanding data relationships.
|
||||
globs: []
|
||||
alwaysApply: agent-requested
|
||||
globs: schema.prisma
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Formbricks Database Schema Reference
|
||||
|
||||
This rule provides a reference to the Formbricks database structure. For the most up-to-date and complete schema definitions, please refer to the schema.prisma file directly.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
@@ -281,15 +281,9 @@ runs:
|
||||
tags: ${{ inputs.registry_type == 'ecr' && steps.ecr-tags.outputs.tags || (inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'true' && steps.ghcr-meta-experimental.outputs.tags) || (inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'false' && steps.ghcr-extra-tags.outputs.tags) || (inputs.registry_type == 'ghcr' && format('ghcr.io/{0}:{1}', inputs.ghcr_image_name, steps.version.outputs.version)) || (inputs.registry_type == 'ecr' && format('{0}/{1}:{2}', inputs.ecr_registry, inputs.ecr_repository, steps.version.outputs.version)) }}
|
||||
labels: ${{ inputs.registry_type == 'ghcr' && inputs.experimental_mode == 'true' && steps.ghcr-meta-experimental.outputs.labels || '' }}
|
||||
secrets: |
|
||||
database_url=${{ env.DUMMY_DATABASE_URL }}
|
||||
encryption_key=${{ env.DUMMY_ENCRYPTION_KEY }}
|
||||
redis_url=${{ env.DUMMY_REDIS_URL }}
|
||||
sentry_auth_token=${{ env.SENTRY_AUTH_TOKEN }}
|
||||
env:
|
||||
DEPOT_PROJECT_TOKEN: ${{ env.DEPOT_PROJECT_TOKEN }}
|
||||
DUMMY_DATABASE_URL: ${{ env.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ env.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ env.DUMMY_REDIS_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Sign GHCR image (GHCR only)
|
||||
|
||||
3
.github/workflows/build-and-push-ecr.yml
vendored
3
.github/workflows/build-and-push-ecr.yml
vendored
@@ -88,7 +88,4 @@ jobs:
|
||||
make_latest: ${{ inputs.MAKE_LATEST }}
|
||||
env:
|
||||
DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
@@ -70,9 +70,7 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
secrets: |
|
||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
redis_url=redis://localhost:6379
|
||||
sentry_auth_token=
|
||||
|
||||
- name: Verify and Initialize PostgreSQL
|
||||
run: |
|
||||
@@ -129,7 +127,6 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
run: |
|
||||
echo "🧪 Testing if the Docker image starts correctly..."
|
||||
|
||||
@@ -141,7 +138,7 @@ jobs:
|
||||
$DOCKER_RUN_ARGS \
|
||||
-p 3000:3000 \
|
||||
-e DATABASE_URL="postgresql://test:test@host.docker.internal:5432/formbricks" \
|
||||
-e ENCRYPTION_KEY="$DUMMY_ENCRYPTION_KEY" \
|
||||
-e ENCRYPTION_KEY="test-key-00000000000000000000000000000000000000000000000000" \
|
||||
-e REDIS_URL="redis://host.docker.internal:6379" \
|
||||
-d "formbricks-test:$GITHUB_SHA"
|
||||
|
||||
|
||||
1
.github/workflows/e2e.yml
vendored
1
.github/workflows/e2e.yml
vendored
@@ -17,7 +17,6 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TELEMETRY_DISABLED: 1
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
|
||||
6
.github/workflows/formbricks-release.yml
vendored
6
.github/workflows/formbricks-release.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
||||
- check-latest-release
|
||||
with:
|
||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
|
||||
|
||||
docker-build-cloud:
|
||||
name: Build & push Formbricks Cloud to ECR
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
with:
|
||||
image_tag: ${{ needs.docker-build-community.outputs.VERSION }}
|
||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
|
||||
needs:
|
||||
- check-latest-release
|
||||
- docker-build-community
|
||||
@@ -154,4 +154,4 @@ jobs:
|
||||
release_tag: ${{ github.event.release.tag_name }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
is_prerelease: ${{ github.event.release.prerelease }}
|
||||
make_latest: ${{ needs.check-latest-release.outputs.is_latest }}
|
||||
make_latest: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
|
||||
|
||||
@@ -44,7 +44,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
3
.github/workflows/release-docker-github.yml
vendored
3
.github/workflows/release-docker-github.yml
vendored
@@ -102,7 +102,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DEPOT_PROJECT_TOKEN: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
@@ -25,10 +25,6 @@ RUN corepack prepare pnpm@9.15.9 --activate
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||
|
||||
# Copy the secrets handling script
|
||||
COPY apps/web/scripts/docker/read-secrets.sh /tmp/read-secrets.sh
|
||||
RUN chmod +x /tmp/read-secrets.sh
|
||||
|
||||
# Increase Node.js memory limit as a regular build argument
|
||||
ARG NODE_OPTIONS="--max_old_space_size=8192"
|
||||
ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||
@@ -37,6 +33,10 @@ ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||
# but needs explicit declaration for some build systems (like Depot)
|
||||
ARG TARGETARCH
|
||||
|
||||
# Base path for the application (optional)
|
||||
ARG BASE_PATH=""
|
||||
ENV BASE_PATH=${BASE_PATH}
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -57,13 +57,11 @@ RUN pnpm install --ignore-scripts
|
||||
# Build the database package first
|
||||
RUN pnpm build --filter=@formbricks/database
|
||||
|
||||
# Build the project using our secret reader script
|
||||
# This mounts the secrets only during this build step without storing them in layers
|
||||
RUN --mount=type=secret,id=database_url \
|
||||
--mount=type=secret,id=encryption_key \
|
||||
--mount=type=secret,id=redis_url \
|
||||
--mount=type=secret,id=sentry_auth_token \
|
||||
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
|
||||
# Build the project - only mount Sentry token for optional sourcemap uploads
|
||||
# DATABASE_URL, REDIS_URL, ENCRYPTION_KEY defaults are provided by env.ts during build
|
||||
RUN --mount=type=secret,id=sentry_auth_token \
|
||||
SENTRY_AUTH_TOKEN=$(cat /run/secrets/sentry_auth_token 2>/dev/null || echo "") \
|
||||
pnpm build --filter=@formbricks/web...
|
||||
|
||||
# Extract Prisma version
|
||||
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
|
||||
@@ -124,7 +122,7 @@ RUN chmod -R 755 ./node_modules/@noble/hashes
|
||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||
RUN chmod -R 755 ./node_modules/zod
|
||||
|
||||
RUN npm install -g prisma
|
||||
RUN npm install -g prisma@6
|
||||
|
||||
# Create a startup script to handle the conditional logic
|
||||
COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
|
||||
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
@@ -40,14 +38,6 @@ const ProjectOnboardingLayout = async (props) => {
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-slate-50">
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
user={user}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
organizationBilling={organization.billing}
|
||||
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
||||
/>
|
||||
<ToasterClient />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||
|
||||
const SurveyEditorEnvironmentLayout = async (props) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
|
||||
const { t, session, user } = await environmentIdLayoutChecks(params.environmentId);
|
||||
|
||||
if (!session) {
|
||||
return redirect(`/auth/login`);
|
||||
@@ -25,15 +24,9 @@ const SurveyEditorEnvironmentLayout = async (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<EnvironmentIdBaseLayout
|
||||
environmentId={params.environmentId}
|
||||
session={session}
|
||||
user={user}
|
||||
organization={organization}>
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
</EnvironmentIdBaseLayout>
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { Session } from "next-auth";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
interface PosthogIdentifyProps {
|
||||
session: Session;
|
||||
user: TUser;
|
||||
environmentId?: string;
|
||||
organizationId?: string;
|
||||
organizationName?: string;
|
||||
organizationBilling?: TOrganizationBilling;
|
||||
isPosthogEnabled: boolean;
|
||||
}
|
||||
|
||||
export const PosthogIdentify = ({
|
||||
session,
|
||||
user,
|
||||
environmentId,
|
||||
organizationId,
|
||||
organizationName,
|
||||
organizationBilling,
|
||||
isPosthogEnabled,
|
||||
}: PosthogIdentifyProps) => {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (isPosthogEnabled && session.user && posthog) {
|
||||
posthog.identify(session.user.id, {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
});
|
||||
if (environmentId) {
|
||||
posthog.group("environment", environmentId, { name: environmentId });
|
||||
}
|
||||
if (organizationId) {
|
||||
posthog.group("organization", organizationId, {
|
||||
name: organizationName,
|
||||
plan: organizationBilling?.plan,
|
||||
responseLimit: organizationBilling?.limits.monthly.responses,
|
||||
miuLimit: organizationBilling?.limits.monthly.miu,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
posthog,
|
||||
session.user,
|
||||
environmentId,
|
||||
organizationId,
|
||||
organizationName,
|
||||
organizationBilling,
|
||||
user.name,
|
||||
user.email,
|
||||
isPosthogEnabled,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/comp
|
||||
import { EnvironmentContextWrapper } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getEnvironmentLayoutData } from "@/modules/environments/lib/utils";
|
||||
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
||||
|
||||
const EnvLayout = async (props: {
|
||||
@@ -24,11 +23,7 @@ const EnvLayout = async (props: {
|
||||
const layoutData = await getEnvironmentLayoutData(params.environmentId, session.user.id);
|
||||
|
||||
return (
|
||||
<EnvironmentIdBaseLayout
|
||||
environmentId={params.environmentId}
|
||||
session={layoutData.session}
|
||||
user={layoutData.user}
|
||||
organization={layoutData.organization}>
|
||||
<>
|
||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
||||
<EnvironmentContextWrapper
|
||||
environment={layoutData.environment}
|
||||
@@ -36,7 +31,7 @@ const EnvLayout = async (props: {
|
||||
organization={layoutData.organization}>
|
||||
<EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>
|
||||
</EnvironmentContextWrapper>
|
||||
</EnvironmentIdBaseLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
@@ -16,7 +15,6 @@ interface AirtableWrapperProps {
|
||||
airtableArray: TIntegrationItem[];
|
||||
airtableIntegration?: TIntegrationAirtable;
|
||||
surveys: TSurvey[];
|
||||
environment: TEnvironment;
|
||||
isEnabled: boolean;
|
||||
webAppUrl: string;
|
||||
locale: TUserLocale;
|
||||
@@ -27,7 +25,6 @@ export const AirtableWrapper = ({
|
||||
airtableArray,
|
||||
airtableIntegration,
|
||||
surveys,
|
||||
environment,
|
||||
isEnabled,
|
||||
webAppUrl,
|
||||
locale,
|
||||
@@ -48,7 +45,6 @@ export const AirtableWrapper = ({
|
||||
<ManageIntegration
|
||||
airtableArray={airtableArray}
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
airtableIntegration={airtableIntegration}
|
||||
setIsConnected={setIsConnected}
|
||||
surveys={surveys}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
@@ -15,12 +14,11 @@ import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { IntegrationModalInputs } from "../lib/types";
|
||||
|
||||
interface ManageIntegrationProps {
|
||||
airtableIntegration: TIntegrationAirtable;
|
||||
environment: TEnvironment;
|
||||
environmentId: string;
|
||||
setIsConnected: (data: boolean) => void;
|
||||
surveys: TSurvey[];
|
||||
@@ -29,7 +27,7 @@ interface ManageIntegrationProps {
|
||||
}
|
||||
|
||||
export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
|
||||
const { airtableIntegration, environmentId, setIsConnected, surveys, airtableArray } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tableHeaders = [
|
||||
@@ -132,12 +130,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 w-full">
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
noWidgetRequired={true}
|
||||
emptyMessage={t("environments.integrations.airtable.no_integrations_yet")}
|
||||
/>
|
||||
<EmptyState text={t("environments.integrations.airtable.no_integrations_yet")} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ const Page = async (props) => {
|
||||
airtableArray={airtableArray}
|
||||
environmentId={environment.id}
|
||||
surveys={surveys}
|
||||
environment={environment}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -60,7 +60,6 @@ export const GoogleSheetWrapper = ({
|
||||
selectedIntegration={selectedIntegration}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
setOpenAddIntegrationModal={setIsModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
@@ -15,10 +14,9 @@ import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
|
||||
interface ManageIntegrationProps {
|
||||
environment: TEnvironment;
|
||||
googleSheetIntegration: TIntegrationGoogleSheets;
|
||||
setOpenAddIntegrationModal: (v: boolean) => void;
|
||||
setIsConnected: (v: boolean) => void;
|
||||
@@ -27,7 +25,6 @@ interface ManageIntegrationProps {
|
||||
}
|
||||
|
||||
export const ManageIntegration = ({
|
||||
environment,
|
||||
googleSheetIntegration,
|
||||
setOpenAddIntegrationModal,
|
||||
setIsConnected,
|
||||
@@ -90,12 +87,7 @@ export const ManageIntegration = ({
|
||||
</div>
|
||||
{!integrationArray || integrationArray.length === 0 ? (
|
||||
<div className="mt-4 w-full">
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
noWidgetRequired={true}
|
||||
emptyMessage={t("environments.integrations.google_sheets.no_integrations_yet")}
|
||||
/>
|
||||
<EmptyState text={t("environments.integrations.google_sheets.no_integrations_yet")} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TIntegrationNotionDatabase,
|
||||
} from "@formbricks/types/integration/notion";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import {
|
||||
ERRORS,
|
||||
@@ -122,7 +123,7 @@ export const AddIntegrationModal = ({
|
||||
const questions = selectedSurvey
|
||||
? replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: getLocalizedValue(q.headline, "default"),
|
||||
name: getTextContent(getLocalizedValue(q.headline, "default")),
|
||||
type: q.type,
|
||||
}))
|
||||
: [];
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
@@ -12,11 +11,10 @@ import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface ManageIntegrationProps {
|
||||
environment: TEnvironment;
|
||||
notionIntegration: TIntegrationNotion;
|
||||
setOpenAddIntegrationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsConnected: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@@ -28,7 +26,6 @@ interface ManageIntegrationProps {
|
||||
}
|
||||
|
||||
export const ManageIntegration = ({
|
||||
environment,
|
||||
notionIntegration,
|
||||
setOpenAddIntegrationModal,
|
||||
setIsConnected,
|
||||
@@ -101,12 +98,7 @@ export const ManageIntegration = ({
|
||||
</div>
|
||||
{!integrationArray || integrationArray.length === 0 ? (
|
||||
<div className="mt-4 w-full">
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
noWidgetRequired={true}
|
||||
emptyMessage={t("environments.integrations.notion.no_databases_found")}
|
||||
/>
|
||||
<EmptyState text={t("environments.integrations.notion.no_databases_found")} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
||||
|
||||
@@ -64,7 +64,6 @@ export const NotionWrapper = ({
|
||||
selectedIntegration={selectedIntegration}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
notionIntegration={notionIntegration}
|
||||
setOpenAddIntegrationModal={setIsModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Trash2Icon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
@@ -12,10 +11,9 @@ import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
|
||||
interface ManageIntegrationProps {
|
||||
environment: TEnvironment;
|
||||
slackIntegration: TIntegrationSlack;
|
||||
setOpenAddIntegrationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsConnected: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@@ -29,7 +27,6 @@ interface ManageIntegrationProps {
|
||||
}
|
||||
|
||||
export const ManageIntegration = ({
|
||||
environment,
|
||||
slackIntegration,
|
||||
setOpenAddIntegrationModal,
|
||||
setIsConnected,
|
||||
@@ -106,12 +103,7 @@ export const ManageIntegration = ({
|
||||
</div>
|
||||
{!integrationArray || integrationArray.length === 0 ? (
|
||||
<div className="mt-4 w-full">
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
noWidgetRequired={true}
|
||||
emptyMessage={t("environments.integrations.slack.connect_your_first_slack_channel")}
|
||||
/>
|
||||
<EmptyState text={t("environments.integrations.slack.connect_your_first_slack_channel")} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
||||
|
||||
@@ -78,7 +78,6 @@ export const SlackWrapper = ({
|
||||
selectedIntegration={selectedIntegration}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
slackIntegration={slackIntegration}
|
||||
setOpenAddIntegrationModal={setIsModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
|
||||
@@ -215,7 +215,7 @@ export const EditProfileDetailsForm = ({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[var(--radix-dropdown-menu-trigger-width)] bg-slate-50 text-slate-700"
|
||||
className="min-w-[var(--radix-dropdown-menu-trigger-width)] bg-white text-slate-700"
|
||||
align="start">
|
||||
<DropdownMenuRadioGroup value={field.value} onValueChange={field.onChange}>
|
||||
{appLanguages.map((lang) => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
@@ -25,7 +26,7 @@ export const generateMetadata = async (props: Props): Promise<Metadata> => {
|
||||
};
|
||||
|
||||
const SurveyLayout = async ({ children }) => {
|
||||
return <>{children}</>;
|
||||
return <ResponseFilterProvider>{children}</ResponseFilterProvider>;
|
||||
};
|
||||
|
||||
export default SurveyLayout;
|
||||
|
||||
@@ -8,8 +8,8 @@ import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getResponsesAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { ResponseDataView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView";
|
||||
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
|
||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||
@@ -26,6 +26,7 @@ interface ResponsePageProps {
|
||||
isReadOnly: boolean;
|
||||
isQuotasAllowed: boolean;
|
||||
quotas: TSurveyQuota[];
|
||||
initialResponses?: TResponseWithQuotas[];
|
||||
}
|
||||
|
||||
export const ResponsePage = ({
|
||||
@@ -39,11 +40,12 @@ export const ResponsePage = ({
|
||||
isReadOnly,
|
||||
isQuotasAllowed,
|
||||
quotas,
|
||||
initialResponses = [],
|
||||
}: ResponsePageProps) => {
|
||||
const [responses, setResponses] = useState<TResponseWithQuotas[]>([]);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||
const [isFetchingFirstPage, setFetchingFirstPage] = useState<boolean>(true);
|
||||
const [responses, setResponses] = useState<TResponseWithQuotas[]>(initialResponses);
|
||||
const [page, setPage] = useState<number | null>(null);
|
||||
const [hasMore, setHasMore] = useState<boolean>(initialResponses.length >= responsesPerPage);
|
||||
const [isFetchingFirstPage, setIsFetchingFirstPage] = useState<boolean>(false);
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
|
||||
const filters = useMemo(
|
||||
@@ -56,6 +58,7 @@ export const ResponsePage = ({
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
if (page === null) return;
|
||||
const newPage = page + 1;
|
||||
|
||||
let newResponses: TResponseWithQuotas[] = [];
|
||||
@@ -93,10 +96,22 @@ export const ResponsePage = ({
|
||||
}
|
||||
}, [searchParams, resetState]);
|
||||
|
||||
// Only fetch if filters are applied (not on initial mount with no filters)
|
||||
const hasFilters =
|
||||
selectedFilter?.responseStatus !== "all" ||
|
||||
(selectedFilter?.filter && selectedFilter.filter.length > 0) ||
|
||||
(dateRange.from && dateRange.to);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInitialResponses = async () => {
|
||||
const fetchFilteredResponses = async () => {
|
||||
try {
|
||||
setFetchingFirstPage(true);
|
||||
// skip call for initial mount
|
||||
if (page === null && !hasFilters) {
|
||||
setPage(1);
|
||||
return;
|
||||
}
|
||||
setPage(1);
|
||||
setIsFetchingFirstPage(true);
|
||||
let responses: TResponseWithQuotas[] = [];
|
||||
|
||||
const getResponsesActionResponse = await getResponsesAction({
|
||||
@@ -110,19 +125,16 @@ export const ResponsePage = ({
|
||||
|
||||
if (responses.length < responsesPerPage) {
|
||||
setHasMore(false);
|
||||
} else {
|
||||
setHasMore(true);
|
||||
}
|
||||
setResponses(responses);
|
||||
} finally {
|
||||
setFetchingFirstPage(false);
|
||||
setIsFetchingFirstPage(false);
|
||||
}
|
||||
};
|
||||
fetchInitialResponses();
|
||||
}, [surveyId, filters, responsesPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
}, [filters]);
|
||||
fetchFilteredResponses();
|
||||
}, [filters, responsesPerPage, selectedFilter, dateRange, surveyId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,9 +2,8 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI
|
||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED, RESPONSES_PER_PAGE } from "@/lib/constants";
|
||||
import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
@@ -14,7 +13,6 @@ import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
|
||||
import { getIsContactsEnabled, getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getQuotas } from "@/modules/ee/quotas/lib/quotas";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
|
||||
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -23,45 +21,44 @@ const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { session, environment, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||
const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const survey = await getSurvey(params.surveyId);
|
||||
const [survey, user, tags, isContactsEnabled, responseCount, locale] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getUser(session.user.id),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
getIsContactsEnabled(),
|
||||
getResponseCountBySurveyId(params.surveyId),
|
||||
findMatchingLocale(),
|
||||
]);
|
||||
|
||||
if (!survey) {
|
||||
throw new Error(t("common.survey_not_found"));
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
const isContactsEnabled = await getIsContactsEnabled();
|
||||
const segments = isContactsEnabled ? await getSegments(params.environmentId) : [];
|
||||
|
||||
// Get response count for the CTA component
|
||||
const responseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||
const displayCount = await getDisplayCountBySurveyId(params.surveyId);
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||
if (!organizationId) {
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
const organizationBilling = await getOrganizationBilling(organizationId);
|
||||
|
||||
const segments = isContactsEnabled ? await getSegments(params.environmentId) : [];
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
const organizationBilling = await getOrganizationBilling(organization.id);
|
||||
if (!organizationBilling) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const isQuotasAllowed = await getIsQuotasEnabled(organizationBilling.plan);
|
||||
|
||||
const quotas = isQuotasAllowed ? await getQuotas(survey.id) : [];
|
||||
|
||||
// Fetch initial responses on the server to prevent duplicate client-side fetch
|
||||
const initialResponses = await getResponses(params.surveyId, RESPONSES_PER_PAGE, 0);
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader
|
||||
@@ -74,7 +71,6 @@ const Page = async (props) => {
|
||||
user={user}
|
||||
publicDomain={publicDomain}
|
||||
responseCount={responseCount}
|
||||
displayCount={displayCount}
|
||||
segments={segments}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
@@ -94,6 +90,7 @@ const Page = async (props) => {
|
||||
isReadOnly={isReadOnly}
|
||||
isQuotasAllowed={isQuotasAllowed}
|
||||
quotas={quotas}
|
||||
initialResponses={initialResponses}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
|
||||
interface AddressSummaryProps {
|
||||
@@ -29,42 +30,48 @@ export const AddressSummary = ({ questionSummary, environmentId, survey, locale
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.samples.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.samples.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 pl-6 font-semibold">
|
||||
<ArrayResponse value={response.value} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 pl-6 font-semibold">
|
||||
<ArrayResponse value={response.value} />
|
||||
</div>
|
||||
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { CSSProperties, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface ClickableBarSegmentProps {
|
||||
children: ReactNode;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const ClickableBarSegment = ({
|
||||
children,
|
||||
onClick,
|
||||
className = "",
|
||||
style,
|
||||
}: ClickableBarSegmentProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className={className} style={style} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.click_to_filter")}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
|
||||
interface ContactInfoSummaryProps {
|
||||
@@ -34,42 +35,48 @@ export const ContactInfoSummary = ({
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.samples.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.samples.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 pl-6 font-semibold">
|
||||
<ArrayResponse value={response.value} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 pl-6 font-semibold">
|
||||
<ArrayResponse value={response.value} />
|
||||
</div>
|
||||
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
|
||||
interface DateQuestionSummary {
|
||||
@@ -55,41 +56,47 @@ export const DateQuestionSummary = ({
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
||||
{renderResponseValue(response.value)}
|
||||
</div>
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
||||
{renderResponseValue(response.value)}
|
||||
</div>
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{visibleResponses < questionSummary.samples.length && (
|
||||
{questionSummary.samples.length > 0 && visibleResponses < questionSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
|
||||
interface FileUploadSummaryProps {
|
||||
@@ -45,71 +46,77 @@ export const FileUploadSummary = ({
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.files.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 grid">
|
||||
{Array.isArray(response.value) &&
|
||||
(response.value.length > 0 ? (
|
||||
response.value.map((fileUrl) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a href={fileUrl} key={fileUrl} target="_blank" rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center p-2">
|
||||
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">
|
||||
{t("common.skipped")}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
{questionSummary.files.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
questionSummary.files.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 grid">
|
||||
{Array.isArray(response.value) &&
|
||||
(response.value.length > 0 ? (
|
||||
response.value.map((fileUrl) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a href={fileUrl} key={fileUrl} target="_blank" rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center p-2">
|
||||
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">
|
||||
{t("common.skipped")}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{visibleResponses < questionSummary.files.length && (
|
||||
{questionSummary.files.length > 0 && visibleResponses < questionSummary.files.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
|
||||
interface HiddenFieldsSummaryProps {
|
||||
environment: TEnvironment;
|
||||
@@ -51,40 +52,46 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
|
||||
<div className="col-span-2 pl-4 md:pl-6">{t("common.response")}</div>
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||
<div
|
||||
key={`${response.value}-${idx}`}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environment.id}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
||||
{response.value}
|
||||
</div>
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
))}
|
||||
{visibleResponses < questionSummary.samples.length && (
|
||||
) : (
|
||||
questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||
<div
|
||||
key={`${response.value}-${idx}`}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environment.id}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
||||
{response.value}
|
||||
</div>
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{questionSummary.samples.length > 0 && visibleResponses < questionSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
|
||||
@@ -85,96 +85,98 @@ export const MultipleChoiceSummary = ({
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result) => {
|
||||
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
|
||||
return (
|
||||
<Fragment key={result.value}>
|
||||
<button
|
||||
type="button"
|
||||
className="group w-full cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
|
||||
? t("environments.surveys.summary.includes_either")
|
||||
: t("environments.surveys.summary.includes_all"),
|
||||
[result.value]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-2 sm:justify-normal">
|
||||
<p className="font-semibold text-slate-700 underline-offset-4 group-hover:underline">
|
||||
{result.value}
|
||||
</p>
|
||||
{choiceId && <IdBadge id={choiceId} />}
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
</p>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group-hover:opacity-80">
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
</div>
|
||||
</button>
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">
|
||||
{t("environments.surveys.summary.other_values_found")}
|
||||
<div className="px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
<div className="space-y-5">
|
||||
{results.map((result) => {
|
||||
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
|
||||
return (
|
||||
<Fragment key={result.value}>
|
||||
<button
|
||||
type="button"
|
||||
className="group w-full cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
|
||||
? t("environments.surveys.summary.includes_either")
|
||||
: t("environments.surveys.summary.includes_all"),
|
||||
[result.value]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-2 sm:justify-normal">
|
||||
<p className="font-semibold text-slate-700 underline-offset-4 group-hover:underline">
|
||||
{result.value}
|
||||
</p>
|
||||
{choiceId && <IdBadge id={choiceId} />}
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
</p>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && t("common.user")}</div>
|
||||
</div>
|
||||
{result.others
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
.slice(0, visibleOtherResponses)
|
||||
.map((otherValue, idx) => (
|
||||
<div key={`${idx}-${otherValue}`} dir="auto">
|
||||
{surveyType === "link" && (
|
||||
<div className="ph-no-capture col-span-1 m-2 flex h-10 items-center rounded-lg pl-4 text-sm font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
)}
|
||||
{surveyType === "app" && otherValue.contact && (
|
||||
<Link
|
||||
href={
|
||||
otherValue.contact.id
|
||||
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
|
||||
: { pathname: null }
|
||||
}
|
||||
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
|
||||
<div className="ph-no-capture col-span-1 pl-4 font-medium text-slate-900">
|
||||
<div className="group-hover:opacity-80">
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
</div>
|
||||
</button>
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">
|
||||
{t("environments.surveys.summary.other_values_found")}
|
||||
</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && t("common.user")}</div>
|
||||
</div>
|
||||
{result.others
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
.slice(0, visibleOtherResponses)
|
||||
.map((otherValue, idx) => (
|
||||
<div key={`${idx}-${otherValue}`} dir="auto">
|
||||
{surveyType === "link" && (
|
||||
<div className="ph-no-capture col-span-1 m-2 flex h-10 items-center rounded-lg pl-4 text-sm font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.contact.id && <PersonAvatar personId={otherValue.contact.id} />}
|
||||
<span>
|
||||
{getContactIdentifier(otherValue.contact, otherValue.contactAttributes)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
)}
|
||||
{surveyType === "app" && otherValue.contact && (
|
||||
<Link
|
||||
href={
|
||||
otherValue.contact.id
|
||||
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
|
||||
: { pathname: null }
|
||||
}
|
||||
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
|
||||
<div className="ph-no-capture col-span-1 pl-4 font-medium text-slate-900">
|
||||
<span>{otherValue.value}</span>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.contact.id && <PersonAvatar personId={otherValue.contact.id} />}
|
||||
<span>
|
||||
{getContactIdentifier(otherValue.contact, otherValue.contactAttributes)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{visibleOtherResponses < result.others.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{visibleOtherResponses < result.others.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart, BarChartHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
@@ -9,8 +11,12 @@ import {
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { HalfCircle, ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/modules/ui/components/tabs";
|
||||
import { TooltipProvider } from "@/modules/ui/components/tooltip";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { ClickableBarSegment } from "./ClickableBarSegment";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { SatisfactionIndicator } from "./SatisfactionIndicator";
|
||||
|
||||
interface NPSSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryNps;
|
||||
@@ -24,8 +30,20 @@ interface NPSSummaryProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
const calculateNPSOpacity = (rating: number): number => {
|
||||
if (rating <= 6) {
|
||||
return 0.3 + (rating / 6) * 0.3;
|
||||
}
|
||||
if (rating <= 8) {
|
||||
return 0.6 + ((rating - 6) / 2) * 0.2;
|
||||
}
|
||||
return 0.8 + ((rating - 8) / 2) * 0.2;
|
||||
};
|
||||
|
||||
export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"aggregated" | "individual">("aggregated");
|
||||
|
||||
const applyFilter = (group: string) => {
|
||||
const filters = {
|
||||
promoters: {
|
||||
@@ -61,38 +79,112 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
|
||||
<button
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
key={group}
|
||||
onClick={() => applyFilter(group)}>
|
||||
<div
|
||||
className={`mb-2 flex justify-between ${group === "dismissed" ? "mb-2 border-t bg-white pt-4 text-sm md:text-base" : ""}`}>
|
||||
<div className="mr-8 flex space-x-1">
|
||||
<p
|
||||
className={`font-semibold capitalize text-slate-700 ${group === "dismissed" ? "" : "text-slate-700"}`}>
|
||||
{group}
|
||||
</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary[group]?.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary[group]?.count}{" "}
|
||||
{questionSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={questionSummary.promoters.percentage} />
|
||||
<div>
|
||||
{t("environments.surveys.summary.promoters")}:{" "}
|
||||
{convertFloatToNDecimal(questionSummary.promoters.percentage, 2)}%
|
||||
</div>
|
||||
<ProgressBar
|
||||
barColor={group === "dismissed" ? "bg-slate-600" : "bg-brand-dark"}
|
||||
progress={questionSummary[group]?.percentage / 100}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as "aggregated" | "individual")}>
|
||||
<div className="flex justify-end px-4 md:px-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="aggregated" icon={<BarChartHorizontal className="h-4 w-4" />}>
|
||||
{t("environments.surveys.summary.aggregated")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="individual" icon={<BarChart className="h-4 w-4" />}>
|
||||
{t("environments.surveys.summary.individual")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="aggregated" className="mt-4">
|
||||
<div className="px-4 pb-6 pt-4 md:px-6">
|
||||
<div className="space-y-5 text-sm md:text-base">
|
||||
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
|
||||
<button
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
key={group}
|
||||
onClick={() => applyFilter(group)}>
|
||||
<div
|
||||
className={`mb-2 flex justify-between ${group === "dismissed" ? "mb-2 border-t bg-white pt-4 text-sm md:text-base" : ""}`}>
|
||||
<div className="mr-8 flex space-x-1">
|
||||
<p
|
||||
className={`font-semibold capitalize text-slate-700 ${group === "dismissed" ? "" : "text-slate-700"}`}>
|
||||
{group}
|
||||
</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary[group]?.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary[group]?.count}{" "}
|
||||
{questionSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar
|
||||
barColor={group === "dismissed" ? "bg-slate-600" : "bg-brand-dark"}
|
||||
progress={questionSummary[group]?.percentage / 100}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="individual" className="mt-4">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="grid grid-cols-11 gap-2 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{questionSummary.choices.map((choice) => {
|
||||
const opacity = calculateNPSOpacity(choice.rating);
|
||||
|
||||
return (
|
||||
<ClickableBarSegment
|
||||
key={choice.rating}
|
||||
className="group flex cursor-pointer flex-col items-center"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
choice.rating.toString()
|
||||
)
|
||||
}>
|
||||
<div className="flex h-32 w-full flex-col items-center justify-end">
|
||||
<div
|
||||
className="bg-brand-dark w-full rounded-t-lg border border-slate-200 transition-all group-hover:brightness-110"
|
||||
style={{
|
||||
height: `${Math.max(choice.percentage, 2)}%`,
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center rounded-b-lg border border-t-0 border-slate-200 bg-slate-50 px-1 py-2">
|
||||
<div className="mb-1.5 text-xs font-medium text-slate-500">{choice.rating}</div>
|
||||
<div className="mb-1 flex items-center space-x-1">
|
||||
<div className="text-base font-semibold text-slate-700">{choice.count}</div>
|
||||
<div className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600">
|
||||
{convertFloatToNDecimal(choice.percentage, 1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClickableBarSegment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-center pb-4 pt-4">
|
||||
<HalfCircle value={questionSummary.score} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
|
||||
@@ -35,59 +36,65 @@ export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale
|
||||
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<div className="border-t border-slate-200"></div>
|
||||
<div className="max-h-[40vh] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader className="bg-slate-100">
|
||||
<TableRow>
|
||||
<TableHead>{t("common.user")}</TableHead>
|
||||
<TableHead>{t("common.response")}</TableHead>
|
||||
<TableHead>{t("common.time")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<TableRow key={response.id}>
|
||||
<TableCell>
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-normal text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{typeof response.value === "string"
|
||||
? renderHyperlinkedContent(response.value)
|
||||
: response.value}
|
||||
</TableCell>
|
||||
<TableCell width={120}>
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</TableCell>
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[40vh] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader className="bg-slate-100">
|
||||
<TableRow>
|
||||
<TableHead className="w-1/4">{t("common.user")}</TableHead>
|
||||
<TableHead className="w-2/4">{t("common.response")}</TableHead>
|
||||
<TableHead className="w-1/4">{t("common.time")}</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{visibleResponses < questionSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<TableRow key={response.id}>
|
||||
<TableCell className="w-1/4">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getContactIdentifier(response.contact, response.contactAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-normal text-slate-600 md:ml-2">{t("common.anonymous")}</p>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="w-2/4 font-medium">
|
||||
{typeof response.value === "string"
|
||||
? renderHyperlinkedContent(response.value)
|
||||
: response.value}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/4">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{visibleResponses < questionSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,8 +57,8 @@ export const QuestionSummaryHeader = ({
|
||||
{t("environments.surveys.edit.optional")}
|
||||
</div>
|
||||
)}
|
||||
<IdBadge id={questionSummary.question.id} />
|
||||
</div>
|
||||
<IdBadge id={questionSummary.question.id} label={t("common.question_id")} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
|
||||
interface RatingScaleLegendProps {
|
||||
scale: TSurveyRatingQuestion["scale"];
|
||||
range: number;
|
||||
}
|
||||
|
||||
export const RatingScaleLegend = ({ scale, range }: RatingScaleLegendProps) => {
|
||||
return (
|
||||
<div className="mt-3 flex w-full items-start justify-between px-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<RatingResponse scale={scale} answer={1} range={range} addColors={false} variant="scale" />
|
||||
<span className="text-xs text-slate-500">1</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-xs text-slate-500">{range}</span>
|
||||
<RatingResponse scale={scale} answer={range} range={range} addColors={false} variant="scale" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { BarChart, BarChartHorizontal, CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
@@ -11,9 +11,15 @@ import {
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/modules/ui/components/tabs";
|
||||
import { TooltipProvider } from "@/modules/ui/components/tooltip";
|
||||
import { ClickableBarSegment } from "./ClickableBarSegment";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { RatingScaleLegend } from "./RatingScaleLegend";
|
||||
import { SatisfactionIndicator } from "./SatisfactionIndicator";
|
||||
|
||||
interface RatingSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryRating;
|
||||
@@ -29,6 +35,8 @@ interface RatingSummaryProps {
|
||||
|
||||
export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"aggregated" | "individual">("aggregated");
|
||||
|
||||
const getIconBasedOnScale = useMemo(() => {
|
||||
const scale = questionSummary.question.scale;
|
||||
if (scale === "number") return <CircleSlash2 className="h-4 w-4" />;
|
||||
@@ -42,52 +50,170 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
{getIconBasedOnScale}
|
||||
<div>
|
||||
{t("environments.surveys.summary.overall")}: {questionSummary.average.toFixed(2)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
{getIconBasedOnScale}
|
||||
<div>
|
||||
{t("environments.surveys.summary.overall")}: {questionSummary.average.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={questionSummary.csat.satisfiedPercentage} />
|
||||
<div>
|
||||
CSAT: {questionSummary.csat.satisfiedPercentage}%{" "}
|
||||
{t("environments.surveys.summary.satisfied")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{questionSummary.choices.map((result) => (
|
||||
<button
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
key={result.rating}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
}>
|
||||
<div className="text flex justify-between px-2 pb-2">
|
||||
<div className="mr-8 flex items-center space-x-1">
|
||||
<div className="font-semibold text-slate-700">
|
||||
<RatingResponse
|
||||
scale={questionSummary.question.scale}
|
||||
answer={result.rating}
|
||||
range={questionSummary.question.range}
|
||||
addColors={questionSummary.question.isColorCodingEnabled}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as "aggregated" | "individual")}>
|
||||
<div className="flex justify-end px-4 md:px-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="aggregated" icon={<BarChartHorizontal className="h-4 w-4" />}>
|
||||
{t("environments.surveys.summary.aggregated")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="individual" icon={<BarChart className="h-4 w-4" />}>
|
||||
{t("environments.surveys.summary.individual")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="aggregated" className="mt-4">
|
||||
<div className="px-4 pb-6 pt-4 md:px-6">
|
||||
{questionSummary.responseCount === 0 ? (
|
||||
<>
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<RatingScaleLegend
|
||||
scale={questionSummary.question.scale}
|
||||
range={questionSummary.question.range}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="flex h-12 w-full overflow-hidden rounded-t-lg border border-slate-200">
|
||||
{questionSummary.choices.map((result, index) => {
|
||||
if (result.percentage === 0) return null;
|
||||
|
||||
const range = questionSummary.question.range;
|
||||
const opacity = 0.3 + (result.rating / range) * 0.8;
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === questionSummary.choices.length - 1;
|
||||
|
||||
return (
|
||||
<ClickableBarSegment
|
||||
key={result.rating}
|
||||
className="relative h-full cursor-pointer transition-opacity hover:brightness-110"
|
||||
style={{
|
||||
width: `${result.percentage}%`,
|
||||
borderRight: isLast ? "none" : "1px solid rgb(226, 232, 240)",
|
||||
}}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
}>
|
||||
<div
|
||||
className={`bg-brand-dark h-full ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
|
||||
style={{ opacity }}
|
||||
/>
|
||||
</ClickableBarSegment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<div className="flex w-full overflow-hidden rounded-b-lg border border-t-0 border-slate-200 bg-slate-50">
|
||||
{questionSummary.choices.map((result, index) => {
|
||||
if (result.percentage === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={result.rating}
|
||||
className="flex flex-col items-center justify-center py-2"
|
||||
style={{
|
||||
width: `${result.percentage}%`,
|
||||
borderRight:
|
||||
index < questionSummary.choices.length - 1
|
||||
? "1px solid rgb(226, 232, 240)"
|
||||
: "none",
|
||||
}}>
|
||||
<div className="mb-1 flex items-center justify-center">
|
||||
<RatingResponse
|
||||
scale={questionSummary.question.scale}
|
||||
answer={result.rating}
|
||||
range={questionSummary.question.range}
|
||||
addColors={false}
|
||||
variant="aggregated"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-slate-600">
|
||||
{convertFloatToNDecimal(result.percentage, 1)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
</p>
|
||||
<RatingScaleLegend
|
||||
scale={questionSummary.question.scale}
|
||||
range={questionSummary.question.range}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="individual" className="mt-4">
|
||||
<div className="px-4 pb-6 pt-4 md:px-6">
|
||||
<div className="space-y-5 text-sm md:text-base">
|
||||
{questionSummary.choices.map((result) => (
|
||||
<div key={result.rating}>
|
||||
<button
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
}>
|
||||
<div className="text flex justify-between px-2 pb-2">
|
||||
<div className="mr-8 flex items-center space-x-1">
|
||||
<div className="font-semibold text-slate-700">
|
||||
<RatingResponse
|
||||
scale={questionSummary.question.scale}
|
||||
answer={result.rating}
|
||||
range={questionSummary.question.range}
|
||||
addColors={questionSummary.question.isColorCodingEnabled}
|
||||
variant="individual"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{result.count} {result.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{result.count} {result.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{questionSummary.dismissed && questionSummary.dismissed.count > 0 && (
|
||||
<div className="rounded-b-lg border-t bg-white px-6 py-4">
|
||||
<div key="dismissed">
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
interface SatisfactionIndicatorProps {
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export const SatisfactionIndicator = ({ percentage }: SatisfactionIndicatorProps) => {
|
||||
let colorClass = "";
|
||||
|
||||
if (percentage > 80) {
|
||||
colorClass = "bg-emerald-500";
|
||||
} else if (percentage >= 55) {
|
||||
colorClass = "bg-orange-500";
|
||||
} else {
|
||||
colorClass = "bg-rose-500";
|
||||
}
|
||||
|
||||
return <div className={`h-3 w-3 rounded-full ${colorClass}`} />;
|
||||
};
|
||||
@@ -3,15 +3,20 @@
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TI18nString, TSurveyQuestionId, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveySummary,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import {
|
||||
SelectedFilterValue,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary";
|
||||
import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
|
||||
import { ConsentSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
|
||||
@@ -29,7 +34,7 @@ import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/
|
||||
import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
|
||||
import { AddressSummary } from "./AddressSummary";
|
||||
|
||||
@@ -54,7 +59,7 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
const filterObject: SelectedFilterValue = { ...selectedFilter };
|
||||
const value = {
|
||||
id: questionId,
|
||||
label: getLocalizedValue(label, "default"),
|
||||
label: getTextContent(getLocalizedValue(label, "default")),
|
||||
questionType: questionType,
|
||||
type: OptionsType.QUESTIONS,
|
||||
};
|
||||
@@ -103,12 +108,7 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
) : summary.length === 0 ? (
|
||||
<SkeletonLoader type="summary" />
|
||||
) : responseCount === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="response"
|
||||
environment={environment}
|
||||
noWidgetRequired={survey.type === "link"}
|
||||
emptyMessage={t("environments.surveys.summary.no_responses_found")}
|
||||
/>
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} />
|
||||
) : (
|
||||
summary.map((questionSummary) => {
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.OpenText) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cn } from "@/modules/ui/lib/utils";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
surveySummary: TSurveySummary["meta"];
|
||||
quotasCount: number;
|
||||
isLoading: boolean;
|
||||
tab: "dropOffs" | "quotas" | undefined;
|
||||
setTab: React.Dispatch<React.SetStateAction<"dropOffs" | "quotas" | undefined>>;
|
||||
@@ -31,6 +32,7 @@ const formatTime = (ttc) => {
|
||||
|
||||
export const SummaryMetadata = ({
|
||||
surveySummary,
|
||||
quotasCount,
|
||||
isLoading,
|
||||
tab,
|
||||
setTab,
|
||||
@@ -61,7 +63,7 @@ export const SummaryMetadata = ({
|
||||
<div
|
||||
className={cn(
|
||||
`grid gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-2 lg:grid-cols-3 2xl:grid-cols-5`,
|
||||
isQuotasAllowed && "2xl:grid-cols-6"
|
||||
isQuotasAllowed && quotasCount > 0 && "2xl:grid-cols-6"
|
||||
)}>
|
||||
<StatCard
|
||||
label={t("environments.surveys.summary.impressions")}
|
||||
@@ -105,7 +107,7 @@ export const SummaryMetadata = ({
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{isQuotasAllowed && (
|
||||
{isQuotasAllowed && quotasCount > 0 && (
|
||||
<InteractiveCard
|
||||
key="quotas"
|
||||
tab="quotas"
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getSurveySummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
|
||||
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
|
||||
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
|
||||
@@ -115,6 +115,7 @@ export const SummaryPage = ({
|
||||
<>
|
||||
<SummaryMetadata
|
||||
surveySummary={surveySummary.meta}
|
||||
quotasCount={surveySummary.quotas?.length ?? 0}
|
||||
isLoading={isLoading}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
|
||||
@@ -29,7 +29,6 @@ interface SurveyAnalysisCTAProps {
|
||||
user: TUser;
|
||||
publicDomain: string;
|
||||
responseCount: number;
|
||||
displayCount: number;
|
||||
segments: TSegment[];
|
||||
isContactsEnabled: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -48,7 +47,6 @@ export const SurveyAnalysisCTA = ({
|
||||
user,
|
||||
publicDomain,
|
||||
responseCount,
|
||||
displayCount,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
@@ -96,7 +94,6 @@ export const SurveyAnalysisCTA = ({
|
||||
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||
environmentId: environment.id,
|
||||
surveyId: surveyId,
|
||||
targetEnvironmentId: environment.id,
|
||||
});
|
||||
@@ -170,7 +167,7 @@ export const SurveyAnalysisCTA = ({
|
||||
icon: ListRestart,
|
||||
tooltip: t("environments.surveys.summary.reset_survey"),
|
||||
onClick: () => setIsResetModalOpen(true),
|
||||
isVisible: !isReadOnly && (responseCount > 0 || displayCount > 0),
|
||||
isVisible: !isReadOnly,
|
||||
},
|
||||
{
|
||||
icon: SquarePenIcon,
|
||||
|
||||
@@ -2334,6 +2334,147 @@ describe("NPS question type tests", () => {
|
||||
// Score should be -100 since all valid responses are detractors
|
||||
expect(summary[0].score).toBe(-100);
|
||||
});
|
||||
|
||||
test("getQuestionSummary includes individual score breakdown in choices array for NPS", async () => {
|
||||
const question = {
|
||||
id: "nps-q1",
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "How likely are you to recommend us?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "nps-q1": 0 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "nps-q1": 5 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "nps-q1": 7 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r4",
|
||||
data: { "nps-q1": 9 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r5",
|
||||
data: { "nps-q1": 10 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "nps-q1", impressions: 5, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
expect(summary[0].choices).toBeDefined();
|
||||
expect(summary[0].choices).toHaveLength(11); // Scores 0-10
|
||||
|
||||
// Verify specific scores
|
||||
const score0 = summary[0].choices.find((c: any) => c.rating === 0);
|
||||
expect(score0.count).toBe(1);
|
||||
expect(score0.percentage).toBe(20); // 1/5 * 100
|
||||
|
||||
const score5 = summary[0].choices.find((c: any) => c.rating === 5);
|
||||
expect(score5.count).toBe(1);
|
||||
expect(score5.percentage).toBe(20);
|
||||
|
||||
const score7 = summary[0].choices.find((c: any) => c.rating === 7);
|
||||
expect(score7.count).toBe(1);
|
||||
expect(score7.percentage).toBe(20);
|
||||
|
||||
const score9 = summary[0].choices.find((c: any) => c.rating === 9);
|
||||
expect(score9.count).toBe(1);
|
||||
expect(score9.percentage).toBe(20);
|
||||
|
||||
const score10 = summary[0].choices.find((c: any) => c.rating === 10);
|
||||
expect(score10.count).toBe(1);
|
||||
expect(score10.percentage).toBe(20);
|
||||
|
||||
// Verify scores with no responses have 0 count
|
||||
const score1 = summary[0].choices.find((c: any) => c.rating === 1);
|
||||
expect(score1.count).toBe(0);
|
||||
expect(score1.percentage).toBe(0);
|
||||
});
|
||||
|
||||
test("getQuestionSummary handles NPS individual score breakdown with no responses", async () => {
|
||||
const question = {
|
||||
id: "nps-q1",
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "How likely are you to recommend us?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses: any[] = [];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "nps-q1", impressions: 0, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
expect(summary[0].choices).toBeDefined();
|
||||
expect(summary[0].choices).toHaveLength(11); // Scores 0-10
|
||||
|
||||
// All scores should have 0 count and percentage
|
||||
summary[0].choices.forEach((choice: any) => {
|
||||
expect(choice.count).toBe(0);
|
||||
expect(choice.percentage).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rating question type tests", () => {
|
||||
@@ -2557,6 +2698,549 @@ describe("Rating question type tests", () => {
|
||||
// Verify dismissed is 0
|
||||
expect(summary[0].dismissed.count).toBe(0);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with range 3", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 3,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 3 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 2 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 3 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 3: satisfied = score 3
|
||||
// 2 out of 3 responses are satisfied (score 3)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(2);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(67); // Math.round((2/3) * 100)
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with range 4", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 4,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 3 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 4 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 2 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 4: satisfied = scores 3-4
|
||||
// 2 out of 3 responses are satisfied (scores 3 and 4)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(2);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(67);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with range 5", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 4 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 5 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 3 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 5: satisfied = scores 4-5
|
||||
// 2 out of 3 responses are satisfied (scores 4 and 5)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(2);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(67);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with range 6", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 6,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 5 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 6 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 4 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 6: satisfied = scores 5-6
|
||||
// 2 out of 3 responses are satisfied (scores 5 and 6)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(2);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(67);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with range 7", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 7,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 6 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 7 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 5 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 7: satisfied = scores 6-7
|
||||
// 2 out of 3 responses are satisfied (scores 6 and 7)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(2);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(67);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with range 10", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 10,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 8 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 9 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 10 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r4",
|
||||
data: { "rating-q1": 7 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 4, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 10: satisfied = scores 8-10
|
||||
// 3 out of 4 responses are satisfied (scores 8, 9, 10)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(3);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(75);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with all satisfied", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 4 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 5 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 5: satisfied = scores 4-5
|
||||
// All 2 responses are satisfied
|
||||
expect(summary[0].csat.satisfiedCount).toBe(2);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(100);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with none satisfied", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { "rating-q1": 1 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { "rating-q1": 2 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r3",
|
||||
data: { "rating-q1": 3 },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: null,
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
// Range 5: satisfied = scores 4-5
|
||||
// None of the responses are satisfied (all are 1, 2, or 3)
|
||||
expect(summary[0].csat.satisfiedCount).toBe(0);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(0);
|
||||
});
|
||||
|
||||
test("getQuestionSummary calculates CSAT for Rating question with no responses", async () => {
|
||||
const question = {
|
||||
id: "rating-q1",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rate our service" },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
};
|
||||
|
||||
const survey = {
|
||||
id: "survey-1",
|
||||
questions: [question],
|
||||
languages: [],
|
||||
welcomeCard: { enabled: false },
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const responses: any[] = [];
|
||||
|
||||
const dropOff = [
|
||||
{ questionId: "rating-q1", impressions: 0, dropOffCount: 0, dropOffPercentage: 0 },
|
||||
] as unknown as TSurveySummary["dropOff"];
|
||||
|
||||
const summary: any = await getQuestionSummary(survey, responses, dropOff);
|
||||
|
||||
expect(summary[0].csat.satisfiedCount).toBe(0);
|
||||
expect(summary[0].csat.satisfiedPercentage).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PictureSelection question type tests", () => {
|
||||
|
||||
@@ -532,13 +532,31 @@ export const getQuestionSummary = async (
|
||||
|
||||
Object.entries(choiceCountMap).forEach(([label, count]) => {
|
||||
values.push({
|
||||
rating: parseInt(label),
|
||||
rating: Number.parseInt(label),
|
||||
count,
|
||||
percentage:
|
||||
totalResponseCount > 0 ? convertFloatTo2Decimal((count / totalResponseCount) * 100) : 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate CSAT based on range
|
||||
let satisfiedCount = 0;
|
||||
if (range === 3) {
|
||||
satisfiedCount = choiceCountMap[3] || 0;
|
||||
} else if (range === 4) {
|
||||
satisfiedCount = (choiceCountMap[3] || 0) + (choiceCountMap[4] || 0);
|
||||
} else if (range === 5) {
|
||||
satisfiedCount = (choiceCountMap[4] || 0) + (choiceCountMap[5] || 0);
|
||||
} else if (range === 6) {
|
||||
satisfiedCount = (choiceCountMap[5] || 0) + (choiceCountMap[6] || 0);
|
||||
} else if (range === 7) {
|
||||
satisfiedCount = (choiceCountMap[6] || 0) + (choiceCountMap[7] || 0);
|
||||
} else if (range === 10) {
|
||||
satisfiedCount = (choiceCountMap[8] || 0) + (choiceCountMap[9] || 0) + (choiceCountMap[10] || 0);
|
||||
}
|
||||
const satisfiedPercentage =
|
||||
totalResponseCount > 0 ? Math.round((satisfiedCount / totalResponseCount) * 100) : 0;
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
@@ -548,6 +566,10 @@ export const getQuestionSummary = async (
|
||||
dismissed: {
|
||||
count: dismissed,
|
||||
},
|
||||
csat: {
|
||||
satisfiedCount,
|
||||
satisfiedPercentage,
|
||||
},
|
||||
});
|
||||
|
||||
values = [];
|
||||
@@ -563,10 +585,17 @@ export const getQuestionSummary = async (
|
||||
score: 0,
|
||||
};
|
||||
|
||||
// Track individual score counts (0-10)
|
||||
const scoreCountMap: Record<number, number> = {};
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
scoreCountMap[i] = 0;
|
||||
}
|
||||
|
||||
responses.forEach((response) => {
|
||||
const value = response.data[question.id];
|
||||
if (typeof value === "number") {
|
||||
data.total++;
|
||||
scoreCountMap[value]++;
|
||||
if (value >= 9) {
|
||||
data.promoters++;
|
||||
} else if (value >= 7) {
|
||||
@@ -585,6 +614,13 @@ export const getQuestionSummary = async (
|
||||
? convertFloatTo2Decimal(((data.promoters - data.detractors) / data.total) * 100)
|
||||
: 0;
|
||||
|
||||
// Build choices array with individual score breakdown
|
||||
const choices = Object.entries(scoreCountMap).map(([rating, count]) => ({
|
||||
rating: Number.parseInt(rating),
|
||||
count,
|
||||
percentage: data.total > 0 ? convertFloatTo2Decimal((count / data.total) * 100) : 0,
|
||||
}));
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
@@ -607,6 +643,7 @@ export const getQuestionSummary = async (
|
||||
count: data.dismissed,
|
||||
percentage: data.total > 0 ? convertFloatTo2Decimal((data.dismissed / data.total) * 100) : 0,
|
||||
},
|
||||
choices,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
user={user}
|
||||
publicDomain={publicDomain}
|
||||
responseCount={initialSurveySummary?.meta.totalResponses ?? 0}
|
||||
displayCount={initialSurveySummary?.meta.displayCount ?? 0}
|
||||
segments={segments}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
DateRange,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import { downloadResponsesFile } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/utils";
|
||||
import { getFormattedFilters, getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
|
||||
@@ -4,7 +4,7 @@ import clsx from "clsx";
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TI18nString, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
|
||||
type QuestionFilterComboBoxProps = {
|
||||
filterOptions: string[] | undefined;
|
||||
filterComboBoxOptions: string[] | undefined;
|
||||
filterOptions: (string | TI18nString)[] | undefined;
|
||||
filterComboBoxOptions: (string | TI18nString)[] | undefined;
|
||||
filterValue: string | undefined;
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
onChangeFilterValue: (o: string) => void;
|
||||
@@ -74,7 +74,7 @@ export const QuestionFilterComboBox = ({
|
||||
if (!isMultiple) return filterComboBoxOptions;
|
||||
|
||||
return filterComboBoxOptions?.filter((o) => {
|
||||
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
return !filterComboBoxValue?.includes(optionValue);
|
||||
});
|
||||
}, [isMultiple, filterComboBoxOptions, filterComboBoxValue, defaultLanguageCode]);
|
||||
@@ -91,14 +91,15 @@ export const QuestionFilterComboBox = ({
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
options?.filter((o) => {
|
||||
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue =
|
||||
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
return optionValue.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
}),
|
||||
[options, searchQuery, defaultLanguageCode]
|
||||
);
|
||||
|
||||
const handleCommandItemSelect = (o: string) => {
|
||||
const value = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const handleCommandItemSelect = (o: string | TI18nString) => {
|
||||
const value = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
|
||||
if (isMultiple) {
|
||||
const newValue = Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, value] : [value];
|
||||
@@ -200,14 +201,18 @@ export const QuestionFilterComboBox = ({
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-white">
|
||||
{filterOptions?.map((o, index) => (
|
||||
<DropdownMenuItem
|
||||
key={`${o}-${index}`}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onChangeFilterValue(o)}>
|
||||
{o}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{filterOptions?.map((o, index) => {
|
||||
const optionValue =
|
||||
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${optionValue}-${index}`}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onChangeFilterValue(optionValue)}>
|
||||
{optionValue}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
@@ -269,7 +274,8 @@ export const QuestionFilterComboBox = ({
|
||||
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{filteredOptions?.map((o) => {
|
||||
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue =
|
||||
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
return (
|
||||
<CommandItem
|
||||
key={optionValue}
|
||||
|
||||
@@ -209,7 +209,7 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question
|
||||
|
||||
{open && (
|
||||
<div className="animate-in absolute top-full z-10 mt-1 w-full overflow-auto rounded-md shadow-md outline-none">
|
||||
<CommandList>
|
||||
<CommandList className="max-h-[600px]">
|
||||
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
|
||||
{options?.map((data) => (
|
||||
<Fragment key={data.header}>
|
||||
|
||||
@@ -4,15 +4,16 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TI18nString, TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
SelectedFilterValue,
|
||||
TResponseStatus,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
|
||||
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
|
||||
import {
|
||||
@@ -25,9 +26,17 @@ import {
|
||||
import { OptionsType, QuestionOption, QuestionsComboBox } from "./QuestionsComboBox";
|
||||
|
||||
export type QuestionFilterOptions = {
|
||||
type: TSurveyQuestionTypeEnum | "Attributes" | "Tags" | "Languages" | "Quotas";
|
||||
filterOptions: string[];
|
||||
filterComboBoxOptions: string[];
|
||||
type:
|
||||
| TSurveyQuestionTypeEnum
|
||||
| "Attributes"
|
||||
| "Tags"
|
||||
| "Languages"
|
||||
| "Quotas"
|
||||
| "Hidden Fields"
|
||||
| "Meta"
|
||||
| OptionsType.OTHERS;
|
||||
filterOptions: (string | TI18nString)[];
|
||||
filterComboBoxOptions: (string | TI18nString)[];
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -69,6 +78,12 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [filterValue, setFilterValue] = useState<SelectedFilterValue>(selectedFilter);
|
||||
|
||||
const getDefaultFilterValue = (option?: QuestionFilterOptions): string | undefined => {
|
||||
if (!option || option.filterOptions.length === 0) return undefined;
|
||||
const firstOption = option.filterOptions[0];
|
||||
return typeof firstOption === "object" ? getLocalizedValue(firstOption, "default") : firstOption;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch the initial data for the filter and load it into the state
|
||||
const handleInitialData = async () => {
|
||||
@@ -94,15 +109,18 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
}, [isOpen, setSelectedOptions, survey]);
|
||||
|
||||
const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => {
|
||||
const matchingFilterOption = selectedOptions.questionFilterOptions.find(
|
||||
(q) => q.type === value.type || q.type === value.questionType
|
||||
);
|
||||
const defaultFilterValue = getDefaultFilterValue(matchingFilterOption);
|
||||
|
||||
if (filterValue.filter[index].questionType) {
|
||||
// Create a new array and copy existing values from SelectedFilter
|
||||
filterValue.filter[index] = {
|
||||
questionType: value,
|
||||
filterType: {
|
||||
filterComboBoxValue: undefined,
|
||||
filterValue: selectedOptions.questionFilterOptions.find(
|
||||
(q) => q.type === value.type || q.type === value.questionType
|
||||
)?.filterOptions[0],
|
||||
filterValue: defaultFilterValue,
|
||||
},
|
||||
};
|
||||
setFilterValue({ filter: [...filterValue.filter], responseStatus: filterValue.responseStatus });
|
||||
@@ -111,9 +129,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
filterValue.filter[index].questionType = value;
|
||||
filterValue.filter[index].filterType = {
|
||||
filterComboBoxValue: undefined,
|
||||
filterValue: selectedOptions.questionFilterOptions.find(
|
||||
(q) => q.type === value.type || q.type === value.questionType
|
||||
)?.filterOptions[0],
|
||||
filterValue: defaultFilterValue,
|
||||
};
|
||||
setFilterValue({ ...filterValue });
|
||||
}
|
||||
@@ -217,11 +233,13 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
setFilterValue(selectedFilter);
|
||||
}, [selectedFilter]);
|
||||
|
||||
const activeFilterCount = filterValue.filter.length + (filterValue.responseStatus === "all" ? 0 : 1);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<PopoverTriggerButton isOpen={isOpen}>
|
||||
Filter <b>{filterValue.filter.length > 0 && `(${filterValue.filter.length})`}</b>
|
||||
Filter <b>{activeFilterCount > 0 && `(${activeFilterCount})`}</b>
|
||||
</PopoverTriggerButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { Suspense } from "react";
|
||||
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
|
||||
import { IS_POSTHOG_CONFIGURED, POSTHOG_API_HOST, POSTHOG_API_KEY } from "@/lib/constants";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { ClientLogout } from "@/modules/ui/components/client-logout";
|
||||
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
|
||||
import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client";
|
||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
|
||||
const AppLayout = async ({ children }) => {
|
||||
@@ -21,20 +18,9 @@ const AppLayout = async ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<NoMobileOverlay />
|
||||
<Suspense>
|
||||
<PostHogPageview
|
||||
posthogEnabled={IS_POSTHOG_CONFIGURED}
|
||||
postHogApiHost={POSTHOG_API_HOST}
|
||||
postHogApiKey={POSTHOG_API_KEY}
|
||||
/>
|
||||
</Suspense>
|
||||
<PHProvider posthogEnabled={IS_POSTHOG_CONFIGURED}>
|
||||
<>
|
||||
<IntercomClientWrapper user={user} />
|
||||
<ToasterClient />
|
||||
{children}
|
||||
</>
|
||||
</PHProvider>
|
||||
<IntercomClientWrapper user={user} />
|
||||
<ToasterClient />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Organization } from "@prisma/client";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
|
||||
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
|
||||
|
||||
export const handleBillingLimitsCheck = async (
|
||||
environmentId: string,
|
||||
organizationId: string,
|
||||
organizationBilling: Organization["billing"]
|
||||
): Promise<void> => {
|
||||
if (!IS_FORMBRICKS_CLOUD) return;
|
||||
|
||||
const responsesCount = await getMonthlyOrganizationResponseCount(organizationId);
|
||||
const responsesLimit = organizationBilling.limits.monthly.responses;
|
||||
|
||||
if (responsesLimit && responsesCount >= responsesLimit) {
|
||||
try {
|
||||
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
|
||||
plan: organizationBilling.plan,
|
||||
limits: {
|
||||
projects: null,
|
||||
monthly: {
|
||||
responses: responsesLimit,
|
||||
miu: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// Log error but do not throw
|
||||
logger.error(err, "Error sending plan limits reached event to Posthog");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -18,10 +18,6 @@ import {
|
||||
getMonthlyOrganizationResponseCount,
|
||||
getOrganizationByEnvironmentId,
|
||||
} from "@/lib/organization/service";
|
||||
import {
|
||||
capturePosthogEnvironmentEvent,
|
||||
sendPlanLimitsReachedEventToPosthogWeekly,
|
||||
} from "@/lib/posthogServer";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
|
||||
|
||||
@@ -58,20 +54,6 @@ const checkResponseLimit = async (environmentId: string): Promise<boolean> => {
|
||||
const monthlyResponseLimit = organization.billing.limits.monthly.responses;
|
||||
const isLimitReached = monthlyResponseLimit !== null && currentResponseCount >= monthlyResponseLimit;
|
||||
|
||||
if (isLimitReached) {
|
||||
try {
|
||||
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
|
||||
plan: organization.billing.plan,
|
||||
limits: {
|
||||
projects: null,
|
||||
monthly: { responses: monthlyResponseLimit, miu: null },
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error }, `Error sending plan limits reached event to Posthog`);
|
||||
}
|
||||
}
|
||||
|
||||
return isLimitReached;
|
||||
};
|
||||
|
||||
@@ -111,10 +93,7 @@ export const GET = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
if (!environment.appSetupCompleted) {
|
||||
await Promise.all([
|
||||
updateEnvironment(environment.id, { appSetupCompleted: true }),
|
||||
capturePosthogEnvironmentEvent(environmentId, "app setup completed"),
|
||||
]);
|
||||
await updateEnvironment(environment.id, { appSetupCompleted: true });
|
||||
}
|
||||
|
||||
// check organization subscriptions and response limits
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDisplay } from "./lib/display";
|
||||
|
||||
@@ -59,7 +58,6 @@ export const POST = withV1ApiWrapper({
|
||||
try {
|
||||
const response = await createDisplay(inputValidation.data);
|
||||
|
||||
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
|
||||
return {
|
||||
response: responses.successResponse(response, true),
|
||||
};
|
||||
|
||||
@@ -8,16 +8,11 @@ import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
|
||||
import {
|
||||
capturePosthogEnvironmentEvent,
|
||||
sendPlanLimitsReachedEventToPosthogWeekly,
|
||||
} from "@/lib/posthogServer";
|
||||
import { EnvironmentStateData, getEnvironmentStateData } from "./data";
|
||||
import { getEnvironmentState } from "./environmentState";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/lib/posthogServer");
|
||||
vi.mock("@/lib/cache", () => ({
|
||||
cache: {
|
||||
withCache: vi.fn(),
|
||||
@@ -43,7 +38,6 @@ vi.mock("@/lib/constants", () => ({
|
||||
RECAPTCHA_SECRET_KEY: "mock_recaptcha_secret_key",
|
||||
IS_RECAPTCHA_CONFIGURED: true,
|
||||
IS_PRODUCTION: true,
|
||||
IS_POSTHOG_CONFIGURED: false,
|
||||
ENTERPRISE_LICENSE_KEY: "mock_enterprise_license_key",
|
||||
}));
|
||||
|
||||
@@ -188,9 +182,7 @@ describe("getEnvironmentState", () => {
|
||||
expect(result.data).toEqual(expectedData);
|
||||
expect(getEnvironmentStateData).toHaveBeenCalledWith(environmentId);
|
||||
expect(prisma.environment.update).not.toHaveBeenCalled();
|
||||
expect(capturePosthogEnvironmentEvent).not.toHaveBeenCalled();
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(mockOrganization.id);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError if environment not found", async () => {
|
||||
@@ -226,7 +218,6 @@ describe("getEnvironmentState", () => {
|
||||
where: { id: environmentId },
|
||||
data: { appSetupCompleted: true },
|
||||
});
|
||||
expect(capturePosthogEnvironmentEvent).toHaveBeenCalledWith(environmentId, "app setup completed");
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -237,16 +228,6 @@ describe("getEnvironmentState", () => {
|
||||
|
||||
expect(result.data.surveys).toEqual([]);
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(mockOrganization.id);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalledWith(environmentId, {
|
||||
plan: mockOrganization.billing.plan,
|
||||
limits: {
|
||||
projects: null,
|
||||
monthly: {
|
||||
miu: null,
|
||||
responses: mockOrganization.billing.limits.monthly.responses,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return surveys if monthly response limit not reached (Cloud)", async () => {
|
||||
@@ -256,21 +237,6 @@ describe("getEnvironmentState", () => {
|
||||
|
||||
expect(result.data.surveys).toEqual(mockSurveys);
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(mockOrganization.id);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle error when sending Posthog limit reached event", async () => {
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
|
||||
const posthogError = new Error("Posthog failed");
|
||||
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
|
||||
|
||||
const result = await getEnvironmentState(environmentId);
|
||||
|
||||
expect(result.data.surveys).toEqual([]);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
posthogError,
|
||||
"Error sending plan limits reached event to Posthog"
|
||||
);
|
||||
});
|
||||
|
||||
test("should include recaptchaSiteKey if recaptcha variables are set", async () => {
|
||||
@@ -313,7 +279,6 @@ describe("getEnvironmentState", () => {
|
||||
|
||||
// Should return surveys even with high count since limit is null (unlimited)
|
||||
expect(result.data.surveys).toEqual(mockSurveys);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should propagate database update errors", async () => {
|
||||
@@ -331,21 +296,6 @@ describe("getEnvironmentState", () => {
|
||||
await expect(getEnvironmentState(environmentId)).rejects.toThrow("Database error");
|
||||
});
|
||||
|
||||
test("should propagate PostHog event capture errors", async () => {
|
||||
const incompleteEnvironmentData = {
|
||||
...mockEnvironmentStateData,
|
||||
environment: {
|
||||
...mockEnvironmentStateData.environment,
|
||||
appSetupCompleted: false,
|
||||
},
|
||||
};
|
||||
vi.mocked(getEnvironmentStateData).mockResolvedValue(incompleteEnvironmentData);
|
||||
vi.mocked(capturePosthogEnvironmentEvent).mockRejectedValue(new Error("PostHog error"));
|
||||
|
||||
// Should throw error since Promise.all will fail if PostHog event capture fails
|
||||
await expect(getEnvironmentState(environmentId)).rejects.toThrow("PostHog error");
|
||||
});
|
||||
|
||||
test("should include recaptchaSiteKey when IS_RECAPTCHA_CONFIGURED is true", async () => {
|
||||
const result = await getEnvironmentState(environmentId);
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import "server-only";
|
||||
import { createCacheKey } from "@formbricks/cache";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TJsEnvironmentState } from "@formbricks/types/js";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_RECAPTCHA_CONFIGURED, RECAPTCHA_SITE_KEY } from "@/lib/constants";
|
||||
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
|
||||
import {
|
||||
capturePosthogEnvironmentEvent,
|
||||
sendPlanLimitsReachedEventToPosthogWeekly,
|
||||
} from "@/lib/posthogServer";
|
||||
import { getEnvironmentStateData } from "./data";
|
||||
|
||||
/**
|
||||
@@ -33,13 +28,10 @@ export const getEnvironmentState = async (
|
||||
// Handle app setup completion update if needed
|
||||
// This is a one-time setup flag that can tolerate TTL-based cache expiration
|
||||
if (!environment.appSetupCompleted) {
|
||||
await Promise.all([
|
||||
prisma.environment.update({
|
||||
where: { id: environmentId },
|
||||
data: { appSetupCompleted: true },
|
||||
}),
|
||||
capturePosthogEnvironmentEvent(environmentId, "app setup completed"),
|
||||
]);
|
||||
await prisma.environment.update({
|
||||
where: { id: environmentId },
|
||||
data: { appSetupCompleted: true },
|
||||
});
|
||||
}
|
||||
|
||||
// Check monthly response limits for Formbricks Cloud
|
||||
@@ -49,24 +41,6 @@ export const getEnvironmentState = async (
|
||||
const currentResponseCount = await getMonthlyOrganizationResponseCount(organization.id);
|
||||
isMonthlyResponsesLimitReached =
|
||||
monthlyResponseLimit !== null && currentResponseCount >= monthlyResponseLimit;
|
||||
|
||||
// Send plan limits event if needed
|
||||
if (isMonthlyResponsesLimitReached) {
|
||||
try {
|
||||
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
|
||||
plan: organization.billing.plan,
|
||||
limits: {
|
||||
projects: null,
|
||||
monthly: {
|
||||
miu: null,
|
||||
responses: organization.billing.limits.monthly.responses,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err, "Error sending plan limits reached event to Posthog");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the response data
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/environment/lib/environmentState";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
@@ -28,15 +29,38 @@ export const GET = withV1ApiWrapper({
|
||||
const params = await props.params;
|
||||
|
||||
try {
|
||||
// Simple validation for environmentId (faster than Zod for high-frequency endpoint)
|
||||
// Basic type check for environmentId
|
||||
if (typeof params.environmentId !== "string") {
|
||||
return {
|
||||
response: responses.badRequestResponse("Environment ID is required", undefined, true),
|
||||
};
|
||||
}
|
||||
|
||||
const environmentId = params.environmentId.trim();
|
||||
|
||||
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
|
||||
// This catches all invalid formats including:
|
||||
// - null/undefined passed as string "null" or "undefined"
|
||||
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
|
||||
// - Empty or whitespace-only IDs
|
||||
// - Any other invalid CUID v1 format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
environmentId: params.environmentId,
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.errors[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
);
|
||||
return {
|
||||
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
|
||||
};
|
||||
}
|
||||
|
||||
// Use optimized environment state fetcher with new caching approach
|
||||
const environmentState = await getEnvironmentState(params.environmentId);
|
||||
const environmentState = await getEnvironmentState(environmentId);
|
||||
const { data } = environmentState;
|
||||
|
||||
return {
|
||||
@@ -46,12 +70,12 @@ export const GET = withV1ApiWrapper({
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour for SDK to recheck
|
||||
},
|
||||
true,
|
||||
// Optimized cache headers for Cloudflare CDN and browser caching
|
||||
// max-age=3600: 1hr browser cache (per guidelines)
|
||||
// s-maxage=1800: 30min Cloudflare cache (per guidelines)
|
||||
// stale-while-revalidate=1800: 30min stale serving during revalidation
|
||||
// stale-if-error=3600: 1hr stale serving on origin errors
|
||||
"public, s-maxage=1800, max-age=3600, stale-while-revalidate=1800, stale-if-error=3600"
|
||||
// Cache headers aligned with Redis cache TTL (1 minute)
|
||||
// max-age=60: 1min browser cache
|
||||
// s-maxage=60: 1min Cloudflare CDN cache
|
||||
// stale-while-revalidate=60: 1min stale serving during revalidation
|
||||
// stale-if-error=60: 1min stale serving on origin errors
|
||||
"public, s-maxage=60, max-age=60, stale-while-revalidate=60, stale-if-error=60"
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseInput } from "@formbricks/types/responses";
|
||||
import {
|
||||
getMonthlyOrganizationResponseCount,
|
||||
getOrganizationByEnvironmentId,
|
||||
} from "@/lib/organization/service";
|
||||
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { createResponse, createResponseWithQuotaEvaluation } from "./response";
|
||||
@@ -24,22 +19,13 @@ vi.mock("@/lib/constants", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getMonthlyOrganizationResponseCount: vi.fn(),
|
||||
getOrganizationByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/posthogServer", () => ({
|
||||
sendPlanLimitsReachedEventToPosthogWeekly: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/response/utils", () => ({
|
||||
calculateTtcTotal: vi.fn((ttc) => ttc),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/telemetry", () => ({
|
||||
captureTelemetry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate", () => ({
|
||||
validateInputs: vi.fn(),
|
||||
}));
|
||||
@@ -138,35 +124,6 @@ describe("createResponse", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should check response limits if IS_FORMBRICKS_CLOUD is true", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
|
||||
|
||||
await createResponse(mockResponseInput, prisma);
|
||||
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should send limit reached event if IS_FORMBRICKS_CLOUD is true and limit reached", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
|
||||
|
||||
await createResponse(mockResponseInput, prisma);
|
||||
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalledWith(environmentId, {
|
||||
plan: "free",
|
||||
limits: {
|
||||
projects: null,
|
||||
monthly: {
|
||||
responses: 100,
|
||||
miu: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError if organization not found", async () => {
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
|
||||
await expect(createResponse(mockResponseInput, prisma)).rejects.toThrow(ResourceNotFoundError);
|
||||
@@ -186,20 +143,6 @@ describe("createResponse", () => {
|
||||
vi.mocked(prisma.response.create).mockRejectedValue(genericError);
|
||||
await expect(createResponse(mockResponseInput)).rejects.toThrow(genericError);
|
||||
});
|
||||
|
||||
test("should log error but not throw if sendPlanLimitsReachedEventToPosthogWeekly fails", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
|
||||
const posthogError = new Error("PostHog error");
|
||||
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
|
||||
|
||||
await createResponse(mockResponseInput);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
posthogError,
|
||||
"Error sending plan limits reached event to Posthog"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createResponseWithQuotaEvaluation", () => {
|
||||
|
||||
@@ -6,11 +6,9 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { handleBillingLimitsCheck } from "@/app/api/lib/utils";
|
||||
import { buildPrismaResponseData } from "@/app/api/v1/lib/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { captureTelemetry } from "@/lib/telemetry";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContactByUserId } from "./contact";
|
||||
@@ -83,7 +81,6 @@ export const createResponse = async (
|
||||
tx: Prisma.TransactionClient
|
||||
): Promise<TResponse> => {
|
||||
validateInputs([responseInput, ZResponseInput]);
|
||||
captureTelemetry("response created");
|
||||
|
||||
const { environmentId, userId, finished, ttc: initialTtc } = responseInput;
|
||||
|
||||
@@ -121,8 +118,6 @@ export const createResponse = async (
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
await handleBillingLimitsCheck(environmentId, organization.id, organization.billing);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { headers } from "next/headers";
|
||||
import { NextRequest } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
@@ -10,7 +10,6 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
@@ -51,7 +50,7 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZId.safeParse(environmentId);
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
@@ -172,11 +171,6 @@ export const POST = withV1ApiWrapper({
|
||||
});
|
||||
}
|
||||
|
||||
await capturePosthogEnvironmentEvent(survey.environmentId, "response created", {
|
||||
surveyId: responseData.surveyId,
|
||||
surveyType: survey.type,
|
||||
});
|
||||
|
||||
const quotaObj = createQuotaFullObject(quotaFull);
|
||||
|
||||
const responseDataWithQuota = {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseInput } from "@formbricks/types/responses";
|
||||
import {
|
||||
getMonthlyOrganizationResponseCount,
|
||||
getOrganizationByEnvironmentId,
|
||||
} from "@/lib/organization/service";
|
||||
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getResponseContact } from "@/lib/response/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
@@ -96,9 +92,6 @@ const mockTransformedResponses = [mockResponse, { ...mockResponse, id: "response
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: true,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
@@ -118,10 +111,8 @@ vi.mock("@/lib/constants", () => ({
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
}));
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/lib/posthogServer");
|
||||
vi.mock("@/lib/response/service");
|
||||
vi.mock("@/lib/response/utils");
|
||||
vi.mock("@/lib/telemetry");
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
@@ -162,7 +153,6 @@ describe("Response Lib Tests", () => {
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue({
|
||||
...mockResponsePrisma,
|
||||
});
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
|
||||
|
||||
const response = await createResponse(mockResponseInputWithUserId, mockTx);
|
||||
|
||||
@@ -217,68 +207,6 @@ describe("Response Lib Tests", () => {
|
||||
|
||||
await expect(createResponse(mockResponseInput, mockTx)).rejects.toThrow(genericError);
|
||||
});
|
||||
|
||||
describe("Cloud specific tests", () => {
|
||||
test("should check response limit and send event if limit reached", async () => {
|
||||
// IS_FORMBRICKS_CLOUD is true by default from the top-level mock
|
||||
const limit = 100;
|
||||
const mockOrgWithBilling = {
|
||||
...mockOrganization,
|
||||
billing: { limits: { monthly: { responses: limit } } },
|
||||
} as any;
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrgWithBilling);
|
||||
vi.mocked(calculateTtcTotal).mockReturnValue({ total: 10 });
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma);
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(limit); // Limit reached
|
||||
|
||||
await createResponse(mockResponseInput, mockTx);
|
||||
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should check response limit and not send event if limit not reached", async () => {
|
||||
const limit = 100;
|
||||
const mockOrgWithBilling = {
|
||||
...mockOrganization,
|
||||
billing: { limits: { monthly: { responses: limit } } },
|
||||
} as any;
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrgWithBilling);
|
||||
vi.mocked(calculateTtcTotal).mockReturnValue({ total: 10 });
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma);
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(limit - 1); // Limit not reached
|
||||
|
||||
await createResponse(mockResponseInput, mockTx);
|
||||
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should log error if sendPlanLimitsReachedEventToPosthogWeekly fails", async () => {
|
||||
const limit = 100;
|
||||
const mockOrgWithBilling = {
|
||||
...mockOrganization,
|
||||
billing: { limits: { monthly: { responses: limit } } },
|
||||
} as any;
|
||||
const posthogError = new Error("Posthog error");
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrgWithBilling);
|
||||
vi.mocked(calculateTtcTotal).mockReturnValue({ total: 10 });
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma);
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(limit); // Limit reached
|
||||
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
|
||||
|
||||
// Expecting successful response creation despite PostHog error
|
||||
const response = await createResponse(mockResponseInput, mockTx);
|
||||
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalled();
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
posthogError,
|
||||
"Error sending plan limits reached event to Posthog"
|
||||
);
|
||||
expect(response).toEqual(mockResponse); // Should still return the created response
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResponsesByEnvironmentIds", () => {
|
||||
|
||||
@@ -8,14 +8,12 @@ import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { handleBillingLimitsCheck } from "@/app/api/lib/utils";
|
||||
import { buildPrismaResponseData } from "@/app/api/v1/lib/utils";
|
||||
import { RESPONSES_PER_PAGE } from "@/lib/constants";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getResponseContact } from "@/lib/response/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { captureTelemetry } from "@/lib/telemetry";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContactByUserId } from "./contact";
|
||||
@@ -93,7 +91,6 @@ export const createResponse = async (
|
||||
tx?: Prisma.TransactionClient
|
||||
): Promise<TResponse> => {
|
||||
validateInputs([responseInput, ZResponseInput]);
|
||||
captureTelemetry("response created");
|
||||
|
||||
const { environmentId, userId, finished, ttc: initialTtc } = responseInput;
|
||||
|
||||
@@ -131,8 +128,6 @@ export const createResponse = async (
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
await handleBillingLimitsCheck(environmentId, organization.id, organization.billing);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displays/types/display";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDisplay } from "./lib/display";
|
||||
|
||||
@@ -49,7 +48,6 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
try {
|
||||
const response = await createDisplay(inputValidation.data);
|
||||
|
||||
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
|
||||
return responses.successResponse(response, true);
|
||||
} catch (error) {
|
||||
if (error instanceof ResourceNotFoundError) {
|
||||
|
||||
@@ -8,13 +8,8 @@ import { TResponseWithQuotaFull, TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import {
|
||||
getMonthlyOrganizationResponseCount,
|
||||
getOrganizationByEnvironmentId,
|
||||
} from "@/lib/organization/service";
|
||||
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { captureTelemetry } from "@/lib/telemetry";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContact } from "./contact";
|
||||
@@ -49,9 +44,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/lib/posthogServer");
|
||||
vi.mock("@/lib/response/utils");
|
||||
vi.mock("@/lib/telemetry");
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@/modules/ee/quotas/lib/evaluation-service");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
@@ -166,9 +159,6 @@ describe("createResponse V2", () => {
|
||||
...ttc,
|
||||
_total: Object.values(ttc).reduce((a, b) => a + b, 0),
|
||||
}));
|
||||
vi.mocked(captureTelemetry).mockResolvedValue(undefined);
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
|
||||
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockResolvedValue(undefined);
|
||||
vi.mocked(evaluateResponseQuotas).mockResolvedValue({
|
||||
shouldEndSurvey: false,
|
||||
quotaFull: null,
|
||||
@@ -179,32 +169,6 @@ describe("createResponse V2", () => {
|
||||
mockIsFormbricksCloud = false;
|
||||
});
|
||||
|
||||
test("should check response limits if IS_FORMBRICKS_CLOUD is true", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
await createResponse(mockResponseInput, mockTx);
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should send limit reached event if IS_FORMBRICKS_CLOUD is true and limit reached", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
|
||||
|
||||
await createResponse(mockResponseInput, mockTx);
|
||||
|
||||
expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith(organizationId);
|
||||
expect(sendPlanLimitsReachedEventToPosthogWeekly).toHaveBeenCalledWith(environmentId, {
|
||||
plan: "free",
|
||||
limits: {
|
||||
projects: null,
|
||||
monthly: {
|
||||
responses: 100,
|
||||
miu: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError if organization not found", async () => {
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
|
||||
await expect(createResponse(mockResponseInput, mockTx)).rejects.toThrow(ResourceNotFoundError);
|
||||
@@ -225,20 +189,6 @@ describe("createResponse V2", () => {
|
||||
await expect(createResponse(mockResponseInput, mockTx)).rejects.toThrow(genericError);
|
||||
});
|
||||
|
||||
test("should log error but not throw if sendPlanLimitsReachedEventToPosthogWeekly fails", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(100);
|
||||
const posthogError = new Error("PostHog error");
|
||||
vi.mocked(sendPlanLimitsReachedEventToPosthogWeekly).mockRejectedValue(posthogError);
|
||||
|
||||
await createResponse(mockResponseInput, mockTx); // Should not throw
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
posthogError,
|
||||
"Error sending plan limits reached event to Posthog"
|
||||
);
|
||||
});
|
||||
|
||||
test("should correctly map prisma tags to response tags", async () => {
|
||||
const mockTag: TTag = { id: "tag1", name: "Tag 1", environmentId };
|
||||
const prismaResponseWithTags = {
|
||||
@@ -269,7 +219,6 @@ describe("createResponseWithQuotaEvaluation V2", () => {
|
||||
...ttc,
|
||||
_total: Object.values(ttc).reduce((a, b) => a + b, 0),
|
||||
}));
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(50);
|
||||
vi.mocked(evaluateResponseQuotas).mockResolvedValue({
|
||||
shouldEndSurvey: false,
|
||||
quotaFull: null,
|
||||
|
||||
@@ -6,12 +6,10 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { handleBillingLimitsCheck } from "@/app/api/lib/utils";
|
||||
import { responseSelection } from "@/app/api/v1/client/[environmentId]/responses/lib/response";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { captureTelemetry } from "@/lib/telemetry";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContact } from "./contact";
|
||||
@@ -91,7 +89,6 @@ export const createResponse = async (
|
||||
tx?: Prisma.TransactionClient
|
||||
): Promise<TResponse> => {
|
||||
validateInputs([responseInput, ZResponseInput]);
|
||||
captureTelemetry("response created");
|
||||
|
||||
const { environmentId, contactId, finished, ttc: initialTtc } = responseInput;
|
||||
|
||||
@@ -129,8 +126,6 @@ export const createResponse = async (
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
await handleBillingLimitsCheck(environmentId, organization.id, organization.billing);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { headers } from "next/headers";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -43,7 +42,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
}
|
||||
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZId.safeParse(environmentId);
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInputV2.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
@@ -148,11 +147,6 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
});
|
||||
}
|
||||
|
||||
await capturePosthogEnvironmentEvent(environmentId, "response created", {
|
||||
surveyId: responseData.surveyId,
|
||||
surveyType: survey.type,
|
||||
});
|
||||
|
||||
const quotaObj = createQuotaFullObject(quotaFull);
|
||||
|
||||
const responseDataWithQuota = {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TTag } from "@formbricks/types/tags";
|
||||
import {
|
||||
DateRange,
|
||||
SelectedFilterValue,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { generateQuestionAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
|
||||
|
||||
@@ -213,8 +213,8 @@ describe("surveys", () => {
|
||||
id: "q8",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
rows: [{ id: "r1", label: "Row 1" }],
|
||||
columns: [{ id: "c1", label: "Column 1" }],
|
||||
rows: [{ id: "r1", label: { default: "Row 1" } }],
|
||||
columns: [{ id: "c1", label: { default: "Column 1" } }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
],
|
||||
createdAt: new Date(),
|
||||
@@ -268,6 +268,64 @@ describe("surveys", () => {
|
||||
expect(sourceFilterOption).toBeDefined();
|
||||
expect(sourceFilterOption?.filterOptions).toEqual(["Equals", "Not equals"]);
|
||||
});
|
||||
|
||||
test("should include quota options in filter options when quotas are provided", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const quotas = [{ id: "quota1" }];
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, quotas as any);
|
||||
|
||||
const quotaFilterOption = result.questionFilterOptions.find((o) => o.id === "quota1");
|
||||
expect(quotaFilterOption).toBeDefined();
|
||||
expect(quotaFilterOption?.type).toBe("Quotas");
|
||||
expect(quotaFilterOption?.filterOptions).toEqual(["Status"]);
|
||||
expect(quotaFilterOption?.filterComboBoxOptions).toEqual([
|
||||
"Screened in",
|
||||
"Screened out (overquota)",
|
||||
"Not in quota",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should include multiple quota options when multiple quotas are provided", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const quotas = [{ id: "quota1" }, { id: "quota2" }];
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, quotas as any);
|
||||
|
||||
const quota1 = result.questionFilterOptions.find((o) => o.id === "quota1");
|
||||
const quota2 = result.questionFilterOptions.find((o) => o.id === "quota2");
|
||||
|
||||
expect(quota1).toBeDefined();
|
||||
expect(quota2).toBeDefined();
|
||||
expect(quota1?.filterComboBoxOptions).toEqual([
|
||||
"Screened in",
|
||||
"Screened out (overquota)",
|
||||
"Not in quota",
|
||||
]);
|
||||
expect(quota2?.filterComboBoxOptions).toEqual([
|
||||
"Screened in",
|
||||
"Screened out (overquota)",
|
||||
"Not in quota",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFormattedFilters", () => {
|
||||
@@ -867,6 +925,75 @@ describe("surveys", () => {
|
||||
expect(result.meta?.url).toEqual({ op: "contains", value: "formbricks.com" });
|
||||
expect(result.meta?.source).toEqual({ op: "equals", value: "newsletter" });
|
||||
});
|
||||
|
||||
test("should filter by quota with screened in status", () => {
|
||||
const selectedFilter: SelectedFilterValue = {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Screened in" },
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = getFormattedFilters(survey, selectedFilter, {} as any);
|
||||
|
||||
expect(result.quotas?.quota1).toEqual({ op: "screenedIn" });
|
||||
});
|
||||
|
||||
test("should filter by quota with screened out status", () => {
|
||||
const selectedFilter: SelectedFilterValue = {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Screened out (overquota)" },
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = getFormattedFilters(survey, selectedFilter, {} as any);
|
||||
|
||||
expect(result.quotas?.quota1).toEqual({ op: "screenedOut" });
|
||||
});
|
||||
|
||||
test("should filter by quota with not in quota status", () => {
|
||||
const selectedFilter: SelectedFilterValue = {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Not in quota" },
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = getFormattedFilters(survey, selectedFilter, {} as any);
|
||||
|
||||
expect(result.quotas?.quota1).toEqual({ op: "screenedOutNotInQuota" });
|
||||
});
|
||||
|
||||
test("should filter by multiple quotas with different statuses", () => {
|
||||
const selectedFilter: SelectedFilterValue = {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Screened in" },
|
||||
},
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 2", id: "quota2" },
|
||||
filterType: { filterComboBoxValue: "Not in quota" },
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = getFormattedFilters(survey, selectedFilter, {} as any);
|
||||
|
||||
expect(result.quotas?.quota1).toEqual({ op: "screenedIn" });
|
||||
expect(result.quotas?.quota2).toEqual({ op: "screenedOutNotInQuota" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTodayDate", () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DateRange,
|
||||
FilterValue,
|
||||
SelectedFilterValue,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import {
|
||||
OptionsType,
|
||||
QuestionOption,
|
||||
@@ -76,9 +76,9 @@ export const generateQuestionAndFilterOptions = (
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
} => {
|
||||
let questionOptions: QuestionOptions[] = [];
|
||||
let questionFilterOptions: any = [];
|
||||
let questionFilterOptions: QuestionFilterOptions[] = [];
|
||||
|
||||
let questionsOptions: any = [];
|
||||
let questionsOptions: QuestionOption[] = [];
|
||||
|
||||
survey.questions.forEach((q) => {
|
||||
if (Object.keys(conditionOptions).includes(q.type)) {
|
||||
@@ -121,8 +121,8 @@ export const generateQuestionAndFilterOptions = (
|
||||
} else if (q.type === TSurveyQuestionTypeEnum.Matrix) {
|
||||
questionFilterOptions.push({
|
||||
type: q.type,
|
||||
filterOptions: q.rows.flatMap((row) => Object.values(row)),
|
||||
filterComboBoxOptions: q.columns.flatMap((column) => Object.values(column)),
|
||||
filterOptions: q.rows.map((row) => getLocalizedValue(row.label, "default")),
|
||||
filterComboBoxOptions: q.columns.map((column) => getLocalizedValue(column.label, "default")),
|
||||
id: q.id,
|
||||
});
|
||||
} else {
|
||||
@@ -236,7 +236,7 @@ export const generateQuestionAndFilterOptions = (
|
||||
questionFilterOptions.push({
|
||||
type: "Quotas",
|
||||
filterOptions: ["Status"],
|
||||
filterComboBoxOptions: ["Screened in", "Screened out (overquota)", "Screened out (not in quota)"],
|
||||
filterComboBoxOptions: ["Screened in", "Screened out (overquota)", "Not in quota"],
|
||||
id: quota.id,
|
||||
});
|
||||
});
|
||||
@@ -549,7 +549,7 @@ export const getFormattedFilters = (
|
||||
const statusMap: Record<string, "screenedIn" | "screenedOut" | "screenedOutNotInQuota"> = {
|
||||
"Screened in": "screenedIn",
|
||||
"Screened out (overquota)": "screenedOut",
|
||||
"Screened out (not in quota)": "screenedOutNotInQuota",
|
||||
"Not in quota": "screenedOutNotInQuota",
|
||||
};
|
||||
const op = statusMap[String(filterType.filterComboBoxValue)];
|
||||
if (op) filters.quotas[quotaId] = { op };
|
||||
|
||||
@@ -1504,7 +1504,7 @@ const docsFeedback = (t: TFunction): TTemplate => {
|
||||
buildOpenTextQuestion({
|
||||
headline: t("templates.docs_feedback_question_2_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
inputType: "url",
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
@@ -2073,7 +2073,7 @@ const careerDevelopmentSurvey = (t: TFunction): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.career_development_survey_name"),
|
||||
role: "productManager",
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.career_development_survey_description"),
|
||||
@@ -2160,7 +2160,7 @@ const professionalDevelopmentSurvey = (t: TFunction): TTemplate => {
|
||||
return buildSurvey(
|
||||
{
|
||||
name: t("templates.professional_development_survey_name"),
|
||||
role: "productManager",
|
||||
role: "peopleManager",
|
||||
industries: ["saas", "eCommerce", "other"],
|
||||
channels: ["link"],
|
||||
description: t("templates.professional_development_survey_description"),
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { LinkSurveyLoading } from "@/modules/survey/link/loading";
|
||||
|
||||
export default LinkSurveyLoading;
|
||||
@@ -7,7 +7,18 @@
|
||||
},
|
||||
"locale": {
|
||||
"source": "en-US",
|
||||
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW"]
|
||||
"targets": [
|
||||
"de-DE",
|
||||
"fr-FR",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
"pt-PT",
|
||||
"ro-RO",
|
||||
"zh-Hans-CN",
|
||||
"zh-Hant-TW",
|
||||
"nl-NL",
|
||||
"es-ES"
|
||||
]
|
||||
},
|
||||
"version": 1.8
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ checksums:
|
||||
common/clear_filters: 8f40ab5af527e4b190da94e7b6221379
|
||||
common/clear_selection: af5d720527735d4253e289400d29ec9e
|
||||
common/click: 9c2744de6b5ac7333d9dae1d5cf4a76d
|
||||
common/click_to_filter: 527714113ca5fd3504e7d0bd31bca303
|
||||
common/clicks: f9e154545f87d8ede27b529e5fdf2015
|
||||
common/close: 2c2e22f8424a1031de89063bd0022e16
|
||||
common/code: 343bc5386149b97cece2b093c39034b2
|
||||
@@ -183,6 +184,7 @@ checksums:
|
||||
common/error_rate_limit_description: 37791a33a947204662ee9c6544e90f51
|
||||
common/error_rate_limit_title: 23ac9419e267e610e1bfd38e1dc35dc0
|
||||
common/expand_rows: b6e06327cb8718dfd6651720843e4dad
|
||||
common/failed_to_copy_to_clipboard: de836a7d628d36c832809252f188f784
|
||||
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
|
||||
common/failed_to_load_projects: 0bba9f9b2b38c189706a486a1bb134c3
|
||||
common/finish: ffa7a10f71182b48fefed7135bee24fa
|
||||
@@ -191,6 +193,7 @@ checksums:
|
||||
common/full_name: f45991923345e8322c9ff8cd6b7e2b16
|
||||
common/gathering_responses: c5914490ed81bd77f13d411739f0c9ef
|
||||
common/general: b891e8f15579fc5d97bcaf3637f5ae59
|
||||
common/generate: 0345bf322c191e70d01fd6607ec5c2f8
|
||||
common/go_back: b917ea82facb90c88c523b255d29f84b
|
||||
common/go_to_dashboard: a6efa97d25e36fedc0af794f6ba610f2
|
||||
common/hidden: fa290c6ada5869d744ed35e9cca64699
|
||||
@@ -301,7 +304,7 @@ checksums:
|
||||
common/project_not_found: be3b516c02b05553acb4ae338511f645
|
||||
common/project_permission_not_found: ace6b03f06bd14e884e4295c5022d61b
|
||||
common/projects: fe8af5cfb3c95cb35534872a325b225e
|
||||
common/question: 0576462ce60d4263d7c482463fcc9547
|
||||
common/question: 2a47e06b62410b16003c4979dee0099f
|
||||
common/question_id: d0c3672976c281411bdccf749faf5ffd
|
||||
common/questions: 38d08215fd7a8026077c7b64eea6bb59
|
||||
common/quota: edd33b180b463ee7a70a64a5c4ad7f02
|
||||
@@ -398,6 +401,7 @@ checksums:
|
||||
common/user_id: 37f5ba37f71cb50607af32a6a203b1d4
|
||||
common/user_not_found: 5903581136ac6c1c1351a482a6d8fdf7
|
||||
common/variable: c13db5775ba9791b1522cc55c9c7acce
|
||||
common/variable_ids: 44bf93b70703b7699fa9f21bc6c8eed4
|
||||
common/variables: ffd3eec5497af36d7b4e4185bad1313a
|
||||
common/verified_email: d4a9e5e47d622c6ef2fede44233076c7
|
||||
common/video: 8050c90e4289b105a0780f0fdda6ff66
|
||||
@@ -491,6 +495,7 @@ checksums:
|
||||
environments/actions/add_css_class_or_id: cfc4d88412c5b9ef1157e28db4afdcc5
|
||||
environments/actions/add_regular_expression_here: 797fde3681996b85bc63c3550dec1fd4
|
||||
environments/actions/add_url: 8eba7972136a42da78a8fa4798da8e87
|
||||
environments/actions/and: 53e8eb67a396fcb5e419bb4cbf0008df
|
||||
environments/actions/click: 9c2744de6b5ac7333d9dae1d5cf4a76d
|
||||
environments/actions/contains: 41c8c25407527a5336404313f4c8d650
|
||||
environments/actions/create_action: 3abcc6dbbca18d3218ba49f90c4a66fd
|
||||
@@ -521,6 +526,7 @@ checksums:
|
||||
environments/actions/limit_to_specific_pages: f8ba95b2fc68d965689594b8a545417c
|
||||
environments/actions/matches_regex: 208b4d02b38714b4523923239e4a66b0
|
||||
environments/actions/on_all_pages: ccb8ee531a55e21eb8157c36fa75ad9a
|
||||
environments/actions/or: 0208d355f231c386b19390f0bea41b95
|
||||
environments/actions/page_filter: fe98a0bcbedb938e58cc3730589caa95
|
||||
environments/actions/page_view: 019c12b6739f6f7b1500f96ee275d47c
|
||||
environments/actions/select_match_type: b555dce1cb5c61538d3fbd792b2c71a2
|
||||
@@ -557,9 +563,18 @@ checksums:
|
||||
environments/contacts/contacts_table_refresh_success: 40951396e88e5c8fdafa0b3bb4fadca8
|
||||
environments/contacts/delete_contact_confirmation: 4304d36277daa205b4aa09f5e0d494ab
|
||||
environments/contacts/delete_contact_confirmation_with_quotas: 7c0e2e223ca55101270ac2988c53e616
|
||||
environments/contacts/generate_personal_link: 9ac0865f6876d40fe858f94eae781eb8
|
||||
environments/contacts/generate_personal_link_description: b9dbaf9e2d8362505b7e3cfa40f415a6
|
||||
environments/contacts/no_published_link_surveys_available: 9c1abc5b21aba827443cdf87dd6c8bfe
|
||||
environments/contacts/no_published_surveys: bd945b0e2e2328c17615c94143bdd62b
|
||||
environments/contacts/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
||||
environments/contacts/not_provided: a09e4d61bbeb04b927406a50116445e2
|
||||
environments/contacts/personal_link_generated: efb7a0420bd459847eb57bca41a4ab0d
|
||||
environments/contacts/personal_link_generated_but_clipboard_failed: 4eb1e208e729bd5ac00c33f72fc38d53
|
||||
environments/contacts/personal_survey_link: 5b3f1afc53733718c4ed5b1443b6a604
|
||||
environments/contacts/please_select_a_survey: 465aa7048773079c8ffdde8b333b78eb
|
||||
environments/contacts/search_contact: 020205a93846ab3e12c203ac4fa97c12
|
||||
environments/contacts/select_a_survey: 1f49086dfb874307aae1136e88c3d514
|
||||
environments/contacts/select_attribute: d93fb60eb4fbb42bf13a22f6216fbd79
|
||||
environments/contacts/unlock_contacts_description: c5572047f02b4c39e5109f9de715499d
|
||||
environments/contacts/unlock_contacts_title: a8b3d7db03eb404d9267fd5cdd6d5ddb
|
||||
@@ -581,6 +596,7 @@ checksums:
|
||||
environments/contacts/upload_contacts_modal_pick_different_file: e748a6e81a425ef9aa33f96ca4edc157
|
||||
environments/contacts/upload_contacts_modal_preview: c4406f8d9a54f131abfff4e9928228bb
|
||||
environments/contacts/upload_contacts_modal_upload_btn: 47b7f3bcf478a7d8dc258d2efc80af37
|
||||
environments/contacts/upload_contacts_success: cd5d6b6d587586dd4f944868c92835bc
|
||||
environments/formbricks_logo: b7ee57de32c8b13463cc8ca8643eddd4
|
||||
environments/integrations/activepieces_integration_description: 62a8fbf86762bab01c7d2db2ba60fff4
|
||||
environments/integrations/additional_settings: 20936205a75745fba2c4047375a04db3
|
||||
@@ -721,20 +737,23 @@ checksums:
|
||||
environments/project/api_keys/secret: f041e5eb96121c8b4f2b8af7e0f83a9b
|
||||
environments/project/api_keys/unable_to_delete_api_key: 1fd76d9a22c5f5f8c241c4891fca8295
|
||||
environments/project/app-connection/app_connection: 778d2305e1a9c8efe91c2c7b4af37ae4
|
||||
environments/project/app-connection/app_connection_description: 01327bfae3da950d796890b6605afed2
|
||||
environments/project/app-connection/cache_update_delay_description: 1cb2c46fdb6762ccb348d21086063a4f
|
||||
environments/project/app-connection/cache_update_delay_title: fef7f99f0228f9e30093574ac7770e7e
|
||||
environments/project/app-connection/app_connection_description: dde226414bd2265cbd0daf6635efcfdd
|
||||
environments/project/app-connection/cache_update_delay_description: 3368e4a8090b7684117a16c94f0c409c
|
||||
environments/project/app-connection/cache_update_delay_title: 60e4a0fcfbd8850bddf29b5c3f59550c
|
||||
environments/project/app-connection/environment_id: 3dba898b081c18cd4cae131765ef411f
|
||||
environments/project/app-connection/environment_id_description: 8b4a763d069b000cfa1a2025a13df80c
|
||||
environments/project/app-connection/formbricks_sdk_connected: 29e8a40ad6a7fdb5af5ee9451a70a9aa
|
||||
environments/project/app-connection/formbricks_sdk_not_connected: 557c534e665750978ba6edb0eacb428e
|
||||
environments/project/app-connection/formbricks_sdk_not_connected_description: 666b2b25f06e76554cc2d60f925bcd4b
|
||||
environments/project/app-connection/formbricks_sdk_not_connected_description: 4ddbacae084238bd0cefeded0fe9dbb9
|
||||
environments/project/app-connection/how_to_setup: 3bad40037f280b47fe6418fcbeb4c717
|
||||
environments/project/app-connection/how_to_setup_description: 2ae5cd9456a8acd3986e3d3678e70ed2
|
||||
environments/project/app-connection/receiving_data: 9f2a48c0b0278861add70b526061264c
|
||||
environments/project/app-connection/recheck: f95f2bbe6990a123d60255c87bdd59f7
|
||||
environments/project/app-connection/sdk_connection_details: 89f2c169fd1604c1df5a834517f1eae1
|
||||
environments/project/app-connection/sdk_connection_details_description: d9b5d06776a139aef6fc8ed53d71bf0a
|
||||
environments/project/app-connection/setup_alert_description: 6d676044d01dc2147731ffab7df6c259
|
||||
environments/project/app-connection/setup_alert_title: 9561cca2b391e0df81e8a982921ff2bb
|
||||
environments/project/app-connection/webapp_url: d64d8cc3c4c4ecce780d94755f7e4de9
|
||||
environments/project/general/cannot_delete_only_project: 24751701a42d8b4d2ba6112a5f642bad
|
||||
environments/project/general/delete_project: e4a2a227105c4ec71e561ab1f140eb26
|
||||
environments/project/general/delete_project_confirmation: 54a4ee78867537e0244c7170453cdb3f
|
||||
@@ -745,7 +764,7 @@ checksums:
|
||||
environments/project/general/project_deleted_successfully: dbedf0f0739b822f3951de4aeb2fc26f
|
||||
environments/project/general/project_name_settings_description: 079c6380ad539543a9aa8772bc1b0fa2
|
||||
environments/project/general/project_name_updated_successfully: f95f70f4a49d451dc0441a51d05a3aa3
|
||||
environments/project/general/recontact_waiting_time: 9c5ebb18960dec73def053de89e63272
|
||||
environments/project/general/recontact_waiting_time: 0566dc710b4b9644e276e311b419c4c0
|
||||
environments/project/general/recontact_waiting_time_settings_description: 8922cde1f95777f9a2747fb4bed57ab5
|
||||
environments/project/general/this_action_cannot_be_undone: 3d8b13374ffd3cefc0f3f7ce077bd9c9
|
||||
environments/project/general/wait_x_days_before_showing_next_survey: d96228788d32ec23dc0d8c8ba77150a6
|
||||
@@ -808,7 +827,6 @@ checksums:
|
||||
environments/project/tags/add_tag: 2cfa04ceea966149f2b5d40d9c131141
|
||||
environments/project/tags/count: 9c5848662eb8024ddf360f7e4001a968
|
||||
environments/project/tags/delete_tag_confirmation: a9fb98064cd156242899643f3d2ef032
|
||||
environments/project/tags/empty_message: da71bd7c7b5bf634469d20e010d25503
|
||||
environments/project/tags/manage_tags: 2761d558b82b6104befbc240ae2379c6
|
||||
environments/project/tags/manage_tags_description: ce7cc42da3646fba960502d7e4e49cd2
|
||||
environments/project/tags/merge: 95051c859b8778be51226b43be6f1075
|
||||
@@ -915,15 +933,12 @@ checksums:
|
||||
environments/settings/billing/manage_subscription: 31cafd367fc70d656d8dd979d537dc96
|
||||
environments/settings/billing/monthly: 818f1192e32bb855597f930d3e78806e
|
||||
environments/settings/billing/monthly_identified_users: 0795735f6b241d31edac576a77dd7e55
|
||||
environments/settings/billing/per_month: 64e96490ee2d7811496cf04adae30aa4
|
||||
environments/settings/billing/per_year: bf02408d157486e53c15a521a5645617
|
||||
environments/settings/billing/plan_upgraded_successfully: 52e2a258cc9ca8a512c288bf6f18cf37
|
||||
environments/settings/billing/premium_support_with_slas: 2e33d4442c16bfececa6cae7b2081e5d
|
||||
environments/settings/billing/remove_branding: 88b6b818750e478bfa153b33dd658280
|
||||
environments/settings/billing/startup: 4c4ac5a0b9dc62100bca6c6465f31c4c
|
||||
environments/settings/billing/startup_description: 964fcb2c77f49b80266c94606e3f4506
|
||||
environments/settings/billing/switch_plan: fb3e1941051a4273ca29224803570f4b
|
||||
environments/settings/billing/switch_plan_confirmation_text: 910a6df56964619975c6ed5651a55db7
|
||||
environments/settings/billing/team_access_roles: 1cc4af14e589f6c09ab92a4f21958049
|
||||
environments/settings/billing/unable_to_upgrade_plan: 50fc725609411d139e534c85eeb2879e
|
||||
environments/settings/billing/unlimited_miu: 29c3f5bd01c2a09fdf1d3601665ce90f
|
||||
@@ -1144,7 +1159,6 @@ checksums:
|
||||
environments/surveys/edit/allow_multi_select: 7b4b83f7a0205e2a0a8971671a69a174
|
||||
environments/surveys/edit/allow_multiple_files: dbd99f9d1026e4f7c5a5d03f71ba379d
|
||||
environments/surveys/edit/allow_users_to_select_more_than_one_image: d683e0b538d1366400292a771f3fbd08
|
||||
environments/surveys/edit/always_show_survey: b0ae6a873ce2eeb0aea2e6d4cb04c540
|
||||
environments/surveys/edit/and_launch_surveys_in_your_website_or_app: a3edcdb4aea792a27d90aad1930f001a
|
||||
environments/surveys/edit/animation: 66a18eacfb92fc9fc9db188d2dde4f81
|
||||
environments/surveys/edit/app_survey_description: bdfacfce478e97f70b700a1382dfa687
|
||||
@@ -1227,8 +1241,7 @@ checksums:
|
||||
environments/surveys/edit/custom_hostname: bc2b1c8de3f9b8ef145b45aeba6ab429
|
||||
environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f
|
||||
environments/surveys/edit/date_format: e95dfc41ac944874868487457ddc057a
|
||||
environments/surveys/edit/days_before_showing_this_survey_again: 8b4623eab862615fa60064400008eb23
|
||||
environments/surveys/edit/decide_how_often_people_can_answer_this_survey: 58427b0f0a7a258c24fa2acd9913e95e
|
||||
environments/surveys/edit/days_before_showing_this_survey_again: 354fb28c5ff076f022d82a20c749ee46
|
||||
environments/surveys/edit/delete_choice: fd750208d414b9ad8c980c161a0199e1
|
||||
environments/surveys/edit/disable_the_visibility_of_survey_progress: 2af631010114307ac2a91612559c9618
|
||||
environments/surveys/edit/display_an_estimate_of_completion_time_for_survey: 03f0a816569399c1c61d08dbc913de06
|
||||
@@ -1243,7 +1256,7 @@ checksums:
|
||||
environments/surveys/edit/edit_link: 40ba9e15beac77a46c5baf30be84ac54
|
||||
environments/surveys/edit/edit_recall: 38a4a7378d02453e35d06f2532eef318
|
||||
environments/surveys/edit/edit_translations: 2b21bea4b53e88342559272701e9fbf3
|
||||
environments/surveys/edit/enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey: 71977f91ec151b61ee3528ac2618afed
|
||||
environments/surveys/edit/enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey: c70466147d49dcbb3686452f35c46428
|
||||
environments/surveys/edit/enable_recaptcha_to_protect_your_survey_from_spam: 4483a5763718d201ac97caa1e1216e13
|
||||
environments/surveys/edit/enable_spam_protection: e1fb0dd0723044bf040b92d8fc58015d
|
||||
environments/surveys/edit/end_screen_card: 6146c2bcb87291e25ecb03abd2d9a479
|
||||
@@ -1256,9 +1269,9 @@ checksums:
|
||||
environments/surveys/edit/equals_one_of: 369a451add4b79bc003f952f0e1bfcc9
|
||||
environments/surveys/edit/error_publishing_survey: bf9fab1d8ea7132a2e9b4b7b09f18b1f
|
||||
environments/surveys/edit/error_saving_changes: b75aa9e4e42e1d43c8f9c33c2b7dc9a7
|
||||
environments/surveys/edit/even_after_they_submitted_a_response_e_g_feedback_box: c6668f9cf127fd922bec695dc548fe12
|
||||
environments/surveys/edit/even_after_they_submitted_a_response_e_g_feedback_box: 7b99f30397dcde76f65e1ab64bdbd113
|
||||
environments/surveys/edit/everyone: 2112aa71b568773e8e8a792c63f4d413
|
||||
environments/surveys/edit/external_urls_paywall_tooltip: 0dbb62557e8a6fa817f0e74709eeb3d2
|
||||
environments/surveys/edit/external_urls_paywall_tooltip: a8860ff0a2ad5f283bc0becba374cd54
|
||||
environments/surveys/edit/fallback_missing: 43dbedbe1a178d455e5f80783a7b6722
|
||||
environments/surveys/edit/fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first: ad4afe2980e1dfeffb20aa78eb892350
|
||||
environments/surveys/edit/fieldId_is_used_in_quota_please_remove_it_from_quota_first: 374c563964fc805ab0b8974e781687d9
|
||||
@@ -1327,8 +1340,9 @@ checksums:
|
||||
environments/surveys/edit/hostname: 9bdaa7692869999df51bb60d58d9ef62
|
||||
environments/surveys/edit/how_funky_do_you_want_your_cards_in_survey_type_derived_surveys: 3cb16b37510c01af20a80f51b598346e
|
||||
environments/surveys/edit/if_you_need_more_please: a7d208c283caf6b93800b809fca80768
|
||||
environments/surveys/edit/if_you_really_want_that_answer_ask_until_you_get_it: 33f0320ec85067a06198a841348e9fc6
|
||||
environments/surveys/edit/ignore_waiting_time_between_surveys: 8145b6aef535fde5ee54dea63e66f64a
|
||||
environments/surveys/edit/if_you_really_want_that_answer_ask_until_you_get_it: 31c18a8c7c578db2ba49eed663d1739f
|
||||
environments/surveys/edit/ignore_global_waiting_time: 1e7f1465aeb6d26c325ad7f135b207a8
|
||||
environments/surveys/edit/ignore_global_waiting_time_description: 37d173a4d537622de40677389238d859
|
||||
environments/surveys/edit/image: 048ba7a239de0fbd883ade8558415830
|
||||
environments/surveys/edit/includes_all_of: ec72f90c0839d4c3bb518deb03894031
|
||||
environments/surveys/edit/includes_one_of: 6d5be5d7c2494179e88bd7302b247884
|
||||
@@ -1395,9 +1409,10 @@ checksums:
|
||||
environments/surveys/edit/optional: 396fb9a0472daf401c392bdc3e248943
|
||||
environments/surveys/edit/options: 59156082418d80acb211f973b1218f11
|
||||
environments/surveys/edit/override_theme_with_individual_styles_for_this_survey: edffc97f5d3372419fe0444de0a5aa3f
|
||||
environments/surveys/edit/overwrite_global_waiting_time: 7bc23bd502b6bd048356b67acd956d9d
|
||||
environments/surveys/edit/overwrite_global_waiting_time_description: 795cf6e93d4c01d2e43aa0ebab601c6e
|
||||
environments/surveys/edit/overwrite_placement: d7278be243e52c5091974e0fc4a7c342
|
||||
environments/surveys/edit/overwrite_the_global_placement_of_the_survey: 874075712254b1ce92e099d89f675a48
|
||||
environments/surveys/edit/overwrites_waiting_period_between_surveys_to_x_days: 8d5596b024cbe8c82b021dcf6c73ba05
|
||||
environments/surveys/edit/pick_a_background_from_our_library_or_upload_your_own: b83bcbdc8131fc9524d272ff5dede754
|
||||
environments/surveys/edit/picture_idx: 55e053ad1ade5d17c582406706036028
|
||||
environments/surveys/edit/pin_can_only_contain_numbers: 417c854d44620a7229ebd9ab8cbb3613
|
||||
@@ -1454,7 +1469,8 @@ checksums:
|
||||
environments/surveys/edit/range: 1fad969ecf3de1c21df046b93053c422
|
||||
environments/surveys/edit/recall_data: 39beabd626c0af15316885cff5d5d9b8
|
||||
environments/surveys/edit/recall_information_from: 884cfd143456fab1a91f0744cc92f0c8
|
||||
environments/surveys/edit/recontact_options: 0f570378a531da60448fde37abd50214
|
||||
environments/surveys/edit/recontact_options_section: 57a23e1bcab6baa484b27b615e6c906a
|
||||
environments/surveys/edit/recontact_options_section_description: 1e04011440c339a3b5cfff12d55b7f12
|
||||
environments/surveys/edit/redirect_thank_you_card: 09f721c4b62e2584e40a53507092ea83
|
||||
environments/surveys/edit/redirect_to_url: f17d726bbc3391561447b3f4010635cf
|
||||
environments/surveys/edit/remove_description: b52de820b4bbcb354eb62246c4112a9a
|
||||
@@ -1463,6 +1479,8 @@ checksums:
|
||||
environments/surveys/edit/required: 04d7fb6f37ffe0a6ca97d49e2a8b6eb5
|
||||
environments/surveys/edit/reset_to_theme_styles: f9edc3970ec23d6c4d2d7accc292ef3a
|
||||
environments/surveys/edit/reset_to_theme_styles_main_text: d86fb2213d3b2efbd0361526dc6cb27b
|
||||
environments/surveys/edit/respect_global_waiting_time: 850e7e64ec890c591b2d07741ef26e11
|
||||
environments/surveys/edit/respect_global_waiting_time_description: 5235fee102d619cb391c5aa2c75b61be
|
||||
environments/surveys/edit/response_limit_can_t_be_set_to_0: 278664873ee3b1046dbcb58848efc12a
|
||||
environments/surveys/edit/response_limit_needs_to_exceed_number_of_received_responses: 9a9c223c0918ded716ddfaa84fbaa8d9
|
||||
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
|
||||
@@ -1487,7 +1505,7 @@ checksums:
|
||||
environments/surveys/edit/show_advanced_settings: b6f5bbbb84f34e51cd72ccd332e9613e
|
||||
environments/surveys/edit/show_button: 6b364aac9d7ac71f34a438607c9693bc
|
||||
environments/surveys/edit/show_language_switch: b6915a7f26d7079f2d4d844d74440413
|
||||
environments/surveys/edit/show_multiple_times: 5e6e0244c20feca78723c79aa1ddcf62
|
||||
environments/surveys/edit/show_multiple_times: 05239c532c9c05ef5d2990ba6ce12f60
|
||||
environments/surveys/edit/show_only_once: 31858baf60ebcf193c7e35d9084af0af
|
||||
environments/surveys/edit/show_survey_maximum_of: 721ed61b01a9fc8ce4becb72823bb72e
|
||||
environments/surveys/edit/show_survey_to_users: d5e90fd17babfea978fce826e9df89b0
|
||||
@@ -1517,13 +1535,12 @@ checksums:
|
||||
environments/surveys/edit/switch_multi_lanugage_on_to_get_started: d2ca06684af26bd6b5121a4656bb6458
|
||||
environments/surveys/edit/targeted: ca615f1fc3b490d5a2187b27fb4a2073
|
||||
environments/surveys/edit/ten_points: a1317b82003859f77fb3138c55450d63
|
||||
environments/surveys/edit/the_survey_will_be_shown_multiple_times_until_they_respond: 219b15081cbafaa391e266bd2cc4c9d4
|
||||
environments/surveys/edit/the_survey_will_be_shown_once_even_if_person_doesnt_respond: c145b7be481ae1fe6f66298d9a5cf838
|
||||
environments/surveys/edit/the_survey_will_be_shown_multiple_times_until_they_respond: 2d8d7d2351bd7533eb3788cce228c654
|
||||
environments/surveys/edit/the_survey_will_be_shown_once_even_if_person_doesnt_respond: 6062aaa5cf8e58e79b75b6b588ae9598
|
||||
environments/surveys/edit/then: 5e941fb7dd51a18651fcfb865edd5ba6
|
||||
environments/surveys/edit/this_action_will_remove_all_the_translations_from_this_survey: 3340c89696f10bdc01b9a1047ff0b987
|
||||
environments/surveys/edit/this_extension_is_already_added: 201d636539836c95958e28cecd8f3240
|
||||
environments/surveys/edit/this_file_type_is_not_supported: f365b9a2e05aa062ab0bc1af61f642e2
|
||||
environments/surveys/edit/this_setting_overwrites_your: 6f980149a5a4adc2cfe3dac4f367e7e5
|
||||
environments/surveys/edit/three_points: d7f299aec752d7d690ef0ab6373327ae
|
||||
environments/surveys/edit/times: 5ab156c13df6bfd75c0b17ad0a92c78a
|
||||
environments/surveys/edit/to_keep_the_placement_over_all_surveys_consistent_you_can: 7a078e6a39d4c30b465137d2b6ef3e67
|
||||
@@ -1534,7 +1551,7 @@ checksums:
|
||||
environments/surveys/edit/unlock_targeting_description: 8e315dc41c2849754839a1460643c5fb
|
||||
environments/surveys/edit/unlock_targeting_title: 6098caf969cac64cd54e217471ae42d4
|
||||
environments/surveys/edit/unsaved_changes_warning: a164f276c9f7344022aa4640b32abcf9
|
||||
environments/surveys/edit/until_they_submit_a_response: c980c520f5b5883ed46f2e1c006082b5
|
||||
environments/surveys/edit/until_they_submit_a_response: 2a0fd5dcc6cc40a72ed9b974f22eaf68
|
||||
environments/surveys/edit/upgrade_notice_description: 32b66a4f257ad8d38bc38dcc95fe23c4
|
||||
environments/surveys/edit/upgrade_notice_title: 40866066ebc558ad0c92a4f19f12090c
|
||||
environments/surveys/edit/upload: 4a6c84aa16db0f4e5697f49b45257bc7
|
||||
@@ -1542,7 +1559,6 @@ checksums:
|
||||
environments/surveys/edit/upper_label: 1fa48bce3fade6ffc1a52d9fdddf9e17
|
||||
environments/surveys/edit/url_filters: e524879d2eb74463d7fd06a7e0f53421
|
||||
environments/surveys/edit/url_not_supported: af8a753467c617b596aadef1aaaed664
|
||||
environments/surveys/edit/use_with_caution: 7c35d3ad68dd001e53cbd9d57c96af91
|
||||
environments/surveys/edit/variable_is_used_in_logic_of_question_please_remove_it_from_logic_first: bd9d9c7cf0be671c4e8cf67e2ae6659e
|
||||
environments/surveys/edit/variable_is_used_in_quota_please_remove_it_from_quota_first: 0d36e5b2713f5450fe346e0af0aaa29c
|
||||
environments/surveys/edit/variable_name_is_already_taken_please_choose_another: 6da42fe8733c6379158bce9a176f76d7
|
||||
@@ -1552,11 +1568,13 @@ checksums:
|
||||
environments/surveys/edit/variable_used_in_recall_welcome: 60321b2f40ae01cd10f99ed77bb986ba
|
||||
environments/surveys/edit/verify_email_before_submission: c05d345dc35f2d33839e4cfd72d11eb2
|
||||
environments/surveys/edit/verify_email_before_submission_description: 434ab3ee6134367513b633a9d4f7d772
|
||||
environments/surveys/edit/visibility_and_recontact: c27cb4ff3a4262266902a335c3ad5d84
|
||||
environments/surveys/edit/visibility_and_recontact_description: 2969ab679e1f6111dd96e95cee26e219
|
||||
environments/surveys/edit/wait: 014d18ade977bf08d75b995076596708
|
||||
environments/surveys/edit/wait_a_few_seconds_after_the_trigger_before_showing_the_survey: 13d5521cf73be5afeba71f5db5847919
|
||||
environments/surveys/edit/waiting_period: 21775d12b2cb831134b1f47450eaf1f3
|
||||
environments/surveys/edit/waiting_time_across_surveys: 5c5a7653d797c86c4008f13a40434ad8
|
||||
environments/surveys/edit/waiting_time_across_surveys_description: 1bbee2fee49f842056547c336f8fd788
|
||||
environments/surveys/edit/welcome_message: 986a434e3895c8ee0b267df95cc40051
|
||||
environments/surveys/edit/when_conditions_match_waiting_time_will_be_ignored_and_survey_shown: e7fe9c56664da4670e52e38656d8705d
|
||||
environments/surveys/edit/without_a_filter_all_of_your_users_can_be_surveyed: 451990569c61f25d01044cc45b1ce122
|
||||
environments/surveys/edit/you_have_not_created_a_segment_yet: c6658bd1cee9c5c957c675db044708dd
|
||||
environments/surveys/edit/you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations: b12b28699e02ff9ba69bcbae838ba5da
|
||||
@@ -1597,7 +1615,7 @@ checksums:
|
||||
environments/surveys/responses/last_name: 2c9a7de7738ca007ba9023c385149c26
|
||||
environments/surveys/responses/not_completed: df34eab65a6291f2c5e15a0e349c4eba
|
||||
environments/surveys/responses/os: a4c753bb2c004a58d02faeed6b4da476
|
||||
environments/surveys/responses/person_attributes: 8f7f8a9040ce8efb3cb54ce33b590866
|
||||
environments/surveys/responses/person_attributes: 07ae67ae73d7a2a7c67008694a83f0a3
|
||||
environments/surveys/responses/phone: b9537ee90fc5b0116942e0af29d926cc
|
||||
environments/surveys/responses/respondent_skipped_questions: d85daf579ade534dc7e639689156fcd5
|
||||
environments/surveys/responses/response_deleted_successfully: 6cec5427c271800619fee8c812d7db18
|
||||
@@ -1692,6 +1710,7 @@ checksums:
|
||||
environments/surveys/share/social_media/title: 1bf4899b063ee8f02f7188576555828b
|
||||
environments/surveys/summary/added_filter_for_responses_where_answer_to_question: 5bddf0d4f771efd06d58441d11fa5091
|
||||
environments/surveys/summary/added_filter_for_responses_where_answer_to_question_is_skipped: 74ca713c491cfc33751a5db3de972821
|
||||
environments/surveys/summary/aggregated: 9d4e77225d5952abed414fffd828c078
|
||||
environments/surveys/summary/all_responses_csv: 16c0c211853f0839a79f1127ec679ca2
|
||||
environments/surveys/summary/all_responses_excel: 8bf18916ab127f16bfcf9f38956710b0
|
||||
environments/surveys/summary/all_time: 62258944e7c2e83f3ebf69074b2c2156
|
||||
@@ -1715,7 +1734,6 @@ checksums:
|
||||
environments/surveys/summary/filtered_responses_csv: aad66a98be6a09cac8bef9e4db4a75cf
|
||||
environments/surveys/summary/filtered_responses_excel: 06e57bae9e41979fd7fc4b8bfe3466f9
|
||||
environments/surveys/summary/generating_qr_code: 5026d4a76f995db458195e5215d9bbd9
|
||||
environments/surveys/summary/go_to_setup_checklist: d70bd018d651d01c41ae10370e71d0be
|
||||
environments/surveys/summary/impressions: 7fe38d42d68a64d3fd8436a063751584
|
||||
environments/surveys/summary/impressions_tooltip: 4d0823cbf360304770c7c5913e33fdc8
|
||||
environments/surveys/summary/in_app/connection_description: 9710bbf8048a8a5c3b2b56db9d946b73
|
||||
@@ -1747,7 +1765,7 @@ checksums:
|
||||
environments/surveys/summary/in_app/title: a2d1b633244d0e0504ec6f8f561c7a6b
|
||||
environments/surveys/summary/includes_all: b0e3679282417c62d511c258362f860e
|
||||
environments/surveys/summary/includes_either: 186d6923c1693e80d7b664b8367d4221
|
||||
environments/surveys/summary/install_widget: 55d403de32e3d0da7513ab199f1d1934
|
||||
environments/surveys/summary/individual: 52ebce389ed97a13b6089802055ed667
|
||||
environments/surveys/summary/is_equal_to: f4aab30ef188eb25dcc0e392cf8e86bb
|
||||
environments/surveys/summary/is_less_than: 6109d595ba21497c59b1c91d7fd09a13
|
||||
environments/surveys/summary/last_30_days: a738894cfc5e592052f1e16787744568
|
||||
@@ -1760,6 +1778,7 @@ checksums:
|
||||
environments/surveys/summary/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
||||
environments/surveys/summary/other_values_found: 48a74ee68c05f7fb162072b50c683b6a
|
||||
environments/surveys/summary/overall: 6c6d6533013d4739766af84b2871bca6
|
||||
environments/surveys/summary/promoters: 41fbb8d0439227661253a82fda39f521
|
||||
environments/surveys/summary/qr_code: 48cb2a8c07a3d1647f766f93bb9e9382
|
||||
environments/surveys/summary/qr_code_description: 19f48dcf473809f178abf4212657ef14
|
||||
environments/surveys/summary/qr_code_download_failed: 2764b5615112800da27eecafc21e3472
|
||||
@@ -1769,6 +1788,7 @@ checksums:
|
||||
environments/surveys/summary/quotas_completed_tooltip: ec5c4dc67eda27c06764354f695db613
|
||||
environments/surveys/summary/reset_survey: 8c88ddb81f5f787d183d2e7cb43e7c64
|
||||
environments/surveys/summary/reset_survey_warning: 6b44be171d7e2716f234387b100b173d
|
||||
environments/surveys/summary/satisfied: 4d542ba354b85e644acbca5691d2ce45
|
||||
environments/surveys/summary/selected_responses_csv: 9cef3faccd54d4f24647791e6359db90
|
||||
environments/surveys/summary/selected_responses_excel: a0ade8b2658e887a4a3f2ad3bdb0c686
|
||||
environments/surveys/summary/setup_integrations: 70de06d73be671a0cd58a3fd4fa62e53
|
||||
@@ -1784,7 +1804,6 @@ checksums:
|
||||
environments/surveys/summary/ttc_tooltip: 9b1cbe32cc81111314bd3b6fd050c2e7
|
||||
environments/surveys/summary/unknown_question_type: e4152a7457d2b94f48dcc70aaba9922f
|
||||
environments/surveys/summary/use_personal_links: da2b3e7e1aaf2ea2bd4efed2dda4247c
|
||||
environments/surveys/summary/waiting_for_response: 0194a84e0850b8e98435632d5331a916
|
||||
environments/surveys/summary/whats_next: d920145bfa2147014062f6f2d1d451a4
|
||||
environments/surveys/summary/your_survey_is_public: 3f5cb5949a5f4020a3d4d74fdfc95e83
|
||||
environments/surveys/summary/youre_not_plugged_in_yet: 9217467742cdcf7edf8d59cc1472ede6
|
||||
|
||||
@@ -19,8 +19,7 @@ export const ENCRYPTION_KEY = env.ENCRYPTION_KEY;
|
||||
// Other
|
||||
export const CRON_SECRET = env.CRON_SECRET;
|
||||
export const DEFAULT_BRAND_COLOR = "#64748b";
|
||||
export const FB_LOGO_URL =
|
||||
"https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png";
|
||||
export const FB_LOGO_URL = `${WEBAPP_URL}/logo-transparent.png`;
|
||||
|
||||
export const PRIVACY_URL = env.PRIVACY_URL;
|
||||
export const TERMS_URL = env.TERMS_URL;
|
||||
@@ -170,11 +169,13 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [
|
||||
"de-DE",
|
||||
"pt-BR",
|
||||
"fr-FR",
|
||||
"nl-NL",
|
||||
"zh-Hant-TW",
|
||||
"pt-PT",
|
||||
"ro-RO",
|
||||
"ja-JP",
|
||||
"zh-Hans-CN",
|
||||
"es-ES",
|
||||
];
|
||||
|
||||
// Billing constants
|
||||
@@ -217,10 +218,6 @@ export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY;
|
||||
export const INTERCOM_APP_ID = env.INTERCOM_APP_ID;
|
||||
export const IS_INTERCOM_CONFIGURED = Boolean(env.INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
|
||||
|
||||
export const POSTHOG_API_KEY = env.POSTHOG_API_KEY;
|
||||
export const POSTHOG_API_HOST = env.POSTHOG_API_HOST;
|
||||
export const IS_POSTHOG_CONFIGURED = Boolean(POSTHOG_API_KEY && POSTHOG_API_HOST);
|
||||
|
||||
export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;
|
||||
export const TURNSTILE_SITE_KEY = env.TURNSTILE_SITE_KEY;
|
||||
export const IS_TURNSTILE_CONFIGURED = Boolean(env.TURNSTILE_SITE_KEY && TURNSTILE_SECRET_KEY);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
// During build time, we only need valid-format URLs for Prisma to generate the client.
|
||||
// Actual connectivity and secrets are validated at runtime by the startup script.
|
||||
const isBuildTime = process.env.NEXT_PHASE === "phase-production-build";
|
||||
|
||||
export const env = createEnv({
|
||||
/*
|
||||
* Serverside Environment variables, not available on the client.
|
||||
@@ -14,14 +18,21 @@ export const env = createEnv({
|
||||
CRON_SECRET: z.string().optional(),
|
||||
BREVO_API_KEY: z.string().optional(),
|
||||
BREVO_LIST_ID: z.string().optional(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
DATABASE_URL: isBuildTime
|
||||
? z
|
||||
.string()
|
||||
.optional()
|
||||
.default("postgresql://formbricks:formbricks@localhost:5432/formbricks?schema=public")
|
||||
: z.string().url(),
|
||||
DEBUG: z.enum(["1", "0"]).optional(),
|
||||
AUTH_DEFAULT_TEAM_ID: z.string().optional(),
|
||||
AUTH_SKIP_INVITE_FOR_SSO: z.enum(["1", "0"]).optional(),
|
||||
E2E_TESTING: z.enum(["1", "0"]).optional(),
|
||||
EMAIL_AUTH_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
ENCRYPTION_KEY: z.string(),
|
||||
ENCRYPTION_KEY: isBuildTime
|
||||
? z.string().optional().default("0000000000000000000000000000000000000000000000000000000000000000")
|
||||
: z.string(),
|
||||
ENTERPRISE_LICENSE_KEY: z.string().optional(),
|
||||
GITHUB_ID: z.string().optional(),
|
||||
GITHUB_SECRET: z.string().optional(),
|
||||
@@ -54,13 +65,12 @@ export const env = createEnv({
|
||||
OIDC_ISSUER: z.string().optional(),
|
||||
OIDC_SIGNING_ALGORITHM: z.string().optional(),
|
||||
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
|
||||
REDIS_URL:
|
||||
process.env.NODE_ENV === "test"
|
||||
REDIS_URL: isBuildTime
|
||||
? z.string().optional().default("redis://localhost:6379")
|
||||
: process.env.NODE_ENV === "test"
|
||||
? z.string().optional()
|
||||
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
|
||||
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
POSTHOG_API_HOST: z.string().optional(),
|
||||
POSTHOG_API_KEY: z.string().optional(),
|
||||
PRIVACY_URL: z
|
||||
.string()
|
||||
.url()
|
||||
@@ -103,7 +113,6 @@ export const env = createEnv({
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
TELEMETRY_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
TERMS_URL: z
|
||||
.string()
|
||||
.url()
|
||||
@@ -172,8 +181,6 @@ export const env = createEnv({
|
||||
MAIL_FROM_NAME: process.env.MAIL_FROM_NAME,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
SENTRY_DSN: process.env.SENTRY_DSN,
|
||||
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY,
|
||||
POSTHOG_API_HOST: process.env.POSTHOG_API_HOST,
|
||||
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
|
||||
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
||||
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
|
||||
@@ -206,7 +213,6 @@ export const env = createEnv({
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
PUBLIC_URL: process.env.PUBLIC_URL,
|
||||
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED,
|
||||
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
|
||||
TURNSTILE_SITE_KEY: process.env.TURNSTILE_SITE_KEY,
|
||||
RECAPTCHA_SITE_KEY: process.env.RECAPTCHA_SITE_KEY,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from "@formbricks/types/environment";
|
||||
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
|
||||
import { getOrganizationsByUserId } from "../organization/service";
|
||||
import { capturePosthogEnvironmentEvent } from "../posthogServer";
|
||||
import { getUserProjects } from "../project/service";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
@@ -173,10 +172,6 @@ export const createEnvironment = async (
|
||||
},
|
||||
});
|
||||
|
||||
await capturePosthogEnvironmentEvent(environment.id, "environment created", {
|
||||
environmentType: environment.type,
|
||||
});
|
||||
|
||||
return environment;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -137,6 +137,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Engleză (SUA)",
|
||||
"ja-JP": "英語(米国)",
|
||||
"zh-Hans-CN": "英语(美国)",
|
||||
"nl-NL": "Engels (VS)",
|
||||
"es-ES": "Inglés (EE.UU.)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -151,6 +153,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Germană",
|
||||
"ja-JP": "ドイツ語",
|
||||
"zh-Hans-CN": "德语",
|
||||
"nl-NL": "Duits",
|
||||
"es-ES": "Alemán",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -165,6 +169,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Portugheză (Brazilia)",
|
||||
"ja-JP": "ポルトガル語(ブラジル)",
|
||||
"zh-Hans-CN": "葡萄牙语(巴西)",
|
||||
"nl-NL": "Portugees (Brazilië)",
|
||||
"es-ES": "Portugués (Brasil)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -179,6 +185,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Franceză",
|
||||
"ja-JP": "フランス語",
|
||||
"zh-Hans-CN": "法语",
|
||||
"nl-NL": "Frans",
|
||||
"es-ES": "Francés",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -193,6 +201,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Chineză (Tradicională)",
|
||||
"ja-JP": "中国語(繁体字)",
|
||||
"zh-Hans-CN": "繁体中文",
|
||||
"nl-NL": "Chinees (Traditioneel)",
|
||||
"es-ES": "Chino (Tradicional)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -207,6 +217,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Portugheză (Portugalia)",
|
||||
"ja-JP": "ポルトガル語(ポルトガル)",
|
||||
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
|
||||
"nl-NL": "Portugees (Portugal)",
|
||||
"es-ES": "Portugués (Portugal)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -221,6 +233,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Română",
|
||||
"ja-JP": "ルーマニア語",
|
||||
"zh-Hans-CN": "罗马尼亚语",
|
||||
"nl-NL": "Roemeens",
|
||||
"es-ES": "Rumano",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -235,6 +249,8 @@ export const appLanguages = [
|
||||
"ro-RO": "Japoneză",
|
||||
"ja-JP": "日本語",
|
||||
"zh-Hans-CN": "日语",
|
||||
"nl-NL": "Japans",
|
||||
"es-ES": "Japonés",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -249,6 +265,40 @@ export const appLanguages = [
|
||||
"ro-RO": "Chineză (Simplificată)",
|
||||
"ja-JP": "中国語(簡体字)",
|
||||
"zh-Hans-CN": "简体中文",
|
||||
"nl-NL": "Chinees (Vereenvoudigd)",
|
||||
"es-ES": "Chino (Simplificado)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "nl-NL",
|
||||
label: {
|
||||
"en-US": "Dutch",
|
||||
"de-DE": "Niederländisch",
|
||||
"pt-BR": "Holandês",
|
||||
"fr-FR": "Néerlandais",
|
||||
"zh-Hant-TW": "荷蘭語",
|
||||
"pt-PT": "Holandês",
|
||||
"ro-RO": "Olandeză",
|
||||
"ja-JP": "オランダ語",
|
||||
"zh-Hans-CN": "荷兰语",
|
||||
"nl-NL": "Nederlands",
|
||||
"es-ES": "Neerlandés",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "es-ES",
|
||||
label: {
|
||||
"en-US": "Spanish",
|
||||
"de-DE": "Spanisch",
|
||||
"pt-BR": "Espanhol",
|
||||
"fr-FR": "Espagnol",
|
||||
"zh-Hant-TW": "西班牙語",
|
||||
"pt-PT": "Espanhol",
|
||||
"ro-RO": "Spaniol",
|
||||
"ja-JP": "スペイン語",
|
||||
"zh-Hans-CN": "西班牙语",
|
||||
"nl-NL": "Spaans",
|
||||
"es-ES": "Español",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { PostHog } from "posthog-node";
|
||||
import { createCacheKey } from "@formbricks/cache";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TOrganizationBillingPlan, TOrganizationBillingPlanLimits } from "@formbricks/types/organizations";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { IS_POSTHOG_CONFIGURED, IS_PRODUCTION, POSTHOG_API_HOST, POSTHOG_API_KEY } from "./constants";
|
||||
|
||||
const enabled = IS_PRODUCTION && IS_POSTHOG_CONFIGURED;
|
||||
|
||||
export const capturePosthogEnvironmentEvent = async (
|
||||
environmentId: string,
|
||||
eventName: string,
|
||||
properties: any = {}
|
||||
) => {
|
||||
if (!enabled || typeof POSTHOG_API_HOST !== "string" || typeof POSTHOG_API_KEY !== "string") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const client = new PostHog(POSTHOG_API_KEY, {
|
||||
host: POSTHOG_API_HOST,
|
||||
});
|
||||
client.capture({
|
||||
// workaround with a static string as exaplained in PostHog docs: https://posthog.com/docs/product-analytics/group-analytics
|
||||
distinctId: "environmentEvents",
|
||||
event: eventName,
|
||||
groups: { environment: environmentId },
|
||||
properties,
|
||||
});
|
||||
await client.shutdown();
|
||||
} catch (error) {
|
||||
logger.error(error, "error sending posthog event");
|
||||
}
|
||||
};
|
||||
|
||||
export const sendPlanLimitsReachedEventToPosthogWeekly = async (
|
||||
environmentId: string,
|
||||
billing: {
|
||||
plan: TOrganizationBillingPlan;
|
||||
limits: TOrganizationBillingPlanLimits;
|
||||
}
|
||||
) =>
|
||||
await cache.withCache(
|
||||
async () => {
|
||||
try {
|
||||
await capturePosthogEnvironmentEvent(environmentId, "plan limit reached", {
|
||||
...billing,
|
||||
});
|
||||
return "success";
|
||||
} catch (error) {
|
||||
logger.error(error, "error sending plan limits reached event to posthog weekly");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
createCacheKey.custom("analytics", environmentId, `plan_limits_${billing.plan}`),
|
||||
60 * 60 * 24 * 7 * 1000 // 7 days in milliseconds
|
||||
);
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
getOrganizationByEnvironmentId,
|
||||
subscribeOrganizationMembersToSurveyResponses,
|
||||
} from "@/lib/organization/service";
|
||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||
import { evaluateLogic } from "@/lib/surveyLogic/utils";
|
||||
import {
|
||||
mockActionClass,
|
||||
@@ -44,11 +43,6 @@ vi.mock("@/lib/organization/service", () => ({
|
||||
subscribeOrganizationMembersToSurveyResponses: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock posthogServer
|
||||
vi.mock("@/lib/posthogServer", () => ({
|
||||
capturePosthogEnvironmentEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock actionClass service
|
||||
vi.mock("@/lib/actionClass/service", () => ({
|
||||
getActionClasses: vi.fn(),
|
||||
@@ -646,7 +640,6 @@ describe("Tests for createSurvey", () => {
|
||||
expect(prisma.survey.create).toHaveBeenCalled();
|
||||
expect(result.name).toEqual(mockSurveyOutput.name);
|
||||
expect(subscribeOrganizationMembersToSurveyResponses).toHaveBeenCalled();
|
||||
expect(capturePosthogEnvironmentEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("creates a private segment for app surveys", async () => {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/lib/organization/service";
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { ITEMS_PER_PAGE } from "../constants";
|
||||
import { capturePosthogEnvironmentEvent } from "../posthogServer";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { checkForInvalidImagesInQuestions, transformPrismaSurvey } from "./utils";
|
||||
|
||||
@@ -673,11 +672,6 @@ export const createSurvey = async (
|
||||
await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy, organization.id);
|
||||
}
|
||||
|
||||
await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
|
||||
surveyId: survey.id,
|
||||
surveyType: survey.type,
|
||||
});
|
||||
|
||||
return transformedSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/* We use this telemetry service to better understand how Formbricks is being used
|
||||
and how we can improve it. All data including the IP address is collected anonymously
|
||||
and we cannot trace anything back to you or your customers. If you still want to
|
||||
disable telemetry, set the environment variable TELEMETRY_DISABLED=1 */
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { IS_PRODUCTION } from "./constants";
|
||||
import { env } from "./env";
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
// We are using the hashed CRON_SECRET as the distinct identifier for the instance for telemetry.
|
||||
// The hash cannot be traced back to the original value or the instance itself.
|
||||
// This is to ensure that the telemetry data is anonymous but still unique to the instance.
|
||||
const getTelemetryId = (): string => {
|
||||
return crypto.createHash("sha256").update(env.CRON_SECRET).digest("hex");
|
||||
};
|
||||
|
||||
export const captureTelemetry = async (eventName: string, properties = {}) => {
|
||||
if (env.TELEMETRY_DISABLED !== "1" && IS_PRODUCTION) {
|
||||
try {
|
||||
await fetch("https://telemetry.formbricks.com/capture/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
api_key: "phc_SoIFUJ8b9ufDm0YOnoOxJf6PXyuHpO7N6RztxFdZTy", // NOSONAR // This is a public API key for telemetry and not a secret
|
||||
event: eventName,
|
||||
properties: {
|
||||
distinct_id: getTelemetryId(),
|
||||
...properties,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, "error sending telemetry");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatDistance, intlFormat } from "date-fns";
|
||||
import { de, enUS, fr, ja, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
|
||||
import { de, enUS, es, fr, ja, nl, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
export const convertDateString = (dateString: string | null) => {
|
||||
@@ -91,6 +91,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
||||
return ptBR;
|
||||
case "fr-FR":
|
||||
return fr;
|
||||
case "nl-NL":
|
||||
return nl;
|
||||
case "zh-Hant-TW":
|
||||
return zhTW;
|
||||
case "pt-PT":
|
||||
@@ -101,6 +103,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
||||
return ja;
|
||||
case "zh-Hans-CN":
|
||||
return zhCN;
|
||||
case "es-ES":
|
||||
return es;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ export const I18nProvider = ({ children, language, defaultLanguage }: I18nProvid
|
||||
initializeI18n();
|
||||
}, [locale, defaultLanguage]);
|
||||
|
||||
// Don't render children until i18n is ready to prevent hydration issues
|
||||
// Don't render children until i18n is ready to prevent race conditions
|
||||
if (!isReady) {
|
||||
return <div style={{ visibility: "hidden" }}>{children}</div>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "Filter löschen",
|
||||
"clear_selection": "Auswahl aufheben",
|
||||
"click": "Klick",
|
||||
"click_to_filter": "Klicken zum Filtern",
|
||||
"clicks": "Klicks",
|
||||
"close": "Schließen",
|
||||
"code": "Code",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "Maximale Anzahl an Anfragen erreicht. Bitte später erneut versuchen.",
|
||||
"error_rate_limit_title": "Rate Limit Überschritten",
|
||||
"expand_rows": "Zeilen erweitern",
|
||||
"failed_to_copy_to_clipboard": "Fehler beim Kopieren in die Zwischenablage",
|
||||
"failed_to_load_organizations": "Fehler beim Laden der Organisationen",
|
||||
"failed_to_load_projects": "Fehler beim Laden der Projekte",
|
||||
"finish": "Fertigstellen",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "Name",
|
||||
"gathering_responses": "Antworten sammeln",
|
||||
"general": "Allgemein",
|
||||
"generate": "Generieren",
|
||||
"go_back": "Geh zurück",
|
||||
"go_to_dashboard": "Zum Dashboard gehen",
|
||||
"hidden": "Versteckt",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "Benutzer-ID",
|
||||
"user_not_found": "Benutzer nicht gefunden",
|
||||
"variable": "Variable",
|
||||
"variable_ids": "Variablen-IDs",
|
||||
"variables": "Variablen",
|
||||
"verified_email": "Verifizierte E-Mail",
|
||||
"video": "Video",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "CSS-Klasse oder ID hinzufügen",
|
||||
"add_regular_expression_here": "Fügen Sie hier einen regulären Ausdruck hinzu",
|
||||
"add_url": "URL hinzufügen",
|
||||
"and": "UND",
|
||||
"click": "Klicken",
|
||||
"contains": "enthält",
|
||||
"create_action": "Aktion erstellen",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "Auf bestimmte Seiten beschränken",
|
||||
"matches_regex": "Entspricht Regex",
|
||||
"on_all_pages": "Auf allen Seiten",
|
||||
"or": "ODER",
|
||||
"page_filter": "Seitenfilter",
|
||||
"page_view": "Seitenansicht",
|
||||
"select_match_type": "Wähle den Spieltyp aus",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert",
|
||||
"delete_contact_confirmation": "Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren.",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn dieser Kontakt Antworten hat, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.}}",
|
||||
"generate_personal_link": "Persönlichen Link generieren",
|
||||
"generate_personal_link_description": "Wähle eine veröffentlichte Umfrage aus, um einen personalisierten Link für diesen Kontakt zu generieren.",
|
||||
"no_published_link_surveys_available": "Keine veröffentlichten Link-Umfragen verfügbar. Bitte veröffentliche zuerst eine Link-Umfrage.",
|
||||
"no_published_surveys": "Keine veröffentlichten Umfragen",
|
||||
"no_responses_found": "Keine Antworten gefunden",
|
||||
"not_provided": "Nicht angegeben",
|
||||
"personal_link_generated": "Persönlicher Link erfolgreich generiert",
|
||||
"personal_link_generated_but_clipboard_failed": "Persönlicher Link wurde generiert, konnte aber nicht in die Zwischenablage kopiert werden: {url}",
|
||||
"personal_survey_link": "Link zur persönlichen Umfrage",
|
||||
"please_select_a_survey": "Bitte wähle eine Umfrage aus",
|
||||
"search_contact": "Kontakt suchen",
|
||||
"select_a_survey": "Wähle eine Umfrage aus",
|
||||
"select_attribute": "Attribut auswählen",
|
||||
"unlock_contacts_description": "Verwalte Kontakte und sende gezielte Umfragen",
|
||||
"unlock_contacts_title": "Kontakte mit einem höheren Plan freischalten",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Aktualisieren",
|
||||
"upload_contacts_modal_pick_different_file": "Wähle eine andere Datei",
|
||||
"upload_contacts_modal_preview": "Hier ist eine Vorschau deiner Daten.",
|
||||
"upload_contacts_modal_upload_btn": "Kontakte hochladen"
|
||||
"upload_contacts_modal_upload_btn": "Kontakte hochladen",
|
||||
"upload_contacts_success": "Kontakte erfolgreich hochgeladen"
|
||||
},
|
||||
"formbricks_logo": "Formbricks-Logo",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "App-Verbindung",
|
||||
"app_connection_description": "Verbinde deine App mit Formbricks.",
|
||||
"cache_update_delay_description": "Wenn du Aktualisierungen an Umfragen, Kontakten, Aktionen oder anderen Daten vornimmst, kann es bis zu 5 Minuten dauern, bis diese Änderungen in deiner lokalen App, die das Formbricks SDK verwendet, angezeigt werden. Diese Verzögerung ist auf eine Einschränkung unseres aktuellen Caching-Systems zurückzuführen. Wir arbeiten aktiv an einer Überarbeitung des Cache und werden in Formbricks 4.0 eine Lösung veröffentlichen.",
|
||||
"cache_update_delay_title": "Änderungen werden aufgrund von Caching nach 5 Minuten angezeigt",
|
||||
"app_connection_description": "Verbinde deine App oder Website mit Formbricks.",
|
||||
"cache_update_delay_description": "Wenn Sie Aktualisierungen an Umfragen, Kontakten, Aktionen oder anderen Daten vornehmen, kann es bis zu 1 Minute dauern, bis diese Änderungen in Ihrer lokalen App, die das Formbricks SDK ausführt, erscheinen.",
|
||||
"cache_update_delay_title": "Änderungen werden aufgrund von Caching nach etwa 1 Minute angezeigt",
|
||||
"environment_id": "Deine Umgebungs-ID",
|
||||
"environment_id_description": "Diese ID identifiziert eindeutig diese Formbricks Umgebung.",
|
||||
"formbricks_sdk_connected": "Formbricks SDK ist verbunden",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK ist noch nicht verbunden.",
|
||||
"formbricks_sdk_not_connected_description": "Verbinde deine Website oder App mit Formbricks",
|
||||
"formbricks_sdk_not_connected_description": "Füge das Formbricks SDK zu deiner Website oder App hinzu, um sie mit Formbricks zu verbinden",
|
||||
"how_to_setup": "Wie einrichten",
|
||||
"how_to_setup_description": "Befolge diese Schritte, um das Formbricks Widget in deiner App einzurichten.",
|
||||
"receiving_data": "Daten werden empfangen 💃🕺",
|
||||
"recheck": "Erneut prüfen",
|
||||
"sdk_connection_details": "SDK-Verbindungsdetails",
|
||||
"sdk_connection_details_description": "Deine eindeutige Umgebungs-ID und SDK-Verbindungs-URL zur Integration von Formbricks mit deiner Anwendung.",
|
||||
"setup_alert_description": "Befolge dieses Schritt-für-Schritt-Tutorial, um deine App oder Website in weniger als 5 Minuten zu verbinden.",
|
||||
"setup_alert_title": "Wie man verbindet"
|
||||
"setup_alert_title": "Wie man verbindet",
|
||||
"webapp_url": "SDK-Verbindungs-URL"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Dies ist dein einziges Projekt, es kann nicht gelöscht werden. Erstelle zuerst ein neues Projekt.",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "Projekt erfolgreich gelöscht",
|
||||
"project_name_settings_description": "Ändere den Namen deines Projekts.",
|
||||
"project_name_updated_successfully": "Projektname erfolgreich aktualisiert",
|
||||
"recontact_waiting_time": "Wartezeit für erneuten Kontakt",
|
||||
"recontact_waiting_time": "Projektweite Wartezeit zwischen Umfragen",
|
||||
"recontact_waiting_time_settings_description": "Steuere, wie oft Nutzer in allen App-Umfragen eine Umfrage angezeigt bekommen können.",
|
||||
"this_action_cannot_be_undone": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"wait_x_days_before_showing_next_survey": "Warte X Tage, bevor die nächste Umfrage angezeigt wird:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "Tag hinzufügen",
|
||||
"count": "zählen",
|
||||
"delete_tag_confirmation": "Bist Du sicher, dass Du diesen Tag löschen möchtest?",
|
||||
"empty_message": "Markiere eine Antwort, um deine Liste der Tags hier zu finden.",
|
||||
"manage_tags": "Tags verwalten",
|
||||
"manage_tags_description": "Zusammenführen und Antwort-Tags entfernen.",
|
||||
"merge": "Zusammenführen",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"allow_multi_select": "Mehrfachauswahl erlauben",
|
||||
"allow_multiple_files": "Mehrere Dateien zulassen",
|
||||
"allow_users_to_select_more_than_one_image": "Erlaube Nutzern, mehr als ein Bild auszuwählen",
|
||||
"always_show_survey": "Umfrage immer anzeigen",
|
||||
"and_launch_surveys_in_your_website_or_app": "und Umfragen auf deiner Website oder App starten.",
|
||||
"animation": "Animation",
|
||||
"app_survey_description": "Bette eine Umfrage in deine Web-App oder Website ein, um Antworten zu sammeln.",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"custom_hostname": "Benutzerdefinierter Hostname",
|
||||
"darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.",
|
||||
"date_format": "Datumsformat",
|
||||
"days_before_showing_this_survey_again": "Tage, bevor diese Umfrage erneut angezeigt wird.",
|
||||
"decide_how_often_people_can_answer_this_survey": "Entscheide, wie oft Leute diese Umfrage beantworten können.",
|
||||
"days_before_showing_this_survey_again": "Tage nachdem eine beliebige Umfrage angezeigt wurde, bevor diese Umfrage erscheinen kann.",
|
||||
"delete_choice": "Auswahl löschen",
|
||||
"disable_the_visibility_of_survey_progress": "Deaktiviere die Sichtbarkeit des Umfragefortschritts.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Zeige eine Schätzung der Fertigstellungszeit für die Umfrage an",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "Bearbeitungslink",
|
||||
"edit_recall": "Erinnerung bearbeiten",
|
||||
"edit_translations": "{lang} -Übersetzungen bearbeiten",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Teilnehmer können die Umfragesprache jederzeit während der Umfrage ändern.",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Befragten erlauben, die Sprache jederzeit zu wechseln. Benötigt mind. 2 aktive Sprachen.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Spamschutz verwendet reCAPTCHA v3, um Spam-Antworten herauszufiltern.",
|
||||
"enable_spam_protection": "Spamschutz",
|
||||
"end_screen_card": "Abschluss-Karte",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "Entspricht einem von",
|
||||
"error_publishing_survey": "Beim Veröffentlichen der Umfrage ist ein Fehler aufgetreten.",
|
||||
"error_saving_changes": "Fehler beim Speichern der Änderungen",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Sogar nachdem sie eine Antwort eingereicht haben (z.B. Feedback-Box)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Mehrfachantworten erlauben; weiterhin anzeigen, auch nach einer Antwort (z.B. Feedback-Box).",
|
||||
"everyone": "Jeder",
|
||||
"external_urls_paywall_tooltip": "Bitte aktualisieren, um die externe URL anzupassen. Phishing-Prävention.",
|
||||
"external_urls_paywall_tooltip": "Bitte führen sie ein upgrade auf den Startup-plan durch, um externe URLs anzupassen. Dies hilft uns, phishing zu verhindern.",
|
||||
"fallback_missing": "Fehlender Fallback",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "Verstecktes Feld \"{fieldId}\" wird in der \"{quotaName}\" Quote verwendet",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein",
|
||||
"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",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Weiterhin anzeigen, wenn ausgelöst, bis eine Antwort abgegeben wird.",
|
||||
"ignore_global_waiting_time": "Projektweite Wartezeit ignorieren",
|
||||
"ignore_global_waiting_time_description": "Diese Umfrage kann angezeigt werden, wenn ihre Bedingungen erfüllt sind, auch wenn kürzlich eine andere Umfrage angezeigt wurde.",
|
||||
"image": "Bild",
|
||||
"includes_all_of": "Enthält alles von",
|
||||
"includes_one_of": "Enthält eines von",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "Optional",
|
||||
"options": "Optionen",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Styling für diese Umfrage überschreiben.",
|
||||
"overwrite_global_waiting_time": "Benutzerdefinierte Wartezeit festlegen",
|
||||
"overwrite_global_waiting_time_description": "Die Projektkonfiguration nur für diese Umfrage überschreiben.",
|
||||
"overwrite_placement": "Platzierung überschreiben",
|
||||
"overwrite_the_global_placement_of_the_survey": "Platzierung für diese Umfrage überschreiben",
|
||||
"overwrites_waiting_period_between_surveys_to_x_days": "Überschreibt die Wartezeit zwischen Umfragen auf {days} Tag(e).",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Wähle einen Hintergrund aus oder lade deinen eigenen hoch.",
|
||||
"picture_idx": "Bild {idx}",
|
||||
"pin_can_only_contain_numbers": "PIN darf nur Zahlen enthalten.",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "Reichweite",
|
||||
"recall_data": "Daten abrufen",
|
||||
"recall_information_from": "Information abrufen von ...",
|
||||
"recontact_options": "Optionen zur erneuten Kontaktaufnahme",
|
||||
"recontact_options_section": "Optionen zur erneuten Kontaktaufnahme",
|
||||
"recontact_options_section_description": "Wenn die Wartezeit es erlaubt, wählen Sie aus, wie oft diese Umfrage einer Person angezeigt werden kann.",
|
||||
"redirect_thank_you_card": "Weiterleitung anlegen",
|
||||
"redirect_to_url": "Zu URL weiterleiten",
|
||||
"remove_description": "Beschreibung entfernen",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "Erforderlich",
|
||||
"reset_to_theme_styles": "Styling zurücksetzen",
|
||||
"reset_to_theme_styles_main_text": "Bist Du sicher, dass Du das Styling auf die Themenstile zurücksetzen möchtest? Dadurch wird jegliches benutzerdefinierte Styling entfernt.",
|
||||
"respect_global_waiting_time": "Projektweite Wartezeit verwenden",
|
||||
"respect_global_waiting_time_description": "Diese Umfrage folgt der in der Projektkonfiguration festgelegten Wartezeit. Sie wird nur angezeigt, wenn in diesem Zeitraum keine andere Umfrage erschienen ist.",
|
||||
"response_limit_can_t_be_set_to_0": "Das Antwortlimit kann nicht auf 0 gesetzt werden",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "Antwortlimit muss die Anzahl der erhaltenen Antworten ({responseCount}) überschreiten.",
|
||||
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "Erweiterte Einstellungen anzeigen",
|
||||
"show_button": "Button anzeigen",
|
||||
"show_language_switch": "Sprachwechsel anzeigen",
|
||||
"show_multiple_times": "Mehrfach anzeigen",
|
||||
"show_multiple_times": "Begrenzte Anzahl von Malen anzeigen",
|
||||
"show_only_once": "Nur einmal anzeigen",
|
||||
"show_survey_maximum_of": "Umfrage maximal anzeigen von",
|
||||
"show_survey_to_users": "Umfrage % der Nutzer anzeigen",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "Schalte Mehrsprachigkeit ein, um loszulegen 👉",
|
||||
"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.",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Höchstens die angegebene Anzahl von Malen anzeigen oder bis sie antworten (je nachdem, was zuerst eintritt).",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Einmal anzeigen, auch wenn sie nicht antworten.",
|
||||
"then": "dann",
|
||||
"this_action_will_remove_all_the_translations_from_this_survey": "Diese Aktion entfernt alle Übersetzungen aus dieser Umfrage.",
|
||||
"this_extension_is_already_added": "Diese Erweiterung ist bereits hinzugefügt.",
|
||||
"this_file_type_is_not_supported": "Dieser Dateityp wird nicht unterstützt.",
|
||||
"this_setting_overwrites_your": "Diese Einstellung überschreibt deine",
|
||||
"three_points": "3 Punkte",
|
||||
"times": "Zeiten",
|
||||
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Um die Platzierung über alle Umfragen hinweg konsistent zu halten, kannst du",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "Spezifische Nutzergruppen basierend auf Attributen oder Geräteinformationen ansprechen",
|
||||
"unlock_targeting_title": "Targeting mit einem höheren Plan freischalten",
|
||||
"unsaved_changes_warning": "Du hast ungespeicherte Änderungen in deiner Umfrage. Möchtest Du sie speichern, bevor Du gehst?",
|
||||
"until_they_submit_a_response": "Bis sie eine Antwort einreichen",
|
||||
"until_they_submit_a_response": "Fragen, bis sie eine Antwort abgeben",
|
||||
"upgrade_notice_description": "Erstelle mehrsprachige Umfragen und entdecke viele weitere Funktionen",
|
||||
"upgrade_notice_title": "Schalte mehrsprachige Umfragen mit einem höheren Plan frei",
|
||||
"upload": "Hochladen",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "Oberes Label",
|
||||
"url_filters": "URL-Filter",
|
||||
"url_not_supported": "URL nicht unterstützt",
|
||||
"use_with_caution": "Mit Vorsicht verwenden",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" wird in der \"{quotaName}\" Quote verwendet",
|
||||
"variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "Variable \"{variable}\" wird in der Willkommenskarte abgerufen.",
|
||||
"verify_email_before_submission": "E-Mail vor dem Absenden überprüfen",
|
||||
"verify_email_before_submission_description": "Lass nur Leute mit einer echten E-Mail antworten.",
|
||||
"visibility_and_recontact": "Sichtbarkeit & erneute Kontaktaufnahme",
|
||||
"visibility_and_recontact_description": "Steuern Sie, wann diese Umfrage erscheinen kann und wie oft sie erneut erscheinen kann.",
|
||||
"wait": "Warte",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Warte ein paar Sekunden nach dem Auslöser, bevor Du die Umfrage anzeigst",
|
||||
"waiting_period": "Wartezeit",
|
||||
"waiting_time_across_surveys": "Projektweite Wartezeit",
|
||||
"waiting_time_across_surveys_description": "Um Umfragemüdigkeit zu vermeiden, wählen Sie aus, wie diese Umfrage mit der projektweiten Wartezeit interagiert.",
|
||||
"welcome_message": "Willkommensnachricht",
|
||||
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Wenn die Bedingungen übereinstimmen, wird die Wartezeit ignoriert und die Umfrage angezeigt.",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Ohne Filter können alle deine Nutzer befragt werden.",
|
||||
"you_have_not_created_a_segment_yet": "Du hast noch keinen Segment erstellt.",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations": "Du musst zwei oder mehr Sprachen in deinem Projekt einrichten, um mit Übersetzungen zu arbeiten.",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "Nachname",
|
||||
"not_completed": "Nicht abgeschlossen ⏳",
|
||||
"os": "Betriebssystem",
|
||||
"person_attributes": "Personenattribute",
|
||||
"person_attributes": "Personenattribute zum Zeitpunkt der Einreichung",
|
||||
"phone": "Telefon",
|
||||
"respondent_skipped_questions": "Der Befragte hat diese Fragen übersprungen.",
|
||||
"response_deleted_successfully": "Antwort erfolgreich gelöscht.",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "Filter hinzugefügt für Antworten, bei denen die Antwort auf Frage {questionIdx} {filterComboBoxValue} - {filterValue} ist",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "Filter hinzugefügt für Antworten, bei denen die Frage {questionIdx} übersprungen wurde",
|
||||
"aggregated": "Aggregiert",
|
||||
"all_responses_csv": "Alle Antworten (CSV)",
|
||||
"all_responses_excel": "Alle Antworten (Excel)",
|
||||
"all_time": "Gesamt",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "Gefilterte Antworten (CSV)",
|
||||
"filtered_responses_excel": "Gefilterte Antworten (Excel)",
|
||||
"generating_qr_code": "QR-Code wird generiert",
|
||||
"go_to_setup_checklist": "Gehe zur Einrichtungs-Checkliste 👉",
|
||||
"impressions": "Eindrücke",
|
||||
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "Beinhaltet alles",
|
||||
"includes_either": "Beinhaltet entweder",
|
||||
"install_widget": "Formbricks Widget installieren",
|
||||
"individual": "Individuell",
|
||||
"is_equal_to": "Ist gleich",
|
||||
"is_less_than": "ist weniger als",
|
||||
"last_30_days": "Letzte 30 Tage",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "Keine Antworten gefunden",
|
||||
"other_values_found": "Andere Werte gefunden",
|
||||
"overall": "Insgesamt",
|
||||
"promoters": "Promotoren",
|
||||
"qr_code": "QR-Code",
|
||||
"qr_code_description": "Antworten, die per QR-Code gesammelt werden, sind anonym.",
|
||||
"qr_code_download_failed": "QR-Code-Download fehlgeschlagen",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "Die Anzahl der von den Befragten abgeschlossenen Quoten.",
|
||||
"reset_survey": "Umfrage zurücksetzen",
|
||||
"reset_survey_warning": "Das Zurücksetzen einer Umfrage entfernt alle Antworten und Anzeigen, die mit dieser Umfrage verbunden sind. Dies kann nicht rückgängig gemacht werden.",
|
||||
"satisfied": "Zufrieden",
|
||||
"selected_responses_csv": "Ausgewählte Antworten (CSV)",
|
||||
"selected_responses_excel": "Ausgewählte Antworten (Excel)",
|
||||
"setup_integrations": "Integrationen einrichten",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "Durchschnittliche Zeit zum Beantworten der Frage.",
|
||||
"unknown_question_type": "Unbekannter Fragetyp",
|
||||
"use_personal_links": "Nutze persönliche Links",
|
||||
"waiting_for_response": "Warte auf eine Antwort 🧘♂️",
|
||||
"whats_next": "Was kommt als Nächstes?",
|
||||
"your_survey_is_public": "Deine Umfrage ist öffentlich",
|
||||
"youre_not_plugged_in_yet": "Du bist noch nicht verbunden!"
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "Clear filters",
|
||||
"clear_selection": "Clear selection",
|
||||
"click": "Click",
|
||||
"click_to_filter": "Click to filter",
|
||||
"clicks": "Clicks",
|
||||
"close": "Close",
|
||||
"code": "Code",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "Maximum number of requests reached. Please try again later.",
|
||||
"error_rate_limit_title": "Rate Limit Exceeded",
|
||||
"expand_rows": "Expand rows",
|
||||
"failed_to_copy_to_clipboard": "Failed to copy to clipboard",
|
||||
"failed_to_load_organizations": "Failed to load organizations",
|
||||
"failed_to_load_projects": "Failed to load projects",
|
||||
"finish": "Finish",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "Full name",
|
||||
"gathering_responses": "Gathering responses",
|
||||
"general": "General",
|
||||
"generate": "Generate",
|
||||
"go_back": "Go Back",
|
||||
"go_to_dashboard": "Go to Dashboard",
|
||||
"hidden": "Hidden",
|
||||
@@ -328,7 +331,7 @@
|
||||
"project_not_found": "Project not found",
|
||||
"project_permission_not_found": "Project permission not found",
|
||||
"projects": "Projects",
|
||||
"question": "Question",
|
||||
"question": "question",
|
||||
"question_id": "Question ID",
|
||||
"questions": "Questions",
|
||||
"quota": "Quota",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "User ID",
|
||||
"user_not_found": "User not found",
|
||||
"variable": "Variable",
|
||||
"variable_ids": "Variable IDs",
|
||||
"variables": "Variables",
|
||||
"verified_email": "Verified Email",
|
||||
"video": "Video",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "Add CSS class or id",
|
||||
"add_regular_expression_here": "Add a regular expression here",
|
||||
"add_url": "Add URL",
|
||||
"and": "AND",
|
||||
"click": "Click",
|
||||
"contains": "Contains",
|
||||
"create_action": "Create action",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "Limit to specific pages",
|
||||
"matches_regex": "Matches regex",
|
||||
"on_all_pages": "On all pages",
|
||||
"or": "OR",
|
||||
"page_filter": "Page filter",
|
||||
"page_view": "Page View",
|
||||
"select_match_type": "Select match type",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "Contacts refreshed successfully",
|
||||
"delete_contact_confirmation": "This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact's data will be lost.",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact's data will be lost. If this contact has responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.} other {This will delete all survey responses and contact attributes associated with these contacts. Any targeting and personalization based on these contacts' data will be lost. If these contacts have responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.}}",
|
||||
"generate_personal_link": "Generate Personal Link",
|
||||
"generate_personal_link_description": "Select a published survey to generate a personalized link for this contact.",
|
||||
"no_published_link_surveys_available": "No published link surveys available. Please publish a link survey first.",
|
||||
"no_published_surveys": "No published surveys",
|
||||
"no_responses_found": "No responses found",
|
||||
"not_provided": "Not provided",
|
||||
"personal_link_generated": "Personal link generated successfully",
|
||||
"personal_link_generated_but_clipboard_failed": "Personal link generated but failed to copy to clipboard: {url}",
|
||||
"personal_survey_link": "Personal Survey Link",
|
||||
"please_select_a_survey": "Please select a survey",
|
||||
"search_contact": "Search contact",
|
||||
"select_a_survey": "Select a survey",
|
||||
"select_attribute": "Select Attribute",
|
||||
"unlock_contacts_description": "Manage contacts and send out targeted surveys",
|
||||
"unlock_contacts_title": "Unlock contacts with a higher plan",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Update",
|
||||
"upload_contacts_modal_pick_different_file": "Pick a different file",
|
||||
"upload_contacts_modal_preview": "Here's a preview of your data.",
|
||||
"upload_contacts_modal_upload_btn": "Upload contacts"
|
||||
"upload_contacts_modal_upload_btn": "Upload contacts",
|
||||
"upload_contacts_success": "Contacts uploaded successfully"
|
||||
},
|
||||
"formbricks_logo": "Formbricks Logo",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "App Connection",
|
||||
"app_connection_description": "Connect your app to Formbricks.",
|
||||
"cache_update_delay_description": "When you make updates to surveys, contacts, actions, or other data, it can take up to 5 minutes for those changes to appear in your local app running the Formbricks SDK. This delay is due to a limitation in our current caching system. We’re actively reworking the cache and will release a fix in Formbricks 4.0.",
|
||||
"cache_update_delay_title": "Changes will be reflected after 5 minutes due to caching",
|
||||
"app_connection_description": "Connect your app or website to Formbricks.",
|
||||
"cache_update_delay_description": "When you make updates to surveys, contacts, actions, or other data, it can take up to 1 minute for those changes to appear in your local app running the Formbricks SDK.",
|
||||
"cache_update_delay_title": "Changes will be reflected after ~1 minute due to caching",
|
||||
"environment_id": "Your Environment ID",
|
||||
"environment_id_description": "This id uniquely identifies this Formbricks environment.",
|
||||
"formbricks_sdk_connected": "Formbricks SDK is connected",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.",
|
||||
"formbricks_sdk_not_connected_description": "Connect your website or app with Formbricks",
|
||||
"formbricks_sdk_not_connected_description": "Add the Formbricks SDK to your website or app to connect it with Formbricks",
|
||||
"how_to_setup": "How to setup",
|
||||
"how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.",
|
||||
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-check",
|
||||
"sdk_connection_details": "SDK Connection Details",
|
||||
"sdk_connection_details_description": "Your unique environment ID and SDK connection URL for integrating Formbricks with your application.",
|
||||
"setup_alert_description": "Follow this step-by-step tutorial to connect your app or website in under 5 minutes.",
|
||||
"setup_alert_title": "How to connect"
|
||||
"setup_alert_title": "How to connect",
|
||||
"webapp_url": "SDK Connection URL"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "This is your only project, it cannot be deleted. Create a new project first.",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "Project deleted successfully",
|
||||
"project_name_settings_description": "Change your projects name.",
|
||||
"project_name_updated_successfully": "Project name updated successfully",
|
||||
"recontact_waiting_time": "Recontact Waiting Time",
|
||||
"recontact_waiting_time": "Project-wide Waiting Time Between Surveys",
|
||||
"recontact_waiting_time_settings_description": "Control how frequently users can be surveyed across all app surveys.",
|
||||
"this_action_cannot_be_undone": "This action cannot be undone.",
|
||||
"wait_x_days_before_showing_next_survey": "Wait X days before showing next survey:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "Add Tag",
|
||||
"count": "Count",
|
||||
"delete_tag_confirmation": "Are you sure you want to delete this tag?",
|
||||
"empty_message": "Tag a submission to find your list of tags here.",
|
||||
"manage_tags": "Manage Tags",
|
||||
"manage_tags_description": "Merge and remove response tags.",
|
||||
"merge": "Merge",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"allow_multi_select": "Allow multi-select",
|
||||
"allow_multiple_files": "Allow multiple files",
|
||||
"allow_users_to_select_more_than_one_image": "Allow users to select more than one image",
|
||||
"always_show_survey": "Always show survey",
|
||||
"and_launch_surveys_in_your_website_or_app": "and launch surveys in your website or app.",
|
||||
"animation": "Animation",
|
||||
"app_survey_description": "Embed a survey in your web app or website to collect responses.",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"custom_hostname": "Custom hostname",
|
||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||
"date_format": "Date format",
|
||||
"days_before_showing_this_survey_again": "days before showing this survey again.",
|
||||
"decide_how_often_people_can_answer_this_survey": "Decide how often people can answer this survey.",
|
||||
"days_before_showing_this_survey_again": "days after any survey is shown before this survey can appear.",
|
||||
"delete_choice": "Delete choice",
|
||||
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "Edit link",
|
||||
"edit_recall": "Edit Recall",
|
||||
"edit_translations": "Edit {lang} translations",
|
||||
"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.",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Allow respondents to switch language at any time. Needs min. 2 active languages.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Spam protection uses reCAPTCHA v3 to filter out the spam responses.",
|
||||
"enable_spam_protection": "Spam protection",
|
||||
"end_screen_card": "End screen card",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "Equals one of",
|
||||
"error_publishing_survey": "An error occured while publishing the survey.",
|
||||
"error_saving_changes": "Error saving changes",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Even after they submitted a response (e.g. Feedback Box)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Allow multiple responses; continue showing even after a response (e.g., Feedback Box).",
|
||||
"everyone": "Everyone",
|
||||
"external_urls_paywall_tooltip": "Please upgrade to customize external URL. Phishing prevention.",
|
||||
"external_urls_paywall_tooltip": "Please upgrade to Startup plan to customize external URLs. This helps us prevent phishing.",
|
||||
"fallback_missing": "Fallback missing",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "Hidden field \"{fieldId}\" is being used in \"{quotaName}\" quota",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys",
|
||||
"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",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Keep showing whenever triggered until a response is submitted.",
|
||||
"ignore_global_waiting_time": "Ignore project-wide waiting time",
|
||||
"ignore_global_waiting_time_description": "This survey can show whenever its conditions are met, even if another survey was shown recently.",
|
||||
"image": "Image",
|
||||
"includes_all_of": "Includes all of",
|
||||
"includes_one_of": "Includes one of",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "Optional",
|
||||
"options": "Options",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Set custom waiting time",
|
||||
"overwrite_global_waiting_time_description": "Override the project configuration for this survey only.",
|
||||
"overwrite_placement": "Overwrite placement",
|
||||
"overwrite_the_global_placement_of_the_survey": "Overwrite the global placement of the survey",
|
||||
"overwrites_waiting_period_between_surveys_to_x_days": "Overwrites waiting period between surveys to {days} day(s).",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Pick a background from our library or upload your own.",
|
||||
"picture_idx": "Picture {idx}",
|
||||
"pin_can_only_contain_numbers": "PIN can only contain numbers.",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "Range",
|
||||
"recall_data": "Recall data",
|
||||
"recall_information_from": "Recall information from ...",
|
||||
"recontact_options": "Recontact Options",
|
||||
"recontact_options_section": "Recontact options",
|
||||
"recontact_options_section_description": "If the waiting time allows, choose how often this survey can be shown to a person.",
|
||||
"redirect_thank_you_card": "Redirect thank you card",
|
||||
"redirect_to_url": "Redirect to Url",
|
||||
"remove_description": "Remove description",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "Required",
|
||||
"reset_to_theme_styles": "Reset to theme styles",
|
||||
"reset_to_theme_styles_main_text": "Are you sure you want to reset the styling to the theme styles? This will remove all custom styling.",
|
||||
"respect_global_waiting_time": "Use project-wide waiting time",
|
||||
"respect_global_waiting_time_description": "This survey follows the waiting time set in project configuration. It only shows if no other survey has appeared during that period.",
|
||||
"response_limit_can_t_be_set_to_0": "Response limit can't be set to 0",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "Response limit needs to exceed number of received responses ({responseCount}).",
|
||||
"response_limits_redirections_and_more": "Response limits, redirections and more.",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "Show Advanced settings",
|
||||
"show_button": "Show Button",
|
||||
"show_language_switch": "Show language switch",
|
||||
"show_multiple_times": "Show multiple times",
|
||||
"show_multiple_times": "Show a limited number of times",
|
||||
"show_only_once": "Show only once",
|
||||
"show_survey_maximum_of": "Show survey maximum of",
|
||||
"show_survey_to_users": "Show survey to % of users",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"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.",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Show at most the specified number of times, or until they respond (whichever comes first).",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Show a single time, even if they don't respond.",
|
||||
"then": "Then",
|
||||
"this_action_will_remove_all_the_translations_from_this_survey": "This action will remove all the translations from this survey.",
|
||||
"this_extension_is_already_added": "This extension is already added.",
|
||||
"this_file_type_is_not_supported": "This file type is not supported.",
|
||||
"this_setting_overwrites_your": "This setting overwrites your",
|
||||
"three_points": "3 points",
|
||||
"times": "times",
|
||||
"to_keep_the_placement_over_all_surveys_consistent_you_can": "To keep the placement over all surveys consistent, you can",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "Target specific user groups based on attributes or device information",
|
||||
"unlock_targeting_title": "Unlock targeting with a higher plan",
|
||||
"unsaved_changes_warning": "You have unsaved changes in your survey. Would you like to save them before leaving?",
|
||||
"until_they_submit_a_response": "Until they submit a response",
|
||||
"until_they_submit_a_response": "Ask until they submit a response",
|
||||
"upgrade_notice_description": "Create multilingual surveys and unlock many more features",
|
||||
"upgrade_notice_title": "Unlock multi-language surveys with a higher plan",
|
||||
"upload": "Upload",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "Upper Label",
|
||||
"url_filters": "URL Filters",
|
||||
"url_not_supported": "URL not supported",
|
||||
"use_with_caution": "Use with caution",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" is being used in \"{quotaName}\" quota",
|
||||
"variable_name_is_already_taken_please_choose_another": "Variable name is already taken, please choose another.",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "Variable \"{variable}\" is being recalled in Welcome Card.",
|
||||
"verify_email_before_submission": "Verify email before submission",
|
||||
"verify_email_before_submission_description": "Only let people with a real email respond.",
|
||||
"visibility_and_recontact": "Visibility & Recontact",
|
||||
"visibility_and_recontact_description": "Control when this survey can appear and how often it can reappear.",
|
||||
"wait": "Wait",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Wait a few seconds after the trigger before showing the survey",
|
||||
"waiting_period": "waiting period",
|
||||
"waiting_time_across_surveys": "Project-wide waiting time",
|
||||
"waiting_time_across_surveys_description": "To prevent survey fatigue, choose how this survey interacts with the project-wide waiting time.",
|
||||
"welcome_message": "Welcome message",
|
||||
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "When conditions match, waiting time will be ignored and survey shown.",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Without a filter, all of your users can be surveyed.",
|
||||
"you_have_not_created_a_segment_yet": "You have not created a segment yet",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations": "You need to have two or more languages set up in your project to work with translations.",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "Last Name",
|
||||
"not_completed": "Not Completed ⏳",
|
||||
"os": "OS",
|
||||
"person_attributes": "Person attributes",
|
||||
"person_attributes": "Person attributes at time of submission",
|
||||
"phone": "Phone",
|
||||
"respondent_skipped_questions": "Respondent skipped these questions.",
|
||||
"response_deleted_successfully": "Response deleted successfully.",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "Added filter for responses where answer to question {questionIdx} is {filterComboBoxValue} - {filterValue} ",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "Added filter for responses where answer to question {questionIdx} is skipped",
|
||||
"aggregated": "Aggregated",
|
||||
"all_responses_csv": "All responses (CSV)",
|
||||
"all_responses_excel": "All responses (Excel)",
|
||||
"all_time": "All time",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "Filtered responses (CSV)",
|
||||
"filtered_responses_excel": "Filtered responses (Excel)",
|
||||
"generating_qr_code": "Generating QR code",
|
||||
"go_to_setup_checklist": "Go to Setup Checklist \uD83D\uDC49",
|
||||
"impressions": "Impressions",
|
||||
"impressions_tooltip": "Number of times the survey has been viewed.",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "Includes all",
|
||||
"includes_either": "Includes either",
|
||||
"install_widget": "Install Formbricks Widget",
|
||||
"individual": "Individual",
|
||||
"is_equal_to": "Is equal to",
|
||||
"is_less_than": "Is less than",
|
||||
"last_30_days": "Last 30 days",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "No responses found",
|
||||
"other_values_found": "Other values found",
|
||||
"overall": "Overall",
|
||||
"promoters": "Promoters",
|
||||
"qr_code": "QR code",
|
||||
"qr_code_description": "Responses collected via QR code are anonymous.",
|
||||
"qr_code_download_failed": "QR code download failed",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "The number of quotas completed by the respondents.",
|
||||
"reset_survey": "Reset survey",
|
||||
"reset_survey_warning": "Resetting a survey removes all responses and displays associated with this survey. This cannot be undone.",
|
||||
"satisfied": "Satisfied",
|
||||
"selected_responses_csv": "Selected responses (CSV)",
|
||||
"selected_responses_excel": "Selected responses (Excel)",
|
||||
"setup_integrations": "Setup integrations",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "Average time to complete the question.",
|
||||
"unknown_question_type": "Unknown Question Type",
|
||||
"use_personal_links": "Use personal links",
|
||||
"waiting_for_response": "Waiting for a response \uD83E\uDDD8♂️",
|
||||
"whats_next": "What's next?",
|
||||
"your_survey_is_public": "Your survey is public",
|
||||
"youre_not_plugged_in_yet": "You're not plugged in yet!"
|
||||
|
||||
2933
apps/web/locales/es-ES.json
Normal file
2933
apps/web/locales/es-ES.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "Effacer les filtres",
|
||||
"clear_selection": "Effacer la sélection",
|
||||
"click": "Cliquez",
|
||||
"click_to_filter": "Cliquer pour filtrer",
|
||||
"clicks": "Clics",
|
||||
"close": "Fermer",
|
||||
"code": "Code",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "Nombre maximal de demandes atteint. Veuillez réessayer plus tard.",
|
||||
"error_rate_limit_title": "Limite de Taux Dépassée",
|
||||
"expand_rows": "Développer les lignes",
|
||||
"failed_to_copy_to_clipboard": "Échec de la copie dans le presse-papiers",
|
||||
"failed_to_load_organizations": "Échec du chargement des organisations",
|
||||
"failed_to_load_projects": "Échec du chargement des projets",
|
||||
"finish": "Terminer",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "Nom complet",
|
||||
"gathering_responses": "Collecte des réponses",
|
||||
"general": "Général",
|
||||
"generate": "Générer",
|
||||
"go_back": "Retourner",
|
||||
"go_to_dashboard": "Aller au tableau de bord",
|
||||
"hidden": "Caché",
|
||||
@@ -328,7 +331,7 @@
|
||||
"project_not_found": "Projet non trouvé",
|
||||
"project_permission_not_found": "Autorisation de projet non trouvée",
|
||||
"projects": "Projets",
|
||||
"question": "Question",
|
||||
"question": "question",
|
||||
"question_id": "ID de la question",
|
||||
"questions": "Questions",
|
||||
"quota": "Quota",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "Identifiant d'utilisateur",
|
||||
"user_not_found": "Utilisateur non trouvé",
|
||||
"variable": "Variable",
|
||||
"variable_ids": "Identifiants variables",
|
||||
"variables": "Variables",
|
||||
"verified_email": "Email vérifié",
|
||||
"video": "Vidéo",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "Ajouter une classe ou un identifiant CSS",
|
||||
"add_regular_expression_here": "Ajouter une expression régulière",
|
||||
"add_url": "Ajouter une URL",
|
||||
"and": "ET",
|
||||
"click": "Cliquez",
|
||||
"contains": "Contient",
|
||||
"create_action": "Créer une action",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "Sur certaines pages",
|
||||
"matches_regex": "Correspond à l'expression régulière",
|
||||
"on_all_pages": "Sur toutes les pages",
|
||||
"or": "OU",
|
||||
"page_filter": "Filtrage des pages",
|
||||
"page_view": "Vue de page",
|
||||
"select_match_type": "Sélectionner le type de match",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "Contacts rafraîchis avec succès",
|
||||
"delete_contact_confirmation": "Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus.",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus. Si ce contact a des réponses qui comptent dans les quotas de l'enquête, les comptes de quotas seront réduits mais les limites de quota resteront inchangées.}}",
|
||||
"generate_personal_link": "Générer un lien personnel",
|
||||
"generate_personal_link_description": "Sélectionnez une enquête publiée pour générer un lien personnalisé pour ce contact.",
|
||||
"no_published_link_surveys_available": "Aucune enquête par lien publiée n'est disponible. Veuillez d'abord publier une enquête par lien.",
|
||||
"no_published_surveys": "Aucune enquête publiée",
|
||||
"no_responses_found": "Aucune réponse trouvée",
|
||||
"not_provided": "Non fourni",
|
||||
"personal_link_generated": "Lien personnel généré avec succès",
|
||||
"personal_link_generated_but_clipboard_failed": "Lien personnel généré mais échec de la copie dans le presse-papiers : {url}",
|
||||
"personal_survey_link": "Lien vers le sondage personnel",
|
||||
"please_select_a_survey": "Veuillez sélectionner une enquête",
|
||||
"search_contact": "Rechercher un contact",
|
||||
"select_a_survey": "Sélectionner une enquête",
|
||||
"select_attribute": "Sélectionner un attribut",
|
||||
"unlock_contacts_description": "Gérer les contacts et envoyer des enquêtes ciblées",
|
||||
"unlock_contacts_title": "Débloquez des contacts avec un plan supérieur.",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Mettre à jour",
|
||||
"upload_contacts_modal_pick_different_file": "Choisissez un fichier différent",
|
||||
"upload_contacts_modal_preview": "Voici un aperçu de vos données.",
|
||||
"upload_contacts_modal_upload_btn": "Importer des contacts"
|
||||
"upload_contacts_modal_upload_btn": "Importer des contacts",
|
||||
"upload_contacts_success": "Contacts téléchargés avec succès"
|
||||
},
|
||||
"formbricks_logo": "Logo Formbricks",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "Connexion d'une application",
|
||||
"app_connection_description": "Vous pouvez connecter une application à Formbricks.",
|
||||
"cache_update_delay_description": "Lorsque vous effectuez des mises à jour sur les sondages, contacts, actions ou autres données, cela peut prendre jusqu'à 5 minutes pour que ces modifications apparaissent dans votre application locale exécutant le SDK Formbricks. Ce délai est dû à une limitation de notre système de mise en cache actuel. Nous retravaillons activement le cache et publierons une correction dans Formbricks 4.0.",
|
||||
"cache_update_delay_title": "Les modifications seront reflétées après 5 minutes en raison de la mise en cache",
|
||||
"app_connection_description": "Connectez votre application ou site web à Formbricks.",
|
||||
"cache_update_delay_description": "Lorsque vous effectuez des mises à jour sur les sondages, contacts, actions ou autres données, ces changements peuvent prendre jusqu'à 1 minute pour apparaître dans votre application locale exécutant le SDK Formbricks.",
|
||||
"cache_update_delay_title": "Les modifications seront visibles après environ 1 minute en raison de la mise en cache",
|
||||
"environment_id": "Identifiant de votre environnement",
|
||||
"environment_id_description": "Cet identifiant unique est attribué à votre environnement Formbricks.",
|
||||
"formbricks_sdk_connected": "Le SDK Formbricks est connecté",
|
||||
"formbricks_sdk_not_connected": "Le SDK Formbricks n'est pas encore connecté.",
|
||||
"formbricks_sdk_not_connected_description": "Connectez votre site Web ou votre application à Formbricks.",
|
||||
"formbricks_sdk_not_connected_description": "Ajoutez le SDK Formbricks à votre site web ou application pour le connecter à Formbricks",
|
||||
"how_to_setup": "Comment configurer",
|
||||
"how_to_setup_description": "Suivez ces étapes pour configurer le widget Formbricks dans votre application.",
|
||||
"receiving_data": "Réception des données 💃🕺",
|
||||
"recheck": "Réessayer",
|
||||
"sdk_connection_details": "Détails de connexion SDK",
|
||||
"sdk_connection_details_description": "Votre ID d'environnement unique et votre URL de connexion SDK pour intégrer Formbricks à votre application.",
|
||||
"setup_alert_description": "Suivez les indications de ce tutoriel pour connecter votre application ou votre site Web en moins de cinq minutes.",
|
||||
"setup_alert_title": "Connexion"
|
||||
"setup_alert_title": "Connexion",
|
||||
"webapp_url": "URL de connexion SDK"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Comme il s'agit de votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "Projet supprimé avec succès",
|
||||
"project_name_settings_description": "Vous pouvez modifier le nom de votre projet.",
|
||||
"project_name_updated_successfully": "Le nom du projet a été mis à jour avec succès.",
|
||||
"recontact_waiting_time": "Délai avant nouveau contact",
|
||||
"recontact_waiting_time": "Temps d'attente entre les enquêtes à l'échelle du projet",
|
||||
"recontact_waiting_time_settings_description": "Vous pouvez contrôler la fréquence à laquelle les utilisateurs sont sollicités pour répondre aux enquêtes.",
|
||||
"this_action_cannot_be_undone": "Cette action ne peut pas être annulée.",
|
||||
"wait_x_days_before_showing_next_survey": "Nombre de jours devant s'écouler avant une nouvelle sollicitation :",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "Ajouter une étiquette",
|
||||
"count": "Compter",
|
||||
"delete_tag_confirmation": "Êtes-vous sûr de vouloir supprimer cette étiquette ?",
|
||||
"empty_message": "Ajoutez une balise à une réponse pour afficher votre liste de balises.",
|
||||
"manage_tags": "Gérer les étiquettes",
|
||||
"manage_tags_description": "Vous pouvez fusionner et supprimer des balises de réponse.",
|
||||
"merge": "Fusionner",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"allow_multi_select": "Autoriser la sélection multiple",
|
||||
"allow_multiple_files": "Autoriser plusieurs fichiers",
|
||||
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plusieurs images",
|
||||
"always_show_survey": "Afficher toujours l'enquête",
|
||||
"and_launch_surveys_in_your_website_or_app": "et lancez des enquêtes sur votre site web ou votre application.",
|
||||
"animation": "Animation",
|
||||
"app_survey_description": "Intégrez une enquête dans votre application web ou votre site web pour collecter des réponses.",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"custom_hostname": "Nom d'hôte personnalisé",
|
||||
"darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.",
|
||||
"date_format": "Format de date",
|
||||
"days_before_showing_this_survey_again": "jours avant de montrer à nouveau cette enquête.",
|
||||
"decide_how_often_people_can_answer_this_survey": "Décidez à quelle fréquence les gens peuvent répondre à cette enquête.",
|
||||
"days_before_showing_this_survey_again": "jours après qu'une enquête soit affichée avant que cette enquête puisse apparaître.",
|
||||
"delete_choice": "Supprimer l'option",
|
||||
"disable_the_visibility_of_survey_progress": "Désactiver la visibilité de la progression du sondage.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Afficher une estimation du temps de complétion pour l'enquête.",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "Modifier le lien",
|
||||
"edit_recall": "Modifier le rappel",
|
||||
"edit_translations": "Modifier les traductions {lang}",
|
||||
"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.",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permettre aux répondants de changer de langue à tout moment. Nécessite au moins 2 langues actives.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "La protection contre le spam utilise reCAPTCHA v3 pour filtrer les réponses indésirables.",
|
||||
"enable_spam_protection": "Protection contre le spam",
|
||||
"end_screen_card": "Carte de fin d'écran",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "Égal à l'un de",
|
||||
"error_publishing_survey": "Une erreur est survenue lors de la publication de l'enquête.",
|
||||
"error_saving_changes": "Erreur lors de l'enregistrement des modifications",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Même après avoir soumis une réponse (par exemple, la boîte de feedback)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Autoriser plusieurs réponses ; continuer à afficher même après une réponse (par exemple, boîte de commentaires).",
|
||||
"everyone": "Tout le monde",
|
||||
"external_urls_paywall_tooltip": "Veuillez passer à la version supérieure pour personnaliser l'URL externe. Prévention contre l'hameçonnage.",
|
||||
"external_urls_paywall_tooltip": "Veuillez passer au forfait Startup pour personnaliser les URL externes. Cela nous aide à prévenir le phishing.",
|
||||
"fallback_missing": "Fallback manquant",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "Le champ masqué \"{fieldId}\" est utilisé dans le quota \"{quotaName}\"",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"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}",
|
||||
"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",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuer à afficher à chaque déclenchement jusqu'à ce qu'une réponse soit soumise.",
|
||||
"ignore_global_waiting_time": "Ignorer le temps d'attente à l'échelle du projet",
|
||||
"ignore_global_waiting_time_description": "Cette enquête peut s'afficher chaque fois que ses conditions sont remplies, même si une autre enquête a été affichée récemment.",
|
||||
"image": "Image",
|
||||
"includes_all_of": "Comprend tous les",
|
||||
"includes_one_of": "Comprend un de",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "Optionnel",
|
||||
"options": "Options",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Définir un temps d'attente personnalisé",
|
||||
"overwrite_global_waiting_time_description": "Remplacer la configuration du projet pour cette enquête uniquement.",
|
||||
"overwrite_placement": "Surcharge de placement",
|
||||
"overwrite_the_global_placement_of_the_survey": "Surcharger le placement global de l'enquête",
|
||||
"overwrites_waiting_period_between_surveys_to_x_days": "Remplace la période d'attente entre les enquêtes par {days} jour(s).",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Choisissez un arrière-plan dans notre bibliothèque ou téléchargez le vôtre.",
|
||||
"picture_idx": "Image {idx}",
|
||||
"pin_can_only_contain_numbers": "Le code PIN ne peut contenir que des chiffres.",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "Plage",
|
||||
"recall_data": "Rappel des données",
|
||||
"recall_information_from": "Rappeler les informations de ...",
|
||||
"recontact_options": "Options de recontact",
|
||||
"recontact_options_section": "Options de recontact",
|
||||
"recontact_options_section_description": "Si le temps d'attente le permet, choisissez la fréquence à laquelle cette enquête peut être présentée à une personne.",
|
||||
"redirect_thank_you_card": "Carte de remerciement de redirection",
|
||||
"redirect_to_url": "Rediriger vers l'URL",
|
||||
"remove_description": "Supprimer la description",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "Requis",
|
||||
"reset_to_theme_styles": "Réinitialiser aux styles de thème",
|
||||
"reset_to_theme_styles_main_text": "Êtes-vous sûr de vouloir réinitialiser le style aux styles du thème ? Cela supprimera tous les styles personnalisés.",
|
||||
"respect_global_waiting_time": "Utiliser le temps d'attente à l'échelle du projet",
|
||||
"respect_global_waiting_time_description": "Cette enquête respecte le temps d'attente défini dans la configuration du projet. Elle ne s'affiche que si aucune autre enquête n'est apparue pendant cette période.",
|
||||
"response_limit_can_t_be_set_to_0": "La limite de réponse ne peut pas être fixée à 0.",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "La limite de réponses doit dépasser le nombre de réponses reçues ({responseCount}).",
|
||||
"response_limits_redirections_and_more": "Limites de réponse, redirections et plus.",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "Afficher les paramètres avancés",
|
||||
"show_button": "Afficher le bouton",
|
||||
"show_language_switch": "Afficher le changement de langue",
|
||||
"show_multiple_times": "Afficher plusieurs fois",
|
||||
"show_multiple_times": "Afficher un nombre limité de fois",
|
||||
"show_only_once": "Afficher une seule fois",
|
||||
"show_survey_maximum_of": "Afficher le maximum du sondage de",
|
||||
"show_survey_to_users": "Afficher l'enquête à % des utilisateurs",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "Activez le multilingue pour commencer 👉",
|
||||
"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.",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Afficher au maximum le nombre de fois spécifié, ou jusqu'à ce qu'ils répondent (selon la première éventualité).",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Afficher une seule fois, même si la personne ne répond pas.",
|
||||
"then": "Alors",
|
||||
"this_action_will_remove_all_the_translations_from_this_survey": "Cette action supprimera toutes les traductions de cette enquête.",
|
||||
"this_extension_is_already_added": "Cette extension est déjà ajoutée.",
|
||||
"this_file_type_is_not_supported": "Ce type de fichier n'est pas pris en charge.",
|
||||
"this_setting_overwrites_your": "Ce paramètre écrase votre",
|
||||
"three_points": "3 points",
|
||||
"times": "fois",
|
||||
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Pour maintenir la cohérence du placement sur tous les sondages, vous pouvez",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "Cibler des groupes d'utilisateurs spécifiques en fonction des attributs ou des informations sur l'appareil",
|
||||
"unlock_targeting_title": "Débloquez le ciblage avec un plan supérieur.",
|
||||
"unsaved_changes_warning": "Vous avez des modifications non enregistrées dans votre enquête. Souhaitez-vous les enregistrer avant de partir ?",
|
||||
"until_they_submit_a_response": "Jusqu'à ce qu'ils soumettent une réponse",
|
||||
"until_they_submit_a_response": "Demander jusqu'à ce qu'ils soumettent une réponse",
|
||||
"upgrade_notice_description": "Créez des sondages multilingues et débloquez de nombreuses autres fonctionnalités",
|
||||
"upgrade_notice_title": "Débloquez les sondages multilingues avec un plan supérieur",
|
||||
"upload": "Télécharger",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "Étiquette supérieure",
|
||||
"url_filters": "Filtres d'URL",
|
||||
"url_not_supported": "URL non supportée",
|
||||
"use_with_caution": "À utiliser avec précaution",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" est utilisée dans le quota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "Le nom de la variable est déjà pris, veuillez en choisir un autre.",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "La variable \"{variable}\" est rappelée dans la carte de bienvenue.",
|
||||
"verify_email_before_submission": "Vérifiez l'email avant la soumission",
|
||||
"verify_email_before_submission_description": "Ne laissez répondre que les personnes ayant une véritable adresse e-mail.",
|
||||
"visibility_and_recontact": "Visibilité et recontact",
|
||||
"visibility_and_recontact_description": "Contrôlez quand cette enquête peut apparaître et à quelle fréquence elle peut réapparaître.",
|
||||
"wait": "Attendre",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Attendez quelques secondes après le déclencheur avant de montrer l'enquête.",
|
||||
"waiting_period": "période d'attente",
|
||||
"waiting_time_across_surveys": "Temps d'attente à l'échelle du projet",
|
||||
"waiting_time_across_surveys_description": "Pour éviter la lassitude face aux enquêtes, choisissez comment cette enquête interagit avec le temps d'attente à l'échelle du projet.",
|
||||
"welcome_message": "Message de bienvenue",
|
||||
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Lorsque les conditions correspondent, le temps d'attente sera ignoré et l'enquête sera affichée.",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sans filtre, tous vos utilisateurs peuvent être sondés.",
|
||||
"you_have_not_created_a_segment_yet": "Tu n'as pas encore créé de segment.",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations": "Vous devez avoir deux langues ou plus configurées dans votre projet pour travailler avec des traductions.",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "Nom de famille",
|
||||
"not_completed": "Non terminé ⏳",
|
||||
"os": "Système d'exploitation",
|
||||
"person_attributes": "Attributs de la personne",
|
||||
"person_attributes": "Attributs de la personne au moment de la soumission",
|
||||
"phone": "Téléphone",
|
||||
"respondent_skipped_questions": "Le répondant a sauté ces questions.",
|
||||
"response_deleted_successfully": "Réponse supprimée avec succès.",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"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",
|
||||
"aggregated": "Agrégé",
|
||||
"all_responses_csv": "Tous les réponses (CSV)",
|
||||
"all_responses_excel": "Tous les réponses (Excel)",
|
||||
"all_time": "Tout le temps",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "Réponses filtrées (CSV)",
|
||||
"filtered_responses_excel": "Réponses filtrées (Excel)",
|
||||
"generating_qr_code": "Génération du code QR",
|
||||
"go_to_setup_checklist": "Allez à la liste de contrôle de configuration 👉",
|
||||
"impressions": "Impressions",
|
||||
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "Comprend tous",
|
||||
"includes_either": "Comprend soit",
|
||||
"install_widget": "Installer le widget Formbricks",
|
||||
"individual": "Individuel",
|
||||
"is_equal_to": "Est égal à",
|
||||
"is_less_than": "est inférieur à",
|
||||
"last_30_days": "30 derniers jours",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "Aucune réponse trouvée",
|
||||
"other_values_found": "D'autres valeurs trouvées",
|
||||
"overall": "Globalement",
|
||||
"promoters": "Promoteurs",
|
||||
"qr_code": "Code QR",
|
||||
"qr_code_description": "Les réponses collectées via le code QR sont anonymes.",
|
||||
"qr_code_download_failed": "Échec du téléchargement du code QR",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "Le nombre de quotas complétés par les répondants.",
|
||||
"reset_survey": "Réinitialiser l'enquête",
|
||||
"reset_survey_warning": "Réinitialiser un sondage supprime toutes les réponses et les affichages associés à ce sondage. Cela ne peut pas être annulé.",
|
||||
"satisfied": "Satisfait",
|
||||
"selected_responses_csv": "Réponses sélectionnées (CSV)",
|
||||
"selected_responses_excel": "Réponses sélectionnées (Excel)",
|
||||
"setup_integrations": "Configurer les intégrations",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "Temps moyen pour compléter la question.",
|
||||
"unknown_question_type": "Type de question inconnu",
|
||||
"use_personal_links": "Utilisez des liens personnels",
|
||||
"waiting_for_response": "En attente d'une réponse 🧘♂️",
|
||||
"whats_next": "Qu'est-ce qui vient ensuite ?",
|
||||
"your_survey_is_public": "Votre enquête est publique.",
|
||||
"youre_not_plugged_in_yet": "Vous n'êtes pas encore branché !"
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "フィルターをクリア",
|
||||
"clear_selection": "選択をクリア",
|
||||
"click": "クリック",
|
||||
"click_to_filter": "クリックしてフィルター",
|
||||
"clicks": "クリック数",
|
||||
"close": "閉じる",
|
||||
"code": "コード",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "リクエストの最大数に達しました。後でもう一度試してください。",
|
||||
"error_rate_limit_title": "レート制限を超えました",
|
||||
"expand_rows": "行を展開",
|
||||
"failed_to_copy_to_clipboard": "クリップボードへのコピーに失敗しました",
|
||||
"failed_to_load_organizations": "組織の読み込みに失敗しました",
|
||||
"failed_to_load_projects": "プロジェクトの読み込みに失敗しました",
|
||||
"finish": "完了",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "氏名",
|
||||
"gathering_responses": "回答を収集しています",
|
||||
"general": "一般",
|
||||
"generate": "生成",
|
||||
"go_back": "戻る",
|
||||
"go_to_dashboard": "ダッシュボードへ移動",
|
||||
"hidden": "非表示",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "ユーザーID",
|
||||
"user_not_found": "ユーザーが見つかりません",
|
||||
"variable": "変数",
|
||||
"variable_ids": "変数ID",
|
||||
"variables": "変数",
|
||||
"verified_email": "認証済みメールアドレス",
|
||||
"video": "動画",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "CSSクラスまたはIDを追加",
|
||||
"add_regular_expression_here": "ここに正規表現を追加",
|
||||
"add_url": "URLを追加",
|
||||
"and": "AND",
|
||||
"click": "クリック",
|
||||
"contains": "を含む",
|
||||
"create_action": "アクションを作成",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "特定のページに制限",
|
||||
"matches_regex": "正規表現に一致する",
|
||||
"on_all_pages": "すべてのページで",
|
||||
"or": "OR",
|
||||
"page_filter": "ページフィルター",
|
||||
"page_view": "ページビュー",
|
||||
"select_match_type": "一致タイプを選択",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "連絡先を正常に更新しました",
|
||||
"delete_contact_confirmation": "これにより、この連絡先に関連付けられているすべてのフォーム回答と連絡先属性が削除されます。この連絡先のデータに基づいたターゲティングとパーソナライゼーションはすべて失われます。",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {これにより この連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。この連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。この連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。} other {これにより これらの連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。これらの連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。これらの連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。}}",
|
||||
"generate_personal_link": "個人リンクを生成",
|
||||
"generate_personal_link_description": "公開されたフォームを選択して、この連絡先用のパーソナライズされたリンクを生成します。",
|
||||
"no_published_link_surveys_available": "公開されたリンクフォームはありません。まずリンクフォームを公開してください。",
|
||||
"no_published_surveys": "公開されたフォームはありません",
|
||||
"no_responses_found": "回答が見つかりません",
|
||||
"not_provided": "提供されていません",
|
||||
"personal_link_generated": "個人リンクが正常に生成されました",
|
||||
"personal_link_generated_but_clipboard_failed": "個人用リンクは生成されましたが、クリップボードへのコピーに失敗しました: {url}",
|
||||
"personal_survey_link": "個人調査リンク",
|
||||
"please_select_a_survey": "フォームを選択してください",
|
||||
"search_contact": "連絡先を検索",
|
||||
"select_a_survey": "フォームを選択",
|
||||
"select_attribute": "属性を選択",
|
||||
"unlock_contacts_description": "連絡先を管理し、特定のフォームを送信します",
|
||||
"unlock_contacts_title": "上位プランで連絡先をアンロック",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "更新",
|
||||
"upload_contacts_modal_pick_different_file": "別のファイルを選択",
|
||||
"upload_contacts_modal_preview": "データのプレビューです。",
|
||||
"upload_contacts_modal_upload_btn": "連絡先をアップロード"
|
||||
"upload_contacts_modal_upload_btn": "連絡先をアップロード",
|
||||
"upload_contacts_success": "連絡先のアップロードに成功しました"
|
||||
},
|
||||
"formbricks_logo": "Formbricksのロゴ",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "アプリ接続",
|
||||
"app_connection_description": "あなたのアプリをFormbricksに接続します。",
|
||||
"cache_update_delay_description": "フォーム・連絡先・アクションなどを更新してから、Formbricks SDK を実行中のローカルアプリに反映されるまで最大5分かかる場合があります。これは現在のキャッシュ方式の制限によるものです。私たちはキャッシュを改修中で、Formbricks 4.0 で修正を提供予定です。",
|
||||
"cache_update_delay_title": "キャッシュのため変更の反映に最大5分かかります",
|
||||
"app_connection_description": "アプリやウェブサイトをFormbricksに接続します。",
|
||||
"cache_update_delay_description": "アンケート、連絡先、アクション、またはその他のデータを更新した場合、Formbricks SDKを実行しているローカルアプリにそれらの変更が反映されるまでに最大1分かかることがあります。",
|
||||
"cache_update_delay_title": "キャッシュの影響により、変更が反映されるまでに約1分かかります",
|
||||
"environment_id": "あなたのEnvironmentId",
|
||||
"environment_id_description": "このIDはこのFormbricks環境を一意に識別します。",
|
||||
"formbricks_sdk_connected": "Formbricks SDK は接続されています",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK はまだ接続されていません。",
|
||||
"formbricks_sdk_not_connected_description": "あなたのウェブサイトまたはアプリをFormbricksに接続してください",
|
||||
"formbricks_sdk_not_connected_description": "FormbricksSDKをウェブサイトやアプリに追加して、Formbricksと接続してください",
|
||||
"how_to_setup": "セットアップ方法",
|
||||
"how_to_setup_description": "アプリ内でFormbricksウィジェットを設定する手順に従ってください。",
|
||||
"receiving_data": "データ受信中 💃🕺",
|
||||
"recheck": "再チェック",
|
||||
"sdk_connection_details": "SDK接続詳細",
|
||||
"sdk_connection_details_description": "FormbricksをアプリケーションとAPI統合するためのEnvironmentIdとSDK接続URL。",
|
||||
"setup_alert_description": "5 分以内でアプリまたはウェブサイト を 接続する手順をステップバイステップ の チュートリアルに従ってください。",
|
||||
"setup_alert_title": "接続方法"
|
||||
"setup_alert_title": "接続方法",
|
||||
"webapp_url": "SDK接続URL"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "これは唯一のプロジェクトのため削除できません。まず新しいプロジェクトを作成してください。",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "プロジェクトを削除しました",
|
||||
"project_name_settings_description": "プロジェクト名を変更します。",
|
||||
"project_name_updated_successfully": "プロジェクト名を更新しました",
|
||||
"recontact_waiting_time": "再接触の待機時間",
|
||||
"recontact_waiting_time": "フォーム間のプロジェクト全体の待機時間",
|
||||
"recontact_waiting_time_settings_description": "アプリ内フォーム全体で、ユーザーにどの頻度で表示するかを制御します。",
|
||||
"this_action_cannot_be_undone": "この操作は取り消せません。",
|
||||
"wait_x_days_before_showing_next_survey": "次のフォームを表示するまでの待機日数:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "タグを追加",
|
||||
"count": "件数",
|
||||
"delete_tag_confirmation": "このタグを削除してもよろしいですか?",
|
||||
"empty_message": "送信にタグ付けすると、ここにタグ一覧が表示されます。",
|
||||
"manage_tags": "タグを管理",
|
||||
"manage_tags_description": "回答タグを統合・削除します。",
|
||||
"merge": "統合",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"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": "回答を収集するために、ウェブアプリまたはウェブサイトにフォームを埋め込みます。",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"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": "このフォームに人々が何回回答できるかを決定します。",
|
||||
"days_before_showing_this_survey_again": "任意のフォームが表示された後、このフォームが再表示されるまでの日数。",
|
||||
"delete_choice": "選択肢を削除",
|
||||
"disable_the_visibility_of_survey_progress": "フォームの進捗状況の表示を無効にする。",
|
||||
"display_an_estimate_of_completion_time_for_survey": "フォームの完了時間の目安を表示",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "編集 リンク",
|
||||
"edit_recall": "リコールを編集",
|
||||
"edit_translations": "{lang} 翻訳を編集",
|
||||
"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": "回答者がいつでも言語を切り替えられるようにします。最低2つのアクティブな言語が必要です。",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "スパム対策はreCAPTCHA v3を使用してスパム回答をフィルタリングします。",
|
||||
"enable_spam_protection": "スパム対策",
|
||||
"end_screen_card": "終了画面カード",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "のいずれかと等しい",
|
||||
"error_publishing_survey": "フォームの公開中にエラーが発生しました。",
|
||||
"error_saving_changes": "変更の保存中にエラーが発生しました",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "回答を送信した後でも(例:フィードバックボックス)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "複数の回答を許可;回答後も表示を継続(例:フィードボックス)。",
|
||||
"everyone": "全員",
|
||||
"external_urls_paywall_tooltip": "外部 URL をカスタマイズするにはアップグレードしてください 。 フィッシング防止 。",
|
||||
"external_urls_paywall_tooltip": "外部URLをカスタマイズするには、スタートアッププランへのアップグレードが必要です。これによりフィッシング詐欺を防止することができます。",
|
||||
"fallback_missing": "フォールバックがありません",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "隠しフィールド \"{fieldId}\" は \"{quotaName}\" クォータ で使用されています",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "ホスト名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか",
|
||||
"if_you_need_more_please": "さらに必要な場合は、",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "本当にその回答が欲しいなら、それを得るまで尋ねてください。",
|
||||
"ignore_waiting_time_between_surveys": "フォーム間の待機時間を無視する",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "回答が提出されるまで、トリガーされるたびに表示し続けます。",
|
||||
"ignore_global_waiting_time": "プロジェクト全体の待機時間を無視する",
|
||||
"ignore_global_waiting_time_description": "このフォームは、最近別のフォームが表示されていても、条件が満たされればいつでも表示できます。",
|
||||
"image": "画像",
|
||||
"includes_all_of": "のすべてを含む",
|
||||
"includes_one_of": "のいずれかを含む",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "オプション",
|
||||
"options": "オプション",
|
||||
"override_theme_with_individual_styles_for_this_survey": "このフォームの個別のスタイルでテーマを上書きします。",
|
||||
"overwrite_global_waiting_time": "カスタム待機時間を設定する",
|
||||
"overwrite_global_waiting_time_description": "このフォームのみプロジェクト設定を上書きします。",
|
||||
"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は数字のみでなければなりません。",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "範囲",
|
||||
"recall_data": "データを呼び出す",
|
||||
"recall_information_from": "... からの情報を呼び戻す",
|
||||
"recontact_options": "再接触オプション",
|
||||
"recontact_options_section": "再接触オプション",
|
||||
"recontact_options_section_description": "待機時間が許可する場合、このフォームを一人の人にどれくらいの頻度で表示できるかを選択します。",
|
||||
"redirect_thank_you_card": "サンクスクカードをリダイレクト",
|
||||
"redirect_to_url": "URLにリダイレクト",
|
||||
"remove_description": "説明を削除",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "必須",
|
||||
"reset_to_theme_styles": "テーマのスタイルにリセット",
|
||||
"reset_to_theme_styles_main_text": "スタイルをテーマのスタイルにリセットしてもよろしいですか?これにより、すべてのカスタムスタイルが削除されます。",
|
||||
"respect_global_waiting_time": "プロジェクト全体の待機時間を使用する",
|
||||
"respect_global_waiting_time_description": "このフォームはプロジェクト設定で設定された待機時間に従います。その期間中に他のフォームが表示されていない場合にのみ表示されます。",
|
||||
"response_limit_can_t_be_set_to_0": "回答数の上限を0に設定することはできません",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "回答数の上限は、受信済みの回答数 ({responseCount}) を超える必要があります。",
|
||||
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "詳細設定を表示",
|
||||
"show_button": "ボタンを表示",
|
||||
"show_language_switch": "言語切り替えを表示",
|
||||
"show_multiple_times": "複数回表示",
|
||||
"show_multiple_times": "限られた回数表示する",
|
||||
"show_only_once": "一度だけ表示",
|
||||
"show_survey_maximum_of": "フォームの最大表示回数",
|
||||
"show_survey_to_users": "ユーザーの {percentage}% にフォームを表示",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "始めるには多言語をオンにしてください 👉",
|
||||
"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": "回答しなくても、一度だけフォームが表示されます。",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "指定された回数まで、または回答があるまで表示します(どちらか先に達した方)。",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "回答がなくても1回だけ表示します。",
|
||||
"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": "すべてのフォームの配置を一貫させるために、",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "属性またはデバイス情報に基づいて、特定のユーザーグループをターゲットにします",
|
||||
"unlock_targeting_title": "上位プランでターゲティングをアンロック",
|
||||
"unsaved_changes_warning": "フォームに未保存の変更があります。離れる前に保存しますか?",
|
||||
"until_they_submit_a_response": "回答を送信するまで",
|
||||
"until_they_submit_a_response": "回答が提出されるまで質問する",
|
||||
"upgrade_notice_description": "多言語フォームを作成し、さらに多くの機能をアンロック",
|
||||
"upgrade_notice_title": "上位プランで多言語フォームをアンロック",
|
||||
"upload": "アップロード",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "上限ラベル",
|
||||
"url_filters": "URLフィルター",
|
||||
"url_not_supported": "URLはサポートされていません",
|
||||
"use_with_caution": "注意して使用",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "変数 \"{variableName}\" は \"{quotaName}\" クォータ で使用されています",
|
||||
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "変数 \"{variable}\" が ウェルカム カード で 呼び出され て います。",
|
||||
"verify_email_before_submission": "送信前にメールアドレスを認証",
|
||||
"verify_email_before_submission_description": "有効なメールアドレスを持つ人のみが回答できるようにする",
|
||||
"visibility_and_recontact": "表示と再接触",
|
||||
"visibility_and_recontact_description": "このフォームがいつ表示され、どのくらいの頻度で再表示できるかをコントロールします。",
|
||||
"wait": "待つ",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "トリガーから数秒待ってからフォームを表示します",
|
||||
"waiting_period": "待機期間",
|
||||
"waiting_time_across_surveys": "プロジェクト全体の待機時間",
|
||||
"waiting_time_across_surveys_description": "フォーム疲れを防ぐため、このフォームがプロジェクト全体の待機時間とどのように相互作用するかを選択します。",
|
||||
"welcome_message": "ウェルカムメッセージ",
|
||||
"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": "翻訳を操作するには、プロジェクトで2つ以上の言語を設定する必要があります。",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "姓",
|
||||
"not_completed": "未完了 ⏳",
|
||||
"os": "OS",
|
||||
"person_attributes": "人物属性",
|
||||
"person_attributes": "回答時の個人属性",
|
||||
"phone": "電話",
|
||||
"respondent_skipped_questions": "回答者はこれらの質問をスキップしました。",
|
||||
"response_deleted_successfully": "回答を正常に削除しました。",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "質問 {questionIdx} の回答が {filterComboBoxValue} - {filterValue} である回答のフィルターを追加しました",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "質問 {questionIdx} の回答がスキップされた回答のフィルターを追加しました",
|
||||
"aggregated": "集計済み",
|
||||
"all_responses_csv": "すべての回答 (CSV)",
|
||||
"all_responses_excel": "すべての回答 (Excel)",
|
||||
"all_time": "全期間",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "フィルター済み回答 (CSV)",
|
||||
"filtered_responses_excel": "フィルター済み回答 (Excel)",
|
||||
"generating_qr_code": "QRコードを生成中",
|
||||
"go_to_setup_checklist": "セットアップチェックリストへ移動 👉",
|
||||
"impressions": "表示回数",
|
||||
"impressions_tooltip": "フォームが表示された回数。",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "すべてを含む",
|
||||
"includes_either": "どちらかを含む",
|
||||
"install_widget": "Formbricksウィジェットをインストール",
|
||||
"individual": "個人",
|
||||
"is_equal_to": "と等しい",
|
||||
"is_less_than": "より小さい",
|
||||
"last_30_days": "過去30日間",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "回答が見つかりません",
|
||||
"other_values_found": "他の値が見つかりました",
|
||||
"overall": "全体",
|
||||
"promoters": "推奨者",
|
||||
"qr_code": "QRコード",
|
||||
"qr_code_description": "QRコード経由で収集された回答は匿名です。",
|
||||
"qr_code_download_failed": "QRコードのダウンロードに失敗しました",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "回答者 によって 完了 した 定員 の 数。",
|
||||
"reset_survey": "フォームをリセット",
|
||||
"reset_survey_warning": "フォームをリセットすると、このフォームに関連付けられているすべての回答と表示が削除されます。この操作は元に戻せません。",
|
||||
"satisfied": "満足",
|
||||
"selected_responses_csv": "選択した回答 (CSV)",
|
||||
"selected_responses_excel": "選択した回答 (Excel)",
|
||||
"setup_integrations": "連携を設定",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "フォームを完了するまでの平均時間。",
|
||||
"unknown_question_type": "不明な質問の種類",
|
||||
"use_personal_links": "個人リンクを使用",
|
||||
"waiting_for_response": "回答を待っています 🧘♂️",
|
||||
"whats_next": "次は何をしますか?",
|
||||
"your_survey_is_public": "あなたのフォームは公開されています",
|
||||
"youre_not_plugged_in_yet": "まだ接続されていません!"
|
||||
|
||||
2933
apps/web/locales/nl-NL.json
Normal file
2933
apps/web/locales/nl-NL.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "Limpar filtros",
|
||||
"clear_selection": "Limpar seleção",
|
||||
"click": "Clica",
|
||||
"click_to_filter": "Clique para filtrar",
|
||||
"clicks": "cliques",
|
||||
"close": "Fechar",
|
||||
"code": "Código",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "Número máximo de requisições atingido. Por favor, tente novamente mais tarde.",
|
||||
"error_rate_limit_title": "Limite de Taxa Excedido",
|
||||
"expand_rows": "Expandir linhas",
|
||||
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
|
||||
"failed_to_load_organizations": "Falha ao carregar organizações",
|
||||
"failed_to_load_projects": "Falha ao carregar projetos",
|
||||
"finish": "Terminar",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "Nome completo",
|
||||
"gathering_responses": "Recolhendo respostas",
|
||||
"general": "Geral",
|
||||
"generate": "Gerar",
|
||||
"go_back": "Voltar",
|
||||
"go_to_dashboard": "Ir para o Painel",
|
||||
"hidden": "Escondido",
|
||||
@@ -328,7 +331,7 @@
|
||||
"project_not_found": "Projeto não encontrado",
|
||||
"project_permission_not_found": "Permissão do projeto não encontrada",
|
||||
"projects": "Projetos",
|
||||
"question": "Pergunta",
|
||||
"question": "pergunta",
|
||||
"question_id": "ID da Pergunta",
|
||||
"questions": "Perguntas",
|
||||
"quota": "Cota",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "ID do usuário",
|
||||
"user_not_found": "Usuário não encontrado",
|
||||
"variable": "variável",
|
||||
"variable_ids": "IDs de variáveis",
|
||||
"variables": "Variáveis",
|
||||
"verified_email": "Email Verificado",
|
||||
"video": "vídeo",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "Adicionar classe ou id CSS",
|
||||
"add_regular_expression_here": "Adicionar uma expressão regular aqui",
|
||||
"add_url": "Adicionar URL",
|
||||
"and": "E",
|
||||
"click": "Clica",
|
||||
"contains": "contém",
|
||||
"create_action": "criar ação",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "Limitar a páginas específicas",
|
||||
"matches_regex": "Correspondência regex",
|
||||
"on_all_pages": "Em todas as páginas",
|
||||
"or": "OU",
|
||||
"page_filter": "filtro de página",
|
||||
"page_view": "Visualização de Página",
|
||||
"select_match_type": "Selecionar tipo de partida",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "Contatos atualizados com sucesso",
|
||||
"delete_contact_confirmation": "Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos. Se este contato tiver respostas que contam para cotas da pesquisa, as contagens das cotas serão reduzidas, mas os limites das cotas permanecerão inalterados.}}",
|
||||
"generate_personal_link": "Gerar link pessoal",
|
||||
"generate_personal_link_description": "Selecione uma pesquisa publicada para gerar um link personalizado para este contato.",
|
||||
"no_published_link_surveys_available": "Não há pesquisas de link publicadas disponíveis. Por favor, publique uma pesquisa de link primeiro.",
|
||||
"no_published_surveys": "Sem pesquisas publicadas",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"not_provided": "Não fornecido",
|
||||
"personal_link_generated": "Link pessoal gerado com sucesso",
|
||||
"personal_link_generated_but_clipboard_failed": "Link pessoal gerado, mas falha ao copiar para a área de transferência: {url}",
|
||||
"personal_survey_link": "Link da pesquisa pessoal",
|
||||
"please_select_a_survey": "Por favor, selecione uma pesquisa",
|
||||
"search_contact": "Buscar contato",
|
||||
"select_a_survey": "Selecione uma pesquisa",
|
||||
"select_attribute": "Selecionar Atributo",
|
||||
"unlock_contacts_description": "Gerencie contatos e envie pesquisas direcionadas",
|
||||
"unlock_contacts_title": "Desbloqueie contatos com um plano superior",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Atualizar",
|
||||
"upload_contacts_modal_pick_different_file": "Escolha um arquivo diferente",
|
||||
"upload_contacts_modal_preview": "Aqui está uma prévia dos seus dados.",
|
||||
"upload_contacts_modal_upload_btn": "Fazer upload de contatos"
|
||||
"upload_contacts_modal_upload_btn": "Fazer upload de contatos",
|
||||
"upload_contacts_success": "Contatos carregados com sucesso"
|
||||
},
|
||||
"formbricks_logo": "Logo da Formbricks",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "Conexão do App",
|
||||
"app_connection_description": "Conecte seu app ao Formbricks.",
|
||||
"cache_update_delay_description": "Quando você faz atualizações em pesquisas, contatos, ações ou outros dados, pode levar até 5 minutos para que essas mudanças apareçam no seu app local rodando o SDK do Formbricks. Esse atraso é devido a uma limitação no nosso sistema de cache atual. Estamos ativamente retrabalhando o cache e planejamos lançar uma correção no Formbricks 4.0.",
|
||||
"cache_update_delay_title": "As mudanças serão refletidas após 5 minutos devido ao cache",
|
||||
"app_connection_description": "Conecte seu app ou site ao Formbricks.",
|
||||
"cache_update_delay_description": "Quando você faz atualizações em pesquisas, contatos, ações ou outros dados, pode levar até 1 minuto para que essas alterações apareçam no seu aplicativo local executando o SDK do Formbricks.",
|
||||
"cache_update_delay_title": "As alterações serão refletidas após ~1 minuto devido ao cache",
|
||||
"environment_id": "Seu Id do Ambiente",
|
||||
"environment_id_description": "Este ID identifica exclusivamente este ambiente do Formbricks.",
|
||||
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
|
||||
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.",
|
||||
"formbricks_sdk_not_connected_description": "Conecte seu site ou app com o Formbricks",
|
||||
"formbricks_sdk_not_connected_description": "Adicione o SDK do Formbricks ao seu site ou app para conectá-lo ao Formbricks",
|
||||
"how_to_setup": "Como configurar",
|
||||
"how_to_setup_description": "Siga esses passos para configurar o widget do Formbricks no seu app.",
|
||||
"receiving_data": "Recebendo dados 💃🕺",
|
||||
"recheck": "Verificar novamente",
|
||||
"sdk_connection_details": "Detalhes de Conexão do SDK",
|
||||
"sdk_connection_details_description": "Seu ID de ambiente único e URL de conexão do SDK para integrar o Formbricks com seu aplicativo.",
|
||||
"setup_alert_description": "Siga este tutorial passo a passo para conectar seu app ou site em menos de 5 minutos.",
|
||||
"setup_alert_title": "Como conectar"
|
||||
"setup_alert_title": "Como conectar",
|
||||
"webapp_url": "URL de conexão do SDK"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Esse é seu único projeto, não pode ser deletado. Crie um novo projeto primeiro.",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "Projeto deletado com sucesso",
|
||||
"project_name_settings_description": "Mude o nome do seu projeto.",
|
||||
"project_name_updated_successfully": "Nome do projeto atualizado com sucesso",
|
||||
"recontact_waiting_time": "Tempo de Espera para Recontato",
|
||||
"recontact_waiting_time": "Tempo de espera entre pesquisas em todo o projeto",
|
||||
"recontact_waiting_time_settings_description": "Controle com que frequência os usuários podem ser pesquisados em todas as pesquisas do app.",
|
||||
"this_action_cannot_be_undone": "Essa ação não pode ser desfeita.",
|
||||
"wait_x_days_before_showing_next_survey": "Espere X dias antes de mostrar a próxima pesquisa:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "Adicionar Tag",
|
||||
"count": "Contar",
|
||||
"delete_tag_confirmation": "Tem certeza de que quer deletar essa tag?",
|
||||
"empty_message": "Marque uma submissão para encontrar sua lista de tags aqui.",
|
||||
"manage_tags": "Gerenciar Tags",
|
||||
"manage_tags_description": "Mesclar e remover tags de resposta.",
|
||||
"merge": "mesclar",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"allow_multi_select": "Permitir seleção múltipla",
|
||||
"allow_multiple_files": "Permitir vários arquivos",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir que os usuários selecionem mais de uma imagem",
|
||||
"always_show_survey": "Mostrar pesquisa sempre",
|
||||
"and_launch_surveys_in_your_website_or_app": "e lançar pesquisas no seu site ou app.",
|
||||
"animation": "animação",
|
||||
"app_survey_description": "Embuta uma pesquisa no seu app ou site para coletar respostas.",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"custom_hostname": "Hostname personalizado",
|
||||
"darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.",
|
||||
"date_format": "Formato de data",
|
||||
"days_before_showing_this_survey_again": "dias antes de mostrar essa pesquisa de novo.",
|
||||
"decide_how_often_people_can_answer_this_survey": "Decida com que frequência as pessoas podem responder a essa pesquisa.",
|
||||
"days_before_showing_this_survey_again": "dias após qualquer pesquisa ser mostrada antes que esta pesquisa possa aparecer.",
|
||||
"delete_choice": "Deletar opção",
|
||||
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa de tempo de conclusão da pesquisa",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "Editar link",
|
||||
"edit_recall": "Editar Lembrete",
|
||||
"edit_translations": "Editar traduções de {lang}",
|
||||
"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.",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os respondentes alterem o idioma a qualquer momento. Necessita de no mínimo 2 idiomas ativos.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.",
|
||||
"enable_spam_protection": "Proteção contra spam",
|
||||
"end_screen_card": "cartão de tela final",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "É igual a um de",
|
||||
"error_publishing_survey": "Ocorreu um erro ao publicar a pesquisa.",
|
||||
"error_saving_changes": "Erro ao salvar alterações",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Mesmo depois de eles enviarem uma resposta (por exemplo, Caixa de Feedback)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir múltiplas respostas; continuar mostrando mesmo após uma resposta (ex.: caixa de feedback).",
|
||||
"everyone": "Todo mundo",
|
||||
"external_urls_paywall_tooltip": "Por favor, faça upgrade para personalizar o URL externo. Prevenção de phishing.",
|
||||
"external_urls_paywall_tooltip": "Por favor, faça upgrade para o plano Startup para personalizar URLs externos. Isso nos ajuda a prevenir phishing.",
|
||||
"fallback_missing": "Faltando alternativa",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} é usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "Campo oculto \"{fieldId}\" está sendo usado na cota \"{quotaName}\"",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}",
|
||||
"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",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar mostrando sempre que acionada até que uma resposta seja enviada.",
|
||||
"ignore_global_waiting_time": "Ignorar tempo de espera do projeto",
|
||||
"ignore_global_waiting_time_description": "Esta pesquisa pode ser mostrada sempre que suas condições forem atendidas, mesmo que outra pesquisa tenha sido mostrada recentemente.",
|
||||
"image": "imagem",
|
||||
"includes_all_of": "Inclui tudo de",
|
||||
"includes_one_of": "Inclui um de",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substitua o tema com estilos individuais para essa pesquisa.",
|
||||
"overwrite_global_waiting_time": "Definir tempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Substituir a configuração do projeto apenas para esta pesquisa.",
|
||||
"overwrite_placement": "Substituir posicionamento",
|
||||
"overwrite_the_global_placement_of_the_survey": "Substituir a posição global da pesquisa",
|
||||
"overwrites_waiting_period_between_surveys_to_x_days": "Substitui o período de espera entre as pesquisas para {days} dia(s).",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Escolha um fundo da nossa biblioteca ou faça upload do seu próprio.",
|
||||
"picture_idx": "Imagem {idx}",
|
||||
"pin_can_only_contain_numbers": "O PIN só pode conter números.",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "alcance",
|
||||
"recall_data": "Lembrar dados",
|
||||
"recall_information_from": "Recuperar informações de ...",
|
||||
"recontact_options": "Opções de Recontato",
|
||||
"recontact_options_section": "Opções de recontato",
|
||||
"recontact_options_section_description": "Se o tempo de espera permitir, escolha com que frequência esta pesquisa pode ser mostrada a uma pessoa.",
|
||||
"redirect_thank_you_card": "Redirecionar cartão de agradecimento",
|
||||
"redirect_to_url": "Redirecionar para URL",
|
||||
"remove_description": "Remover descrição",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "Obrigatório",
|
||||
"reset_to_theme_styles": "Redefinir para estilos do tema",
|
||||
"reset_to_theme_styles_main_text": "Tem certeza de que quer redefinir o estilo para o tema padrão? Isso vai remover todas as personalizações.",
|
||||
"respect_global_waiting_time": "Usar tempo de espera do projeto",
|
||||
"respect_global_waiting_time_description": "Esta pesquisa segue o tempo de espera definido na configuração do projeto. Ela só é mostrada se nenhuma outra pesquisa tiver aparecido durante esse período.",
|
||||
"response_limit_can_t_be_set_to_0": "Limite de resposta não pode ser 0",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).",
|
||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "Mostrar configurações avançadas",
|
||||
"show_button": "Mostrar Botão",
|
||||
"show_language_switch": "Mostrar troca de idioma",
|
||||
"show_multiple_times": "Mostrar várias vezes",
|
||||
"show_multiple_times": "Mostrar um número limitado de vezes",
|
||||
"show_only_once": "Mostrar só uma vez",
|
||||
"show_survey_maximum_of": "Mostrar no máximo",
|
||||
"show_survey_to_users": "Mostrar pesquisa para % dos usuários",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "Ative o modo multilíngue para começar 👉",
|
||||
"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.",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Mostrar no máximo o número especificado de vezes, ou até que respondam (o que ocorrer primeiro).",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar uma única vez, mesmo que não respondam.",
|
||||
"then": "Então",
|
||||
"this_action_will_remove_all_the_translations_from_this_survey": "Essa ação vai remover todas as traduções dessa pesquisa.",
|
||||
"this_extension_is_already_added": "Essa extensão já foi adicionada.",
|
||||
"this_file_type_is_not_supported": "Esse tipo de arquivo não é suportado.",
|
||||
"this_setting_overwrites_your": "Essa configuração sobrescreve seu",
|
||||
"three_points": "3 pontos",
|
||||
"times": "times",
|
||||
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todas as pesquisas, você pode",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "Direcione grupos específicos de usuários com base em atributos ou informações do dispositivo",
|
||||
"unlock_targeting_title": "Desbloqueie o direcionamento com um plano superior",
|
||||
"unsaved_changes_warning": "Você tem alterações não salvas na sua pesquisa. Quer salvar antes de sair?",
|
||||
"until_they_submit_a_response": "Até eles enviarem uma resposta",
|
||||
"until_they_submit_a_response": "Perguntar até que enviem uma resposta",
|
||||
"upgrade_notice_description": "Crie pesquisas multilíngues e desbloqueie muitas outras funcionalidades",
|
||||
"upgrade_notice_title": "Desbloqueie pesquisas multilíngues com um plano superior",
|
||||
"upload": "Enviar",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "Etiqueta Superior",
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL não suportada",
|
||||
"use_with_caution": "Use com cuidado",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} está sendo usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está sendo usada na cota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "Variável \"{variable}\" está sendo recordada no Card de Boas-Vindas.",
|
||||
"verify_email_before_submission": "Verifique o e-mail antes de enviar",
|
||||
"verify_email_before_submission_description": "Deixe só quem tem um email real responder.",
|
||||
"visibility_and_recontact": "Visibilidade e recontato",
|
||||
"visibility_and_recontact_description": "Controle quando esta pesquisa pode aparecer e com que frequência pode reaparecer.",
|
||||
"wait": "Espera",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Espera alguns segundos depois do gatilho antes de mostrar a pesquisa",
|
||||
"waiting_period": "período de espera",
|
||||
"waiting_time_across_surveys": "Tempo de espera em todo o projeto",
|
||||
"waiting_time_across_surveys_description": "Para evitar fadiga de pesquisas, escolha como esta pesquisa interage com o tempo de espera em todo o projeto.",
|
||||
"welcome_message": "Mensagem de boas-vindas",
|
||||
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Quando as condições forem atendidas, o tempo de espera será ignorado e a pesquisa será exibida.",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus usuários podem ser pesquisados.",
|
||||
"you_have_not_created_a_segment_yet": "Você ainda não criou um segmento.",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations": "Você precisa ter dois ou mais idiomas configurados no seu projeto para trabalhar com traduções.",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "Sobrenome",
|
||||
"not_completed": "Não Concluído ⏳",
|
||||
"os": "sistema operacional",
|
||||
"person_attributes": "Atributos da pessoa",
|
||||
"person_attributes": "Atributos da pessoa no momento do envio",
|
||||
"phone": "Celular",
|
||||
"respondent_skipped_questions": "Respondente pulou essas perguntas.",
|
||||
"response_deleted_successfully": "Resposta deletada com sucesso.",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} foi pulada",
|
||||
"aggregated": "Agregado",
|
||||
"all_responses_csv": "Todas as respostas (CSV)",
|
||||
"all_responses_excel": "Todas as respostas (Excel)",
|
||||
"all_time": "Todo o tempo",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "Respostas filtradas (CSV)",
|
||||
"filtered_responses_excel": "Respostas filtradas (Excel)",
|
||||
"generating_qr_code": "Gerando código QR",
|
||||
"go_to_setup_checklist": "Vai para a Lista de Configuração 👉",
|
||||
"impressions": "Impressões",
|
||||
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "Inclui tudo",
|
||||
"includes_either": "Inclui ou",
|
||||
"install_widget": "Instalar Widget do Formbricks",
|
||||
"individual": "Individual",
|
||||
"is_equal_to": "É igual a",
|
||||
"is_less_than": "É menor que",
|
||||
"last_30_days": "Últimos 30 dias",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "No geral",
|
||||
"promoters": "Promotores",
|
||||
"qr_code": "Código QR",
|
||||
"qr_code_description": "Respostas coletadas via código QR são anônimas.",
|
||||
"qr_code_download_failed": "falha no download do código QR",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "Número de cotas preenchidas pelos respondentes.",
|
||||
"reset_survey": "Redefinir pesquisa",
|
||||
"reset_survey_warning": "Redefinir uma pesquisa remove todas as respostas e exibições associadas a esta pesquisa. Isto não pode ser desfeito.",
|
||||
"satisfied": "Satisfeito",
|
||||
"selected_responses_csv": "Respostas selecionadas (CSV)",
|
||||
"selected_responses_excel": "Respostas selecionadas (Excel)",
|
||||
"setup_integrations": "Configurar integrações",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "Tempo médio para completar a pergunta.",
|
||||
"unknown_question_type": "Tipo de pergunta desconhecido",
|
||||
"use_personal_links": "Use links pessoais",
|
||||
"waiting_for_response": "Aguardando uma resposta 🧘♂️",
|
||||
"whats_next": "E agora?",
|
||||
"your_survey_is_public": "Sua pesquisa é pública",
|
||||
"youre_not_plugged_in_yet": "Você ainda não tá conectado!"
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "Limpar filtros",
|
||||
"clear_selection": "Limpar seleção",
|
||||
"click": "Clique",
|
||||
"click_to_filter": "Clique para filtrar",
|
||||
"clicks": "Cliques",
|
||||
"close": "Fechar",
|
||||
"code": "Código",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "Número máximo de pedidos alcançado. Por favor, tente novamente mais tarde.",
|
||||
"error_rate_limit_title": "Limite de Taxa Excedido",
|
||||
"expand_rows": "Expandir linhas",
|
||||
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
|
||||
"failed_to_load_organizations": "Falha ao carregar organizações",
|
||||
"failed_to_load_projects": "Falha ao carregar projetos",
|
||||
"finish": "Concluir",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "Nome completo",
|
||||
"gathering_responses": "A recolher respostas",
|
||||
"general": "Geral",
|
||||
"generate": "Gerar",
|
||||
"go_back": "Voltar",
|
||||
"go_to_dashboard": "Ir para o Painel",
|
||||
"hidden": "Oculto",
|
||||
@@ -328,7 +331,7 @@
|
||||
"project_not_found": "Projeto não encontrado",
|
||||
"project_permission_not_found": "Permissão do projeto não encontrada",
|
||||
"projects": "Projetos",
|
||||
"question": "Pergunta",
|
||||
"question": "pergunta",
|
||||
"question_id": "ID da pergunta",
|
||||
"questions": "Perguntas",
|
||||
"quota": "Quota",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "ID do Utilizador",
|
||||
"user_not_found": "Utilizador não encontrado",
|
||||
"variable": "Variável",
|
||||
"variable_ids": "IDs de variáveis",
|
||||
"variables": "Variáveis",
|
||||
"verified_email": "Email verificado",
|
||||
"video": "Vídeo",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "Adicionar classe ou id CSS",
|
||||
"add_regular_expression_here": "Adicione uma expressão regular aqui",
|
||||
"add_url": "Adicionar URL",
|
||||
"and": "E",
|
||||
"click": "Clique",
|
||||
"contains": "Contém",
|
||||
"create_action": "Criar ação",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "Limitar a páginas específicas",
|
||||
"matches_regex": "Coincide com regex",
|
||||
"on_all_pages": "Em todas as páginas",
|
||||
"or": "OU",
|
||||
"page_filter": "Filtro de página",
|
||||
"page_view": "Visualização de Página",
|
||||
"select_match_type": "Selecionar tipo de correspondência",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "Contactos atualizados com sucesso",
|
||||
"delete_contact_confirmation": "Isto irá eliminar todas as respostas das pesquisas e os atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isto irá eliminar todas as respostas das pesquisas e os atributos de contacto associados a este contacto. Qualquer segmentação e personalização baseados nos dados deste contacto serão perdidos. Se este contacto tiver respostas que contribuam para as quotas das pesquisas, as contagens de quotas serão reduzidas, mas os limites das quotas permanecerão inalterados.}}",
|
||||
"generate_personal_link": "Gerar Link Pessoal",
|
||||
"generate_personal_link_description": "Selecione um inquérito publicado para gerar um link personalizado para este contacto.",
|
||||
"no_published_link_surveys_available": "Não existem inquéritos de link publicados disponíveis. Por favor, publique primeiro um inquérito de link.",
|
||||
"no_published_surveys": "Sem inquéritos publicados",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"not_provided": "Não fornecido",
|
||||
"personal_link_generated": "Link pessoal gerado com sucesso",
|
||||
"personal_link_generated_but_clipboard_failed": "Link pessoal gerado mas falha ao copiar para a área de transferência: {url}",
|
||||
"personal_survey_link": "Link do inquérito pessoal",
|
||||
"please_select_a_survey": "Por favor, selecione um inquérito",
|
||||
"search_contact": "Procurar contacto",
|
||||
"select_a_survey": "Selecione um inquérito",
|
||||
"select_attribute": "Selecionar Atributo",
|
||||
"unlock_contacts_description": "Gerir contactos e enviar inquéritos direcionados",
|
||||
"unlock_contacts_title": "Desbloqueie os contactos com um plano superior",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Atualizar",
|
||||
"upload_contacts_modal_pick_different_file": "Escolher um ficheiro diferente",
|
||||
"upload_contacts_modal_preview": "Aqui está uma pré-visualização dos seus dados.",
|
||||
"upload_contacts_modal_upload_btn": "Carregar contactos"
|
||||
"upload_contacts_modal_upload_btn": "Carregar contactos",
|
||||
"upload_contacts_success": "Contactos carregados com sucesso"
|
||||
},
|
||||
"formbricks_logo": "Logotipo do Formbricks",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "Conexão de aplicação",
|
||||
"app_connection_description": "Conecte a sua aplicação ao Formbricks",
|
||||
"cache_update_delay_description": "Quando fizer atualizações para inquéritos, contactos, ações ou outros dados, pode demorar até 5 minutos para que essas alterações apareçam na sua aplicação local a correr o SDK do Formbricks. Este atraso deve-se a uma limitação no nosso atual sistema de cache. Estamos a trabalhar ativamente na reformulação da cache e lançaremos uma correção no Formbricks 4.0.",
|
||||
"cache_update_delay_title": "As alterações serão refletidas após 5 minutos devido ao armazenamento em cache.",
|
||||
"app_connection_description": "Ligue a sua aplicação ou website ao Formbricks.",
|
||||
"cache_update_delay_description": "Quando faz atualizações a inquéritos, contactos, ações ou outros dados, pode demorar até 1 minuto para que essas alterações apareçam na sua aplicação local que executa o SDK Formbricks.",
|
||||
"cache_update_delay_title": "As alterações serão refletidas após ~1 minuto devido ao armazenamento em cache",
|
||||
"environment_id": "O seu identificador",
|
||||
"environment_id_description": "Este id identifica o seu espaço Formbricks.",
|
||||
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
|
||||
"formbricks_sdk_not_connected": "O Formbricks SDK ainda não está conectado",
|
||||
"formbricks_sdk_not_connected_description": "Ligue o seu website ou aplicação ao Formbricks",
|
||||
"formbricks_sdk_not_connected_description": "Adicione o SDK do Formbricks ao seu website ou aplicação para o ligar ao Formbricks",
|
||||
"how_to_setup": "Como configurar",
|
||||
"how_to_setup_description": "Siga estes passos para configurar o widget Formbricks na sua aplicação.",
|
||||
"receiving_data": "A receber dados 💃🕺",
|
||||
"recheck": "Verificar novamente",
|
||||
"sdk_connection_details": "Detalhes de Conexão SDK",
|
||||
"sdk_connection_details_description": "O seu ID de ambiente único e URL de conexão SDK para integrar o Formbricks com a sua aplicação.",
|
||||
"setup_alert_description": "Siga este tutorial passo a passo para conectar a sua app ou site em menos de 5 minutos",
|
||||
"setup_alert_title": "Como conectar"
|
||||
"setup_alert_title": "Como conectar",
|
||||
"webapp_url": "URL de ligação do SDK"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Este é o seu único projeto, não pode ser eliminado. Crie um novo projeto primeiro.",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "Projeto eliminado com sucesso",
|
||||
"project_name_settings_description": "Altere o nome dos seus projetos.",
|
||||
"project_name_updated_successfully": "Nome do projeto atualizado com sucesso",
|
||||
"recontact_waiting_time": "Tempo de espera de recontacto",
|
||||
"recontact_waiting_time": "Tempo de Espera Entre Inquéritos em Todo o Projeto",
|
||||
"recontact_waiting_time_settings_description": "Controle a regularidade com que os utilizadores podem ser inquiridos em todos os inquéritos da aplicação.",
|
||||
"this_action_cannot_be_undone": "Esta ação não pode ser desfeita.",
|
||||
"wait_x_days_before_showing_next_survey": "Dias de espera:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "Adicionar Etiqueta",
|
||||
"count": "Contagem",
|
||||
"delete_tag_confirmation": "Tem a certeza de que deseja eliminar esta etiqueta?",
|
||||
"empty_message": "Crie etiquetas para as suas submissões e veja-as aqui",
|
||||
"manage_tags": "Gerir Etiquetas",
|
||||
"manage_tags_description": "Junte e remova etiquetas de resposta",
|
||||
"merge": "Fundir",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"allow_multi_select": "Permitir seleção múltipla",
|
||||
"allow_multiple_files": "Permitir vários ficheiros",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir aos utilizadores selecionar mais do que uma imagem",
|
||||
"always_show_survey": "Mostrar sempre o inquérito",
|
||||
"and_launch_surveys_in_your_website_or_app": "e lance inquéritos no seu site ou aplicação.",
|
||||
"animation": "Animação",
|
||||
"app_survey_description": "Incorpore um inquérito na sua aplicação web ou site para recolher respostas.",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"custom_hostname": "Nome do host personalizado",
|
||||
"darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.",
|
||||
"date_format": "Formato da data",
|
||||
"days_before_showing_this_survey_again": "dias antes de mostrar este inquérito novamente.",
|
||||
"decide_how_often_people_can_answer_this_survey": "Decida com que frequência as pessoas podem responder a este inquérito.",
|
||||
"days_before_showing_this_survey_again": "dias após qualquer inquérito ser mostrado antes que este inquérito possa aparecer.",
|
||||
"delete_choice": "Eliminar escolha",
|
||||
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa do tempo de conclusão do inquérito",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "Editar link",
|
||||
"edit_recall": "Editar Lembrete",
|
||||
"edit_translations": "Editar traduções {lang}",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir aos participantes mudar a língua do inquérito a qualquer momento durante o inquérito.",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os inquiridos mudem de idioma a qualquer momento. Necessita de pelo menos 2 idiomas ativos.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.",
|
||||
"enable_spam_protection": "Proteção contra spam",
|
||||
"end_screen_card": "Cartão de ecrã final",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "Igual a um de",
|
||||
"error_publishing_survey": "Ocorreu um erro ao publicar o questionário.",
|
||||
"error_saving_changes": "Erro ao guardar alterações",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Mesmo depois de terem enviado uma resposta (por exemplo, Caixa de Feedback)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir múltiplas respostas; continuar a mostrar mesmo após uma resposta (por exemplo, Caixa de Feedback).",
|
||||
"everyone": "Todos",
|
||||
"external_urls_paywall_tooltip": "Por favor, atualize para personalizar o URL externo. Prevenção contra phishing.",
|
||||
"external_urls_paywall_tooltip": "Por favor, atualize para o plano Startup para personalizar URLs externos. Isto ajuda-nos a prevenir o phishing.",
|
||||
"fallback_missing": "Substituição em falta",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} é usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "Campo oculto \"{fieldId}\" está a ser usado na quota \"{quotaName}\"",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "Nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Se precisar de mais, por favor",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Se realmente quiser essa resposta, pergunte até obtê-la.",
|
||||
"ignore_waiting_time_between_surveys": "Ignorar tempo de espera entre inquéritos",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar a mostrar sempre que acionado até que uma resposta seja submetida.",
|
||||
"ignore_global_waiting_time": "Ignorar tempo de espera de todo o projeto",
|
||||
"ignore_global_waiting_time_description": "Este inquérito pode ser mostrado sempre que as suas condições forem cumpridas, mesmo que outro inquérito tenha sido mostrado recentemente.",
|
||||
"image": "Imagem",
|
||||
"includes_all_of": "Inclui todos de",
|
||||
"includes_one_of": "Inclui um de",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substituir o tema com estilos individuais para este inquérito.",
|
||||
"overwrite_global_waiting_time": "Definir tempo de espera personalizado",
|
||||
"overwrite_global_waiting_time_description": "Substituir a configuração do projeto apenas para este inquérito.",
|
||||
"overwrite_placement": "Substituir colocação",
|
||||
"overwrite_the_global_placement_of_the_survey": "Substituir a colocação global do inquérito",
|
||||
"overwrites_waiting_period_between_surveys_to_x_days": "Substitui o período de espera entre inquéritos para {days} dia(s).",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Escolha um fundo da nossa biblioteca ou carregue o seu próprio.",
|
||||
"picture_idx": "Imagem {idx}",
|
||||
"pin_can_only_contain_numbers": "O PIN só pode conter números.",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "Intervalo",
|
||||
"recall_data": "Recuperar dados",
|
||||
"recall_information_from": "Recordar informação de ...",
|
||||
"recontact_options": "Opções de Recontacto",
|
||||
"recontact_options_section": "Opções de recontacto",
|
||||
"recontact_options_section_description": "Se o tempo de espera permitir, escolha com que frequência este inquérito pode ser mostrado a uma pessoa.",
|
||||
"redirect_thank_you_card": "Redirecionar cartão de agradecimento",
|
||||
"redirect_to_url": "Redirecionar para Url",
|
||||
"remove_description": "Remover descrição",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "Obrigatório",
|
||||
"reset_to_theme_styles": "Repor para estilos do tema",
|
||||
"reset_to_theme_styles_main_text": "Tem a certeza de que deseja repor o estilo para os estilos do tema? Isto irá remover todos os estilos personalizados.",
|
||||
"respect_global_waiting_time": "Usar tempo de espera de todo o projeto",
|
||||
"respect_global_waiting_time_description": "Este inquérito segue o tempo de espera definido na configuração do projeto. Só é mostrado se nenhum outro inquérito tiver aparecido durante esse período.",
|
||||
"response_limit_can_t_be_set_to_0": "O limite de respostas não pode ser definido como 0",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).",
|
||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "Mostrar definições avançadas",
|
||||
"show_button": "Mostrar Botão",
|
||||
"show_language_switch": "Mostrar alternador de idioma",
|
||||
"show_multiple_times": "Mostrar várias vezes",
|
||||
"show_multiple_times": "Mostrar um número limitado de vezes",
|
||||
"show_only_once": "Mostrar apenas uma vez",
|
||||
"show_survey_maximum_of": "Mostrar inquérito máximo de",
|
||||
"show_survey_to_users": "Mostrar inquérito a % dos utilizadores",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "Ative o modo multilingue para começar 👉",
|
||||
"targeted": "Alvo",
|
||||
"ten_points": "10 pontos",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "O inquérito será mostrado várias vezes até que respondam",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "O inquérito será mostrado uma vez, mesmo que a pessoa não responda.",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Mostrar no máximo o número especificado de vezes, ou até que respondam (o que ocorrer primeiro).",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar uma única vez, mesmo que não respondam.",
|
||||
"then": "Então",
|
||||
"this_action_will_remove_all_the_translations_from_this_survey": "Esta ação irá remover todas as traduções deste inquérito.",
|
||||
"this_extension_is_already_added": "Esta extensão já está adicionada.",
|
||||
"this_file_type_is_not_supported": "Este tipo de ficheiro não é suportado.",
|
||||
"this_setting_overwrites_your": "Esta configuração substitui o seu",
|
||||
"three_points": "3 pontos",
|
||||
"times": "tempos",
|
||||
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todos os questionários, pode",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "Alvo de grupos de utilizadores específicos com base em atributos ou informações do dispositivo",
|
||||
"unlock_targeting_title": "Desbloqueie a segmentação com um plano superior",
|
||||
"unsaved_changes_warning": "Tem alterações não guardadas no seu inquérito. Gostaria de as guardar antes de sair?",
|
||||
"until_they_submit_a_response": "Até que enviem uma resposta",
|
||||
"until_they_submit_a_response": "Perguntar até que submetam uma resposta",
|
||||
"upgrade_notice_description": "Crie inquéritos multilingues e desbloqueie muitas mais funcionalidades",
|
||||
"upgrade_notice_title": "Desbloqueie inquéritos multilingues com um plano superior",
|
||||
"upload": "Carregar",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "Etiqueta Superior",
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL não suportado",
|
||||
"use_with_caution": "Usar com cautela",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está a ser utilizada na quota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "Variável \"{variable}\" está a ser recordada no cartão de boas-vindas.",
|
||||
"verify_email_before_submission": "Verificar email antes da submissão",
|
||||
"verify_email_before_submission_description": "Permitir apenas que pessoas com um email real respondam.",
|
||||
"visibility_and_recontact": "Visibilidade e Recontacto",
|
||||
"visibility_and_recontact_description": "Controlar quando este inquérito pode aparecer e com que frequência pode reaparecer.",
|
||||
"wait": "Aguardar",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Aguarde alguns segundos após o gatilho antes de mostrar o inquérito",
|
||||
"waiting_period": "período de espera",
|
||||
"waiting_time_across_surveys": "Tempo de espera em todo o projeto",
|
||||
"waiting_time_across_surveys_description": "Para evitar a fadiga de inquéritos, escolha como este inquérito interage com o tempo de espera em todo o projeto.",
|
||||
"welcome_message": "Mensagem de boas-vindas",
|
||||
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Quando as condições corresponderem, o tempo de espera será ignorado e o inquérito será mostrado.",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus utilizadores podem ser pesquisados.",
|
||||
"you_have_not_created_a_segment_yet": "Ainda não criou um segmento",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations": "Precisa de ter duas ou mais línguas configuradas no seu projeto para trabalhar com traduções.",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "Apelido",
|
||||
"not_completed": "Não Concluído ⏳",
|
||||
"os": "SO",
|
||||
"person_attributes": "Atributos da pessoa",
|
||||
"person_attributes": "Atributos da pessoa no momento da submissão",
|
||||
"phone": "Telefone",
|
||||
"respondent_skipped_questions": "O respondente saltou estas perguntas.",
|
||||
"response_deleted_successfully": "Resposta eliminada com sucesso.",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é ignorada",
|
||||
"aggregated": "Agregado",
|
||||
"all_responses_csv": "Todas as respostas (CSV)",
|
||||
"all_responses_excel": "Todas as respostas (Excel)",
|
||||
"all_time": "Todo o tempo",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "Respostas filtradas (CSV)",
|
||||
"filtered_responses_excel": "Respostas filtradas (Excel)",
|
||||
"generating_qr_code": "A gerar código QR",
|
||||
"go_to_setup_checklist": "Ir para a Lista de Verificação de Configuração 👉",
|
||||
"impressions": "Impressões",
|
||||
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "Inclui tudo",
|
||||
"includes_either": "Inclui qualquer um",
|
||||
"install_widget": "Instalar Widget Formbricks",
|
||||
"individual": "Individual",
|
||||
"is_equal_to": "É igual a",
|
||||
"is_less_than": "É menos que",
|
||||
"last_30_days": "Últimos 30 dias",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "Geral",
|
||||
"promoters": "Promotores",
|
||||
"qr_code": "Código QR",
|
||||
"qr_code_description": "Respostas recolhidas através de código QR são anónimas.",
|
||||
"qr_code_download_failed": "Falha ao transferir o código QR",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "O número de quotas concluídas pelos respondentes.",
|
||||
"reset_survey": "Reiniciar inquérito",
|
||||
"reset_survey_warning": "Repor um inquérito remove todas as respostas e visualizações associadas a este inquérito. Isto não pode ser desfeito.",
|
||||
"satisfied": "Satisfeito",
|
||||
"selected_responses_csv": "Respostas selecionadas (CSV)",
|
||||
"selected_responses_excel": "Respostas selecionadas (Excel)",
|
||||
"setup_integrations": "Configurar integrações",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "Tempo médio para concluir a pergunta.",
|
||||
"unknown_question_type": "Tipo de Pergunta Desconhecido",
|
||||
"use_personal_links": "Utilize links pessoais",
|
||||
"waiting_for_response": "A aguardar uma resposta 🧘♂️",
|
||||
"whats_next": "O que se segue?",
|
||||
"your_survey_is_public": "O seu inquérito é público",
|
||||
"youre_not_plugged_in_yet": "Ainda não está ligado!"
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "Curăță filtrele",
|
||||
"clear_selection": "Șterge selecția",
|
||||
"click": "Click",
|
||||
"click_to_filter": "Click pentru a filtra",
|
||||
"clicks": "Clickuri",
|
||||
"close": "Închide",
|
||||
"code": "Cod",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "Numărul maxim de cereri atins. Vă rugăm să încercați din nou mai târziu.",
|
||||
"error_rate_limit_title": "Limită de cereri depășită",
|
||||
"expand_rows": "Extinde rândurile",
|
||||
"failed_to_copy_to_clipboard": "Nu s-a reușit copierea în clipboard",
|
||||
"failed_to_load_organizations": "Nu s-a reușit încărcarea organizațiilor",
|
||||
"failed_to_load_projects": "Nu s-a reușit încărcarea proiectelor",
|
||||
"finish": "Finalizează",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "Nume complet",
|
||||
"gathering_responses": "Culegere răspunsuri",
|
||||
"general": "General",
|
||||
"generate": "Generează",
|
||||
"go_back": "Înapoi",
|
||||
"go_to_dashboard": "Mergi la Tablou de Bord",
|
||||
"hidden": "Ascuns",
|
||||
@@ -328,7 +331,7 @@
|
||||
"project_not_found": "Proiectul nu a fost găsit",
|
||||
"project_permission_not_found": "Permisiunea proiectului nu a fost găsită",
|
||||
"projects": "Proiecte",
|
||||
"question": "Întrebare",
|
||||
"question": "întrebare",
|
||||
"question_id": "ID întrebare",
|
||||
"questions": "Întrebări",
|
||||
"quota": "Cotă",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "ID Utilizator",
|
||||
"user_not_found": "Utilizatorul nu a fost găsit",
|
||||
"variable": "Variabilă",
|
||||
"variable_ids": "ID-uri variabile",
|
||||
"variables": "Variante",
|
||||
"verified_email": "Email verificat",
|
||||
"video": "Video",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "Adăugați clasă CSS sau id",
|
||||
"add_regular_expression_here": "Adăugați o expresie regulată aici",
|
||||
"add_url": "Adaugă URL",
|
||||
"and": "ȘI",
|
||||
"click": "Click",
|
||||
"contains": "Conține",
|
||||
"create_action": "Creează acțiune",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "Limitează la pagini specifice",
|
||||
"matches_regex": "Se potrivește cu regex",
|
||||
"on_all_pages": "Pe toate paginile",
|
||||
"or": "SAU",
|
||||
"page_filter": "Filtru pagină",
|
||||
"page_view": "Vizualizare Pagina",
|
||||
"select_match_type": "Selectați tipul de potrivire",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "Contactele au fost actualizate cu succes",
|
||||
"delete_contact_confirmation": "Acest lucru va șterge toate răspunsurile la sondaj și atributele de contact asociate cu acest contact. Orice țintire și personalizare bazată pe datele acestui contact vor fi pierdute.",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Această acțiune va șterge toate răspunsurile chestionarului și atributele de contact asociate cu acest contact. Orice țintire și personalizare bazată pe datele acestui contact vor fi pierdute. Dacă acest contact are răspunsuri care contează pentru cotele chestionarului, numărul cotelor va fi redus, dar limitele cotelor vor rămâne neschimbate.} other {Aceste acțiuni vor șterge toate răspunsurile chestionarului și atributele de contact asociate cu acești contacți. Orice țintire și personalizare bazată pe datele acestor contacți vor fi pierdute. Dacă acești contacți au răspunsuri care contează pentru cotele chestionarului, numărul cotelor va fi redus, dar limitele cotelor vor rămâne neschimbate.} }",
|
||||
"generate_personal_link": "Generează link personal",
|
||||
"generate_personal_link_description": "Selectați un sondaj publicat pentru a genera un link personalizat pentru acest contact.",
|
||||
"no_published_link_surveys_available": "Nu există sondaje publicate pentru linkuri disponibile. Vă rugăm să publicați mai întâi un sondaj pentru linkuri.",
|
||||
"no_published_surveys": "Nu există sondaje publicate",
|
||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||
"not_provided": "Nu a fost furnizat",
|
||||
"personal_link_generated": "Linkul personal a fost generat cu succes",
|
||||
"personal_link_generated_but_clipboard_failed": "Linkul personal a fost generat, dar nu s-a reușit copierea în clipboard: {url}",
|
||||
"personal_survey_link": "Link către sondajul personal",
|
||||
"please_select_a_survey": "Vă rugăm să selectați un sondaj",
|
||||
"search_contact": "Căutați contact",
|
||||
"select_a_survey": "Selectați un sondaj",
|
||||
"select_attribute": "Selectează atributul",
|
||||
"unlock_contacts_description": "Gestionează contactele și trimite sondaje țintite",
|
||||
"unlock_contacts_title": "Deblocați contactele cu un plan superior.",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Actualizare",
|
||||
"upload_contacts_modal_pick_different_file": "Selectați un alt fișier",
|
||||
"upload_contacts_modal_preview": "Iată o previzualizare a datelor tale.",
|
||||
"upload_contacts_modal_upload_btn": "Încărcați contacte"
|
||||
"upload_contacts_modal_upload_btn": "Încărcați contacte",
|
||||
"upload_contacts_success": "Contactele au fost încărcate cu succes"
|
||||
},
|
||||
"formbricks_logo": "Logo Formbricks",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "Conectare aplicație",
|
||||
"app_connection_description": "Conectează aplicația ta la Formbricks.",
|
||||
"cache_update_delay_description": "Când faci actualizări la sondaje, contacte, acțiuni sau alte date, poate dura până la 5 minute pentru ca aceste modificări să apară în aplicația locală care rulează SDK Formbricks. Această întârziere se datorează unei limitări în sistemul nostru actual de caching. Revedem activ cache-ul și vom lansa o soluție în Formbricks 4.0.",
|
||||
"cache_update_delay_title": "Modificările vor fi reflectate după 5 minute datorită memorării în cache",
|
||||
"app_connection_description": "Conectează-ți aplicația sau site-ul la Formbricks.",
|
||||
"cache_update_delay_description": "Când efectuați actualizări la sondaje, contacte, acțiuni sau alte date, poate dura până la 1 minut pentru ca aceste modificări să apară în aplicația locală care rulează SDK-ul Formbricks.",
|
||||
"cache_update_delay_title": "Modificările vor fi vizibile după ~1 minut din cauza memoriei cache",
|
||||
"environment_id": "ID-ul mediului tău",
|
||||
"environment_id_description": "Acest id identifică în mod unic acest mediu Formbricks.",
|
||||
"formbricks_sdk_connected": "SDK Formbricks este conectat",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK nu este încă conectat.",
|
||||
"formbricks_sdk_not_connected_description": "Conectează-ți site-ul sau aplicația cu Formbricks",
|
||||
"formbricks_sdk_not_connected_description": "Adaugă SDK-ul Formbricks pe site-ul sau în aplicația ta pentru a-l conecta la Formbricks.",
|
||||
"how_to_setup": "Cum să configurezi",
|
||||
"how_to_setup_description": "Urmează acești pași pentru a configura widget-ul Formbricks în aplicația ta.",
|
||||
"receiving_data": "Recepționare date 💃🕺",
|
||||
"recheck": "Re-verifică",
|
||||
"sdk_connection_details": "Detalii de conexiune SDK",
|
||||
"sdk_connection_details_description": "ID-ul mediului tău unic și URL-ul de conexiune SDK pentru a integra Formbricks cu aplicația ta.",
|
||||
"setup_alert_description": "Urmează acest tutorial pas cu pas pentru a-ți conecta aplicația sau site-ul în mai puțin de 5 minute.",
|
||||
"setup_alert_title": "Cum să conectezi"
|
||||
"setup_alert_title": "Cum să conectezi",
|
||||
"webapp_url": "URL de conexiune SDK"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Acesta este singurul tău proiect, nu poate fi șters. Creează mai întâi un proiect nou.",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "Proiect șters cu succes!",
|
||||
"project_name_settings_description": "Schimbați numele proiectului.",
|
||||
"project_name_updated_successfully": "Numele proiectului actualizat cu succes",
|
||||
"recontact_waiting_time": "Timp de așteptare până la recontactare",
|
||||
"recontact_waiting_time": "Timp de așteptare la nivel de proiect între sondaje",
|
||||
"recontact_waiting_time_settings_description": "Controlează cât de des pot fi utilizatorii chestionați în toate sondajele din aplicație.",
|
||||
"this_action_cannot_be_undone": "Această acțiune nu poate fi anulată.",
|
||||
"wait_x_days_before_showing_next_survey": "Așteaptă X zile înainte de a afișa următorul sondaj:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "Adaugă Etichetă",
|
||||
"count": "Număr",
|
||||
"delete_tag_confirmation": "Sigur doriți să ștergeți această etichetă?",
|
||||
"empty_message": "Marcați o trimitere pentru a găsi lista de etichete aici.",
|
||||
"manage_tags": "Gestionați etichetele",
|
||||
"manage_tags_description": "Îmbinați și eliminați etichetele de răspuns.",
|
||||
"merge": "Îmbinare",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"allow_multi_select": "Permite selectare multiplă",
|
||||
"allow_multiple_files": "Permite fișiere multiple",
|
||||
"allow_users_to_select_more_than_one_image": "Permite utilizatorilor să selecteze mai mult de o imagine",
|
||||
"always_show_survey": "Arată întotdeauna sondajul",
|
||||
"and_launch_surveys_in_your_website_or_app": "și lansați chestionare pe site-ul sau în aplicația dvs.",
|
||||
"animation": "Animație",
|
||||
"app_survey_description": "Incorporați un chestionar în aplicația web sau pe site-ul dvs. pentru a colecta răspunsuri.",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"custom_hostname": "Gazdă personalizată",
|
||||
"darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.",
|
||||
"date_format": "Format dată",
|
||||
"days_before_showing_this_survey_again": "zile înainte de a afișa din nou acest sondaj.",
|
||||
"decide_how_often_people_can_answer_this_survey": "Decide cât de des pot răspunde oamenii la acest sondaj",
|
||||
"days_before_showing_this_survey_again": "zile după afișarea oricărui sondaj înainte ca acest sondaj să poată apărea din nou.",
|
||||
"delete_choice": "Șterge alegerea",
|
||||
"disable_the_visibility_of_survey_progress": "Dezactivați vizibilitatea progresului sondajului",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Afișează o estimare a timpului de finalizare pentru sondaj",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "Editare legătură",
|
||||
"edit_recall": "Editează Referințele",
|
||||
"edit_translations": "Editează traducerile {lang}",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permite participanților să schimbe limba sondajului în orice moment în timpul sondajului.",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permite respondenților să schimbe limba în orice moment. Necesită minimum 2 limbi active.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Protecția împotriva spamului folosește reCAPTCHA v3 pentru a filtra răspunsurile de spam.",
|
||||
"enable_spam_protection": "Protecția împotriva spamului",
|
||||
"end_screen_card": "Ecran final card",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "Egal unu dintre",
|
||||
"error_publishing_survey": "A apărut o eroare în timpul publicării sondajului.",
|
||||
"error_saving_changes": "Eroare la salvarea modificărilor",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Chiar și după ce au furnizat un răspuns (de ex. Cutia de Feedback)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permite răspunsuri multiple; continuă afișarea chiar și după un răspuns (de exemplu, Caseta de Feedback).",
|
||||
"everyone": "Toată lumea",
|
||||
"external_urls_paywall_tooltip": "Vă rugăm să faceți upgrade pentru a personaliza URL-ul extern. Prevenire phishing.",
|
||||
"external_urls_paywall_tooltip": "Vă rugăm să treceți la planul Startup pentru a personaliza URL-urile externe. Acest lucru ne ajută să prevenim phishing-ul.",
|
||||
"fallback_missing": "Rezerva lipsă",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} este folosit în logică întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "Câmpul ascuns \"{fieldId}\" este folosit în cota \"{quotaName}\"",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "Nume gazdă",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Dacă aveți nevoie de mai multe, vă rugăm să",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Dacă într-adevăr îți dorești acel răspuns, întreabă până îl primești.",
|
||||
"ignore_waiting_time_between_surveys": "Ignoră perioada de așteptare între sondaje",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuă afișarea ori de câte ori este declanșat până când se trimite un răspuns.",
|
||||
"ignore_global_waiting_time": "Ignoră timpul de așteptare la nivel de proiect",
|
||||
"ignore_global_waiting_time_description": "Acest sondaj poate fi afișat ori de câte ori condițiile sale sunt îndeplinite, chiar dacă un alt sondaj a fost afișat recent.",
|
||||
"image": "Imagine",
|
||||
"includes_all_of": "Include toate\",\"contextDescription\":\"Part of a survey completion screen referencing conditions met when all items are included\"}",
|
||||
"includes_one_of": "Include una dintre",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "Opțional",
|
||||
"options": "Opțiuni",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Suprascrie tema cu stiluri individuale pentru acest sondaj.",
|
||||
"overwrite_global_waiting_time": "Setează un timp de așteptare personalizat",
|
||||
"overwrite_global_waiting_time_description": "Suprascrie configurația proiectului doar pentru acest sondaj.",
|
||||
"overwrite_placement": "Suprascriere amplasare",
|
||||
"overwrite_the_global_placement_of_the_survey": "Suprascrie amplasarea globală a sondajului",
|
||||
"overwrites_waiting_period_between_surveys_to_x_days": "Suprascrie perioada de așteptare dintre sondaje la {days} zi(le).",
|
||||
"pick_a_background_from_our_library_or_upload_your_own": "Alege un fundal din biblioteca noastră sau încarcă unul propriu.",
|
||||
"picture_idx": "Poză {idx}",
|
||||
"pin_can_only_contain_numbers": "PIN-ul poate conține doar numere.",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "Interval",
|
||||
"recall_data": "Reamintiți datele",
|
||||
"recall_information_from": "Reamintiți informațiile din ...",
|
||||
"recontact_options": "Opțiuni de recontactare",
|
||||
"recontact_options_section": "Opțiuni de recontactare",
|
||||
"recontact_options_section_description": "Dacă timpul de așteptare permite, alege cât de des poate fi afișat acest sondaj unei persoane.",
|
||||
"redirect_thank_you_card": "Redirecționează cardul de mulțumire",
|
||||
"redirect_to_url": "Redirecționează către URL",
|
||||
"remove_description": "Eliminați descrierea",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "Obligatoriu",
|
||||
"reset_to_theme_styles": "Resetare la stilurile temei",
|
||||
"reset_to_theme_styles_main_text": "Sigur doriți să resetați stilul la stilurile de temă? Acest lucru va elimina toate stilizările personalizate.",
|
||||
"respect_global_waiting_time": "Folosește timpul de așteptare la nivel de proiect",
|
||||
"respect_global_waiting_time_description": "Acest sondaj respectă timpul de așteptare setat în configurația proiectului. Este afișat doar dacă niciun alt sondaj nu a apărut în acea perioadă.",
|
||||
"response_limit_can_t_be_set_to_0": "Limitul de răspunsuri nu poate fi setat la 0",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "Limita răspunsurilor trebuie să depășească numărul de răspunsuri primite ({responseCount}).",
|
||||
"response_limits_redirections_and_more": "Limite de răspunsuri, redirecționări și altele.",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "Comutați pe modul multilingv pentru a începe 👉",
|
||||
"targeted": "Ţintite",
|
||||
"ten_points": "10 puncte",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Sondajul va fi afișat de mai multe ori până când vor răspunde",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Sondajul va fi afișat o singură dată, chiar dacă persoana nu răspunde.",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Afișează de cel mult numărul specificat de ori sau până când răspund (oricare dintre acestea survine prima).",
|
||||
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Afișează o singură dată, chiar dacă persoana nu răspunde.",
|
||||
"then": "Apoi",
|
||||
"this_action_will_remove_all_the_translations_from_this_survey": "Această acțiune va elimina toate traducerile din acest sondaj.",
|
||||
"this_extension_is_already_added": "Această extensie este deja adăugată.",
|
||||
"this_file_type_is_not_supported": "Acest tip de fișier nu este acceptat.",
|
||||
"this_setting_overwrites_your": "Această setare suprascrie",
|
||||
"three_points": "3 puncte",
|
||||
"times": "ori",
|
||||
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Pentru a menține amplasarea consecventă pentru toate sondajele, puteți",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "Vizează grupuri specifice de utilizatori pe baza atributelor sau a informațiilor despre dispozitiv",
|
||||
"unlock_targeting_title": "Deblocați țintirea cu un plan superior",
|
||||
"unsaved_changes_warning": "Aveți modificări nesalvate în sondajul dumneavoastră. Doriți să le salvați înainte de a pleca?",
|
||||
"until_they_submit_a_response": "Până când vor furniza un răspuns",
|
||||
"until_they_submit_a_response": "Întreabă până când trimit un răspuns",
|
||||
"upgrade_notice_description": "Creați sondaje multilingve și deblocați multe alte caracteristici",
|
||||
"upgrade_notice_title": "Deblocați sondajele multilingve cu un plan superior",
|
||||
"upload": "Încărcați",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "Etichetă superioară",
|
||||
"url_filters": "Filtre URL",
|
||||
"url_not_supported": "URL nesuportat",
|
||||
"use_with_caution": "Folosește cu precauție",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} este folosit în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabila \"{variableName}\" este folosită în cota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "Numele variabilei este deja utilizat, vă rugăm să alegeți altul.",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "Variabila \"{variable}\" este reamintită în cardul de bun venit.",
|
||||
"verify_email_before_submission": "Verifică emailul înainte de trimitere",
|
||||
"verify_email_before_submission_description": "Permite doar persoanelor cu un email real să răspundă.",
|
||||
"visibility_and_recontact": "Vizibilitate și recontactare",
|
||||
"visibility_and_recontact_description": "Controlează când poate apărea acest sondaj și cât de des poate reapărea.",
|
||||
"wait": "Așteptați",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "Așteptați câteva secunde după declanșare înainte de a afișa sondajul",
|
||||
"waiting_period": "perioada de așteptare",
|
||||
"waiting_time_across_surveys": "Timp de așteptare la nivel de proiect",
|
||||
"waiting_time_across_surveys_description": "Pentru a preveni oboseala cauzată de sondaje, alege cum interacționează acest sondaj cu timpul de așteptare la nivel de proiect.",
|
||||
"welcome_message": "Mesaj de bun venit",
|
||||
"when_conditions_match_waiting_time_will_be_ignored_and_survey_shown": "Când condițiile se potrivesc, timpul de așteptare va fi ignorat și sondajul va fi afișat.",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Fără un filtru, toți utilizatorii pot fi chestionați.",
|
||||
"you_have_not_created_a_segment_yet": "Nu ai creat încă un segment",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations": "Trebuie să aveți două sau mai multe limbi configurate în proiectul dvs. pentru a lucra cu traducerile.",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "Nume de familie",
|
||||
"not_completed": "Necompletat ⏳",
|
||||
"os": "SO",
|
||||
"person_attributes": "Atribute persoană",
|
||||
"person_attributes": "Atributele persoanei la momentul trimiterii",
|
||||
"phone": "Telefon",
|
||||
"respondent_skipped_questions": "Respondenții au sărit peste aceste întrebări.",
|
||||
"response_deleted_successfully": "Răspuns șters cu succes.",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "Filtru adăugat pentru răspunsuri unde răspunsul la întrebarea {questionIdx} este {filterComboBoxValue} - {filterValue}",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "Filtru adăugat pentru răspunsuri unde răspunsul la întrebarea {questionIdx} este omis",
|
||||
"aggregated": "Agregat",
|
||||
"all_responses_csv": "Toate răspunsurile (CSV)",
|
||||
"all_responses_excel": "Toate răspunsurile (Excel)",
|
||||
"all_time": "Pe parcursul întregii perioade",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "Răspunsuri filtrate (CSV)",
|
||||
"filtered_responses_excel": "Răspunsuri filtrate (Excel)",
|
||||
"generating_qr_code": "Se generează codul QR",
|
||||
"go_to_setup_checklist": "Mergi la lista de verificare a configurării 👉",
|
||||
"impressions": "Impresii",
|
||||
"impressions_tooltip": "Număr de ori când sondajul a fost vizualizat.",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "Include tot",
|
||||
"includes_either": "Include fie",
|
||||
"install_widget": "Instalați Widgetul Formbricks",
|
||||
"individual": "Individual",
|
||||
"is_equal_to": "Este egal cu",
|
||||
"is_less_than": "Este mai puțin de",
|
||||
"last_30_days": "Ultimele 30 de zile",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||
"other_values_found": "Alte valori găsite",
|
||||
"overall": "General",
|
||||
"promoters": "Promotori",
|
||||
"qr_code": "Cod QR",
|
||||
"qr_code_description": "Răspunsurile colectate prin cod QR sunt anonime.",
|
||||
"qr_code_download_failed": "Descărcarea codului QR a eșuat",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "Numărul de cote completate de respondenți.",
|
||||
"reset_survey": "Resetează chestionarul",
|
||||
"reset_survey_warning": "Resetarea unui sondaj elimină toate răspunsurile și afișajele asociate cu acest sondaj. Aceasta nu poate fi anulată.",
|
||||
"satisfied": "Mulțumit",
|
||||
"selected_responses_csv": "Răspunsuri selectate (CSV)",
|
||||
"selected_responses_excel": "Răspunsuri selectate (Excel)",
|
||||
"setup_integrations": "Configurare integrare",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "Timp mediu pentru a completa întrebarea.",
|
||||
"unknown_question_type": "Tip de întrebare necunoscut",
|
||||
"use_personal_links": "Folosește linkuri personale",
|
||||
"waiting_for_response": "Așteptând un răspuns 🧘♂️",
|
||||
"whats_next": "Ce urmează?",
|
||||
"your_survey_is_public": "Sondajul tău este public",
|
||||
"youre_not_plugged_in_yet": "Nu sunteţi încă conectat!"
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "清除 过滤器",
|
||||
"clear_selection": "清除 选择",
|
||||
"click": "点击",
|
||||
"click_to_filter": "点击筛选",
|
||||
"clicks": "点击",
|
||||
"close": "关闭",
|
||||
"code": "代码",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "请求 达到 最大 上限 , 请 稍后 再试 。",
|
||||
"error_rate_limit_title": "速率 限制 超过",
|
||||
"expand_rows": "展开 行",
|
||||
"failed_to_copy_to_clipboard": "复制到剪贴板失败",
|
||||
"failed_to_load_organizations": "加载组织失败",
|
||||
"failed_to_load_projects": "加载项目失败",
|
||||
"finish": "完成",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "全名",
|
||||
"gathering_responses": "收集反馈",
|
||||
"general": "通用",
|
||||
"generate": "生成",
|
||||
"go_back": "返回 ",
|
||||
"go_to_dashboard": "转到 Dashboard",
|
||||
"hidden": "隐藏",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "用户 ID",
|
||||
"user_not_found": "用户 不存在",
|
||||
"variable": "变量",
|
||||
"variable_ids": "变量 ID",
|
||||
"variables": "变量",
|
||||
"verified_email": "已验证 电子邮件",
|
||||
"video": "视频",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "添加 CSS class 或 id",
|
||||
"add_regular_expression_here": "在 这里 添加 正则 表达式",
|
||||
"add_url": "添加 URL",
|
||||
"and": "与",
|
||||
"click": "点击",
|
||||
"contains": "包含",
|
||||
"create_action": "创建 操作",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "限制 特定 页面",
|
||||
"matches_regex": "匹配 正则表达式",
|
||||
"on_all_pages": "在 所有 页面",
|
||||
"or": "或",
|
||||
"page_filter": "页面 过滤器",
|
||||
"page_view": "页面 查看",
|
||||
"select_match_type": "选择 匹配 类型",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "联系人 已成功刷新",
|
||||
"delete_contact_confirmation": "这将删除与此联系人相关的所有调查问卷回复和联系人属性。基于此联系人数据的任何定位和个性化将会丢失。",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {这将删除与此联系人相关的所有调查回复和联系人属性。基于此联系人数据的任何定位和个性化将丢失。如果此联系人有影响调查配额的回复,配额数量将减少,但配额限制将保持不变。} other {这将删除与这些联系人相关的所有调查回复和联系人属性。基于这些联系人数据的任何定位和个性化将丢失。如果这些联系人有影响调查配额的回复,配额数量将减少,但配额限制将保持不变。}}",
|
||||
"generate_personal_link": "生成个人链接",
|
||||
"generate_personal_link_description": "选择一个已发布的调查,为此联系人生成个性化链接。",
|
||||
"no_published_link_surveys_available": "没有可用的已发布链接调查。请先发布一个链接调查。",
|
||||
"no_published_surveys": "没有已发布的调查",
|
||||
"no_responses_found": "未找到 响应",
|
||||
"not_provided": "未提供",
|
||||
"personal_link_generated": "个人链接生成成功",
|
||||
"personal_link_generated_but_clipboard_failed": "个性化链接已生成,但复制到剪贴板失败:{url}",
|
||||
"personal_survey_link": "个人调查链接",
|
||||
"please_select_a_survey": "请选择一个调查",
|
||||
"search_contact": "搜索 联系人",
|
||||
"select_a_survey": "选择一个调查",
|
||||
"select_attribute": "选择 属性",
|
||||
"unlock_contacts_description": "管理 联系人 并 发送 定向 调查",
|
||||
"unlock_contacts_title": "通过 更 高级 划解锁 联系人",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "更新",
|
||||
"upload_contacts_modal_pick_different_file": "选择不同的文件",
|
||||
"upload_contacts_modal_preview": "这是 你 的 数据 预览。",
|
||||
"upload_contacts_modal_upload_btn": "上传 联系人"
|
||||
"upload_contacts_modal_upload_btn": "上传 联系人",
|
||||
"upload_contacts_success": "联系人上传成功"
|
||||
},
|
||||
"formbricks_logo": "Formbricks Logo",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "应用程序 连接",
|
||||
"app_connection_description": "连接 您 的 应用 与 Formbricks。",
|
||||
"cache_update_delay_description": "当 你 对 调查 、 联系人 、 操作 或 其他 数据 进行 更新 时 , 可能 需要 最多 5 分钟 更改 才能 显示 在 你 本地 运行 Formbricks SDK 的 应用程序 中 。 这个 延迟 是 由于 我们 当前 缓存 系统 的 限制 。 我们 正在 积极 重新设计 缓存 并 将 在 Formbricks 4.0 中 发布 修复 。",
|
||||
"cache_update_delay_title": "更改 将 在 5 分钟 后 由于 缓存 而 显示",
|
||||
"app_connection_description": "将您的应用或网站连接到 Formbricks。",
|
||||
"cache_update_delay_description": "当您更新问卷、联系人、操作或其他数据时,这些更改可能需要最多 1 分钟才能在运行 Formbricks SDK 的本地应用中显示。",
|
||||
"cache_update_delay_title": "由于缓存,变更将在约 1 分钟后生效",
|
||||
"environment_id": "你的 环境 ID",
|
||||
"environment_id_description": "这个 id 独特地 标识 这个 Formbricks 环境。",
|
||||
"formbricks_sdk_connected": "Formbricks SDK 已连接",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK 尚未连接。",
|
||||
"formbricks_sdk_not_connected_description": "连接 您 的 网站 或 应用 与 Formbricks",
|
||||
"formbricks_sdk_not_connected_description": "将 Formbricks SDK 添加到您的网站或应用,以将其连接到 Formbricks",
|
||||
"how_to_setup": "如何设置",
|
||||
"how_to_setup_description": "遵循这些步骤在你的应用中设置 Formbricks 小部件。",
|
||||
"receiving_data": "接收 数据 💃🕺",
|
||||
"recheck": "重新检查",
|
||||
"sdk_connection_details": "SDK 连接详情",
|
||||
"sdk_connection_details_description": "您唯一的环境 ID 和 SDK 连接 URL,用于将 Formbricks 与您的应用程序集成。",
|
||||
"setup_alert_description": "按照 此 步骤教程 在 5 分钟 以内 连接 你的 应用 或 网站。",
|
||||
"setup_alert_title": "如何 连接"
|
||||
"setup_alert_title": "如何 连接",
|
||||
"webapp_url": "SDK连接URL"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "这是 您 唯一的 项目,不可 删除。请 先 创建一个新的 项目。",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "项目 删除 成功",
|
||||
"project_name_settings_description": "更改 您 的 项目 名称。",
|
||||
"project_name_updated_successfully": "项目 名称 更新 成功",
|
||||
"recontact_waiting_time": "再联系 等待 时间",
|
||||
"recontact_waiting_time": "项目范围内的调查等待时间",
|
||||
"recontact_waiting_time_settings_description": "控制用户可以通过所有应用程序 调查 的 频率。",
|
||||
"this_action_cannot_be_undone": "此 操作 无法 撤消。",
|
||||
"wait_x_days_before_showing_next_survey": "等待 X 天后再显示下一个 调查:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "添加 标签",
|
||||
"count": "数量",
|
||||
"delete_tag_confirmation": "您 确定 要 删除 此 标签 吗?",
|
||||
"empty_message": "标记一个提交以在此处找到您的标签列表。",
|
||||
"manage_tags": "管理标签",
|
||||
"manage_tags_description": "合并 和 删除 response 标签。",
|
||||
"merge": "合并",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"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": "在 你的 网络 应用 或 网站 中 嵌入 问卷 收集 反馈 。",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"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": "决定 人 可以 回答 这份 调查 的 频率 。",
|
||||
"days_before_showing_this_survey_again": "在显示此调查之前,需等待的天数。",
|
||||
"delete_choice": "删除 选择",
|
||||
"disable_the_visibility_of_survey_progress": "禁用问卷 进度 的可见性。",
|
||||
"display_an_estimate_of_completion_time_for_survey": "显示 调查 预计 完成 时间",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "编辑 链接",
|
||||
"edit_recall": "编辑 调用",
|
||||
"edit_translations": "编辑 {lang} 翻译",
|
||||
"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": "允许受访者在调查过程中随时切换语言。需要至少启用两种语言。",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "垃圾 邮件 保护 使用 reCAPTCHA v3 来 过滤 掉 垃圾 响应 。",
|
||||
"enable_spam_protection": "垃圾 邮件 保护",
|
||||
"end_screen_card": "结束 屏幕 卡片",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "等于 其中 一个",
|
||||
"error_publishing_survey": "发布调查时发生了错误",
|
||||
"error_saving_changes": "保存 更改 时 出错",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "即使 他们 提交 了 回复(例如 反馈框)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "允许多次回应;即使已提交回应,仍会继续显示(例如,反馈框)。",
|
||||
"everyone": "所有 人",
|
||||
"external_urls_paywall_tooltip": "请升级 以自定义 外部 URL 。 网络钓鱼 预防 。",
|
||||
"external_urls_paywall_tooltip": "请升级到 Startup 计划以自定义外部 URL。这有助于我们防止网络钓鱼。",
|
||||
"fallback_missing": "备用 缺失",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{fieldId} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "隐藏 字段 \"{fieldId}\" 正在 被 \"{quotaName}\" 配额 使用",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "主 机 名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣",
|
||||
"if_you_need_more_please": "如果你需要更多,请",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "如果 你 真想 要 那个 答案,就 不断 询问 直到 得到。",
|
||||
"ignore_waiting_time_between_surveys": "忽略 调查 之间 的 等待 时间",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "每次触发时都会显示,直到提交回应为止。",
|
||||
"ignore_global_waiting_time": "忽略项目范围内的等待时间",
|
||||
"ignore_global_waiting_time_description": "只要满足条件,此调查即可显示,即使最近刚显示过其他调查。",
|
||||
"image": "图片",
|
||||
"includes_all_of": "包括所有 ",
|
||||
"includes_one_of": "包括一 个",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "可选",
|
||||
"options": "选项",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用 个性化 样式 替代 这份 问卷 的 主题。",
|
||||
"overwrite_global_waiting_time": "设置自定义等待时间",
|
||||
"overwrite_global_waiting_time_description": "仅为此调查覆盖项目配置。",
|
||||
"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 只能包含数字。",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "范围",
|
||||
"recall_data": "调用 数据",
|
||||
"recall_information_from": "从 ... 召回信息",
|
||||
"recontact_options": "重新 联系 选项",
|
||||
"recontact_options_section": "重新联系选项",
|
||||
"recontact_options_section_description": "如果等待时间允许,请选择此调查可以向某人显示的频率。",
|
||||
"redirect_thank_you_card": "重定向感谢卡",
|
||||
"redirect_to_url": "重定向到 URL",
|
||||
"remove_description": "移除 描述",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "必需的",
|
||||
"reset_to_theme_styles": "重置 为 主题 风格",
|
||||
"reset_to_theme_styles_main_text": "您 确定 要 将 样式 重置 为 主题 样式吗?这 将 删除 所有 自定义 样式。",
|
||||
"respect_global_waiting_time": "使用项目范围内的等待时间",
|
||||
"respect_global_waiting_time_description": "此调查遵循项目配置中设置的等待时间。仅在该期间未显示其他调查时才会显示。",
|
||||
"response_limit_can_t_be_set_to_0": "不 能 将 响应 限制 设置 为 0",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "限制 响应 需要 超过 收到 的 响应 数量 ({responseCount})。",
|
||||
"response_limits_redirections_and_more": "响应 限制 、 重定向 和 更多 。",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "显示 高级设置",
|
||||
"show_button": "显示 按钮",
|
||||
"show_language_switch": "显示 语言 切换",
|
||||
"show_multiple_times": "显示 多次",
|
||||
"show_multiple_times": "显示有限次数",
|
||||
"show_only_once": "仅 显示 一次",
|
||||
"show_survey_maximum_of": "显示 调查 最大 一次",
|
||||
"show_survey_to_users": "显示 问卷 给 % 的 用户",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "打开多语言以开始 👉",
|
||||
"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": "调查 将 显示 一次,即使 你 不 回复。",
|
||||
"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": "为了 保持 所有 调查 的 放置 一致,您 可以",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "根据 属性 或 设备信息 定位 特定 用户组",
|
||||
"unlock_targeting_title": "通过 更 高级 划解锁 定位",
|
||||
"unsaved_changes_warning": "您在调查中有未保存的更改。离开前是否要保存?",
|
||||
"until_they_submit_a_response": "直到 他们 提交 回复",
|
||||
"until_they_submit_a_response": "持续显示直到提交回应",
|
||||
"upgrade_notice_description": "创建 多语言 调查 并 解锁 更多 功能",
|
||||
"upgrade_notice_title": "解锁 更高 计划 中 的 多语言 调查",
|
||||
"upload": "上传",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "上限标签",
|
||||
"url_filters": "URL 过滤器",
|
||||
"url_not_supported": "URL 不支持",
|
||||
"use_with_caution": "谨慎 使用",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{variable} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "变量 \"{variableName}\" 正在 被 \"{quotaName}\" 配额 使用",
|
||||
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "变量 \"{variable}\" 正在召回于欢迎 卡 。",
|
||||
"verify_email_before_submission": "提交 之前 验证电子邮件",
|
||||
"verify_email_before_submission_description": "仅允许 拥有 有效 电子邮件 的 人 回应。",
|
||||
"visibility_and_recontact": "可见性与重新联系",
|
||||
"visibility_and_recontact_description": "控制此调查何时可以显示以及可以重新显示的频率。",
|
||||
"wait": "等待",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "触发后等待几秒再显示问卷",
|
||||
"waiting_period": "等待期",
|
||||
"waiting_time_across_surveys": "项目范围内的等待时间",
|
||||
"waiting_time_across_surveys_description": "为防止调查疲劳,请选择此调查如何与项目范围内的等待时间交互。",
|
||||
"welcome_message": "欢迎 信息",
|
||||
"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": "您 需要在您的项目中设置两种或更多语言才能进行翻译。",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "姓",
|
||||
"not_completed": "未完成 ⏳",
|
||||
"os": "操作系统",
|
||||
"person_attributes": "人员 属性",
|
||||
"person_attributes": "提交时的个人属性",
|
||||
"phone": "电话",
|
||||
"respondent_skipped_questions": "受访者跳过 这些问题。",
|
||||
"response_deleted_successfully": "响应 删除 成功",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "为 回答 问题 {questionIdx} 的 答复 增加 了 筛选器,筛选条件 是 {filterComboBoxValue} - {filterValue}",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "为 回答 问题 {questionIdx} 的 答复 增加 了 筛选器,筛选条件 是 略过",
|
||||
"aggregated": "汇总",
|
||||
"all_responses_csv": "所有 反馈 (CSV)",
|
||||
"all_responses_excel": "所有 反馈 (Excel)",
|
||||
"all_time": "所有 时间",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "过滤 反馈 (CSV)",
|
||||
"filtered_responses_excel": "过滤 反馈 (Excel)",
|
||||
"generating_qr_code": "正在生成二维码",
|
||||
"go_to_setup_checklist": "前往 设置 检查列表 👉",
|
||||
"impressions": "印象",
|
||||
"impressions_tooltip": "调查 被 查看 的 次数",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "包括所有 ",
|
||||
"includes_either": "包含 任意一个",
|
||||
"install_widget": "安装 Formbricks 小组件",
|
||||
"individual": "个人",
|
||||
"is_equal_to": "等于",
|
||||
"is_less_than": "少于",
|
||||
"last_30_days": "最近 30 天",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "未找到响应",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整体",
|
||||
"promoters": "推荐者",
|
||||
"qr_code": "二维码",
|
||||
"qr_code_description": "通过 QR 码 收集 的 响应 是 匿名 的。",
|
||||
"qr_code_download_failed": "二维码下载失败",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "受访者完成的配额数量。",
|
||||
"reset_survey": "重置 调查",
|
||||
"reset_survey_warning": "重置 一个调查 会移除与 此调查 相关 的 所有响应 和 展示 。此操作 不能 撤销 。",
|
||||
"satisfied": "满意",
|
||||
"selected_responses_csv": "选定 反馈 (CSV)",
|
||||
"selected_responses_excel": "选定 反馈 (Excel)",
|
||||
"setup_integrations": "设置 集成",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "完成 本 问题 的 平均 时间",
|
||||
"unknown_question_type": "未知 问题 类型",
|
||||
"use_personal_links": "使用 个人 链接",
|
||||
"waiting_for_response": "等待回复 🧘♂️",
|
||||
"whats_next": "接下来 是 什么?",
|
||||
"your_survey_is_public": "您的 调查 是 公共 的",
|
||||
"youre_not_plugged_in_yet": "您 还 没 有 连 接!"
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"clear_filters": "清除篩選器",
|
||||
"clear_selection": "清除選取",
|
||||
"click": "點擊",
|
||||
"click_to_filter": "點擊篩選",
|
||||
"clicks": "點擊數",
|
||||
"close": "關閉",
|
||||
"code": "程式碼",
|
||||
@@ -210,6 +211,7 @@
|
||||
"error_rate_limit_description": "已達 到最大 請求 次數。請 稍後 再試。",
|
||||
"error_rate_limit_title": "限流超過",
|
||||
"expand_rows": "展開列",
|
||||
"failed_to_copy_to_clipboard": "無法複製到剪貼簿",
|
||||
"failed_to_load_organizations": "無法載入組織",
|
||||
"failed_to_load_projects": "無法載入專案",
|
||||
"finish": "完成",
|
||||
@@ -218,6 +220,7 @@
|
||||
"full_name": "全名",
|
||||
"gathering_responses": "收集回應中",
|
||||
"general": "一般",
|
||||
"generate": "產生",
|
||||
"go_back": "返回",
|
||||
"go_to_dashboard": "前往儀表板",
|
||||
"hidden": "隱藏",
|
||||
@@ -425,6 +428,7 @@
|
||||
"user_id": "使用者 ID",
|
||||
"user_not_found": "找不到使用者",
|
||||
"variable": "變數",
|
||||
"variable_ids": "變數 ID",
|
||||
"variables": "變數",
|
||||
"verified_email": "已驗證的電子郵件",
|
||||
"video": "影片",
|
||||
@@ -523,6 +527,7 @@
|
||||
"add_css_class_or_id": "新增 CSS 類別或 ID",
|
||||
"add_regular_expression_here": "新增正則表達式在此",
|
||||
"add_url": "新增網址",
|
||||
"and": "且",
|
||||
"click": "點擊",
|
||||
"contains": "包含",
|
||||
"create_action": "建立操作",
|
||||
@@ -553,6 +558,7 @@
|
||||
"limit_to_specific_pages": "限制為特定頁面",
|
||||
"matches_regex": "符合 正則 表達式",
|
||||
"on_all_pages": "在所有頁面上",
|
||||
"or": "或",
|
||||
"page_filter": "頁面篩選器",
|
||||
"page_view": "頁面檢視",
|
||||
"select_match_type": "選取比對類型",
|
||||
@@ -593,9 +599,18 @@
|
||||
"contacts_table_refresh_success": "聯絡人已成功重新整理",
|
||||
"delete_contact_confirmation": "這將刪除與此聯繫人相關的所有調查回應和聯繫屬性。任何基於此聯繫人數據的定位和個性化將會丟失。",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {這將刪除與這個 contact 相關的所有調查響應和聯繫人屬性。基於這個 contact 數據的任何定向和個性化功能將會丟失。如果這個 contact 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。} other {這將刪除與這些 contacts 相關的所有調查響應和聯繫人屬性。基於這些 contacts 數據的任何定向和個性化功能將會丟失。如果這些 contacts 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。}}",
|
||||
"generate_personal_link": "產生個人連結",
|
||||
"generate_personal_link_description": "選擇一個已發佈的問卷,為此聯絡人產生個人化連結。",
|
||||
"no_published_link_surveys_available": "沒有可用的已發佈連結問卷。請先發佈一個連結問卷。",
|
||||
"no_published_surveys": "沒有已發佈的問卷",
|
||||
"no_responses_found": "找不到回應",
|
||||
"not_provided": "未提供",
|
||||
"personal_link_generated": "個人連結已成功產生",
|
||||
"personal_link_generated_but_clipboard_failed": "已生成個人連結,但無法複製到剪貼簿:{url}",
|
||||
"personal_survey_link": "個人調查連結",
|
||||
"please_select_a_survey": "請選擇一個問卷",
|
||||
"search_contact": "搜尋聯絡人",
|
||||
"select_a_survey": "選擇問卷",
|
||||
"select_attribute": "選取屬性",
|
||||
"unlock_contacts_description": "管理聯絡人並發送目標問卷",
|
||||
"unlock_contacts_title": "使用更高等級的方案解鎖聯絡人",
|
||||
@@ -616,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "更新",
|
||||
"upload_contacts_modal_pick_different_file": "選取不同的檔案",
|
||||
"upload_contacts_modal_preview": "這是您的資料預覽。",
|
||||
"upload_contacts_modal_upload_btn": "上傳聯絡人"
|
||||
"upload_contacts_modal_upload_btn": "上傳聯絡人",
|
||||
"upload_contacts_success": "聯絡人已成功上傳"
|
||||
},
|
||||
"formbricks_logo": "Formbricks 標誌",
|
||||
"integrations": {
|
||||
@@ -774,20 +790,23 @@
|
||||
},
|
||||
"app-connection": {
|
||||
"app_connection": "應用程式連線",
|
||||
"app_connection_description": "將您的應用程式連線至 Formbricks。",
|
||||
"cache_update_delay_description": "當您對調查、聯絡人、操作或其他資料進行更新時,可能需要長達 5 分鐘這些變更才能顯示在執行 Formbricks SDK 的本地應用程式中。此延遲是因我們目前快取系統的限制。我們正積極重新設計快取,並將在 Formbricks 4.0 中發佈修補程式。",
|
||||
"cache_update_delay_title": "更改將於 5 分鐘後因快取而反映",
|
||||
"app_connection_description": "將您的應用程式或網站連接到 Formbricks。",
|
||||
"cache_update_delay_description": "當您更新問卷調查、聯絡人、操作或其他資料時,這些變更可能需要最多 1 分鐘的時間,才會顯示在執行 Formbricks SDK 的本地應用程式中。",
|
||||
"cache_update_delay_title": "由於快取,變更約需 1 分鐘後才會反映",
|
||||
"environment_id": "您的 EnvironmentId",
|
||||
"environment_id_description": "此 ID 可唯一識別此 Formbricks 環境。",
|
||||
"formbricks_sdk_connected": "Formbricks SDK 已連線",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK 尚未連線。",
|
||||
"formbricks_sdk_not_connected_description": "將您的網站或應用程式與 Formbricks 連線",
|
||||
"formbricks_sdk_not_connected_description": "將 Formbricks SDK 添加到您的網站或應用程式,以連接到 Formbricks",
|
||||
"how_to_setup": "如何設定",
|
||||
"how_to_setup_description": "請按照這些步驟在您的應用程式中設定 Formbricks 小工具。",
|
||||
"receiving_data": "正在接收資料 💃🕺",
|
||||
"recheck": "重新檢查",
|
||||
"sdk_connection_details": "SDK 連線詳細資訊",
|
||||
"sdk_connection_details_description": "您的唯一環境 ID 和 SDK 連線 URL,用於將 Formbricks 與您的應用程式整合。",
|
||||
"setup_alert_description": "遵循 此 分步 教程 ,在 5 分鐘 內 將您的應用程式 或 網站 連線 。",
|
||||
"setup_alert_title": "如何 連線"
|
||||
"setup_alert_title": "如何 連線",
|
||||
"webapp_url": "SDK 連接 URL"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "這是您唯一的專案,無法刪除。請先建立新專案。",
|
||||
@@ -800,7 +819,7 @@
|
||||
"project_deleted_successfully": "專案已成功刪除",
|
||||
"project_name_settings_description": "變更您的專案名稱。",
|
||||
"project_name_updated_successfully": "專案名稱已成功更新",
|
||||
"recontact_waiting_time": "重新聯絡等待時間",
|
||||
"recontact_waiting_time": "專案範圍內的問卷等待時間",
|
||||
"recontact_waiting_time_settings_description": "控制使用者在所有應用程式問卷中可以被調查的頻率。",
|
||||
"this_action_cannot_be_undone": "此操作無法復原。",
|
||||
"wait_x_days_before_showing_next_survey": "在顯示下一個問卷之前等待 X 天:",
|
||||
@@ -869,7 +888,6 @@
|
||||
"add_tag": "新增標籤",
|
||||
"count": "計數",
|
||||
"delete_tag_confirmation": "您確定要刪除此標籤嗎?",
|
||||
"empty_message": "標記提交內容,在此處找到您的標籤清單。",
|
||||
"manage_tags": "管理標籤",
|
||||
"manage_tags_description": "合併和移除回應標籤。",
|
||||
"merge": "合併",
|
||||
@@ -1226,7 +1244,6 @@
|
||||
"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 應用程式或網站中以收集回應。",
|
||||
@@ -1309,8 +1326,7 @@
|
||||
"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": "決定人們可以回答此問卷的頻率。",
|
||||
"days_before_showing_this_survey_again": "在顯示此問卷之前,需等待其他問卷顯示後的天數。",
|
||||
"delete_choice": "刪除選項",
|
||||
"disable_the_visibility_of_survey_progress": "停用問卷進度的可見性。",
|
||||
"display_an_estimate_of_completion_time_for_survey": "顯示問卷的估計完成時間",
|
||||
@@ -1325,7 +1341,7 @@
|
||||
"edit_link": "編輯 連結",
|
||||
"edit_recall": "編輯回憶",
|
||||
"edit_translations": "編輯 '{'language'}' 翻譯",
|
||||
"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": "允許受訪者隨時切換語言。需要至少啟用兩種語言。",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "垃圾郵件保護使用 reCAPTCHA v3 過濾垃圾回應。",
|
||||
"enable_spam_protection": "垃圾郵件保護",
|
||||
"end_screen_card": "結束畫面卡片",
|
||||
@@ -1338,9 +1354,9 @@
|
||||
"equals_one_of": "等於其中之一",
|
||||
"error_publishing_survey": "發布問卷時發生錯誤。",
|
||||
"error_saving_changes": "儲存變更時發生錯誤",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "即使他們提交回應之後(例如,意見反應方塊)",
|
||||
"even_after_they_submitted_a_response_e_g_feedback_box": "允許多次回應;即使已提交回應仍繼續顯示(例如:意見回饋框)。",
|
||||
"everyone": "所有人",
|
||||
"external_urls_paywall_tooltip": "請升級以自訂 external URL 。 Phishing 預防。",
|
||||
"external_urls_paywall_tooltip": "請升級至 Startup 計劃以自訂外部 URL。這有助於防止網路釣魚攻擊。",
|
||||
"fallback_missing": "遺失的回退",
|
||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'fieldId'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "隱藏欄位 \"{fieldId}\" 正被使用於 \"{quotaName}\" 配額中",
|
||||
@@ -1409,8 +1425,9 @@
|
||||
"hostname": "主機名稱",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫",
|
||||
"if_you_need_more_please": "如果您需要更多,請",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "如果您真的想要該答案,請詢問直到您獲得它。",
|
||||
"ignore_waiting_time_between_surveys": "忽略問卷之間的等待時間",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "每次觸發時都顯示,直到提交回應為止。",
|
||||
"ignore_global_waiting_time": "忽略專案範圍內的等待時間",
|
||||
"ignore_global_waiting_time_description": "此問卷在符合條件時即可顯示,即使最近已顯示過其他問卷。",
|
||||
"image": "圖片",
|
||||
"includes_all_of": "包含全部",
|
||||
"includes_one_of": "包含其中之一",
|
||||
@@ -1477,9 +1494,10 @@
|
||||
"optional": "選填",
|
||||
"options": "選項",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用此問卷的個別樣式覆寫主題。",
|
||||
"overwrite_global_waiting_time": "設定自訂等待時間",
|
||||
"overwrite_global_waiting_time_description": "僅覆蓋此問卷的專案設定。",
|
||||
"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 碼只能包含數字。",
|
||||
@@ -1538,7 +1556,8 @@
|
||||
"range": "範圍",
|
||||
"recall_data": "回憶數據",
|
||||
"recall_information_from": "從 ... 獲取 信息",
|
||||
"recontact_options": "重新聯絡選項",
|
||||
"recontact_options_section": "重新聯絡選項",
|
||||
"recontact_options_section_description": "如果等待時間允許,選擇此問卷可以向同一人顯示的頻率。",
|
||||
"redirect_thank_you_card": "重新導向感謝卡片",
|
||||
"redirect_to_url": "重新導向至網址",
|
||||
"remove_description": "移除描述",
|
||||
@@ -1547,6 +1566,8 @@
|
||||
"required": "必填",
|
||||
"reset_to_theme_styles": "重設為主題樣式",
|
||||
"reset_to_theme_styles_main_text": "您確定要將樣式重設為主題樣式嗎?這將移除所有自訂樣式。",
|
||||
"respect_global_waiting_time": "使用專案範圍內的等待時間",
|
||||
"respect_global_waiting_time_description": "此問卷遵循專案設定的等待時間。僅在該期間內未顯示其他問卷時才會顯示。",
|
||||
"response_limit_can_t_be_set_to_0": "回應限制不能設定為 0",
|
||||
"response_limit_needs_to_exceed_number_of_received_responses": "回應限制必須超過收到的回應數 ('{'responseCount'}')。",
|
||||
"response_limits_redirections_and_more": "回應限制、重新導向等。",
|
||||
@@ -1571,7 +1592,7 @@
|
||||
"show_advanced_settings": "顯示進階設定",
|
||||
"show_button": "顯示按鈕",
|
||||
"show_language_switch": "顯示語言切換",
|
||||
"show_multiple_times": "多次顯示",
|
||||
"show_multiple_times": "顯示有限次數",
|
||||
"show_only_once": "僅顯示一次",
|
||||
"show_survey_maximum_of": "最多顯示問卷",
|
||||
"show_survey_to_users": "將問卷顯示給 % 的使用者",
|
||||
@@ -1601,13 +1622,12 @@
|
||||
"switch_multi_lanugage_on_to_get_started": "開啟多語言以開始使用 👉",
|
||||
"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": "即使使用者沒有回應,也只會顯示一次問卷。",
|
||||
"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": "若要保持所有問卷的位置一致,您可以",
|
||||
@@ -1618,7 +1638,7 @@
|
||||
"unlock_targeting_description": "根據屬性或裝置資訊鎖定特定使用者群組",
|
||||
"unlock_targeting_title": "使用更高等級的方案解鎖目標設定",
|
||||
"unsaved_changes_warning": "您的問卷中有未儲存的變更。您要先儲存它們再離開嗎?",
|
||||
"until_they_submit_a_response": "直到他們提交回應",
|
||||
"until_they_submit_a_response": "持續詢問直到提交回應",
|
||||
"upgrade_notice_description": "建立多語言問卷並解鎖更多功能",
|
||||
"upgrade_notice_title": "使用更高等級的方案解鎖多語言問卷",
|
||||
"upload": "上傳",
|
||||
@@ -1626,7 +1646,6 @@
|
||||
"upper_label": "上標籤",
|
||||
"url_filters": "網址篩選器",
|
||||
"url_not_supported": "不支援網址",
|
||||
"use_with_caution": "謹慎使用",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "變數 \"{variableName}\" 正被使用於 \"{quotaName}\" 配額中",
|
||||
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
|
||||
@@ -1636,11 +1655,13 @@
|
||||
"variable_used_in_recall_welcome": "變數 \"{variable}\" 於 歡迎 Card 中被召回。",
|
||||
"verify_email_before_submission": "提交前驗證電子郵件",
|
||||
"verify_email_before_submission_description": "僅允許擁有真實電子郵件的人員回應。",
|
||||
"visibility_and_recontact": "可見性與重新聯絡",
|
||||
"visibility_and_recontact_description": "控制此問卷何時可以顯示以及可以重新顯示的頻率。",
|
||||
"wait": "等待",
|
||||
"wait_a_few_seconds_after_the_trigger_before_showing_the_survey": "在觸發後等待幾秒鐘再顯示問卷",
|
||||
"waiting_period": "等待時間",
|
||||
"waiting_time_across_surveys": "專案範圍內的等待時間",
|
||||
"waiting_time_across_surveys_description": "為避免問卷疲勞,選擇此問卷如何與專案範圍內的等待時間互動。",
|
||||
"welcome_message": "歡迎訊息",
|
||||
"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": "您需要在您的專案中設定兩個或更多語言,才能使用翻譯。",
|
||||
@@ -1685,7 +1706,7 @@
|
||||
"last_name": "姓氏",
|
||||
"not_completed": "未完成 ⏳",
|
||||
"os": "作業系統",
|
||||
"person_attributes": "人員屬性",
|
||||
"person_attributes": "提交時的個人屬性",
|
||||
"phone": "電話",
|
||||
"respondent_skipped_questions": "回應者跳過這些問題。",
|
||||
"response_deleted_successfully": "回應已成功刪除。",
|
||||
@@ -1798,6 +1819,7 @@
|
||||
"summary": {
|
||||
"added_filter_for_responses_where_answer_to_question": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案為 '{'filterComboBoxValue'}' - '{'filterValue'}'",
|
||||
"added_filter_for_responses_where_answer_to_question_is_skipped": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案被跳過",
|
||||
"aggregated": "匯總",
|
||||
"all_responses_csv": "所有回應 (CSV)",
|
||||
"all_responses_excel": "所有回應 (Excel)",
|
||||
"all_time": "全部時間",
|
||||
@@ -1821,7 +1843,6 @@
|
||||
"filtered_responses_csv": "篩選回應 (CSV)",
|
||||
"filtered_responses_excel": "篩選回應 (Excel)",
|
||||
"generating_qr_code": "正在生成 QR code",
|
||||
"go_to_setup_checklist": "前往設定檢查清單 👉",
|
||||
"impressions": "曝光數",
|
||||
"impressions_tooltip": "問卷已檢視的次數。",
|
||||
"in_app": {
|
||||
@@ -1855,7 +1876,7 @@
|
||||
},
|
||||
"includes_all": "包含全部",
|
||||
"includes_either": "包含其中一個",
|
||||
"install_widget": "安裝 Formbricks 小工具",
|
||||
"individual": "個人",
|
||||
"is_equal_to": "等於",
|
||||
"is_less_than": "小於",
|
||||
"last_30_days": "過去 30 天",
|
||||
@@ -1868,6 +1889,7 @@
|
||||
"no_responses_found": "找不到回應",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整體",
|
||||
"promoters": "推廣者",
|
||||
"qr_code": "QR 碼",
|
||||
"qr_code_description": "透過 QR code 收集的回應都是匿名的。",
|
||||
"qr_code_download_failed": "QR code 下載失敗",
|
||||
@@ -1877,6 +1899,7 @@
|
||||
"quotas_completed_tooltip": "受訪者完成的 配額 數量。",
|
||||
"reset_survey": "重設問卷",
|
||||
"reset_survey_warning": "重置 調查 會 移除 與 此 調查 相關 的 所有 回應 和 顯示 。 這 是 不可 撤銷 的 。",
|
||||
"satisfied": "滿意",
|
||||
"selected_responses_csv": "選擇的回應 (CSV)",
|
||||
"selected_responses_excel": "選擇的回應 (Excel)",
|
||||
"setup_integrations": "設定整合",
|
||||
@@ -1892,7 +1915,6 @@
|
||||
"ttc_tooltip": "完成 問題 的 平均 時間。",
|
||||
"unknown_question_type": "未知的問題類型",
|
||||
"use_personal_links": "使用 個人 連結",
|
||||
"waiting_for_response": "正在等待回應 🧘♂️",
|
||||
"whats_next": "下一步是什麼?",
|
||||
"your_survey_is_public": "您的問卷是公開的",
|
||||
"youre_not_plugged_in_yet": "您尚未插入任何內容!"
|
||||
|
||||
@@ -85,6 +85,6 @@ export const middleware = async (originalRequest: NextRequest) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public).*)",
|
||||
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public|animated-bgs).*)",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { TagError } from "@/modules/projects/settings/types/tag";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Tag } from "@/modules/ui/components/tag";
|
||||
import { TagsCombobox } from "@/modules/ui/components/tags-combobox";
|
||||
import { createTagAction, createTagToResponseAction, deleteTagOnResponseAction } from "../actions";
|
||||
import { SingleResponseCardMetadata } from "./SingleResponseCardMetadata";
|
||||
|
||||
interface ResponseTagsWrapperProps {
|
||||
tags: {
|
||||
@@ -24,6 +24,8 @@ interface ResponseTagsWrapperProps {
|
||||
environmentTags: TTag[];
|
||||
updateFetchedResponses: () => void;
|
||||
isReadOnly?: boolean;
|
||||
response: TResponse;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
@@ -33,9 +35,10 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
environmentTags,
|
||||
updateFetchedResponses,
|
||||
isReadOnly,
|
||||
response,
|
||||
locale,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [tagsState, setTagsState] = useState(tags);
|
||||
@@ -79,7 +82,6 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
if (errorMessage?.code === TagError.TAG_NAME_ALREADY_EXISTS) {
|
||||
toast.error(t("environments.surveys.responses.tag_already_exists"), {
|
||||
duration: 2000,
|
||||
icon: <SettingsIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
} else {
|
||||
toast.error(t("environments.surveys.responses.an_error_occurred_creating_the_tag"));
|
||||
@@ -131,6 +133,7 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 border-t border-slate-200 px-6 py-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<SingleResponseCardMetadata response={response} locale={locale} />
|
||||
{tagsState?.map((tag) => (
|
||||
<Tag
|
||||
key={tag.tagId}
|
||||
@@ -157,18 +160,6 @@ export const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isReadOnly && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => {
|
||||
router.push(`/environments/${environmentId}/project/tags`);
|
||||
}}>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user