mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
8 Commits
4.1.0
...
hotfix/jwt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5b054af23 | ||
|
|
391b3a3fb0 | ||
|
|
0bcd85d658 | ||
|
|
f59df49588 | ||
|
|
f08fabfb13 | ||
|
|
ee8af9dd74 | ||
|
|
1091b40bd1 | ||
|
|
87a2d727ed |
99
.github/workflows/build-and-push-ecr.yml
vendored
Normal file
99
.github/workflows/build-and-push-ecr.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
name: Build & Push Docker to ECR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: "Image tag to push (e.g., v3.16.1)"
|
||||
required: true
|
||||
default: "v3.16.1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
ECR_REGION: ${{ vars.ECR_REGION }}
|
||||
# ECR settings are sourced from repository/environment variables for portability across envs/forks
|
||||
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
|
||||
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
|
||||
DOCKERFILE: apps/web/Dockerfile
|
||||
CONTEXT: .
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate image tag input
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||
echo "❌ Image tag is required (non-empty)."
|
||||
exit 1
|
||||
fi
|
||||
if (( ${#IMAGE_TAG} > 128 )); then
|
||||
echo "❌ Image tag must be at most 128 characters."
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "${IMAGE_TAG}" =~ ^[a-z0-9._-]+$ ]]; then
|
||||
echo "❌ Image tag may only contain lowercase letters, digits, '.', '_' and '-'."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${IMAGE_TAG}" =~ ^[.-] || "${IMAGE_TAG}" =~ [.-]$ ]]; then
|
||||
echo "❌ Image tag must not start or end with '.' or '-'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate required variables
|
||||
shell: bash
|
||||
env:
|
||||
ECR_REGISTRY: ${{ env.ECR_REGISTRY }}
|
||||
ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
|
||||
ECR_REGION: ${{ env.ECR_REGION }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${ECR_REGISTRY}" || -z "${ECR_REPOSITORY}" || -z "${ECR_REGION}" ]]; then
|
||||
echo "ECR_REGION, ECR_REGISTRY and ECR_REPOSITORY must be set via repository or environment variables (Settings → Variables)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ECR_PUSH_ROLE_ARN }}
|
||||
aws-region: ${{ env.ECR_REGION }}
|
||||
|
||||
- name: Log in to Amazon ECR
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||
|
||||
- name: Build and push image (Depot remote builder)
|
||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||
with:
|
||||
project: tw0fqmsx3c
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: ${{ env.CONTEXT }}
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ inputs.image_tag }}
|
||||
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest
|
||||
secrets: |
|
||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
@@ -37,7 +37,7 @@ on:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
helmfile-deploy:
|
||||
|
||||
17
.github/workflows/formbricks-release.yml
vendored
17
.github/workflows/formbricks-release.yml
vendored
@@ -7,12 +7,13 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ENVIRONMENT: ${{ github.event.release.prerelease && 'staging' || 'production' }}
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
name: Build & release docker image
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
uses: ./.github/workflows/release-docker-github.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
@@ -20,6 +21,9 @@ jobs:
|
||||
|
||||
helm-chart-release:
|
||||
name: Release Helm Chart
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: ./.github/workflows/release-helm-chart.yml
|
||||
secrets: inherit
|
||||
needs:
|
||||
@@ -29,6 +33,9 @@ jobs:
|
||||
|
||||
deploy-formbricks-cloud:
|
||||
name: Deploy Helm Chart to Formbricks Cloud
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/deploy-formbricks-cloud.yml
|
||||
needs:
|
||||
@@ -36,7 +43,7 @@ jobs:
|
||||
- helm-chart-release
|
||||
with:
|
||||
VERSION: v${{ needs.docker-build.outputs.VERSION }}
|
||||
ENVIRONMENT: ${{ env.ENVIRONMENT }}
|
||||
ENVIRONMENT: ${{ github.event.release.prerelease && 'staging' || 'production' }}
|
||||
|
||||
upload-sentry-sourcemaps:
|
||||
name: Upload Sentry Sourcemaps
|
||||
@@ -64,4 +71,4 @@ jobs:
|
||||
docker_image: ghcr.io/formbricks/formbricks:v${{ needs.docker-build.outputs.VERSION }}
|
||||
release_version: v${{ needs.docker-build.outputs.VERSION }}
|
||||
sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
environment: ${{ env.ENVIRONMENT }}
|
||||
environment: ${{ github.event.release.prerelease && 'staging' || 'production' }}
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
SquareStack,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
@@ -77,6 +77,7 @@ export const ShareSurveyModal = ({
|
||||
description: string;
|
||||
componentType: React.ComponentType<unknown>;
|
||||
componentProps: unknown;
|
||||
disabled?: boolean;
|
||||
}[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -111,6 +112,7 @@ export const ShareSurveyModal = ({
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
},
|
||||
disabled: survey.singleUse?.enabled,
|
||||
},
|
||||
{
|
||||
id: ShareViaType.WEBSITE_EMBED,
|
||||
@@ -121,6 +123,7 @@ export const ShareSurveyModal = ({
|
||||
description: t("environments.surveys.share.embed_on_website.description"),
|
||||
componentType: WebsiteEmbedTab,
|
||||
componentProps: { surveyUrl },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
},
|
||||
{
|
||||
id: ShareViaType.EMAIL,
|
||||
@@ -131,6 +134,7 @@ export const ShareSurveyModal = ({
|
||||
description: t("environments.surveys.share.send_email.description"),
|
||||
componentType: EmailTab,
|
||||
componentProps: { surveyId: survey.id, email },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
},
|
||||
{
|
||||
id: ShareViaType.SOCIAL_MEDIA,
|
||||
@@ -141,6 +145,7 @@ export const ShareSurveyModal = ({
|
||||
description: t("environments.surveys.share.social_media.description"),
|
||||
componentType: SocialMediaTab,
|
||||
componentProps: { surveyUrl, surveyTitle: survey.name },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
},
|
||||
{
|
||||
id: ShareViaType.QR_CODE,
|
||||
@@ -151,6 +156,7 @@ export const ShareSurveyModal = ({
|
||||
description: t("environments.surveys.summary.qr_code_description"),
|
||||
componentType: QRCodeTab,
|
||||
componentProps: { surveyUrl },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
},
|
||||
{
|
||||
id: ShareViaType.DYNAMIC_POPUP,
|
||||
@@ -177,9 +183,9 @@ export const ShareSurveyModal = ({
|
||||
t,
|
||||
survey,
|
||||
publicDomain,
|
||||
setSurveyUrl,
|
||||
user.locale,
|
||||
surveyUrl,
|
||||
isReadOnly,
|
||||
environmentId,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
@@ -188,9 +194,14 @@ export const ShareSurveyModal = ({
|
||||
]
|
||||
);
|
||||
|
||||
const [activeId, setActiveId] = useState<ShareViaType | ShareSettingsType>(
|
||||
survey.type === "link" ? ShareViaType.ANON_LINKS : ShareViaType.APP
|
||||
);
|
||||
const getDefaultActiveId = useCallback(() => {
|
||||
if (survey.type !== "link") {
|
||||
return ShareViaType.APP;
|
||||
}
|
||||
return ShareViaType.ANON_LINKS;
|
||||
}, [survey.type]);
|
||||
|
||||
const [activeId, setActiveId] = useState<ShareViaType | ShareSettingsType>(getDefaultActiveId());
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -198,11 +209,19 @@ export const ShareSurveyModal = ({
|
||||
}
|
||||
}, [open, modalView]);
|
||||
|
||||
// Ensure active tab is not disabled - if it is, switch to default
|
||||
useEffect(() => {
|
||||
const activeTab = linkTabs.find((tab) => tab.id === activeId);
|
||||
if (activeTab?.disabled) {
|
||||
setActiveId(getDefaultActiveId());
|
||||
}
|
||||
}, [activeId, linkTabs, getDefaultActiveId]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
setShowView("start");
|
||||
setActiveId(ShareViaType.ANON_LINKS);
|
||||
setActiveId(getDefaultActiveId());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -150,13 +150,13 @@ export const LinkSettingsTab = ({ isReadOnly, locale }: LinkSettingsTabProps) =>
|
||||
name: "title",
|
||||
label: t("environments.surveys.share.link_settings.link_title"),
|
||||
description: t("environments.surveys.share.link_settings.link_title_description"),
|
||||
placeholder: t("environments.surveys.share.link_settings.link_title_placeholder"),
|
||||
placeholder: survey.name,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
label: t("environments.surveys.share.link_settings.link_description"),
|
||||
description: t("environments.surveys.share.link_settings.link_description_description"),
|
||||
placeholder: t("environments.surveys.share.link_settings.link_description_placeholder"),
|
||||
placeholder: "Please complete this survey.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ interface ShareViewProps {
|
||||
componentProps: any;
|
||||
title: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
activeId: ShareViaType | ShareSettingsType;
|
||||
setActiveId: React.Dispatch<React.SetStateAction<ShareViaType | ShareSettingsType>>;
|
||||
@@ -109,12 +110,13 @@ export const ShareView = ({ tabs, activeId, setActiveId }: ShareViewProps) => {
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"flex w-full justify-start rounded-md p-2 text-slate-600 hover:bg-slate-100 hover:text-slate-900",
|
||||
tab.id === activeId
|
||||
tab.id === activeId && !tab.disabled
|
||||
? "bg-slate-100 font-medium text-slate-900"
|
||||
: "text-slate-700"
|
||||
)}
|
||||
tooltip={tab.label}
|
||||
isActive={tab.id === activeId}>
|
||||
isActive={tab.id === activeId}
|
||||
disabled={tab.disabled}>
|
||||
<tab.icon className="h-4 w-4 text-slate-700" />
|
||||
<span>{tab.label}</span>
|
||||
</SidebarMenuButton>
|
||||
@@ -136,9 +138,10 @@ export const ShareView = ({ tabs, activeId, setActiveId }: ShareViewProps) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
disabled={tab.disabled}
|
||||
className={cn(
|
||||
"rounded-md px-4 py-2",
|
||||
tab.id === activeId
|
||||
tab.id === activeId && !tab.disabled
|
||||
? "bg-white text-slate-900 shadow-sm hover:bg-white"
|
||||
: "border-transparent text-slate-700 hover:text-slate-900"
|
||||
)}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { env } from "@/lib/env";
|
||||
import * as crypto from "@/lib/crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import {
|
||||
@@ -14,12 +15,69 @@ import {
|
||||
verifyTokenForLinkSurvey,
|
||||
} from "./jwt";
|
||||
|
||||
const TEST_ENCRYPTION_KEY = "0".repeat(32); // 32-byte key for AES-256-GCM
|
||||
const TEST_NEXTAUTH_SECRET = "test-nextauth-secret";
|
||||
const DIFFERENT_SECRET = "different-secret";
|
||||
|
||||
// Error message constants
|
||||
const NEXTAUTH_SECRET_ERROR = "NEXTAUTH_SECRET is not set";
|
||||
const ENCRYPTION_KEY_ERROR = "ENCRYPTION_KEY is not set";
|
||||
|
||||
// Helper function to test error cases for missing secrets/keys
|
||||
const testMissingSecretsError = async (
|
||||
testFn: (...args: any[]) => any,
|
||||
args: any[],
|
||||
options: {
|
||||
testNextAuthSecret?: boolean;
|
||||
testEncryptionKey?: boolean;
|
||||
isAsync?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
const { testNextAuthSecret = true, testEncryptionKey = true, isAsync = false } = options;
|
||||
|
||||
if (testNextAuthSecret) {
|
||||
const constants = await import("@/lib/constants");
|
||||
const originalSecret = (constants as any).NEXTAUTH_SECRET;
|
||||
(constants as any).NEXTAUTH_SECRET = undefined;
|
||||
|
||||
if (isAsync) {
|
||||
await expect(testFn(...args)).rejects.toThrow(NEXTAUTH_SECRET_ERROR);
|
||||
} else {
|
||||
expect(() => testFn(...args)).toThrow(NEXTAUTH_SECRET_ERROR);
|
||||
}
|
||||
|
||||
// Restore
|
||||
(constants as any).NEXTAUTH_SECRET = originalSecret;
|
||||
}
|
||||
|
||||
if (testEncryptionKey) {
|
||||
const constants = await import("@/lib/constants");
|
||||
const originalKey = (constants as any).ENCRYPTION_KEY;
|
||||
(constants as any).ENCRYPTION_KEY = undefined;
|
||||
|
||||
if (isAsync) {
|
||||
await expect(testFn(...args)).rejects.toThrow(ENCRYPTION_KEY_ERROR);
|
||||
} else {
|
||||
expect(() => testFn(...args)).toThrow(ENCRYPTION_KEY_ERROR);
|
||||
}
|
||||
|
||||
// Restore
|
||||
(constants as any).ENCRYPTION_KEY = originalKey;
|
||||
}
|
||||
};
|
||||
|
||||
// Mock environment variables
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENCRYPTION_KEY: "0".repeat(32), // 32-byte key for AES-256-GCM
|
||||
ENCRYPTION_KEY: "0".repeat(32),
|
||||
NEXTAUTH_SECRET: "test-nextauth-secret",
|
||||
} as typeof env,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
NEXTAUTH_SECRET: "test-nextauth-secret",
|
||||
ENCRYPTION_KEY: "0".repeat(32),
|
||||
}));
|
||||
|
||||
// Mock prisma
|
||||
@@ -31,22 +89,65 @@ vi.mock("@formbricks/database", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe("JWT Functions", () => {
|
||||
// Mock logger
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("JWT Functions - Comprehensive Security Tests", () => {
|
||||
const mockUser = {
|
||||
id: "test-user-id",
|
||||
email: "test@example.com",
|
||||
};
|
||||
|
||||
let mockSymmetricEncrypt: any;
|
||||
let mockSymmetricDecrypt: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default crypto mocks
|
||||
mockSymmetricEncrypt = vi
|
||||
.spyOn(crypto, "symmetricEncrypt")
|
||||
.mockImplementation((text: string) => `encrypted_${text}`);
|
||||
|
||||
mockSymmetricDecrypt = vi
|
||||
.spyOn(crypto, "symmetricDecrypt")
|
||||
.mockImplementation((encryptedText: string) => encryptedText.replace("encrypted_", ""));
|
||||
|
||||
(prisma.user.findUnique as any).mockResolvedValue(mockUser);
|
||||
});
|
||||
|
||||
describe("createToken", () => {
|
||||
test("should create a valid token", () => {
|
||||
const token = createToken(mockUser.id, mockUser.email);
|
||||
test("should create a valid token with encrypted user ID", () => {
|
||||
const token = createToken(mockUser.id);
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(mockUser.id, TEST_ENCRYPTION_KEY);
|
||||
});
|
||||
|
||||
test("should accept custom options", () => {
|
||||
const customOptions = { expiresIn: "1h" };
|
||||
const token = createToken(mockUser.id, customOptions);
|
||||
expect(token).toBeDefined();
|
||||
|
||||
// Verify the token contains the expected expiration
|
||||
const decoded = jwt.decode(token) as any;
|
||||
expect(decoded.exp).toBeDefined();
|
||||
expect(decoded.iat).toBeDefined();
|
||||
// Should expire in approximately 1 hour (3600 seconds)
|
||||
expect(decoded.exp - decoded.iat).toBe(3600);
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET is not set", async () => {
|
||||
await testMissingSecretsError(createToken, [mockUser.id], {
|
||||
testNextAuthSecret: true,
|
||||
testEncryptionKey: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +157,18 @@ describe("JWT Functions", () => {
|
||||
const token = createTokenForLinkSurvey(surveyId, mockUser.email);
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(mockUser.email, TEST_ENCRYPTION_KEY);
|
||||
});
|
||||
|
||||
test("should include surveyId in payload", () => {
|
||||
const surveyId = "test-survey-id";
|
||||
const token = createTokenForLinkSurvey(surveyId, mockUser.email);
|
||||
const decoded = jwt.decode(token) as any;
|
||||
expect(decoded.surveyId).toBe(surveyId);
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
await testMissingSecretsError(createTokenForLinkSurvey, ["survey-id", mockUser.email]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,24 +177,30 @@ describe("JWT Functions", () => {
|
||||
const token = createEmailToken(mockUser.email);
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(mockUser.email, TEST_ENCRYPTION_KEY);
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET is not set", () => {
|
||||
const originalSecret = env.NEXTAUTH_SECRET;
|
||||
try {
|
||||
(env as any).NEXTAUTH_SECRET = undefined;
|
||||
expect(() => createEmailToken(mockUser.email)).toThrow("NEXTAUTH_SECRET is not set");
|
||||
} finally {
|
||||
(env as any).NEXTAUTH_SECRET = originalSecret;
|
||||
}
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
await testMissingSecretsError(createEmailToken, [mockUser.email]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEmailFromEmailToken", () => {
|
||||
test("should extract email from valid token", () => {
|
||||
const token = createEmailToken(mockUser.email);
|
||||
const extractedEmail = getEmailFromEmailToken(token);
|
||||
expect(extractedEmail).toBe(mockUser.email);
|
||||
describe("createEmailChangeToken", () => {
|
||||
test("should create a valid email change token with 1 day expiration", () => {
|
||||
const token = createEmailChangeToken(mockUser.id, mockUser.email);
|
||||
expect(token).toBeDefined();
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(mockUser.id, TEST_ENCRYPTION_KEY);
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(mockUser.email, TEST_ENCRYPTION_KEY);
|
||||
|
||||
const decoded = jwt.decode(token) as any;
|
||||
expect(decoded.exp).toBeDefined();
|
||||
expect(decoded.iat).toBeDefined();
|
||||
// Should expire in approximately 1 day (86400 seconds)
|
||||
expect(decoded.exp - decoded.iat).toBe(86400);
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
await testMissingSecretsError(createEmailChangeToken, [mockUser.id, mockUser.email]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +210,50 @@ describe("JWT Functions", () => {
|
||||
const token = createInviteToken(inviteId, mockUser.email);
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(inviteId, TEST_ENCRYPTION_KEY);
|
||||
expect(mockSymmetricEncrypt).toHaveBeenCalledWith(mockUser.email, TEST_ENCRYPTION_KEY);
|
||||
});
|
||||
|
||||
test("should accept custom options", () => {
|
||||
const inviteId = "test-invite-id";
|
||||
const customOptions = { expiresIn: "24h" };
|
||||
const token = createInviteToken(inviteId, mockUser.email, customOptions);
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const decoded = jwt.decode(token) as any;
|
||||
expect(decoded.exp).toBeDefined();
|
||||
expect(decoded.iat).toBeDefined();
|
||||
// Should expire in approximately 24 hours (86400 seconds)
|
||||
expect(decoded.exp - decoded.iat).toBe(86400);
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
await testMissingSecretsError(createInviteToken, ["invite-id", mockUser.email]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEmailFromEmailToken", () => {
|
||||
test("should extract email from valid token", () => {
|
||||
const token = createEmailToken(mockUser.email);
|
||||
const extractedEmail = getEmailFromEmailToken(token);
|
||||
expect(extractedEmail).toBe(mockUser.email);
|
||||
expect(mockSymmetricDecrypt).toHaveBeenCalledWith(`encrypted_${mockUser.email}`, TEST_ENCRYPTION_KEY);
|
||||
});
|
||||
|
||||
test("should fall back to original email if decryption fails", () => {
|
||||
mockSymmetricDecrypt.mockImplementationOnce(() => {
|
||||
throw new Error("Decryption failed");
|
||||
});
|
||||
|
||||
// Create token manually with unencrypted email for legacy compatibility
|
||||
const legacyToken = jwt.sign({ email: mockUser.email }, TEST_NEXTAUTH_SECRET);
|
||||
const extractedEmail = getEmailFromEmailToken(legacyToken);
|
||||
expect(extractedEmail).toBe(mockUser.email);
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
const token = jwt.sign({ email: "test@example.com" }, TEST_NEXTAUTH_SECRET);
|
||||
await testMissingSecretsError(getEmailFromEmailToken, [token]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,23 +269,194 @@ describe("JWT Functions", () => {
|
||||
const result = verifyTokenForLinkSurvey("invalid-token", "test-survey-id");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should return null if NEXTAUTH_SECRET is not set", async () => {
|
||||
const constants = await import("@/lib/constants");
|
||||
const originalSecret = (constants as any).NEXTAUTH_SECRET;
|
||||
(constants as any).NEXTAUTH_SECRET = undefined;
|
||||
|
||||
const result = verifyTokenForLinkSurvey("any-token", "test-survey-id");
|
||||
expect(result).toBeNull();
|
||||
|
||||
// Restore
|
||||
(constants as any).NEXTAUTH_SECRET = originalSecret;
|
||||
});
|
||||
|
||||
test("should return null if surveyId doesn't match", () => {
|
||||
const surveyId = "test-survey-id";
|
||||
const differentSurveyId = "different-survey-id";
|
||||
const token = createTokenForLinkSurvey(surveyId, mockUser.email);
|
||||
const result = verifyTokenForLinkSurvey(token, differentSurveyId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should return null if email is missing from payload", () => {
|
||||
const tokenWithoutEmail = jwt.sign({ surveyId: "test-survey-id" }, TEST_NEXTAUTH_SECRET);
|
||||
const result = verifyTokenForLinkSurvey(tokenWithoutEmail, "test-survey-id");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should fall back to original email if decryption fails", () => {
|
||||
mockSymmetricDecrypt.mockImplementationOnce(() => {
|
||||
throw new Error("Decryption failed");
|
||||
});
|
||||
|
||||
// Create legacy token with unencrypted email
|
||||
const legacyToken = jwt.sign(
|
||||
{
|
||||
email: mockUser.email,
|
||||
surveyId: "test-survey-id",
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
const result = verifyTokenForLinkSurvey(legacyToken, "test-survey-id");
|
||||
expect(result).toBe(mockUser.email);
|
||||
});
|
||||
|
||||
test("should fall back to original email if ENCRYPTION_KEY is not set", async () => {
|
||||
const constants = await import("@/lib/constants");
|
||||
const originalKey = (constants as any).ENCRYPTION_KEY;
|
||||
(constants as any).ENCRYPTION_KEY = undefined;
|
||||
|
||||
// Create a token with unencrypted email (as it would be if ENCRYPTION_KEY was not set during creation)
|
||||
const token = jwt.sign(
|
||||
{
|
||||
email: mockUser.email,
|
||||
surveyId: "survey-id",
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
const result = verifyTokenForLinkSurvey(token, "survey-id");
|
||||
expect(result).toBe(mockUser.email);
|
||||
|
||||
// Restore
|
||||
(constants as any).ENCRYPTION_KEY = originalKey;
|
||||
});
|
||||
|
||||
test("should verify legacy survey tokens with surveyId-based secret", async () => {
|
||||
const surveyId = "test-survey-id";
|
||||
|
||||
// Create legacy token with old format (NEXTAUTH_SECRET + surveyId)
|
||||
const legacyToken = jwt.sign({ email: `encrypted_${mockUser.email}` }, TEST_NEXTAUTH_SECRET + surveyId);
|
||||
|
||||
const result = verifyTokenForLinkSurvey(legacyToken, surveyId);
|
||||
expect(result).toBe(mockUser.email);
|
||||
});
|
||||
|
||||
test("should reject survey tokens that fail both new and legacy verification", async () => {
|
||||
const surveyId = "test-survey-id";
|
||||
const invalidToken = jwt.sign({ email: "encrypted_test@example.com" }, "wrong-secret");
|
||||
|
||||
const result = verifyTokenForLinkSurvey(invalidToken, surveyId);
|
||||
expect(result).toBeNull();
|
||||
|
||||
// Verify error logging
|
||||
const { logger } = await import("@formbricks/logger");
|
||||
expect(logger.error).toHaveBeenCalledWith(expect.any(Error), "Survey link token verification failed");
|
||||
});
|
||||
|
||||
test("should reject legacy survey tokens for wrong survey", () => {
|
||||
const correctSurveyId = "correct-survey-id";
|
||||
const wrongSurveyId = "wrong-survey-id";
|
||||
|
||||
// Create legacy token for one survey
|
||||
const legacyToken = jwt.sign(
|
||||
{ email: `encrypted_${mockUser.email}` },
|
||||
TEST_NEXTAUTH_SECRET + correctSurveyId
|
||||
);
|
||||
|
||||
// Try to verify with different survey ID
|
||||
const result = verifyTokenForLinkSurvey(legacyToken, wrongSurveyId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyToken", () => {
|
||||
test("should verify valid token", async () => {
|
||||
const token = createToken(mockUser.id, mockUser.email);
|
||||
const token = createToken(mockUser.id);
|
||||
const verified = await verifyToken(token);
|
||||
expect(verified).toEqual({
|
||||
id: mockUser.id,
|
||||
id: mockUser.id, // Returns the decrypted user ID
|
||||
email: mockUser.email,
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw error if user not found", async () => {
|
||||
(prisma.user.findUnique as any).mockResolvedValue(null);
|
||||
const token = createToken(mockUser.id, mockUser.email);
|
||||
const token = createToken(mockUser.id);
|
||||
await expect(verifyToken(token)).rejects.toThrow("User not found");
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET is not set", async () => {
|
||||
await testMissingSecretsError(verifyToken, ["any-token"], {
|
||||
testNextAuthSecret: true,
|
||||
testEncryptionKey: false,
|
||||
isAsync: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw error for invalid token signature", async () => {
|
||||
const invalidToken = jwt.sign({ id: "test-id" }, DIFFERENT_SECRET);
|
||||
await expect(verifyToken(invalidToken)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should throw error if token payload is missing id", async () => {
|
||||
const tokenWithoutId = jwt.sign({ email: mockUser.email }, TEST_NEXTAUTH_SECRET);
|
||||
await expect(verifyToken(tokenWithoutId)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should return raw id from payload", async () => {
|
||||
// Create token with unencrypted id
|
||||
const token = jwt.sign({ id: mockUser.id }, TEST_NEXTAUTH_SECRET);
|
||||
const verified = await verifyToken(token);
|
||||
expect(verified).toEqual({
|
||||
id: mockUser.id, // Returns the raw ID from payload
|
||||
email: mockUser.email,
|
||||
});
|
||||
});
|
||||
|
||||
test("should verify legacy tokens with email-based secret", async () => {
|
||||
// Create legacy token with old format (NEXTAUTH_SECRET + userEmail)
|
||||
const legacyToken = jwt.sign({ id: `encrypted_${mockUser.id}` }, TEST_NEXTAUTH_SECRET + mockUser.email);
|
||||
|
||||
const verified = await verifyToken(legacyToken);
|
||||
expect(verified).toEqual({
|
||||
id: mockUser.id, // Returns the decrypted user ID
|
||||
email: mockUser.email,
|
||||
});
|
||||
});
|
||||
|
||||
test("should prioritize new tokens over legacy tokens", async () => {
|
||||
// Create both new and legacy tokens for the same user
|
||||
const newToken = createToken(mockUser.id);
|
||||
const legacyToken = jwt.sign({ id: `encrypted_${mockUser.id}` }, TEST_NEXTAUTH_SECRET + mockUser.email);
|
||||
|
||||
// New token should verify without triggering legacy path
|
||||
const verifiedNew = await verifyToken(newToken);
|
||||
expect(verifiedNew.id).toBe(mockUser.id); // Returns decrypted user ID
|
||||
|
||||
// Legacy token should trigger legacy path
|
||||
const verifiedLegacy = await verifyToken(legacyToken);
|
||||
expect(verifiedLegacy.id).toBe(mockUser.id); // Returns decrypted user ID
|
||||
});
|
||||
|
||||
test("should reject tokens that fail both new and legacy verification", async () => {
|
||||
const invalidToken = jwt.sign({ id: "encrypted_test-id" }, "wrong-secret");
|
||||
await expect(verifyToken(invalidToken)).rejects.toThrow("Invalid token");
|
||||
|
||||
// Verify both methods were attempted
|
||||
const { logger } = await import("@formbricks/logger");
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
"Token verification failed with new method"
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
"Token verification failed with legacy method"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyInviteToken", () => {
|
||||
@@ -139,6 +473,53 @@ describe("JWT Functions", () => {
|
||||
test("should throw error for invalid token", () => {
|
||||
expect(() => verifyInviteToken("invalid-token")).toThrow("Invalid or expired invite token");
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
await testMissingSecretsError(verifyInviteToken, ["any-token"]);
|
||||
});
|
||||
|
||||
test("should throw error if inviteId is missing", () => {
|
||||
const tokenWithoutInviteId = jwt.sign({ email: mockUser.email }, TEST_NEXTAUTH_SECRET);
|
||||
expect(() => verifyInviteToken(tokenWithoutInviteId)).toThrow("Invalid or expired invite token");
|
||||
});
|
||||
|
||||
test("should throw error if email is missing", () => {
|
||||
const tokenWithoutEmail = jwt.sign({ inviteId: "test-invite-id" }, TEST_NEXTAUTH_SECRET);
|
||||
expect(() => verifyInviteToken(tokenWithoutEmail)).toThrow("Invalid or expired invite token");
|
||||
});
|
||||
|
||||
test("should fall back to original values if decryption fails", () => {
|
||||
mockSymmetricDecrypt.mockImplementation(() => {
|
||||
throw new Error("Decryption failed");
|
||||
});
|
||||
|
||||
const inviteId = "test-invite-id";
|
||||
const legacyToken = jwt.sign(
|
||||
{
|
||||
inviteId,
|
||||
email: mockUser.email,
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
const verified = verifyInviteToken(legacyToken);
|
||||
expect(verified).toEqual({
|
||||
inviteId,
|
||||
email: mockUser.email,
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw error for token with wrong signature", () => {
|
||||
const invalidToken = jwt.sign(
|
||||
{
|
||||
inviteId: "test-invite-id",
|
||||
email: mockUser.email,
|
||||
},
|
||||
DIFFERENT_SECRET
|
||||
);
|
||||
|
||||
expect(() => verifyInviteToken(invalidToken)).toThrow("Invalid or expired invite token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyEmailChangeToken", () => {
|
||||
@@ -150,22 +531,478 @@ describe("JWT Functions", () => {
|
||||
expect(result).toEqual({ id: userId, email });
|
||||
});
|
||||
|
||||
test("should throw error if NEXTAUTH_SECRET or ENCRYPTION_KEY is not set", async () => {
|
||||
await testMissingSecretsError(verifyEmailChangeToken, ["any-token"], { isAsync: true });
|
||||
});
|
||||
|
||||
test("should throw error if token is invalid or missing fields", async () => {
|
||||
// Create a token with missing fields
|
||||
const jwt = await import("jsonwebtoken");
|
||||
const token = jwt.sign({ foo: "bar" }, env.NEXTAUTH_SECRET as string);
|
||||
const token = jwt.sign({ foo: "bar" }, TEST_NEXTAUTH_SECRET);
|
||||
await expect(verifyEmailChangeToken(token)).rejects.toThrow(
|
||||
"Token is invalid or missing required fields"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error if id is missing", async () => {
|
||||
const token = jwt.sign({ email: "test@example.com" }, TEST_NEXTAUTH_SECRET);
|
||||
await expect(verifyEmailChangeToken(token)).rejects.toThrow(
|
||||
"Token is invalid or missing required fields"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error if email is missing", async () => {
|
||||
const token = jwt.sign({ id: "test-id" }, TEST_NEXTAUTH_SECRET);
|
||||
await expect(verifyEmailChangeToken(token)).rejects.toThrow(
|
||||
"Token is invalid or missing required fields"
|
||||
);
|
||||
});
|
||||
|
||||
test("should return original id/email if decryption fails", async () => {
|
||||
// Create a token with non-encrypted id/email
|
||||
const jwt = await import("jsonwebtoken");
|
||||
mockSymmetricDecrypt.mockImplementation(() => {
|
||||
throw new Error("Decryption failed");
|
||||
});
|
||||
|
||||
const payload = { id: "plain-id", email: "plain@example.com" };
|
||||
const token = jwt.sign(payload, env.NEXTAUTH_SECRET as string);
|
||||
const token = jwt.sign(payload, TEST_NEXTAUTH_SECRET);
|
||||
const result = await verifyEmailChangeToken(token);
|
||||
expect(result).toEqual(payload);
|
||||
});
|
||||
|
||||
test("should throw error for token with wrong signature", async () => {
|
||||
const invalidToken = jwt.sign(
|
||||
{
|
||||
id: "test-id",
|
||||
email: "test@example.com",
|
||||
},
|
||||
DIFFERENT_SECRET
|
||||
);
|
||||
|
||||
await expect(verifyEmailChangeToken(invalidToken)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// SECURITY SCENARIO TESTS
|
||||
describe("Security Scenarios", () => {
|
||||
describe("Algorithm Confusion Attack Prevention", () => {
|
||||
test("should reject 'none' algorithm tokens in verifyToken", async () => {
|
||||
// Create malicious token with "none" algorithm
|
||||
const maliciousToken =
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
})
|
||||
).toString("base64url") +
|
||||
"." +
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
id: "encrypted_malicious-id",
|
||||
})
|
||||
).toString("base64url") +
|
||||
".";
|
||||
|
||||
await expect(verifyToken(maliciousToken)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should reject 'none' algorithm tokens in verifyTokenForLinkSurvey", () => {
|
||||
const maliciousToken =
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
})
|
||||
).toString("base64url") +
|
||||
"." +
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
email: "encrypted_attacker@evil.com",
|
||||
surveyId: "test-survey-id",
|
||||
})
|
||||
).toString("base64url") +
|
||||
".";
|
||||
|
||||
const result = verifyTokenForLinkSurvey(maliciousToken, "test-survey-id");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should reject 'none' algorithm tokens in verifyInviteToken", () => {
|
||||
const maliciousToken =
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
})
|
||||
).toString("base64url") +
|
||||
"." +
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
inviteId: "encrypted_malicious-invite",
|
||||
email: "encrypted_attacker@evil.com",
|
||||
})
|
||||
).toString("base64url") +
|
||||
".";
|
||||
|
||||
expect(() => verifyInviteToken(maliciousToken)).toThrow("Invalid or expired invite token");
|
||||
});
|
||||
|
||||
test("should reject 'none' algorithm tokens in verifyEmailChangeToken", async () => {
|
||||
const maliciousToken =
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
})
|
||||
).toString("base64url") +
|
||||
"." +
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
id: "encrypted_malicious-id",
|
||||
email: "encrypted_attacker@evil.com",
|
||||
})
|
||||
).toString("base64url") +
|
||||
".";
|
||||
|
||||
await expect(verifyEmailChangeToken(maliciousToken)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("should reject RS256 algorithm tokens (HS256/RS256 confusion)", async () => {
|
||||
// Create malicious token with RS256 algorithm header but HS256 signature
|
||||
const maliciousHeader = Buffer.from(
|
||||
JSON.stringify({
|
||||
alg: "RS256",
|
||||
typ: "JWT",
|
||||
})
|
||||
).toString("base64url");
|
||||
|
||||
const maliciousPayload = Buffer.from(
|
||||
JSON.stringify({
|
||||
id: "encrypted_malicious-id",
|
||||
})
|
||||
).toString("base64url");
|
||||
|
||||
// Create signature using HMAC (as if it were HS256)
|
||||
const crypto = require("crypto");
|
||||
const signature = crypto
|
||||
.createHmac("sha256", TEST_NEXTAUTH_SECRET)
|
||||
.update(`${maliciousHeader}.${maliciousPayload}`)
|
||||
.digest("base64url");
|
||||
|
||||
const maliciousToken = `${maliciousHeader}.${maliciousPayload}.${signature}`;
|
||||
|
||||
await expect(verifyToken(maliciousToken)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should only accept HS256 algorithm", async () => {
|
||||
// Test that other valid algorithms are rejected
|
||||
const otherAlgorithms = ["HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"];
|
||||
|
||||
for (const alg of otherAlgorithms) {
|
||||
const maliciousHeader = Buffer.from(
|
||||
JSON.stringify({
|
||||
alg,
|
||||
typ: "JWT",
|
||||
})
|
||||
).toString("base64url");
|
||||
|
||||
const maliciousPayload = Buffer.from(
|
||||
JSON.stringify({
|
||||
id: "encrypted_test-id",
|
||||
})
|
||||
).toString("base64url");
|
||||
|
||||
const maliciousToken = `${maliciousHeader}.${maliciousPayload}.fake-signature`;
|
||||
|
||||
await expect(verifyToken(maliciousToken)).rejects.toThrow("Invalid token");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Token Tampering", () => {
|
||||
test("should reject tokens with modified payload", async () => {
|
||||
const token = createToken(mockUser.id);
|
||||
const [header, payload, signature] = token.split(".");
|
||||
|
||||
// Modify the payload
|
||||
const decodedPayload = JSON.parse(Buffer.from(payload, "base64url").toString());
|
||||
decodedPayload.id = "malicious-id";
|
||||
const tamperedPayload = Buffer.from(JSON.stringify(decodedPayload)).toString("base64url");
|
||||
const tamperedToken = `${header}.${tamperedPayload}.${signature}`;
|
||||
|
||||
await expect(verifyToken(tamperedToken)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should reject tokens with modified signature", async () => {
|
||||
const token = createToken(mockUser.id);
|
||||
const [header, payload] = token.split(".");
|
||||
const tamperedToken = `${header}.${payload}.tamperedsignature`;
|
||||
|
||||
await expect(verifyToken(tamperedToken)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should reject malformed tokens", async () => {
|
||||
const malformedTokens = [
|
||||
"not.a.jwt",
|
||||
"only.two.parts",
|
||||
"too.many.parts.here.invalid",
|
||||
"",
|
||||
"invalid-base64",
|
||||
];
|
||||
|
||||
for (const malformedToken of malformedTokens) {
|
||||
await expect(verifyToken(malformedToken)).rejects.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cross-Survey Token Reuse", () => {
|
||||
test("should reject survey tokens used for different surveys", () => {
|
||||
const surveyId1 = "survey-1";
|
||||
const surveyId2 = "survey-2";
|
||||
|
||||
const token = createTokenForLinkSurvey(surveyId1, mockUser.email);
|
||||
const result = verifyTokenForLinkSurvey(token, surveyId2);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Expired Tokens", () => {
|
||||
test("should reject expired tokens", async () => {
|
||||
const expiredToken = jwt.sign(
|
||||
{
|
||||
id: "encrypted_test-id",
|
||||
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
await expect(verifyToken(expiredToken)).rejects.toThrow("Invalid token");
|
||||
});
|
||||
|
||||
test("should reject expired email change tokens", async () => {
|
||||
const expiredToken = jwt.sign(
|
||||
{
|
||||
id: "encrypted_test-id",
|
||||
email: "encrypted_test@example.com",
|
||||
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
await expect(verifyEmailChangeToken(expiredToken)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Encryption Key Attacks", () => {
|
||||
test("should fail gracefully with wrong encryption key", async () => {
|
||||
mockSymmetricDecrypt.mockImplementation(() => {
|
||||
throw new Error("Authentication tag verification failed");
|
||||
});
|
||||
|
||||
// Mock findUnique to only return user for correct decrypted ID, not ciphertext
|
||||
(prisma.user.findUnique as any).mockImplementation(({ where }: { where: { id: string } }) => {
|
||||
if (where.id === mockUser.id) {
|
||||
return Promise.resolve(mockUser);
|
||||
}
|
||||
return Promise.resolve(null); // Return null for ciphertext IDs
|
||||
});
|
||||
|
||||
const token = createToken(mockUser.id);
|
||||
// Should fail because ciphertext passed as userId won't match any user in DB
|
||||
await expect(verifyToken(token)).rejects.toThrow(/User not found/i);
|
||||
});
|
||||
|
||||
test("should handle encryption key not set gracefully", async () => {
|
||||
const constants = await import("@/lib/constants");
|
||||
const originalKey = (constants as any).ENCRYPTION_KEY;
|
||||
(constants as any).ENCRYPTION_KEY = undefined;
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
email: "test@example.com",
|
||||
surveyId: "test-survey-id",
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
const result = verifyTokenForLinkSurvey(token, "test-survey-id");
|
||||
expect(result).toBe("test@example.com");
|
||||
|
||||
// Restore
|
||||
(constants as any).ENCRYPTION_KEY = originalKey;
|
||||
});
|
||||
});
|
||||
|
||||
describe("SQL Injection Attempts", () => {
|
||||
test("should safely handle malicious user IDs", async () => {
|
||||
const maliciousIds = [
|
||||
"'; DROP TABLE users; --",
|
||||
"1' OR '1'='1",
|
||||
"admin'/*",
|
||||
"<script>alert('xss')</script>",
|
||||
"../../etc/passwd",
|
||||
];
|
||||
|
||||
for (const maliciousId of maliciousIds) {
|
||||
mockSymmetricDecrypt.mockReturnValueOnce(maliciousId);
|
||||
|
||||
const token = jwt.sign({ id: "encrypted_malicious" }, TEST_NEXTAUTH_SECRET);
|
||||
|
||||
// The function should look up the user safely
|
||||
await verifyToken(token);
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: maliciousId },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Token Reuse and Replay Attacks", () => {
|
||||
test("should allow legitimate token reuse within validity period", async () => {
|
||||
const token = createToken(mockUser.id);
|
||||
|
||||
// First use
|
||||
const result1 = await verifyToken(token);
|
||||
expect(result1.id).toBe(mockUser.id); // Returns decrypted user ID
|
||||
|
||||
// Second use (should still work)
|
||||
const result2 = await verifyToken(token);
|
||||
expect(result2.id).toBe(mockUser.id); // Returns decrypted user ID
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy Token Compatibility", () => {
|
||||
test("should handle legacy unencrypted tokens gracefully", async () => {
|
||||
// Legacy token with plain text data
|
||||
const legacyToken = jwt.sign({ id: mockUser.id }, TEST_NEXTAUTH_SECRET);
|
||||
const result = await verifyToken(legacyToken);
|
||||
|
||||
expect(result.id).toBe(mockUser.id); // Returns raw ID from payload
|
||||
expect(result.email).toBe(mockUser.email);
|
||||
});
|
||||
|
||||
test("should handle mixed encrypted/unencrypted fields", async () => {
|
||||
mockSymmetricDecrypt
|
||||
.mockImplementationOnce(() => mockUser.id) // id decrypts successfully
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("Email not encrypted");
|
||||
}); // email fails
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: "encrypted_test-id",
|
||||
email: "plain-email@example.com",
|
||||
},
|
||||
TEST_NEXTAUTH_SECRET
|
||||
);
|
||||
|
||||
const result = await verifyEmailChangeToken(token);
|
||||
expect(result.id).toBe(mockUser.id);
|
||||
expect(result.email).toBe("plain-email@example.com");
|
||||
});
|
||||
|
||||
test("should verify old format user tokens with email-based secrets", async () => {
|
||||
// Simulate old token format with per-user secret
|
||||
const oldFormatToken = jwt.sign(
|
||||
{ id: `encrypted_${mockUser.id}` },
|
||||
TEST_NEXTAUTH_SECRET + mockUser.email
|
||||
);
|
||||
|
||||
const result = await verifyToken(oldFormatToken);
|
||||
expect(result.id).toBe(mockUser.id); // Returns decrypted user ID
|
||||
expect(result.email).toBe(mockUser.email);
|
||||
});
|
||||
|
||||
test("should verify old format survey tokens with survey-based secrets", () => {
|
||||
const surveyId = "legacy-survey-id";
|
||||
|
||||
// Simulate old survey token format
|
||||
const oldFormatSurveyToken = jwt.sign(
|
||||
{ email: `encrypted_${mockUser.email}` },
|
||||
TEST_NEXTAUTH_SECRET + surveyId
|
||||
);
|
||||
|
||||
const result = verifyTokenForLinkSurvey(oldFormatSurveyToken, surveyId);
|
||||
expect(result).toBe(mockUser.email);
|
||||
});
|
||||
|
||||
test("should gracefully handle database errors during legacy verification", async () => {
|
||||
// Create token that will fail new method
|
||||
const legacyToken = jwt.sign(
|
||||
{ id: `encrypted_${mockUser.id}` },
|
||||
TEST_NEXTAUTH_SECRET + mockUser.email
|
||||
);
|
||||
|
||||
// Make database lookup fail
|
||||
(prisma.user.findUnique as any).mockRejectedValueOnce(new Error("DB connection lost"));
|
||||
|
||||
await expect(verifyToken(legacyToken)).rejects.toThrow("DB connection lost");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases and Error Handling", () => {
|
||||
test("should handle database connection errors gracefully", async () => {
|
||||
(prisma.user.findUnique as any).mockRejectedValue(new Error("Database connection failed"));
|
||||
|
||||
const token = createToken(mockUser.id);
|
||||
await expect(verifyToken(token)).rejects.toThrow("Database connection failed");
|
||||
});
|
||||
|
||||
test("should handle crypto module errors", () => {
|
||||
mockSymmetricEncrypt.mockImplementation(() => {
|
||||
throw new Error("Crypto module error");
|
||||
});
|
||||
|
||||
expect(() => createToken(mockUser.id)).toThrow("Crypto module error");
|
||||
});
|
||||
|
||||
test("should validate email format in tokens", () => {
|
||||
const invalidEmails = ["", "not-an-email", "missing@", "@missing-local.com", "spaces in@email.com"];
|
||||
|
||||
invalidEmails.forEach((invalidEmail) => {
|
||||
expect(() => createEmailToken(invalidEmail)).not.toThrow();
|
||||
// Note: JWT functions don't validate email format, they just encrypt/decrypt
|
||||
// Email validation should happen at a higher level
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle extremely long inputs", () => {
|
||||
const longString = "a".repeat(10000);
|
||||
|
||||
expect(() => createToken(longString)).not.toThrow();
|
||||
expect(() => createEmailToken(longString)).not.toThrow();
|
||||
});
|
||||
|
||||
test("should handle special characters in user data", () => {
|
||||
const specialChars = "!@#$%^&*()_+-=[]{}|;:'\",.<>?/~`";
|
||||
|
||||
expect(() => createToken(specialChars)).not.toThrow();
|
||||
expect(() => createEmailToken(specialChars)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Performance and Resource Exhaustion", () => {
|
||||
test("should handle rapid token creation without memory leaks", () => {
|
||||
const tokens: string[] = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
tokens.push(createToken(`user-${i}`));
|
||||
}
|
||||
|
||||
expect(tokens.length).toBe(1000);
|
||||
expect(tokens.every((token) => typeof token === "string")).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle rapid token verification", async () => {
|
||||
const token = createToken(mockUser.id);
|
||||
|
||||
const verifications: Promise<any>[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
verifications.push(verifyToken(token));
|
||||
}
|
||||
|
||||
const results = await Promise.all(verifications);
|
||||
expect(results.length).toBe(100);
|
||||
expect(results.every((result: any) => result.id === mockUser.id)).toBe(true); // Returns decrypted user ID
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,43 +1,64 @@
|
||||
import { ENCRYPTION_KEY, NEXTAUTH_SECRET } from "@/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
|
||||
import { env } from "@/lib/env";
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const createToken = (userId: string, userEmail: string, options = {}): string => {
|
||||
const encryptedUserId = symmetricEncrypt(userId, env.ENCRYPTION_KEY);
|
||||
return jwt.sign({ id: encryptedUserId }, env.NEXTAUTH_SECRET + userEmail, options);
|
||||
};
|
||||
export const createTokenForLinkSurvey = (surveyId: string, userEmail: string): string => {
|
||||
const encryptedEmail = symmetricEncrypt(userEmail, env.ENCRYPTION_KEY);
|
||||
return jwt.sign({ email: encryptedEmail }, env.NEXTAUTH_SECRET + surveyId);
|
||||
// Helper function to decrypt with fallback to plain text
|
||||
const decryptWithFallback = (encryptedText: string, key: string): string => {
|
||||
try {
|
||||
return symmetricDecrypt(encryptedText, key);
|
||||
} catch {
|
||||
return encryptedText; // Return as-is if decryption fails (legacy format)
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyEmailChangeToken = async (token: string): Promise<{ id: string; email: string }> => {
|
||||
if (!env.NEXTAUTH_SECRET) {
|
||||
export const createToken = (userId: string, options = {}): string => {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, env.NEXTAUTH_SECRET) as { id: string; email: string };
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const encryptedUserId = symmetricEncrypt(userId, ENCRYPTION_KEY);
|
||||
return jwt.sign({ id: encryptedUserId }, NEXTAUTH_SECRET, options);
|
||||
};
|
||||
export const createTokenForLinkSurvey = (surveyId: string, userEmail: string): string => {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const encryptedEmail = symmetricEncrypt(userEmail, ENCRYPTION_KEY);
|
||||
return jwt.sign({ email: encryptedEmail, surveyId }, NEXTAUTH_SECRET);
|
||||
};
|
||||
|
||||
export const verifyEmailChangeToken = async (token: string): Promise<{ id: string; email: string }> => {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, NEXTAUTH_SECRET, { algorithms: ["HS256"] }) as {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
if (!payload?.id || !payload?.email) {
|
||||
throw new Error("Token is invalid or missing required fields");
|
||||
}
|
||||
|
||||
let decryptedId: string;
|
||||
let decryptedEmail: string;
|
||||
|
||||
try {
|
||||
decryptedId = symmetricDecrypt(payload.id, env.ENCRYPTION_KEY);
|
||||
} catch {
|
||||
decryptedId = payload.id;
|
||||
}
|
||||
|
||||
try {
|
||||
decryptedEmail = symmetricDecrypt(payload.email, env.ENCRYPTION_KEY);
|
||||
} catch {
|
||||
decryptedEmail = payload.email;
|
||||
}
|
||||
// Decrypt both fields with fallback
|
||||
const decryptedId = decryptWithFallback(payload.id, ENCRYPTION_KEY);
|
||||
const decryptedEmail = decryptWithFallback(payload.email, ENCRYPTION_KEY);
|
||||
|
||||
return {
|
||||
id: decryptedId,
|
||||
@@ -46,127 +67,230 @@ export const verifyEmailChangeToken = async (token: string): Promise<{ id: strin
|
||||
};
|
||||
|
||||
export const createEmailChangeToken = (userId: string, email: string): string => {
|
||||
const encryptedUserId = symmetricEncrypt(userId, env.ENCRYPTION_KEY);
|
||||
const encryptedEmail = symmetricEncrypt(email, env.ENCRYPTION_KEY);
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const encryptedUserId = symmetricEncrypt(userId, ENCRYPTION_KEY);
|
||||
const encryptedEmail = symmetricEncrypt(email, ENCRYPTION_KEY);
|
||||
|
||||
const payload = {
|
||||
id: encryptedUserId,
|
||||
email: encryptedEmail,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, env.NEXTAUTH_SECRET as string, {
|
||||
return jwt.sign(payload, NEXTAUTH_SECRET, {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
};
|
||||
|
||||
export const createEmailToken = (email: string): string => {
|
||||
if (!env.NEXTAUTH_SECRET) {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
const encryptedEmail = symmetricEncrypt(email, env.ENCRYPTION_KEY);
|
||||
return jwt.sign({ email: encryptedEmail }, env.NEXTAUTH_SECRET);
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const encryptedEmail = symmetricEncrypt(email, ENCRYPTION_KEY);
|
||||
return jwt.sign({ email: encryptedEmail }, NEXTAUTH_SECRET);
|
||||
};
|
||||
|
||||
export const getEmailFromEmailToken = (token: string): string => {
|
||||
if (!env.NEXTAUTH_SECRET) {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, env.NEXTAUTH_SECRET) as JwtPayload;
|
||||
try {
|
||||
// Try to decrypt first (for newer tokens)
|
||||
const decryptedEmail = symmetricDecrypt(payload.email, env.ENCRYPTION_KEY);
|
||||
return decryptedEmail;
|
||||
} catch {
|
||||
// If decryption fails, return the original email (for older tokens)
|
||||
return payload.email;
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, NEXTAUTH_SECRET, { algorithms: ["HS256"] }) as JwtPayload & {
|
||||
email: string;
|
||||
};
|
||||
return decryptWithFallback(payload.email, ENCRYPTION_KEY);
|
||||
};
|
||||
|
||||
export const createInviteToken = (inviteId: string, email: string, options = {}): string => {
|
||||
if (!env.NEXTAUTH_SECRET) {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
const encryptedInviteId = symmetricEncrypt(inviteId, env.ENCRYPTION_KEY);
|
||||
const encryptedEmail = symmetricEncrypt(email, env.ENCRYPTION_KEY);
|
||||
return jwt.sign({ inviteId: encryptedInviteId, email: encryptedEmail }, env.NEXTAUTH_SECRET, options);
|
||||
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
const encryptedInviteId = symmetricEncrypt(inviteId, ENCRYPTION_KEY);
|
||||
const encryptedEmail = symmetricEncrypt(email, ENCRYPTION_KEY);
|
||||
return jwt.sign({ inviteId: encryptedInviteId, email: encryptedEmail }, NEXTAUTH_SECRET, options);
|
||||
};
|
||||
|
||||
export const verifyTokenForLinkSurvey = (token: string, surveyId: string): string | null => {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { email } = jwt.verify(token, env.NEXTAUTH_SECRET + surveyId) as JwtPayload;
|
||||
let payload: JwtPayload & { email: string; surveyId?: string };
|
||||
|
||||
// Try primary method first (consistent secret)
|
||||
try {
|
||||
// Try to decrypt first (for newer tokens)
|
||||
if (!env.ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
payload = jwt.verify(token, NEXTAUTH_SECRET, { algorithms: ["HS256"] }) as JwtPayload & {
|
||||
email: string;
|
||||
surveyId: string;
|
||||
};
|
||||
} catch (primaryError) {
|
||||
logger.error(primaryError, "Token verification failed with primary method");
|
||||
|
||||
// Fallback to legacy method (surveyId-based secret)
|
||||
try {
|
||||
payload = jwt.verify(token, NEXTAUTH_SECRET + surveyId, { algorithms: ["HS256"] }) as JwtPayload & {
|
||||
email: string;
|
||||
};
|
||||
} catch (legacyError) {
|
||||
logger.error(legacyError, "Token verification failed with legacy method");
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
const decryptedEmail = symmetricDecrypt(email, env.ENCRYPTION_KEY);
|
||||
return decryptedEmail;
|
||||
} catch {
|
||||
// If decryption fails, return the original email (for older tokens)
|
||||
return email;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
// Verify the surveyId matches if present in payload (new format)
|
||||
if (payload.surveyId && payload.surveyId !== surveyId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { email } = payload;
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decrypt email with fallback to plain text
|
||||
if (!ENCRYPTION_KEY) {
|
||||
return email; // Return as-is if encryption key not set
|
||||
}
|
||||
|
||||
return decryptWithFallback(email, ENCRYPTION_KEY);
|
||||
} catch (error) {
|
||||
logger.error(error, "Survey link token verification failed");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyToken = async (token: string): Promise<JwtPayload> => {
|
||||
// First decode to get the ID
|
||||
const decoded = jwt.decode(token);
|
||||
const payload: JwtPayload = decoded as JwtPayload;
|
||||
// Helper function to get user email for legacy verification
|
||||
const getUserEmailForLegacyVerification = async (
|
||||
token: string,
|
||||
userId?: string
|
||||
): Promise<{ userId: string; userEmail: string }> => {
|
||||
if (!userId) {
|
||||
const decoded = jwt.decode(token);
|
||||
|
||||
if (!payload) {
|
||||
throw new Error("Token is invalid");
|
||||
// Validate decoded token structure before using it
|
||||
if (
|
||||
!decoded ||
|
||||
typeof decoded !== "object" ||
|
||||
!decoded.id ||
|
||||
typeof decoded.id !== "string" ||
|
||||
decoded.id.trim() === ""
|
||||
) {
|
||||
logger.error("Invalid token: missing or invalid user ID");
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
userId = decoded.id;
|
||||
}
|
||||
|
||||
const { id } = payload;
|
||||
if (!id) {
|
||||
throw new Error("Token missing required field: id");
|
||||
const decryptedId = decryptWithFallback(userId, ENCRYPTION_KEY);
|
||||
|
||||
// Validate decrypted ID before database query
|
||||
if (!decryptedId || typeof decryptedId !== "string" || decryptedId.trim() === "") {
|
||||
logger.error("Invalid token: missing or invalid user ID");
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
// Try to decrypt the ID (for newer tokens), if it fails use the ID as-is (for older tokens)
|
||||
let decryptedId: string;
|
||||
try {
|
||||
decryptedId = symmetricDecrypt(id, env.ENCRYPTION_KEY);
|
||||
} catch {
|
||||
decryptedId = id;
|
||||
}
|
||||
|
||||
// If no email provided, look up the user
|
||||
const foundUser = await prisma.user.findUnique({
|
||||
where: { id: decryptedId },
|
||||
});
|
||||
|
||||
if (!foundUser) {
|
||||
throw new Error("User not found");
|
||||
const errorMessage = "User not found";
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const userEmail = foundUser.email;
|
||||
return { userId: decryptedId, userEmail: foundUser.email };
|
||||
};
|
||||
|
||||
return { id: decryptedId, email: userEmail };
|
||||
export const verifyToken = async (token: string): Promise<JwtPayload> => {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
let payload: JwtPayload & { id: string };
|
||||
let userData: { userId: string; userEmail: string } | null = null;
|
||||
|
||||
// Try new method first, with smart fallback to legacy
|
||||
try {
|
||||
payload = jwt.verify(token, NEXTAUTH_SECRET, { algorithms: ["HS256"] }) as JwtPayload & {
|
||||
id: string;
|
||||
};
|
||||
} catch (newMethodError) {
|
||||
logger.error(newMethodError, "Token verification failed with new method");
|
||||
|
||||
// Get user email for legacy verification
|
||||
userData = await getUserEmailForLegacyVerification(token);
|
||||
|
||||
// Try legacy verification with email-based secret
|
||||
try {
|
||||
payload = jwt.verify(token, NEXTAUTH_SECRET + userData.userEmail, {
|
||||
algorithms: ["HS256"],
|
||||
}) as JwtPayload & {
|
||||
id: string;
|
||||
};
|
||||
} catch (legacyMethodError) {
|
||||
logger.error(legacyMethodError, "Token verification failed with legacy method");
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload?.id) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
// Get user email if we don't have it yet
|
||||
userData ??= await getUserEmailForLegacyVerification(token, payload.id);
|
||||
|
||||
return { id: userData.userId, email: userData.userEmail };
|
||||
};
|
||||
|
||||
export const verifyInviteToken = (token: string): { inviteId: string; email: string } => {
|
||||
if (!NEXTAUTH_SECRET) {
|
||||
throw new Error("NEXTAUTH_SECRET is not set");
|
||||
}
|
||||
|
||||
if (!ENCRYPTION_KEY) {
|
||||
throw new Error("ENCRYPTION_KEY is not set");
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.decode(token);
|
||||
const payload: JwtPayload = decoded as JwtPayload;
|
||||
const payload = jwt.verify(token, NEXTAUTH_SECRET, { algorithms: ["HS256"] }) as JwtPayload & {
|
||||
inviteId: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const { inviteId, email } = payload;
|
||||
const { inviteId: encryptedInviteId, email: encryptedEmail } = payload;
|
||||
|
||||
let decryptedInviteId: string;
|
||||
let decryptedEmail: string;
|
||||
|
||||
try {
|
||||
// Try to decrypt first (for newer tokens)
|
||||
decryptedInviteId = symmetricDecrypt(inviteId, env.ENCRYPTION_KEY);
|
||||
decryptedEmail = symmetricDecrypt(email, env.ENCRYPTION_KEY);
|
||||
} catch {
|
||||
// If decryption fails, use original values (for older tokens)
|
||||
decryptedInviteId = inviteId;
|
||||
decryptedEmail = email;
|
||||
if (!encryptedInviteId || !encryptedEmail) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
// Decrypt both fields with fallback to original values
|
||||
const decryptedInviteId = decryptWithFallback(encryptedInviteId, ENCRYPTION_KEY);
|
||||
const decryptedEmail = decryptWithFallback(encryptedEmail, ENCRYPTION_KEY);
|
||||
|
||||
return {
|
||||
inviteId: decryptedInviteId,
|
||||
email: decryptedEmail,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
addConditionBelow,
|
||||
@@ -109,6 +109,7 @@ describe("surveyLogic", () => {
|
||||
languages: [],
|
||||
triggers: [],
|
||||
segment: null,
|
||||
recaptcha: null,
|
||||
};
|
||||
|
||||
const simpleGroup = (): TConditionGroup => ({
|
||||
@@ -175,7 +176,8 @@ describe("surveyLogic", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
removeCondition(group, "c");
|
||||
const result = removeCondition(group, "c");
|
||||
expect(result).toBe(true);
|
||||
expect(group.conditions).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -433,6 +435,8 @@ describe("surveyLogic", () => {
|
||||
)
|
||||
).toBe(true);
|
||||
expect(evaluateLogic(mockSurvey, { f: "foo" }, vars, group(baseCond("isSet")), "en")).toBe(true);
|
||||
expect(evaluateLogic(mockSurvey, { f: "foo" }, vars, group(baseCond("isNotEmpty")), "en")).toBe(true);
|
||||
expect(evaluateLogic(mockSurvey, { f: "" }, vars, group(baseCond("isNotSet")), "en")).toBe(true);
|
||||
expect(evaluateLogic(mockSurvey, { f: "" }, vars, group(baseCond("isEmpty")), "en")).toBe(true);
|
||||
expect(
|
||||
evaluateLogic(mockSurvey, { f: "foo" }, vars, group({ ...baseCond("isAnyOf", ["foo", "bar"]) }), "en")
|
||||
@@ -510,7 +514,8 @@ describe("surveyLogic", () => {
|
||||
expect(group.conditions.length).toBe(2);
|
||||
toggleGroupConnector(group, "notfound");
|
||||
expect(group.connector).toBe("and");
|
||||
removeCondition(group, "notfound");
|
||||
const result = removeCondition(group, "notfound");
|
||||
expect(result).toBe(false);
|
||||
expect(group.conditions.length).toBe(2);
|
||||
duplicateCondition(group, "notfound");
|
||||
expect(group.conditions.length).toBe(2);
|
||||
@@ -520,6 +525,192 @@ describe("surveyLogic", () => {
|
||||
expect(group.conditions.length).toBe(2);
|
||||
});
|
||||
|
||||
test("removeCondition returns false when condition not found in nested groups", () => {
|
||||
const nestedGroup: TConditionGroup = {
|
||||
id: "nested",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: "nestedC1",
|
||||
leftOperand: { type: "hiddenField", value: "nf1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "nv1" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const group: TConditionGroup = {
|
||||
id: "parent",
|
||||
connector: "and",
|
||||
conditions: [nestedGroup],
|
||||
};
|
||||
|
||||
const result = removeCondition(group, "nonexistent");
|
||||
expect(result).toBe(false);
|
||||
expect(group.conditions).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("removeCondition successfully removes from nested groups and cleans up", () => {
|
||||
const nestedGroup: TConditionGroup = {
|
||||
id: "nested",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: "nestedC1",
|
||||
leftOperand: { type: "hiddenField", value: "nf1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "nv1" },
|
||||
},
|
||||
{
|
||||
id: "nestedC2",
|
||||
leftOperand: { type: "hiddenField", value: "nf2" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "nv2" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const otherCondition: TSingleCondition = {
|
||||
id: "otherCondition",
|
||||
leftOperand: { type: "hiddenField", value: "other" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "value" },
|
||||
};
|
||||
|
||||
const group: TConditionGroup = {
|
||||
id: "parent",
|
||||
connector: "and",
|
||||
conditions: [nestedGroup, otherCondition],
|
||||
};
|
||||
|
||||
const result = removeCondition(group, "nestedC1");
|
||||
expect(result).toBe(true);
|
||||
expect(group.conditions).toHaveLength(2);
|
||||
expect((group.conditions[0] as TConditionGroup).conditions).toHaveLength(1);
|
||||
expect((group.conditions[0] as TConditionGroup).conditions[0].id).toBe("nestedC2");
|
||||
expect(group.conditions[1].id).toBe("otherCondition");
|
||||
});
|
||||
|
||||
test("removeCondition flattens group when nested group has only one condition left", () => {
|
||||
const deeplyNestedGroup: TConditionGroup = {
|
||||
id: "deepNested",
|
||||
connector: "or",
|
||||
conditions: [
|
||||
{
|
||||
id: "deepC1",
|
||||
leftOperand: { type: "hiddenField", value: "df1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "dv1" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const nestedGroup: TConditionGroup = {
|
||||
id: "nested",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: "nestedC1",
|
||||
leftOperand: { type: "hiddenField", value: "nf1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "nv1" },
|
||||
},
|
||||
deeplyNestedGroup,
|
||||
],
|
||||
};
|
||||
|
||||
const otherCondition: TSingleCondition = {
|
||||
id: "otherCondition",
|
||||
leftOperand: { type: "hiddenField", value: "other" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "value" },
|
||||
};
|
||||
|
||||
const group: TConditionGroup = {
|
||||
id: "parent",
|
||||
connector: "and",
|
||||
conditions: [nestedGroup, otherCondition],
|
||||
};
|
||||
|
||||
// Remove the regular condition, leaving only the deeply nested group in the nested group
|
||||
const result = removeCondition(group, "nestedC1");
|
||||
expect(result).toBe(true);
|
||||
|
||||
// The parent group should still have 2 conditions: the nested group and the other condition
|
||||
expect(group.conditions).toHaveLength(2);
|
||||
// The nested group should still be there but now contain only the deeply nested group
|
||||
expect(group.conditions[0].id).toBe("nested");
|
||||
expect((group.conditions[0] as TConditionGroup).conditions).toHaveLength(1);
|
||||
// The nested group should contain the flattened content from the deeply nested group
|
||||
expect((group.conditions[0] as TConditionGroup).conditions[0].id).toBe("deepC1");
|
||||
expect(group.conditions[1].id).toBe("otherCondition");
|
||||
});
|
||||
|
||||
test("removeCondition removes empty groups after cleanup", () => {
|
||||
const emptyNestedGroup: TConditionGroup = {
|
||||
id: "emptyNested",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: "toBeRemoved",
|
||||
leftOperand: { type: "hiddenField", value: "f1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "v1" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const group: TConditionGroup = {
|
||||
id: "parent",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
emptyNestedGroup,
|
||||
{
|
||||
id: "keepThis",
|
||||
leftOperand: { type: "hiddenField", value: "f2" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "v2" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Remove the only condition from the nested group
|
||||
const result = removeCondition(group, "toBeRemoved");
|
||||
expect(result).toBe(true);
|
||||
|
||||
// The empty nested group should be removed, leaving only the other condition
|
||||
expect(group.conditions).toHaveLength(1);
|
||||
expect(group.conditions[0].id).toBe("keepThis");
|
||||
});
|
||||
|
||||
test("deleteEmptyGroups with complex nested structure", () => {
|
||||
const deepEmptyGroup: TConditionGroup = { id: "deepEmpty", connector: "and", conditions: [] };
|
||||
const middleGroup: TConditionGroup = {
|
||||
id: "middle",
|
||||
connector: "or",
|
||||
conditions: [deepEmptyGroup],
|
||||
};
|
||||
const topGroup: TConditionGroup = {
|
||||
id: "top",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
middleGroup,
|
||||
{
|
||||
id: "validCondition",
|
||||
leftOperand: { type: "hiddenField", value: "f" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "v" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
deleteEmptyGroups(topGroup);
|
||||
|
||||
// Should remove the nested empty groups and keep only the valid condition
|
||||
expect(topGroup.conditions).toHaveLength(1);
|
||||
expect(topGroup.conditions[0].id).toBe("validCondition");
|
||||
});
|
||||
|
||||
// Additional tests for complete coverage
|
||||
|
||||
test("addConditionBelow with nested group correctly adds condition", () => {
|
||||
|
||||
@@ -94,21 +94,48 @@ export const toggleGroupConnector = (group: TConditionGroup, resourceId: string)
|
||||
}
|
||||
};
|
||||
|
||||
export const removeCondition = (group: TConditionGroup, resourceId: string) => {
|
||||
for (let i = 0; i < group.conditions.length; i++) {
|
||||
export const removeCondition = (group: TConditionGroup, resourceId: string): boolean => {
|
||||
for (let i = group.conditions.length - 1; i >= 0; i--) {
|
||||
const item = group.conditions[i];
|
||||
|
||||
if (item.id === resourceId) {
|
||||
group.conditions.splice(i, 1);
|
||||
return;
|
||||
cleanupGroup(group);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isConditionGroup(item)) {
|
||||
removeCondition(item, resourceId);
|
||||
if (isConditionGroup(item) && removeCondition(item, resourceId)) {
|
||||
cleanupGroup(group);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
deleteEmptyGroups(group);
|
||||
return false;
|
||||
};
|
||||
|
||||
const cleanupGroup = (group: TConditionGroup) => {
|
||||
// Remove empty condition groups first
|
||||
for (let i = group.conditions.length - 1; i >= 0; i--) {
|
||||
const condition = group.conditions[i];
|
||||
if (isConditionGroup(condition)) {
|
||||
cleanupGroup(condition);
|
||||
|
||||
// Remove if empty after cleanup
|
||||
if (condition.conditions.length === 0) {
|
||||
group.conditions.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten if group has only one condition and it's a condition group
|
||||
if (group.conditions.length === 1 && isConditionGroup(group.conditions[0])) {
|
||||
group.connector = group.conditions[0].connector || "and";
|
||||
group.conditions = group.conditions[0].conditions;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEmptyGroups = (group: TConditionGroup) => {
|
||||
cleanupGroup(group);
|
||||
};
|
||||
|
||||
export const duplicateCondition = (group: TConditionGroup, resourceId: string) => {
|
||||
@@ -130,18 +157,6 @@ export const duplicateCondition = (group: TConditionGroup, resourceId: string) =
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEmptyGroups = (group: TConditionGroup) => {
|
||||
for (let i = 0; i < group.conditions.length; i++) {
|
||||
const resource = group.conditions[i];
|
||||
|
||||
if (isConditionGroup(resource) && resource.conditions.length === 0) {
|
||||
group.conditions.splice(i, 1);
|
||||
} else if (isConditionGroup(resource)) {
|
||||
deleteEmptyGroups(resource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createGroupFromResource = (group: TConditionGroup, resourceId: string) => {
|
||||
for (let i = 0; i < group.conditions.length; i++) {
|
||||
const item = group.conditions[i];
|
||||
@@ -670,8 +685,9 @@ const performCalculation = (
|
||||
if (typeof val === "number" || typeof val === "string") {
|
||||
if (variable.type === "number" && !isNaN(Number(val))) {
|
||||
operandValue = Number(val);
|
||||
} else {
|
||||
operandValue = val;
|
||||
}
|
||||
operandValue = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Kontoeinstellungen",
|
||||
"action": "Aktion",
|
||||
"actions": "Aktionen",
|
||||
"actions_description": "Code- und No-Code-Aktionen werden verwendet, um Abfangumfragen innerhalb von Apps und auf Websites auszulösen.",
|
||||
"active_surveys": "Aktive Umfragen",
|
||||
"activity": "Aktivität",
|
||||
"add": "Hinzufügen",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "Filter hinzufügen",
|
||||
"add_logo": "Logo hinzufügen",
|
||||
"add_member": "Mitglied hinzufügen",
|
||||
"add_new_project": "Neues Projekt hinzufügen",
|
||||
"add_project": "Projekt hinzufügen",
|
||||
"add_to_team": "Zum Team hinzufügen",
|
||||
"all": "Alle",
|
||||
@@ -141,7 +143,6 @@
|
||||
"apply_filters": "Filter anwenden",
|
||||
"are_you_sure": "Bist Du sicher?",
|
||||
"attributes": "Attribute",
|
||||
"avatar": "Avatar",
|
||||
"back": "Zurück",
|
||||
"billing": "Abrechnung",
|
||||
"booked": "Gebucht",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "Abbrechen",
|
||||
"centered_modal": "Zentriertes Modalfenster",
|
||||
"choices": "Entscheidungen",
|
||||
"choose_environment": "Umgebung auswählen",
|
||||
"choose_organization": "Organisation auswählen",
|
||||
"choose_project": "Projekt wählen",
|
||||
"clear_all": "Alles löschen",
|
||||
"clear_filters": "Filter löschen",
|
||||
"clear_selection": "Auswahl aufheben",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "Formbricks verbinden",
|
||||
"connected": "Verbunden",
|
||||
"contacts": "Kontakte",
|
||||
"continue": "Weitermachen",
|
||||
"copied": "Kopiert",
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert",
|
||||
"copy": "Kopieren",
|
||||
"copy_code": "Code kopieren",
|
||||
"copy_link": "Link kopieren",
|
||||
"count_contacts": "{value, plural, other {'{'value, plural,\none '{{#}' Kontakt'}'\nother '{{#}' Kontakte'}'\n'}'}}",
|
||||
"count_responses": "{value, plural, other {{count} Antworten}}",
|
||||
"create_new_organization": "Neue Organisation erstellen",
|
||||
"create_project": "Projekt erstellen",
|
||||
"create_segment": "Segment erstellen",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "Erstellt am",
|
||||
"created_by": "Erstellt von",
|
||||
"customer_success": "Kundenerfolg",
|
||||
"danger_zone": "Gefahrenzone",
|
||||
"dark_overlay": "Dunkle Überlagerung",
|
||||
"date": "Datum",
|
||||
"default": "Standard",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "E-Commerce",
|
||||
"edit": "Bearbeiten",
|
||||
"email": "E-Mail",
|
||||
"ending_card": "Abschluss-Karte",
|
||||
"enterprise_license": "Enterprise Lizenz",
|
||||
"environment_not_found": "Umgebung nicht gefunden",
|
||||
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
|
||||
"error": "Fehler",
|
||||
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
|
||||
"error_component_title": "Fehler beim Laden der Ressourcen",
|
||||
"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",
|
||||
"finish": "Fertigstellen",
|
||||
"follow_these": "Folge diesen",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "Bezeichnung",
|
||||
"language": "Sprache",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"license": "Lizenz",
|
||||
"light_overlay": "Helle Überlagerung",
|
||||
"limits_reached": "Limits erreicht",
|
||||
"link": "Link",
|
||||
"link_and_email": "Link & E-Mail",
|
||||
"link_survey": "Link-Umfrage",
|
||||
"link_surveys": "Umfragen verknüpfen",
|
||||
"load_more": "Mehr laden",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
||||
"metadata": "Metadaten",
|
||||
"minimum": "Minimum",
|
||||
"mobile_overlay_text": "Formbricks ist für Geräte mit kleineren Auflösungen nicht verfügbar.",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funktioniert am besten auf einem größeren Bildschirm. Um Umfragen zu verwalten oder zu erstellen, wechsle zu einem anderen Gerät.",
|
||||
"mobile_overlay_surveys_look_good": "Keine Sorge – deine Umfragen sehen auf jedem Gerät und jeder Bildschirmgröße großartig aus!",
|
||||
"mobile_overlay_title": "Oops, Bildschirm zu klein erkannt!",
|
||||
"move_down": "Nach unten bewegen",
|
||||
"move_up": "Nach oben bewegen",
|
||||
"multiple_languages": "Mehrsprachigkeit",
|
||||
"name": "Name",
|
||||
"new": "Neu",
|
||||
"new_survey": "Neue Umfrage",
|
||||
"new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!",
|
||||
"next": "Weiter",
|
||||
"no_background_image_found": "Kein Hintergrundbild gefunden.",
|
||||
"no_code": "No Code",
|
||||
"no_files_uploaded": "Keine Dateien hochgeladen",
|
||||
"no_quotas_found": "Keine Kontingente gefunden",
|
||||
"no_result_found": "Kein Ergebnis gefunden",
|
||||
"no_results": "Keine Ergebnisse",
|
||||
"no_surveys_found": "Keine Umfragen gefunden.",
|
||||
@@ -284,6 +295,7 @@
|
||||
"organization": "Organisation",
|
||||
"organization_id": "Organisations-ID",
|
||||
"organization_not_found": "Organisation nicht gefunden",
|
||||
"organization_settings": "Organisationseinstellungen",
|
||||
"organization_teams_not_found": "Organisations-Teams nicht gefunden",
|
||||
"other": "Andere",
|
||||
"others": "Andere",
|
||||
@@ -307,7 +319,8 @@
|
||||
"product_manager": "Produktmanager",
|
||||
"profile": "Profil",
|
||||
"profile_id": "Profil-ID",
|
||||
"project_configuration": "Projektkonfiguration",
|
||||
"progress": "Fortschritt",
|
||||
"project_configuration": "Projekteinstellungen",
|
||||
"project_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
|
||||
"project_id": "Projekt-ID",
|
||||
"project_name": "Projektname",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "Frage",
|
||||
"question_id": "Frage-ID",
|
||||
"questions": "Fragen",
|
||||
"quota": "Kontingent",
|
||||
"quotas": "Quoten",
|
||||
"quotas_description": "Begrenze die Anzahl der Antworten, die du von Teilnehmern erhältst, die bestimmte Kriterien erfüllen.",
|
||||
"read_docs": "Dokumentation lesen",
|
||||
"recipients": "Empfänger",
|
||||
"remove": "Entfernen",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "Speichern",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"saving": "Speichern",
|
||||
"scheduled": "Geplant",
|
||||
"search": "Suchen",
|
||||
"security": "Sicherheit",
|
||||
"segment": "Segment",
|
||||
@@ -366,6 +381,7 @@
|
||||
"start_free_trial": "Kostenlos starten",
|
||||
"status": "Status",
|
||||
"step_by_step_manual": "Schritt-für-Schritt-Anleitung",
|
||||
"storage_not_configured": "Dateispeicher nicht eingerichtet, Uploads werden wahrscheinlich fehlschlagen",
|
||||
"styling": "Styling",
|
||||
"submit": "Abschicken",
|
||||
"summary": "Zusammenfassung",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "Umfrage live",
|
||||
"survey_not_found": "Umfrage nicht gefunden",
|
||||
"survey_paused": "Umfrage pausiert.",
|
||||
"survey_scheduled": "Umfrage geplant.",
|
||||
"survey_type": "Umfragetyp",
|
||||
"surveys": "Umfragen",
|
||||
"switch_organization": "Organisation wechseln",
|
||||
"switch_to": "Wechseln zu {environment}",
|
||||
"table_items_deleted_successfully": "{type}s erfolgreich gelöscht",
|
||||
"table_settings": "Tabelleinstellungen",
|
||||
@@ -577,8 +591,7 @@
|
||||
"contacts_table_refresh": "Kontakte aktualisieren",
|
||||
"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.",
|
||||
"first_name": "Vorname",
|
||||
"last_name": "Nachname",
|
||||
"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.}}",
|
||||
"no_responses_found": "Keine Antworten gefunden",
|
||||
"not_provided": "Nicht angegeben",
|
||||
"search_contact": "Kontakt suchen",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "Zugriffskontrolle",
|
||||
"add_api_key": "API-Schlüssel hinzufügen",
|
||||
"api_key": "API-Schlüssel",
|
||||
"api_key_copied_to_clipboard": "API-Schlüssel in die Zwischenablage kopiert",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "Dies ist die URL deines Formbricks Backends.",
|
||||
"app_connection": "App-Verbindung",
|
||||
"app_connection_description": "Verbinde deine App mit Formbricks.",
|
||||
"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",
|
||||
"check_out_the_docs": "Schau dir die Docs an.",
|
||||
"dive_into_the_docs": "Tauch in die Docs ein.",
|
||||
"does_your_widget_work": "Funktioniert dein Widget?",
|
||||
"environment_id": "Deine Umgebungs-ID",
|
||||
"environment_id_description": "Diese ID identifiziert eindeutig diese Formbricks Umgebung.",
|
||||
"environment_id_description_with_environment_id": "Wird verwendet, um die richtige Umgebung zu identifizieren: {environmentId} ist deine.",
|
||||
"formbricks_sdk": "Formbricks SDK",
|
||||
"formbricks_sdk_connected": "Formbricks SDK ist verbunden",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK ist noch nicht verbunden.",
|
||||
"formbricks_sdk_not_connected_description": "Verbinde deine Website oder App mit Formbricks",
|
||||
"have_a_problem": "Hast Du ein Problem?",
|
||||
"how_to_setup": "Wie einrichten",
|
||||
"how_to_setup_description": "Befolge diese Schritte, um das Formbricks Widget in deiner App einzurichten.",
|
||||
"identifying_your_users": "deine Nutzer identifizieren",
|
||||
"if_you_are_planning_to": "Wenn Du planst zu",
|
||||
"insert_this_code_into_the": "Füge diesen Code in die",
|
||||
"need_a_more_detailed_setup_guide_for": "Brauche eine detailliertere Anleitung für",
|
||||
"not_working": "Klappt nicht?",
|
||||
"open_an_issue_on_github": "Eine Issue auf GitHub öffnen",
|
||||
"open_the_browser_console_to_see_the_logs": "Öffne die Browser Konsole, um die Logs zu sehen.",
|
||||
"receiving_data": "Daten werden empfangen \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Erneut prüfen",
|
||||
"scroll_to_the_top": "Scroll nach oben!",
|
||||
"step_1": "Schritt 1: Installiere mit pnpm, npm oder yarn",
|
||||
"step_2": "Schritt 2: Widget initialisieren",
|
||||
"step_2_description": "Importiere Formbricks und initialisiere das Widget in deiner Komponente (z.B. App.tsx):",
|
||||
"step_3": "Schritt 3: Debug-Modus",
|
||||
"switch_on_the_debug_mode_by_appending": "Schalte den Debug-Modus ein, indem Du anhängst",
|
||||
"tag_of_your_app": "Tag deiner App",
|
||||
"to_the_url_where_you_load_the": "URL, wo Du die lädst",
|
||||
"want_to_learn_how_to_add_user_attributes": "Willst Du lernen, wie man Attribute hinzufügt?",
|
||||
"you_are_done": "Du bist fertig \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "du kannst die Benutzer-ID festlegen mit",
|
||||
"your_app_now_communicates_with_formbricks": "Deine App kommuniziert jetzt mit Formbricks - sie sendet Ereignisse und lädt Umfragen automatisch!"
|
||||
"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"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Dies ist dein einziges Projekt, es kann nicht gelöscht werden. Erstelle zuerst ein neues Projekt.",
|
||||
@@ -989,7 +977,6 @@
|
||||
"free": "Kostenlos",
|
||||
"free_description": "Unbegrenzte Umfragen, Teammitglieder und mehr.",
|
||||
"get_2_months_free": "2 Monate gratis",
|
||||
"get_in_touch": "Kontaktiere uns",
|
||||
"hosted_in_frankfurt": "Gehostet in Frankfurt",
|
||||
"ios_android_sdks": "iOS & Android SDK für mobile Umfragen",
|
||||
"link_surveys": "Umfragen verlinken (teilbar)",
|
||||
@@ -1111,9 +1098,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Was passiert, wenn Du das Konto löschst",
|
||||
"avatar_update_failed": "Aktualisierung des Avatars fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"backup_code": "Backup-Code",
|
||||
"change_image": "Bild ändern",
|
||||
"confirm_delete_account": "Lösche dein Konto mit all deinen persönlichen Informationen und Daten",
|
||||
"confirm_delete_my_account": "Konto löschen",
|
||||
"confirm_your_current_password_to_get_started": "Bestätige dein aktuelles Passwort, um loszulegen.",
|
||||
@@ -1124,17 +1109,13 @@
|
||||
"email_change_initiated": "Deine Anfrage zur Änderung der E-Mail wurde eingeleitet.",
|
||||
"enable_two_factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Gib den Code aus deiner Authentifizierungs-App unten ein.",
|
||||
"file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.",
|
||||
"invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.",
|
||||
"lost_access": "Zugriff verloren",
|
||||
"or_enter_the_following_code_manually": "Oder gib den folgenden Code manuell ein:",
|
||||
"organization_identification": "Hilf deiner Organisation, Dich auf Formbricks zu identifizieren",
|
||||
"organizations_delete_message": "Du bist der einzige Besitzer dieser Organisationen, also werden sie <b>auch gelöscht.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Dauerhafte Entfernung all deiner persönlichen Informationen und Daten",
|
||||
"personal_information": "Persönliche Informationen",
|
||||
"please_enter_email_to_confirm_account_deletion": "Bitte gib {email} in das folgende Feld ein, um die endgültige Löschung deines Kontos zu bestätigen:",
|
||||
"profile_updated_successfully": "Dein Profil wurde erfolgreich aktualisiert",
|
||||
"remove_image": "Bild entfernen",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Speichere die folgenden Backup-Codes an einem sicheren Ort.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scanne den QR-Code unten mit deiner Authentifizierungs-App.",
|
||||
"security_description": "Verwalte dein Passwort und andere Sicherheitseinstellungen wie Zwei-Faktor-Authentifizierung (2FA).",
|
||||
@@ -1144,10 +1125,8 @@
|
||||
"two_factor_code": "Zwei-Faktor-Code",
|
||||
"unlock_two_factor_authentication": "Zwei-Faktor-Authentifizierung mit einem höheren Plan freischalten",
|
||||
"update_personal_info": "Persönliche Daten aktualisieren",
|
||||
"upload_image": "Bild hochladen",
|
||||
"warning_cannot_delete_account": "Du bist der einzige Besitzer dieser Organisation. Bitte übertrage das Eigentum zuerst an ein anderes Mitglied.",
|
||||
"warning_cannot_undo": "Das kann nicht rückgängig gemacht werden",
|
||||
"you_must_select_a_file": "Du musst eine Datei auswählen."
|
||||
"warning_cannot_undo": "Das kann nicht rückgängig gemacht werden"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Füge Mitglieder zum Team hinzu und bestimme ihre Rolle.",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "Umfrage automatisch schließen nach",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Schließe die Umfrage automatisch nach einer bestimmten Anzahl von Antworten.",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Schließe die Umfrage automatisch, wenn der Benutzer nach einer bestimmten Anzahl von Sekunden nicht antwortet.",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Schließt die Umfrage automatisch zu Beginn des Tages (UTC).",
|
||||
"automatically_mark_the_survey_as_complete_after": "Umfrage automatisch als abgeschlossen markieren nach",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Umfrage automatisch zu Beginn des Tages (UTC) freigeben.",
|
||||
"back_button_label": "Zurück\"- Button ",
|
||||
"background_styling": "Hintergründe",
|
||||
"brand_color": "Markenfarbe",
|
||||
@@ -1309,14 +1286,13 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
|
||||
"choose_where_to_run_the_survey": "Wähle, wo die Umfrage durchgeführt werden soll.",
|
||||
"city": "Stadt",
|
||||
"close_survey_on_date": "Umfrage am Datum schließen",
|
||||
"close_survey_on_response_limit": "Umfrage bei Erreichen des Antwortlimits schließen",
|
||||
"color": "Farbe",
|
||||
"column_used_in_logic_error": "Diese Spalte wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||
"columns": "Spalten",
|
||||
"company": "Firma",
|
||||
"company_logo": "Firmenlogo",
|
||||
"completed_responses": "unvollständige oder vollständige Antworten.",
|
||||
"completed_responses": "Abgeschlossene Antworten.",
|
||||
"concat": "Verketten +",
|
||||
"conditional_logic": "Bedingte Logik",
|
||||
"confirm_default_language": "Standardsprache bestätigen",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "Abschluss-Karte",
|
||||
"ending_card": "Abschluss-Karte",
|
||||
"ending_card_used_in_logic": "Diese Abschlusskarte wird in der Logik der Frage {questionIndex} verwendet.",
|
||||
"ending_used_in_quota": "Dieses Ende wird in der \"{quotaName}\" Quote verwendet",
|
||||
"ends_with": "endet mit",
|
||||
"equals": "Gleich",
|
||||
"equals_one_of": "Entspricht einem von",
|
||||
@@ -1366,6 +1343,7 @@
|
||||
"fallback_for": "Ersatz für",
|
||||
"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",
|
||||
"field_name_eg_score_price": "Feldname z.B. Punktzahl, Preis",
|
||||
"first_name": "Vorname",
|
||||
"five_points_recommended": "5 Punkte (empfohlen)",
|
||||
@@ -1397,8 +1375,9 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "Betreff der E-Mail",
|
||||
"follow_ups_modal_action_to_description": "Empfänger-E-Mail-Adresse",
|
||||
"follow_ups_modal_action_to_label": "An",
|
||||
"follow_ups_modal_action_to_warning": "Kein E-Mail-Feld in der Umfrage gefunden.",
|
||||
"follow_ups_modal_action_to_warning": "Keine gültigen Optionen für den Versand von E-Mails gefunden, bitte fügen Sie einige Freitext- / Kontaktinformationen-Fragen oder versteckte Felder hinzu",
|
||||
"follow_ups_modal_create_heading": "Neues Follow-up erstellen",
|
||||
"follow_ups_modal_created_successfull_toast": "Nachverfolgung erstellt und wird gespeichert, sobald du die Umfrage speicherst.",
|
||||
"follow_ups_modal_edit_heading": "Follow-up bearbeiten",
|
||||
"follow_ups_modal_edit_no_id": "Keine Survey Follow-up-ID angegeben, das Survey-Follow-up kann nicht aktualisiert werden",
|
||||
"follow_ups_modal_name_label": "Name des Follow-ups",
|
||||
@@ -1408,8 +1387,9 @@
|
||||
"follow_ups_modal_trigger_label": "Auslöser",
|
||||
"follow_ups_modal_trigger_type_ending": "Teilnehmer sieht einen bestimmten Abschluss",
|
||||
"follow_ups_modal_trigger_type_ending_select": "Abschlüsse auswählen: ",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Keine Abschlüsse in der Umfrage gefunden!",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Bitte wähle mindestens ein Ende aus oder ändere den Auslöser-Typ",
|
||||
"follow_ups_modal_trigger_type_response": "Teilnehmer schließt Umfrage ab",
|
||||
"follow_ups_modal_updated_successfull_toast": "Nachverfolgung aktualisiert und wird gespeichert, sobald du die Umfrage speicherst.",
|
||||
"follow_ups_new": "Neues Follow-up",
|
||||
"follow_ups_upgrade_button_text": "Upgrade, um Follow-ups zu aktivieren",
|
||||
"form_styling": "Umfrage Styling",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "Frage dupliziert.",
|
||||
"question_id_updated": "Frage-ID aktualisiert",
|
||||
"question_used_in_logic": "Diese Frage wird in der Logik der Frage {questionIndex} verwendet.",
|
||||
"question_used_in_quota": "Diese Frage wird in der \"{quotaName}\" Quote verwendet",
|
||||
"quotas": {
|
||||
"add_quota": "Quote hinzufügen",
|
||||
"change_quota_for_public_survey": "Quote für öffentliche Umfrage ändern?",
|
||||
"confirm_quota_changes": "Änderungen der Quoten bestätigen",
|
||||
"confirm_quota_changes_body": "Du hast ungespeicherte Änderungen in deinem Kontingent. Möchtest Du sie speichern, bevor Du gehst?",
|
||||
"continue_survey_normally": "Umfrage normal fortsetzen",
|
||||
"count_partial_submissions": "Teilweise Abgaben zählen",
|
||||
"count_partial_submissions_description": "Einschließlich Teilnehmer, die die Quotenanforderungen erfüllen, aber die Umfrage nicht abgeschlossen haben",
|
||||
"create_quota_for_public_survey": "Quote für öffentliche Umfrage erstellen?",
|
||||
"create_quota_for_public_survey_description": "Nur zukünftige Antworten werden für das Kontingent berücksichtigt",
|
||||
"create_quota_for_public_survey_text": "Diese Umfrage ist bereits öffentlich. Bestehende Antworten werden für die neue Quote nicht berücksichtigt.",
|
||||
"delete_quota_confirmation_text": "Dies wird die Quote {quotaName} dauerhaft löschen.",
|
||||
"duplicate_quota": "Duplizieren der Quote",
|
||||
"edit_quota": "Bearbeite Quote",
|
||||
"end_survey_for_matching_participants": "Umfrage für passende Teilnehmer beenden",
|
||||
"inclusion_criteria": "Einschlusskriterien",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, other {Limit muss größer oder gleich der Anzahl der Antworten sein}}",
|
||||
"limited_to_x_responses": "Begrenzt auf {limit}",
|
||||
"new_quota": "Neues Kontingent",
|
||||
"quota_created_successfull_toast": "Kontingent erfolgreich erstellt",
|
||||
"quota_deleted_successfull_toast": "Kontingent erfolgreich gelöscht",
|
||||
"quota_duplicated_successfull_toast": "Kontingent erfolgreich dupliziert",
|
||||
"quota_name_placeholder": "z.B., Teilnehmende im Alter von 18-25",
|
||||
"quota_updated_successfull_toast": "Kontingent erfolgreich aktualisiert",
|
||||
"response_limit": "Grenzen",
|
||||
"save_changes_confirmation_body": "Jegliche Änderungen an den Einschlusskriterien betreffen nur zukünftige Antworten.\nWir empfehlen, entweder ein bestehendes Kontingent zu duplizieren oder ein neues zu erstellen.",
|
||||
"save_changes_confirmation_text": "Vorhandene Antworten bleiben im Kontingent",
|
||||
"select_ending_card": "Abschlusskarte auswählen",
|
||||
"upgrade_prompt_title": "Verwende Quoten mit einem höheren Plan",
|
||||
"when_quota_has_been_reached": "Wenn das Kontingent erreicht ist"
|
||||
},
|
||||
"randomize_all": "Alle Optionen zufällig anordnen",
|
||||
"randomize_all_except_last": "Alle Optionen zufällig anordnen außer der letzten",
|
||||
"range": "Reichweite",
|
||||
@@ -1517,7 +1529,6 @@
|
||||
"redirect_thank_you_card": "Weiterleitung anlegen",
|
||||
"redirect_to_url": "Zu URL weiterleiten",
|
||||
"redirect_to_url_not_available_on_free_plan": "Umleitung zu URL ist im kostenlosen Plan nicht verfügbar",
|
||||
"release_survey_on_date": "Umfrage an Datum veröffentlichen",
|
||||
"remove_description": "Beschreibung entfernen",
|
||||
"remove_translations": "Übersetzungen entfernen",
|
||||
"require_answer": "Antwort erforderlich",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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.",
|
||||
"variable_name_must_start_with_a_letter": "Variablenname muss mit einem Buchstaben beginnen.",
|
||||
"verify_email_before_submission": "E-Mail vor dem Absenden überprüfen",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "Adresszeile 2",
|
||||
"an_error_occurred_deleting_the_tag": "Beim Löschen des Tags ist ein Fehler aufgetreten",
|
||||
"browser": "Browser",
|
||||
"bulk_delete_response_quotas": "Die Antworten sind Teil der Quoten für diese Umfrage. Wie möchten Sie die Quoten verwalten?",
|
||||
"city": "Stadt",
|
||||
"company": "Firma",
|
||||
"completed": "Erledigt ✅",
|
||||
"country": "Land",
|
||||
"decrement_quotas": "Alle Grenzwerte der Kontingente einschließlich dieser Antwort verringern",
|
||||
"delete_response_confirmation": "Dies wird die Umfrageantwort einschließlich aller Antworten, Tags, angehängter Dokumente und Antwort-Metadaten löschen.",
|
||||
"delete_response_quotas": "Die Antwort ist Teil der Quoten für diese Umfrage. Wie möchten Sie die Quoten verwalten?",
|
||||
"device": "Gerät",
|
||||
"device_info": "Geräteinfo",
|
||||
"email": "E-Mail",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "Die Meta-Daten werden basierend auf dem `lang` Wert in der URL geladen.",
|
||||
"link_description": "Linkbeschreibung",
|
||||
"link_description_description": "Beschreibung mit 55-200 Zeichen funktionieren am besten.",
|
||||
"link_description_placeholder": "Hilf uns, indem du deine Gedanken teilst.",
|
||||
"link_title": "Linktitel",
|
||||
"link_title_description": "Kurze Titel funktionieren am besten als Meta-Titel.",
|
||||
"link_title_placeholder": "Kundenfeedback-Umfrage",
|
||||
"preview_image": "Vorschaubild",
|
||||
"preview_image_description": "Querformatige Bilder mit kleiner Dateigröße (<4MB) funktionieren am besten.",
|
||||
"title": "Link-Einstellungen"
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "Benachrichtigungen konfigurieren",
|
||||
"congrats": "Glückwunsch! Deine Umfrage ist jetzt live.",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Verbinde deine Website oder App mit Formbricks, um loszulegen.",
|
||||
"current_count": "Aktuelle Anzahl",
|
||||
"custom_range": "Benutzerdefinierter Bereich...",
|
||||
"delete_all_existing_responses_and_displays": "Alle bestehenden Antworten und Anzeigen löschen",
|
||||
"download_qr_code": "QR Code herunterladen",
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "Letztes Monat",
|
||||
"last_quarter": "Letztes Quartal",
|
||||
"last_year": "Letztes Jahr",
|
||||
"limit": "Limit",
|
||||
"no_responses_found": "Keine Antworten gefunden",
|
||||
"other_values_found": "Andere Werte gefunden",
|
||||
"overall": "Insgesamt",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "QR-Code-Download fehlgeschlagen",
|
||||
"qr_code_download_with_start_soon": "QR Code-Download startet bald",
|
||||
"qr_code_generation_failed": "Es gab ein Problem beim Laden des QR-Codes für die Umfrage. Bitte versuchen Sie es erneut.",
|
||||
"quotas_completed": "Kontingente abgeschlossen",
|
||||
"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.",
|
||||
"selected_responses_csv": "Ausgewählte Antworten (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "Dieses Quartal",
|
||||
"this_year": "Dieses Jahr",
|
||||
"time_to_complete": "Zeit zur Fertigstellung",
|
||||
"ttc_tooltip": "Durchschnittliche Zeit bis zum Abschluss der Umfrage.",
|
||||
"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 \uD83E\uDDD8♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "Umfrage erfolgreich gelöscht",
|
||||
"survey_duplicated_successfully": "Umfrage erfolgreich dupliziert",
|
||||
"survey_duplication_error": "Duplizieren der Umfrage fehlgeschlagen",
|
||||
"survey_status_tooltip": "Um den Umfragestatus zu aktualisieren, aktualisiere den Zeitplan in den Umfrageoptionen.",
|
||||
"templates": {
|
||||
"all_channels": "Alle Kanäle",
|
||||
"all_industries": "Alle Branchen",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Account settings",
|
||||
"action": "Action",
|
||||
"actions": "Actions",
|
||||
"actions_description": "Code and No-Code Actions are used to trigger intercept surveys within apps & on websites.",
|
||||
"active_surveys": "Active surveys",
|
||||
"activity": "Activity",
|
||||
"add": "Add",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "Add filter",
|
||||
"add_logo": "Add logo",
|
||||
"add_member": "Add member",
|
||||
"add_new_project": "Add new project",
|
||||
"add_project": "Add project",
|
||||
"add_to_team": "Add to team",
|
||||
"all": "All",
|
||||
@@ -141,7 +143,6 @@
|
||||
"apply_filters": "Apply filters",
|
||||
"are_you_sure": "Are you sure?",
|
||||
"attributes": "Attributes",
|
||||
"avatar": "Avatar",
|
||||
"back": "Back",
|
||||
"billing": "Billing",
|
||||
"booked": "Booked",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "Cancel",
|
||||
"centered_modal": "Centered Modal",
|
||||
"choices": "Choices",
|
||||
"choose_environment": "Choose environment",
|
||||
"choose_organization": "Choose organization",
|
||||
"choose_project": "Choose project",
|
||||
"clear_all": "Clear all",
|
||||
"clear_filters": "Clear filters",
|
||||
"clear_selection": "Clear selection",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "Connect Formbricks",
|
||||
"connected": "Connected",
|
||||
"contacts": "Contacts",
|
||||
"continue": "Continue",
|
||||
"copied": "Copied",
|
||||
"copied_to_clipboard": "Copied to clipboard",
|
||||
"copy": "Copy",
|
||||
"copy_code": "Copy code",
|
||||
"copy_link": "Copy Link",
|
||||
"count_contacts": "{value, plural, one {{value} contact} other {{value} contacts}}",
|
||||
"count_responses": "{value, plural, one {{value} response} other {{value} responses}}",
|
||||
"create_new_organization": "Create new organization",
|
||||
"create_project": "Create project",
|
||||
"create_segment": "Create segment",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "Created at",
|
||||
"created_by": "Created by",
|
||||
"customer_success": "Customer Success",
|
||||
"danger_zone": "Danger Zone",
|
||||
"dark_overlay": "Dark overlay",
|
||||
"date": "Date",
|
||||
"default": "Default",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "E-Commerce",
|
||||
"edit": "Edit",
|
||||
"email": "Email",
|
||||
"ending_card": "Ending card",
|
||||
"enterprise_license": "Enterprise License",
|
||||
"environment_not_found": "Environment not found",
|
||||
"environment_notice": "You're currently in the {environment} environment.",
|
||||
"error": "Error",
|
||||
"error_component_description": "This resource doesn't exist or you don't have the necessary rights to access it.",
|
||||
"error_component_title": "Error loading resources",
|
||||
"error_rate_limit_description": "Maximum number of requests reached. Please try again later.",
|
||||
"error_rate_limit_title": "Rate Limit Exceeded",
|
||||
"expand_rows": "Expand rows",
|
||||
"finish": "Finish",
|
||||
"follow_these": "Follow these",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "Label",
|
||||
"language": "Language",
|
||||
"learn_more": "Learn more",
|
||||
"license": "License",
|
||||
"light_overlay": "Light overlay",
|
||||
"limits_reached": "Limits Reached",
|
||||
"link": "Link",
|
||||
"link_and_email": "Link & Email",
|
||||
"link_survey": "Link Survey",
|
||||
"link_surveys": "Link Surveys",
|
||||
"load_more": "Load more",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "Membership not found",
|
||||
"metadata": "Metadata",
|
||||
"minimum": "Minimum",
|
||||
"mobile_overlay_text": "Formbricks is not available for devices with smaller resolutions.",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks works best on a bigger screen. To manage or build surveys, switch to another device.",
|
||||
"mobile_overlay_surveys_look_good": "Don't worry – your surveys look great on every device and screen size!",
|
||||
"mobile_overlay_title": "Oops, tiny screen detected!",
|
||||
"move_down": "Move down",
|
||||
"move_up": "Move up",
|
||||
"multiple_languages": "Multiple languages",
|
||||
"name": "Name",
|
||||
"new": "New",
|
||||
"new_survey": "New Survey",
|
||||
"new_version_available": "Formbricks {version} is here. Upgrade now!",
|
||||
"next": "Next",
|
||||
"no_background_image_found": "No background image found.",
|
||||
"no_code": "No code",
|
||||
"no_files_uploaded": "No files were uploaded",
|
||||
"no_quotas_found": "No quotas found",
|
||||
"no_result_found": "No result found",
|
||||
"no_results": "No results",
|
||||
"no_surveys_found": "No surveys found.",
|
||||
@@ -284,6 +295,7 @@
|
||||
"organization": "Organization",
|
||||
"organization_id": "Organization ID",
|
||||
"organization_not_found": "Organization not found",
|
||||
"organization_settings": "Organization settings",
|
||||
"organization_teams_not_found": "Organization teams not found",
|
||||
"other": "Other",
|
||||
"others": "Others",
|
||||
@@ -307,7 +319,8 @@
|
||||
"product_manager": "Product Manager",
|
||||
"profile": "Profile",
|
||||
"profile_id": "Profile ID",
|
||||
"project_configuration": "Project's Configuration",
|
||||
"progress": "Progress",
|
||||
"project_configuration": "Project Configuration",
|
||||
"project_creation_description": "Organize surveys in projects for better access control.",
|
||||
"project_id": "Project ID",
|
||||
"project_name": "Project Name",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "Question",
|
||||
"question_id": "Question ID",
|
||||
"questions": "Questions",
|
||||
"quota": "Quota",
|
||||
"quotas": "Quotas",
|
||||
"quotas_description": "Limit the amount of responses you receive from participants who meet certain criteria.",
|
||||
"read_docs": "Read Docs",
|
||||
"recipients": "Recipients",
|
||||
"remove": "Remove",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "Save",
|
||||
"save_changes": "Save changes",
|
||||
"saving": "Saving",
|
||||
"scheduled": "Scheduled",
|
||||
"search": "Search",
|
||||
"security": "Security",
|
||||
"segment": "Segment",
|
||||
@@ -366,6 +381,7 @@
|
||||
"start_free_trial": "Start Free Trial",
|
||||
"status": "Status",
|
||||
"step_by_step_manual": "Step by step manual",
|
||||
"storage_not_configured": "File storage not set up, uploads will likely fail",
|
||||
"styling": "Styling",
|
||||
"submit": "Submit",
|
||||
"summary": "Summary",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "Survey live",
|
||||
"survey_not_found": "Survey not found",
|
||||
"survey_paused": "Survey paused.",
|
||||
"survey_scheduled": "Survey scheduled.",
|
||||
"survey_type": "Survey Type",
|
||||
"surveys": "Surveys",
|
||||
"switch_organization": "Switch organization",
|
||||
"switch_to": "Switch to {environment}",
|
||||
"table_items_deleted_successfully": "{type}s deleted successfully",
|
||||
"table_settings": "Table settings",
|
||||
@@ -577,8 +591,7 @@
|
||||
"contacts_table_refresh": "Refresh contacts",
|
||||
"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.",
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"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.}}",
|
||||
"no_responses_found": "No responses found",
|
||||
"not_provided": "Not provided",
|
||||
"search_contact": "Search contact",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "Access Control",
|
||||
"add_api_key": "Add API Key",
|
||||
"api_key": "API Key",
|
||||
"api_key_copied_to_clipboard": "API key copied to clipboard",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "Unable to delete API Key"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "This is the URL of your Formbricks backend.",
|
||||
"app_connection": "App Connection",
|
||||
"app_connection_description": "Connect your app to Formbricks.",
|
||||
"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",
|
||||
"check_out_the_docs": "Check out the docs.",
|
||||
"dive_into_the_docs": "Dive into the docs.",
|
||||
"does_your_widget_work": "Does your widget work?",
|
||||
"environment_id": "Your EnvironmentId",
|
||||
"environment_id": "Your Environment ID",
|
||||
"environment_id_description": "This id uniquely identifies this Formbricks environment.",
|
||||
"environment_id_description_with_environment_id": "Used to identify the correct environment: {environmentId} is yours.",
|
||||
"formbricks_sdk": "Formbricks SDK",
|
||||
"formbricks_sdk_connected": "Formbricks SDK is connected",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.",
|
||||
"formbricks_sdk_not_connected_description": "Connect your website or app with Formbricks",
|
||||
"have_a_problem": "Have a problem?",
|
||||
"how_to_setup": "How to setup",
|
||||
"how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.",
|
||||
"identifying_your_users": "identifying your users",
|
||||
"if_you_are_planning_to": "If you are planning to",
|
||||
"insert_this_code_into_the": "Insert this code into the",
|
||||
"need_a_more_detailed_setup_guide_for": "Need a more detailed setup guide for",
|
||||
"not_working": "Not working?",
|
||||
"open_an_issue_on_github": "Open an issue on GitHub",
|
||||
"open_the_browser_console_to_see_the_logs": "Open the browser console to see the logs.",
|
||||
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-check",
|
||||
"scroll_to_the_top": "Scroll to the top!",
|
||||
"step_1": "Step 1: Install with pnpm, npm or yarn",
|
||||
"step_2": "Step 2: Initialize widget",
|
||||
"step_2_description": "Import Formbricks and initialize the widget in your Component (e.g. App.tsx):",
|
||||
"step_3": "Step 3: Debug mode",
|
||||
"switch_on_the_debug_mode_by_appending": "Switch on the debug mode by appending",
|
||||
"tag_of_your_app": "tag of your app",
|
||||
"to_the_url_where_you_load_the": "to the URL where you load the",
|
||||
"want_to_learn_how_to_add_user_attributes": "Want to learn how to add user attributes, custom events and more?",
|
||||
"you_are_done": "You're done \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "you can set the user id with",
|
||||
"your_app_now_communicates_with_formbricks": "Your app now communicates with Formbricks - sending events, and loading surveys automatically!"
|
||||
"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"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "This is your only project, it cannot be deleted. Create a new project first.",
|
||||
@@ -989,7 +977,6 @@
|
||||
"free": "Free",
|
||||
"free_description": "Unlimited Surveys, Team Members, and more.",
|
||||
"get_2_months_free": "Get 2 months free",
|
||||
"get_in_touch": "Get in touch",
|
||||
"hosted_in_frankfurt": "Hosted in Frankfurt",
|
||||
"ios_android_sdks": "iOS & Android SDK for mobile surveys",
|
||||
"link_surveys": "Link Surveys (Shareable)",
|
||||
@@ -1111,9 +1098,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Account deletion consequences",
|
||||
"avatar_update_failed": "Avatar update failed. Please try again.",
|
||||
"backup_code": "Backup Code",
|
||||
"change_image": "Change image",
|
||||
"confirm_delete_account": "Delete your account with all of your personal information and data",
|
||||
"confirm_delete_my_account": "Delete My Account",
|
||||
"confirm_your_current_password_to_get_started": "Confirm your current password to get started.",
|
||||
@@ -1124,17 +1109,13 @@
|
||||
"email_change_initiated": "Your email change request has been initiated.",
|
||||
"enable_two_factor_authentication": "Enable two factor authentication",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Enter the code from your authenticator app below.",
|
||||
"file_size_must_be_less_than_10mb": "File size must be less than 10MB.",
|
||||
"invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.",
|
||||
"lost_access": "Lost access",
|
||||
"or_enter_the_following_code_manually": "Or enter the following code manually:",
|
||||
"organization_identification": "Assist your organization in identifying you on Formbricks",
|
||||
"organizations_delete_message": "You are the only owner of these organizations, so they <b>will be deleted as well.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Permanent removal of all of your personal information and data",
|
||||
"personal_information": "Personal information",
|
||||
"please_enter_email_to_confirm_account_deletion": "Please enter {email} in the following field to confirm the definitive deletion of your account:",
|
||||
"profile_updated_successfully": "Your profile was updated successfully",
|
||||
"remove_image": "Remove image",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Save the following backup codes in a safe place.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scan the QR code below with your authenticator app.",
|
||||
"security_description": "Manage your password and other security settings like two-factor authentication (2FA).",
|
||||
@@ -1144,10 +1125,8 @@
|
||||
"two_factor_code": "Two-Factor Code",
|
||||
"unlock_two_factor_authentication": "Unlock two-factor authentication with a higher plan",
|
||||
"update_personal_info": "Update your personal information",
|
||||
"upload_image": "Upload image",
|
||||
"warning_cannot_delete_account": "You are the only owner of this organization. Please transfer ownership to another member first.",
|
||||
"warning_cannot_undo": "This cannot be undone",
|
||||
"you_must_select_a_file": "You must select a file."
|
||||
"warning_cannot_undo": "This cannot be undone"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Add members to the team and determine their role.",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "Automatically close survey after",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Automatically close the survey after a certain number of responses.",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Automatically closes the survey at the beginning of the day (UTC).",
|
||||
"automatically_mark_the_survey_as_complete_after": "Automatically mark the survey as complete after",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Automatically release the survey at the beginning of the day (UTC).",
|
||||
"back_button_label": "\"Back\" Button Label",
|
||||
"background_styling": "Background Styling",
|
||||
"brand_color": "Brand color",
|
||||
@@ -1309,14 +1286,13 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.",
|
||||
"choose_where_to_run_the_survey": "Choose where to run the survey.",
|
||||
"city": "City",
|
||||
"close_survey_on_date": "Close survey on date",
|
||||
"close_survey_on_response_limit": "Close survey on response limit",
|
||||
"color": "Color",
|
||||
"column_used_in_logic_error": "This column is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"columns": "Columns",
|
||||
"company": "Company",
|
||||
"company_logo": "Company logo",
|
||||
"completed_responses": "partial or completed responses.",
|
||||
"completed_responses": "completed responses.",
|
||||
"concat": "Concat +",
|
||||
"conditional_logic": "Conditional Logic",
|
||||
"confirm_default_language": "Confirm default language",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "End screen card",
|
||||
"ending_card": "Ending card",
|
||||
"ending_card_used_in_logic": "This ending card is used in logic of question {questionIndex}.",
|
||||
"ending_used_in_quota": "This ending is being used in \"{quotaName}\" quota",
|
||||
"ends_with": "Ends with",
|
||||
"equals": "Equals",
|
||||
"equals_one_of": "Equals one of",
|
||||
@@ -1366,6 +1343,7 @@
|
||||
"fallback_for": "Fallback for ",
|
||||
"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",
|
||||
"field_name_eg_score_price": "Field name e.g, score, price",
|
||||
"first_name": "First Name",
|
||||
"five_points_recommended": "5 points (recommended)",
|
||||
@@ -1397,8 +1375,9 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "Subject of the email",
|
||||
"follow_ups_modal_action_to_description": "Email address to send the email to",
|
||||
"follow_ups_modal_action_to_label": "To",
|
||||
"follow_ups_modal_action_to_warning": "No email field detected in the survey",
|
||||
"follow_ups_modal_action_to_warning": "No valid options found for sending emails, please add some open-text / contact-info questions or hidden fields",
|
||||
"follow_ups_modal_create_heading": "Create a new follow-up",
|
||||
"follow_ups_modal_created_successfull_toast": "Follow-up created and will be saved once you save the survey.",
|
||||
"follow_ups_modal_edit_heading": "Edit this follow-up",
|
||||
"follow_ups_modal_edit_no_id": "No survey follow up id provided, can't update the survey follow up",
|
||||
"follow_ups_modal_name_label": "Follow-up name",
|
||||
@@ -1408,8 +1387,9 @@
|
||||
"follow_ups_modal_trigger_label": "Trigger",
|
||||
"follow_ups_modal_trigger_type_ending": "Respondent sees a specific ending",
|
||||
"follow_ups_modal_trigger_type_ending_select": "Select endings: ",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "No endings found in the survey!",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Please select at least one ending or change the trigger type",
|
||||
"follow_ups_modal_trigger_type_response": "Respondent completes survey",
|
||||
"follow_ups_modal_updated_successfull_toast": "Follow-up updated and will be saved once you save the survey.",
|
||||
"follow_ups_new": "New follow-up",
|
||||
"follow_ups_upgrade_button_text": "Upgrade to enable follow-ups",
|
||||
"form_styling": "Form styling",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "Question duplicated.",
|
||||
"question_id_updated": "Question ID updated",
|
||||
"question_used_in_logic": "This question is used in logic of question {questionIndex}.",
|
||||
"question_used_in_quota": "This question is being used in \"{quotaName}\" quota",
|
||||
"quotas": {
|
||||
"add_quota": "Add quota",
|
||||
"change_quota_for_public_survey": "Change quota for public survey?",
|
||||
"confirm_quota_changes": "Confirm quota changes",
|
||||
"confirm_quota_changes_body": "You have unsaved changes in your quota. Would you like to save them before leaving?",
|
||||
"continue_survey_normally": "Continue survey normally",
|
||||
"count_partial_submissions": "Count partial submissions",
|
||||
"count_partial_submissions_description": "Include respondents that match the quota criteria but did not complete the survey",
|
||||
"create_quota_for_public_survey": "Create quota for public survey?",
|
||||
"create_quota_for_public_survey_description": "Only future answers will be screened into quota",
|
||||
"create_quota_for_public_survey_text": "This survey is already public. Existing responses will not be taken into account for the new quota.",
|
||||
"delete_quota_confirmation_text": "This will permanently delete the quota {quotaName}.",
|
||||
"duplicate_quota": "Duplicate quota",
|
||||
"edit_quota": "Edit quota",
|
||||
"end_survey_for_matching_participants": "End survey for matching participants",
|
||||
"inclusion_criteria": "Inclusion Criteria",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, one {You already have {value} response for this quota, so the limit must be greater than {value}.} other {You already have {value} responses for this quota, so the limit must be greater than {value}.}}",
|
||||
"limited_to_x_responses": "Limited to {limit}",
|
||||
"new_quota": "New Quota",
|
||||
"quota_created_successfull_toast": "Quota created successfully",
|
||||
"quota_deleted_successfull_toast": "Quota deleted successfully",
|
||||
"quota_duplicated_successfull_toast": "Quota duplicated successfully",
|
||||
"quota_name_placeholder": "e.g., Age 18-25 participants",
|
||||
"quota_updated_successfull_toast": "Quota updated successfully",
|
||||
"response_limit": "Limits",
|
||||
"save_changes_confirmation_body": "Any changes to the inclusion criteria only affect future responses. \nWe recommend to either duplicate an existing or create a new quota.",
|
||||
"save_changes_confirmation_text": "Existing responses stay in the quota",
|
||||
"select_ending_card": "Select ending card",
|
||||
"upgrade_prompt_title": "Use quotas with a higher plan",
|
||||
"when_quota_has_been_reached": "When quota has been reached"
|
||||
},
|
||||
"randomize_all": "Randomize all",
|
||||
"randomize_all_except_last": "Randomize all except last",
|
||||
"range": "Range",
|
||||
@@ -1517,7 +1529,6 @@
|
||||
"redirect_thank_you_card": "Redirect thank you card",
|
||||
"redirect_to_url": "Redirect to Url",
|
||||
"redirect_to_url_not_available_on_free_plan": "Redirect To Url is not available on free plan",
|
||||
"release_survey_on_date": "Release survey on date",
|
||||
"remove_description": "Remove description",
|
||||
"remove_translations": "Remove translations",
|
||||
"require_answer": "Require Answer",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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.",
|
||||
"variable_name_must_start_with_a_letter": "Variable name must start with a letter.",
|
||||
"verify_email_before_submission": "Verify email before submission",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "Address Line 2",
|
||||
"an_error_occurred_deleting_the_tag": "An error occurred deleting the tag",
|
||||
"browser": "Browser",
|
||||
"bulk_delete_response_quotas": "The responses are part of quotas for this survey. How do you want to handle the quotas?",
|
||||
"city": "City",
|
||||
"company": "Company",
|
||||
"completed": "Completed ✅",
|
||||
"country": "Country",
|
||||
"decrement_quotas": "Decrement all limits of quotas including this response",
|
||||
"delete_response_confirmation": "This will delete the survey response, including all answers, tags, attached documents, and response metadata.",
|
||||
"delete_response_quotas": "The response is part of quotas for this survey. How do you want to handle the quotas?",
|
||||
"device": "Device",
|
||||
"device_info": "Device info",
|
||||
"email": "Email",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "The meta data is loaded based on the `lang` value in the URL.",
|
||||
"link_description": "Link description",
|
||||
"link_description_description": "Descriptions between 55-200 characters perform best.",
|
||||
"link_description_placeholder": "Help us improve by sharing your thoughts.",
|
||||
"link_title": "Link title",
|
||||
"link_title_description": "Short titles perform best as Meta Titles.",
|
||||
"link_title_placeholder": "Customer Feedback Survey",
|
||||
"preview_image": "Preview image",
|
||||
"preview_image_description": "Landscape images with small file sizes (<4MB) perform best.",
|
||||
"title": "Link settings"
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "Configure alerts",
|
||||
"congrats": "Congrats! Your survey is live.",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Connect your website or app with Formbricks to get started.",
|
||||
"current_count": "Current count",
|
||||
"custom_range": "Custom range...",
|
||||
"delete_all_existing_responses_and_displays": "Delete all existing responses and displays",
|
||||
"download_qr_code": "Download QR code",
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "Last month",
|
||||
"last_quarter": "Last quarter",
|
||||
"last_year": "Last year",
|
||||
"limit": "Limit",
|
||||
"no_responses_found": "No responses found",
|
||||
"other_values_found": "Other values found",
|
||||
"overall": "Overall",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "QR code download failed",
|
||||
"qr_code_download_with_start_soon": "QR code download will start soon",
|
||||
"qr_code_generation_failed": "There was a problem, loading the survey QR Code. Please try again.",
|
||||
"quotas_completed": "Quotas completed",
|
||||
"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.",
|
||||
"selected_responses_csv": "Selected responses (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "This quarter",
|
||||
"this_year": "This year",
|
||||
"time_to_complete": "Time to Complete",
|
||||
"ttc_tooltip": "Average time to complete the survey.",
|
||||
"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♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "Survey deleted successfully!",
|
||||
"survey_duplicated_successfully": "Survey duplicated successfully.",
|
||||
"survey_duplication_error": "Failed to duplicate the survey.",
|
||||
"survey_status_tooltip": "To update the survey status, update the schedule and close setting in the survey response options.",
|
||||
"templates": {
|
||||
"all_channels": "All channels",
|
||||
"all_industries": "All industries",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Paramètres du compte",
|
||||
"action": "Action",
|
||||
"actions": "Actions",
|
||||
"actions_description": "Les actions avec ou sans code sont utilisées pour déclencher des enquêtes d'interception dans les applications et sur les sites Web.",
|
||||
"active_surveys": "Sondages actifs",
|
||||
"activity": "Activité",
|
||||
"add": "Ajouter",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"add_logo": "Ajouter un logo",
|
||||
"add_member": "Ajouter un membre",
|
||||
"add_new_project": "Ajouter un nouveau projet",
|
||||
"add_project": "Ajouter un projet",
|
||||
"add_to_team": "Ajouter à l'équipe",
|
||||
"all": "Tout",
|
||||
@@ -141,7 +143,6 @@
|
||||
"apply_filters": "Appliquer des filtres",
|
||||
"are_you_sure": "Es-tu sûr ?",
|
||||
"attributes": "Attributs",
|
||||
"avatar": "Avatar",
|
||||
"back": "Retour",
|
||||
"billing": "Facturation",
|
||||
"booked": "Réservé",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "Annuler",
|
||||
"centered_modal": "Modal centré",
|
||||
"choices": "Choix",
|
||||
"choose_environment": "Choisir l'environnement",
|
||||
"choose_organization": "Choisir l'organisation",
|
||||
"choose_project": "Choisir projet",
|
||||
"clear_all": "Tout effacer",
|
||||
"clear_filters": "Effacer les filtres",
|
||||
"clear_selection": "Effacer la sélection",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "Connecter Formbricks",
|
||||
"connected": "Connecté",
|
||||
"contacts": "Contacts",
|
||||
"continue": "Continuer",
|
||||
"copied": "Copié",
|
||||
"copied_to_clipboard": "Copié dans le presse-papiers",
|
||||
"copy": "Copier",
|
||||
"copy_code": "Copier le code",
|
||||
"copy_link": "Copier le lien",
|
||||
"count_contacts": "{value, plural, one {# contact} other {# contacts} }",
|
||||
"count_responses": "{value, plural, other {# réponses}}",
|
||||
"create_new_organization": "Créer une nouvelle organisation",
|
||||
"create_project": "Créer un projet",
|
||||
"create_segment": "Créer un segment",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "Créé le",
|
||||
"created_by": "Créé par",
|
||||
"customer_success": "Succès Client",
|
||||
"danger_zone": "Zone de danger",
|
||||
"dark_overlay": "Superposition sombre",
|
||||
"date": "Date",
|
||||
"default": "Par défaut",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "E-commerce",
|
||||
"edit": "Modifier",
|
||||
"email": "Email",
|
||||
"ending_card": "Carte de fin",
|
||||
"enterprise_license": "Licence d'entreprise",
|
||||
"environment_not_found": "Environnement non trouvé",
|
||||
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
|
||||
"error": "Erreur",
|
||||
"error_component_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.",
|
||||
"error_component_title": "Erreur de chargement des ressources",
|
||||
"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",
|
||||
"finish": "Terminer",
|
||||
"follow_these": "Suivez ceci",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "Étiquette",
|
||||
"language": "Langue",
|
||||
"learn_more": "En savoir plus",
|
||||
"license": "Licence",
|
||||
"light_overlay": "Superposition légère",
|
||||
"limits_reached": "Limites atteints",
|
||||
"link": "Lien",
|
||||
"link_and_email": "Liens et e-mail",
|
||||
"link_survey": "Enquête de lien",
|
||||
"link_surveys": "Sondages de lien",
|
||||
"load_more": "Charger plus",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "Abonnement non trouvé",
|
||||
"metadata": "Métadonnées",
|
||||
"minimum": "Min",
|
||||
"mobile_overlay_text": "Formbricks n'est pas disponible pour les appareils avec des résolutions plus petites.",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks fonctionne mieux sur un écran plus grand. Pour gérer ou créer des sondages, passez à un autre appareil.",
|
||||
"mobile_overlay_surveys_look_good": "Ne t'inquiète pas – tes enquêtes sont superbes sur tous les appareils et tailles d'écran!",
|
||||
"mobile_overlay_title": "Oups, écran minuscule détecté!",
|
||||
"move_down": "Déplacer vers le bas",
|
||||
"move_up": "Déplacer vers le haut",
|
||||
"multiple_languages": "Plusieurs langues",
|
||||
"name": "Nom",
|
||||
"new": "Nouveau",
|
||||
"new_survey": "Nouveau Sondage",
|
||||
"new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !",
|
||||
"next": "Suivant",
|
||||
"no_background_image_found": "Aucune image de fond trouvée.",
|
||||
"no_code": "Pas de code",
|
||||
"no_files_uploaded": "Aucun fichier n'a été téléchargé.",
|
||||
"no_quotas_found": "Aucun quota trouvé",
|
||||
"no_result_found": "Aucun résultat trouvé",
|
||||
"no_results": "Aucun résultat",
|
||||
"no_surveys_found": "Aucun sondage trouvé.",
|
||||
@@ -284,6 +295,7 @@
|
||||
"organization": "Organisation",
|
||||
"organization_id": "ID de l'organisation",
|
||||
"organization_not_found": "Organisation non trouvée",
|
||||
"organization_settings": "Paramètres de l'organisation",
|
||||
"organization_teams_not_found": "Équipes d'organisation non trouvées",
|
||||
"other": "Autre",
|
||||
"others": "Autres",
|
||||
@@ -307,6 +319,7 @@
|
||||
"product_manager": "Chef de produit",
|
||||
"profile": "Profil",
|
||||
"profile_id": "Identifiant de profil",
|
||||
"progress": "Progression",
|
||||
"project_configuration": "Configuration du projet",
|
||||
"project_creation_description": "Organisez les enquêtes en projets pour un meilleur contrôle d'accès.",
|
||||
"project_id": "ID de projet",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "Question",
|
||||
"question_id": "ID de la question",
|
||||
"questions": "Questions",
|
||||
"quota": "Quota",
|
||||
"quotas": "Quotas",
|
||||
"quotas_description": "Limitez le nombre de réponses que vous recevez de la part des participants répondant à certains critères.",
|
||||
"read_docs": "Lire les documents",
|
||||
"recipients": "Destinataires",
|
||||
"remove": "Retirer",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "Enregistrer",
|
||||
"save_changes": "Enregistrer les modifications",
|
||||
"saving": "Sauvegarder",
|
||||
"scheduled": "Programmé",
|
||||
"search": "Recherche",
|
||||
"security": "Sécurité",
|
||||
"segment": "Segmenter",
|
||||
@@ -366,6 +381,7 @@
|
||||
"start_free_trial": "Commencer l'essai gratuit",
|
||||
"status": "Statut",
|
||||
"step_by_step_manual": "Manuel étape par étape",
|
||||
"storage_not_configured": "Stockage de fichiers non configuré, les téléchargements risquent d'échouer",
|
||||
"styling": "Style",
|
||||
"submit": "Soumettre",
|
||||
"summary": "Résumé",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "Sondage en direct",
|
||||
"survey_not_found": "Sondage non trouvé",
|
||||
"survey_paused": "Sondage en pause.",
|
||||
"survey_scheduled": "Sondage programmé.",
|
||||
"survey_type": "Type de sondage",
|
||||
"surveys": "Enquêtes",
|
||||
"switch_organization": "Changer d'organisation",
|
||||
"switch_to": "Passer à {environment}",
|
||||
"table_items_deleted_successfully": "{type}s supprimés avec succès",
|
||||
"table_settings": "Réglages de table",
|
||||
@@ -577,8 +591,7 @@
|
||||
"contacts_table_refresh": "Rafraîchir les contacts",
|
||||
"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.",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom de famille",
|
||||
"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.}}",
|
||||
"no_responses_found": "Aucune réponse trouvée",
|
||||
"not_provided": "Non fourni",
|
||||
"search_contact": "Rechercher un contact",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "Contrôle d'accès",
|
||||
"add_api_key": "Ajouter une clé API",
|
||||
"api_key": "Clé API",
|
||||
"api_key_copied_to_clipboard": "Clé API copiée dans le presse-papiers",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "Impossible de supprimer la clé API"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "Ceci est l'URL de votre backend Formbricks.",
|
||||
"app_connection": "Connexion d'application",
|
||||
"app_connection_description": "Connectez votre application à Formbricks.",
|
||||
"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",
|
||||
"check_out_the_docs": "Consultez la documentation.",
|
||||
"dive_into_the_docs": "Plongez dans la documentation.",
|
||||
"does_your_widget_work": "Votre widget fonctionne-t-il ?",
|
||||
"environment_id": "Votre identifiant d'environnement",
|
||||
"environment_id_description": "Cet identifiant identifie de manière unique cet environnement Formbricks.",
|
||||
"environment_id_description_with_environment_id": "Utilisé pour identifier l'environnement correct : {environmentId} est le vôtre.",
|
||||
"formbricks_sdk": "SDK Formbricks",
|
||||
"formbricks_sdk_connected": "Le SDK Formbricks est connecté",
|
||||
"formbricks_sdk_not_connected": "Le SDK Formbricks n'est pas encore connecté.",
|
||||
"formbricks_sdk_not_connected_description": "Connectez votre site web ou votre application à Formbricks.",
|
||||
"have_a_problem": "Vous avez un problème ?",
|
||||
"how_to_setup": "Comment configurer",
|
||||
"how_to_setup_description": "Suivez ces étapes pour configurer le widget Formbricks dans votre application.",
|
||||
"identifying_your_users": "identifier vos utilisateurs",
|
||||
"if_you_are_planning_to": "Si vous prévoyez de",
|
||||
"insert_this_code_into_the": "Insérez ce code dans le",
|
||||
"need_a_more_detailed_setup_guide_for": "Besoin d'un guide d'installation plus détaillé pour",
|
||||
"not_working": "Ça ne fonctionne pas ?",
|
||||
"open_an_issue_on_github": "Ouvrir un problème sur GitHub",
|
||||
"open_the_browser_console_to_see_the_logs": "Ouvrez la console du navigateur pour voir les journaux.",
|
||||
"receiving_data": "Réception des données \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-vérifier",
|
||||
"scroll_to_the_top": "Faites défiler vers le haut !",
|
||||
"step_1": "Étape 1 : Installer avec pnpm, npm ou yarn",
|
||||
"step_2": "Étape 2 : Initialiser le widget",
|
||||
"step_2_description": "Importez Formbricks et initialisez le widget dans votre composant (par exemple, App.tsx) :",
|
||||
"step_3": "Étape 3 : Mode débogage",
|
||||
"switch_on_the_debug_mode_by_appending": "Activez le mode débogage en ajoutant",
|
||||
"tag_of_your_app": "étiquette de votre application",
|
||||
"to_the_url_where_you_load_the": "vers l'URL où vous chargez le",
|
||||
"want_to_learn_how_to_add_user_attributes": "Vous voulez apprendre à ajouter des attributs utilisateur, des événements personnalisés et plus encore ?",
|
||||
"you_are_done": "Vous avez terminé \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "vous pouvez définir l'ID utilisateur avec",
|
||||
"your_app_now_communicates_with_formbricks": "Votre application communique désormais avec Formbricks - envoyant des événements et chargeant des enquêtes automatiquement !"
|
||||
"setup_alert_description": "Suivez ce tutoriel étape par étape pour connecter votre application ou site web en moins de 5 minutes.",
|
||||
"setup_alert_title": "Comment connecter"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Ceci est votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.",
|
||||
@@ -989,7 +977,6 @@
|
||||
"free": "Gratuit",
|
||||
"free_description": "Sondages illimités, membres d'équipe, et plus encore.",
|
||||
"get_2_months_free": "Obtenez 2 mois gratuits",
|
||||
"get_in_touch": "Prenez contact",
|
||||
"hosted_in_frankfurt": "Hébergé à Francfort",
|
||||
"ios_android_sdks": "SDK iOS et Android pour les sondages mobiles",
|
||||
"link_surveys": "Sondages par lien (partageables)",
|
||||
@@ -1111,9 +1098,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Conséquences de la suppression de compte",
|
||||
"avatar_update_failed": "La mise à jour de l'avatar a échoué. Veuillez réessayer.",
|
||||
"backup_code": "Code de sauvegarde",
|
||||
"change_image": "Changer l'image",
|
||||
"confirm_delete_account": "Supprimez votre compte avec toutes vos informations personnelles et données.",
|
||||
"confirm_delete_my_account": "Supprimer mon compte",
|
||||
"confirm_your_current_password_to_get_started": "Confirmez votre mot de passe actuel pour commencer.",
|
||||
@@ -1124,17 +1109,13 @@
|
||||
"email_change_initiated": "Votre demande de changement d'email a été initiée.",
|
||||
"enable_two_factor_authentication": "Activer l'authentification à deux facteurs",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Entrez le code de votre application d'authentification ci-dessous.",
|
||||
"file_size_must_be_less_than_10mb": "La taille du fichier doit être inférieure à 10 Mo.",
|
||||
"invalid_file_type": "Type de fichier invalide. Seuls les fichiers JPEG, PNG et WEBP sont autorisés.",
|
||||
"lost_access": "Accès perdu",
|
||||
"or_enter_the_following_code_manually": "Ou entrez le code suivant manuellement :",
|
||||
"organization_identification": "Aidez votre organisation à vous identifier sur Formbricks",
|
||||
"organizations_delete_message": "Tu es le seul propriétaire de ces organisations, elles <b>seront aussi supprimées.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Suppression permanente de toutes vos informations et données personnelles.",
|
||||
"personal_information": "Informations personnelles",
|
||||
"please_enter_email_to_confirm_account_deletion": "Veuillez entrer {email} dans le champ suivant pour confirmer la suppression définitive de votre compte :",
|
||||
"profile_updated_successfully": "Votre profil a été mis à jour avec succès.",
|
||||
"remove_image": "Supprimer l'image",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Enregistrez les codes de sauvegarde suivants dans un endroit sûr.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scannez le code QR ci-dessous avec votre application d'authentification.",
|
||||
"security_description": "Gérez votre mot de passe et d'autres paramètres de sécurité comme l'authentification à deux facteurs (2FA).",
|
||||
@@ -1144,10 +1125,8 @@
|
||||
"two_factor_code": "Code à deux facteurs",
|
||||
"unlock_two_factor_authentication": "Débloquez l'authentification à deux facteurs avec une offre supérieure",
|
||||
"update_personal_info": "Mettez à jour vos informations personnelles",
|
||||
"upload_image": "Télécharger l'image",
|
||||
"warning_cannot_delete_account": "Tu es le seul propriétaire de cette organisation. Transfère la propriété à un autre membre d'abord.",
|
||||
"warning_cannot_undo": "Ceci ne peut pas être annulé",
|
||||
"you_must_select_a_file": "Vous devez sélectionner un fichier."
|
||||
"warning_cannot_undo": "Ceci ne peut pas être annulé"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Ajoutez des membres à l'équipe et déterminez leur rôle.",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "Fermer automatiquement l'enquête après",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fermer automatiquement l'enquête après un certain nombre de réponses.",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fermer automatiquement l'enquête si l'utilisateur ne répond pas après un certain nombre de secondes.",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Ferme automatiquement l'enquête au début de la journée (UTC).",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marquer automatiquement l'enquête comme terminée après",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Libérer automatiquement l'enquête au début de la journée (UTC).",
|
||||
"back_button_label": "Label du bouton \"Retour''",
|
||||
"background_styling": "Style de fond",
|
||||
"brand_color": "Couleur de marque",
|
||||
@@ -1309,14 +1286,13 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
|
||||
"choose_where_to_run_the_survey": "Choisissez où réaliser l'enquête.",
|
||||
"city": "Ville",
|
||||
"close_survey_on_date": "Clôturer l'enquête à la date",
|
||||
"close_survey_on_response_limit": "Fermer l'enquête sur la limite de réponse",
|
||||
"color": "Couleur",
|
||||
"column_used_in_logic_error": "Cette colonne est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||
"columns": "Colonnes",
|
||||
"company": "Société",
|
||||
"company_logo": "Logo de l'entreprise",
|
||||
"completed_responses": "des réponses partielles ou complètes.",
|
||||
"completed_responses": "Réponses terminées",
|
||||
"concat": "Concat +",
|
||||
"conditional_logic": "Logique conditionnelle",
|
||||
"confirm_default_language": "Confirmer la langue par défaut",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "Carte de fin d'écran",
|
||||
"ending_card": "Carte de fin",
|
||||
"ending_card_used_in_logic": "Cette carte de fin est utilisée dans la logique de la question '{'questionIndex'}'.",
|
||||
"ending_used_in_quota": "Cette fin est utilisée dans le quota \"{quotaName}\"",
|
||||
"ends_with": "Se termine par",
|
||||
"equals": "Égal",
|
||||
"equals_one_of": "Égal à l'un de",
|
||||
@@ -1366,6 +1343,7 @@
|
||||
"fallback_for": "Solution de repli pour ",
|
||||
"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}\"",
|
||||
"field_name_eg_score_price": "Nom du champ par exemple, score, prix",
|
||||
"first_name": "Prénom",
|
||||
"five_points_recommended": "5 points (recommandé)",
|
||||
@@ -1397,8 +1375,9 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "Objet de l'email",
|
||||
"follow_ups_modal_action_to_description": "Adresse e-mail à laquelle envoyer l'e-mail",
|
||||
"follow_ups_modal_action_to_label": "à",
|
||||
"follow_ups_modal_action_to_warning": "Aucun champ d'email détecté dans l'enquête",
|
||||
"follow_ups_modal_action_to_warning": "Aucune option valable trouvée pour l'envoi d'emails, veuillez ajouter des questions à texte libre / info-contact ou des champs cachés",
|
||||
"follow_ups_modal_create_heading": "Créer un nouveau suivi",
|
||||
"follow_ups_modal_created_successfull_toast": "\"Suivi créé et sera enregistré une fois que vous sauvegarderez le sondage.\"",
|
||||
"follow_ups_modal_edit_heading": "Modifier ce suivi",
|
||||
"follow_ups_modal_edit_no_id": "Aucun identifiant de suivi d'enquête fourni, impossible de mettre à jour le suivi de l'enquête.",
|
||||
"follow_ups_modal_name_label": "Nom de suivi",
|
||||
@@ -1408,8 +1387,9 @@
|
||||
"follow_ups_modal_trigger_label": "Déclencheur",
|
||||
"follow_ups_modal_trigger_type_ending": "Le répondant voit une fin spécifique",
|
||||
"follow_ups_modal_trigger_type_ending_select": "Choisir des fins :",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Aucune fin trouvée dans l'enquête !",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Veuillez sélectionner au moins une fin ou changer le type de déclencheur.",
|
||||
"follow_ups_modal_trigger_type_response": "Le répondant complète l'enquête",
|
||||
"follow_ups_modal_updated_successfull_toast": "\"Suivi mis à jour et sera enregistré une fois que vous sauvegarderez le sondage.\"",
|
||||
"follow_ups_new": "Nouveau suivi",
|
||||
"follow_ups_upgrade_button_text": "Passez à la version supérieure pour activer les relances",
|
||||
"form_styling": "Style de formulaire",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "Question dupliquée.",
|
||||
"question_id_updated": "ID de la question mis à jour",
|
||||
"question_used_in_logic": "Cette question est utilisée dans la logique de la question '{'questionIndex'}'.",
|
||||
"question_used_in_quota": "Cette question est utilisée dans le quota \"{quotaName}\"",
|
||||
"quotas": {
|
||||
"add_quota": "Ajouter un quota",
|
||||
"change_quota_for_public_survey": "Changer le quota pour le sondage public ?",
|
||||
"confirm_quota_changes": "Confirmer les modifications de quotas",
|
||||
"confirm_quota_changes_body": "Vous avez des modifications non enregistrées dans votre quota. Souhaitez-vous les enregistrer avant de partir ?",
|
||||
"continue_survey_normally": "Continuer le sondage normalement",
|
||||
"count_partial_submissions": "Compter les soumissions partielles",
|
||||
"count_partial_submissions_description": "Inclure les répondants qui correspondent aux critères de quota mais n'ont pas terminé le sondage",
|
||||
"create_quota_for_public_survey": "Créer un quota pour le sondage public ?",
|
||||
"create_quota_for_public_survey_description": "Seules les réponses futures seront filtrées dans le quota",
|
||||
"create_quota_for_public_survey_text": "Ce sondage est déjà public. Les réponses existantes ne seront pas prises en compte pour le nouveau quota.",
|
||||
"delete_quota_confirmation_text": "Cela supprimera définitivement le quota {quotaName}.",
|
||||
"duplicate_quota": "Dupliquer le quota",
|
||||
"edit_quota": "Modifier le quota",
|
||||
"end_survey_for_matching_participants": "Terminer l'enquête pour les participants correspondants",
|
||||
"inclusion_criteria": "Critères d'inclusion",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, other {La limite doit être supérieure ou égale au nombre de réponses}}",
|
||||
"limited_to_x_responses": "Limité à {limit}",
|
||||
"new_quota": "Nouveau Quota",
|
||||
"quota_created_successfull_toast": "Quota créé avec succès",
|
||||
"quota_deleted_successfull_toast": "Quota supprimé avec succès",
|
||||
"quota_duplicated_successfull_toast": "Quota dupliqué avec succès",
|
||||
"quota_name_placeholder": "par ex., Participants âgés de 18 à 25 ans",
|
||||
"quota_updated_successfull_toast": "Quota mis à jour avec succès",
|
||||
"response_limit": "Limites",
|
||||
"save_changes_confirmation_body": "Les modifications apportées aux critères d'inclusion n'affectent que les réponses futures. \nNous vous recommandons soit de dupliquer un quota existant, soit d'en créer un nouveau.",
|
||||
"save_changes_confirmation_text": "\"Les réponses existantes restent dans le quota\"",
|
||||
"select_ending_card": "Sélectionner la carte de fin",
|
||||
"upgrade_prompt_title": "Utilisez des quotas avec un plan supérieur",
|
||||
"when_quota_has_been_reached": "Quand le quota est atteint"
|
||||
},
|
||||
"randomize_all": "Randomiser tout",
|
||||
"randomize_all_except_last": "Randomiser tout sauf le dernier",
|
||||
"range": "Plage",
|
||||
@@ -1517,7 +1529,6 @@
|
||||
"redirect_thank_you_card": "Carte de remerciement de redirection",
|
||||
"redirect_to_url": "Rediriger vers l'URL",
|
||||
"redirect_to_url_not_available_on_free_plan": "La redirection vers l'URL n'est pas disponible sur le plan gratuit.",
|
||||
"release_survey_on_date": "Publier l'enquête à la date",
|
||||
"remove_description": "Supprimer la description",
|
||||
"remove_translations": "Supprimer les traductions",
|
||||
"require_answer": "Réponse requise",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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.",
|
||||
"variable_name_must_start_with_a_letter": "Le nom de la variable doit commencer par une lettre.",
|
||||
"verify_email_before_submission": "Vérifiez l'email avant la soumission",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "Ligne d'adresse 2",
|
||||
"an_error_occurred_deleting_the_tag": "Une erreur est survenue lors de la suppression de l'étiquette.",
|
||||
"browser": "Navigateur",
|
||||
"bulk_delete_response_quotas": "Les réponses font partie des quotas pour ce sondage. Comment voulez-vous gérer les quotas ?",
|
||||
"city": "Ville",
|
||||
"company": "Société",
|
||||
"completed": "Terminé ✅",
|
||||
"country": "Pays",
|
||||
"decrement_quotas": "Décrémentez toutes les limites des quotas y compris cette réponse",
|
||||
"delete_response_confirmation": "Cela supprimera la réponse au sondage, y compris toutes les réponses, les étiquettes, les documents joints et les métadonnées de réponse.",
|
||||
"delete_response_quotas": "La réponse fait partie des quotas pour ce sondage. Comment voulez-vous gérer les quotas ?",
|
||||
"device": "Dispositif",
|
||||
"device_info": "Informations sur l'appareil",
|
||||
"email": "Email",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "Les métadonnées sont chargées en fonction de la valeur « lang » dans l'URL.",
|
||||
"link_description": "Description du lien",
|
||||
"link_description_description": "« Les descriptions entre 55 et 200 caractères donnent les meilleurs résultats. »",
|
||||
"link_description_placeholder": "Aidez-nous à nous améliorer en partageant vos pensées.",
|
||||
"link_title": "Titre du lien",
|
||||
"link_title_description": "Les titres courts fonctionnent mieux comme titres méta.",
|
||||
"link_title_placeholder": "Sondage de Retour Clients",
|
||||
"preview_image": "Aperçu de l'image",
|
||||
"preview_image_description": "Les images en paysage avec de petites tailles de fichier (<4MB) fonctionnent le mieux.",
|
||||
"title": "Paramètres de lien"
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "Configurer les alertes",
|
||||
"congrats": "Félicitations ! Votre enquête est en ligne.",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Connectez votre site web ou votre application à Formbricks pour commencer.",
|
||||
"current_count": "Nombre actuel",
|
||||
"custom_range": "Plage personnalisée...",
|
||||
"delete_all_existing_responses_and_displays": "Supprimer toutes les réponses existantes et les affichages",
|
||||
"download_qr_code": "Télécharger code QR",
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "Le mois dernier",
|
||||
"last_quarter": "dernier trimestre",
|
||||
"last_year": "l'année dernière",
|
||||
"limit": "Limite",
|
||||
"no_responses_found": "Aucune réponse trouvée",
|
||||
"other_values_found": "D'autres valeurs trouvées",
|
||||
"overall": "Globalement",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "Échec du téléchargement du code QR",
|
||||
"qr_code_download_with_start_soon": "Le téléchargement du code QR débutera bientôt",
|
||||
"qr_code_generation_failed": "\"Un problème est survenu lors du chargement du code QR du sondage. Veuillez réessayer.\"",
|
||||
"quotas_completed": "Quotas terminés",
|
||||
"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é.",
|
||||
"selected_responses_csv": "Réponses sélectionnées (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "Ce trimestre",
|
||||
"this_year": "Cette année",
|
||||
"time_to_complete": "Temps à compléter",
|
||||
"ttc_tooltip": "Temps moyen pour compléter l'enquête.",
|
||||
"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 \uD83E\uDDD8♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "Enquête supprimée avec succès !",
|
||||
"survey_duplicated_successfully": "Enquête dupliquée avec succès.",
|
||||
"survey_duplication_error": "Échec de la duplication de l'enquête.",
|
||||
"survey_status_tooltip": "Pour mettre à jour le statut de l'enquête, mettez à jour le calendrier et fermez les paramètres dans les options de réponse à l'enquête.",
|
||||
"templates": {
|
||||
"all_channels": "Tous les canaux",
|
||||
"all_industries": "Tous les secteurs",
|
||||
|
||||
2891
apps/web/locales/ja-JP.json
Normal file
2891
apps/web/locales/ja-JP.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Configurações da conta",
|
||||
"action": "Ação",
|
||||
"actions": "Ações",
|
||||
"actions_description": "Ações de Código e Sem Código são usadas para acionar interceptar pesquisas dentro de apps & em sites.",
|
||||
"active_surveys": "Pesquisas ativas",
|
||||
"activity": "Atividade",
|
||||
"add": "Adicionar",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "Adicionar filtro",
|
||||
"add_logo": "Adicionar logo",
|
||||
"add_member": "Adicionar membro",
|
||||
"add_new_project": "Adicionar novo projeto",
|
||||
"add_project": "Adicionar projeto",
|
||||
"add_to_team": "Adicionar à equipe",
|
||||
"all": "Todos",
|
||||
@@ -141,7 +143,6 @@
|
||||
"apply_filters": "Aplicar filtros",
|
||||
"are_you_sure": "Certeza?",
|
||||
"attributes": "atributos",
|
||||
"avatar": "Avatar",
|
||||
"back": "Voltar",
|
||||
"billing": "Faturamento",
|
||||
"booked": "Reservado",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "Cancelar",
|
||||
"centered_modal": "Modal Centralizado",
|
||||
"choices": "Escolhas",
|
||||
"choose_environment": "Escolher ambiente",
|
||||
"choose_organization": "Escolher organização",
|
||||
"choose_project": "Escolher projeto",
|
||||
"clear_all": "Limpar tudo",
|
||||
"clear_filters": "Limpar filtros",
|
||||
"clear_selection": "Limpar seleção",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "Conectar Formbricks",
|
||||
"connected": "conectado",
|
||||
"contacts": "Contatos",
|
||||
"continue": "Continuar",
|
||||
"copied": "Copiado",
|
||||
"copied_to_clipboard": "Copiado para a área de transferência",
|
||||
"copy": "Copiar",
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"count_contacts": "{value, plural, one {# contato} other {# contatos} }",
|
||||
"count_responses": "{value, plural, other {# respostas}}",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_project": "Criar projeto",
|
||||
"create_segment": "Criar segmento",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "Data de criação",
|
||||
"created_by": "Criado por",
|
||||
"customer_success": "Sucesso do Cliente",
|
||||
"danger_zone": "Zona de Perigo",
|
||||
"dark_overlay": "sobreposição escura",
|
||||
"date": "Encontro",
|
||||
"default": "Padrão",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "comércio eletrônico",
|
||||
"edit": "Editar",
|
||||
"email": "Email",
|
||||
"ending_card": "Cartão de encerramento",
|
||||
"enterprise_license": "Licença Empresarial",
|
||||
"environment_not_found": "Ambiente não encontrado",
|
||||
"environment_notice": "Você está atualmente no ambiente {environment}.",
|
||||
"error": "Erro",
|
||||
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
|
||||
"error_component_title": "Erro ao carregar recursos",
|
||||
"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",
|
||||
"finish": "Terminar",
|
||||
"follow_these": "Siga esses",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "Etiqueta",
|
||||
"language": "Língua",
|
||||
"learn_more": "Saiba mais",
|
||||
"license": "Licença",
|
||||
"light_overlay": "sobreposição leve",
|
||||
"limits_reached": "Limites Atingidos",
|
||||
"link": "link",
|
||||
"link_and_email": "Link & E-mail",
|
||||
"link_survey": "Pesquisa de Link",
|
||||
"link_surveys": "Link de Pesquisas",
|
||||
"load_more": "Carregar mais",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "Assinatura não encontrada",
|
||||
"metadata": "metadados",
|
||||
"minimum": "Mínimo",
|
||||
"mobile_overlay_text": "O Formbricks não está disponível para dispositivos com resoluções menores.",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor em uma tela maior. Para gerenciar ou criar pesquisas, mude para outro dispositivo.",
|
||||
"mobile_overlay_surveys_look_good": "Não se preocupe – suas pesquisas ficam ótimas em qualquer dispositivo e tamanho de tela!",
|
||||
"mobile_overlay_title": "Eita, tela pequena detectada!",
|
||||
"move_down": "Descer",
|
||||
"move_up": "Subir",
|
||||
"multiple_languages": "Vários idiomas",
|
||||
"name": "Nome",
|
||||
"new": "Novo",
|
||||
"new_survey": "Nova Pesquisa",
|
||||
"new_version_available": "Formbricks {version} chegou. Atualize agora!",
|
||||
"next": "Próximo",
|
||||
"no_background_image_found": "Imagem de fundo não encontrada.",
|
||||
"no_code": "Sem código",
|
||||
"no_files_uploaded": "Nenhum arquivo foi enviado",
|
||||
"no_quotas_found": "Nenhuma cota encontrada",
|
||||
"no_result_found": "Nenhum resultado encontrado",
|
||||
"no_results": "Nenhum resultado",
|
||||
"no_surveys_found": "Não foram encontradas pesquisas.",
|
||||
@@ -284,6 +295,7 @@
|
||||
"organization": "organização",
|
||||
"organization_id": "ID da Organização",
|
||||
"organization_not_found": "Organização não encontrada",
|
||||
"organization_settings": "Configurações da Organização",
|
||||
"organization_teams_not_found": "Equipes da organização não encontradas",
|
||||
"other": "outro",
|
||||
"others": "Outros",
|
||||
@@ -307,6 +319,7 @@
|
||||
"product_manager": "Gerente de Produto",
|
||||
"profile": "Perfil",
|
||||
"profile_id": "ID de Perfil",
|
||||
"progress": "Progresso",
|
||||
"project_configuration": "Configuração do Projeto",
|
||||
"project_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
|
||||
"project_id": "ID do Projeto",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "Pergunta",
|
||||
"question_id": "ID da Pergunta",
|
||||
"questions": "Perguntas",
|
||||
"quota": "Cota",
|
||||
"quotas": "Cotas",
|
||||
"quotas_description": "Limite a quantidade de respostas que você recebe de participantes que atendem a determinados critérios.",
|
||||
"read_docs": "Ler Documentação",
|
||||
"recipients": "Destinatários",
|
||||
"remove": "remover",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "Salvar",
|
||||
"save_changes": "Salvar alterações",
|
||||
"saving": "Salvando",
|
||||
"scheduled": "agendado",
|
||||
"search": "Buscar",
|
||||
"security": "Segurança",
|
||||
"segment": "segmento",
|
||||
@@ -366,6 +381,7 @@
|
||||
"start_free_trial": "Iniciar Teste Grátis",
|
||||
"status": "status",
|
||||
"step_by_step_manual": "Manual passo a passo",
|
||||
"storage_not_configured": "Armazenamento de arquivos não configurado, uploads provavelmente falharão",
|
||||
"styling": "Estilização",
|
||||
"submit": "Enviar",
|
||||
"summary": "Resumo",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "Pesquisa ao vivo",
|
||||
"survey_not_found": "Pesquisa não encontrada",
|
||||
"survey_paused": "Pesquisa pausada.",
|
||||
"survey_scheduled": "Pesquisa agendada.",
|
||||
"survey_type": "Tipo de Pesquisa",
|
||||
"surveys": "Pesquisas",
|
||||
"switch_organization": "Mudar organização",
|
||||
"switch_to": "Mudar para {environment}",
|
||||
"table_items_deleted_successfully": "{type}s deletados com sucesso",
|
||||
"table_settings": "Arrumação da mesa",
|
||||
@@ -577,8 +591,7 @@
|
||||
"contacts_table_refresh": "Atualizar contatos",
|
||||
"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.",
|
||||
"first_name": "Primeiro Nome",
|
||||
"last_name": "Sobrenome",
|
||||
"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.}}",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"not_provided": "Não fornecido",
|
||||
"search_contact": "Buscar contato",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "Controle de Acesso",
|
||||
"add_api_key": "Adicionar Chave API",
|
||||
"api_key": "Chave de API",
|
||||
"api_key_copied_to_clipboard": "Chave da API copiada para a área de transferência",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "Não foi possível deletar a Chave API"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "Essa é a URL do seu backend do Formbricks.",
|
||||
"app_connection": "Conexão do App",
|
||||
"app_connection_description": "Conecte seu app ao Formbricks.",
|
||||
"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",
|
||||
"check_out_the_docs": "Confere a documentação.",
|
||||
"dive_into_the_docs": "Mergulha na documentação.",
|
||||
"does_your_widget_work": "Seu widget funciona?",
|
||||
"environment_id": "Seu Id do Ambiente",
|
||||
"environment_id_description": "Este ID identifica exclusivamente este ambiente do Formbricks.",
|
||||
"environment_id_description_with_environment_id": "Usado para identificar o ambiente correto: {environmentId} é o seu.",
|
||||
"formbricks_sdk": "SDK do Formbricks",
|
||||
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
|
||||
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.",
|
||||
"formbricks_sdk_not_connected_description": "Conecte seu site ou app com o Formbricks",
|
||||
"have_a_problem": "Tá com problema?",
|
||||
"how_to_setup": "Como configurar",
|
||||
"how_to_setup_description": "Siga esses passos para configurar o widget do Formbricks no seu app.",
|
||||
"identifying_your_users": "identificando seus usuários",
|
||||
"if_you_are_planning_to": "Se você está planejando",
|
||||
"insert_this_code_into_the": "Insere esse código no",
|
||||
"need_a_more_detailed_setup_guide_for": "Preciso de um guia de configuração mais detalhado para",
|
||||
"not_working": "Não tá funcionando?",
|
||||
"open_an_issue_on_github": "Abre uma issue no GitHub",
|
||||
"open_the_browser_console_to_see_the_logs": "Abre o console do navegador pra ver os logs.",
|
||||
"receiving_data": "Recebendo dados \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Verificar novamente",
|
||||
"scroll_to_the_top": "Rola pra cima!",
|
||||
"step_1": "Passo 1: Instale com pnpm, npm ou yarn",
|
||||
"step_2": "Passo 2: Iniciar widget",
|
||||
"step_2_description": "Importe o Formbricks e inicialize o widget no seu Componente (por exemplo, App.tsx):",
|
||||
"step_3": "Passo 3: Modo de depuração",
|
||||
"switch_on_the_debug_mode_by_appending": "Ative o modo de depuração adicionando",
|
||||
"tag_of_your_app": "etiqueta do seu app",
|
||||
"to_the_url_where_you_load_the": "para a URL onde você carrega o",
|
||||
"want_to_learn_how_to_add_user_attributes": "Quer aprender como adicionar atributos de usuário, eventos personalizados e mais?",
|
||||
"you_are_done": "Você terminou \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "você pode definir o id do usuário com",
|
||||
"your_app_now_communicates_with_formbricks": "Seu app agora se comunica com o Formbricks - enviando eventos e carregando pesquisas automaticamente!"
|
||||
"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"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Esse é seu único projeto, não pode ser deletado. Crie um novo projeto primeiro.",
|
||||
@@ -989,7 +977,6 @@
|
||||
"free": "grátis",
|
||||
"free_description": "Pesquisas ilimitadas, membros da equipe e mais.",
|
||||
"get_2_months_free": "Ganhe 2 meses grátis",
|
||||
"get_in_touch": "Entre em contato",
|
||||
"hosted_in_frankfurt": "Hospedado em Frankfurt",
|
||||
"ios_android_sdks": "SDK para iOS e Android para pesquisas móveis",
|
||||
"link_surveys": "Link de Pesquisas (Compartilhável)",
|
||||
@@ -1111,9 +1098,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Consequências da exclusão da conta",
|
||||
"avatar_update_failed": "Falha ao atualizar o avatar. Por favor, tente novamente.",
|
||||
"backup_code": "Código de Backup",
|
||||
"change_image": "Mudar imagem",
|
||||
"confirm_delete_account": "Apague sua conta com todas as suas informações pessoais e dados",
|
||||
"confirm_delete_my_account": "Excluir Minha Conta",
|
||||
"confirm_your_current_password_to_get_started": "Confirme sua senha atual para começar.",
|
||||
@@ -1124,17 +1109,13 @@
|
||||
"email_change_initiated": "Sua solicitação de alteração de e-mail foi iniciada.",
|
||||
"enable_two_factor_authentication": "Ativar autenticação de dois fatores",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Digite o código do seu app autenticador abaixo.",
|
||||
"file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.",
|
||||
"invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.",
|
||||
"lost_access": "Perdi o acesso",
|
||||
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
||||
"organization_identification": "Ajude sua organização a te identificar no Formbricks",
|
||||
"organizations_delete_message": "Você é o único dono dessas organizações, então elas <b>também serão apagadas.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Remoção permanente de todas as suas informações e dados pessoais",
|
||||
"personal_information": "Informações pessoais",
|
||||
"please_enter_email_to_confirm_account_deletion": "Por favor, insira {email} no campo abaixo para confirmar a exclusão definitiva da sua conta:",
|
||||
"profile_updated_successfully": "Seu perfil foi atualizado com sucesso",
|
||||
"remove_image": "Remover imagem",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup em um lugar seguro.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Escaneie o código QR abaixo com seu app autenticador.",
|
||||
"security_description": "Gerencie sua senha e outras configurações de segurança como a autenticação de dois fatores (2FA).",
|
||||
@@ -1144,10 +1125,8 @@
|
||||
"two_factor_code": "Código de Dois Fatores",
|
||||
"unlock_two_factor_authentication": "Desbloqueia a autenticação de dois fatores com um plano melhor",
|
||||
"update_personal_info": "Atualize suas informações pessoais",
|
||||
"upload_image": "Enviar imagem",
|
||||
"warning_cannot_delete_account": "Você é o único dono desta organização. Transfere a propriedade para outra pessoa primeiro.",
|
||||
"warning_cannot_undo": "Isso não pode ser desfeito",
|
||||
"you_must_select_a_file": "Você tem que selecionar um arquivo."
|
||||
"warning_cannot_undo": "Isso não pode ser desfeito"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adicione membros à equipe e determine sua função.",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "Fechar pesquisa automaticamente após",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente a pesquisa depois de um certo número de respostas.",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Feche automaticamente a pesquisa se o usuário não responder depois de alguns segundos.",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Fecha automaticamente a pesquisa no começo do dia (UTC).",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente a pesquisa como concluída após",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Liberar automaticamente a pesquisa no começo do dia (UTC).",
|
||||
"back_button_label": "Voltar",
|
||||
"background_styling": "Estilo de Fundo",
|
||||
"brand_color": "Cor da marca",
|
||||
@@ -1309,14 +1286,13 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.",
|
||||
"choose_where_to_run_the_survey": "Escolha onde realizar a pesquisa.",
|
||||
"city": "cidade",
|
||||
"close_survey_on_date": "Fechar pesquisa na data",
|
||||
"close_survey_on_response_limit": "Fechar pesquisa ao atingir limite de respostas",
|
||||
"color": "cor",
|
||||
"column_used_in_logic_error": "Esta coluna é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"columns": "colunas",
|
||||
"company": "empresa",
|
||||
"company_logo": "Logo da empresa",
|
||||
"completed_responses": "respostas parciais ou completas.",
|
||||
"completed_responses": "Respostas concluídas.",
|
||||
"concat": "Concatenar +",
|
||||
"conditional_logic": "Lógica Condicional",
|
||||
"confirm_default_language": "Confirmar idioma padrão",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "cartão de tela final",
|
||||
"ending_card": "Cartão de encerramento",
|
||||
"ending_card_used_in_logic": "Esse cartão de encerramento é usado na lógica da pergunta {questionIndex}.",
|
||||
"ending_used_in_quota": "Este final está sendo usado na cota \"{quotaName}\"",
|
||||
"ends_with": "Termina com",
|
||||
"equals": "Igual",
|
||||
"equals_one_of": "É igual a um de",
|
||||
@@ -1366,6 +1343,7 @@
|
||||
"fallback_for": "Alternativa para",
|
||||
"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}\"",
|
||||
"field_name_eg_score_price": "Nome do campo, por exemplo, pontuação, preço",
|
||||
"first_name": "Primeiro Nome",
|
||||
"five_points_recommended": "5 pontos (recomendado)",
|
||||
@@ -1397,8 +1375,9 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "Assunto do e-mail",
|
||||
"follow_ups_modal_action_to_description": "Endereço de e-mail para enviar o e-mail para",
|
||||
"follow_ups_modal_action_to_label": "Para",
|
||||
"follow_ups_modal_action_to_warning": "Nenhum campo de e-mail detectado na pesquisa",
|
||||
"follow_ups_modal_action_to_warning": "Nenhuma opção válida encontrada para envio de emails, por favor, adicione algumas perguntas de texto livre / informações de contato ou campos ocultos",
|
||||
"follow_ups_modal_create_heading": "Criar um novo acompanhamento",
|
||||
"follow_ups_modal_created_successfull_toast": "Acompanhamento criado e será salvo assim que você salvar a pesquisa.",
|
||||
"follow_ups_modal_edit_heading": "Editar este acompanhamento",
|
||||
"follow_ups_modal_edit_no_id": "Nenhum ID de acompanhamento da pesquisa fornecido, não é possível atualizar o acompanhamento da pesquisa",
|
||||
"follow_ups_modal_name_label": "Nome do acompanhamento",
|
||||
@@ -1408,8 +1387,9 @@
|
||||
"follow_ups_modal_trigger_label": "Gatilho",
|
||||
"follow_ups_modal_trigger_type_ending": "Respondente vê um final específico",
|
||||
"follow_ups_modal_trigger_type_ending_select": "Selecione os finais: ",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Nenhum final encontrado na pesquisa!",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Por favor, selecione pelo menos um encerramento ou altere o tipo de gatilho",
|
||||
"follow_ups_modal_trigger_type_response": "Respondente completa a pesquisa",
|
||||
"follow_ups_modal_updated_successfull_toast": "Acompanhamento atualizado e será salvo assim que você salvar a pesquisa.",
|
||||
"follow_ups_new": "Novo acompanhamento",
|
||||
"follow_ups_upgrade_button_text": "Atualize para habilitar os Acompanhamentos",
|
||||
"form_styling": "Estilização de Formulários",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
"question_used_in_logic": "Essa pergunta é usada na lógica da pergunta {questionIndex}.",
|
||||
"question_used_in_quota": "Esta questão está sendo usada na cota \"{quotaName}\"",
|
||||
"quotas": {
|
||||
"add_quota": "Adicionar cota",
|
||||
"change_quota_for_public_survey": "Alterar cota para pesquisa pública?",
|
||||
"confirm_quota_changes": "Confirmar Alterações nas Cotas",
|
||||
"confirm_quota_changes_body": "Você tem alterações não salvas na sua cota. Quer salvar antes de sair?",
|
||||
"continue_survey_normally": "Continuar pesquisa normalmente",
|
||||
"count_partial_submissions": "Contar respostas parciais",
|
||||
"count_partial_submissions_description": "Incluir respondentes que atendem aos critérios de cota, mas não completaram a pesquisa",
|
||||
"create_quota_for_public_survey": "Criar cota para pesquisa pública?",
|
||||
"create_quota_for_public_survey_description": "Apenas respostas futuras serão filtradas para a cota",
|
||||
"create_quota_for_public_survey_text": "Esta pesquisa já é pública. Respostas existentes não serão consideradas para a nova cota.",
|
||||
"delete_quota_confirmation_text": "Isso irá apagar permanentemente a cota {quotaName}.",
|
||||
"duplicate_quota": "Duplicar cota",
|
||||
"edit_quota": "Editar cota",
|
||||
"end_survey_for_matching_participants": "Encerrar a pesquisa para participantes correspondentes",
|
||||
"inclusion_criteria": "Critérios de Inclusão",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, other {O limite deve ser maior ou igual ao número de respostas}}",
|
||||
"limited_to_x_responses": "Limitado a {limit}",
|
||||
"new_quota": "Nova Cota",
|
||||
"quota_created_successfull_toast": "Cota criada com sucesso",
|
||||
"quota_deleted_successfull_toast": "Cota deletada com sucesso",
|
||||
"quota_duplicated_successfull_toast": "Cota duplicada com sucesso",
|
||||
"quota_name_placeholder": "ex.: Participantes de 18-25 anos",
|
||||
"quota_updated_successfull_toast": "Cota atualizada com sucesso",
|
||||
"response_limit": "Limites",
|
||||
"save_changes_confirmation_body": "Quaisquer alterações nos critérios de inclusão afetam apenas respostas futuras. \nRecomendamos duplicar uma cota existente ou criar uma nova.",
|
||||
"save_changes_confirmation_text": "Respostas existentes permanecem na cota",
|
||||
"select_ending_card": "Selecione cartão de final",
|
||||
"upgrade_prompt_title": "Use cotas com um plano superior",
|
||||
"when_quota_has_been_reached": "Quando a cota for atingida"
|
||||
},
|
||||
"randomize_all": "Randomizar tudo",
|
||||
"randomize_all_except_last": "Randomizar tudo, exceto o último",
|
||||
"range": "alcance",
|
||||
@@ -1517,7 +1529,6 @@
|
||||
"redirect_thank_you_card": "Redirecionar cartão de agradecimento",
|
||||
"redirect_to_url": "Redirecionar para URL",
|
||||
"redirect_to_url_not_available_on_free_plan": "Redirecionar para URL não está disponível no plano gratuito",
|
||||
"release_survey_on_date": "Lançar pesquisa na data",
|
||||
"remove_description": "Remover descrição",
|
||||
"remove_translations": "Remover traduções",
|
||||
"require_answer": "Preciso de Resposta",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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.",
|
||||
"variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.",
|
||||
"verify_email_before_submission": "Verifique o e-mail antes de enviar",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "Complemento",
|
||||
"an_error_occurred_deleting_the_tag": "Ocorreu um erro ao deletar a tag",
|
||||
"browser": "navegador",
|
||||
"bulk_delete_response_quotas": "As respostas fazem parte das cotas desta pesquisa. Como você quer gerenciar as cotas?",
|
||||
"city": "Cidade",
|
||||
"company": "empresa",
|
||||
"completed": "Concluído ✅",
|
||||
"country": "País",
|
||||
"decrement_quotas": "Diminua todos os limites de cotas, incluindo esta resposta",
|
||||
"delete_response_confirmation": "Isso irá excluir a resposta da pesquisa, incluindo todas as respostas, etiquetas, documentos anexados e metadados da resposta.",
|
||||
"delete_response_quotas": "A resposta faz parte das cotas desta pesquisa. Como você quer gerenciar as cotas?",
|
||||
"device": "dispositivo",
|
||||
"device_info": "Informações do dispositivo",
|
||||
"email": "Email",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "Os metadados são carregados com base no valor `lang` na URL.",
|
||||
"link_description": "Descrição do link",
|
||||
"link_description_description": "\"Descrições entre 55-200 caracteres têm um melhor desempenho.\"",
|
||||
"link_description_placeholder": "Ajude-nos a melhorar compartilhando suas opiniões.",
|
||||
"link_title": "Título do link",
|
||||
"link_title_description": "Títulos curtos têm melhor desempenho como Meta Títulos.",
|
||||
"link_title_placeholder": "Pesquisa de Feedback do Cliente",
|
||||
"preview_image": "Imagem de prévia",
|
||||
"preview_image_description": "Imagens em paisagem com tamanhos de arquivo pequenos (<4MB) têm o melhor desempenho.",
|
||||
"title": "Configurações de link"
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "Configurar alertas",
|
||||
"congrats": "Parabéns! Sua pesquisa está no ar.",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Conecte seu site ou app com o Formbricks para começar.",
|
||||
"current_count": "Contagem Atual",
|
||||
"custom_range": "Intervalo personalizado...",
|
||||
"delete_all_existing_responses_and_displays": "Excluir todas as respostas e exibições existentes",
|
||||
"download_qr_code": "baixar código QR",
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "Último mês",
|
||||
"last_quarter": "Último trimestre",
|
||||
"last_year": "Último ano",
|
||||
"limit": "Limite",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "No geral",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "falha no download do código QR",
|
||||
"qr_code_download_with_start_soon": "O download do código QR começará em breve",
|
||||
"qr_code_generation_failed": "Houve um problema ao carregar o Código QR do questionário. Por favor, tente novamente.",
|
||||
"quotas_completed": "Cotas concluídas",
|
||||
"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.",
|
||||
"selected_responses_csv": "Respostas selecionadas (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "Este trimestre",
|
||||
"this_year": "Este ano",
|
||||
"time_to_complete": "Tempo para Concluir",
|
||||
"ttc_tooltip": "Tempo médio para completar a pesquisa.",
|
||||
"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 \uD83E\uDDD8♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "Pesquisa deletada com sucesso!",
|
||||
"survey_duplicated_successfully": "Pesquisa duplicada com sucesso.",
|
||||
"survey_duplication_error": "Falha ao duplicar a pesquisa.",
|
||||
"survey_status_tooltip": "Para atualizar o status da pesquisa, atualize o cronograma e feche a configuração nas opções de resposta da pesquisa.",
|
||||
"templates": {
|
||||
"all_channels": "Todos os canais",
|
||||
"all_industries": "Todas as indústrias",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Configurações da conta",
|
||||
"action": "Ação",
|
||||
"actions": "Ações",
|
||||
"actions_description": "Ações com Código e Sem Código são usadas para acionar inquéritos de intercepção dentro de apps e em websites.",
|
||||
"active_surveys": "Inquéritos ativos",
|
||||
"activity": "Atividade",
|
||||
"add": "Adicionar",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "Adicionar filtro",
|
||||
"add_logo": "Adicionar logótipo",
|
||||
"add_member": "Adicionar membro",
|
||||
"add_new_project": "Adicionar novo projeto",
|
||||
"add_project": "Adicionar projeto",
|
||||
"add_to_team": "Adicionar à equipa",
|
||||
"all": "Todos",
|
||||
@@ -141,7 +143,6 @@
|
||||
"apply_filters": "Aplicar filtros",
|
||||
"are_you_sure": "Tem a certeza?",
|
||||
"attributes": "Atributos",
|
||||
"avatar": "Avatar",
|
||||
"back": "Voltar",
|
||||
"billing": "Faturação",
|
||||
"booked": "Reservado",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "Cancelar",
|
||||
"centered_modal": "Modal Centralizado",
|
||||
"choices": "Escolhas",
|
||||
"choose_environment": "Escolha o ambiente",
|
||||
"choose_organization": "Escolher organização",
|
||||
"choose_project": "Escolher projeto",
|
||||
"clear_all": "Limpar tudo",
|
||||
"clear_filters": "Limpar filtros",
|
||||
"clear_selection": "Limpar seleção",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "Ligar Formbricks",
|
||||
"connected": "Conectado",
|
||||
"contacts": "Contactos",
|
||||
"continue": "Continuar",
|
||||
"copied": "Copiado",
|
||||
"copied_to_clipboard": "Copiado para a área de transferência",
|
||||
"copy": "Copiar",
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"count_contacts": "{value, plural, one {# contacto} other {# contactos} }",
|
||||
"count_responses": "{value, plural, other {# respostas}}",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_project": "Criar projeto",
|
||||
"create_segment": "Criar segmento",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "Criado em",
|
||||
"created_by": "Criado por",
|
||||
"customer_success": "Sucesso do Cliente",
|
||||
"danger_zone": "Zona de Perigo",
|
||||
"dark_overlay": "Sobreposição escura",
|
||||
"date": "Data",
|
||||
"default": "Padrão",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "Comércio Eletrónico",
|
||||
"edit": "Editar",
|
||||
"email": "Email",
|
||||
"ending_card": "Cartão de encerramento",
|
||||
"enterprise_license": "Licença Enterprise",
|
||||
"environment_not_found": "Ambiente não encontrado",
|
||||
"environment_notice": "Está atualmente no ambiente {environment}.",
|
||||
"error": "Erro",
|
||||
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
|
||||
"error_component_title": "Erro ao carregar recursos",
|
||||
"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",
|
||||
"finish": "Concluir",
|
||||
"follow_these": "Siga estes",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "Etiqueta",
|
||||
"language": "Idioma",
|
||||
"learn_more": "Saiba mais",
|
||||
"license": "Licença",
|
||||
"light_overlay": "Sobreposição leve",
|
||||
"limits_reached": "Limites Atingidos",
|
||||
"link": "Link",
|
||||
"link_and_email": "Link e Email",
|
||||
"link_survey": "Ligar Inquérito",
|
||||
"link_surveys": "Ligar Inquéritos",
|
||||
"load_more": "Carregar mais",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "Associação não encontrada",
|
||||
"metadata": "Metadados",
|
||||
"minimum": "Mínimo",
|
||||
"mobile_overlay_text": "O Formbricks não está disponível para dispositivos com resoluções menores.",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor num ecrã maior. Para gerir ou criar inquéritos, mude de dispositivo.",
|
||||
"mobile_overlay_surveys_look_good": "Não se preocupe – os seus inquéritos têm uma ótima aparência em todos os dispositivos e tamanhos de ecrã!",
|
||||
"mobile_overlay_title": "Oops, ecrã pequeno detectado!",
|
||||
"move_down": "Mover para baixo",
|
||||
"move_up": "Mover para cima",
|
||||
"multiple_languages": "Várias línguas",
|
||||
"name": "Nome",
|
||||
"new": "Novo",
|
||||
"new_survey": "Novo inquérito",
|
||||
"new_version_available": "Formbricks {version} está aqui. Atualize agora!",
|
||||
"next": "Seguinte",
|
||||
"no_background_image_found": "Nenhuma imagem de fundo encontrada.",
|
||||
"no_code": "Sem código",
|
||||
"no_files_uploaded": "Nenhum ficheiro foi carregado",
|
||||
"no_quotas_found": "Nenhum quota encontrado",
|
||||
"no_result_found": "Nenhum resultado encontrado",
|
||||
"no_results": "Nenhum resultado",
|
||||
"no_surveys_found": "Nenhum inquérito encontrado.",
|
||||
@@ -284,6 +295,7 @@
|
||||
"organization": "Organização",
|
||||
"organization_id": "ID da Organização",
|
||||
"organization_not_found": "Organização não encontrada",
|
||||
"organization_settings": "Configurações da Organização",
|
||||
"organization_teams_not_found": "Equipas da organização não encontradas",
|
||||
"other": "Outro",
|
||||
"others": "Outros",
|
||||
@@ -307,6 +319,7 @@
|
||||
"product_manager": "Gestor de Produto",
|
||||
"profile": "Perfil",
|
||||
"profile_id": "ID do Perfil",
|
||||
"progress": "Progresso",
|
||||
"project_configuration": "Configuração do Projeto",
|
||||
"project_creation_description": "Organize questionários em projetos para um melhor controlo de acesso.",
|
||||
"project_id": "ID do Projeto",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "Pergunta",
|
||||
"question_id": "ID da pergunta",
|
||||
"questions": "Perguntas",
|
||||
"quota": "Quota",
|
||||
"quotas": "Quotas",
|
||||
"quotas_description": "Limitar a quantidade de respostas recebidas de participantes que atendem a certos critérios.",
|
||||
"read_docs": "Ler Documentos",
|
||||
"recipients": "Destinatários",
|
||||
"remove": "Remover",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "Guardar",
|
||||
"save_changes": "Guardar alterações",
|
||||
"saving": "Guardando",
|
||||
"scheduled": "Agendado",
|
||||
"search": "Procurar",
|
||||
"security": "Segurança",
|
||||
"segment": "Segmento",
|
||||
@@ -366,6 +381,7 @@
|
||||
"start_free_trial": "Iniciar Teste Grátis",
|
||||
"status": "Estado",
|
||||
"step_by_step_manual": "Manual passo a passo",
|
||||
"storage_not_configured": "Armazenamento de ficheiros não configurado, uploads provavelmente falharão",
|
||||
"styling": "Estilo",
|
||||
"submit": "Submeter",
|
||||
"summary": "Resumo",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "Inquérito ao vivo",
|
||||
"survey_not_found": "Inquérito não encontrado",
|
||||
"survey_paused": "Inquérito pausado.",
|
||||
"survey_scheduled": "Inquérito agendado.",
|
||||
"survey_type": "Tipo de Inquérito",
|
||||
"surveys": "Inquéritos",
|
||||
"switch_organization": "Mudar de organização",
|
||||
"switch_to": "Mudar para {environment}",
|
||||
"table_items_deleted_successfully": "{type}s eliminados com sucesso",
|
||||
"table_settings": "Configurações da tabela",
|
||||
@@ -577,8 +591,7 @@
|
||||
"contacts_table_refresh": "Atualizar contactos",
|
||||
"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.",
|
||||
"first_name": "Primeiro Nome",
|
||||
"last_name": "Apelido",
|
||||
"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.}}",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"not_provided": "Não fornecido",
|
||||
"search_contact": "Procurar contacto",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "Controlo de Acesso",
|
||||
"add_api_key": "Adicionar Chave API",
|
||||
"api_key": "Chave API",
|
||||
"api_key_copied_to_clipboard": "Chave API copiada para a área de transferência",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "Não é possível eliminar a chave API"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "Este é o URL do seu backend Formbricks.",
|
||||
"app_connection": "Ligação de Aplicação",
|
||||
"app_connection_description": "Ligue a sua aplicação ao Formbricks",
|
||||
"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.",
|
||||
"check_out_the_docs": "Consulte a documentação.",
|
||||
"dive_into_the_docs": "Mergulhe na documentação.",
|
||||
"does_your_widget_work": "O seu widget funciona?",
|
||||
"environment_id": "O seu EnvironmentId",
|
||||
"environment_id": "O Seu ID de Ambiente",
|
||||
"environment_id_description": "Este id identifica de forma única este ambiente Formbricks.",
|
||||
"environment_id_description_with_environment_id": "Usado para identificar o ambiente correto: {environmentId} é o seu.",
|
||||
"formbricks_sdk": "SDK Formbricks",
|
||||
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
|
||||
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado",
|
||||
"formbricks_sdk_not_connected_description": "Ligue o seu website ou aplicação ao Formbricks",
|
||||
"have_a_problem": "Tem um problema?",
|
||||
"how_to_setup": "Como configurar",
|
||||
"how_to_setup_description": "Siga estes passos para configurar o widget Formbricks na sua aplicação.",
|
||||
"identifying_your_users": "identificar os seus utilizadores",
|
||||
"if_you_are_planning_to": "Se está a planear",
|
||||
"insert_this_code_into_the": "Insira este código no",
|
||||
"need_a_more_detailed_setup_guide_for": "Precisa de um guia de configuração mais detalhado para",
|
||||
"not_working": "Não está a funcionar?",
|
||||
"open_an_issue_on_github": "Abrir um problema no GitHub",
|
||||
"open_the_browser_console_to_see_the_logs": "Abra a consola do navegador para ver os registos.",
|
||||
"receiving_data": "A receber dados \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Verificar novamente",
|
||||
"scroll_to_the_top": "Rolar para o topo!",
|
||||
"step_1": "Passo 1: Instalar com pnpm, npm ou yarn",
|
||||
"step_2": "Passo 2: Inicializar widget",
|
||||
"step_2_description": "Importar Formbricks e inicializar o widget no seu Componente (por exemplo, App.tsx):",
|
||||
"step_3": "Passo 3: Modo de depuração",
|
||||
"switch_on_the_debug_mode_by_appending": "Ativar o modo de depuração adicionando",
|
||||
"tag_of_your_app": "tag da sua aplicação",
|
||||
"to_the_url_where_you_load_the": "para o URL onde carrega o",
|
||||
"want_to_learn_how_to_add_user_attributes": "Quer aprender a adicionar atributos de utilizador, eventos personalizados e mais?",
|
||||
"you_are_done": "Está concluído \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "pode definir o ID do utilizador com",
|
||||
"your_app_now_communicates_with_formbricks": "A sua aplicação agora comunica com o Formbricks - enviando eventos e carregando inquéritos automaticamente!"
|
||||
"setup_alert_description": "Siga este tutorial passo-a-passo para ligar a sua aplicação ou website em menos de 5 minutos",
|
||||
"setup_alert_title": "Como conectar"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Este é o seu único projeto, não pode ser eliminado. Crie um novo projeto primeiro.",
|
||||
@@ -989,7 +977,6 @@
|
||||
"free": "Grátis",
|
||||
"free_description": "Inquéritos ilimitados, membros da equipa e mais.",
|
||||
"get_2_months_free": "Obtenha 2 meses grátis",
|
||||
"get_in_touch": "Entre em contacto",
|
||||
"hosted_in_frankfurt": "Hospedado em Frankfurt",
|
||||
"ios_android_sdks": "SDK iOS e Android para inquéritos móveis",
|
||||
"link_surveys": "Ligar Inquéritos (Partilhável)",
|
||||
@@ -1111,9 +1098,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Consequências da eliminação da conta",
|
||||
"avatar_update_failed": "Falha na atualização do avatar. Por favor, tente novamente.",
|
||||
"backup_code": "Código de Backup",
|
||||
"change_image": "Alterar imagem",
|
||||
"confirm_delete_account": "Eliminar a sua conta com todas as suas informações e dados pessoais",
|
||||
"confirm_delete_my_account": "Eliminar a Minha Conta",
|
||||
"confirm_your_current_password_to_get_started": "Confirme a sua palavra-passe atual para começar.",
|
||||
@@ -1124,17 +1109,13 @@
|
||||
"email_change_initiated": "O seu pedido de alteração de email foi iniciado.",
|
||||
"enable_two_factor_authentication": "Ativar autenticação de dois fatores",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Introduza o código da sua aplicação de autenticação abaixo.",
|
||||
"file_size_must_be_less_than_10mb": "O tamanho do ficheiro deve ser inferior a 10MB.",
|
||||
"invalid_file_type": "Tipo de ficheiro inválido. Apenas são permitidos ficheiros JPEG, PNG e WEBP.",
|
||||
"lost_access": "Perdeu o acesso",
|
||||
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
||||
"organization_identification": "Ajude a sua organização a identificá-lo no Formbricks",
|
||||
"organizations_delete_message": "É o único proprietário destas organizações, por isso <b>também serão eliminadas.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Remoção permanente de todas as suas informações e dados pessoais",
|
||||
"personal_information": "Informações pessoais",
|
||||
"please_enter_email_to_confirm_account_deletion": "Por favor, insira {email} no campo seguinte para confirmar a eliminação definitiva da sua conta:",
|
||||
"profile_updated_successfully": "O seu perfil foi atualizado com sucesso",
|
||||
"remove_image": "Remover imagem",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup num local seguro.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Digitalize o código QR abaixo com a sua aplicação de autenticação.",
|
||||
"security_description": "Gerir a sua palavra-passe e outras definições de segurança, como a autenticação de dois fatores (2FA).",
|
||||
@@ -1144,10 +1125,8 @@
|
||||
"two_factor_code": "Código de Dois Fatores",
|
||||
"unlock_two_factor_authentication": "Desbloqueie a autenticação de dois fatores com um plano superior",
|
||||
"update_personal_info": "Atualize as suas informações pessoais",
|
||||
"upload_image": "Carregar imagem",
|
||||
"warning_cannot_delete_account": "É o único proprietário desta organização. Transfira a propriedade para outro membro primeiro.",
|
||||
"warning_cannot_undo": "Isto não pode ser desfeito",
|
||||
"you_must_select_a_file": "Deve selecionar um ficheiro."
|
||||
"warning_cannot_undo": "Isto não pode ser desfeito"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adicionar membros à equipa e determinar o seu papel.",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "Fechar automaticamente o inquérito após",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Fechar automaticamente o inquérito após um certo número de respostas",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Fechar automaticamente o inquérito se o utilizador não responder após um certo número de segundos.",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Encerrar automaticamente o inquérito no início do dia (UTC).",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente o inquérito como concluído após",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Lançar automaticamente o inquérito no início do dia (UTC).",
|
||||
"back_button_label": "Rótulo do botão \"Voltar\"",
|
||||
"background_styling": "Estilo de Fundo",
|
||||
"brand_color": "Cor da marca",
|
||||
@@ -1309,14 +1286,13 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.",
|
||||
"choose_where_to_run_the_survey": "Escolha onde realizar o inquérito.",
|
||||
"city": "Cidade",
|
||||
"close_survey_on_date": "Encerrar inquérito na data",
|
||||
"close_survey_on_response_limit": "Fechar inquérito no limite de respostas",
|
||||
"color": "Cor",
|
||||
"column_used_in_logic_error": "Esta coluna é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"columns": "Colunas",
|
||||
"company": "Empresa",
|
||||
"company_logo": "Logotipo da empresa",
|
||||
"completed_responses": "respostas parciais ou completas",
|
||||
"completed_responses": "Respostas concluídas",
|
||||
"concat": "Concatenar +",
|
||||
"conditional_logic": "Lógica Condicional",
|
||||
"confirm_default_language": "Confirmar idioma padrão",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "Cartão de ecrã final",
|
||||
"ending_card": "Cartão de encerramento",
|
||||
"ending_card_used_in_logic": "Este cartão final é usado na lógica da pergunta {questionIndex}.",
|
||||
"ending_used_in_quota": "Este final está a ser usado na quota \"{quotaName}\"",
|
||||
"ends_with": "Termina com",
|
||||
"equals": "Igual",
|
||||
"equals_one_of": "Igual a um de",
|
||||
@@ -1366,6 +1343,7 @@
|
||||
"fallback_for": "Alternativa para ",
|
||||
"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}\"",
|
||||
"field_name_eg_score_price": "Nome do campo, por exemplo, pontuação, preço",
|
||||
"first_name": "Primeiro Nome",
|
||||
"five_points_recommended": "5 pontos (recomendado)",
|
||||
@@ -1397,8 +1375,9 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "Assunto do email",
|
||||
"follow_ups_modal_action_to_description": "Endereço de email para enviar o email",
|
||||
"follow_ups_modal_action_to_label": "Para",
|
||||
"follow_ups_modal_action_to_warning": "Nenhum campo de email detetado no inquérito",
|
||||
"follow_ups_modal_action_to_warning": "Não foram encontradas opções válidas para envio de emails, por favor adicione algumas perguntas de texto livre / informações de contato ou campos escondidos",
|
||||
"follow_ups_modal_create_heading": "Criar um novo acompanhamento",
|
||||
"follow_ups_modal_created_successfull_toast": "Seguimento criado e será guardado assim que guardar o questionário.",
|
||||
"follow_ups_modal_edit_heading": "Editar este acompanhamento",
|
||||
"follow_ups_modal_edit_no_id": "Nenhum ID de acompanhamento do inquérito fornecido, não é possível atualizar o acompanhamento do inquérito",
|
||||
"follow_ups_modal_name_label": "Nome do acompanhamento",
|
||||
@@ -1408,8 +1387,9 @@
|
||||
"follow_ups_modal_trigger_label": "Desencadeador",
|
||||
"follow_ups_modal_trigger_type_ending": "O respondente vê um final específico",
|
||||
"follow_ups_modal_trigger_type_ending_select": "Selecionar finais: ",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Não foram encontrados finais no inquérito!",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Por favor, selecione pelo menos um final ou mude o tipo de gatilho",
|
||||
"follow_ups_modal_trigger_type_response": "Respondente conclui inquérito",
|
||||
"follow_ups_modal_updated_successfull_toast": "Seguimento atualizado e será guardado assim que guardar o questionário.",
|
||||
"follow_ups_new": "Novo acompanhamento",
|
||||
"follow_ups_upgrade_button_text": "Atualize para ativar os acompanhamentos",
|
||||
"form_styling": "Estilo do formulário",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
"question_used_in_logic": "Esta pergunta é usada na lógica da pergunta {questionIndex}.",
|
||||
"question_used_in_quota": "Esta pergunta está a ser usada na quota \"{quotaName}\"",
|
||||
"quotas": {
|
||||
"add_quota": "Adicionar quota",
|
||||
"change_quota_for_public_survey": "Alterar quota para inquérito público?",
|
||||
"confirm_quota_changes": "Confirmar Alterações das Quotas",
|
||||
"confirm_quota_changes_body": "Tem alterações não guardadas na sua cota. Gostaria de as guardar antes de sair?",
|
||||
"continue_survey_normally": "Continua a pesquisa normalmente",
|
||||
"count_partial_submissions": "Contar submissões parciais",
|
||||
"count_partial_submissions_description": "Incluir respondentes que correspondem aos critérios de quota mas não completaram o inquérito",
|
||||
"create_quota_for_public_survey": "Criar quota para inquérito público?",
|
||||
"create_quota_for_public_survey_description": "Apenas respostas futuras serão controladas no limite",
|
||||
"create_quota_for_public_survey_text": "Este questionário já é público. As respostas existentes não serão consideradas na nova quota.",
|
||||
"delete_quota_confirmation_text": "Isto irá apagar permanentemente a quota {quotaName}.",
|
||||
"duplicate_quota": "Duplicar quota",
|
||||
"edit_quota": "Editar cota",
|
||||
"end_survey_for_matching_participants": "Encerrar inquérito para participantes correspondentes",
|
||||
"inclusion_criteria": "Critérios de Inclusão",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, other {Limite deve ser maior ou igual ao número de respostas}}",
|
||||
"limited_to_x_responses": "Limitado a {limit}",
|
||||
"new_quota": "Nova Cota",
|
||||
"quota_created_successfull_toast": "Quota criada com sucesso",
|
||||
"quota_deleted_successfull_toast": "Quota eliminada com sucesso",
|
||||
"quota_duplicated_successfull_toast": "Quota duplicada com sucesso",
|
||||
"quota_name_placeholder": "por exemplo, Participantes Idade 18-25",
|
||||
"quota_updated_successfull_toast": "Quota atualizada com sucesso",
|
||||
"response_limit": "Limites",
|
||||
"save_changes_confirmation_body": "Quaisquer alterações aos critérios de inclusão afetam apenas respostas futuras. \nRecomendamos duplicar uma cota existente ou criar uma nova.",
|
||||
"save_changes_confirmation_text": "As respostas existentes permanecem na cota",
|
||||
"select_ending_card": "Selecionar cartão de encerramento",
|
||||
"upgrade_prompt_title": "Utilize quotas com um plano superior",
|
||||
"when_quota_has_been_reached": "Quando a quota foi atingida"
|
||||
},
|
||||
"randomize_all": "Aleatorizar todos",
|
||||
"randomize_all_except_last": "Aleatorizar todos exceto o último",
|
||||
"range": "Intervalo",
|
||||
@@ -1517,7 +1529,6 @@
|
||||
"redirect_thank_you_card": "Redirecionar cartão de agradecimento",
|
||||
"redirect_to_url": "Redirecionar para Url",
|
||||
"redirect_to_url_not_available_on_free_plan": "Redirecionar para URL não está disponível no plano gratuito",
|
||||
"release_survey_on_date": "Lançar inquérito na data",
|
||||
"remove_description": "Remover descrição",
|
||||
"remove_translations": "Remover traduções",
|
||||
"require_answer": "Exigir Resposta",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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.",
|
||||
"variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.",
|
||||
"verify_email_before_submission": "Verificar email antes da submissão",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "Endereço Linha 2",
|
||||
"an_error_occurred_deleting_the_tag": "Ocorreu um erro ao eliminar a etiqueta",
|
||||
"browser": "Navegador",
|
||||
"bulk_delete_response_quotas": "As respostas são parte das quotas deste inquérito. Como deseja gerir as quotas?",
|
||||
"city": "Cidade",
|
||||
"company": "Empresa",
|
||||
"completed": "Concluído ✅",
|
||||
"country": "País",
|
||||
"decrement_quotas": "Decrementar todos os limites das cotas incluindo esta resposta",
|
||||
"delete_response_confirmation": "Isto irá apagar a resposta do inquérito, incluindo todas as respostas, etiquetas, documentos anexos e metadados da resposta.",
|
||||
"delete_response_quotas": "A resposta faz parte das quotas deste inquérito. Como deseja gerir as quotas?",
|
||||
"device": "Dispositivo",
|
||||
"device_info": "Informações do dispositivo",
|
||||
"email": "Email",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "Os metadados são carregados com base no valor `lang` no URL.",
|
||||
"link_description": "Descrição do link",
|
||||
"link_description_description": "Descrições entre 55 a 200 caracteres têm melhor desempenho.",
|
||||
"link_description_placeholder": "Ajude-nos a melhorar compartilhando suas opiniões.",
|
||||
"link_title": "Título do Link",
|
||||
"link_title_description": "Títulos curtos têm melhor desempenho como Meta Titles.",
|
||||
"link_title_placeholder": "Inquérito de Feedback do Cliente",
|
||||
"preview_image": "Pré-visualização da imagem",
|
||||
"preview_image_description": "Imagens de paisagem com tamanhos pequenos (<4MB) apresentam melhor desempenho.",
|
||||
"title": "Definições de ligação"
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "Configurar alertas",
|
||||
"congrats": "Parabéns! O seu inquérito está ativo.",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Ligue o seu website ou aplicação ao Formbricks para começar.",
|
||||
"current_count": "Contagem atual",
|
||||
"custom_range": "Intervalo personalizado...",
|
||||
"delete_all_existing_responses_and_displays": "Excluir todas as respostas existentes e exibições",
|
||||
"download_qr_code": "Transferir código QR",
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "Último mês",
|
||||
"last_quarter": "Último trimestre",
|
||||
"last_year": "Ano passado",
|
||||
"limit": "Limite",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "Geral",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "Falha ao transferir o código QR",
|
||||
"qr_code_download_with_start_soon": "O download do código QR começará em breve",
|
||||
"qr_code_generation_failed": "Ocorreu um problema ao carregar o Código QR do questionário. Por favor, tente novamente.",
|
||||
"quotas_completed": "Quotas concluídas",
|
||||
"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.",
|
||||
"selected_responses_csv": "Respostas selecionadas (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "Este trimestre",
|
||||
"this_year": "Este ano",
|
||||
"time_to_complete": "Tempo para Concluir",
|
||||
"ttc_tooltip": "Tempo médio para concluir o inquérito.",
|
||||
"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 \uD83E\uDDD8♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "Inquérito eliminado com sucesso!",
|
||||
"survey_duplicated_successfully": "Inquérito duplicado com sucesso.",
|
||||
"survey_duplication_error": "Falha ao duplicar o inquérito.",
|
||||
"survey_status_tooltip": "Para atualizar o estado do inquérito, atualize o agendamento e feche a configuração nas opções de resposta do inquérito.",
|
||||
"templates": {
|
||||
"all_channels": "Todos os canais",
|
||||
"all_industries": "Todas as indústrias",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"auth": {
|
||||
"continue_with_azure": "Continuă cu Microsoft",
|
||||
"continue_with_email": "Continuă cu Email",
|
||||
"continue_with_email": "Continuă cu email",
|
||||
"continue_with_github": "Continuă cu GitHub",
|
||||
"continue_with_google": "Continuă cu Google",
|
||||
"continue_with_oidc": "Continuă cu {oidcDisplayName}",
|
||||
@@ -49,7 +49,7 @@
|
||||
"invite_not_found": "Invitația nu a fost găsită \uD83D\uDE25",
|
||||
"invite_not_found_description": "Codul de invitație nu poate fi găsit sau a fost deja utilizat.",
|
||||
"login": "Autentificare",
|
||||
"welcome_to_organization": "Ești în \uD83C\uDF89",
|
||||
"welcome_to_organization": "Ai fost acceptat \uD83C\uDF89",
|
||||
"welcome_to_organization_description": "Bun venit în organizație."
|
||||
},
|
||||
"last_used": "Ultima utilizare",
|
||||
@@ -65,7 +65,7 @@
|
||||
"new_to_formbricks": "Nou în Formbricks?",
|
||||
"use_a_backup_code": "Folosiți un cod de rezervă"
|
||||
},
|
||||
"saml_connection_error": "Ceva a mers prost. Vă rugăm să verificați consola aplicației pentru mai multe detalii.",
|
||||
"saml_connection_error": "Ceva nu a mers. Vă rugăm să verificați consola aplicației pentru mai multe detalii.",
|
||||
"signup": {
|
||||
"captcha_failed": "Captcha eșuat",
|
||||
"have_an_account": "Ai un cont?",
|
||||
@@ -73,16 +73,16 @@
|
||||
"password_validation_contain_at_least_1_number": "Conține cel puțin 1 număr",
|
||||
"password_validation_minimum_8_and_maximum_128_characters": "Minim 8 & Maxim 128 caractere",
|
||||
"password_validation_uppercase_and_lowercase": "Amestec de majuscule și minuscule",
|
||||
"please_verify_captcha": "Vă rugăm să verificați reCAPTCHA",
|
||||
"privacy_policy": "Politica de Confidențialitate",
|
||||
"terms_of_service": "Termeni de Serviciu",
|
||||
"please_verify_captcha": "Vă rugăm să verificați CAPTCHA",
|
||||
"privacy_policy": "Politica de confidențialitate",
|
||||
"terms_of_service": "Termeni de utilizare a serviciului",
|
||||
"title": "Creați-vă contul Formbricks"
|
||||
},
|
||||
"signup_without_verification_success": {
|
||||
"user_successfully_created": "Utilizator creat cu succes",
|
||||
"user_successfully_created_info": "Am verificat pentru un cont asociat cu {email}. Dacă nu a existat niciunul, am creat unul pentru tine. Dacă un cont deja exista, nu s-au făcut modificări. Vă rugăm să vă conectați mai jos pentru a continua."
|
||||
},
|
||||
"testimonial_1": "\"Măsurăm claritatea documentațiilor noastre și învățăm din pierderi în folosirea aceleași platforme. Produs grozav, echipă foarte receptivă!\"",
|
||||
"testimonial_1": "\"Măsurăm claritatea documentațiilor noastre și învățăm din greșeli în folosirea platformei. Produs grozav, echipă foarte receptivă!\"",
|
||||
"testimonial_all_features_included": "Toate funcționalitățile incluse",
|
||||
"testimonial_free_and_open_source": "Gratuit și open-source",
|
||||
"testimonial_no_credit_card_required": "Nu este necesar niciun card de credit",
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Setări cont",
|
||||
"action": "Acțiune",
|
||||
"actions": "Acțiuni",
|
||||
"actions_description": "Acțiunile Cod și No-Code sunt utilizate pentru a declanșa chestionare de interceptare în aplicații și pe site-uri web.",
|
||||
"active_surveys": "Sondaje active",
|
||||
"activity": "Activitate",
|
||||
"add": "Adaugă",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "Adăugați filtru",
|
||||
"add_logo": "Adaugă logo",
|
||||
"add_member": "Adaugă membru",
|
||||
"add_new_project": "Adaugă proiect nou",
|
||||
"add_project": "Adaugă proiect",
|
||||
"add_to_team": "Adaugă la echipă",
|
||||
"all": "Toate",
|
||||
@@ -137,11 +139,10 @@
|
||||
"anonymous": "Anonim",
|
||||
"api_keys": "Chei API",
|
||||
"app": "Aplicație",
|
||||
"app_survey": "Sondaj Aplicație",
|
||||
"app_survey": "Sondaj aplicație",
|
||||
"apply_filters": "Aplică filtre",
|
||||
"are_you_sure": "Ești sigur?",
|
||||
"attributes": "Atribute",
|
||||
"avatar": "Avatar",
|
||||
"back": "Înapoi",
|
||||
"billing": "Facturare",
|
||||
"booked": "Rezervat",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "Anulare",
|
||||
"centered_modal": "Modală centralizată",
|
||||
"choices": "Alegeri",
|
||||
"choose_environment": "Alege mediul",
|
||||
"choose_organization": "Alege organizația",
|
||||
"choose_project": "Alege proiectul",
|
||||
"clear_all": "Șterge tot",
|
||||
"clear_filters": "Curăță filtrele",
|
||||
"clear_selection": "Șterge selecția",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "Conectează Formbricks",
|
||||
"connected": "Conectat",
|
||||
"contacts": "Contacte",
|
||||
"continue": "Continuă",
|
||||
"copied": "Copiat",
|
||||
"copied_to_clipboard": "Copiat în clipboard",
|
||||
"copy": "Copiază",
|
||||
"copy_code": "Copiază codul",
|
||||
"copy_link": "Copiază legătura",
|
||||
"count_contacts": "{value, plural, one {# contact} other {# contacte} }",
|
||||
"count_responses": "{value, plural, one {# răspuns} other {# răspunsuri} }",
|
||||
"create_new_organization": "Creează organizație nouă",
|
||||
"create_project": "Creează proiect",
|
||||
"create_segment": "Creați segment",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "Creat la",
|
||||
"created_by": "Creat de",
|
||||
"customer_success": "Succesul Clientului",
|
||||
"danger_zone": "Zonă periculoasă",
|
||||
"dark_overlay": "Suprapunere întunecată",
|
||||
"date": "Dată",
|
||||
"default": "Implicit",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "Comerț electronic",
|
||||
"edit": "Editare",
|
||||
"email": "Email",
|
||||
"ending_card": "Cardul de finalizare",
|
||||
"enterprise_license": "Licență Întreprindere",
|
||||
"environment_not_found": "Mediul nu a fost găsit",
|
||||
"environment_notice": "Te afli în prezent în mediul {environment}",
|
||||
"error": "Eroare",
|
||||
"error_component_description": "Această resursă nu există sau nu aveți drepturile necesare pentru a o accesa.",
|
||||
"error_component_title": "Eroare la încărcarea resurselor",
|
||||
"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",
|
||||
"finish": "Finalizează",
|
||||
"follow_these": "Urmați acestea",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "Etichetă",
|
||||
"language": "Limba",
|
||||
"learn_more": "Află mai multe",
|
||||
"license": "Licență",
|
||||
"light_overlay": "Suprapunere ușoară",
|
||||
"limits_reached": "Limite atinse",
|
||||
"link": "Legătura",
|
||||
"link_and_email": "Link & email",
|
||||
"link_survey": "Conectează chestionarul",
|
||||
"link_surveys": "Conectează chestionarele",
|
||||
"load_more": "Încarcă mai multe",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "Apartenența nu a fost găsită",
|
||||
"metadata": "Metadate",
|
||||
"minimum": "Minim",
|
||||
"mobile_overlay_text": "Formbricks nu este disponibil pentru dispozitive cu rezoluții mai mici.",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funcționează cel mai bine pe un ecran mai mare. Pentru a gestiona sau crea chestionare, treceți la un alt dispozitiv.",
|
||||
"mobile_overlay_surveys_look_good": "Nu vă faceți griji – chestionarele dumneavoastră arată grozav pe orice dispozitiv și dimensiune a ecranului!",
|
||||
"mobile_overlay_title": "Ups, ecran mic detectat!",
|
||||
"move_down": "Mută în jos",
|
||||
"move_up": "Mută sus",
|
||||
"multiple_languages": "Mai multe limbi",
|
||||
"name": "Nume",
|
||||
"new": "Nou",
|
||||
"new_survey": "Chestionar Nou",
|
||||
"new_version_available": "Formbricks {version} este disponibil. Actualizați acum!",
|
||||
"next": "Următorul",
|
||||
"no_background_image_found": "Nu a fost găsită nicio imagine de fundal.",
|
||||
"no_code": "Fără Cod",
|
||||
"no_files_uploaded": "Nu au fost încărcate fișiere",
|
||||
"no_quotas_found": "Nicio cotă găsită",
|
||||
"no_result_found": "Niciun rezultat găsit",
|
||||
"no_results": "Nicio rezultat",
|
||||
"no_surveys_found": "Nu au fost găsite sondaje.",
|
||||
@@ -283,13 +294,14 @@
|
||||
"or": "sau",
|
||||
"organization": "Organizație",
|
||||
"organization_id": "ID Organizație",
|
||||
"organization_not_found": "Organizație nu a fost găsită",
|
||||
"organization_not_found": "Organizația nu a fost găsită",
|
||||
"organization_settings": "Setări Organizație",
|
||||
"organization_teams_not_found": "Echipele organizației nu au fost găsite",
|
||||
"other": "Altele",
|
||||
"others": "Altele",
|
||||
"overview": "Prezentare generală",
|
||||
"password": "Parolă",
|
||||
"paused": "Pauzat",
|
||||
"paused": "Pauză",
|
||||
"pending_downgrade": "Reducere în aşteptare",
|
||||
"people_manager": "Manager de persoane",
|
||||
"person": "Persoană",
|
||||
@@ -307,7 +319,8 @@
|
||||
"product_manager": "Manager de Produs",
|
||||
"profile": "Profil",
|
||||
"profile_id": "ID Profil",
|
||||
"project_configuration": "Configurarea Proiectului",
|
||||
"progress": "Progres",
|
||||
"project_configuration": "Configurare proiect",
|
||||
"project_creation_description": "Organizați sondajele în proiecte pentru un control mai bun al accesului.",
|
||||
"project_id": "ID proiect",
|
||||
"project_name": "Nume proiect",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "Întrebare",
|
||||
"question_id": "ID întrebare",
|
||||
"questions": "Întrebări",
|
||||
"quota": "Cotă",
|
||||
"quotas": "Cote",
|
||||
"quotas_description": "Limitați numărul de răspunsuri primite de la participanții care îndeplinesc anumite criterii.",
|
||||
"read_docs": "Citește documentația",
|
||||
"recipients": "Destinatari",
|
||||
"remove": "Șterge",
|
||||
@@ -325,7 +341,7 @@
|
||||
"report_survey": "Raportează chestionarul",
|
||||
"request_pricing": "Solicită Prețuri",
|
||||
"request_trial_license": "Solicitați o licență de încercare",
|
||||
"reset_to_default": "Revină la implicit",
|
||||
"reset_to_default": "Revino la implicit",
|
||||
"response": "Răspuns",
|
||||
"responses": "Răspunsuri",
|
||||
"restart": "Repornește",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "Salvează",
|
||||
"save_changes": "Salvează modificările",
|
||||
"saving": "Salvare",
|
||||
"scheduled": "Programat",
|
||||
"search": "Căutare",
|
||||
"security": "Securitate",
|
||||
"segment": "Segment",
|
||||
@@ -355,7 +370,7 @@
|
||||
"share_feedback": "Împărtășește feedback",
|
||||
"show": "Afișează",
|
||||
"show_response_count": "Afișează numărul de răspunsuri",
|
||||
"shown": "Arătat",
|
||||
"shown": "Afișat",
|
||||
"size": "Mărime",
|
||||
"skipped": "Sărit",
|
||||
"skips": "Salturi",
|
||||
@@ -363,9 +378,10 @@
|
||||
"something_went_wrong": "Ceva nu a mers bine",
|
||||
"something_went_wrong_please_try_again": "Ceva nu a mers bine. Vă rugăm să încercați din nou.",
|
||||
"sort_by": "Sortare după",
|
||||
"start_free_trial": "Începe Perioada de Testare Gratuită",
|
||||
"start_free_trial": "Începe perioada de testare gratuită",
|
||||
"status": "Stare",
|
||||
"step_by_step_manual": "Manual pas cu pas",
|
||||
"storage_not_configured": "Stocarea fișierelor neconfigurată, upload-urile vor eșua probabil",
|
||||
"styling": "Stilizare",
|
||||
"submit": "Trimite",
|
||||
"summary": "Sumar",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "Chestionar activ",
|
||||
"survey_not_found": "Sondajul nu a fost găsit",
|
||||
"survey_paused": "Chestionar oprit.",
|
||||
"survey_scheduled": "Chestionar programat.",
|
||||
"survey_type": "Tip Chestionar",
|
||||
"surveys": "Sondaje",
|
||||
"switch_organization": "Comută organizația",
|
||||
"switch_to": "Comută la {environment}",
|
||||
"table_items_deleted_successfully": "\"{type} șterse cu succes\"",
|
||||
"table_settings": "Setări tabel",
|
||||
@@ -434,15 +448,15 @@
|
||||
"click_or_drag_to_upload_files": "Faceți clic sau trageți pentru a încărca fișiere.",
|
||||
"email_customization_preview_email_heading": "Salut {userName}",
|
||||
"email_customization_preview_email_subject": "Previzualizare Personalizare Email Formbricks",
|
||||
"email_customization_preview_email_text": "Acesta este un previzualizare a e-mailului pentru a vă arăta ce logo va fi afișat în e-mailurile.",
|
||||
"email_customization_preview_email_text": "Acesta este o previzualizare a emailului pentru a vă arăta ce logo va fi afișat în emailurile viitoare.",
|
||||
"email_footer_text_1": "O zi minunată!",
|
||||
"email_footer_text_2": "Echipa Formbricks",
|
||||
"email_template_text_1": "Acest email a fost trimis prin Formbricks.",
|
||||
"embed_survey_preview_email_didnt_request": "Nu ați solicitat asta?",
|
||||
"embed_survey_preview_email_environment_id": "ID de mediu",
|
||||
"embed_survey_preview_email_fight_spam": "Ajută-ne să combatem spam-ul și trimite acest e-mail la hola@formbricks.com",
|
||||
"embed_survey_preview_email_heading": "Previzualizare Incorporare Email",
|
||||
"embed_survey_preview_email_subject": "Previzualizare Chestionar Email Formbricks",
|
||||
"embed_survey_preview_email_heading": "Previzualizare încorporare email",
|
||||
"embed_survey_preview_email_subject": "Previzualizare chestionar email Formbricks",
|
||||
"embed_survey_preview_email_text": "Așa arată fragmentul de cod încorporat într-un email:",
|
||||
"forgot_password_email_change_password": "Schimbați parola",
|
||||
"forgot_password_email_did_not_request": "Dacă nu ați solicitat acest lucru, vă rugăm să ignorați acest email.",
|
||||
@@ -464,7 +478,7 @@
|
||||
"password_changed_email_heading": "Parola modificată",
|
||||
"password_changed_email_text": "Parola dumneavoastră a fost schimbată cu succes.",
|
||||
"password_reset_notify_email_subject": "Parola dumneavoastră Formbricks a fost schimbată",
|
||||
"privacy_policy": "Politica de Confidențialitate",
|
||||
"privacy_policy": "Politica de confidențialitate",
|
||||
"reject": "Respinge",
|
||||
"render_email_response_value_file_upload_response_link_not_included": "Linkul către fișierul încărcat nu este inclus din motive de confidențialitate a datelor",
|
||||
"response_finished_email_subject": "Un răspuns pentru {surveyName} a fost finalizat ✅",
|
||||
@@ -492,7 +506,7 @@
|
||||
"verification_email_to_fill_survey": "Pentru a completa sondajul, vă rugăm să faceți clic pe butonul de mai jos:",
|
||||
"verification_email_verify_email": "Verifică emailul",
|
||||
"verification_new_email_subject": "Verificare schimbare email",
|
||||
"verification_security_notice": "Dacă nu ați cerut această modificare a e-mailului, vă rugăm să ignorați acest e-mail sau să contactați suportul imediat.",
|
||||
"verification_security_notice": "Dacă nu ați cerut această modificare a emailului, vă rugăm să ignorați acest email sau să contactați suportul imediat.",
|
||||
"verified_link_survey_email_subject": "Chestionarul tău este gata să fie completat."
|
||||
},
|
||||
"environments": {
|
||||
@@ -501,7 +515,7 @@
|
||||
"action_copy_failed": "Copierea acțiunii a eșuat",
|
||||
"action_created_successfully": "Acțiune creată cu succes",
|
||||
"action_deleted_successfully": "Acțiune ștearsă cu succes.",
|
||||
"action_type": "Tip Acțiune",
|
||||
"action_type": "Tip acțiune",
|
||||
"action_updated_successfully": "Acțiune actualizată cu succes",
|
||||
"action_with_key_already_exists": "Acțiunea cu cheia {key} există deja",
|
||||
"action_with_name_already_exists": "Acțiunea cu numele {name} există deja",
|
||||
@@ -564,7 +578,7 @@
|
||||
"congrats": "Felicitări!",
|
||||
"connection_successful_message": "Bravo! Suntem conectați.",
|
||||
"do_it_later": "Am să o fac mai târziu",
|
||||
"finish_onboarding": "Încheie Înregistrarea",
|
||||
"finish_onboarding": "Încheie înregistrarea",
|
||||
"headline": "Conectați aplicația sau site-ul dvs.",
|
||||
"import_formbricks_and_initialize_the_widget_in_your_component": "Importați Formbricks și inițializați widgetul în componenta dumneavoastră (de exemplu, App.tsx):",
|
||||
"insert_this_code_into_the_head_tag_of_your_website": "Introduceți acest cod în eticheta head a site-ului dvs.:",
|
||||
@@ -577,12 +591,11 @@
|
||||
"contacts_table_refresh": "Reîmprospătare contacte",
|
||||
"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.",
|
||||
"first_name": "Prenume",
|
||||
"last_name": "Nume de familie",
|
||||
"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.} }",
|
||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||
"not_provided": "Neprovidat",
|
||||
"not_provided": "Nu a fost furnizat",
|
||||
"search_contact": "Căutați contact",
|
||||
"select_attribute": "Selectează Atributul",
|
||||
"select_attribute": "Selectează atributul",
|
||||
"unlock_contacts_description": "Gestionează contactele și trimite sondaje țintite",
|
||||
"unlock_contacts_title": "Deblocați contactele cu un plan superior.",
|
||||
"upload_contacts_modal_attributes_description": "Mapează coloanele din CSV-ul tău la atributele din Formbricks.",
|
||||
@@ -629,7 +642,7 @@
|
||||
"connected_with_email": "Conectat cu {email}",
|
||||
"connecting_integration_failed_please_try_again": "Conectarea integrării a eșuat. Vă rugăm să încercați din nou!",
|
||||
"create_survey_warning": "Trebuie să creezi un sondaj pentru a putea configura această integrare",
|
||||
"delete_integration": "Șterge Integrarea",
|
||||
"delete_integration": "Șterge integrarea",
|
||||
"delete_integration_confirmation": "Sigur doriți să ștergeți această integrare?",
|
||||
"google_sheet_integration_description": "Completați instantaneu foile de calcul cu datele chestionarului",
|
||||
"google_sheets": {
|
||||
@@ -647,7 +660,7 @@
|
||||
"no_integrations_yet": "Integrațiile tale Google Sheet vor apărea aici de îndată ce le vei adăuga. ⏲️",
|
||||
"spreadsheet_url": "URL foaie de calcul"
|
||||
},
|
||||
"include_created_at": "Include Data Creării",
|
||||
"include_created_at": "Include data creării",
|
||||
"include_hidden_fields": "Include câmpuri ascunse",
|
||||
"include_metadata": "Includere Metadata (Browser, Țară, etc.)",
|
||||
"include_variables": "Include Variabile",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "Control acces",
|
||||
"add_api_key": "Adaugă Cheie API",
|
||||
"api_key": "Cheie API",
|
||||
"api_key_copied_to_clipboard": "Cheia API a fost copiată în clipboard",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "Imposibil de șters cheia API"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "Acesta este URL-ul backend-ului tău Formbricks.",
|
||||
"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",
|
||||
"check_out_the_docs": "Consultați documentația.",
|
||||
"dive_into_the_docs": "Accesați documentația.",
|
||||
"does_your_widget_work": "Funcționează widgetul dvs.?",
|
||||
"environment_id": "ID-ul Mediului Dvs.",
|
||||
"environment_id": "ID-ul mediului tău",
|
||||
"environment_id_description": "Acest id identifică în mod unic acest mediu Formbricks.",
|
||||
"environment_id_description_with_environment_id": "Folosit pentru a identifica mediul corect: {environmentId} este al tău.",
|
||||
"formbricks_sdk": "SDK 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",
|
||||
"have_a_problem": "Aveți o problemă?",
|
||||
"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.",
|
||||
"identifying_your_users": "identificarea utilizatorilor tăi",
|
||||
"if_you_are_planning_to": "Dacă planifici să",
|
||||
"insert_this_code_into_the": "Insereză acest cod în",
|
||||
"need_a_more_detailed_setup_guide_for": "Aveți nevoie de un ghid de configurare mai detaliat pentru",
|
||||
"not_working": "Nu funcționează?",
|
||||
"open_an_issue_on_github": "Deschideți o problemă pe GitHub",
|
||||
"open_the_browser_console_to_see_the_logs": "Deschide consola browserului pentru a vedea jurnalele.",
|
||||
"receiving_data": "Recepționare date \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-verifică",
|
||||
"scroll_to_the_top": "Derulați în partea de sus!",
|
||||
"step_1": "Pasul 1: Instalează cu pnpm, npm sau yarn",
|
||||
"step_2": "Pasul 2: Inițializează widget-ul",
|
||||
"step_2_description": "Importați Formbricks și inițializați widgetul în componenta dumneavoastră (de exemplu, App.tsx):",
|
||||
"step_3": "Pasul 3: Modul de depanare",
|
||||
"switch_on_the_debug_mode_by_appending": "Activează modul de depanare prin adăugare",
|
||||
"tag_of_your_app": "eticheta aplicației tale",
|
||||
"to_the_url_where_you_load_the": "la adresa URL de unde încarci",
|
||||
"want_to_learn_how_to_add_user_attributes": "Doriți să aflați cum să adăugați atribute ale utilizatorului, evenimente personalizate și altele?",
|
||||
"you_are_done": "Ai terminat \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "poți seta ID-ul utilizatorului cu",
|
||||
"your_app_now_communicates_with_formbricks": "Aplicația ta comunică acum cu Formbricks - trimite evenimente și încarcă automat sondajele!"
|
||||
"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"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "Acesta este singurul tău proiect, nu poate fi șters. Creează mai întâi un proiect nou.",
|
||||
@@ -971,52 +959,51 @@
|
||||
"all_integrations": "Toate integrațiile",
|
||||
"annually": "Anual",
|
||||
"api_webhooks": "API & Webhook-uri",
|
||||
"app_surveys": "Sondaje de Aplicație",
|
||||
"app_surveys": "Sondaje în aplicație",
|
||||
"attribute_based_targeting": "Targetare bazată pe atribute",
|
||||
"current": "Curent",
|
||||
"current_plan": "Plan curent",
|
||||
"current_tier_limit": "Limită curentă a nivelului",
|
||||
"custom": "Personalizat & Scalare",
|
||||
"custom_contacts_limit": "Limit Personalizat Contacte",
|
||||
"custom_contacts_limit": "Limită personalizată contacte",
|
||||
"custom_project_limit": "Limit Personalizat Proiect",
|
||||
"custom_response_limit": "Limit Personalizat Răspunsuri",
|
||||
"email_embedded_surveys": "Sondaje încorporate în email",
|
||||
"email_follow_ups": "Urmăriri Email",
|
||||
"email_follow_ups": "Email follow-up",
|
||||
"enterprise_description": "Suport Premium și limite personalizate.",
|
||||
"everybody_has_the_free_plan_by_default": "Toată lumea are planul gratuit implicit!",
|
||||
"everything_in_free": "Totul în Gratuit",
|
||||
"everything_in_startup": "Totul în Startup",
|
||||
"free": "Gratuit",
|
||||
"free_description": "Sondaje Nelimitate, Membri În Echipă și altele.",
|
||||
"free_description": "Sondaje nelimitate, membri în echipă și altele.",
|
||||
"get_2_months_free": "Primește 2 luni gratuite",
|
||||
"get_in_touch": "Contactați-ne",
|
||||
"hosted_in_frankfurt": "Găzduit în Frankfurt",
|
||||
"ios_android_sdks": "SDK iOS & Android pentru sondaje mobile",
|
||||
"link_surveys": "Sondaje Link (Distribuibil)",
|
||||
"logic_jumps_hidden_fields_recurring_surveys": "Salturi Logice, Câmpuri Ascunse, Sondaje Recurente, etc.",
|
||||
"manage_card_details": "Gestionați Detaliile Cardului",
|
||||
"manage_subscription": "Gestionați Abonamentul",
|
||||
"manage_card_details": "Gestionați detaliile cardului",
|
||||
"manage_subscription": "Gestionați abonamentul",
|
||||
"monthly": "Lunar",
|
||||
"monthly_identified_users": "Utilizatori Identificați Lunar",
|
||||
"monthly_identified_users": "Utilizatori identificați lunar",
|
||||
"per_month": "pe lună",
|
||||
"per_year": "pe an",
|
||||
"plan_upgraded_successfully": "Planul a fost upgradat cu succes",
|
||||
"premium_support_with_slas": "Suport premium cu SLA-uri",
|
||||
"remove_branding": "Eliminare Branding",
|
||||
"remove_branding": "Eliminare branding",
|
||||
"startup": "Pornire",
|
||||
"startup_description": "Totul din versiunea gratuită cu funcții suplimentare.",
|
||||
"switch_plan": "Schimbă Planul",
|
||||
"switch_plan": "Schimbă planul",
|
||||
"switch_plan_confirmation_text": "Sigur doriți să treceți la planul {plan}? Vi se va percepe {price} {period}.",
|
||||
"team_access_roles": "Roluri Acces Echipă",
|
||||
"team_access_roles": "Roluri acces echipă",
|
||||
"unable_to_upgrade_plan": "Nu se poate upgrada planul",
|
||||
"unlimited_miu": "MIU Nelimitat",
|
||||
"unlimited_projects": "Proiecte Nelimitate",
|
||||
"unlimited_projects": "Proiecte nelimitate",
|
||||
"unlimited_responses": "Răspunsuri nelimitate",
|
||||
"unlimited_surveys": "Sondaje Nelimitate",
|
||||
"unlimited_team_members": "Membri Nelimitați În Echipă",
|
||||
"unlimited_surveys": "Sondaje nelimitate",
|
||||
"unlimited_team_members": "Membri nelimitați în echipă",
|
||||
"upgrade": "Actualizare",
|
||||
"uptime_sla_99": "Disponibilitate SLA (99%)",
|
||||
"website_surveys": "Sondaje ale Site-ului"
|
||||
"website_surveys": "Sondaje ale site-ului"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Jurnale de audit",
|
||||
@@ -1048,11 +1035,11 @@
|
||||
"create_new_organization_description": "Creați o organizație nouă pentru a gestiona un alt set de proiecte.",
|
||||
"customize_email_with_a_higher_plan": "Personalizați emailul cu un plan superior",
|
||||
"delete_member_confirmation": "Membrii șterși vor pierde accesul la toate proiectele și sondajele organizației tale.",
|
||||
"delete_organization": "Șterge Organizație",
|
||||
"delete_organization": "Șterge organizație",
|
||||
"delete_organization_description": "Șterge organizația cu toate proiectele ei, incluzând toate sondajele, răspunsurile, persoanele, acțiunile și atributele.",
|
||||
"delete_organization_warning": "Înainte de a continua cu ștergerea acestei organizații, vă rugăm să fiți conștienți de următoarele consecințe:",
|
||||
"delete_organization_warning_1": "Ștergerea permanentă a tuturor proiectelor legate de această organizație.",
|
||||
"delete_organization_warning_2": "Această acțiune nu poate fi anulată. Dacă e dispărută, e dispărută.",
|
||||
"delete_organization_warning_2": "Această acțiune este ireversibilă",
|
||||
"delete_organization_warning_3": "Vă rugăm să introduceți {organizationName} în câmpul următor pentru a confirma ștergerea definitivă a acestei organizații:",
|
||||
"eliminate_branding_with_whitelabel": "Eliminați brandingul Formbricks și activați opțiuni suplimentare de personalizare white-label.",
|
||||
"email_customization_preview_email_heading": "Salut {userName}",
|
||||
@@ -1074,7 +1061,7 @@
|
||||
"manage_members_description": "Adăugați sau eliminați membri din organizația dvs.",
|
||||
"member_deleted_successfully": "Membru șters cu succes",
|
||||
"member_invited_successfully": "Membru invitat cu succes",
|
||||
"once_its_gone_its_gone": "Odată ce a dispărut, a dispărut.",
|
||||
"once_its_gone_its_gone": "Odată șters, nu va putea fi recuperat.",
|
||||
"only_org_owner_can_perform_action": "Doar proprietarii organizației pot accesa această setare.",
|
||||
"organization_created_successfully": "Organizație creată cu succes!",
|
||||
"organization_deleted_successfully": "Organizație ștearsă cu succes!",
|
||||
@@ -1090,7 +1077,7 @@
|
||||
"remove_logo": "Înlătură siglă",
|
||||
"replace_logo": "Înlocuiește sigla",
|
||||
"resend_invitation_email": "Retrimite emailul de invitație",
|
||||
"share_invite_link": "Distribuie Link-ul de Invitație",
|
||||
"share_invite_link": "Distribuie link-ul de invitație",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Distribuie acest link pentru a permite membrului organizației să se alăture organizației tale:",
|
||||
"test_email_sent_successfully": "Email de test trimis cu succes",
|
||||
"use_multi_language_surveys_with_a_higher_plan": "Utilizați chestionare multilingve cu un plan superior",
|
||||
@@ -1111,43 +1098,35 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Consecințele ștergerii contului",
|
||||
"avatar_update_failed": "Actualizarea avatarului a eșuat. Vă rugăm să încercați din nou.",
|
||||
"backup_code": "Cod de rezervă",
|
||||
"change_image": "Schimbă imaginea",
|
||||
"confirm_delete_account": "Șterge contul tău cu toate informațiile personale și datele tale",
|
||||
"confirm_delete_my_account": "Șterge Contul Meu",
|
||||
"confirm_delete_my_account": "Șterge contul meu",
|
||||
"confirm_your_current_password_to_get_started": "Confirmaţi parola curentă pentru a începe.",
|
||||
"delete_account": "Șterge Cont",
|
||||
"delete_account": "Șterge cont",
|
||||
"disable_two_factor_authentication": "Dezactivează autentificarea în doi pași",
|
||||
"disable_two_factor_authentication_description": "Dacă este nevoie să dezactivați autentificarea în doi pași, vă recomandăm să o reactivați cât mai curând posibil.",
|
||||
"each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator": "Fiecare cod de rezervă poate fi utilizat o singură dată pentru a acorda acces fără autentificatorul tău.",
|
||||
"email_change_initiated": "Cererea dvs. de schimbare a e-mailului a fost inițiată.",
|
||||
"enable_two_factor_authentication": "Activează autentificarea în doi pași",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Introduceți codul din aplicația dvs. de autentificare mai jos.",
|
||||
"file_size_must_be_less_than_10mb": "Dimensiunea fișierului trebuie să fie mai mică de 10MB.",
|
||||
"invalid_file_type": "Tip de fișier invalid. Sunt permise numai fișiere JPEG, PNG și WEBP.",
|
||||
"lost_access": "Acces pierdut",
|
||||
"or_enter_the_following_code_manually": "Sau introduceți manual următorul cod:",
|
||||
"organization_identification": "Ajutați organizația să vă identifice pe Formbricks",
|
||||
"organizations_delete_message": "Ești singurul proprietar al acestor organizații, deci ele <b>vor fi șterse și ele.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Ștergerea permanentă a tuturor informațiilor și datelor tale personale",
|
||||
"personal_information": "Informații personale",
|
||||
"please_enter_email_to_confirm_account_deletion": "Vă rugăm să introduceți {email} în câmpul următor pentru a confirma ștergerea definitivă a contului dumneavoastră:",
|
||||
"profile_updated_successfully": "Profilul dvs. a fost actualizat cu succes",
|
||||
"remove_image": "Șterge imaginea",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Salvează următoarele coduri de rezervă într-un loc sigur.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scanați codul QR de mai jos cu aplicația dvs. de autentificare.",
|
||||
"security_description": "Gestionează parola și alte setări de securitate, precum autentificarea în doi pași (2FA).",
|
||||
"two_factor_authentication": "Autentificare în doi pași",
|
||||
"two_factor_authentication_description": "Adăugați un strat suplimentar de securitate la contul dvs. în cazul în care parola este furată.",
|
||||
"two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autentificare în doi pași activată. Introduceți codul de șase cifre din aplicația dvs. de autentificare.",
|
||||
"two_factor_code": "Codul cu doi factori",
|
||||
"two_factor_code": "Codul pentru dublă autentificare",
|
||||
"unlock_two_factor_authentication": "Deblocați autentificarea în doi pași cu un plan superior",
|
||||
"update_personal_info": "Actualizează informațiile tale personale",
|
||||
"upload_image": "Încărcați imagine",
|
||||
"warning_cannot_delete_account": "Ești singurul proprietar al acestei organizații. Te rugăm să transferi proprietatea către un alt membru mai întâi.",
|
||||
"warning_cannot_undo": "Aceasta nu poate fi anulată",
|
||||
"you_must_select_a_file": "Trebuie să selectați un fișier."
|
||||
"warning_cannot_undo": "Aceasta nu poate fi anulată"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adaugă membri în echipă și stabilește rolul lor.",
|
||||
@@ -1196,7 +1175,7 @@
|
||||
}
|
||||
},
|
||||
"surveys": {
|
||||
"all_set_time_to_create_first_survey": "Ești gata! Timp să creezi primul tău chestionar",
|
||||
"all_set_time_to_create_first_survey": "Ești gata! Este timpul să creezi primul tău chestionar",
|
||||
"alphabetical": "Alfabetic",
|
||||
"copy_survey": "Copiază sondajul",
|
||||
"copy_survey_description": "Copiază acest sondaj într-un alt mediu",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "Închideți automat sondajul după",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "Închideți automat sondajul după un număr anumit de răspunsuri.",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Închideți automat sondajul dacă utilizatorul nu răspunde după un anumit număr de secunde.",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "Închide automat sondajul la începutul zilei (UTC).",
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcați automat sondajul ca finalizat după",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "Eliberați automat sondajul la începutul zilei (UTC).",
|
||||
"back_button_label": "Etichetă buton \"Înapoi\"",
|
||||
"background_styling": "Stilizare fundal",
|
||||
"brand_color": "Culoarea brandului",
|
||||
@@ -1285,7 +1262,7 @@
|
||||
"caution_explanation_new_responses_separated": "Răspunsurile înainte de schimbare pot să nu fie sau să fie incluse doar parțial în rezumatul sondajului.",
|
||||
"caution_explanation_only_new_responses_in_summary": "Toate datele, inclusiv răspunsurile anterioare, rămân disponibile ca descărcare pe pagina de rezumat a sondajului.",
|
||||
"caution_explanation_responses_are_safe": "Răspunsurile mai vechi și mai noi se amestecă, ceea ce poate duce la rezumate de date înșelătoare.",
|
||||
"caution_recommendation": "Aceasta poate cauza inconsistențe de date în rezumatul sondajului. Vă recomandăm să duplicați sondajul în schimb.",
|
||||
"caution_recommendation": "Aceasta poate cauza inconsistențe de date în rezultatul sondajului. Vă recomandăm să duplicați sondajul în schimb.",
|
||||
"caution_text": "Schimbările vor duce la inconsecvențe",
|
||||
"centered_modal_overlay_color": "Culoare suprapunere modală centralizată",
|
||||
"change_anyway": "Schimbă oricum",
|
||||
@@ -1309,19 +1286,18 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
|
||||
"choose_where_to_run_the_survey": "Alegeți unde să rulați chestionarul.",
|
||||
"city": "Oraș",
|
||||
"close_survey_on_date": "Închide sondajul la dată",
|
||||
"close_survey_on_response_limit": "Închideți sondajul la limită de răspunsuri",
|
||||
"color": "Culoare",
|
||||
"column_used_in_logic_error": "Această coloană este folosită în logica întrebării {questionIndex}. Vă rugăm să o eliminați din logică mai întâi.",
|
||||
"columns": "Coloane",
|
||||
"company": "Companie",
|
||||
"company_logo": "Sigla companiei",
|
||||
"completed_responses": "răspunsuri parțiale sau finalizate",
|
||||
"completed_responses": "Răspunsuri completate",
|
||||
"concat": "Concat +",
|
||||
"conditional_logic": "Logică condițională",
|
||||
"confirm_default_language": "Confirmați limba implicită",
|
||||
"confirm_survey_changes": "Confirmă modificările sondajului",
|
||||
"contact_fields": "C<EFBFBD>mpuri de contact",
|
||||
"contact_fields": "Câmpuri de contact",
|
||||
"contains": "Conține",
|
||||
"continue_to_settings": "Continuă către Setări",
|
||||
"control_which_file_types_can_be_uploaded": "Controlează ce tipuri de fișiere pot fi încărcate.",
|
||||
@@ -1348,7 +1324,7 @@
|
||||
"does_not_include_all_of": "Nu include toate",
|
||||
"does_not_include_one_of": "Nu include una dintre",
|
||||
"does_not_start_with": "Nu începe cu",
|
||||
"edit_recall": "Editează Amintirea",
|
||||
"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_recaptcha_to_protect_your_survey_from_spam": "Protecția împotriva spamului folosește reCAPTCHA v3 pentru a filtra răspunsurile de spam.",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "Ecran final card",
|
||||
"ending_card": "Cardul de finalizare",
|
||||
"ending_card_used_in_logic": "Această carte de încheiere este folosită în logica întrebării {questionIndex}.",
|
||||
"ending_used_in_quota": "Finalul acesta este folosit în cota \"{quotaName}\"",
|
||||
"ends_with": "Se termină cu",
|
||||
"equals": "Egal",
|
||||
"equals_one_of": "Egal unu dintre",
|
||||
@@ -1366,22 +1343,23 @@
|
||||
"fallback_for": "Varianta de rezervă pentru",
|
||||
"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}\"",
|
||||
"field_name_eg_score_price": "Nume câmp, de exemplu, scor, preț",
|
||||
"first_name": "Prenume",
|
||||
"five_points_recommended": "5 puncte (recomandat)",
|
||||
"follow_ups": "Urmăriri",
|
||||
"follow_ups": "Follow-up",
|
||||
"follow_ups_delete_modal_text": "Sigur doriți să ștergeți acest follow-up?",
|
||||
"follow_ups_delete_modal_title": "Ștergeți urmărirea?",
|
||||
"follow_ups_delete_modal_title": "Ștergeți follow-up-ul?",
|
||||
"follow_ups_empty_description": "Trimite mesaje respondentilor, ție sau colegilor de echipă.",
|
||||
"follow_ups_empty_heading": "Trimitere automată de urmăriri",
|
||||
"follow_ups_ending_card_delete_modal_text": "Această cartă de sfârșit este folosită în urmăriri ulterioare. Ștergerea sa o va elimina din toate urmăriri ulterioare. Ești sigur că vrei să o ștergi?",
|
||||
"follow_ups_empty_heading": "Trimitere automată de follow-up",
|
||||
"follow_ups_ending_card_delete_modal_text": "Această cartă de sfârșit este folosită în follow-up-uri ulterioare. Ștergerea sa o va elimina din toate follow-up-uri ulterioare. Ești sigur că vrei să o ștergi?",
|
||||
"follow_ups_ending_card_delete_modal_title": "Șterge cardul de finalizare?",
|
||||
"follow_ups_hidden_field_error": "Câmpul ascuns este utilizat într-un follow-up. Vă rugăm să îl eliminați mai întâi din follow-up.",
|
||||
"follow_ups_item_ending_tag": "Finalizare",
|
||||
"follow_ups_item_issue_detected_tag": "Problemă detectată",
|
||||
"follow_ups_item_response_tag": "Orice răspuns",
|
||||
"follow_ups_item_send_email_tag": "Trimite email",
|
||||
"follow_ups_modal_action_attach_response_data_description": "Adăugați datele răspunsului la sondaj la urmărire",
|
||||
"follow_ups_modal_action_attach_response_data_description": "Adăugați datele răspunsului la sondaj la follow-up",
|
||||
"follow_ups_modal_action_attach_response_data_label": "Atașează datele răspunsului",
|
||||
"follow_ups_modal_action_body_label": "Corp",
|
||||
"follow_ups_modal_action_body_placeholder": "Corpul emailului",
|
||||
@@ -1397,20 +1375,22 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "Subiectul emailului",
|
||||
"follow_ups_modal_action_to_description": "Adresă de email către care se trimite emailul",
|
||||
"follow_ups_modal_action_to_label": "Către",
|
||||
"follow_ups_modal_action_to_warning": "Nu s-a detectat niciun câmp de e-mail în sondaj",
|
||||
"follow_ups_modal_action_to_warning": "Nu s-au găsit opțiuni valide pentru trimiterea e-mailurilor, vă rugăm să adăugați întrebări de tip text deschis / informații de contact sau câmpuri ascunse",
|
||||
"follow_ups_modal_create_heading": "Creați o nouă urmărire",
|
||||
"follow_ups_modal_created_successfull_toast": "Urmărirea a fost creată și va fi salvată odată ce salvați sondajul.",
|
||||
"follow_ups_modal_edit_heading": "Editează acest follow-up",
|
||||
"follow_ups_modal_edit_no_id": "Nu a fost furnizat un ID de urmărire al chestionarului, nu pot actualiza urmărirea chestionarului",
|
||||
"follow_ups_modal_name_label": "Numele urmăririi",
|
||||
"follow_ups_modal_name_placeholder": "Denumirea urmăririi tale",
|
||||
"follow_ups_modal_name_label": "Numele ",
|
||||
"follow_ups_modal_name_placeholder": "Denumirea follow-up-ului tău",
|
||||
"follow_ups_modal_subheading": "Trimite mesaje respondentilor, ție sau colegilor de echipă",
|
||||
"follow_ups_modal_trigger_description": "Când ar trebui să fie declanșat acest follow-up?",
|
||||
"follow_ups_modal_trigger_label": "Declanșator",
|
||||
"follow_ups_modal_trigger_type_ending": "Respondentul vede un sfârșit specific",
|
||||
"follow_ups_modal_trigger_type_ending_select": "Selectează finalurile:",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Nu s-au găsit finalizări în sondaj!",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "Vă rugăm să selectați cel puțin un sfârșit sau să schimbați tipul declanșatorului",
|
||||
"follow_ups_modal_trigger_type_response": "Respondent finalizează sondajul",
|
||||
"follow_ups_new": "Urmărire nouă",
|
||||
"follow_ups_modal_updated_successfull_toast": "Urmărirea a fost actualizată și va fi salvată odată ce salvați sondajul.",
|
||||
"follow_ups_new": "Follow-up nou",
|
||||
"follow_ups_upgrade_button_text": "Actualizați pentru a activa urmărările",
|
||||
"form_styling": "Stilizare formular",
|
||||
"formbricks_sdk_is_not_connected": "SDK Formbricks nu este conectat",
|
||||
@@ -1467,10 +1447,10 @@
|
||||
"logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare",
|
||||
"long_answer": "Răspuns lung",
|
||||
"lower_label": "Etichetă inferioară",
|
||||
"manage_languages": "Gestionați Limbile",
|
||||
"manage_languages": "Gestionați limbile",
|
||||
"max_file_size": "Dimensiune maximă fișier",
|
||||
"max_file_size_limit_is": "Limita dimensiunii maxime a fișierului este",
|
||||
"multiply": "Înmulțire *",
|
||||
"multiply": "Multiplicare",
|
||||
"needed_for_self_hosted_cal_com_instance": "Necesar pentru un exemplu autogăzduit Cal.com",
|
||||
"next_button_label": "Etichetă buton \"Următorul\"",
|
||||
"next_question": "Întrebarea următoare",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "Întrebare duplicată.",
|
||||
"question_id_updated": "ID întrebare actualizat",
|
||||
"question_used_in_logic": "Această întrebare este folosită în logica întrebării {questionIndex}.",
|
||||
"question_used_in_quota": "Întrebarea aceasta este folosită în cota \"{quotaName}\"",
|
||||
"quotas": {
|
||||
"add_quota": "Adăugați cotă",
|
||||
"change_quota_for_public_survey": "Schimbați cota pentru sondaj public?",
|
||||
"confirm_quota_changes": "Confirmă modificările cotelor",
|
||||
"confirm_quota_changes_body": "Aveți modificări nesalvate în quota dumneavoastră. Doriți să le salvați înainte de a pleca?",
|
||||
"continue_survey_normally": "Continuă chestionarul în mod normal",
|
||||
"count_partial_submissions": "Număr contestații parțiale",
|
||||
"count_partial_submissions_description": "Includeți respondenții care îndeplinesc criteriile de cotă dar nu au completat sondajul",
|
||||
"create_quota_for_public_survey": "Creați cotă pentru sondaj public?",
|
||||
"create_quota_for_public_survey_description": "Doar răspunsurile viitoare vor fi încorporate în cotă",
|
||||
"create_quota_for_public_survey_text": "Acest sondaj este deja public. Răspunsurile actuale nu vor fi luate în considerare pentru noua cotă.",
|
||||
"delete_quota_confirmation_text": "Acest lucru va șterge definitiv cota {quotaName}.",
|
||||
"duplicate_quota": "Duplicare cotă",
|
||||
"edit_quota": "Editează cota",
|
||||
"end_survey_for_matching_participants": "Încheiere sondaj pentru participanții eligibili",
|
||||
"inclusion_criteria": "Criterii de includere",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, one {Deja aveți {value} răspuns pentru această cotă, astfel încât limita trebuie să fie mai mare decât {value}.} other {Deja aveți {value} răspunsuri pentru această cotă, astfel încât limita trebuie să fie mai mare decât {value}.} }",
|
||||
"limited_to_x_responses": "Limitat la {limit}",
|
||||
"new_quota": "Contingent Nou",
|
||||
"quota_created_successfull_toast": "\"Cota creată cu succes!\"",
|
||||
"quota_deleted_successfull_toast": "\"Cota ștearsă cu succes!\"",
|
||||
"quota_duplicated_successfull_toast": "\"Cota duplicată cu succes!\"",
|
||||
"quota_name_placeholder": "de exemplu, Participanți cu vârsta 18-25 ani",
|
||||
"quota_updated_successfull_toast": "\"Cota actualizată cu succes!\"",
|
||||
"response_limit": "Limitări",
|
||||
"save_changes_confirmation_body": "Orice modificări ale criteriilor de includere afectează doar răspunsurile viitoare. \nRecomandăm fie să duplicați un existent, fie să creați o nouă cotă.",
|
||||
"save_changes_confirmation_text": "Răspunsurile existente rămân în cotă",
|
||||
"select_ending_card": "Selectează cardul de finalizare",
|
||||
"upgrade_prompt_title": "Folosește cote cu un plan superior",
|
||||
"when_quota_has_been_reached": "Când cota a fost atinsă"
|
||||
},
|
||||
"randomize_all": "Randomizează tot",
|
||||
"randomize_all_except_last": "Randomizează tot cu excepția ultimului",
|
||||
"range": "Interval",
|
||||
@@ -1517,18 +1529,17 @@
|
||||
"redirect_thank_you_card": "Redirecționează cardul de mulțumire",
|
||||
"redirect_to_url": "Redirecționează către URL",
|
||||
"redirect_to_url_not_available_on_free_plan": "\"Redirecționarea către URL nu este disponibilă în planul gratuit\"",
|
||||
"release_survey_on_date": "Eliberați sondajul la dată",
|
||||
"remove_description": "Eliminați descrierea",
|
||||
"remove_translations": "Eliminați traducerile",
|
||||
"require_answer": "Cere Răspuns",
|
||||
"require_answer": "Cere răspuns",
|
||||
"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.",
|
||||
"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.",
|
||||
"response_options": "Opțiuni Răspuns",
|
||||
"roundness": "Rotunjirea",
|
||||
"response_options": "Opțiuni răspuns",
|
||||
"roundness": "Rotunjire",
|
||||
"row_used_in_logic_error": "Această linie este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"rows": "Rânduri",
|
||||
"save_and_close": "Salvează & Închide",
|
||||
@@ -1588,7 +1599,7 @@
|
||||
"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",
|
||||
"trigger_survey_when_one_of_the_actions_is_fired": "Declanșați sondajul atunci când una dintre acțiuni este declanșată...",
|
||||
"trigger_survey_when_one_of_the_actions_is_fired": "Declanșați sondajul atunci când una dintre acțiuni este realizată...",
|
||||
"try_lollipop_or_mountain": "Încercați „lollipop” sau „mountain”...",
|
||||
"type_field_id": "ID câmp tip",
|
||||
"unlock_targeting_description": "Vizează grupuri specifice de utilizatori pe baza atributelor sau a informațiilor despre dispozitiv",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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.",
|
||||
"variable_name_must_start_with_a_letter": "Numele variabilei trebuie să înceapă cu o literă.",
|
||||
"verify_email_before_submission": "Verifică emailul înainte de trimitere",
|
||||
@@ -1627,7 +1639,7 @@
|
||||
"complete_responses": "Răspunsuri complete",
|
||||
"partial_responses": "Răspunsuri parțiale"
|
||||
},
|
||||
"new_survey": "Chestionar Nou",
|
||||
"new_survey": "Chestionar nou",
|
||||
"no_surveys_created_yet": "Nu au fost create încă chestionare",
|
||||
"open_options": "Opțiuni deschise",
|
||||
"preview_survey_in_a_new_tab": "Previzualizare chestionar în alt tab",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "Adresă Linie 2",
|
||||
"an_error_occurred_deleting_the_tag": "A apărut o eroare la ștergerea etichetei",
|
||||
"browser": "Browser",
|
||||
"bulk_delete_response_quotas": "Răspunsurile fac parte din cotele pentru acest sondaj. Cum doriți să gestionați cotele?",
|
||||
"city": "Oraș",
|
||||
"company": "Companie",
|
||||
"completed": "Finalizat ✅",
|
||||
"country": "Țară",
|
||||
"decrement_quotas": "Decrementați toate limitele cotelor, inclusiv acest răspuns",
|
||||
"delete_response_confirmation": "Aceasta va șterge răspunsul la sondaj, inclusiv toate răspunsurile, etichetele, documentele atașate și metadatele răspunsului.",
|
||||
"delete_response_quotas": "Răspunsul face parte din cotele pentru acest sondaj. Cum doriți să gestionați cotele?",
|
||||
"device": "Dispozitiv",
|
||||
"device_info": "Informații despre dispozitiv",
|
||||
"email": "Email",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "Meta datele sunt încărcate pe baza valorii `lang` din URL.",
|
||||
"link_description": "Descriere legătură",
|
||||
"link_description_description": "Descrierile între 55-200 de caractere au cele mai bune performanțe.",
|
||||
"link_description_placeholder": "Ajutați-ne să ne îmbunătățim împărtășindu-vă gândurile.",
|
||||
"link_title": "Titlu link",
|
||||
"link_title_description": "Titlurile scurte funcționează cel mai bine ca Meta Title-uri.",
|
||||
"link_title_placeholder": "Chestionar de feedback al clienților",
|
||||
"preview_image": "Previzualizare imagine",
|
||||
"preview_image_description": "Imaginile panoramice cu dimensiuni de fișier mici (<4MB) au cel mai bun randament.",
|
||||
"title": "Setări link"
|
||||
@@ -1742,7 +1755,7 @@
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copiază codul de inserare",
|
||||
"description": "Inserați sondajul dvs. într-un e-mail pentru a obține răspunsuri de la audiența dvs.",
|
||||
"email_preview_tab": "Previzualizare Email",
|
||||
"email_preview_tab": "Previzualizare email",
|
||||
"email_sent": "Email trimis!",
|
||||
"email_subject_label": "Subiect",
|
||||
"email_to_label": "Către",
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "Configurează alertele",
|
||||
"congrats": "Felicitări! Sondajul dumneavoastră este activ.",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Conectează-ți site-ul sau aplicația cu Formbricks pentru a începe.",
|
||||
"current_count": "Număr curent",
|
||||
"custom_range": "Interval personalizat...",
|
||||
"delete_all_existing_responses_and_displays": "Șterge toate răspunsurile și afișările existente",
|
||||
"download_qr_code": "Descărcare cod QR",
|
||||
@@ -1786,7 +1800,7 @@
|
||||
"filter_updated_successfully": "Filtru actualizat cu succes",
|
||||
"filtered_responses_csv": "Răspunsuri filtrate (CSV)",
|
||||
"filtered_responses_excel": "Răspunsuri filtrate (Excel)",
|
||||
"go_to_setup_checklist": "Mergi la Lista de Verificare a Configurării \uD83D\uDC49",
|
||||
"go_to_setup_checklist": "Mergi la lista de verificare a configurării \uD83D\uDC49",
|
||||
"impressions": "Impresii",
|
||||
"impressions_tooltip": "Număr de ori când sondajul a fost vizualizat.",
|
||||
"in_app": {
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "Ultima lună",
|
||||
"last_quarter": "Ultimul trimestru",
|
||||
"last_year": "Anul trecut",
|
||||
"limit": "Limită",
|
||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||
"other_values_found": "Alte valori găsite",
|
||||
"overall": "General",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "Descărcarea codului QR a eșuat",
|
||||
"qr_code_download_with_start_soon": "Descărcarea codului QR va începe în curând",
|
||||
"qr_code_generation_failed": "A apărut o problemă la încărcarea codului QR al chestionarului. Vă rugăm să încercați din nou.",
|
||||
"quotas_completed": "Cote completate",
|
||||
"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ă.",
|
||||
"selected_responses_csv": "Răspunsuri selectate (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "Trimestrul acesta",
|
||||
"this_year": "Anul acesta",
|
||||
"time_to_complete": "Timp de finalizare",
|
||||
"ttc_tooltip": "Timp mediu pentru a completa sondajul.",
|
||||
"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 \uD83E\uDDD8♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "\"Sondaj șters cu succes!\"",
|
||||
"survey_duplicated_successfully": "\"Sondaj duplicat cu succes!\"",
|
||||
"survey_duplication_error": "Eșec la duplicarea sondajului.",
|
||||
"survey_status_tooltip": "Pentru a actualiza starea sondajului, actualizați programarea și setările de închidere în opțiunile de răspuns la sondaj.",
|
||||
"templates": {
|
||||
"all_channels": "Toate canalele",
|
||||
"all_industries": "Toate industriile",
|
||||
@@ -1953,7 +1969,7 @@
|
||||
"intro": {
|
||||
"get_started": "Începeți",
|
||||
"made_with_love_in_kiel": "Creat cu \uD83E\uDD0D în Germania",
|
||||
"paragraph_1": "Formbricks este o Suită de Management al Experiențelor construită pe baza <b>platformei de sondaje open source care crește cel mai rapid</b> din lume.",
|
||||
"paragraph_1": "Formbricks este o suită de management al experiențelor construită pe baza <b>platformei de sondaje open source care crește cel mai rapid</b> din lume.",
|
||||
"paragraph_2": "Rulați sondaje direcționate pe site-uri web, în aplicații sau oriunde online. Adunați informații valoroase pentru a <b>crea experiențe irezistibile</b> pentru clienți, utilizatori și angajați.",
|
||||
"paragraph_3": "Suntem angajați la cel mai înalt grad de confidențialitate a datelor. Găzduirea proprie vă oferă <b>control deplin asupra datelor dumneavoastră</b>.",
|
||||
"welcome_to_formbricks": "Bine ai venit la Formbricks!"
|
||||
@@ -2053,7 +2069,7 @@
|
||||
"career_development_survey_question_4_headline": "Sunt mulțumit de investiția pe care organizația mea o face în formare și educație.",
|
||||
"career_development_survey_question_4_lower_label": "Dezacord puternic",
|
||||
"career_development_survey_question_4_upper_label": "De acord cu tărie",
|
||||
"career_development_survey_question_5_choice_1": "Dezvoltare de Produs",
|
||||
"career_development_survey_question_5_choice_1": "Dezvoltare de produs",
|
||||
"career_development_survey_question_5_choice_2": "Marketing",
|
||||
"career_development_survey_question_5_choice_3": "Relații Publice",
|
||||
"career_development_survey_question_5_choice_4": "Contabilitate",
|
||||
@@ -2123,7 +2139,7 @@
|
||||
"collect_feedback_question_5_headline": "Mai dorești să împărtășești altceva cu echipa noastră?",
|
||||
"collect_feedback_question_5_placeholder": "Tastează răspunsul aici...",
|
||||
"collect_feedback_question_6_choice_1": "Google",
|
||||
"collect_feedback_question_6_choice_2": "Rețele Sociale",
|
||||
"collect_feedback_question_6_choice_2": "Rețele sociale",
|
||||
"collect_feedback_question_6_choice_3": "Prieteni",
|
||||
"collect_feedback_question_6_choice_4": "Podcast",
|
||||
"collect_feedback_question_6_choice_5": "Altele",
|
||||
@@ -2252,7 +2268,7 @@
|
||||
"earned_advocacy_score_question_5_headline": "Ce te-a făcut să îi descurajezi?",
|
||||
"earned_advocacy_score_question_5_placeholder": "Tastează răspunsul aici...",
|
||||
"employee_satisfaction_description": "Evaluează satisfacția angajaților și identifică domeniile de îmbunătățire.",
|
||||
"employee_satisfaction_name": "Satisfacție a Angajatului",
|
||||
"employee_satisfaction_name": "Satisfacție a angajatului",
|
||||
"employee_satisfaction_question_1_headline": "Cât de satisfăcut sunteți de rolul dvs. actual?",
|
||||
"employee_satisfaction_question_1_lower_label": "Nesatisfăcut",
|
||||
"employee_satisfaction_question_1_upper_label": "Foarte mulțumit",
|
||||
@@ -2276,7 +2292,7 @@
|
||||
"employee_satisfaction_question_7_choice_5": "Deloc probabil",
|
||||
"employee_satisfaction_question_7_headline": "Cât de probabil este să recomandați compania noastră unui prieten?",
|
||||
"employee_well_being_description": "Evaluează bunăstarea angajatului prin echilibrul între muncă și viață, volumul de muncă și mediul de lucru.",
|
||||
"employee_well_being_name": "Bunăstarea Angajatului",
|
||||
"employee_well_being_name": "Bunăstarea angajatului",
|
||||
"employee_well_being_question_1_headline": "Simt că am un echilibru bun între viața mea profesională și cea personală.",
|
||||
"employee_well_being_question_1_lower_label": "Echilibru foarte slab",
|
||||
"employee_well_being_question_1_upper_label": "Echilibru excelent",
|
||||
@@ -2338,7 +2354,7 @@
|
||||
"fake_door_follow_up_question_2_choice_4": "Aspectul 4",
|
||||
"fake_door_follow_up_question_2_headline": "Ce ar trebui să includem cu siguranță în construirea acestuia?",
|
||||
"feature_chaser_description": "Urmăriți utilizatorii care tocmai au folosit o funcție specifică.",
|
||||
"feature_chaser_name": "Urmăritor de Funcționalități",
|
||||
"feature_chaser_name": "Urmăritor de funcționalități",
|
||||
"feature_chaser_question_1_headline": "Cât de importantă este [ADD FEATURE] pentru tine?",
|
||||
"feature_chaser_question_1_lower_label": "Neimportant",
|
||||
"feature_chaser_question_1_upper_label": "Foarte important",
|
||||
@@ -2379,7 +2395,7 @@
|
||||
"identify_customer_goals_description": "Înțelegeți mai bine dacă mesajele voastre creează așteptările corecte privind valoarea pe care o oferă produsul vostru.",
|
||||
"identify_customer_goals_name": "Identifică Obiectivele Clienților",
|
||||
"identify_sign_up_barriers_description": "Oferiți o reducere pentru a obține informații despre barierele de înscriere.",
|
||||
"identify_sign_up_barriers_name": "Identificați Barierele de Înscriere",
|
||||
"identify_sign_up_barriers_name": "Identificați barierele de înscriere",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Obține reducere de 10%",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Nu, mulţumesc",
|
||||
"identify_sign_up_barriers_question_1_headline": "Răspunde acestui scurt sondaj, primește 10% reducere!",
|
||||
@@ -2415,7 +2431,7 @@
|
||||
"identify_upsell_opportunities_question_1_choice_4": "5+ ore",
|
||||
"identify_upsell_opportunities_question_1_headline": "Câte ore economisește echipa dumneavoastră pe săptămână folosind $[projectName]?",
|
||||
"improve_activation_rate_description": "Identifică punctele slabe în fluxul de onboarding pentru a crește activarea utilizatorilor.",
|
||||
"improve_activation_rate_name": "Îmbunătățește Rata de Activare",
|
||||
"improve_activation_rate_name": "Îmbunătățește rata de activare",
|
||||
"improve_activation_rate_question_1_choice_1": "Nu părea util pentru mine",
|
||||
"improve_activation_rate_question_1_choice_2": "Dificil de configurat sau utilizat",
|
||||
"improve_activation_rate_question_1_choice_3": "Lipsit de funcții/funcționalități",
|
||||
@@ -2437,12 +2453,12 @@
|
||||
"improve_newsletter_content_name": "Îmbunătățește Conținutul Newsletterului",
|
||||
"improve_newsletter_content_question_1_headline": "Cum ați evalua newsletterul din această săptămână?",
|
||||
"improve_newsletter_content_question_1_lower_label": "Însă",
|
||||
"improve_newsletter_content_question_1_upper_label": "Groza",
|
||||
"improve_newsletter_content_question_1_upper_label": "Grozav",
|
||||
"improve_newsletter_content_question_2_headline": "Ce ar fi făcut ca newsletter-ul din această săptămână să fie mai util?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Tastează răspunsul aici...",
|
||||
"improve_newsletter_content_question_3_button_label": "Bucuros să ajut!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Găsește-ți proprii prieteni",
|
||||
"improve_newsletter_content_question_3_headline": "Mulțumim! ❤️ Răspândește iubirea către UN prieten.",
|
||||
"improve_newsletter_content_question_3_headline": "Mulțumim! ❤️ Răspândește iubirea către un prieten.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Cine gândește ca tine? Ne-ai face o mare favoare dacă ai împărtăși episodul acestei săptămâni cu prietenul tău de creier!</span></p>",
|
||||
"improve_trial_conversion_description": "Află de ce oamenii au încetat perioada de încercare. Aceste informații te ajută să îți îmbunătățești procesul de achiziție.",
|
||||
"improve_trial_conversion_name": "Îmbunătățește Conversia În Proba",
|
||||
@@ -2474,7 +2490,7 @@
|
||||
"integration_setup_survey_question_3_headline": "Ce alte instrumente ați dori să utilizați cu $[projectName]?",
|
||||
"integration_setup_survey_question_3_subheader": "Continuăm să dezvoltăm integrări, a ta poate fi următoarea:",
|
||||
"interview_prompt_description": "Invită un subset specific de utilizatori să programeze un interviu cu echipa ta de produs.",
|
||||
"interview_prompt_name": "Întrebare Interviu",
|
||||
"interview_prompt_name": "Întrebare interviu",
|
||||
"interview_prompt_question_1_button_label": "Rezervă intervalul",
|
||||
"interview_prompt_question_1_headline": "Ai 15 minute să discuți cu noi? \uD83D\uDE4F",
|
||||
"interview_prompt_question_1_html": "Ești unul dintre utilizatorii noștri frecvenți. Ne-ar plăcea să te intervievăm pe scurt!",
|
||||
@@ -2513,16 +2529,16 @@
|
||||
"long_term_retention_check_in_question_9_lower_label": "Nemulțumit",
|
||||
"long_term_retention_check_in_question_9_upper_label": "Foarte mulțumit",
|
||||
"market_attribution_description": "Aflați cum au auzit utilizatorii pentru prima dată despre produsul dumneavoastră.",
|
||||
"market_attribution_name": "Atribuirea Marketingului",
|
||||
"market_attribution_name": "Atribuirea marketingului",
|
||||
"market_attribution_question_1_choice_1": "Recomandare",
|
||||
"market_attribution_question_1_choice_2": "Rețele Sociale",
|
||||
"market_attribution_question_1_choice_2": "Rețele sociale",
|
||||
"market_attribution_question_1_choice_3": "Reclame",
|
||||
"market_attribution_question_1_choice_4": "Căutare Google",
|
||||
"market_attribution_question_1_choice_5": "Într-un Podcast",
|
||||
"market_attribution_question_1_choice_5": "Într-un podcast",
|
||||
"market_attribution_question_1_headline": "Cum ați aflat pentru prima dată despre noi?",
|
||||
"market_attribution_question_1_subheader": "Vă rugăm să selectați una dintre următoarele opțiuni:",
|
||||
"market_site_clarity_description": "Identificați utilizatorii care părăsesc site-ul dvs. de marketing. Îmbunătățiți mesajele dvs.",
|
||||
"market_site_clarity_name": "Claritate Site de Marketing",
|
||||
"market_site_clarity_name": "Claritate site de marketing",
|
||||
"market_site_clarity_question_1_choice_1": "Da, complet",
|
||||
"market_site_clarity_question_1_choice_2": "Un fel de...",
|
||||
"market_site_clarity_question_1_choice_3": "Nu, deloc",
|
||||
@@ -2600,17 +2616,17 @@
|
||||
"onboarding_segmentation_question_2_headline": "Care este dimensiunea companiei dumneavoastră?",
|
||||
"onboarding_segmentation_question_2_subheader": "Vă rugăm să selectați una dintre următoarele opțiuni:",
|
||||
"onboarding_segmentation_question_3_choice_1": "Recomandare",
|
||||
"onboarding_segmentation_question_3_choice_2": "Rețele Sociale",
|
||||
"onboarding_segmentation_question_3_choice_2": "Rețele sociale",
|
||||
"onboarding_segmentation_question_3_choice_3": "Reclame",
|
||||
"onboarding_segmentation_question_3_choice_4": "Căutare Google",
|
||||
"onboarding_segmentation_question_3_choice_5": "Într-un Podcast",
|
||||
"onboarding_segmentation_question_3_headline": "Cum ai aflat pentru prima dată despre noi?",
|
||||
"onboarding_segmentation_question_3_subheader": "Vă rugăm să selectați una dintre următoarele opțiuni:",
|
||||
"picture_selection": "Selecție Poze",
|
||||
"picture_selection": "Selecție poze",
|
||||
"picture_selection_description": "Cereți respondenților să aleagă una sau mai multe imagini",
|
||||
"preview_survey_ending_card_description": "Vă rugăm să continuați onboarding-ul.",
|
||||
"preview_survey_ending_card_headline": "Ai reușit!",
|
||||
"preview_survey_name": "Previzualizare Chestionar",
|
||||
"preview_survey_name": "Previzualizare chestionar",
|
||||
"preview_survey_question_1_headline": "Cum ai evalua {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Nu este bine",
|
||||
"preview_survey_question_1_subheader": "Aceasta este o previzualizare a chestionarului.",
|
||||
@@ -2622,7 +2638,7 @@
|
||||
"preview_survey_welcome_card_headline": "Bun venit!",
|
||||
"preview_survey_welcome_card_html": "Mulțumesc pentru feedback-ul dvs - să începem!",
|
||||
"prioritize_features_description": "Identificați caracteristicile de care utilizatorii dumneavoastră au cel mai mult și cel mai puțin nevoie.",
|
||||
"prioritize_features_name": "Prioritizați Caracteristicile",
|
||||
"prioritize_features_name": "Prioritizați caracteristicile",
|
||||
"prioritize_features_question_1_choice_1": "Caracteristica 1",
|
||||
"prioritize_features_question_1_choice_2": "Caracteristica 2",
|
||||
"prioritize_features_question_1_choice_3": "Caracteristica 3",
|
||||
@@ -2666,7 +2682,7 @@
|
||||
"product_market_fit_superhuman_question_6_headline": "Cum putem îmbunătăți $[projectName] pentru dumneavoastră?",
|
||||
"product_market_fit_superhuman_question_6_subheader": "Vă rugăm să fiți cât mai specific posibil.",
|
||||
"professional_development_growth_survey_description": "Evaluează satisfacția angajaților cu privire la oportunitățile de dezvoltare și creștere profesională.",
|
||||
"professional_development_growth_survey_name": "Sondaj de Creștere și Dezvoltare Profesională",
|
||||
"professional_development_growth_survey_name": "Sondaj de creștere și dezvoltare profesională",
|
||||
"professional_development_growth_survey_question_1_headline": "Simt că am oportunități să cresc și să-mi dezvolt abilitățile la muncă.",
|
||||
"professional_development_growth_survey_question_1_lower_label": "Nicio oportunitate de creștere",
|
||||
"professional_development_growth_survey_question_1_upper_label": "Multe oportunități de creștere",
|
||||
|
||||
2891
apps/web/locales/zh-Hans-CN.json
Normal file
2891
apps/web/locales/zh-Hans-CN.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -118,6 +118,7 @@
|
||||
"account_settings": "帳戶設定",
|
||||
"action": "操作",
|
||||
"actions": "操作",
|
||||
"actions_description": "代碼 和 無代碼 動作 用於 觸發 截取 調查 於 應用程式 和 網站上 。",
|
||||
"active_surveys": "啟用中的問卷",
|
||||
"activity": "活動",
|
||||
"add": "新增",
|
||||
@@ -125,6 +126,7 @@
|
||||
"add_filter": "新增篩選器",
|
||||
"add_logo": "新增標誌",
|
||||
"add_member": "新增成員",
|
||||
"add_new_project": "新增 新專案",
|
||||
"add_project": "新增專案",
|
||||
"add_to_team": "新增至團隊",
|
||||
"all": "全部",
|
||||
@@ -141,7 +143,6 @@
|
||||
"apply_filters": "套用篩選器",
|
||||
"are_you_sure": "您確定嗎?",
|
||||
"attributes": "屬性",
|
||||
"avatar": "頭像",
|
||||
"back": "返回",
|
||||
"billing": "帳單",
|
||||
"booked": "已預訂",
|
||||
@@ -150,6 +151,9 @@
|
||||
"cancel": "取消",
|
||||
"centered_modal": "置中彈窗",
|
||||
"choices": "選項",
|
||||
"choose_environment": "選擇環境",
|
||||
"choose_organization": "選擇 組織",
|
||||
"choose_project": "選擇 專案",
|
||||
"clear_all": "全部清除",
|
||||
"clear_filters": "清除篩選器",
|
||||
"clear_selection": "清除選取",
|
||||
@@ -165,11 +169,14 @@
|
||||
"connect_formbricks": "連線 Formbricks",
|
||||
"connected": "已連線",
|
||||
"contacts": "聯絡人",
|
||||
"continue": "繼續",
|
||||
"copied": "已 複製",
|
||||
"copied_to_clipboard": "已複製到剪貼簿",
|
||||
"copy": "複製",
|
||||
"copy_code": "複製程式碼",
|
||||
"copy_link": "複製連結",
|
||||
"count_contacts": "{value, plural, other {{value} 聯絡人} }",
|
||||
"count_responses": "{value, plural, other {{value} 回應} }",
|
||||
"create_new_organization": "建立新組織",
|
||||
"create_project": "建立專案",
|
||||
"create_segment": "建立區隔",
|
||||
@@ -178,7 +185,6 @@
|
||||
"created_at": "建立時間",
|
||||
"created_by": "建立者",
|
||||
"customer_success": "客戶成功",
|
||||
"danger_zone": "危險區域",
|
||||
"dark_overlay": "深色覆蓋",
|
||||
"date": "日期",
|
||||
"default": "預設",
|
||||
@@ -198,10 +204,15 @@
|
||||
"e_commerce": "電子商務",
|
||||
"edit": "編輯",
|
||||
"email": "電子郵件",
|
||||
"ending_card": "結尾卡片",
|
||||
"enterprise_license": "企業授權",
|
||||
"environment_not_found": "找不到環境",
|
||||
"environment_notice": "您目前在 '{'environment'}' 環境中。",
|
||||
"error": "錯誤",
|
||||
"error_component_description": "此資源不存在或您沒有存取權限。",
|
||||
"error_component_title": "載入資源錯誤",
|
||||
"error_rate_limit_description": "已達 到最大 請求 次數。請 稍後 再試。",
|
||||
"error_rate_limit_title": "限流超過",
|
||||
"expand_rows": "展開列",
|
||||
"finish": "完成",
|
||||
"follow_these": "按照這些步驟",
|
||||
@@ -233,11 +244,9 @@
|
||||
"label": "標籤",
|
||||
"language": "語言",
|
||||
"learn_more": "瞭解更多",
|
||||
"license": "授權",
|
||||
"light_overlay": "淺色覆蓋",
|
||||
"limits_reached": "已達上限",
|
||||
"link": "連結",
|
||||
"link_and_email": "連結與電子郵件",
|
||||
"link_survey": "連結問卷",
|
||||
"link_surveys": "連結問卷",
|
||||
"load_more": "載入更多",
|
||||
@@ -253,18 +262,20 @@
|
||||
"membership_not_found": "找不到成員資格",
|
||||
"metadata": "元數據",
|
||||
"minimum": "最小值",
|
||||
"mobile_overlay_text": "Formbricks 不適用於較小解析度的裝置。",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
|
||||
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
|
||||
"mobile_overlay_title": "糟糕 ,偵測到小螢幕!",
|
||||
"move_down": "下移",
|
||||
"move_up": "上移",
|
||||
"multiple_languages": "多種語言",
|
||||
"name": "名稱",
|
||||
"new": "新增",
|
||||
"new_survey": "新增問卷",
|
||||
"new_version_available": "Formbricks '{'version'}' 已推出。立即升級!",
|
||||
"next": "下一步",
|
||||
"no_background_image_found": "找不到背景圖片。",
|
||||
"no_code": "無程式碼",
|
||||
"no_files_uploaded": "沒有上傳任何檔案",
|
||||
"no_quotas_found": "找不到 配額",
|
||||
"no_result_found": "找不到結果",
|
||||
"no_results": "沒有結果",
|
||||
"no_surveys_found": "找不到問卷。",
|
||||
@@ -284,6 +295,7 @@
|
||||
"organization": "組織",
|
||||
"organization_id": "組織 ID",
|
||||
"organization_not_found": "找不到組織",
|
||||
"organization_settings": "組織設定",
|
||||
"organization_teams_not_found": "找不到組織團隊",
|
||||
"other": "其他",
|
||||
"others": "其他",
|
||||
@@ -307,6 +319,7 @@
|
||||
"product_manager": "產品經理",
|
||||
"profile": "個人資料",
|
||||
"profile_id": "個人資料 ID",
|
||||
"progress": "進度",
|
||||
"project_configuration": "專案組態",
|
||||
"project_creation_description": "組織調查 在 專案中以便更好地存取控制。",
|
||||
"project_id": "專案 ID",
|
||||
@@ -318,6 +331,9 @@
|
||||
"question": "問題",
|
||||
"question_id": "問題 ID",
|
||||
"questions": "問題",
|
||||
"quota": "配額",
|
||||
"quotas": "額度",
|
||||
"quotas_description": "限制 擁有 特定 條件 的 參與者 所 提供 的 回應 數量。",
|
||||
"read_docs": "閱讀文件",
|
||||
"recipients": "收件者",
|
||||
"remove": "移除",
|
||||
@@ -336,7 +352,6 @@
|
||||
"save": "儲存",
|
||||
"save_changes": "儲存變更",
|
||||
"saving": "儲存",
|
||||
"scheduled": "已排程",
|
||||
"search": "搜尋",
|
||||
"security": "安全性",
|
||||
"segment": "區隔",
|
||||
@@ -366,6 +381,7 @@
|
||||
"start_free_trial": "開始免費試用",
|
||||
"status": "狀態",
|
||||
"step_by_step_manual": "逐步手冊",
|
||||
"storage_not_configured": "檔案儲存未設定,上傳可能會失敗",
|
||||
"styling": "樣式設定",
|
||||
"submit": "提交",
|
||||
"summary": "摘要",
|
||||
@@ -376,10 +392,8 @@
|
||||
"survey_live": "問卷已上線",
|
||||
"survey_not_found": "找不到問卷",
|
||||
"survey_paused": "問卷已暫停。",
|
||||
"survey_scheduled": "問卷已排程。",
|
||||
"survey_type": "問卷類型",
|
||||
"surveys": "問卷",
|
||||
"switch_organization": "切換組織",
|
||||
"switch_to": "切換至 '{'environment'}'",
|
||||
"table_items_deleted_successfully": "'{'type'}' 已成功刪除",
|
||||
"table_settings": "表格設定",
|
||||
@@ -577,8 +591,7 @@
|
||||
"contacts_table_refresh": "重新整理聯絡人",
|
||||
"contacts_table_refresh_success": "聯絡人已成功重新整理",
|
||||
"delete_contact_confirmation": "這將刪除與此聯繫人相關的所有調查回應和聯繫屬性。任何基於此聯繫人數據的定位和個性化將會丟失。",
|
||||
"first_name": "名字",
|
||||
"last_name": "姓氏",
|
||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {這將刪除與這個 contact 相關的所有調查響應和聯繫人屬性。基於這個 contact 數據的任何定向和個性化功能將會丟失。如果這個 contact 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。} other {這將刪除與這些 contacts 相關的所有調查響應和聯繫人屬性。基於這些 contacts 數據的任何定向和個性化功能將會丟失。如果這些 contacts 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。}}",
|
||||
"no_responses_found": "找不到回應",
|
||||
"not_provided": "未提供",
|
||||
"search_contact": "搜尋聯絡人",
|
||||
@@ -739,7 +752,6 @@
|
||||
},
|
||||
"project": {
|
||||
"api_keys": {
|
||||
"access_control": "存取控制",
|
||||
"add_api_key": "新增 API 金鑰",
|
||||
"api_key": "API 金鑰",
|
||||
"api_key_copied_to_clipboard": "API 金鑰已複製到剪貼簿",
|
||||
@@ -759,45 +771,21 @@
|
||||
"unable_to_delete_api_key": "無法刪除 API 金鑰"
|
||||
},
|
||||
"app-connection": {
|
||||
"api_host_description": "這是您 Formbricks 後端的網址。",
|
||||
"app_connection": "應用程式連線",
|
||||
"app_connection_description": "將您的應用程式連線至 Formbricks。",
|
||||
"cache_update_delay_description": "當您對調查、聯絡人、操作或其他資料進行更新時,可能需要長達 5 分鐘這些變更才能顯示在執行 Formbricks SDK 的本地應用程式中。此延遲是因我們目前快取系統的限制。我們正積極重新設計快取,並將在 Formbricks 4.0 中發佈修補程式。",
|
||||
"cache_update_delay_title": "更改將於 5 分鐘後因快取而反映",
|
||||
"check_out_the_docs": "查看文件。",
|
||||
"dive_into_the_docs": "深入瞭解文件。",
|
||||
"does_your_widget_work": "您的小工具運作嗎?",
|
||||
"environment_id": "您的 EnvironmentId",
|
||||
"environment_id_description": "此 ID 可唯一識別此 Formbricks 環境。",
|
||||
"environment_id_description_with_environment_id": "用於識別正確的環境:'{'environmentId'}' 是您的。",
|
||||
"formbricks_sdk": "Formbricks SDK",
|
||||
"formbricks_sdk_connected": "Formbricks SDK 已連線",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK 尚未連線。",
|
||||
"formbricks_sdk_not_connected_description": "將您的網站或應用程式與 Formbricks 連線",
|
||||
"have_a_problem": "有問題嗎?",
|
||||
"how_to_setup": "如何設定",
|
||||
"how_to_setup_description": "請按照這些步驟在您的應用程式中設定 Formbricks 小工具。",
|
||||
"identifying_your_users": "識別您的使用者",
|
||||
"if_you_are_planning_to": "如果您計劃",
|
||||
"insert_this_code_into_the": "將此程式碼插入",
|
||||
"need_a_more_detailed_setup_guide_for": "需要更詳細的設定指南,適用於",
|
||||
"not_working": "無法運作?",
|
||||
"open_an_issue_on_github": "在 GitHub 上開啟問題",
|
||||
"open_the_browser_console_to_see_the_logs": "開啟瀏覽器主控台以查看記錄。",
|
||||
"receiving_data": "正在接收資料 \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "重新檢查",
|
||||
"scroll_to_the_top": "捲動至頂端!",
|
||||
"step_1": "步驟 1:使用 pnpm、npm 或 yarn 安裝",
|
||||
"step_2": "步驟 2:初始化小工具",
|
||||
"step_2_description": "匯入 Formbricks 並在您的元件中初始化小工具(例如,App.tsx):",
|
||||
"step_3": "步驟 3:偵錯模式",
|
||||
"switch_on_the_debug_mode_by_appending": "藉由附加以下項目開啟偵錯模式",
|
||||
"tag_of_your_app": "您應用程式的標籤",
|
||||
"to_the_url_where_you_load_the": "到您載入",
|
||||
"want_to_learn_how_to_add_user_attributes": "想瞭解如何新增使用者屬性、自訂事件等嗎?",
|
||||
"you_are_done": "您已完成 \uD83C\uDF89",
|
||||
"you_can_set_the_user_id_with": "您可以使用 user id 設定",
|
||||
"your_app_now_communicates_with_formbricks": "您的應用程式現在可與 Formbricks 通訊 - 自動傳送事件和載入問卷!"
|
||||
"setup_alert_description": "遵循 此 分步 教程 ,在 5 分鐘 內 將您的應用程式 或 網站 連線 。",
|
||||
"setup_alert_title": "如何 連線"
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_project": "這是您唯一的專案,無法刪除。請先建立新專案。",
|
||||
@@ -989,7 +977,6 @@
|
||||
"free": "免費",
|
||||
"free_description": "無限問卷、團隊成員等。",
|
||||
"get_2_months_free": "免費獲得 2 個月",
|
||||
"get_in_touch": "取得聯繫",
|
||||
"hosted_in_frankfurt": "託管在 Frankfurt",
|
||||
"ios_android_sdks": "iOS 和 Android SDK 用於行動問卷",
|
||||
"link_surveys": "連結問卷(可分享)",
|
||||
@@ -1111,9 +1098,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "帳戶刪除後果",
|
||||
"avatar_update_failed": "頭像更新失敗。請再試一次。",
|
||||
"backup_code": "備份碼",
|
||||
"change_image": "變更圖片",
|
||||
"confirm_delete_account": "刪除您的帳戶以及您的所有個人資訊和資料",
|
||||
"confirm_delete_my_account": "刪除我的帳戶",
|
||||
"confirm_your_current_password_to_get_started": "確認您目前的密碼以開始使用。",
|
||||
@@ -1124,17 +1109,13 @@
|
||||
"email_change_initiated": "您的 email 更改請求已啟動。",
|
||||
"enable_two_factor_authentication": "啟用雙重驗證",
|
||||
"enter_the_code_from_your_authenticator_app_below": "在下方輸入您驗證器應用程式中的程式碼。",
|
||||
"file_size_must_be_less_than_10mb": "檔案大小必須小於 10MB。",
|
||||
"invalid_file_type": "無效的檔案類型。僅允許 JPEG、PNG 和 WEBP 檔案。",
|
||||
"lost_access": "無法存取",
|
||||
"or_enter_the_following_code_manually": "或手動輸入下列程式碼:",
|
||||
"organization_identification": "協助您的組織在 Formbricks 上識別您",
|
||||
"organizations_delete_message": "您是這些組織的唯一擁有者,因此它們也 <b>將被刪除。</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "永久移除您的所有個人資訊和資料",
|
||||
"personal_information": "個人資訊",
|
||||
"please_enter_email_to_confirm_account_deletion": "請在以下欄位中輸入 '{'email'}' 以確認永久刪除您的帳戶:",
|
||||
"profile_updated_successfully": "您的個人資料已成功更新",
|
||||
"remove_image": "移除圖片",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "將下列備份碼儲存在安全的地方。",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "使用您的驗證器應用程式掃描下方的 QR 碼。",
|
||||
"security_description": "管理您的密碼和其他安全性設定,例如雙重驗證 (2FA)。",
|
||||
@@ -1144,10 +1125,8 @@
|
||||
"two_factor_code": "雙重驗證碼",
|
||||
"unlock_two_factor_authentication": "使用更高等級的方案解鎖雙重驗證",
|
||||
"update_personal_info": "更新您的個人資訊",
|
||||
"upload_image": "上傳圖片",
|
||||
"warning_cannot_delete_account": "您是此組織的唯一擁有者。請先將所有權轉讓給其他成員。",
|
||||
"warning_cannot_undo": "此操作無法復原",
|
||||
"you_must_select_a_file": "您必須選取檔案。"
|
||||
"warning_cannot_undo": "此操作無法復原"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "將成員新增至團隊並確定其角色。",
|
||||
@@ -1259,9 +1238,7 @@
|
||||
"automatically_close_survey_after": "在指定時間自動關閉問卷",
|
||||
"automatically_close_the_survey_after_a_certain_number_of_responses": "在收到一定數量的回覆後自動關閉問卷。",
|
||||
"automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "如果用戶在特定秒數後未回應,則自動關閉問卷。",
|
||||
"automatically_closes_the_survey_at_the_beginning_of_the_day_utc": "在指定日期(UTC時間)自動關閉問卷。",
|
||||
"automatically_mark_the_survey_as_complete_after": "在指定時間後自動將問卷標記為完成",
|
||||
"automatically_release_the_survey_at_the_beginning_of_the_day_utc": "在指定日期(UTC時間)自動發佈問卷。",
|
||||
"back_button_label": "「返回」按鈕標籤",
|
||||
"background_styling": "背景樣式設定",
|
||||
"brand_color": "品牌顏色",
|
||||
@@ -1309,14 +1286,13 @@
|
||||
"choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。",
|
||||
"choose_where_to_run_the_survey": "選擇在哪裡執行問卷。",
|
||||
"city": "城市",
|
||||
"close_survey_on_date": "在指定日期關閉問卷",
|
||||
"close_survey_on_response_limit": "在回應次數上限關閉問卷",
|
||||
"color": "顏色",
|
||||
"column_used_in_logic_error": "此 column 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"columns": "欄位",
|
||||
"company": "公司",
|
||||
"company_logo": "公司標誌",
|
||||
"completed_responses": "部分或完整答复。",
|
||||
"completed_responses": "完成 回應",
|
||||
"concat": "串連 +",
|
||||
"conditional_logic": "條件邏輯",
|
||||
"confirm_default_language": "確認預設語言",
|
||||
@@ -1356,6 +1332,7 @@
|
||||
"end_screen_card": "結束畫面卡片",
|
||||
"ending_card": "結尾卡片",
|
||||
"ending_card_used_in_logic": "此結尾卡片用於問題 '{'questionIndex'}' 的邏輯中。",
|
||||
"ending_used_in_quota": "此 結尾 正被使用於 \"{quotaName}\" 配額中",
|
||||
"ends_with": "結尾為",
|
||||
"equals": "等於",
|
||||
"equals_one_of": "等於其中之一",
|
||||
@@ -1366,6 +1343,7 @@
|
||||
"fallback_for": "備用 用於 ",
|
||||
"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}\" 配額中",
|
||||
"field_name_eg_score_price": "欄位名稱,例如:分數、價格",
|
||||
"first_name": "名字",
|
||||
"five_points_recommended": "5 分(建議)",
|
||||
@@ -1397,8 +1375,9 @@
|
||||
"follow_ups_modal_action_subject_placeholder": "電子郵件主旨",
|
||||
"follow_ups_modal_action_to_description": "傳送電子郵件的電子郵件地址",
|
||||
"follow_ups_modal_action_to_label": "收件者",
|
||||
"follow_ups_modal_action_to_warning": "問卷中未偵測到電子郵件欄位",
|
||||
"follow_ups_modal_action_to_warning": "未找到 發送電子郵件 有效選項,請添加 一些 開放文本 / 聯絡資訊 問題或隱藏欄位",
|
||||
"follow_ups_modal_create_heading": "建立新的後續追蹤",
|
||||
"follow_ups_modal_created_successfull_toast": "後續 動作 已 建立 並 將 在 你 儲存 調查 後 儲存",
|
||||
"follow_ups_modal_edit_heading": "編輯此後續追蹤",
|
||||
"follow_ups_modal_edit_no_id": "未提供問卷後續追蹤 ID,無法更新問卷後續追蹤",
|
||||
"follow_ups_modal_name_label": "後續追蹤名稱",
|
||||
@@ -1408,8 +1387,9 @@
|
||||
"follow_ups_modal_trigger_label": "觸發器",
|
||||
"follow_ups_modal_trigger_type_ending": "回應者看到特定結尾",
|
||||
"follow_ups_modal_trigger_type_ending_select": "選取結尾:",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "問卷中找不到結尾!",
|
||||
"follow_ups_modal_trigger_type_ending_warning": "請選擇至少一個結尾或更改觸發類型",
|
||||
"follow_ups_modal_trigger_type_response": "回應者完成問卷",
|
||||
"follow_ups_modal_updated_successfull_toast": "後續 動作 已 更新 並 將 在 你 儲存 調查 後 儲存",
|
||||
"follow_ups_new": "新增後續追蹤",
|
||||
"follow_ups_upgrade_button_text": "升級以啟用後續追蹤",
|
||||
"form_styling": "表單樣式設定",
|
||||
@@ -1510,6 +1490,38 @@
|
||||
"question_duplicated": "問題已複製。",
|
||||
"question_id_updated": "問題 ID 已更新",
|
||||
"question_used_in_logic": "此問題用於問題 '{'questionIndex'}' 的邏輯中。",
|
||||
"question_used_in_quota": "此問題 正被使用於 \"{quotaName}\" 配額中",
|
||||
"quotas": {
|
||||
"add_quota": "新增額度",
|
||||
"change_quota_for_public_survey": "更改 公開 問卷 的 額度?",
|
||||
"confirm_quota_changes": "確認配額變更",
|
||||
"confirm_quota_changes_body": "您的 配額 中有 未儲存 的 變更。您 要 先 儲存 它們 再 離開 嗎?",
|
||||
"continue_survey_normally": "正常 繼續 問卷",
|
||||
"count_partial_submissions": "計算 部分提交",
|
||||
"count_partial_submissions_description": "包括符合配額標準但未完成問卷的受訪者",
|
||||
"create_quota_for_public_survey": "為 公開 問卷 建立 額度?",
|
||||
"create_quota_for_public_survey_description": "只有 未來 的 答案 會 被 篩選 進 配額",
|
||||
"create_quota_for_public_survey_text": "這個 調查 已經 是 公開 的。 現有 的 回應 將 不會 被 納入 新 額度 的 考量。",
|
||||
"delete_quota_confirmation_text": "這將永久刪除配額 {quotaName}。",
|
||||
"duplicate_quota": "複製 配額",
|
||||
"edit_quota": "編輯 配額",
|
||||
"end_survey_for_matching_participants": "結束問卷調查 對於 符合條件的參加者",
|
||||
"inclusion_criteria": "納入 條件",
|
||||
"limit_must_be_greater_than_or_equal_to_the_number_of_responses": "{value, plural, other {您已經有 {value} 個 回應 對於 此 配額,因此 限制 必須大於 {value}。} }",
|
||||
"limited_to_x_responses": "限制為 {limit}",
|
||||
"new_quota": "新 配額",
|
||||
"quota_created_successfull_toast": "配額已成功建立。",
|
||||
"quota_deleted_successfull_toast": "配額已成功刪除。",
|
||||
"quota_duplicated_successfull_toast": "配額已成功複製。",
|
||||
"quota_name_placeholder": "例如, 年齡 18-25 參與者",
|
||||
"quota_updated_successfull_toast": "配額已成功更新",
|
||||
"response_limit": "限制",
|
||||
"save_changes_confirmation_body": "任何 變更 包括 條件 只 影響 未來 的 回覆。\n 我們 推薦 複製 現有 的 配額 或 創建 新 的 配額。",
|
||||
"save_changes_confirmation_text": "現有 回應 留在 配額 內",
|
||||
"select_ending_card": "選取結尾卡片",
|
||||
"upgrade_prompt_title": "使用 額度 與 更高 的 計劃",
|
||||
"when_quota_has_been_reached": "當 配額 已達"
|
||||
},
|
||||
"randomize_all": "全部隨機排序",
|
||||
"randomize_all_except_last": "全部隨機排序(最後一項除外)",
|
||||
"range": "範圍",
|
||||
@@ -1517,7 +1529,6 @@
|
||||
"redirect_thank_you_card": "重新導向感謝卡片",
|
||||
"redirect_to_url": "重新導向至網址",
|
||||
"redirect_to_url_not_available_on_free_plan": "重新導向至網址在免費方案中不可用",
|
||||
"release_survey_on_date": "在指定日期發佈問卷",
|
||||
"remove_description": "移除描述",
|
||||
"remove_translations": "移除翻譯",
|
||||
"require_answer": "要求回答",
|
||||
@@ -1604,6 +1615,7 @@
|
||||
"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": "已使用此變數名稱,請選擇另一個名稱。",
|
||||
"variable_name_must_start_with_a_letter": "變數名稱必須以字母開頭。",
|
||||
"verify_email_before_submission": "提交前驗證電子郵件",
|
||||
@@ -1638,11 +1650,14 @@
|
||||
"address_line_2": "地址 2",
|
||||
"an_error_occurred_deleting_the_tag": "刪除標籤時發生錯誤",
|
||||
"browser": "瀏覽器",
|
||||
"bulk_delete_response_quotas": "回應 屬於 此 調查 的 配額 一部分 . 你 想 如何 處理 配額?",
|
||||
"city": "城市",
|
||||
"company": "公司",
|
||||
"completed": "已完成 ✅",
|
||||
"country": "國家/地區",
|
||||
"decrement_quotas": "減少所有配額限制,包括此回應",
|
||||
"delete_response_confirmation": "這將刪除調查響應,包括所有回答、標籤、附件文件以及響應元數據。",
|
||||
"delete_response_quotas": "回應 屬於 此 調查 的 配額 一部分 . 你 想 如何 處理 配額?",
|
||||
"device": "裝置",
|
||||
"device_info": "裝置資訊",
|
||||
"email": "電子郵件",
|
||||
@@ -1715,10 +1730,8 @@
|
||||
"language_help_text": "中 繼資料 會 根據 URL 中 的 `lang` 值 載入。",
|
||||
"link_description": "連結描述",
|
||||
"link_description_description": "描述在 55 - 200 個字符之間的表現最好。",
|
||||
"link_description_placeholder": "幫助 我們 改善 , 分享 您 的 想法 。",
|
||||
"link_title": "連結標題",
|
||||
"link_title_description": "短 標題 在 Meta Titles 中表現最佳。",
|
||||
"link_title_placeholder": "顧客 回饋 調查",
|
||||
"preview_image": "預覽 圖片",
|
||||
"preview_image_description": "景觀 圖片 檔案 大小 小於 4MB 效果 最佳。",
|
||||
"title": "連結 設定"
|
||||
@@ -1776,6 +1789,7 @@
|
||||
"configure_alerts": "設定警示",
|
||||
"congrats": "恭喜!您的問卷已上線。",
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "將您的網站或應用程式與 Formbricks 連線以開始使用。",
|
||||
"current_count": "目前計數",
|
||||
"custom_range": "自訂範圍...",
|
||||
"delete_all_existing_responses_and_displays": "刪除 所有 現有 回應 和 顯示",
|
||||
"download_qr_code": "下載 QR code",
|
||||
@@ -1829,6 +1843,7 @@
|
||||
"last_month": "上個月",
|
||||
"last_quarter": "上一季",
|
||||
"last_year": "去年",
|
||||
"limit": "限制",
|
||||
"no_responses_found": "找不到回應",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整體",
|
||||
@@ -1837,6 +1852,8 @@
|
||||
"qr_code_download_failed": "QR code 下載失敗",
|
||||
"qr_code_download_with_start_soon": "QR code 下載即將開始",
|
||||
"qr_code_generation_failed": "載入調查 QR Code 時發生問題。請再試一次。",
|
||||
"quotas_completed": "配額 已完成",
|
||||
"quotas_completed_tooltip": "受訪者完成的 配額 數量。",
|
||||
"reset_survey": "重設問卷",
|
||||
"reset_survey_warning": "重置 調查 會 移除 與 此 調查 相關 的 所有 回應 和 顯示 。 這 是 不可 撤銷 的 。",
|
||||
"selected_responses_csv": "選擇的回應 (CSV)",
|
||||
@@ -1852,7 +1869,7 @@
|
||||
"this_quarter": "本季",
|
||||
"this_year": "今年",
|
||||
"time_to_complete": "完成時間",
|
||||
"ttc_tooltip": "完成問卷的平均時間。",
|
||||
"ttc_tooltip": "完成 問題 的 平均 時間。",
|
||||
"unknown_question_type": "未知的問題類型",
|
||||
"use_personal_links": "使用 個人 連結",
|
||||
"waiting_for_response": "正在等待回應 \uD83E\uDDD8♂️",
|
||||
@@ -1863,7 +1880,6 @@
|
||||
"survey_deleted_successfully": "問卷已成功刪除!",
|
||||
"survey_duplicated_successfully": "問卷已成功複製。",
|
||||
"survey_duplication_error": "無法複製問卷。",
|
||||
"survey_status_tooltip": "若要更新問卷狀態,請更新問卷回應選項中的排程和關閉設定。",
|
||||
"templates": {
|
||||
"all_channels": "所有管道",
|
||||
"all_industries": "所有產業",
|
||||
|
||||
@@ -31,7 +31,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
SESSION_MAX_AGE: 86400,
|
||||
NEXTAUTH_SECRET: "test-secret",
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
ENCRYPTION_KEY: "test-encryption-key-32-chars-long",
|
||||
ENCRYPTION_KEY: "12345678901234567890123456789012", // 32 bytes for AES-256
|
||||
REDIS_URL: undefined,
|
||||
AUDIT_LOG_ENABLED: false,
|
||||
AUDIT_LOG_GET_USER_IP: false,
|
||||
@@ -148,7 +148,6 @@ describe("authOptions", () => {
|
||||
email: mockUser.email,
|
||||
password: mockHashedPassword,
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "http://example.com/avatar.png",
|
||||
twoFactorEnabled: false,
|
||||
};
|
||||
|
||||
@@ -161,7 +160,6 @@ describe("authOptions", () => {
|
||||
id: fakeUser.id,
|
||||
email: fakeUser.email,
|
||||
emailVerified: fakeUser.emailVerified,
|
||||
imageUrl: fakeUser.imageUrl,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,7 +261,7 @@ describe("authOptions", () => {
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue(); // Rate limiting passes
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue(mockUser as any);
|
||||
|
||||
const credentials = { token: createToken(mockUser.id, mockUser.email) };
|
||||
const credentials = { token: createToken(mockUser.id) };
|
||||
|
||||
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow(
|
||||
"Email already verified"
|
||||
@@ -282,7 +280,7 @@ describe("authOptions", () => {
|
||||
groupId: null,
|
||||
} as any);
|
||||
|
||||
const credentials = { token: createToken(mockUserId, mockUser.email) };
|
||||
const credentials = { token: createToken(mockUserId) };
|
||||
|
||||
const result = await tokenProvider.options.authorize(credentials, {});
|
||||
expect(result.email).toBe(mockUser.email);
|
||||
@@ -305,7 +303,7 @@ describe("authOptions", () => {
|
||||
groupId: null,
|
||||
} as any);
|
||||
|
||||
const credentials = { token: createToken(mockUserId, mockUser.email) };
|
||||
const credentials = { token: createToken(mockUserId) };
|
||||
|
||||
await tokenProvider.options.authorize(credentials, {});
|
||||
|
||||
@@ -317,7 +315,7 @@ describe("authOptions", () => {
|
||||
new Error("Maximum number of requests reached. Please try again later.")
|
||||
);
|
||||
|
||||
const credentials = { token: createToken(mockUserId, mockUser.email) };
|
||||
const credentials = { token: createToken(mockUserId) };
|
||||
|
||||
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow(
|
||||
"Maximum number of requests reached. Please try again later."
|
||||
@@ -341,7 +339,7 @@ describe("authOptions", () => {
|
||||
groupId: null,
|
||||
} as any);
|
||||
|
||||
const credentials = { token: createToken(mockUserId, mockUser.email) };
|
||||
const credentials = { token: createToken(mockUserId) };
|
||||
|
||||
await tokenProvider.options.authorize(credentials, {});
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
logger.info(emailData, "Sending email");
|
||||
|
||||
const transporter = createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
@@ -111,7 +113,7 @@ export const sendVerificationEmail = async ({
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
const t = await getTranslate();
|
||||
const token = createToken(id, email, {
|
||||
const token = createToken(id, {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
const verifyLink = `${WEBAPP_URL}/auth/verify?token=${encodeURIComponent(token)}`;
|
||||
@@ -136,7 +138,7 @@ export const sendForgotPasswordEmail = async (user: {
|
||||
locale: TUserLocale;
|
||||
}): Promise<boolean> => {
|
||||
const t = await getTranslate();
|
||||
const token = createToken(user.id, user.email, {
|
||||
const token = createToken(user.id, {
|
||||
expiresIn: "1d",
|
||||
});
|
||||
const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`;
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("contact-survey page", () => {
|
||||
params: Promise.resolve({ jwt: "token" }),
|
||||
searchParams: Promise.resolve({}),
|
||||
});
|
||||
expect(meta).toEqual({ title: "Survey", description: "Complete this survey" });
|
||||
expect(meta).toEqual({ title: "Survey", description: "Please complete this survey." });
|
||||
});
|
||||
|
||||
test("generateMetadata returns default when verify throws", async () => {
|
||||
@@ -103,7 +103,7 @@ describe("contact-survey page", () => {
|
||||
params: Promise.resolve({ jwt: "token" }),
|
||||
searchParams: Promise.resolve({}),
|
||||
});
|
||||
expect(meta).toEqual({ title: "Survey", description: "Complete this survey" });
|
||||
expect(meta).toEqual({ title: "Survey", description: "Please complete this survey." });
|
||||
});
|
||||
|
||||
test("generateMetadata returns basic metadata when token valid", async () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const generateMetadata = async (props: ContactSurveyPageProps): Promise<M
|
||||
if (!result.ok) {
|
||||
return {
|
||||
title: "Survey",
|
||||
description: "Complete this survey",
|
||||
description: "Please complete this survey.",
|
||||
};
|
||||
}
|
||||
const { surveyId } = result.data;
|
||||
@@ -40,7 +40,7 @@ export const generateMetadata = async (props: ContactSurveyPageProps): Promise<M
|
||||
// If the token is invalid, we'll return generic metadata
|
||||
return {
|
||||
title: "Survey",
|
||||
description: "Complete this survey",
|
||||
description: "Please complete this survey.",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("Metadata Utils", () => {
|
||||
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
|
||||
expect(result).toEqual({
|
||||
title: "Survey",
|
||||
description: "Complete this survey",
|
||||
description: "Please complete this survey.",
|
||||
survey: null,
|
||||
ogImage: undefined,
|
||||
});
|
||||
@@ -108,10 +108,9 @@ describe("Metadata Utils", () => {
|
||||
const result = await getBasicSurveyMetadata(mockSurveyId);
|
||||
|
||||
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
|
||||
expect(getProjectByEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
|
||||
expect(result).toEqual({
|
||||
title: "Welcome Headline | Test Project",
|
||||
description: "Complete this survey",
|
||||
title: "Welcome Headline",
|
||||
description: "Please complete this survey.",
|
||||
survey: mockSurvey,
|
||||
ogImage: undefined,
|
||||
});
|
||||
@@ -129,13 +128,12 @@ describe("Metadata Utils", () => {
|
||||
} as TSurvey;
|
||||
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValue({ name: "Test Project" } as any);
|
||||
|
||||
const result = await getBasicSurveyMetadata(mockSurveyId);
|
||||
|
||||
expect(result).toEqual({
|
||||
title: "Test Survey | Test Project",
|
||||
description: "Complete this survey",
|
||||
title: "Test Survey",
|
||||
description: "Please complete this survey.",
|
||||
survey: mockSurvey,
|
||||
ogImage: undefined,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
|
||||
import { getSurvey } from "@/modules/survey/lib/survey";
|
||||
import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
|
||||
import { Metadata } from "next";
|
||||
|
||||
type TBasicSurveyMetadata = {
|
||||
@@ -12,22 +12,16 @@ type TBasicSurveyMetadata = {
|
||||
ogImage?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to encode name for URL usage
|
||||
*/
|
||||
export const getNameForURL = (url: string) => url.replace(/ /g, "%20");
|
||||
export const getNameForURL = (value: string) => encodeURIComponent(value);
|
||||
|
||||
/**
|
||||
* Utility function to encode brand color for URL usage
|
||||
*/
|
||||
export const getBrandColorForURL = (url: string) => url.replace(/#/g, "%23");
|
||||
export const getBrandColorForURL = (value: string) => encodeURIComponent(value);
|
||||
|
||||
/**
|
||||
* Get basic survey metadata (title and description) based on link metadata, welcome card or survey name
|
||||
*/
|
||||
export const getBasicSurveyMetadata = async (
|
||||
surveyId: string,
|
||||
languageCode?: string
|
||||
languageCode = "default"
|
||||
): Promise<TBasicSurveyMetadata> => {
|
||||
const survey = await getSurvey(surveyId);
|
||||
|
||||
@@ -35,7 +29,7 @@ export const getBasicSurveyMetadata = async (
|
||||
if (!survey) {
|
||||
return {
|
||||
title: "Survey",
|
||||
description: "Complete this survey",
|
||||
description: "Please complete this survey.",
|
||||
survey: null,
|
||||
ogImage: undefined,
|
||||
};
|
||||
@@ -43,38 +37,33 @@ export const getBasicSurveyMetadata = async (
|
||||
|
||||
const metadata = survey.metadata;
|
||||
const welcomeCard = survey.welcomeCard;
|
||||
const useDefaultLanguageCode =
|
||||
languageCode === "default" ||
|
||||
survey.languages.find((lang) => lang.language.code === languageCode)?.default;
|
||||
|
||||
// Determine language code to use for metadata
|
||||
const langCode = languageCode || "default";
|
||||
const langCode = useDefaultLanguageCode ? "default" : languageCode;
|
||||
|
||||
// Set title - priority: custom link metadata > welcome card > survey name
|
||||
let title = "Survey";
|
||||
if (metadata.title?.[langCode]) {
|
||||
title = metadata.title[langCode];
|
||||
} else if (welcomeCard.enabled && welcomeCard.headline?.default) {
|
||||
title = welcomeCard.headline.default;
|
||||
} else {
|
||||
title = survey.name;
|
||||
}
|
||||
const titleFromMetadata = metadata?.title ? getLocalizedValue(metadata.title, langCode) || "" : undefined;
|
||||
const titleFromWelcome =
|
||||
welcomeCard?.enabled && welcomeCard.headline
|
||||
? getLocalizedValue(welcomeCard.headline, langCode) || ""
|
||||
: undefined;
|
||||
let title = titleFromMetadata || titleFromWelcome || survey.name;
|
||||
|
||||
// Set description - priority: custom link metadata > welcome card > default
|
||||
let description = "Complete this survey";
|
||||
if (metadata.description?.[langCode]) {
|
||||
description = metadata.description[langCode];
|
||||
}
|
||||
const descriptionFromMetadata = metadata?.description
|
||||
? getLocalizedValue(metadata.description, langCode) || ""
|
||||
: undefined;
|
||||
let description = descriptionFromMetadata || "Please complete this survey.";
|
||||
|
||||
// Get OG image from link metadata if available
|
||||
const { ogImage } = metadata;
|
||||
|
||||
// Add product name in title if it's Formbricks cloud and not using custom metadata
|
||||
if (!metadata.title?.[langCode]) {
|
||||
if (!titleFromMetadata) {
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
title = `${title} | Formbricks`;
|
||||
} else {
|
||||
const project = await getProjectByEnvironmentId(survey.environmentId);
|
||||
if (project) {
|
||||
title = `${title} | ${project.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +78,13 @@ export const getBasicSurveyMetadata = async (
|
||||
/**
|
||||
* Generate Open Graph metadata for survey
|
||||
*/
|
||||
export const getSurveyOpenGraphMetadata = (surveyId: string, surveyName: string): Metadata => {
|
||||
const brandColor = getBrandColorForURL(COLOR_DEFAULTS.brandColor); // Default color
|
||||
export const getSurveyOpenGraphMetadata = (
|
||||
surveyId: string,
|
||||
surveyName: string,
|
||||
surveyBrandColor?: string
|
||||
): Metadata => {
|
||||
const encodedName = getNameForURL(surveyName);
|
||||
|
||||
const brandColor = getBrandColorForURL(surveyBrandColor ?? COLOR_DEFAULTS.brandColor);
|
||||
const ogImgURL = `/api/v1/client/og?brandColor=${brandColor}&name=${encodedName}`;
|
||||
|
||||
return {
|
||||
|
||||
@@ -20,7 +20,7 @@ vi.mock("./lib/metadata-utils", () => ({
|
||||
describe("getMetadataForLinkSurvey", () => {
|
||||
const mockSurveyId = "survey-123";
|
||||
const mockSurveyName = "Test Survey";
|
||||
const mockDescription = "Complete this survey";
|
||||
const mockDescription = "Please complete this survey.";
|
||||
const mockOgImageUrl = "https://example.com/custom-image.png";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -60,7 +60,7 @@ describe("getMetadataForLinkSurvey", () => {
|
||||
|
||||
expect(getSurveyMetadata).toHaveBeenCalledWith(mockSurveyId);
|
||||
expect(getBasicSurveyMetadata).toHaveBeenCalledWith(mockSurveyId, undefined);
|
||||
expect(getSurveyOpenGraphMetadata).toHaveBeenCalledWith(mockSurveyId, mockSurveyName);
|
||||
expect(getSurveyOpenGraphMetadata).toHaveBeenCalledWith(mockSurveyId, mockSurveyName, undefined);
|
||||
|
||||
expect(result).toEqual({
|
||||
title: mockSurveyName,
|
||||
|
||||
@@ -15,9 +15,10 @@ export const getMetadataForLinkSurvey = async (
|
||||
|
||||
// Get enhanced metadata that includes custom link metadata
|
||||
const { title, description, ogImage } = await getBasicSurveyMetadata(surveyId, languageCode);
|
||||
const surveyBrandColor = survey.styling?.brandColor?.light;
|
||||
|
||||
// Use the shared function for creating the base metadata but override with custom data
|
||||
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title);
|
||||
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title, surveyBrandColor);
|
||||
|
||||
// Override with the custom image URL
|
||||
if (baseMetadata.openGraph) {
|
||||
|
||||
@@ -233,12 +233,31 @@ describe("ConditionsEditor", () => {
|
||||
expect(mockCallbacks.onDuplicateCondition).toHaveBeenCalledWith("cond1");
|
||||
});
|
||||
|
||||
test("calls onCreateGroup from the dropdown menu", async () => {
|
||||
test("calls onCreateGroup from the dropdown menu when enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConditionsEditor conditions={multipleConditions} config={mockConfig} callbacks={mockCallbacks} />
|
||||
);
|
||||
const createGroupButtons = screen.getAllByText("environments.surveys.edit.create_group");
|
||||
await user.click(createGroupButtons[0]); // Click the first one
|
||||
expect(mockCallbacks.onCreateGroup).toHaveBeenCalledWith("cond1");
|
||||
});
|
||||
|
||||
test("disables the 'Create Group' button when there's only one condition", () => {
|
||||
render(<ConditionsEditor conditions={singleCondition} config={mockConfig} callbacks={mockCallbacks} />);
|
||||
const createGroupButton = screen.getByText("environments.surveys.edit.create_group");
|
||||
await user.click(createGroupButton);
|
||||
expect(mockCallbacks.onCreateGroup).toHaveBeenCalledWith("cond1");
|
||||
expect(createGroupButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("enables the 'Create Group' button when there are multiple conditions", () => {
|
||||
render(
|
||||
<ConditionsEditor conditions={multipleConditions} config={mockConfig} callbacks={mockCallbacks} />
|
||||
);
|
||||
const createGroupButtons = screen.getAllByText("environments.surveys.edit.create_group");
|
||||
// Both buttons should be enabled since the main group has multiple conditions
|
||||
createGroupButtons.forEach((button) => {
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test("calls onToggleGroupConnector when the connector is changed", async () => {
|
||||
|
||||
@@ -233,7 +233,8 @@ export function ConditionsEditor({ conditions, config, callbacks, depth = 0 }: C
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => callbacks.onCreateGroup(condition.id)}
|
||||
icon={<WorkflowIcon className="h-4 w-4" />}>
|
||||
icon={<WorkflowIcon className="h-4 w-4" />}
|
||||
disabled={conditions.conditions.length <= 1}>
|
||||
{t("environments.surveys.edit.create_group")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -256,7 +256,7 @@ EOT
|
||||
fi
|
||||
|
||||
echo "📥 Downloading docker-compose.yml from Formbricks GitHub repository..."
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/docker-compose.yml
|
||||
|
||||
echo "🚙 Updating docker-compose.yml with your custom inputs..."
|
||||
sed -i "/WEBAPP_URL:/s|WEBAPP_URL:.*|WEBAPP_URL: \"https://$domain_name\"|" docker-compose.yml
|
||||
|
||||
@@ -765,7 +765,7 @@ export function Survey({
|
||||
<LanguageSwitch
|
||||
surveyLanguages={localSurvey.languages}
|
||||
setSelectedLanguageCode={setselectedLanguage}
|
||||
hoverColor={styling.inputColor?.light ?? "#000000"}
|
||||
hoverColor={styling.inputColor?.light ?? "#f8fafc"}
|
||||
borderRadius={styling.roundness ?? 8}
|
||||
/>
|
||||
)}
|
||||
@@ -776,7 +776,7 @@ export function Survey({
|
||||
{isCloseButtonVisible && (
|
||||
<SurveyCloseButton
|
||||
onClose={onClose}
|
||||
hoverColor={styling.inputColor?.light ?? "#000000"}
|
||||
hoverColor={styling.inputColor?.light ?? "#f8fafc"}
|
||||
borderRadius={styling.roundness ?? 8}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user