mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-23 05:17:49 -05:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0758f7526 | |||
| 4cfb8c6d7b | |||
| e74a51a5ff | |||
| 29cc6a10fe | |||
| 01f765e969 | |||
| 9366960f18 | |||
| 697dc9cc99 | |||
| 83bc272ed2 | |||
| 59cc9c564e | |||
| 20dc147682 | |||
| 2bb7a6f277 | |||
| deb062dd03 | |||
| 474be86d33 | |||
| e7ca66ed77 | |||
| 2b49dbecd3 | |||
| 6da4c6f352 | |||
| 659b240fca | |||
| 19c0b1d14d | |||
| b4472f48e9 | |||
| d197271771 | |||
| 37f652c70e | |||
| 645f0ab0d1 | |||
| 389a7d9e7b | |||
| c4cf468c7e | |||
| cbc3e923e4 | |||
| a96ba8b1e7 | |||
| e830871361 | |||
| 998e5c0819 | |||
| 13a56b0237 | |||
| 0b5418a03a | |||
| 0d8a338965 | |||
| d3250736a9 | |||
| e6ee6a6b0d | |||
| c0b097f929 | |||
| 78d336f8c7 | |||
| 95a7a265b9 | |||
| 136e59da68 | |||
| eb0a87cf80 | |||
| 0dcb98ac29 | |||
| 540f7aaae7 | |||
| 2d4614a0bd | |||
| 633bf18204 | |||
| 9a6cbd05b6 | |||
| 94b0248075 | |||
| 082de1042d | |||
| 8c19587baa | |||
| 433750d3fe | |||
| 61befd5ffd | |||
| 1e7817fb69 | |||
| f250bc7e88 | |||
| c7faa29437 | |||
| 902b8c92e2 | |||
| 17ba0f21af | |||
| a384743751 | |||
| dfa1c3e375 | |||
| 77c9302183 | |||
| 88da043c00 | |||
| 1cc3ceec55 | |||
| 50d15f6e07 |
+6
-1
@@ -185,6 +185,11 @@ ENTERPRISE_LICENSE_KEY=
|
|||||||
# Ignore Rate Limiting across the Formbricks app
|
# Ignore Rate Limiting across the Formbricks app
|
||||||
# RATE_LIMITING_DISABLED=1
|
# RATE_LIMITING_DISABLED=1
|
||||||
|
|
||||||
|
# Allow webhook URLs to point to internal/private network addresses (e.g. localhost, 192.168.x.x)
|
||||||
|
# WARNING: Only enable this if you understand the SSRF risks. Useful for self-hosted instances
|
||||||
|
# that need to send webhooks to internal services.
|
||||||
|
# DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS=1
|
||||||
|
|
||||||
# OpenTelemetry OTLP endpoint (base URL, exporters append /v1/traces and /v1/metrics)
|
# OpenTelemetry OTLP endpoint (base URL, exporters append /v1/traces and /v1/metrics)
|
||||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||||
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||||
@@ -231,4 +236,4 @@ REDIS_URL=redis://localhost:6379
|
|||||||
|
|
||||||
|
|
||||||
# Lingo.dev API key for translation generation
|
# Lingo.dev API key for translation generation
|
||||||
LINGODOTDEV_API_KEY=your_api_key_here
|
LINGO_API_KEY=your_api_key_here
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ We are using SonarQube to identify code smells and security hotspots.
|
|||||||
- Translations are in `apps/web/locales/`. Default is `en-US.json`.
|
- Translations are in `apps/web/locales/`. Default is `en-US.json`.
|
||||||
- Lingo.dev is automatically translating strings from en-US into other languages on commit. Run `pnpm i18n` to generate missing translations and validate keys.
|
- Lingo.dev is automatically translating strings from en-US into other languages on commit. Run `pnpm i18n` to generate missing translations and validate keys.
|
||||||
|
|
||||||
|
## Date and Time Rendering
|
||||||
|
|
||||||
|
- All user-facing dates and times must use shared formatting helpers instead of ad hoc `date-fns`, `Intl`, or `toLocale*` calls in components.
|
||||||
|
- Locale for display must come from the app language source of truth (`user.locale`, `getLocale()`, or `i18n.resolvedLanguage`), not browser defaults or implicit `undefined` locale behavior.
|
||||||
|
- Locale and time zone are different concerns: locale controls formatting, time zone controls the represented clock/calendar moment.
|
||||||
|
- Never infer a time zone from locale. If a product-level time zone source of truth exists, use it explicitly; otherwise preserve the existing semantic meaning of the stored value and avoid introducing browser-dependent conversions.
|
||||||
|
- Machine-facing values for storage, APIs, exports, integrations, and logs must remain stable and non-localized (`ISO 8601` / UTC where applicable).
|
||||||
|
|
||||||
## Database & Prisma Performance
|
## Database & Prisma Performance
|
||||||
|
|
||||||
- Multi-tenancy: All data must be scoped by Organization or Environment.
|
- Multi-tenancy: All data must be scoped by Organization or Environment.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||||
@@ -20,12 +21,12 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
throw new Error(t("common.environment_not_found"));
|
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
const project = await getProjectByEnvironmentId(environment.id);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.workspace_not_found"));
|
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = project.config.channel || null;
|
const channel = project.config.channel || null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
||||||
@@ -23,22 +24,22 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(t("common.session_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
const user = await getUser(session.user.id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
throw new Error(t("common.environment_not_found"));
|
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
const project = await getProjectByEnvironmentId(environment.id);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.workspace_not_found"));
|
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const projects = await getUserProjects(session.user.id, organizationId);
|
const projects = await getUserProjects(session.user.id, organizationId);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
import { getUser } from "@/lib/user/service";
|
import { getUser } from "@/lib/user/service";
|
||||||
@@ -25,7 +25,7 @@ const ProjectOnboardingLayout = async (props: {
|
|||||||
|
|
||||||
const user = await getUser(session.user.id);
|
const user = await getUser(session.user.id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
|
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
|
||||||
@@ -36,7 +36,7 @@ const ProjectOnboardingLayout = async (props: {
|
|||||||
|
|
||||||
const organization = await getOrganization(params.organizationId);
|
const organization = await getOrganization(params.organizationId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
@@ -28,7 +29,7 @@ const OnboardingLayout = async (props: {
|
|||||||
|
|
||||||
const organization = await getOrganization(params.organizationId);
|
const organization = await getOrganization(params.organizationId);
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
|
const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
|
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
|
||||||
@@ -45,7 +46,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
const isAccessControlAllowed = await getAccessControlPermission(organization.id);
|
const isAccessControlAllowed = await getAccessControlPermission(organization.id);
|
||||||
|
|
||||||
if (!organizationTeams) {
|
if (!organizationTeams) {
|
||||||
throw new Error(t("common.organization_teams_not_found"));
|
throw new ResourceNotFoundError(t("common.team"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicDomain = getPublicDomain();
|
const publicDomain = getPublicDomain();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
|
|
||||||
@@ -17,13 +18,13 @@ const SurveyEditorEnvironmentLayout = async (props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
throw new Error(t("common.environment_not_found"));
|
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
import {
|
||||||
|
AuthorizationError,
|
||||||
|
OperationNotAllowedError,
|
||||||
|
ResourceNotFoundError,
|
||||||
|
} from "@formbricks/types/errors";
|
||||||
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
@@ -46,7 +50,7 @@ export const createProjectAction = authenticatedActionClient.inputSchema(ZCreate
|
|||||||
const organization = await getOrganization(organizationId);
|
const organization = await getOrganization(organizationId);
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error("Organization not found");
|
throw new ResourceNotFoundError("Organization", organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.id);
|
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.id);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||||
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
@@ -42,7 +43,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
|||||||
|
|
||||||
// Validate that project permission exists for members
|
// Validate that project permission exists for members
|
||||||
if (isMember && !projectPermission) {
|
if (isMember && !projectPermission) {
|
||||||
throw new Error(t("common.workspace_permission_not_found"));
|
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
@@ -20,15 +21,15 @@ const AccountSettingsLayout = async (props: {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.workspace_not_found"));
|
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(t("common.session_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
@@ -146,18 +147,18 @@ const Page = async (props: {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(t("common.session_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
const autoDisableNotificationType = searchParams["type"];
|
const autoDisableNotificationType = searchParams["type"];
|
||||||
const autoDisableNotificationElementId = searchParams["elementId"];
|
const autoDisableNotificationElementId = searchParams["elementId"];
|
||||||
|
|
||||||
const [user, memberships] = await Promise.all([getUser(session.user.id), getMemberships(session.user.id)]);
|
const [user, memberships] = await Promise.all([getUser(session.user.id), getMemberships(session.user.id)]);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!memberships) {
|
if (!memberships) {
|
||||||
throw new Error(t("common.membership_not_found"));
|
throw new ResourceNotFoundError(t("common.membership"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user?.notificationSettings) {
|
if (user?.notificationSettings) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AuthenticationError } from "@formbricks/types/errors";
|
||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||||
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||||
@@ -28,7 +29,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
const user = session?.user ? await getUser(session.user.id) : null;
|
const user = session?.user ? await getUser(session.user.id) : null;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
|
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { AuthenticationError } from "@formbricks/types/errors";
|
||||||
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
@@ -25,7 +26,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(t("common.session_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.id);
|
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.id);
|
||||||
|
|||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { TFunction } from "i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
|
import type { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/types/enterprise-license";
|
||||||
|
import { Badge } from "@/modules/ui/components/badge";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||||
|
|
||||||
|
type TPublicLicenseFeatureKey = Exclude<keyof TEnterpriseLicenseFeatures, "isMultiOrgEnabled" | "ai">;
|
||||||
|
|
||||||
|
type TFeatureDefinition = {
|
||||||
|
key: TPublicLicenseFeatureKey;
|
||||||
|
labelKey: string;
|
||||||
|
docsUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFeatureDefinitions = (t: TFunction): TFeatureDefinition[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "contacts",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_contacts"),
|
||||||
|
docsUrl:
|
||||||
|
"https://formbricks.com/docs/self-hosting/advanced/enterprise-features/contact-management-segments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "projects",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_projects"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/self-hosting/advanced/license",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "whitelabel",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_whitelabel"),
|
||||||
|
docsUrl:
|
||||||
|
"https://formbricks.com/docs/self-hosting/advanced/enterprise-features/whitelabel-email-follow-ups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "removeBranding",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_remove_branding"),
|
||||||
|
docsUrl:
|
||||||
|
"https://formbricks.com/docs/self-hosting/advanced/enterprise-features/hide-powered-by-formbricks",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "twoFactorAuth",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_two_factor_auth"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/xm-and-surveys/core-features/user-management/two-factor-auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sso",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_sso"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/self-hosting/advanced/enterprise-features/oidc-sso",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "saml",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_saml"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/self-hosting/advanced/enterprise-features/saml-sso",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "spamProtection",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_spam_protection"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/xm-and-surveys/surveys/general-features/spam-protection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auditLogs",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_audit_logs"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/self-hosting/advanced/enterprise-features/audit-logging",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "accessControl",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_access_control"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/self-hosting/advanced/enterprise-features/team-access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "quotas",
|
||||||
|
labelKey: t("environments.settings.enterprise.license_feature_quotas"),
|
||||||
|
docsUrl: "https://formbricks.com/docs/xm-and-surveys/surveys/general-features/quota-management",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EnterpriseLicenseFeaturesTableProps {
|
||||||
|
features: TEnterpriseLicenseFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnterpriseLicenseFeaturesTable = ({ features }: EnterpriseLicenseFeaturesTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsCard
|
||||||
|
title={t("environments.settings.enterprise.license_features_table_title")}
|
||||||
|
description={t("environments.settings.enterprise.license_features_table_description")}
|
||||||
|
noPadding>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="hover:bg-white">
|
||||||
|
<TableHead>{t("environments.settings.enterprise.license_features_table_feature")}</TableHead>
|
||||||
|
<TableHead>{t("environments.settings.enterprise.license_features_table_access")}</TableHead>
|
||||||
|
<TableHead>{t("environments.settings.enterprise.license_features_table_value")}</TableHead>
|
||||||
|
<TableHead>{t("common.documentation")}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{getFeatureDefinitions(t).map((feature) => {
|
||||||
|
const value = features[feature.key];
|
||||||
|
const isEnabled = typeof value === "boolean" ? value : value === null || value > 0;
|
||||||
|
let displayValue: number | string = "—";
|
||||||
|
|
||||||
|
if (typeof value === "number") {
|
||||||
|
displayValue = value;
|
||||||
|
} else if (value === null) {
|
||||||
|
displayValue = t("environments.settings.enterprise.license_features_table_unlimited");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={feature.key} className="hover:bg-white">
|
||||||
|
<TableCell className="font-medium text-slate-900">{t(feature.labelKey)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
type={isEnabled ? "success" : "gray"}
|
||||||
|
size="normal"
|
||||||
|
text={
|
||||||
|
isEnabled
|
||||||
|
? t("environments.settings.enterprise.license_features_table_enabled")
|
||||||
|
: t("environments.settings.enterprise.license_features_table_disabled")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-slate-600">{displayValue}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link
|
||||||
|
href={feature.docsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm font-medium text-slate-700 underline underline-offset-2 hover:text-slate-900">
|
||||||
|
{t("common.read_docs")}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SettingsCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
-3
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { formatDateForDisplay, formatDateTimeForDisplay } from "@/lib/utils/datetime";
|
||||||
import { recheckLicenseAction } from "@/modules/ee/license-check/actions";
|
import { recheckLicenseAction } from "@/modules/ee/license-check/actions";
|
||||||
import type { TLicenseStatus } from "@/modules/ee/license-check/types/enterprise-license";
|
import type { TLicenseStatus } from "@/modules/ee/license-check/types/enterprise-license";
|
||||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
@@ -15,6 +16,7 @@ import { SettingsCard } from "../../../components/SettingsCard";
|
|||||||
|
|
||||||
interface EnterpriseLicenseStatusProps {
|
interface EnterpriseLicenseStatusProps {
|
||||||
status: TLicenseStatus;
|
status: TLicenseStatus;
|
||||||
|
lastChecked: Date;
|
||||||
gracePeriodEnd?: Date;
|
gracePeriodEnd?: Date;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
}
|
}
|
||||||
@@ -44,10 +46,12 @@ const getBadgeConfig = (
|
|||||||
|
|
||||||
export const EnterpriseLicenseStatus = ({
|
export const EnterpriseLicenseStatus = ({
|
||||||
status,
|
status,
|
||||||
|
lastChecked,
|
||||||
gracePeriodEnd,
|
gracePeriodEnd,
|
||||||
environmentId,
|
environmentId,
|
||||||
}: EnterpriseLicenseStatusProps) => {
|
}: EnterpriseLicenseStatusProps) => {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const locale = i18n.resolvedLanguage ?? i18n.language ?? "en-US";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isRechecking, setIsRechecking] = useState(false);
|
const [isRechecking, setIsRechecking] = useState(false);
|
||||||
|
|
||||||
@@ -92,7 +96,12 @@ export const EnterpriseLicenseStatus = ({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Badge type={badgeConfig.type} text={badgeConfig.label} size="normal" className="w-fit" />
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Badge type={badgeConfig.type} text={badgeConfig.label} size="normal" className="w-fit" />
|
||||||
|
<span className="text-sm text-slate-500">
|
||||||
|
{t("common.updated_at")} {formatDateTimeForDisplay(new Date(lastChecked), locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -118,7 +127,7 @@ export const EnterpriseLicenseStatus = ({
|
|||||||
<Alert variant="warning" size="small">
|
<Alert variant="warning" size="small">
|
||||||
<AlertDescription className="overflow-visible whitespace-normal">
|
<AlertDescription className="overflow-visible whitespace-normal">
|
||||||
{t("environments.settings.enterprise.license_unreachable_grace_period", {
|
{t("environments.settings.enterprise.license_unreachable_grace_period", {
|
||||||
gracePeriodEnd: new Date(gracePeriodEnd).toLocaleDateString(undefined, {
|
gracePeriodEnd: formatDateForDisplay(new Date(gracePeriodEnd), locale, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
+14
-9
@@ -10,6 +10,7 @@ import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { EnterpriseLicenseFeaturesTable } from "./components/EnterpriseLicenseFeaturesTable";
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
@@ -93,15 +94,19 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
/>
|
/>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{hasLicense ? (
|
{hasLicense ? (
|
||||||
<EnterpriseLicenseStatus
|
<>
|
||||||
status={licenseState.status}
|
<EnterpriseLicenseStatus
|
||||||
gracePeriodEnd={
|
status={licenseState.status}
|
||||||
licenseState.status === "unreachable"
|
lastChecked={licenseState.lastChecked}
|
||||||
? new Date(licenseState.lastChecked.getTime() + GRACE_PERIOD_MS)
|
gracePeriodEnd={
|
||||||
: undefined
|
licenseState.status === "unreachable"
|
||||||
}
|
? new Date(licenseState.lastChecked.getTime() + GRACE_PERIOD_MS)
|
||||||
environmentId={params.environmentId}
|
: undefined
|
||||||
/>
|
}
|
||||||
|
environmentId={params.environmentId}
|
||||||
|
/>
|
||||||
|
{licenseState.features && <EnterpriseLicenseFeaturesTable features={licenseState.features} />}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
@@ -17,15 +18,15 @@ const Layout = async (props: { params: Promise<{ environmentId: string }>; child
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.workspace_not_found"));
|
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(t("common.session_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
+2
-3
@@ -96,8 +96,8 @@ export const ResponseTable = ({
|
|||||||
const showQuotasColumn = isQuotasAllowed && quotas.length > 0;
|
const showQuotasColumn = isQuotasAllowed && quotas.length > 0;
|
||||||
// Generate columns
|
// Generate columns
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => generateResponseTableColumns(survey, isExpanded ?? false, isReadOnly, t, showQuotasColumn),
|
() => generateResponseTableColumns(survey, isExpanded ?? false, isReadOnly, locale, t, showQuotasColumn),
|
||||||
[survey, isExpanded, isReadOnly, t, showQuotasColumn]
|
[survey, isExpanded, isReadOnly, locale, t, showQuotasColumn]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save settings to localStorage when they change
|
// Save settings to localStorage when they change
|
||||||
@@ -300,7 +300,6 @@ export const ResponseTable = ({
|
|||||||
<DataTableSettingsModal
|
<DataTableSettingsModal
|
||||||
open={isTableSettingsModalOpen}
|
open={isTableSettingsModalOpen}
|
||||||
setOpen={setIsTableSettingsModalOpen}
|
setOpen={setIsTableSettingsModalOpen}
|
||||||
survey={survey}
|
|
||||||
table={table}
|
table={table}
|
||||||
columnOrder={columnOrder}
|
columnOrder={columnOrder}
|
||||||
handleDragEnd={handleDragEnd}
|
handleDragEnd={handleDragEnd}
|
||||||
|
|||||||
+10
-3
@@ -8,10 +8,11 @@ import { TResponseTableData } from "@formbricks/types/responses";
|
|||||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { extractChoiceIdsFromResponse } from "@/lib/response/utils";
|
import { extractChoiceIdsFromResponse } from "@/lib/response/utils";
|
||||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
import { formatDateTimeForDisplay } from "@/lib/utils/datetime";
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
import { recallToHeadline } from "@/lib/utils/recall";
|
||||||
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
@@ -34,6 +35,7 @@ const getElementColumnsData = (
|
|||||||
element: TSurveyElement,
|
element: TSurveyElement,
|
||||||
survey: TSurvey,
|
survey: TSurvey,
|
||||||
isExpanded: boolean,
|
isExpanded: boolean,
|
||||||
|
locale: TUserLocale,
|
||||||
t: TFunction
|
t: TFunction
|
||||||
): ColumnDef<TResponseTableData>[] => {
|
): ColumnDef<TResponseTableData>[] => {
|
||||||
const ELEMENTS_ICON_MAP = getElementIconMap(t);
|
const ELEMENTS_ICON_MAP = getElementIconMap(t);
|
||||||
@@ -167,6 +169,7 @@ const getElementColumnsData = (
|
|||||||
survey={survey}
|
survey={survey}
|
||||||
responseData={responseValue}
|
responseData={responseValue}
|
||||||
language={language}
|
language={language}
|
||||||
|
locale={locale}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
showId={false}
|
showId={false}
|
||||||
/>
|
/>
|
||||||
@@ -218,6 +221,7 @@ const getElementColumnsData = (
|
|||||||
survey={survey}
|
survey={survey}
|
||||||
responseData={responseValue}
|
responseData={responseValue}
|
||||||
language={language}
|
language={language}
|
||||||
|
locale={locale}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
showId={false}
|
showId={false}
|
||||||
/>
|
/>
|
||||||
@@ -259,11 +263,14 @@ export const generateResponseTableColumns = (
|
|||||||
survey: TSurvey,
|
survey: TSurvey,
|
||||||
isExpanded: boolean,
|
isExpanded: boolean,
|
||||||
isReadOnly: boolean,
|
isReadOnly: boolean,
|
||||||
|
locale: TUserLocale,
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
showQuotasColumn: boolean
|
showQuotasColumn: boolean
|
||||||
): ColumnDef<TResponseTableData>[] => {
|
): ColumnDef<TResponseTableData>[] => {
|
||||||
const elements = getElementsFromBlocks(survey.blocks);
|
const elements = getElementsFromBlocks(survey.blocks);
|
||||||
const elementColumns = elements.flatMap((element) => getElementColumnsData(element, survey, isExpanded, t));
|
const elementColumns = elements.flatMap((element) =>
|
||||||
|
getElementColumnsData(element, survey, isExpanded, locale, t)
|
||||||
|
);
|
||||||
|
|
||||||
const dateColumn: ColumnDef<TResponseTableData> = {
|
const dateColumn: ColumnDef<TResponseTableData> = {
|
||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
@@ -271,7 +278,7 @@ export const generateResponseTableColumns = (
|
|||||||
size: 200,
|
size: 200,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date = new Date(row.original.createdAt);
|
const date = new Date(row.original.createdAt);
|
||||||
return <p className="text-slate-900">{getFormattedDateTimeString(date)}</p>;
|
return <p className="text-slate-900">{formatDateTimeForDisplay(date, locale)}</p>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -1,3 +1,4 @@
|
|||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||||
@@ -7,7 +8,6 @@ import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service
|
|||||||
import { getSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
||||||
import { getUser } from "@/lib/user/service";
|
import { getUser } from "@/lib/user/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
|
import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
|
||||||
import { getIsContactsEnabled, getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsContactsEnabled, getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
@@ -23,25 +23,24 @@ const Page = async (props: { params: Promise<{ environmentId: string; surveyId:
|
|||||||
|
|
||||||
const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||||
|
|
||||||
const [survey, user, tags, isContactsEnabled, responseCount, locale] = await Promise.all([
|
const [survey, user, tags, isContactsEnabled, responseCount] = await Promise.all([
|
||||||
getSurvey(params.surveyId),
|
getSurvey(params.surveyId),
|
||||||
getUser(session.user.id),
|
getUser(session.user.id),
|
||||||
getTagsByEnvironmentId(params.environmentId),
|
getTagsByEnvironmentId(params.environmentId),
|
||||||
getIsContactsEnabled(organization.id),
|
getIsContactsEnabled(organization.id),
|
||||||
getResponseCountBySurveyId(params.surveyId),
|
getResponseCountBySurveyId(params.surveyId),
|
||||||
findMatchingLocale(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!survey) {
|
if (!survey) {
|
||||||
throw new Error(t("common.survey_not_found"));
|
throw new ResourceNotFoundError(t("common.survey"), params.surveyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = isContactsEnabled ? await getSegments(params.environmentId) : [];
|
const segments = isContactsEnabled ? await getSegments(params.environmentId) : [];
|
||||||
@@ -50,7 +49,7 @@ const Page = async (props: { params: Promise<{ environmentId: string; surveyId:
|
|||||||
|
|
||||||
const organizationBilling = await getOrganizationBilling(organization.id);
|
const organizationBilling = await getOrganizationBilling(organization.id);
|
||||||
if (!organizationBilling) {
|
if (!organizationBilling) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), organization.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isQuotasAllowed = await getIsQuotasEnabled(organization.id);
|
const isQuotasAllowed = await getIsQuotasEnabled(organization.id);
|
||||||
@@ -86,7 +85,7 @@ const Page = async (props: { params: Promise<{ environmentId: string; surveyId:
|
|||||||
environmentTags={tags}
|
environmentTags={tags}
|
||||||
user={user}
|
user={user}
|
||||||
responsesPerPage={RESPONSES_PER_PAGE}
|
responsesPerPage={RESPONSES_PER_PAGE}
|
||||||
locale={locale}
|
locale={user.locale}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
isQuotasAllowed={isQuotasAllowed}
|
isQuotasAllowed={isQuotasAllowed}
|
||||||
quotas={quotas}
|
quotas={quotas}
|
||||||
|
|||||||
+6
-4
@@ -64,15 +64,17 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
|||||||
|
|
||||||
const ZResetSurveyAction = z.object({
|
const ZResetSurveyAction = z.object({
|
||||||
surveyId: ZId,
|
surveyId: ZId,
|
||||||
organizationId: ZId,
|
|
||||||
projectId: ZId,
|
projectId: ZId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
|
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
|
||||||
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
|
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
|
||||||
|
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||||
|
const projectId = await getProjectIdFromSurveyId(parsedInput.surveyId);
|
||||||
|
|
||||||
await checkAuthorizationUpdated({
|
await checkAuthorizationUpdated({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
organizationId: parsedInput.organizationId,
|
organizationId,
|
||||||
access: [
|
access: [
|
||||||
{
|
{
|
||||||
type: "organization",
|
type: "organization",
|
||||||
@@ -81,12 +83,12 @@ export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSur
|
|||||||
{
|
{
|
||||||
type: "projectTeam",
|
type: "projectTeam",
|
||||||
minPermission: "readWrite",
|
minPermission: "readWrite",
|
||||||
projectId: parsedInput.projectId,
|
projectId,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
|
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||||
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
|
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
|
||||||
ctx.auditLoggingCtx.oldObject = null;
|
ctx.auditLoggingCtx.oldObject = null;
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -7,7 +7,7 @@ import { TSurvey, TSurveyElementSummaryDate } from "@formbricks/types/surveys/ty
|
|||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { timeSince } from "@/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||||
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
import { formatStoredDateForDisplay } from "@/lib/utils/date-display";
|
||||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||||
@@ -32,13 +32,14 @@ export const DateElementSummary = ({ elementSummary, environmentId, survey, loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderResponseValue = (value: string) => {
|
const renderResponseValue = (value: string) => {
|
||||||
const parsedDate = new Date(value);
|
const formattedDate = formatStoredDateForDisplay(value, elementSummary.element.format, locale);
|
||||||
|
|
||||||
const formattedDate = isNaN(parsedDate.getTime())
|
return (
|
||||||
? `${t("common.invalid_date")}(${value})`
|
formattedDate ??
|
||||||
: formatDateWithOrdinal(parsedDate);
|
t("common.invalid_date_with_value", {
|
||||||
|
value,
|
||||||
return formattedDate;
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,7 +60,7 @@ export const DateElementSummary = ({ elementSummary, environmentId, survey, loca
|
|||||||
elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||||
<div
|
<div
|
||||||
key={response.id}
|
key={response.id}
|
||||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent">
|
||||||
<div className="pl-4 md:pl-6">
|
<div className="pl-4 md:pl-6">
|
||||||
{response.contact ? (
|
{response.contact ? (
|
||||||
<Link
|
<Link
|
||||||
@@ -84,7 +85,7 @@ export const DateElementSummary = ({ elementSummary, environmentId, survey, loca
|
|||||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
||||||
{renderResponseValue(response.value)}
|
{renderResponseValue(response.value)}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 text-slate-500 md:px-6">
|
<div className="px-4 md:px-6">
|
||||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-2
@@ -64,7 +64,7 @@ export const SurveyAnalysisCTA = ({
|
|||||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||||
const [isResetting, setIsResetting] = useState(false);
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
|
||||||
const { organizationId, project } = useEnvironment();
|
const { project } = useEnvironment();
|
||||||
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
||||||
|
|
||||||
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||||
@@ -128,7 +128,6 @@ export const SurveyAnalysisCTA = ({
|
|||||||
setIsResetting(true);
|
setIsResetting(true);
|
||||||
const result = await resetSurveyAction({
|
const result = await resetSurveyAction({
|
||||||
surveyId: survey.id,
|
surveyId: survey.id,
|
||||||
organizationId: organizationId,
|
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
});
|
});
|
||||||
if (result?.data) {
|
if (result?.data) {
|
||||||
|
|||||||
+3
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
@@ -9,11 +10,11 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const survey = await getSurvey(surveyId);
|
const survey = await getSurvey(surveyId);
|
||||||
if (!survey) {
|
if (!survey) {
|
||||||
throw new Error("Survey not found");
|
throw new ResourceNotFoundError(t("common.survey"), surveyId);
|
||||||
}
|
}
|
||||||
const project = await getProjectByEnvironmentId(survey.environmentId);
|
const project = await getProjectByEnvironmentId(survey.environmentId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error("Workspace not found");
|
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styling = getStyling(project, survey);
|
const styling = getStyling(project, survey);
|
||||||
|
|||||||
+54
-65
@@ -11,8 +11,7 @@ import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
|||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
|
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
|
||||||
import {
|
import {
|
||||||
getElementSummary,
|
getElementSummary,
|
||||||
getResponsesForSummary,
|
getResponsesForSummary,
|
||||||
@@ -44,7 +43,7 @@ vi.mock("@/lib/survey/service", () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock("@/lib/surveyLogic/utils", () => ({
|
vi.mock("@/lib/surveyLogic/utils", () => ({
|
||||||
evaluateLogic: vi.fn(),
|
evaluateLogic: vi.fn(),
|
||||||
performActions: vi.fn(() => ({ jumpTarget: undefined, requiredQuestionIds: [], calculations: {} })),
|
performActions: vi.fn(() => ({ jumpTarget: undefined, requiredElementIds: [], calculations: {} })),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/utils/validate", () => ({
|
vi.mock("@/lib/utils/validate", () => ({
|
||||||
validateInputs: vi.fn(),
|
validateInputs: vi.fn(),
|
||||||
@@ -229,12 +228,6 @@ describe("getSurveySummaryDropOff", () => {
|
|||||||
vi.mocked(convertFloatTo2Decimal).mockImplementation((num) =>
|
vi.mocked(convertFloatTo2Decimal).mockImplementation((num) =>
|
||||||
num !== undefined && num !== null ? parseFloat(num.toFixed(2)) : 0
|
num !== undefined && num !== null ? parseFloat(num.toFixed(2)) : 0
|
||||||
);
|
);
|
||||||
vi.mocked(evaluateLogic).mockReturnValue(false); // Default: no logic triggers
|
|
||||||
vi.mocked(performActions).mockReturnValue({
|
|
||||||
jumpTarget: undefined,
|
|
||||||
requiredElementIds: [],
|
|
||||||
calculations: {},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("calculates dropOff correctly with welcome card disabled", () => {
|
test("calculates dropOff correctly with welcome card disabled", () => {
|
||||||
@@ -246,7 +239,7 @@ describe("getSurveySummaryDropOff", () => {
|
|||||||
contact: null,
|
contact: null,
|
||||||
contactAttributes: {},
|
contactAttributes: {},
|
||||||
language: "en",
|
language: "en",
|
||||||
ttc: { q1: 10 },
|
ttc: { q1: 10, q2: 5 }, // Saw q2 but didn't answer it
|
||||||
finished: false,
|
finished: false,
|
||||||
}, // Dropped at q2
|
}, // Dropped at q2
|
||||||
{
|
{
|
||||||
@@ -269,22 +262,55 @@ describe("getSurveySummaryDropOff", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(dropOff.length).toBe(2);
|
expect(dropOff.length).toBe(2);
|
||||||
// Q1
|
// Q1: welcome card disabled so impressions = displayCount
|
||||||
expect(dropOff[0].elementId).toBe("q1");
|
expect(dropOff[0].elementId).toBe("q1");
|
||||||
expect(dropOff[0].impressions).toBe(displayCount); // Welcome card disabled, so first question impressions = displayCount
|
expect(dropOff[0].impressions).toBe(displayCount);
|
||||||
expect(dropOff[0].dropOffCount).toBe(displayCount - responses.length); // 5 displays - 2 started = 3 dropped before q1
|
expect(dropOff[0].dropOffCount).toBe(displayCount - responses.length); // 5 displays - 2 started = 3 dropped before q1
|
||||||
expect(dropOff[0].dropOffPercentage).toBe(60); // (3/5)*100
|
expect(dropOff[0].dropOffPercentage).toBe(60); // (3/5)*100
|
||||||
expect(dropOff[0].ttc).toBe(10);
|
expect(dropOff[0].ttc).toBe(10);
|
||||||
|
|
||||||
// Q2
|
// Q2: both responses saw q2 (r1 has ttc for q2, r2 answered q2)
|
||||||
expect(dropOff[1].elementId).toBe("q2");
|
expect(dropOff[1].elementId).toBe("q2");
|
||||||
expect(dropOff[1].impressions).toBe(responses.length); // 2 responses reached q1, so 2 impressions for q2
|
expect(dropOff[1].impressions).toBe(2);
|
||||||
expect(dropOff[1].dropOffCount).toBe(1); // 1 response dropped at q2
|
expect(dropOff[1].dropOffCount).toBe(1); // r1 dropped at q2 (last seen element)
|
||||||
expect(dropOff[1].dropOffPercentage).toBe(50); // (1/2)*100
|
expect(dropOff[1].dropOffPercentage).toBe(50); // (1/2)*100
|
||||||
expect(dropOff[1].ttc).toBe(10);
|
expect(dropOff[1].ttc).toBe(7.5); // avg of r1(5ms) and r2(10ms)
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles logic jumps", () => {
|
test("drop-off attributed to last seen element when user doesn't reach next question", () => {
|
||||||
|
// Welcome card enabled so first element drop-off is NOT overridden by displayCount
|
||||||
|
const surveyWithWelcome: TSurvey = {
|
||||||
|
...surveyWithBlocks,
|
||||||
|
welcomeCard: { enabled: true, headline: { default: "Welcome" } } as unknown as TSurvey["welcomeCard"],
|
||||||
|
};
|
||||||
|
const responses = [
|
||||||
|
{
|
||||||
|
id: "r1",
|
||||||
|
data: { q1: "a" },
|
||||||
|
updatedAt: new Date(),
|
||||||
|
contact: null,
|
||||||
|
contactAttributes: {},
|
||||||
|
language: "en",
|
||||||
|
ttc: { q1: 10 }, // Only saw q1, never reached q2
|
||||||
|
finished: false,
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
const displayCount = 1;
|
||||||
|
const dropOff = getSurveySummaryDropOff(
|
||||||
|
surveyWithWelcome,
|
||||||
|
getElementsFromBlocks(surveyWithWelcome.blocks),
|
||||||
|
responses,
|
||||||
|
displayCount
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(dropOff[0].impressions).toBe(1); // Saw q1
|
||||||
|
expect(dropOff[0].dropOffCount).toBe(1); // Dropped at q1 (last seen element)
|
||||||
|
expect(dropOff[1].impressions).toBe(0); // Never saw q2
|
||||||
|
expect(dropOff[1].dropOffCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles logic jumps — impressions based on actual ttc/data, not logic replay", () => {
|
||||||
|
// Survey with 4 questions across 4 blocks, logic on block2 jumps q2->q4 (skipping q3)
|
||||||
const surveyWithLogic: TSurvey = {
|
const surveyWithLogic: TSurvey = {
|
||||||
...mockBaseSurvey,
|
...mockBaseSurvey,
|
||||||
blocks: [
|
blocks: [
|
||||||
@@ -315,36 +341,6 @@ describe("getSurveySummaryDropOff", () => {
|
|||||||
charLimit: { enabled: false },
|
charLimit: { enabled: false },
|
||||||
},
|
},
|
||||||
] as TSurveyElement[],
|
] as TSurveyElement[],
|
||||||
logic: [
|
|
||||||
{
|
|
||||||
id: "logic1",
|
|
||||||
conditions: {
|
|
||||||
id: "condition1",
|
|
||||||
connector: "and" as const,
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
id: "c1",
|
|
||||||
leftOperand: {
|
|
||||||
type: "element" as const,
|
|
||||||
value: "q2",
|
|
||||||
},
|
|
||||||
operator: "equals" as const,
|
|
||||||
rightOperand: {
|
|
||||||
type: "static" as const,
|
|
||||||
value: "b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
id: "action1",
|
|
||||||
objective: "jumpToBlock" as const,
|
|
||||||
target: "q4",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "block3",
|
id: "block3",
|
||||||
@@ -377,28 +373,21 @@ describe("getSurveySummaryDropOff", () => {
|
|||||||
],
|
],
|
||||||
questions: [],
|
questions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Response where user answered q1, q2, then logic jumped to q4 (skipping q3).
|
||||||
|
// The ttc/data reflects exactly what elements were shown — no logic replay needed.
|
||||||
const responses = [
|
const responses = [
|
||||||
{
|
{
|
||||||
id: "r1",
|
id: "r1",
|
||||||
data: { q1: "a", q2: "b" },
|
data: { q1: "a", q2: "b", q4: "d" },
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
contact: null,
|
contact: null,
|
||||||
contactAttributes: {},
|
contactAttributes: {},
|
||||||
language: "en",
|
language: "en",
|
||||||
ttc: { q1: 10, q2: 10 },
|
ttc: { q1: 10, q2: 10, q4: 10 }, // q3 has no ttc entry — was skipped by logic
|
||||||
finished: false,
|
finished: false,
|
||||||
}, // Jumps from q2 to q4, drops at q4
|
},
|
||||||
];
|
];
|
||||||
vi.mocked(evaluateLogic).mockImplementation((_s, data, _v, _, _l) => {
|
|
||||||
// Simulate logic on q2 triggering
|
|
||||||
return data.q2 === "b";
|
|
||||||
});
|
|
||||||
vi.mocked(performActions).mockImplementation((_s, actions, _d, _v) => {
|
|
||||||
if (actions[0] && "objective" in actions[0] && actions[0].objective === "jumpToBlock") {
|
|
||||||
return { jumpTarget: actions[0].target, requiredElementIds: [], calculations: {} };
|
|
||||||
}
|
|
||||||
return { jumpTarget: undefined, requiredElementIds: [], calculations: {} };
|
|
||||||
});
|
|
||||||
|
|
||||||
const dropOff = getSurveySummaryDropOff(
|
const dropOff = getSurveySummaryDropOff(
|
||||||
surveyWithLogic,
|
surveyWithLogic,
|
||||||
@@ -407,11 +396,11 @@ describe("getSurveySummaryDropOff", () => {
|
|||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(dropOff[0].impressions).toBe(1); // q1
|
expect(dropOff[0].impressions).toBe(1); // q1: seen
|
||||||
expect(dropOff[1].impressions).toBe(1); // q2
|
expect(dropOff[1].impressions).toBe(1); // q2: seen
|
||||||
expect(dropOff[2].impressions).toBe(0); // q3 (skipped)
|
expect(dropOff[2].impressions).toBe(0); // q3: skipped by logic (no ttc, no data)
|
||||||
expect(dropOff[3].impressions).toBe(1); // q4 (jumped to)
|
expect(dropOff[3].impressions).toBe(1); // q4: jumped to, seen
|
||||||
expect(dropOff[3].dropOffCount).toBe(1); // Dropped at q4
|
expect(dropOff[3].dropOffCount).toBe(1); // Dropped at q4 (last seen element, not finished)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+24
-112
@@ -11,7 +11,6 @@ import {
|
|||||||
TResponseData,
|
TResponseData,
|
||||||
TResponseFilterCriteria,
|
TResponseFilterCriteria,
|
||||||
TResponseTtc,
|
TResponseTtc,
|
||||||
TResponseVariables,
|
|
||||||
ZResponseFilterCriteria,
|
ZResponseFilterCriteria,
|
||||||
} from "@formbricks/types/responses";
|
} from "@formbricks/types/responses";
|
||||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
@@ -37,8 +36,7 @@ import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
|||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { buildWhereClause } from "@/lib/response/utils";
|
import { buildWhereClause } from "@/lib/response/utils";
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
import { findElementLocation, getElementsFromBlocks } from "@/lib/survey/utils";
|
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||||
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
|
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { convertFloatTo2Decimal } from "./utils";
|
import { convertFloatTo2Decimal } from "./utils";
|
||||||
|
|
||||||
@@ -93,63 +91,13 @@ export const getSurveySummaryMeta = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateLogicAndGetNextElementId = (
|
// Determine whether a response interacted with a given element.
|
||||||
localSurvey: TSurvey,
|
// An element was "seen" if the respondent has a ttc entry for it OR provided an answer.
|
||||||
elements: TSurveyElement[],
|
// This is more reliable than replaying survey logic, which can misattribute impressions
|
||||||
data: TResponseData,
|
// when branching logic skips elements or when partial response data is insufficient
|
||||||
localVariables: TResponseVariables,
|
// to evaluate conditions correctly.
|
||||||
currentElementIndex: number,
|
const wasElementSeen = (response: TSurveySummaryResponse, elementId: string): boolean => {
|
||||||
currElementTemp: TSurveyElement,
|
return (response.ttc != null && response.ttc[elementId] > 0) || response.data[elementId] !== undefined;
|
||||||
selectedLanguage: string | null
|
|
||||||
): {
|
|
||||||
nextElementId: string | undefined;
|
|
||||||
updatedSurvey: TSurvey;
|
|
||||||
updatedVariables: TResponseVariables;
|
|
||||||
} => {
|
|
||||||
let updatedSurvey = { ...localSurvey };
|
|
||||||
let updatedVariables = { ...localVariables };
|
|
||||||
|
|
||||||
let firstJumpTarget: string | undefined;
|
|
||||||
|
|
||||||
const { block: currentBlock } = findElementLocation(localSurvey, currElementTemp.id);
|
|
||||||
|
|
||||||
if (currentBlock?.logic && currentBlock.logic.length > 0) {
|
|
||||||
for (const logic of currentBlock.logic) {
|
|
||||||
if (evaluateLogic(localSurvey, data, localVariables, logic.conditions, selectedLanguage ?? "default")) {
|
|
||||||
const { jumpTarget, requiredElementIds, calculations } = performActions(
|
|
||||||
updatedSurvey,
|
|
||||||
logic.actions,
|
|
||||||
data,
|
|
||||||
updatedVariables
|
|
||||||
);
|
|
||||||
|
|
||||||
if (requiredElementIds.length > 0) {
|
|
||||||
// Update blocks to mark elements as required
|
|
||||||
updatedSurvey.blocks = updatedSurvey.blocks.map((block) => ({
|
|
||||||
...block,
|
|
||||||
elements: block.elements.map((e) =>
|
|
||||||
requiredElementIds.includes(e.id) ? { ...e, required: true } : e
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
updatedVariables = { ...updatedVariables, ...calculations };
|
|
||||||
|
|
||||||
if (jumpTarget && !firstJumpTarget) {
|
|
||||||
firstJumpTarget = jumpTarget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no jump target was set, check for a fallback logic
|
|
||||||
if (!firstJumpTarget && currentBlock?.logicFallback) {
|
|
||||||
firstJumpTarget = currentBlock.logicFallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the first jump target if found, otherwise go to the next element
|
|
||||||
const nextElementId = firstJumpTarget || elements[currentElementIndex + 1]?.id || undefined;
|
|
||||||
|
|
||||||
return { nextElementId, updatedSurvey, updatedVariables };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSurveySummaryDropOff = (
|
export const getSurveySummaryDropOff = (
|
||||||
@@ -170,16 +118,8 @@ export const getSurveySummaryDropOff = (
|
|||||||
let impressionsArr = new Array(elements.length).fill(0) as number[];
|
let impressionsArr = new Array(elements.length).fill(0) as number[];
|
||||||
let dropOffPercentageArr = new Array(elements.length).fill(0) as number[];
|
let dropOffPercentageArr = new Array(elements.length).fill(0) as number[];
|
||||||
|
|
||||||
const surveyVariablesData = survey.variables?.reduce(
|
|
||||||
(acc, variable) => {
|
|
||||||
acc[variable.id] = variable.value;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string | number>
|
|
||||||
);
|
|
||||||
|
|
||||||
responses.forEach((response) => {
|
responses.forEach((response) => {
|
||||||
// Calculate total time-to-completion
|
// Calculate total time-to-completion per element
|
||||||
Object.keys(totalTtc).forEach((elementId) => {
|
Object.keys(totalTtc).forEach((elementId) => {
|
||||||
if (response.ttc && response.ttc[elementId]) {
|
if (response.ttc && response.ttc[elementId]) {
|
||||||
totalTtc[elementId] += response.ttc[elementId];
|
totalTtc[elementId] += response.ttc[elementId];
|
||||||
@@ -187,51 +127,21 @@ export const getSurveySummaryDropOff = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let localSurvey = structuredClone(survey);
|
// Count impressions based on actual interaction data (ttc + response data)
|
||||||
let localResponseData: TResponseData = { ...response.data };
|
// instead of replaying survey logic which is unreliable with branching
|
||||||
let localVariables: TResponseVariables = {
|
let lastSeenIdx = -1;
|
||||||
...surveyVariablesData,
|
|
||||||
};
|
|
||||||
|
|
||||||
let currQuesIdx = 0;
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
const element = elements[i];
|
||||||
while (currQuesIdx < elements.length) {
|
if (wasElementSeen(response, element.id)) {
|
||||||
const currQues = elements[currQuesIdx];
|
impressionsArr[i]++;
|
||||||
if (!currQues) break;
|
lastSeenIdx = i;
|
||||||
|
|
||||||
// element is not answered and required
|
|
||||||
if (response.data[currQues.id] === undefined && currQues.required) {
|
|
||||||
dropOffArr[currQuesIdx]++;
|
|
||||||
impressionsArr[currQuesIdx]++;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impressionsArr[currQuesIdx]++;
|
// Attribute drop-off to the last element the respondent interacted with
|
||||||
|
if (!response.finished && lastSeenIdx >= 0) {
|
||||||
const { nextElementId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextElementId(
|
dropOffArr[lastSeenIdx]++;
|
||||||
localSurvey,
|
|
||||||
elements,
|
|
||||||
localResponseData,
|
|
||||||
localVariables,
|
|
||||||
currQuesIdx,
|
|
||||||
currQues,
|
|
||||||
response.language
|
|
||||||
);
|
|
||||||
|
|
||||||
localSurvey = updatedSurvey;
|
|
||||||
localVariables = updatedVariables;
|
|
||||||
|
|
||||||
if (nextElementId) {
|
|
||||||
const nextQuesIdx = elements.findIndex((q) => q.id === nextElementId);
|
|
||||||
if (!response.data[nextElementId] && !response.finished) {
|
|
||||||
dropOffArr[nextQuesIdx]++;
|
|
||||||
impressionsArr[nextQuesIdx]++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
currQuesIdx = nextQuesIdx;
|
|
||||||
} else {
|
|
||||||
currQuesIdx++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,6 +150,8 @@ export const getSurveySummaryDropOff = (
|
|||||||
totalTtc[elementId] = responseCounts[elementId] > 0 ? totalTtc[elementId] / responseCounts[elementId] : 0;
|
totalTtc[elementId] = responseCounts[elementId] > 0 ? totalTtc[elementId] / responseCounts[elementId] : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When the welcome card is disabled, the first element's impressions should equal displayCount
|
||||||
|
// because every survey display is an impression of the first element
|
||||||
if (!survey.welcomeCard.enabled) {
|
if (!survey.welcomeCard.enabled) {
|
||||||
dropOffArr[0] = displayCount - impressionsArr[0];
|
dropOffArr[0] = displayCount - impressionsArr[0];
|
||||||
if (impressionsArr[0] > displayCount) dropOffPercentageArr[0] = 0;
|
if (impressionsArr[0] > displayCount) dropOffPercentageArr[0] = 0;
|
||||||
@@ -251,7 +163,7 @@ export const getSurveySummaryDropOff = (
|
|||||||
|
|
||||||
impressionsArr[0] = displayCount;
|
impressionsArr[0] = displayCount;
|
||||||
} else {
|
} else {
|
||||||
dropOffPercentageArr[0] = (dropOffArr[0] / impressionsArr[0]) * 100;
|
dropOffPercentageArr[0] = impressionsArr[0] > 0 ? (dropOffArr[0] / impressionsArr[0]) * 100 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 1; i < elements.length; i++) {
|
for (let i = 1; i < elements.length; i++) {
|
||||||
|
|||||||
+5
-4
@@ -1,4 +1,5 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||||
import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
||||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||||
@@ -32,13 +33,13 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
|||||||
const survey = await getSurvey(params.surveyId);
|
const survey = await getSurvey(params.surveyId);
|
||||||
|
|
||||||
if (!survey) {
|
if (!survey) {
|
||||||
throw new Error(t("common.survey_not_found"));
|
throw new ResourceNotFoundError(t("common.survey"), params.surveyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
const user = await getUser(session.user.id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||||
|
|
||||||
@@ -46,11 +47,11 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
|||||||
const segments = isContactsEnabled ? await getSegments(environment.id) : [];
|
const segments = isContactsEnabled ? await getSegments(environment.id) : [];
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||||
}
|
}
|
||||||
const organizationBilling = await getOrganizationBilling(organizationId);
|
const organizationBilling = await getOrganizationBilling(organizationId);
|
||||||
if (!organizationBilling) {
|
if (!organizationBilling) {
|
||||||
throw new Error(t("common.organization_not_found"));
|
throw new ResourceNotFoundError(t("common.organization"), organizationId);
|
||||||
}
|
}
|
||||||
const isQuotasAllowed = await getIsQuotasEnabled(organizationId);
|
const isQuotasAllowed = await getIsQuotasEnabled(organizationId);
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,16 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||||
import { ZSurvey } from "@formbricks/types/surveys/types";
|
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
|
||||||
import { getResponseDownloadFile, getResponseFilteringValues } from "@/lib/response/service";
|
import { getResponseDownloadFile, getResponseFilteringValues } from "@/lib/response/service";
|
||||||
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
|
||||||
import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getQuotas } from "@/modules/ee/quotas/lib/quotas";
|
import { getQuotas } from "@/modules/ee/quotas/lib/quotas";
|
||||||
import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
|
|
||||||
import { checkSpamProtectionPermission } from "@/modules/survey/lib/permission";
|
|
||||||
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
||||||
|
|
||||||
const ZGetResponsesDownloadUrlAction = z.object({
|
const ZGetResponsesDownloadUrlAction = z.object({
|
||||||
@@ -97,68 +92,3 @@ export const getSurveyFilterDataAction = authenticatedActionClient
|
|||||||
|
|
||||||
return { environmentTags: tags, attributes, meta, hiddenFields, quotas };
|
return { environmentTags: tags, attributes, meta, hiddenFields, quotas };
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if survey follow-ups are enabled for the given organization.
|
|
||||||
*
|
|
||||||
* @param {string} organizationId The ID of the organization to check.
|
|
||||||
* @returns {Promise<void>} A promise that resolves if the permission is granted.
|
|
||||||
* @throws {ResourceNotFoundError} If the organization is not found.
|
|
||||||
* @throws {OperationNotAllowedError} If survey follow-ups are not enabled for the organization.
|
|
||||||
*/
|
|
||||||
const checkSurveyFollowUpsPermission = async (organizationId: string): Promise<void> => {
|
|
||||||
const organization = await getOrganization(organizationId);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
throw new ResourceNotFoundError("Organization not found", organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organizationId);
|
|
||||||
if (!isSurveyFollowUpsEnabled) {
|
|
||||||
throw new OperationNotAllowedError("Survey follow ups are not enabled for this organization");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSurveyAction = authenticatedActionClient.inputSchema(ZSurvey).action(
|
|
||||||
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
|
|
||||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.id);
|
|
||||||
await checkAuthorizationUpdated({
|
|
||||||
userId: ctx.user?.id ?? "",
|
|
||||||
organizationId,
|
|
||||||
access: [
|
|
||||||
{
|
|
||||||
type: "organization",
|
|
||||||
roles: ["owner", "manager"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "projectTeam",
|
|
||||||
projectId: await getProjectIdFromSurveyId(parsedInput.id),
|
|
||||||
minPermission: "readWrite",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { followUps } = parsedInput;
|
|
||||||
|
|
||||||
const oldSurvey = await getSurvey(parsedInput.id);
|
|
||||||
|
|
||||||
if (parsedInput.recaptcha?.enabled) {
|
|
||||||
await checkSpamProtectionPermission(organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (followUps?.length) {
|
|
||||||
await checkSurveyFollowUpsPermission(organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context for audit log
|
|
||||||
ctx.auditLoggingCtx.surveyId = parsedInput.id;
|
|
||||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
|
||||||
ctx.auditLoggingCtx.oldObject = oldSurvey;
|
|
||||||
|
|
||||||
const newSurvey = await updateSurvey(parsedInput);
|
|
||||||
|
|
||||||
ctx.auditLoggingCtx.newObject = newSurvey;
|
|
||||||
|
|
||||||
return newSurvey;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|||||||
+26
-1
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
import {
|
import {
|
||||||
AirplayIcon,
|
AirplayIcon,
|
||||||
ArrowUpFromDotIcon,
|
ArrowUpFromDotIcon,
|
||||||
@@ -54,6 +55,25 @@ export enum OptionsType {
|
|||||||
QUOTAS = "Quotas",
|
QUOTAS = "Quotas",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOptionsTypeTranslationKey = (type: OptionsType, t: TFunction): string => {
|
||||||
|
switch (type) {
|
||||||
|
case OptionsType.ELEMENTS:
|
||||||
|
return t("common.elements");
|
||||||
|
case OptionsType.TAGS:
|
||||||
|
return t("common.tags");
|
||||||
|
case OptionsType.ATTRIBUTES:
|
||||||
|
return t("common.attributes");
|
||||||
|
case OptionsType.OTHERS:
|
||||||
|
return t("common.other_filters");
|
||||||
|
case OptionsType.META:
|
||||||
|
return t("common.meta");
|
||||||
|
case OptionsType.HIDDEN_FIELDS:
|
||||||
|
return t("common.hidden_fields");
|
||||||
|
case OptionsType.QUOTAS:
|
||||||
|
return t("common.quotas");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type ElementOption = {
|
export type ElementOption = {
|
||||||
label: string;
|
label: string;
|
||||||
elementType?: TSurveyElementTypeEnum;
|
elementType?: TSurveyElementTypeEnum;
|
||||||
@@ -218,7 +238,12 @@ export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementCo
|
|||||||
{options?.map((data) => (
|
{options?.map((data) => (
|
||||||
<Fragment key={data.header}>
|
<Fragment key={data.header}>
|
||||||
{data?.option.length > 0 && (
|
{data?.option.length > 0 && (
|
||||||
<CommandGroup heading={<p className="text-sm font-medium text-slate-600">{data.header}</p>}>
|
<CommandGroup
|
||||||
|
heading={
|
||||||
|
<p className="text-sm font-medium text-slate-600">
|
||||||
|
{getOptionsTypeTranslationKey(data.header, t)}
|
||||||
|
</p>
|
||||||
|
}>
|
||||||
{data?.option?.map((o) => (
|
{data?.option?.map((o) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={o.id}
|
key={o.id}
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/modules/ui/components/select";
|
} from "@/modules/ui/components/select";
|
||||||
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
|
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
|
||||||
import { updateSurveyAction } from "../actions";
|
|
||||||
|
|
||||||
interface SurveyStatusDropdownProps {
|
interface SurveyStatusDropdownProps {
|
||||||
environment: TEnvironment;
|
environment: TEnvironment;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { SurveyContextWrapper } from "./context/survey-context";
|
import { SurveyContextWrapper } from "./context/survey-context";
|
||||||
|
|
||||||
interface SurveyLayoutProps {
|
interface SurveyLayoutProps {
|
||||||
@@ -10,9 +12,10 @@ const SurveyLayout = async ({ params, children }: SurveyLayoutProps) => {
|
|||||||
const resolvedParams = await params;
|
const resolvedParams = await params;
|
||||||
|
|
||||||
const survey = await getSurvey(resolvedParams.surveyId);
|
const survey = await getSurvey(resolvedParams.surveyId);
|
||||||
|
const t = await getTranslate();
|
||||||
|
|
||||||
if (!survey) {
|
if (!survey) {
|
||||||
throw new Error("Survey not found");
|
throw new ResourceNotFoundError(t("common.survey"), resolvedParams.surveyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SurveyContextWrapper survey={survey}>{children}</SurveyContextWrapper>;
|
return <SurveyContextWrapper survey={survey}>{children}</SurveyContextWrapper>;
|
||||||
|
|||||||
+6
-8
@@ -4,9 +4,9 @@ import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
|||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import { getAirtableTables } from "@/lib/airtable/service";
|
import { getAirtableTables } from "@/lib/airtable/service";
|
||||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
import { AIRTABLE_CLIENT_ID, DEFAULT_LOCALE, WEBAPP_URL } from "@/lib/constants";
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
@@ -18,11 +18,12 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
||||||
|
|
||||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
||||||
|
|
||||||
const [surveys, integrations] = await Promise.all([
|
const [surveys, integrations, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(params.environmentId),
|
||||||
getIntegrations(params.environmentId),
|
getIntegrations(params.environmentId),
|
||||||
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
|
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
|
||||||
@@ -33,9 +34,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
if (airtableIntegration?.config.key) {
|
if (airtableIntegration?.config.key) {
|
||||||
airtableArray = await getAirtableTables(params.environmentId);
|
airtableArray = await getAirtableTables(params.environmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = await findMatchingLocale();
|
|
||||||
|
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return redirect("./");
|
return redirect("./");
|
||||||
}
|
}
|
||||||
@@ -52,7 +50,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
environmentId={environment.id}
|
environmentId={environment.id}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
locale={locale}
|
locale={locale ?? DEFAULT_LOCALE}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageContentWrapper>
|
</PageContentWrapper>
|
||||||
|
|||||||
+6
-7
@@ -3,13 +3,14 @@ import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-s
|
|||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_LOCALE,
|
||||||
GOOGLE_SHEETS_CLIENT_ID,
|
GOOGLE_SHEETS_CLIENT_ID,
|
||||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||||
GOOGLE_SHEETS_REDIRECT_URL,
|
GOOGLE_SHEETS_REDIRECT_URL,
|
||||||
WEBAPP_URL,
|
WEBAPP_URL,
|
||||||
} from "@/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
@@ -21,19 +22,17 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||||
|
|
||||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
||||||
|
|
||||||
const [surveys, integrations] = await Promise.all([
|
const [surveys, integrations, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(params.environmentId),
|
||||||
getIntegrations(params.environmentId),
|
getIntegrations(params.environmentId),
|
||||||
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
|
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
|
||||||
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
|
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
|
||||||
);
|
);
|
||||||
|
|
||||||
const locale = await findMatchingLocale();
|
|
||||||
|
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return redirect("./");
|
return redirect("./");
|
||||||
}
|
}
|
||||||
@@ -49,7 +48,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
googleSheetIntegration={googleSheetIntegration}
|
googleSheetIntegration={googleSheetIntegration}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
locale={locale}
|
locale={locale ?? DEFAULT_LOCALE}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageContentWrapper>
|
</PageContentWrapper>
|
||||||
|
|||||||
+6
-5
@@ -3,6 +3,7 @@ import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/type
|
|||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/NotionWrapper";
|
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/NotionWrapper";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_LOCALE,
|
||||||
NOTION_AUTH_URL,
|
NOTION_AUTH_URL,
|
||||||
NOTION_OAUTH_CLIENT_ID,
|
NOTION_OAUTH_CLIENT_ID,
|
||||||
NOTION_OAUTH_CLIENT_SECRET,
|
NOTION_OAUTH_CLIENT_SECRET,
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
} from "@/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { getIntegrationByType } from "@/lib/integration/service";
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
import { getNotionDatabases } from "@/lib/notion/service";
|
import { getNotionDatabases } from "@/lib/notion/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
@@ -28,18 +29,18 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
NOTION_REDIRECT_URI
|
NOTION_REDIRECT_URI
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
||||||
|
|
||||||
const [surveys, notionIntegration] = await Promise.all([
|
const [surveys, notionIntegration, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(params.environmentId),
|
||||||
getIntegrationByType(params.environmentId, "notion"),
|
getIntegrationByType(params.environmentId, "notion"),
|
||||||
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let databasesArray: TIntegrationNotionDatabase[] = [];
|
let databasesArray: TIntegrationNotionDatabase[] = [];
|
||||||
if (notionIntegration && (notionIntegration as TIntegrationNotion).config.key?.bot_id) {
|
if (notionIntegration && (notionIntegration as TIntegrationNotion).config.key?.bot_id) {
|
||||||
databasesArray = (await getNotionDatabases(environment.id)) ?? [];
|
databasesArray = (await getNotionDatabases(environment.id)) ?? [];
|
||||||
}
|
}
|
||||||
const locale = await findMatchingLocale();
|
|
||||||
|
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return redirect("./");
|
return redirect("./");
|
||||||
@@ -56,7 +57,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
notionIntegration={notionIntegration as TIntegrationNotion}
|
notionIntegration={notionIntegration as TIntegrationNotion}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
databasesArray={databasesArray}
|
databasesArray={databasesArray}
|
||||||
locale={locale}
|
locale={locale ?? DEFAULT_LOCALE}
|
||||||
/>
|
/>
|
||||||
</PageContentWrapper>
|
</PageContentWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-7
@@ -2,9 +2,9 @@ import { redirect } from "next/navigation";
|
|||||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/SlackWrapper";
|
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/SlackWrapper";
|
||||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
import { DEFAULT_LOCALE, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||||
import { getIntegrationByType } from "@/lib/integration/service";
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
@@ -17,15 +17,14 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
|
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|
||||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
||||||
|
|
||||||
const [surveys, slackIntegration] = await Promise.all([
|
const [surveys, slackIntegration, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(params.environmentId),
|
||||||
getIntegrationByType(params.environmentId, "slack"),
|
getIntegrationByType(params.environmentId, "slack"),
|
||||||
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const locale = await findMatchingLocale();
|
|
||||||
|
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return redirect("./");
|
return redirect("./");
|
||||||
}
|
}
|
||||||
@@ -41,7 +40,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
slackIntegration={slackIntegration as TIntegrationSlack}
|
slackIntegration={slackIntegration as TIntegrationSlack}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
locale={locale}
|
locale={locale ?? DEFAULT_LOCALE}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageContentWrapper>
|
</PageContentWrapper>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
CHATWOOT_WEBSITE_TOKEN,
|
CHATWOOT_WEBSITE_TOKEN,
|
||||||
IS_CHATWOOT_CONFIGURED,
|
IS_CHATWOOT_CONFIGURED,
|
||||||
POSTHOG_KEY,
|
POSTHOG_KEY,
|
||||||
|
SESSION_MAX_AGE,
|
||||||
} from "@/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { getUser } from "@/lib/user/service";
|
import { getUser } from "@/lib/user/service";
|
||||||
|
import { NextAuthProvider } from "@/modules/auth/components/next-auth-provider";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { ClientLogout } from "@/modules/ui/components/client-logout";
|
import { ClientLogout } from "@/modules/ui/components/client-logout";
|
||||||
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
|
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
|
||||||
@@ -23,7 +25,7 @@ const AppLayout = async ({ children }: { children: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<NextAuthProvider sessionMaxAge={SESSION_MAX_AGE}>
|
||||||
<NoMobileOverlay />
|
<NoMobileOverlay />
|
||||||
{POSTHOG_KEY && user && (
|
{POSTHOG_KEY && user && (
|
||||||
<PostHogIdentify posthogKey={POSTHOG_KEY} userId={user.id} email={user.email} name={user.name} />
|
<PostHogIdentify posthogKey={POSTHOG_KEY} userId={user.id} email={user.email} name={user.name} />
|
||||||
@@ -39,7 +41,7 @@ const AppLayout = async ({ children }: { children: React.ReactNode }) => {
|
|||||||
)}
|
)}
|
||||||
<ToasterClient />
|
<ToasterClient />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</NextAuthProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { GET } from "./route";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => {
|
||||||
|
const nextAuthHandler = vi.fn(async () => new Response(null, { status: 200 }));
|
||||||
|
const nextAuth = vi.fn(() => nextAuthHandler);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextAuth,
|
||||||
|
nextAuthHandler,
|
||||||
|
baseSignIn: vi.fn(async () => true),
|
||||||
|
baseSession: vi.fn(async ({ session }: { session: unknown }) => session),
|
||||||
|
baseEventSignIn: vi.fn(),
|
||||||
|
queueAuditEventBackground: vi.fn(),
|
||||||
|
captureException: vi.fn(),
|
||||||
|
loggerError: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("next-auth", () => ({
|
||||||
|
default: mocks.nextAuth,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/constants", () => ({
|
||||||
|
IS_PRODUCTION: false,
|
||||||
|
SENTRY_DSN: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@sentry/nextjs", () => ({
|
||||||
|
captureException: mocks.captureException,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@formbricks/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
withContext: vi.fn(() => ({
|
||||||
|
error: mocks.loggerError,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/auth/lib/authOptions", () => ({
|
||||||
|
authOptions: {
|
||||||
|
callbacks: {
|
||||||
|
signIn: mocks.baseSignIn,
|
||||||
|
session: mocks.baseSession,
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
signIn: mocks.baseEventSignIn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
||||||
|
queueAuditEventBackground: mocks.queueAuditEventBackground,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getWrappedAuthOptions = async (requestId: string = "req-123") => {
|
||||||
|
const request = new Request("http://localhost/api/auth/signin", {
|
||||||
|
headers: { "x-request-id": requestId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await GET(request, {} as any);
|
||||||
|
|
||||||
|
expect(mocks.nextAuth).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
return mocks.nextAuth.mock.calls[0][0];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("auth route audit logging", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs successful sign-in from the NextAuth signIn event after session creation", async () => {
|
||||||
|
const authOptions = await getWrappedAuthOptions();
|
||||||
|
const user = { id: "user_1", email: "user@example.com", name: "User Example" };
|
||||||
|
const account = { provider: "keycloak" };
|
||||||
|
|
||||||
|
await expect(authOptions.callbacks.signIn({ user, account })).resolves.toBe(true);
|
||||||
|
expect(mocks.queueAuditEventBackground).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await authOptions.events.signIn({ user, account, isNewUser: false });
|
||||||
|
|
||||||
|
expect(mocks.baseEventSignIn).toHaveBeenCalledWith({ user, account, isNewUser: false });
|
||||||
|
expect(mocks.queueAuditEventBackground).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "signedIn",
|
||||||
|
targetType: "user",
|
||||||
|
userId: "user_1",
|
||||||
|
targetId: "user_1",
|
||||||
|
organizationId: "unknown",
|
||||||
|
status: "success",
|
||||||
|
userType: "user",
|
||||||
|
newObject: expect.objectContaining({
|
||||||
|
email: "user@example.com",
|
||||||
|
authMethod: "sso",
|
||||||
|
provider: "keycloak",
|
||||||
|
sessionStrategy: "database",
|
||||||
|
isNewUser: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs failed sign-in attempts from the callback stage with the request event id", async () => {
|
||||||
|
const error = new Error("Access denied");
|
||||||
|
mocks.baseSignIn.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
|
const authOptions = await getWrappedAuthOptions("req-failure");
|
||||||
|
const user = { id: "user_2", email: "user2@example.com" };
|
||||||
|
const account = { provider: "credentials" };
|
||||||
|
|
||||||
|
await expect(authOptions.callbacks.signIn({ user, account })).rejects.toThrow("Access denied");
|
||||||
|
|
||||||
|
expect(mocks.baseEventSignIn).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.queueAuditEventBackground).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "signedIn",
|
||||||
|
targetType: "user",
|
||||||
|
userId: "user_2",
|
||||||
|
targetId: "user_2",
|
||||||
|
organizationId: "unknown",
|
||||||
|
status: "failure",
|
||||||
|
userType: "user",
|
||||||
|
eventId: "req-failure",
|
||||||
|
newObject: expect.objectContaining({
|
||||||
|
email: "user2@example.com",
|
||||||
|
authMethod: "password",
|
||||||
|
provider: "credentials",
|
||||||
|
errorMessage: "Access denied",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,10 +6,26 @@ import { logger } from "@formbricks/logger";
|
|||||||
import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants";
|
import { IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants";
|
||||||
import { authOptions as baseAuthOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions as baseAuthOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { queueAuditEventBackground } from "@/modules/ee/audit-logs/lib/handler";
|
import { queueAuditEventBackground } from "@/modules/ee/audit-logs/lib/handler";
|
||||||
import { TAuditStatus, UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
import { UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||||
|
|
||||||
export const fetchCache = "force-no-store";
|
export const fetchCache = "force-no-store";
|
||||||
|
|
||||||
|
const getAuthMethod = (account: Account | null) => {
|
||||||
|
if (account?.provider === "credentials") {
|
||||||
|
return "password";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account?.provider === "token") {
|
||||||
|
return "email_verification";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account?.provider) {
|
||||||
|
return "sso";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
const handler = async (req: Request, ctx: any) => {
|
const handler = async (req: Request, ctx: any) => {
|
||||||
const eventId = req.headers.get("x-request-id") ?? undefined;
|
const eventId = req.headers.get("x-request-id") ?? undefined;
|
||||||
|
|
||||||
@@ -17,44 +33,6 @@ const handler = async (req: Request, ctx: any) => {
|
|||||||
...baseAuthOptions,
|
...baseAuthOptions,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
...baseAuthOptions.callbacks,
|
...baseAuthOptions.callbacks,
|
||||||
async jwt(params: any) {
|
|
||||||
let result: any = params.token;
|
|
||||||
let error: any = undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (baseAuthOptions.callbacks?.jwt) {
|
|
||||||
result = await baseAuthOptions.callbacks.jwt(params);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
error = err;
|
|
||||||
logger.withContext({ eventId, err }).error("JWT callback failed");
|
|
||||||
|
|
||||||
if (SENTRY_DSN && IS_PRODUCTION) {
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit JWT operations (token refresh, updates)
|
|
||||||
if (params.trigger && params.token?.profile?.id) {
|
|
||||||
const status: TAuditStatus = error ? "failure" : "success";
|
|
||||||
const auditLog = {
|
|
||||||
action: "jwtTokenCreated" as const,
|
|
||||||
targetType: "user" as const,
|
|
||||||
userId: params.token.profile.id,
|
|
||||||
targetId: params.token.profile.id,
|
|
||||||
organizationId: UNKNOWN_DATA,
|
|
||||||
status,
|
|
||||||
userType: "user" as const,
|
|
||||||
newObject: { trigger: params.trigger, tokenType: "jwt" },
|
|
||||||
...(error ? { eventId } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
queueAuditEventBackground(auditLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
async session(params: any) {
|
async session(params: any) {
|
||||||
let result: any = params.session;
|
let result: any = params.session;
|
||||||
let error: any = undefined;
|
let error: any = undefined;
|
||||||
@@ -90,7 +68,7 @@ const handler = async (req: Request, ctx: any) => {
|
|||||||
}) {
|
}) {
|
||||||
let result: boolean | string = true;
|
let result: boolean | string = true;
|
||||||
let error: any = undefined;
|
let error: any = undefined;
|
||||||
let authMethod = "unknown";
|
const authMethod = getAuthMethod(account);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (baseAuthOptions.callbacks?.signIn) {
|
if (baseAuthOptions.callbacks?.signIn) {
|
||||||
@@ -102,15 +80,6 @@ const handler = async (req: Request, ctx: any) => {
|
|||||||
credentials,
|
credentials,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine authentication method for more detailed logging
|
|
||||||
if (account?.provider === "credentials") {
|
|
||||||
authMethod = "password";
|
|
||||||
} else if (account?.provider === "token") {
|
|
||||||
authMethod = "email_verification";
|
|
||||||
} else if (account?.provider && account.provider !== "credentials") {
|
|
||||||
authMethod = "sso";
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
result = false;
|
result = false;
|
||||||
@@ -122,30 +91,60 @@ const handler = async (req: Request, ctx: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const status: TAuditStatus = result === false ? "failure" : "success";
|
if (result === false) {
|
||||||
const auditLog = {
|
queueAuditEventBackground({
|
||||||
action: "signedIn" as const,
|
action: "signedIn",
|
||||||
targetType: "user" as const,
|
targetType: "user",
|
||||||
userId: user?.id ?? UNKNOWN_DATA,
|
userId: user?.id ?? UNKNOWN_DATA,
|
||||||
targetId: user?.id ?? UNKNOWN_DATA,
|
targetId: user?.id ?? UNKNOWN_DATA,
|
||||||
organizationId: UNKNOWN_DATA,
|
organizationId: UNKNOWN_DATA,
|
||||||
status,
|
status: "failure",
|
||||||
userType: "user" as const,
|
userType: "user",
|
||||||
newObject: {
|
newObject: {
|
||||||
...user,
|
...user,
|
||||||
authMethod,
|
authMethod,
|
||||||
provider: account?.provider,
|
provider: account?.provider,
|
||||||
...(error ? { errorMessage: error.message } : {}),
|
...(error instanceof Error ? { errorMessage: error.message } : {}),
|
||||||
},
|
},
|
||||||
...(status === "failure" ? { eventId } : {}),
|
eventId,
|
||||||
};
|
});
|
||||||
|
}
|
||||||
queueAuditEventBackground(auditLog);
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
...baseAuthOptions.events,
|
||||||
|
async signIn({ user, account, isNewUser }: any) {
|
||||||
|
try {
|
||||||
|
await baseAuthOptions.events?.signIn?.({ user, account, isNewUser });
|
||||||
|
} catch (err) {
|
||||||
|
logger.withContext({ eventId, err }).error("Sign-in event callback failed");
|
||||||
|
|
||||||
|
if (SENTRY_DSN && IS_PRODUCTION) {
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueAuditEventBackground({
|
||||||
|
action: "signedIn",
|
||||||
|
targetType: "user",
|
||||||
|
userId: user?.id ?? UNKNOWN_DATA,
|
||||||
|
targetId: user?.id ?? UNKNOWN_DATA,
|
||||||
|
organizationId: UNKNOWN_DATA,
|
||||||
|
status: "success",
|
||||||
|
userType: "user",
|
||||||
|
newObject: {
|
||||||
|
...user,
|
||||||
|
authMethod: getAuthMethod(account),
|
||||||
|
provider: account?.provider,
|
||||||
|
sessionStrategy: "database",
|
||||||
|
isNewUser: isNewUser ?? false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextAuth(authOptions)(req, ctx);
|
return NextAuth(authOptions)(req, ctx);
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { TooManyRequestsError } from "@formbricks/types/errors";
|
||||||
|
import { withV3ApiWrapper } from "./api-wrapper";
|
||||||
|
|
||||||
|
const { mockAuthenticateRequest, mockGetServerSession } = vi.hoisted(() => ({
|
||||||
|
mockAuthenticateRequest: vi.fn(),
|
||||||
|
mockGetServerSession: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("next-auth", () => ({
|
||||||
|
getServerSession: mockGetServerSession,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/api/v1/auth", () => ({
|
||||||
|
authenticateRequest: mockAuthenticateRequest,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/auth/lib/authOptions", () => ({
|
||||||
|
authOptions: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/core/rate-limit/helpers", () => ({
|
||||||
|
applyRateLimit: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@formbricks/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
withContext: vi.fn(() => ({
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("withV3ApiWrapper", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
mockGetServerSession.mockResolvedValue(null);
|
||||||
|
mockAuthenticateRequest.mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses session auth first in both mode and injects request id into plain responses", async () => {
|
||||||
|
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||||
|
mockGetServerSession.mockResolvedValue({
|
||||||
|
user: { id: "user_1", name: "Test", email: "t@example.com" },
|
||||||
|
expires: "2026-01-01",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = vi.fn(async ({ authentication, requestId, instance }) => {
|
||||||
|
expect(authentication).toMatchObject({ user: { id: "user_1" } });
|
||||||
|
expect(requestId).toBe("req-1");
|
||||||
|
expect(instance).toBe("/api/v3/surveys");
|
||||||
|
return Response.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(
|
||||||
|
new NextRequest("http://localhost/api/v3/surveys?limit=10", {
|
||||||
|
headers: { "x-request-id": "req-1" },
|
||||||
|
}),
|
||||||
|
{} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("X-Request-Id")).toBe("req-1");
|
||||||
|
expect(handler).toHaveBeenCalledOnce();
|
||||||
|
expect(vi.mocked(applyRateLimit)).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ namespace: "api:v3" }),
|
||||||
|
"user_1"
|
||||||
|
);
|
||||||
|
expect(mockAuthenticateRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to api key auth in both mode", async () => {
|
||||||
|
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||||
|
mockAuthenticateRequest.mockResolvedValue({
|
||||||
|
type: "apiKey",
|
||||||
|
apiKeyId: "key_1",
|
||||||
|
organizationId: "org_1",
|
||||||
|
organizationAccess: { accessControl: { read: true, write: false } },
|
||||||
|
environmentPermissions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = vi.fn(async ({ authentication }) => {
|
||||||
|
expect(authentication).toMatchObject({ apiKeyId: "key_1" });
|
||||||
|
return Response.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(
|
||||||
|
new NextRequest("http://localhost/api/v3/surveys", {
|
||||||
|
headers: { "x-api-key": "fbk_test" },
|
||||||
|
}),
|
||||||
|
{} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(vi.mocked(applyRateLimit)).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ namespace: "api:v3" }),
|
||||||
|
"key_1"
|
||||||
|
);
|
||||||
|
expect(mockGetServerSession).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 401 problem response when authentication is required but missing", async () => {
|
||||||
|
const handler = vi.fn(async () => Response.json({ ok: true }));
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(new NextRequest("http://localhost/api/v3/surveys"), {} as never);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
expect(response.headers.get("Content-Type")).toBe("application/problem+json");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 problem response for invalid query input", async () => {
|
||||||
|
mockGetServerSession.mockResolvedValue({
|
||||||
|
user: { id: "user_1" },
|
||||||
|
expires: "2026-01-01",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = vi.fn(async () => Response.json({ ok: true }));
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
schemas: {
|
||||||
|
query: z.object({
|
||||||
|
limit: z.coerce.number().int().positive(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(
|
||||||
|
new NextRequest("http://localhost/api/v3/surveys?limit=oops", {
|
||||||
|
headers: { "x-request-id": "req-invalid" },
|
||||||
|
}),
|
||||||
|
{} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.invalid_params).toEqual(expect.arrayContaining([expect.objectContaining({ name: "limit" })]));
|
||||||
|
expect(body.requestId).toBe("req-invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses body, repeated query params, and async route params", async () => {
|
||||||
|
const handler = vi.fn(async ({ parsedInput }) => {
|
||||||
|
expect(parsedInput).toEqual({
|
||||||
|
body: { name: "Survey API" },
|
||||||
|
query: { tag: ["a", "b"] },
|
||||||
|
params: { workspaceId: "ws_123" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ ok: true },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-Request-Id": "handler-request-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "none",
|
||||||
|
schemas: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
query: z.object({
|
||||||
|
tag: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(
|
||||||
|
new NextRequest("http://localhost/api/v3/surveys?tag=a&tag=b", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name: "Survey API" }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
params: Promise.resolve({
|
||||||
|
workspaceId: "ws_123",
|
||||||
|
}),
|
||||||
|
} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("X-Request-Id")).toBe("handler-request-id");
|
||||||
|
expect(handler).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 problem response for malformed JSON input", async () => {
|
||||||
|
const handler = vi.fn(async () => Response.json({ ok: true }));
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "none",
|
||||||
|
schemas: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(
|
||||||
|
new NextRequest("http://localhost/api/v3/surveys", {
|
||||||
|
method: "POST",
|
||||||
|
body: "{",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.invalid_params).toEqual([
|
||||||
|
{
|
||||||
|
name: "body",
|
||||||
|
reason: "Malformed JSON input, please check your request body",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 problem response for invalid route params", async () => {
|
||||||
|
const handler = vi.fn(async () => Response.json({ ok: true }));
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "none",
|
||||||
|
schemas: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().min(3),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(new NextRequest("http://localhost/api/v3/surveys"), {
|
||||||
|
params: Promise.resolve({
|
||||||
|
workspaceId: "x",
|
||||||
|
}),
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.invalid_params).toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ name: "workspaceId" })])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 429 problem response when rate limited", async () => {
|
||||||
|
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||||
|
mockGetServerSession.mockResolvedValue({
|
||||||
|
user: { id: "user_1" },
|
||||||
|
expires: "2026-01-01",
|
||||||
|
});
|
||||||
|
vi.mocked(applyRateLimit).mockRejectedValueOnce(new TooManyRequestsError("Too many requests", 60));
|
||||||
|
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
handler: async () => Response.json({ ok: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(new NextRequest("http://localhost/api/v3/surveys"), {} as never);
|
||||||
|
|
||||||
|
expect(response.status).toBe(429);
|
||||||
|
expect(response.headers.get("Retry-After")).toBe("60");
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.code).toBe("too_many_requests");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 500 problem response when the handler throws unexpectedly", async () => {
|
||||||
|
mockGetServerSession.mockResolvedValue({
|
||||||
|
user: { id: "user_1" },
|
||||||
|
expires: "2026-01-01",
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
handler: async () => {
|
||||||
|
throw new Error("boom");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await wrapped(
|
||||||
|
new NextRequest("http://localhost/api/v3/surveys", {
|
||||||
|
headers: { "x-request-id": "req-boom" },
|
||||||
|
}),
|
||||||
|
{} as never
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.code).toBe("internal_server_error");
|
||||||
|
expect(body.requestId).toBe("req-boom");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { type NextRequest } from "next/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { TooManyRequestsError } from "@formbricks/types/errors";
|
||||||
|
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||||
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
|
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||||
|
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||||
|
import type { TRateLimitConfig } from "@/modules/core/rate-limit/types/rate-limit";
|
||||||
|
import {
|
||||||
|
type InvalidParam,
|
||||||
|
problemBadRequest,
|
||||||
|
problemInternalError,
|
||||||
|
problemTooManyRequests,
|
||||||
|
problemUnauthorized,
|
||||||
|
} from "./response";
|
||||||
|
import type { TV3Authentication } from "./types";
|
||||||
|
|
||||||
|
type TV3Schema = z.ZodTypeAny;
|
||||||
|
type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
export type TV3AuthMode = "none" | "session" | "apiKey" | "both";
|
||||||
|
|
||||||
|
export type TV3Schemas = {
|
||||||
|
body?: TV3Schema;
|
||||||
|
query?: TV3Schema;
|
||||||
|
params?: TV3Schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TV3ParsedInput<S extends TV3Schemas | undefined> = S extends object
|
||||||
|
? {
|
||||||
|
[K in keyof S as NonNullable<S[K]> extends TV3Schema ? K : never]: z.infer<NonNullable<S[K]>>;
|
||||||
|
}
|
||||||
|
: Record<string, never>;
|
||||||
|
|
||||||
|
export type TV3HandlerParams<TParsedInput = Record<string, never>, TProps = unknown> = {
|
||||||
|
req: NextRequest;
|
||||||
|
props: TProps;
|
||||||
|
authentication: TV3Authentication;
|
||||||
|
parsedInput: TParsedInput;
|
||||||
|
requestId: string;
|
||||||
|
instance: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWithV3ApiWrapperParams<S extends TV3Schemas | undefined, TProps = unknown> = {
|
||||||
|
auth?: TV3AuthMode;
|
||||||
|
schemas?: S;
|
||||||
|
rateLimit?: boolean;
|
||||||
|
customRateLimitConfig?: TRateLimitConfig;
|
||||||
|
handler: (params: TV3HandlerParams<TV3ParsedInput<S>, TProps>) => MaybePromise<Response>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUnauthenticatedDetail(authMode: TV3AuthMode): string {
|
||||||
|
if (authMode === "session") {
|
||||||
|
return "Session required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === "apiKey") {
|
||||||
|
return "API key required";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Not authenticated";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatZodIssues(error: z.ZodError, fallbackName: "body" | "query" | "params"): InvalidParam[] {
|
||||||
|
return error.issues.map((issue) => ({
|
||||||
|
name: issue.path.length > 0 ? issue.path.join(".") : fallbackName,
|
||||||
|
reason: issue.message,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchParamsToObject(searchParams: URLSearchParams): Record<string, string | string[]> {
|
||||||
|
const query: Record<string, string | string[]> = {};
|
||||||
|
|
||||||
|
for (const key of new Set(searchParams.keys())) {
|
||||||
|
const values = searchParams.getAll(key);
|
||||||
|
query[key] = values.length > 1 ? values : (values[0] ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRateLimitIdentifier(authentication: TV3Authentication): string | null {
|
||||||
|
if (!authentication) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("user" in authentication && authentication.user?.id) {
|
||||||
|
return authentication.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("apiKeyId" in authentication) {
|
||||||
|
return authentication.apiKeyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPromiseLike<T>(value: unknown): value is Promise<T> {
|
||||||
|
return typeof value === "object" && value !== null && "then" in value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRouteParams<TProps>(props: TProps): Promise<Record<string, unknown>> {
|
||||||
|
if (!props || typeof props !== "object" || !("params" in props)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = (props as { params?: unknown }).params;
|
||||||
|
if (!params) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedParams = isPromiseLike<Record<string, unknown>>(params) ? await params : params;
|
||||||
|
return typeof resolvedParams === "object" && resolvedParams !== null
|
||||||
|
? (resolvedParams as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticateV3Request(req: NextRequest, authMode: TV3AuthMode): Promise<TV3Authentication> {
|
||||||
|
if (authMode === "none") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === "both" && req.headers.has("x-api-key")) {
|
||||||
|
const apiKeyAuth = await authenticateRequest(req);
|
||||||
|
if (apiKeyAuth) {
|
||||||
|
return apiKeyAuth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === "session" || authMode === "both") {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (session?.user?.id) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === "session") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === "apiKey" || authMode === "both") {
|
||||||
|
return await authenticateRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseV3Input<S extends TV3Schemas | undefined, TProps>(
|
||||||
|
req: NextRequest,
|
||||||
|
props: TProps,
|
||||||
|
schemas: S | undefined,
|
||||||
|
requestId: string,
|
||||||
|
instance: string
|
||||||
|
): Promise<
|
||||||
|
| { ok: true; parsedInput: TV3ParsedInput<S> }
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
response: Response;
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
const parsedInput = {} as TV3ParsedInput<S>;
|
||||||
|
|
||||||
|
if (schemas?.body) {
|
||||||
|
let bodyData: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
bodyData = await req.json();
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: problemBadRequest(requestId, "Invalid request body", {
|
||||||
|
instance,
|
||||||
|
invalid_params: [{ name: "body", reason: "Malformed JSON input, please check your request body" }],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyResult = schemas.body.safeParse(bodyData);
|
||||||
|
if (!bodyResult.success) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: problemBadRequest(requestId, "Invalid request body", {
|
||||||
|
instance,
|
||||||
|
invalid_params: formatZodIssues(bodyResult.error, "body"),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedInput.body = bodyResult.data as TV3ParsedInput<S>["body"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemas?.query) {
|
||||||
|
const queryResult = schemas.query.safeParse(searchParamsToObject(req.nextUrl.searchParams));
|
||||||
|
if (!queryResult.success) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: problemBadRequest(requestId, "Invalid query parameters", {
|
||||||
|
instance,
|
||||||
|
invalid_params: formatZodIssues(queryResult.error, "query"),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedInput.query = queryResult.data as TV3ParsedInput<S>["query"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemas?.params) {
|
||||||
|
const paramsResult = schemas.params.safeParse(await getRouteParams(props));
|
||||||
|
if (!paramsResult.success) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
response: problemBadRequest(requestId, "Invalid route parameters", {
|
||||||
|
instance,
|
||||||
|
invalid_params: formatZodIssues(paramsResult.error, "params"),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedInput.params = paramsResult.data as TV3ParsedInput<S>["params"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, parsedInput };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRequestIdHeader(response: Response, requestId: string): Response {
|
||||||
|
if (response.headers.get("X-Request-Id")) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers(response.headers);
|
||||||
|
headers.set("X-Request-Id", requestId);
|
||||||
|
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticateV3RequestOrRespond(
|
||||||
|
req: NextRequest,
|
||||||
|
authMode: TV3AuthMode,
|
||||||
|
requestId: string,
|
||||||
|
instance: string
|
||||||
|
): Promise<
|
||||||
|
{ authentication: TV3Authentication; response: null } | { authentication: null; response: Response }
|
||||||
|
> {
|
||||||
|
const authentication = await authenticateV3Request(req, authMode);
|
||||||
|
|
||||||
|
if (!authentication && authMode !== "none") {
|
||||||
|
return {
|
||||||
|
authentication: null,
|
||||||
|
response: problemUnauthorized(requestId, getUnauthenticatedDetail(authMode), instance),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authentication,
|
||||||
|
response: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyV3RateLimitOrRespond(params: {
|
||||||
|
authentication: TV3Authentication;
|
||||||
|
enabled: boolean;
|
||||||
|
config: TRateLimitConfig;
|
||||||
|
requestId: string;
|
||||||
|
log: ReturnType<typeof logger.withContext>;
|
||||||
|
}): Promise<Response | null> {
|
||||||
|
const { authentication, enabled, config, requestId, log } = params;
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = getRateLimitIdentifier(authentication);
|
||||||
|
if (!identifier) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await applyRateLimit(config, identifier);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn({ error, statusCode: 429 }, "V3 API rate limit exceeded");
|
||||||
|
return problemTooManyRequests(
|
||||||
|
requestId,
|
||||||
|
error instanceof Error ? error.message : "Rate limit exceeded",
|
||||||
|
error instanceof TooManyRequestsError ? error.retryAfter : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withV3ApiWrapper = <S extends TV3Schemas | undefined, TProps = unknown>(
|
||||||
|
params: TWithV3ApiWrapperParams<S, TProps>
|
||||||
|
): ((req: NextRequest, props: TProps) => Promise<Response>) => {
|
||||||
|
const { auth = "both", schemas, rateLimit = true, customRateLimitConfig, handler } = params;
|
||||||
|
|
||||||
|
return async (req: NextRequest, props: TProps): Promise<Response> => {
|
||||||
|
const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();
|
||||||
|
const instance = req.nextUrl.pathname;
|
||||||
|
const log = logger.withContext({
|
||||||
|
requestId,
|
||||||
|
method: req.method,
|
||||||
|
path: instance,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authResult = await authenticateV3RequestOrRespond(req, auth, requestId, instance);
|
||||||
|
if (authResult.response) {
|
||||||
|
log.warn({ statusCode: authResult.response.status }, "V3 API authentication failed");
|
||||||
|
return authResult.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedInputResult = await parseV3Input(req, props, schemas, requestId, instance);
|
||||||
|
if (!parsedInputResult.ok) {
|
||||||
|
log.warn({ statusCode: parsedInputResult.response.status }, "V3 API request validation failed");
|
||||||
|
return parsedInputResult.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimitResponse = await applyV3RateLimitOrRespond({
|
||||||
|
authentication: authResult.authentication,
|
||||||
|
enabled: rateLimit,
|
||||||
|
config: customRateLimitConfig ?? rateLimitConfigs.api.v3,
|
||||||
|
requestId,
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
if (rateLimitResponse) {
|
||||||
|
return rateLimitResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await handler({
|
||||||
|
req,
|
||||||
|
props,
|
||||||
|
authentication: authResult.authentication,
|
||||||
|
parsedInput: parsedInputResult.parsedInput,
|
||||||
|
requestId,
|
||||||
|
instance,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ensureRequestIdHeader(response, requestId);
|
||||||
|
} catch (error) {
|
||||||
|
log.error({ error, statusCode: 500 }, "V3 API unexpected error");
|
||||||
|
return problemInternalError(requestId, "An unexpected error occurred.", instance);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { ApiKeyPermission, EnvironmentType } from "@prisma/client";
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
|
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
|
||||||
|
import { getEnvironment } from "@/lib/utils/services";
|
||||||
|
import { requireSessionWorkspaceAccess, requireV3WorkspaceAccess } from "./auth";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
withContext: vi.fn(() => ({
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/helper", () => ({
|
||||||
|
getOrganizationIdFromProjectId: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/services", () => ({
|
||||||
|
getEnvironment: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/action-client/action-client-middleware", () => ({
|
||||||
|
checkAuthorizationUpdated: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const requestId = "req-123";
|
||||||
|
|
||||||
|
describe("requireSessionWorkspaceAccess", () => {
|
||||||
|
test("returns 401 when authentication is null", async () => {
|
||||||
|
const result = await requireSessionWorkspaceAccess(null, "proj_abc", "read", requestId);
|
||||||
|
expect(result).toBeInstanceOf(Response);
|
||||||
|
expect((result as Response).status).toBe(401);
|
||||||
|
expect((result as Response).headers.get("Content-Type")).toBe("application/problem+json");
|
||||||
|
const body = await (result as Response).json();
|
||||||
|
expect(body.requestId).toBe(requestId);
|
||||||
|
expect(body.status).toBe(401);
|
||||||
|
expect(body.code).toBe("not_authenticated");
|
||||||
|
expect(getEnvironment).not.toHaveBeenCalled();
|
||||||
|
expect(checkAuthorizationUpdated).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 401 when authentication is API key (no user)", async () => {
|
||||||
|
const result = await requireSessionWorkspaceAccess(
|
||||||
|
{ apiKeyId: "key_1", organizationId: "org_1", environmentPermissions: [] } as any,
|
||||||
|
"proj_abc",
|
||||||
|
"read",
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
expect(result).toBeInstanceOf(Response);
|
||||||
|
expect((result as Response).status).toBe(401);
|
||||||
|
const body = await (result as Response).json();
|
||||||
|
expect(body.requestId).toBe(requestId);
|
||||||
|
expect(body.code).toBe("not_authenticated");
|
||||||
|
expect(getEnvironment).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when workspace (environment) is not found (avoid leaking existence)", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||||
|
const result = await requireSessionWorkspaceAccess(
|
||||||
|
{ user: { id: "user_1" }, expires: "" } as any,
|
||||||
|
"env_nonexistent",
|
||||||
|
"read",
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
expect(result).toBeInstanceOf(Response);
|
||||||
|
expect((result as Response).status).toBe(403);
|
||||||
|
expect((result as Response).headers.get("Content-Type")).toBe("application/problem+json");
|
||||||
|
const body = await (result as Response).json();
|
||||||
|
expect(body.requestId).toBe(requestId);
|
||||||
|
expect(body.code).toBe("forbidden");
|
||||||
|
expect(getEnvironment).toHaveBeenCalledWith("env_nonexistent");
|
||||||
|
expect(checkAuthorizationUpdated).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when user has no access to workspace", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
||||||
|
id: "env_abc",
|
||||||
|
projectId: "proj_abc",
|
||||||
|
} as any);
|
||||||
|
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_1");
|
||||||
|
vi.mocked(checkAuthorizationUpdated).mockRejectedValueOnce(new AuthorizationError("Not authorized"));
|
||||||
|
const result = await requireSessionWorkspaceAccess(
|
||||||
|
{ user: { id: "user_1" }, expires: "" } as any,
|
||||||
|
"env_abc",
|
||||||
|
"read",
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
expect(result).toBeInstanceOf(Response);
|
||||||
|
expect((result as Response).status).toBe(403);
|
||||||
|
const body = await (result as Response).json();
|
||||||
|
expect(body.requestId).toBe(requestId);
|
||||||
|
expect(body.code).toBe("forbidden");
|
||||||
|
expect(checkAuthorizationUpdated).toHaveBeenCalledWith({
|
||||||
|
userId: "user_1",
|
||||||
|
organizationId: "org_1",
|
||||||
|
access: [
|
||||||
|
{ type: "organization", roles: ["owner", "manager"] },
|
||||||
|
{ type: "projectTeam", projectId: "proj_abc", minPermission: "read" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns workspace context when session is valid and user has access", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
||||||
|
id: "env_abc",
|
||||||
|
projectId: "proj_abc",
|
||||||
|
} as any);
|
||||||
|
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_1");
|
||||||
|
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(undefined as any);
|
||||||
|
const result = await requireSessionWorkspaceAccess(
|
||||||
|
{ user: { id: "user_1" }, expires: "" } as any,
|
||||||
|
"env_abc",
|
||||||
|
"readWrite",
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
expect(result).not.toBeInstanceOf(Response);
|
||||||
|
expect(result).toEqual({
|
||||||
|
environmentId: "env_abc",
|
||||||
|
projectId: "proj_abc",
|
||||||
|
organizationId: "org_1",
|
||||||
|
});
|
||||||
|
expect(checkAuthorizationUpdated).toHaveBeenCalledWith({
|
||||||
|
userId: "user_1",
|
||||||
|
organizationId: "org_1",
|
||||||
|
access: [
|
||||||
|
{ type: "organization", roles: ["owner", "manager"] },
|
||||||
|
{ type: "projectTeam", projectId: "proj_abc", minPermission: "readWrite" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyBase = {
|
||||||
|
type: "apiKey" as const,
|
||||||
|
apiKeyId: "key_1",
|
||||||
|
organizationId: "org_k",
|
||||||
|
organizationAccess: { accessControl: { read: true, write: false } },
|
||||||
|
};
|
||||||
|
|
||||||
|
function envPerm(environmentId: string, permission: ApiKeyPermission = ApiKeyPermission.read) {
|
||||||
|
return {
|
||||||
|
environmentId,
|
||||||
|
environmentType: EnvironmentType.development,
|
||||||
|
projectId: "proj_k",
|
||||||
|
projectName: "K",
|
||||||
|
permission,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("requireV3WorkspaceAccess", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValue({
|
||||||
|
id: "env_k",
|
||||||
|
projectId: "proj_k",
|
||||||
|
} as any);
|
||||||
|
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValue("org_k");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("401 when authentication is null", async () => {
|
||||||
|
const r = await requireV3WorkspaceAccess(null, "env_x", "read", requestId);
|
||||||
|
expect((r as Response).status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delegates to session flow when user is present", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
||||||
|
id: "env_s",
|
||||||
|
projectId: "proj_s",
|
||||||
|
} as any);
|
||||||
|
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_s");
|
||||||
|
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(undefined as any);
|
||||||
|
const r = await requireV3WorkspaceAccess(
|
||||||
|
{ user: { id: "user_1" }, expires: "" } as any,
|
||||||
|
"env_s",
|
||||||
|
"read",
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
expect(r).toEqual({
|
||||||
|
environmentId: "env_s",
|
||||||
|
projectId: "proj_s",
|
||||||
|
organizationId: "org_s",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns context for API key with read on workspace", async () => {
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [envPerm("ws_a", ApiKeyPermission.read)],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "ws_a", "read", requestId);
|
||||||
|
expect(r).toEqual({
|
||||||
|
environmentId: "ws_a",
|
||||||
|
projectId: "proj_k",
|
||||||
|
organizationId: "org_k",
|
||||||
|
});
|
||||||
|
expect(getEnvironment).toHaveBeenCalledWith("ws_a");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns context for API key with write on workspace", async () => {
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [envPerm("ws_b", ApiKeyPermission.write)],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "ws_b", "read", requestId);
|
||||||
|
expect(r).toEqual({
|
||||||
|
environmentId: "ws_b",
|
||||||
|
projectId: "proj_k",
|
||||||
|
organizationId: "org_k",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when API key permission is lower than the required permission", async () => {
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [envPerm("ws_write", ApiKeyPermission.read)],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "ws_write", "readWrite", requestId);
|
||||||
|
expect((r as Response).status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("403 when API key has no matching environment", async () => {
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [envPerm("other_env")],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "wanted", "read", requestId);
|
||||||
|
expect((r as Response).status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("403 when API key permission is not list-eligible (runtime value)", async () => {
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [
|
||||||
|
{
|
||||||
|
...envPerm("ws_c"),
|
||||||
|
permission: "invalid" as unknown as ApiKeyPermission,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "ws_c", "read", requestId);
|
||||||
|
expect((r as Response).status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns context for API key with manage on workspace", async () => {
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [envPerm("ws_m", ApiKeyPermission.manage)],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "ws_m", "manage", requestId);
|
||||||
|
expect(r).toEqual({
|
||||||
|
environmentId: "ws_m",
|
||||||
|
projectId: "proj_k",
|
||||||
|
organizationId: "org_k",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when the workspace cannot be resolved for an API key", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||||
|
const auth = {
|
||||||
|
...keyBase,
|
||||||
|
environmentPermissions: [envPerm("ws_missing", ApiKeyPermission.manage)],
|
||||||
|
};
|
||||||
|
const r = await requireV3WorkspaceAccess(auth as any, "ws_missing", "read", requestId);
|
||||||
|
expect((r as Response).status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("401 when auth is neither session nor valid API key payload", async () => {
|
||||||
|
const r = await requireV3WorkspaceAccess({ user: {} } as any, "env", "read", requestId);
|
||||||
|
expect((r as Response).status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* V3 API auth — session (browser) or API key with environment-scoped access.
|
||||||
|
*/
|
||||||
|
import { ApiKeyPermission } from "@prisma/client";
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
|
import type { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||||
|
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
|
import type { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
|
import { problemForbidden, problemUnauthorized } from "./response";
|
||||||
|
import type { TV3Authentication } from "./types";
|
||||||
|
import { type V3WorkspaceContext, resolveV3WorkspaceContext } from "./workspace-context";
|
||||||
|
|
||||||
|
function apiKeyPermissionAllows(permission: ApiKeyPermission, minPermission: TTeamPermission): boolean {
|
||||||
|
const grantedRank = {
|
||||||
|
[ApiKeyPermission.read]: 1,
|
||||||
|
[ApiKeyPermission.write]: 2,
|
||||||
|
[ApiKeyPermission.manage]: 3,
|
||||||
|
}[permission];
|
||||||
|
|
||||||
|
const requiredRank = {
|
||||||
|
read: 1,
|
||||||
|
readWrite: 2,
|
||||||
|
manage: 3,
|
||||||
|
}[minPermission];
|
||||||
|
|
||||||
|
return grantedRank >= requiredRank;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require session and workspace access. workspaceId is resolved via the V3 workspace-context layer.
|
||||||
|
* Returns a Response (401 or 403) on failure, or the resolved workspace context on success so callers
|
||||||
|
* use internal IDs (environmentId, projectId, organizationId) without resolving again.
|
||||||
|
* We use 403 (not 404) when the workspace is not found to avoid leaking resource existence.
|
||||||
|
*/
|
||||||
|
export async function requireSessionWorkspaceAccess(
|
||||||
|
authentication: TV3Authentication,
|
||||||
|
workspaceId: string,
|
||||||
|
minPermission: TTeamPermission,
|
||||||
|
requestId: string,
|
||||||
|
instance?: string
|
||||||
|
): Promise<Response | V3WorkspaceContext> {
|
||||||
|
// --- Session checks ---
|
||||||
|
if (!authentication) {
|
||||||
|
return problemUnauthorized(requestId, "Not authenticated", instance);
|
||||||
|
}
|
||||||
|
if (!("user" in authentication) || !authentication.user?.id) {
|
||||||
|
return problemUnauthorized(requestId, "Session required", instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = authentication.user.id;
|
||||||
|
const log = logger.withContext({ requestId, workspaceId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve workspaceId → environmentId, projectId, organizationId (single place to change when Workspace exists).
|
||||||
|
const context = await resolveV3WorkspaceContext(workspaceId);
|
||||||
|
|
||||||
|
// Org + project-team access; we use internal IDs from context.
|
||||||
|
await checkAuthorizationUpdated({
|
||||||
|
userId,
|
||||||
|
organizationId: context.organizationId,
|
||||||
|
access: [
|
||||||
|
{ type: "organization", roles: ["owner", "manager"] },
|
||||||
|
{ type: "projectTeam", projectId: context.projectId, minPermission },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return context;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ResourceNotFoundError || err instanceof AuthorizationError) {
|
||||||
|
const message = err instanceof ResourceNotFoundError ? "Workspace not found" : "Forbidden";
|
||||||
|
log.warn({ statusCode: 403, errorCode: err.name }, message);
|
||||||
|
return problemForbidden(requestId, "You are not authorized to access this resource", instance);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Session or API key: authorize `workspaceId` against the resolved V3 workspace context. */
|
||||||
|
export async function requireV3WorkspaceAccess(
|
||||||
|
authentication: TV3Authentication,
|
||||||
|
workspaceId: string,
|
||||||
|
minPermission: TTeamPermission,
|
||||||
|
requestId: string,
|
||||||
|
instance?: string
|
||||||
|
): Promise<Response | V3WorkspaceContext> {
|
||||||
|
if (!authentication) {
|
||||||
|
return problemUnauthorized(requestId, "Not authenticated", instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("user" in authentication && authentication.user?.id) {
|
||||||
|
return requireSessionWorkspaceAccess(authentication, workspaceId, minPermission, requestId, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyAuth = authentication as TAuthenticationApiKey;
|
||||||
|
if (keyAuth.apiKeyId && Array.isArray(keyAuth.environmentPermissions)) {
|
||||||
|
const log = logger.withContext({ requestId, workspaceId, apiKeyId: keyAuth.apiKeyId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = await resolveV3WorkspaceContext(workspaceId);
|
||||||
|
const permission = keyAuth.environmentPermissions.find(
|
||||||
|
(environmentPermission) => environmentPermission.environmentId === context.environmentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!permission || !apiKeyPermissionAllows(permission.permission, minPermission)) {
|
||||||
|
log.warn({ statusCode: 403 }, "API key not allowed for workspace");
|
||||||
|
return problemForbidden(requestId, "You are not authorized to access this resource", instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ResourceNotFoundError) {
|
||||||
|
log.warn({ statusCode: 403, errorCode: error.name }, "Workspace not found");
|
||||||
|
return problemForbidden(requestId, "You are not authorized to access this resource", instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return problemUnauthorized(requestId, "Not authenticated", instance);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import {
|
||||||
|
problemBadRequest,
|
||||||
|
problemForbidden,
|
||||||
|
problemInternalError,
|
||||||
|
problemNotFound,
|
||||||
|
problemTooManyRequests,
|
||||||
|
problemUnauthorized,
|
||||||
|
successListResponse,
|
||||||
|
} from "./response";
|
||||||
|
|
||||||
|
describe("v3 problem responses", () => {
|
||||||
|
test("problemBadRequest includes invalid_params", async () => {
|
||||||
|
const res = problemBadRequest("rid", "bad", {
|
||||||
|
invalid_params: [{ name: "x", reason: "y" }],
|
||||||
|
instance: "/p",
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.headers.get("X-Request-Id")).toBe("rid");
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("bad_request");
|
||||||
|
expect(body.requestId).toBe("rid");
|
||||||
|
expect(body.invalid_params).toEqual([{ name: "x", reason: "y" }]);
|
||||||
|
expect(body.instance).toBe("/p");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problemUnauthorized default detail", async () => {
|
||||||
|
const res = problemUnauthorized("r1");
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.detail).toBe("Not authenticated");
|
||||||
|
expect(body.code).toBe("not_authenticated");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problemForbidden", async () => {
|
||||||
|
const res = problemForbidden("r2", undefined, "/api/x");
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("forbidden");
|
||||||
|
expect(body.instance).toBe("/api/x");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problemInternalError", async () => {
|
||||||
|
const res = problemInternalError("r3", "oops", "/i");
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("internal_server_error");
|
||||||
|
expect(body.detail).toBe("oops");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problemNotFound includes details", async () => {
|
||||||
|
const res = problemNotFound("r4", "Survey", "s1", "/s");
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("not_found");
|
||||||
|
expect(body.details).toEqual({ resource_type: "Survey", resource_id: "s1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problemTooManyRequests with Retry-After", async () => {
|
||||||
|
const res = problemTooManyRequests("r5", "slow down", 60);
|
||||||
|
expect(res.status).toBe(429);
|
||||||
|
expect(res.headers.get("Retry-After")).toBe("60");
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("too_many_requests");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("problemTooManyRequests without Retry-After", async () => {
|
||||||
|
const res = problemTooManyRequests("r6", "nope");
|
||||||
|
expect(res.headers.get("Retry-After")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("successListResponse", () => {
|
||||||
|
test("sets X-Request-Id and default cache", async () => {
|
||||||
|
const res = successListResponse(
|
||||||
|
[{ a: 1 }],
|
||||||
|
{ limit: 10, nextCursor: "cursor-1" },
|
||||||
|
{
|
||||||
|
requestId: "req-x",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers.get("X-Request-Id")).toBe("req-x");
|
||||||
|
expect(res.headers.get("Cache-Control")).toContain("no-store");
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
data: [{ a: 1 }],
|
||||||
|
meta: { limit: 10, nextCursor: "cursor-1" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom Cache-Control", async () => {
|
||||||
|
const res = successListResponse([], { limit: 5, nextCursor: null }, { cache: "private, max-age=0" });
|
||||||
|
expect(res.headers.get("Cache-Control")).toBe("private, max-age=0");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* V3 API response helpers — RFC 9457 Problem Details (application/problem+json)
|
||||||
|
* and list envelope for success responses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PROBLEM_JSON = "application/problem+json" as const;
|
||||||
|
const CACHE_NO_STORE = "private, no-store" as const;
|
||||||
|
|
||||||
|
export type InvalidParam = { name: string; reason: string };
|
||||||
|
|
||||||
|
export type ProblemExtension = {
|
||||||
|
code?: string;
|
||||||
|
requestId: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
invalid_params?: InvalidParam[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProblemBody = {
|
||||||
|
type?: string;
|
||||||
|
title: string;
|
||||||
|
status: number;
|
||||||
|
detail: string;
|
||||||
|
instance?: string;
|
||||||
|
} & ProblemExtension;
|
||||||
|
|
||||||
|
function problemResponse(
|
||||||
|
status: number,
|
||||||
|
title: string,
|
||||||
|
detail: string,
|
||||||
|
requestId: string,
|
||||||
|
options?: {
|
||||||
|
type?: string;
|
||||||
|
instance?: string;
|
||||||
|
code?: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
invalid_params?: InvalidParam[];
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
): Response {
|
||||||
|
const body: ProblemBody = {
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
detail,
|
||||||
|
requestId,
|
||||||
|
...(options?.type && { type: options.type }),
|
||||||
|
...(options?.instance && { instance: options.instance }),
|
||||||
|
...(options?.code && { code: options.code }),
|
||||||
|
...(options?.details && { details: options.details }),
|
||||||
|
...(options?.invalid_params && { invalid_params: options.invalid_params }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": PROBLEM_JSON,
|
||||||
|
"Cache-Control": CACHE_NO_STORE,
|
||||||
|
"X-Request-Id": requestId,
|
||||||
|
...options?.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response.json(body, { status, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function problemBadRequest(
|
||||||
|
requestId: string,
|
||||||
|
detail: string,
|
||||||
|
options?: { invalid_params?: InvalidParam[]; instance?: string }
|
||||||
|
): Response {
|
||||||
|
return problemResponse(400, "Bad Request", detail, requestId, {
|
||||||
|
code: "bad_request",
|
||||||
|
instance: options?.instance,
|
||||||
|
invalid_params: options?.invalid_params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function problemUnauthorized(
|
||||||
|
requestId: string,
|
||||||
|
detail: string = "Not authenticated",
|
||||||
|
instance?: string
|
||||||
|
): Response {
|
||||||
|
return problemResponse(401, "Unauthorized", detail, requestId, {
|
||||||
|
code: "not_authenticated",
|
||||||
|
instance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function problemForbidden(
|
||||||
|
requestId: string,
|
||||||
|
detail: string = "You are not authorized to access this resource",
|
||||||
|
instance?: string
|
||||||
|
): Response {
|
||||||
|
return problemResponse(403, "Forbidden", detail, requestId, {
|
||||||
|
code: "forbidden",
|
||||||
|
instance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 404 with resource details. Do not use for auth-sensitive or existence-sensitive resources:
|
||||||
|
* the body includes resource_type and resource_id, which can leak existence to unauthenticated or unauthorized callers.
|
||||||
|
* Prefer problemForbidden with a generic message for those cases.
|
||||||
|
*/
|
||||||
|
export function problemNotFound(
|
||||||
|
requestId: string,
|
||||||
|
resourceType: string,
|
||||||
|
resourceId: string | null,
|
||||||
|
instance?: string
|
||||||
|
): Response {
|
||||||
|
return problemResponse(404, "Not Found", `${resourceType} not found`, requestId, {
|
||||||
|
code: "not_found",
|
||||||
|
details: { resource_type: resourceType, resource_id: resourceId },
|
||||||
|
instance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function problemInternalError(
|
||||||
|
requestId: string,
|
||||||
|
detail: string = "An unexpected error occurred.",
|
||||||
|
instance?: string
|
||||||
|
): Response {
|
||||||
|
return problemResponse(500, "Internal Server Error", detail, requestId, {
|
||||||
|
code: "internal_server_error",
|
||||||
|
instance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function problemTooManyRequests(requestId: string, detail: string, retryAfter?: number): Response {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (retryAfter !== undefined) {
|
||||||
|
headers["Retry-After"] = String(retryAfter);
|
||||||
|
}
|
||||||
|
return problemResponse(429, "Too Many Requests", detail, requestId, {
|
||||||
|
code: "too_many_requests",
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function successListResponse<T, TMeta extends Record<string, unknown>>(
|
||||||
|
data: T[],
|
||||||
|
meta: TMeta,
|
||||||
|
options?: { requestId?: string; cache?: string }
|
||||||
|
): Response {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": options?.cache ?? CACHE_NO_STORE,
|
||||||
|
};
|
||||||
|
if (options?.requestId) {
|
||||||
|
headers["X-Request-Id"] = options.requestId;
|
||||||
|
}
|
||||||
|
return Response.json({ data, meta }, { status: 200, headers });
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { Session } from "next-auth";
|
||||||
|
import type { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||||
|
|
||||||
|
export type TV3Authentication = TAuthenticationApiKey | Session | null;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
|
||||||
|
import { getEnvironment } from "@/lib/utils/services";
|
||||||
|
import { resolveV3WorkspaceContext } from "./workspace-context";
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/helper", () => ({
|
||||||
|
getOrganizationIdFromProjectId: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/services", () => ({
|
||||||
|
getEnvironment: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("resolveV3WorkspaceContext", () => {
|
||||||
|
test("returns environmentId, projectId and organizationId when workspace exists (today: workspaceId === environmentId)", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
||||||
|
id: "env_abc",
|
||||||
|
projectId: "proj_xyz",
|
||||||
|
} as any);
|
||||||
|
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_123");
|
||||||
|
const result = await resolveV3WorkspaceContext("env_abc");
|
||||||
|
expect(result).toEqual({
|
||||||
|
environmentId: "env_abc",
|
||||||
|
projectId: "proj_xyz",
|
||||||
|
organizationId: "org_123",
|
||||||
|
});
|
||||||
|
expect(getEnvironment).toHaveBeenCalledWith("env_abc");
|
||||||
|
expect(getOrganizationIdFromProjectId).toHaveBeenCalledWith("proj_xyz");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when workspace (environment) does not exist", async () => {
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||||
|
await expect(resolveV3WorkspaceContext("env_nonexistent")).rejects.toThrow(ResourceNotFoundError);
|
||||||
|
expect(getEnvironment).toHaveBeenCalledWith("env_nonexistent");
|
||||||
|
expect(getOrganizationIdFromProjectId).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* V3 API workspace → internal IDs translation layer (retro-compatibility / future-proofing).
|
||||||
|
*
|
||||||
|
* Workspace is the default container for surveys. We are deprecating Environment and making
|
||||||
|
* Workspace that container. In the API, workspaceId refers to that container.
|
||||||
|
*
|
||||||
|
* Today: workspaceId is mapped to environmentId (Environment is the current container for surveys).
|
||||||
|
* When Environment is deprecated and Workspace exists: resolve workspaceId to the Workspace entity
|
||||||
|
* (and derive environmentId or equivalent from it). Change only this file.
|
||||||
|
*/
|
||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
|
||||||
|
import { getEnvironment } from "@/lib/utils/services";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal IDs derived from a V3 workspace identifier.
|
||||||
|
* Today: environmentId is the workspace (Environment = container for surveys until Workspace exists).
|
||||||
|
*/
|
||||||
|
export type V3WorkspaceContext = {
|
||||||
|
/** Environment ID — the container for surveys today. Replaced by workspace when Environment is deprecated. */
|
||||||
|
environmentId: string;
|
||||||
|
/** Project ID used for projectTeam auth. */
|
||||||
|
projectId: string;
|
||||||
|
/** Organization ID used for org-level auth. */
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a V3 API workspaceId to internal environmentId, projectId, and organizationId.
|
||||||
|
* Today: workspaceId is treated as environmentId (workspace = container for surveys = Environment).
|
||||||
|
*
|
||||||
|
* @throws ResourceNotFoundError if the workspace (environment) does not exist.
|
||||||
|
*/
|
||||||
|
export async function resolveV3WorkspaceContext(workspaceId: string): Promise<V3WorkspaceContext> {
|
||||||
|
// Today: workspaceId is the environment id (survey container). Look it up.
|
||||||
|
const environment = await getEnvironment(workspaceId);
|
||||||
|
if (!environment) {
|
||||||
|
throw new ResourceNotFoundError("environment", workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive org for auth; project comes from the environment.
|
||||||
|
const organizationId = await getOrganizationIdFromProjectId(environment.projectId);
|
||||||
|
|
||||||
|
// We looked up by workspaceId (as environment id), so the resolved environment id is workspaceId.
|
||||||
|
return {
|
||||||
|
environmentId: workspaceId,
|
||||||
|
projectId: environment.projectId,
|
||||||
|
organizationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { collectMultiValueQueryParam, parseV3SurveysListQuery } from "./parse-v3-surveys-list-query";
|
||||||
|
|
||||||
|
const wid = "clxx1234567890123456789012";
|
||||||
|
|
||||||
|
function params(qs: string): URLSearchParams {
|
||||||
|
return new URLSearchParams(qs);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("collectMultiValueQueryParam", () => {
|
||||||
|
test("merges repeated keys and comma-separated values", () => {
|
||||||
|
const sp = params("status=draft&status=inProgress&type=link,app");
|
||||||
|
expect(collectMultiValueQueryParam(sp, "status")).toEqual(["draft", "inProgress"]);
|
||||||
|
expect(collectMultiValueQueryParam(sp, "type")).toEqual(["link", "app"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dedupes", () => {
|
||||||
|
const sp = params("status=draft&status=draft");
|
||||||
|
expect(collectMultiValueQueryParam(sp, "status")).toEqual(["draft"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseV3SurveysListQuery", () => {
|
||||||
|
test("rejects unsupported query parameters like filterCriteria", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&filterCriteria={}`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) expect(r.invalid_params[0].name).toBe("filterCriteria");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects unknown query parameters", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&foo=bar`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok)
|
||||||
|
expect(r.invalid_params[0]).toEqual({
|
||||||
|
name: "foo",
|
||||||
|
reason:
|
||||||
|
"Unsupported query parameter. Use only workspaceId, limit, cursor, filter[name][contains], filter[status][in], filter[type][in], sortBy.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects the legacy after query parameter", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&after=legacy-cursor`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) {
|
||||||
|
expect(r.invalid_params[0]).toEqual({
|
||||||
|
name: "after",
|
||||||
|
reason:
|
||||||
|
"Unsupported query parameter. Use only workspaceId, limit, cursor, filter[name][contains], filter[status][in], filter[type][in], sortBy.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects the legacy flat name query parameter", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&name=Foo`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) {
|
||||||
|
expect(r.invalid_params[0]).toEqual({
|
||||||
|
name: "name",
|
||||||
|
reason:
|
||||||
|
"Unsupported query parameter. Use only workspaceId, limit, cursor, filter[name][contains], filter[status][in], filter[type][in], sortBy.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses minimal query", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}`));
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
if (r.ok) {
|
||||||
|
expect(r.limit).toBe(20);
|
||||||
|
expect(r.cursor).toBeNull();
|
||||||
|
expect(r.sortBy).toBe("updatedAt");
|
||||||
|
expect(r.filterCriteria).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds filter from explicit operator params", () => {
|
||||||
|
const r = parseV3SurveysListQuery(
|
||||||
|
params(
|
||||||
|
`workspaceId=${wid}&filter[name][contains]=Foo&filter[status][in]=inProgress&filter[status][in]=draft&filter[type][in]=link&sortBy=updatedAt`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(r.ok).toBe(true);
|
||||||
|
if (r.ok) {
|
||||||
|
expect(r.filterCriteria).toEqual({
|
||||||
|
name: "Foo",
|
||||||
|
status: ["inProgress", "draft"],
|
||||||
|
type: ["link"],
|
||||||
|
});
|
||||||
|
expect(r.sortBy).toBe("updatedAt");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid status", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&filter[status][in]=notastatus`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects the createdBy filter", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&filter[createdBy][in]=you`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) {
|
||||||
|
expect(r.invalid_params[0]).toEqual({
|
||||||
|
name: "filter[createdBy][in]",
|
||||||
|
reason:
|
||||||
|
"Unsupported query parameter. Use only workspaceId, limit, cursor, filter[name][contains], filter[status][in], filter[type][in], sortBy.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects an invalid cursor", () => {
|
||||||
|
const r = parseV3SurveysListQuery(params(`workspaceId=${wid}&cursor=not-a-real-cursor`));
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
if (!r.ok) {
|
||||||
|
expect(r.invalid_params).toEqual([
|
||||||
|
{
|
||||||
|
name: "cursor",
|
||||||
|
reason: "The cursor is invalid.",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* Validates GET /api/v3/surveys query string and builds {@link TSurveyFilterCriteria} for list/count.
|
||||||
|
* Keeps HTTP parsing separate from the route handler and shared survey list service.
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
import { ZId } from "@formbricks/types/common";
|
||||||
|
import {
|
||||||
|
type TSurveyFilterCriteria,
|
||||||
|
ZSurveyFilters,
|
||||||
|
ZSurveyStatus,
|
||||||
|
ZSurveyType,
|
||||||
|
} from "@formbricks/types/surveys/types";
|
||||||
|
import {
|
||||||
|
type TSurveyListPageCursor,
|
||||||
|
type TSurveyListSort,
|
||||||
|
decodeSurveyListPageCursor,
|
||||||
|
normalizeSurveyListSort,
|
||||||
|
} from "@/modules/survey/list/lib/survey-page";
|
||||||
|
|
||||||
|
const V3_SURVEYS_DEFAULT_LIMIT = 20;
|
||||||
|
const V3_SURVEYS_MAX_LIMIT = 100;
|
||||||
|
|
||||||
|
const FILTER_NAME_CONTAINS_QUERY_PARAM = "filter[name][contains]" as const;
|
||||||
|
const FILTER_STATUS_IN_QUERY_PARAM = "filter[status][in]" as const;
|
||||||
|
const FILTER_TYPE_IN_QUERY_PARAM = "filter[type][in]" as const;
|
||||||
|
|
||||||
|
const SUPPORTED_QUERY_PARAMS = [
|
||||||
|
"workspaceId",
|
||||||
|
"limit",
|
||||||
|
"cursor",
|
||||||
|
FILTER_NAME_CONTAINS_QUERY_PARAM,
|
||||||
|
FILTER_STATUS_IN_QUERY_PARAM,
|
||||||
|
FILTER_TYPE_IN_QUERY_PARAM,
|
||||||
|
"sortBy",
|
||||||
|
] as const;
|
||||||
|
const SUPPORTED_QUERY_PARAM_SET = new Set<string>(SUPPORTED_QUERY_PARAMS);
|
||||||
|
|
||||||
|
type InvalidParam = { name: string; reason: string };
|
||||||
|
|
||||||
|
/** Collect repeated query keys and comma-separated values for operator-style filters. */
|
||||||
|
export function collectMultiValueQueryParam(searchParams: URLSearchParams, key: string): string[] {
|
||||||
|
const acc: string[] = [];
|
||||||
|
for (const raw of searchParams.getAll(key)) {
|
||||||
|
for (const part of raw.split(",")) {
|
||||||
|
const t = part.trim();
|
||||||
|
if (t) acc.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...new Set(acc)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZV3SurveysListQuery = z.object({
|
||||||
|
workspaceId: ZId,
|
||||||
|
limit: z.coerce.number().int().min(1).max(V3_SURVEYS_MAX_LIMIT).default(V3_SURVEYS_DEFAULT_LIMIT),
|
||||||
|
cursor: z.string().min(1).optional(),
|
||||||
|
[FILTER_NAME_CONTAINS_QUERY_PARAM]: z
|
||||||
|
.string()
|
||||||
|
.max(512)
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s === undefined || s.trim() === "" ? undefined : s.trim())),
|
||||||
|
[FILTER_STATUS_IN_QUERY_PARAM]: z.array(ZSurveyStatus).optional(),
|
||||||
|
[FILTER_TYPE_IN_QUERY_PARAM]: z.array(ZSurveyType).optional(),
|
||||||
|
sortBy: ZSurveyFilters.shape.sortBy.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TV3SurveysListQuery = z.infer<typeof ZV3SurveysListQuery>;
|
||||||
|
|
||||||
|
export type TV3SurveysListQueryParseResult =
|
||||||
|
| {
|
||||||
|
ok: true;
|
||||||
|
workspaceId: string;
|
||||||
|
limit: number;
|
||||||
|
cursor: TSurveyListPageCursor | null;
|
||||||
|
sortBy: TSurveyListSort;
|
||||||
|
filterCriteria: TSurveyFilterCriteria | undefined;
|
||||||
|
}
|
||||||
|
| { ok: false; invalid_params: InvalidParam[] };
|
||||||
|
|
||||||
|
function getUnsupportedQueryParams(searchParams: URLSearchParams): InvalidParam[] {
|
||||||
|
const unsupportedParams = [
|
||||||
|
...new Set(Array.from(searchParams.keys()).filter((key) => !SUPPORTED_QUERY_PARAM_SET.has(key))),
|
||||||
|
];
|
||||||
|
|
||||||
|
return unsupportedParams.map((name) => ({
|
||||||
|
name,
|
||||||
|
reason: `Unsupported query parameter. Use only ${SUPPORTED_QUERY_PARAMS.join(", ")}.`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilterCriteria(q: TV3SurveysListQuery): TSurveyFilterCriteria | undefined {
|
||||||
|
const f: TSurveyFilterCriteria = {};
|
||||||
|
if (q[FILTER_NAME_CONTAINS_QUERY_PARAM]) f.name = q[FILTER_NAME_CONTAINS_QUERY_PARAM];
|
||||||
|
if (q[FILTER_STATUS_IN_QUERY_PARAM]?.length) f.status = q[FILTER_STATUS_IN_QUERY_PARAM];
|
||||||
|
if (q[FILTER_TYPE_IN_QUERY_PARAM]?.length) f.type = q[FILTER_TYPE_IN_QUERY_PARAM];
|
||||||
|
return Object.keys(f).length > 0 ? f : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseV3SurveysListQuery(searchParams: URLSearchParams): TV3SurveysListQueryParseResult {
|
||||||
|
const unsupportedQueryParams = getUnsupportedQueryParams(searchParams);
|
||||||
|
if (unsupportedQueryParams.length > 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
invalid_params: unsupportedQueryParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusVals = collectMultiValueQueryParam(searchParams, FILTER_STATUS_IN_QUERY_PARAM);
|
||||||
|
const typeVals = collectMultiValueQueryParam(searchParams, FILTER_TYPE_IN_QUERY_PARAM);
|
||||||
|
|
||||||
|
const raw = {
|
||||||
|
workspaceId: searchParams.get("workspaceId"),
|
||||||
|
limit: searchParams.get("limit") ?? undefined,
|
||||||
|
cursor: searchParams.get("cursor")?.trim() || undefined,
|
||||||
|
[FILTER_NAME_CONTAINS_QUERY_PARAM]: searchParams.get(FILTER_NAME_CONTAINS_QUERY_PARAM) ?? undefined,
|
||||||
|
[FILTER_STATUS_IN_QUERY_PARAM]: statusVals.length > 0 ? statusVals : undefined,
|
||||||
|
[FILTER_TYPE_IN_QUERY_PARAM]: typeVals.length > 0 ? typeVals : undefined,
|
||||||
|
sortBy: searchParams.get("sortBy")?.trim() || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = ZV3SurveysListQuery.safeParse(raw);
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
invalid_params: result.error.issues.map((issue) => ({
|
||||||
|
name: issue.path.join(".") || "query",
|
||||||
|
reason: issue.message,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = result.data;
|
||||||
|
const sortBy = normalizeSurveyListSort(q.sortBy);
|
||||||
|
let cursor: TSurveyListPageCursor | null = null;
|
||||||
|
|
||||||
|
if (q.cursor) {
|
||||||
|
try {
|
||||||
|
cursor = decodeSurveyListPageCursor(q.cursor, sortBy);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
invalid_params: [
|
||||||
|
{
|
||||||
|
name: "cursor",
|
||||||
|
reason: error instanceof Error ? error.message : "The cursor is invalid.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
workspaceId: q.workspaceId,
|
||||||
|
limit: q.limit,
|
||||||
|
cursor,
|
||||||
|
sortBy,
|
||||||
|
filterCriteria: buildFilterCriteria(q),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
import { ApiKeyPermission, EnvironmentType } from "@prisma/client";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { requireV3WorkspaceAccess } from "@/app/api/v3/lib/auth";
|
||||||
|
import { getSurveyCount } from "@/modules/survey/list/lib/survey";
|
||||||
|
import { getSurveyListPage } from "@/modules/survey/list/lib/survey-page";
|
||||||
|
import { GET } from "./route";
|
||||||
|
|
||||||
|
const { mockAuthenticateRequest } = vi.hoisted(() => ({
|
||||||
|
mockAuthenticateRequest: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("next-auth", () => ({
|
||||||
|
getServerSession: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/api/v1/auth", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/app/api/v1/auth")>();
|
||||||
|
return { ...actual, authenticateRequest: mockAuthenticateRequest };
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/modules/core/rate-limit/helpers", () => ({
|
||||||
|
applyRateLimit: vi.fn().mockResolvedValue(undefined),
|
||||||
|
applyIPRateLimit: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/constants", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/lib/constants")>();
|
||||||
|
return { ...actual, AUDIT_LOG_ENABLED: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/app/api/v3/lib/auth", () => ({
|
||||||
|
requireV3WorkspaceAccess: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/survey/list/lib/survey-page", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/modules/survey/list/lib/survey-page")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getSurveyListPage: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/modules/survey/list/lib/survey", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/modules/survey/list/lib/survey")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getSurveyCount: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@formbricks/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
withContext: vi.fn(() => ({
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getServerSession = vi.mocked((await import("next-auth")).getServerSession);
|
||||||
|
|
||||||
|
const validWorkspaceId = "clxx1234567890123456789012";
|
||||||
|
const resolvedEnvironmentId = "clzz9876543210987654321098";
|
||||||
|
|
||||||
|
function createRequest(url: string, requestId?: string, extraHeaders?: Record<string, string>): NextRequest {
|
||||||
|
const headers: Record<string, string> = { ...extraHeaders };
|
||||||
|
if (requestId) headers["x-request-id"] = requestId;
|
||||||
|
return new NextRequest(url, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyAuth = {
|
||||||
|
type: "apiKey" as const,
|
||||||
|
apiKeyId: "key_1",
|
||||||
|
organizationId: "org_1",
|
||||||
|
organizationAccess: {
|
||||||
|
accessControl: { read: true, write: false },
|
||||||
|
},
|
||||||
|
environmentPermissions: [
|
||||||
|
{
|
||||||
|
environmentId: validWorkspaceId,
|
||||||
|
environmentType: EnvironmentType.development,
|
||||||
|
projectId: "proj_1",
|
||||||
|
projectName: "P",
|
||||||
|
permission: ApiKeyPermission.read,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("GET /api/v3/surveys", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
getServerSession.mockResolvedValue({
|
||||||
|
user: { id: "user_1", name: "User", email: "u@example.com" },
|
||||||
|
expires: "2026-01-01",
|
||||||
|
} as any);
|
||||||
|
mockAuthenticateRequest.mockResolvedValue(null);
|
||||||
|
vi.mocked(requireV3WorkspaceAccess).mockImplementation(async (auth, workspaceId) => {
|
||||||
|
if (auth && "apiKeyId" in auth) {
|
||||||
|
const p = auth.environmentPermissions.find((e) => e.environmentId === workspaceId);
|
||||||
|
if (!p) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
title: "Forbidden",
|
||||||
|
status: 403,
|
||||||
|
detail: "You are not authorized to access this resource",
|
||||||
|
requestId: "req",
|
||||||
|
}),
|
||||||
|
{ status: 403, headers: { "Content-Type": "application/problem+json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
environmentId: workspaceId,
|
||||||
|
projectId: p.projectId,
|
||||||
|
organizationId: auth.organizationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
environmentId: resolvedEnvironmentId,
|
||||||
|
projectId: "proj_1",
|
||||||
|
organizationId: "org_1",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
vi.mocked(getSurveyListPage).mockResolvedValue({ surveys: [], nextCursor: null });
|
||||||
|
vi.mocked(getSurveyCount).mockResolvedValue(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 401 when no session and no API key", async () => {
|
||||||
|
getServerSession.mockResolvedValue(null);
|
||||||
|
mockAuthenticateRequest.mockResolvedValue(null);
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
expect(res.headers.get("Content-Type")).toBe("application/problem+json");
|
||||||
|
expect(requireV3WorkspaceAccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 200 with session and valid workspaceId", async () => {
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`, "req-456");
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers.get("Content-Type")).toBe("application/json");
|
||||||
|
expect(res.headers.get("X-Request-Id")).toBe("req-456");
|
||||||
|
expect(requireV3WorkspaceAccess).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ user: expect.any(Object) }),
|
||||||
|
validWorkspaceId,
|
||||||
|
"read",
|
||||||
|
"req-456",
|
||||||
|
"/api/v3/surveys"
|
||||||
|
);
|
||||||
|
expect(getSurveyListPage).toHaveBeenCalledWith(resolvedEnvironmentId, {
|
||||||
|
limit: 20,
|
||||||
|
cursor: null,
|
||||||
|
sortBy: "updatedAt",
|
||||||
|
filterCriteria: undefined,
|
||||||
|
});
|
||||||
|
expect(getSurveyCount).toHaveBeenCalledWith(resolvedEnvironmentId, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 200 with x-api-key when workspace is on the key", async () => {
|
||||||
|
getServerSession.mockResolvedValue(null);
|
||||||
|
mockAuthenticateRequest.mockResolvedValue(apiKeyAuth as any);
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`, "req-k", {
|
||||||
|
"x-api-key": "fbk_test",
|
||||||
|
});
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(requireV3WorkspaceAccess).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ apiKeyId: "key_1" }),
|
||||||
|
validWorkspaceId,
|
||||||
|
"read",
|
||||||
|
"req-k",
|
||||||
|
"/api/v3/surveys"
|
||||||
|
);
|
||||||
|
expect(getSurveyListPage).toHaveBeenCalledWith(validWorkspaceId, {
|
||||||
|
limit: 20,
|
||||||
|
cursor: null,
|
||||||
|
sortBy: "updatedAt",
|
||||||
|
filterCriteria: undefined,
|
||||||
|
});
|
||||||
|
expect(getSurveyCount).toHaveBeenCalledWith(validWorkspaceId, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when API key does not include workspace", async () => {
|
||||||
|
getServerSession.mockResolvedValue(null);
|
||||||
|
mockAuthenticateRequest.mockResolvedValue({
|
||||||
|
...apiKeyAuth,
|
||||||
|
environmentPermissions: [
|
||||||
|
{
|
||||||
|
environmentId: "claa1111111111111111111111",
|
||||||
|
environmentType: EnvironmentType.development,
|
||||||
|
projectId: "proj_x",
|
||||||
|
projectName: "X",
|
||||||
|
permission: ApiKeyPermission.read,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any);
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`, undefined, {
|
||||||
|
"x-api-key": "fbk_test",
|
||||||
|
});
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 when the createdBy filter is used", async () => {
|
||||||
|
const req = createRequest(
|
||||||
|
`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}&filter[createdBy][in]=you`
|
||||||
|
);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.invalid_params?.some((p: { name: string }) => p.name === "filter[createdBy][in]")).toBe(true);
|
||||||
|
expect(requireV3WorkspaceAccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 when workspaceId is missing", async () => {
|
||||||
|
const req = createRequest("http://localhost/api/v3/surveys");
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(requireV3WorkspaceAccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 when workspaceId is not cuid2", async () => {
|
||||||
|
const req = createRequest("http://localhost/api/v3/surveys?workspaceId=not-a-cuid");
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 when limit exceeds max", async () => {
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}&limit=101`);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reflects limit, nextCursor, and totalCount in meta", async () => {
|
||||||
|
vi.mocked(getSurveyListPage).mockResolvedValue({
|
||||||
|
surveys: [],
|
||||||
|
nextCursor: "cursor-123",
|
||||||
|
});
|
||||||
|
vi.mocked(getSurveyCount).mockResolvedValue(42);
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}&limit=10`);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.meta).toEqual({ limit: 10, nextCursor: "cursor-123", totalCount: 42 });
|
||||||
|
expect(getSurveyListPage).toHaveBeenCalledWith(resolvedEnvironmentId, {
|
||||||
|
limit: 10,
|
||||||
|
cursor: null,
|
||||||
|
sortBy: "updatedAt",
|
||||||
|
filterCriteria: undefined,
|
||||||
|
});
|
||||||
|
expect(getSurveyCount).toHaveBeenCalledWith(resolvedEnvironmentId, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes filter query to getSurveyListPage", async () => {
|
||||||
|
const filterCriteria = { status: ["inProgress"] };
|
||||||
|
const req = createRequest(
|
||||||
|
`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}&filter[status][in]=inProgress&sortBy=updatedAt`
|
||||||
|
);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(getSurveyListPage).toHaveBeenCalledWith(resolvedEnvironmentId, {
|
||||||
|
limit: 20,
|
||||||
|
cursor: null,
|
||||||
|
sortBy: "updatedAt",
|
||||||
|
filterCriteria,
|
||||||
|
});
|
||||||
|
expect(getSurveyCount).toHaveBeenCalledWith(resolvedEnvironmentId, filterCriteria);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 400 when filterCriteria is used", async () => {
|
||||||
|
const req = createRequest(
|
||||||
|
`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}&filterCriteria=${encodeURIComponent("{}")}`
|
||||||
|
);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(requireV3WorkspaceAccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when auth returns 403", async () => {
|
||||||
|
vi.mocked(requireV3WorkspaceAccess).mockResolvedValueOnce(
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
title: "Forbidden",
|
||||||
|
status: 403,
|
||||||
|
detail: "You are not authorized to access this resource",
|
||||||
|
requestId: "req-789",
|
||||||
|
}),
|
||||||
|
{ status: 403, headers: { "Content-Type": "application/problem+json" } }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("list items expose workspaceId instead of environmentId and omit internal fields", async () => {
|
||||||
|
vi.mocked(getSurveyListPage).mockResolvedValue({
|
||||||
|
surveys: [
|
||||||
|
{
|
||||||
|
id: "s1",
|
||||||
|
name: "Survey 1",
|
||||||
|
environmentId: "env_1",
|
||||||
|
type: "link",
|
||||||
|
status: "draft",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
responseCount: 0,
|
||||||
|
creator: { name: "Test" },
|
||||||
|
singleUse: null,
|
||||||
|
} as any,
|
||||||
|
],
|
||||||
|
nextCursor: null,
|
||||||
|
});
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`);
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.data[0]).not.toHaveProperty("blocks");
|
||||||
|
expect(body.data[0]).not.toHaveProperty("singleUse");
|
||||||
|
expect(body.data[0]).not.toHaveProperty("_count");
|
||||||
|
expect(body.data[0]).not.toHaveProperty("environmentId");
|
||||||
|
expect(body.data[0].id).toBe("s1");
|
||||||
|
expect(body.data[0].workspaceId).toBe("env_1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 403 when getSurveyListPage throws ResourceNotFoundError", async () => {
|
||||||
|
vi.mocked(getSurveyListPage).mockRejectedValueOnce(new ResourceNotFoundError("survey", "s1"));
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`, "req-nf");
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("forbidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 500 when getSurveyListPage throws DatabaseError", async () => {
|
||||||
|
vi.mocked(getSurveyListPage).mockRejectedValueOnce(new DatabaseError("db down"));
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`, "req-db");
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("internal_server_error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 500 on unexpected error from getSurveyListPage", async () => {
|
||||||
|
vi.mocked(getSurveyListPage).mockRejectedValueOnce(new Error("boom"));
|
||||||
|
const req = createRequest(`http://localhost/api/v3/surveys?workspaceId=${validWorkspaceId}`, "req-err");
|
||||||
|
const res = await GET(req, {} as any);
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.code).toBe("internal_server_error");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* GET /api/v3/surveys — list surveys for a workspace.
|
||||||
|
* Session cookie or x-api-key; scope by workspaceId only.
|
||||||
|
*/
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { withV3ApiWrapper } from "@/app/api/v3/lib/api-wrapper";
|
||||||
|
import { requireV3WorkspaceAccess } from "@/app/api/v3/lib/auth";
|
||||||
|
import {
|
||||||
|
problemBadRequest,
|
||||||
|
problemForbidden,
|
||||||
|
problemInternalError,
|
||||||
|
successListResponse,
|
||||||
|
} from "@/app/api/v3/lib/response";
|
||||||
|
import { getSurveyCount } from "@/modules/survey/list/lib/survey";
|
||||||
|
import { getSurveyListPage } from "@/modules/survey/list/lib/survey-page";
|
||||||
|
import { parseV3SurveysListQuery } from "./parse-v3-surveys-list-query";
|
||||||
|
import { serializeV3SurveyListItem } from "./serializers";
|
||||||
|
|
||||||
|
export const GET = withV3ApiWrapper({
|
||||||
|
auth: "both",
|
||||||
|
handler: async ({ req, authentication, requestId, instance }) => {
|
||||||
|
const log = logger.withContext({ requestId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const searchParams = new URL(req.url).searchParams;
|
||||||
|
const parsed = parseV3SurveysListQuery(searchParams);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
log.warn({ statusCode: 400, invalidParams: parsed.invalid_params }, "Validation failed");
|
||||||
|
return problemBadRequest(requestId, "Invalid query parameters", {
|
||||||
|
invalid_params: parsed.invalid_params,
|
||||||
|
instance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = await requireV3WorkspaceAccess(
|
||||||
|
authentication,
|
||||||
|
parsed.workspaceId,
|
||||||
|
"read",
|
||||||
|
requestId,
|
||||||
|
instance
|
||||||
|
);
|
||||||
|
if (authResult instanceof Response) {
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { environmentId } = authResult;
|
||||||
|
|
||||||
|
const [{ surveys, nextCursor }, totalCount] = await Promise.all([
|
||||||
|
getSurveyListPage(environmentId, {
|
||||||
|
limit: parsed.limit,
|
||||||
|
cursor: parsed.cursor,
|
||||||
|
sortBy: parsed.sortBy,
|
||||||
|
filterCriteria: parsed.filterCriteria,
|
||||||
|
}),
|
||||||
|
getSurveyCount(environmentId, parsed.filterCriteria),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return successListResponse(
|
||||||
|
surveys.map(serializeV3SurveyListItem),
|
||||||
|
{
|
||||||
|
limit: parsed.limit,
|
||||||
|
nextCursor,
|
||||||
|
totalCount,
|
||||||
|
},
|
||||||
|
{ requestId, cache: "private, no-store" }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ResourceNotFoundError) {
|
||||||
|
log.warn({ statusCode: 403, errorCode: err.name }, "Resource not found");
|
||||||
|
return problemForbidden(requestId, "You are not authorized to access this resource", instance);
|
||||||
|
}
|
||||||
|
if (err instanceof DatabaseError) {
|
||||||
|
log.error({ error: err, statusCode: 500 }, "Database error");
|
||||||
|
return problemInternalError(requestId, "An unexpected error occurred.", instance);
|
||||||
|
}
|
||||||
|
log.error({ error: err, statusCode: 500 }, "V3 surveys list unexpected error");
|
||||||
|
return problemInternalError(requestId, "An unexpected error occurred.", instance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { TSurvey } from "@/modules/survey/list/types/surveys";
|
||||||
|
|
||||||
|
export type TV3SurveyListItem = Omit<TSurvey, "environmentId" | "singleUse"> & {
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep the v3 API contract isolated from internal persistence naming.
|
||||||
|
* Internally surveys are still scoped by environmentId; externally v3 exposes workspaceId.
|
||||||
|
*/
|
||||||
|
export function serializeV3SurveyListItem(survey: TSurvey): TV3SurveyListItem {
|
||||||
|
const { environmentId, singleUse: _omitSingleUse, ...rest } = survey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
workspaceId: environmentId,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { getIsActiveCustomerAction } from "./actions";
|
||||||
|
|
||||||
interface ChatwootWidgetProps {
|
interface ChatwootWidgetProps {
|
||||||
chatwootBaseUrl: string;
|
chatwootBaseUrl: string;
|
||||||
@@ -12,6 +13,18 @@ interface ChatwootWidgetProps {
|
|||||||
|
|
||||||
const CHATWOOT_SCRIPT_ID = "chatwoot-script";
|
const CHATWOOT_SCRIPT_ID = "chatwoot-script";
|
||||||
|
|
||||||
|
interface ChatwootInstance {
|
||||||
|
setUser: (
|
||||||
|
userId: string,
|
||||||
|
userInfo: {
|
||||||
|
email?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
setCustomAttributes: (attributes: Record<string, unknown>) => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const ChatwootWidget = ({
|
export const ChatwootWidget = ({
|
||||||
userEmail,
|
userEmail,
|
||||||
userName,
|
userName,
|
||||||
@@ -20,15 +33,14 @@ export const ChatwootWidget = ({
|
|||||||
chatwootBaseUrl,
|
chatwootBaseUrl,
|
||||||
}: ChatwootWidgetProps) => {
|
}: ChatwootWidgetProps) => {
|
||||||
const userSetRef = useRef(false);
|
const userSetRef = useRef(false);
|
||||||
|
const customerStatusSetRef = useRef(false);
|
||||||
|
|
||||||
|
const getChatwoot = useCallback((): ChatwootInstance | null => {
|
||||||
|
return (globalThis as unknown as { $chatwoot: ChatwootInstance }).$chatwoot ?? null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setUserInfo = useCallback(() => {
|
const setUserInfo = useCallback(() => {
|
||||||
const $chatwoot = (
|
const $chatwoot = getChatwoot();
|
||||||
globalThis as unknown as {
|
|
||||||
$chatwoot: {
|
|
||||||
setUser: (userId: string, userInfo: { email?: string | null; name?: string | null }) => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
).$chatwoot;
|
|
||||||
if (userId && $chatwoot && !userSetRef.current) {
|
if (userId && $chatwoot && !userSetRef.current) {
|
||||||
$chatwoot.setUser(userId, {
|
$chatwoot.setUser(userId, {
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -36,7 +48,19 @@ export const ChatwootWidget = ({
|
|||||||
});
|
});
|
||||||
userSetRef.current = true;
|
userSetRef.current = true;
|
||||||
}
|
}
|
||||||
}, [userId, userEmail, userName]);
|
}, [userId, userEmail, userName, getChatwoot]);
|
||||||
|
|
||||||
|
const setCustomerStatus = useCallback(async () => {
|
||||||
|
if (customerStatusSetRef.current) return;
|
||||||
|
const $chatwoot = getChatwoot();
|
||||||
|
if (!$chatwoot) return;
|
||||||
|
|
||||||
|
const response = await getIsActiveCustomerAction();
|
||||||
|
if (response?.data !== undefined) {
|
||||||
|
$chatwoot.setCustomAttributes({ isActiveCustomer: response.data });
|
||||||
|
}
|
||||||
|
customerStatusSetRef.current = true;
|
||||||
|
}, [getChatwoot]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatwootWebsiteToken) return;
|
if (!chatwootWebsiteToken) return;
|
||||||
@@ -65,23 +89,19 @@ export const ChatwootWidget = ({
|
|||||||
const handleChatwootReady = () => setUserInfo();
|
const handleChatwootReady = () => setUserInfo();
|
||||||
globalThis.addEventListener("chatwoot:ready", handleChatwootReady);
|
globalThis.addEventListener("chatwoot:ready", handleChatwootReady);
|
||||||
|
|
||||||
|
const handleChatwootOpen = () => setCustomerStatus();
|
||||||
|
globalThis.addEventListener("chatwoot:open", handleChatwootOpen);
|
||||||
|
|
||||||
// Check if Chatwoot is already ready
|
// Check if Chatwoot is already ready
|
||||||
if (
|
if (getChatwoot()) {
|
||||||
(
|
|
||||||
globalThis as unknown as {
|
|
||||||
$chatwoot: {
|
|
||||||
setUser: (userId: string, userInfo: { email?: string | null; name?: string | null }) => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
).$chatwoot
|
|
||||||
) {
|
|
||||||
setUserInfo();
|
setUserInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
globalThis.removeEventListener("chatwoot:ready", handleChatwootReady);
|
globalThis.removeEventListener("chatwoot:ready", handleChatwootReady);
|
||||||
|
globalThis.removeEventListener("chatwoot:open", handleChatwootOpen);
|
||||||
|
|
||||||
const $chatwoot = (globalThis as unknown as { $chatwoot: { reset: () => void } }).$chatwoot;
|
const $chatwoot = getChatwoot();
|
||||||
if ($chatwoot) {
|
if ($chatwoot) {
|
||||||
$chatwoot.reset();
|
$chatwoot.reset();
|
||||||
}
|
}
|
||||||
@@ -90,8 +110,18 @@ export const ChatwootWidget = ({
|
|||||||
scriptElement?.remove();
|
scriptElement?.remove();
|
||||||
|
|
||||||
userSetRef.current = false;
|
userSetRef.current = false;
|
||||||
|
customerStatusSetRef.current = false;
|
||||||
};
|
};
|
||||||
}, [chatwootBaseUrl, chatwootWebsiteToken, userId, userEmail, userName, setUserInfo]);
|
}, [
|
||||||
|
chatwootBaseUrl,
|
||||||
|
chatwootWebsiteToken,
|
||||||
|
userId,
|
||||||
|
userEmail,
|
||||||
|
userName,
|
||||||
|
setUserInfo,
|
||||||
|
setCustomerStatus,
|
||||||
|
getChatwoot,
|
||||||
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { TCloudBillingPlan } from "@formbricks/types/organizations";
|
||||||
|
import { getOrganizationsByUserId } from "@/lib/organization/service";
|
||||||
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
|
|
||||||
|
export const getIsActiveCustomerAction = authenticatedActionClient.action(async ({ ctx }) => {
|
||||||
|
const paidBillingPlans = new Set<TCloudBillingPlan>(["pro", "scale", "custom"]);
|
||||||
|
|
||||||
|
const organizations = await getOrganizationsByUserId(ctx.user.id);
|
||||||
|
return organizations.some((organization) => {
|
||||||
|
const stripe = organization.billing.stripe;
|
||||||
|
const isPaidPlan = stripe?.plan ? paidBillingPlans.has(stripe.plan) : false;
|
||||||
|
const isActiveSubscription =
|
||||||
|
stripe?.subscriptionStatus === "active" || stripe?.subscriptionStatus === "trialing";
|
||||||
|
return isPaidPlan && isActiveSubscription;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { TUserLocale } from "@formbricks/types/user";
|
||||||
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
|
|
||||||
|
interface NoScriptWarningProps {
|
||||||
|
locale: TUserLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoScriptWarning = async ({ locale }: NoScriptWarningProps) => {
|
||||||
|
const t = await getTranslate(locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<noscript>
|
||||||
|
<div className="fixed inset-0 z-[9999] flex h-dvh w-full items-center justify-center bg-slate-50">
|
||||||
|
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center shadow-lg">
|
||||||
|
<h1 className="mb-4 text-2xl font-bold text-slate-800">{t("common.javascript_required")}</h1>
|
||||||
|
<p className="text-slate-600">{t("common.javascript_required_description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { NoScriptWarning } from "@/app/components/NoScriptWarning";
|
||||||
import { SentryProvider } from "@/app/sentry/SentryProvider";
|
import { SentryProvider } from "@/app/sentry/SentryProvider";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
@@ -26,6 +27,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
|||||||
return (
|
return (
|
||||||
<html lang={locale} translate="no">
|
<html lang={locale} translate="no">
|
||||||
<body className="flex h-dvh flex-col transition-all ease-in-out">
|
<body className="flex h-dvh flex-col transition-all ease-in-out">
|
||||||
|
<NoScriptWarning locale={locale} />
|
||||||
<SentryProvider
|
<SentryProvider
|
||||||
sentryDsn={SENTRY_DSN}
|
sentryDsn={SENTRY_DSN}
|
||||||
sentryRelease={SENTRY_RELEASE}
|
sentryRelease={SENTRY_RELEASE}
|
||||||
|
|||||||
@@ -421,6 +421,38 @@ describe("withV1ApiWrapper", () => {
|
|||||||
expect(handler).not.toHaveBeenCalled();
|
expect(handler).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("uses unauthenticatedResponse when provided instead of default 401", async () => {
|
||||||
|
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||||
|
await import("@/app/middleware/endpoint-validator");
|
||||||
|
const { getServerSession } = await import("next-auth");
|
||||||
|
|
||||||
|
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||||
|
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||||
|
isManagementApi: true,
|
||||||
|
authenticationMethod: AuthenticationMethod.Session,
|
||||||
|
});
|
||||||
|
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||||
|
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const custom401 = new Response(JSON.stringify({ title: "Custom", status: 401 }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/problem+json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = vi.fn();
|
||||||
|
const req = createMockRequest({ url: "https://api.test/api/v3/surveys" });
|
||||||
|
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||||
|
const wrapped = withV1ApiWrapper({
|
||||||
|
handler,
|
||||||
|
unauthenticatedResponse: () => custom401,
|
||||||
|
});
|
||||||
|
const res = await wrapped(req, undefined);
|
||||||
|
|
||||||
|
expect(res).toBe(custom401);
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
expect(mockContextualLoggerError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test("handles rate limiting errors", async () => {
|
test("handles rate limiting errors", async () => {
|
||||||
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export interface TWithV1ApiWrapperParams<TResult extends { response: Response },
|
|||||||
action?: TAuditAction;
|
action?: TAuditAction;
|
||||||
targetType?: TAuditTarget;
|
targetType?: TAuditTarget;
|
||||||
customRateLimitConfig?: TRateLimitConfig;
|
customRateLimitConfig?: TRateLimitConfig;
|
||||||
|
/**
|
||||||
|
* When the route requires auth but the client is unauthenticated, the wrapper normally returns
|
||||||
|
* the legacy JSON 401. Use this to return a custom response (e.g. RFC 9457 problem+json for V3).
|
||||||
|
*/
|
||||||
|
unauthenticatedResponse?: (req: NextRequest) => Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ApiV1RouteTypeEnum {
|
enum ApiV1RouteTypeEnum {
|
||||||
@@ -265,7 +270,7 @@ const getRouteType = (
|
|||||||
export const withV1ApiWrapper = <TResult extends { response: Response }, TProps = unknown>(
|
export const withV1ApiWrapper = <TResult extends { response: Response }, TProps = unknown>(
|
||||||
params: TWithV1ApiWrapperParams<TResult, TProps>
|
params: TWithV1ApiWrapperParams<TResult, TProps>
|
||||||
): ((req: NextRequest, props: TProps) => Promise<Response>) => {
|
): ((req: NextRequest, props: TProps) => Promise<Response>) => {
|
||||||
const { handler, action, targetType, customRateLimitConfig } = params;
|
const { handler, action, targetType, customRateLimitConfig, unauthenticatedResponse } = params;
|
||||||
return async (req: NextRequest, props: TProps): Promise<Response> => {
|
return async (req: NextRequest, props: TProps): Promise<Response> => {
|
||||||
// === Audit Log Setup ===
|
// === Audit Log Setup ===
|
||||||
const saveAuditLog = action && targetType;
|
const saveAuditLog = action && targetType;
|
||||||
@@ -287,6 +292,11 @@ export const withV1ApiWrapper = <TResult extends { response: Response }, TProps
|
|||||||
const authentication = await handleAuthentication(authenticationMethod, req);
|
const authentication = await handleAuthentication(authenticationMethod, req);
|
||||||
|
|
||||||
if (!authentication && routeType !== ApiV1RouteTypeEnum.Client) {
|
if (!authentication && routeType !== ApiV1RouteTypeEnum.Client) {
|
||||||
|
if (unauthenticatedResponse) {
|
||||||
|
const res = unauthenticatedResponse(req);
|
||||||
|
await processResponse(res, req, auditLog);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
return responses.notAuthenticatedResponse();
|
return responses.notAuthenticatedResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ describe("endpoint-validator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("isManagementApiRoute", () => {
|
describe("isManagementApiRoute", () => {
|
||||||
|
test("should return Both for v3 surveys routes", () => {
|
||||||
|
expect(isManagementApiRoute("/api/v3/surveys")).toEqual({
|
||||||
|
isManagementApi: true,
|
||||||
|
authenticationMethod: AuthenticationMethod.Both,
|
||||||
|
});
|
||||||
|
expect(isManagementApiRoute("/api/v3/surveys/clxxxxxxxxxxxxxxxxxxxxxxxx")).toEqual({
|
||||||
|
isManagementApi: true,
|
||||||
|
authenticationMethod: AuthenticationMethod.Both,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should return correct object for management API routes with API key authentication", () => {
|
test("should return correct object for management API routes with API key authentication", () => {
|
||||||
expect(isManagementApiRoute("/api/v1/management/something")).toEqual({
|
expect(isManagementApiRoute("/api/v1/management/something")).toEqual({
|
||||||
isManagementApi: true,
|
isManagementApi: true,
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const isClientSideApiRoute = (url: string): { isClientSideApi: boolean; i
|
|||||||
export const isManagementApiRoute = (
|
export const isManagementApiRoute = (
|
||||||
url: string
|
url: string
|
||||||
): { isManagementApi: boolean; authenticationMethod: AuthenticationMethod } => {
|
): { isManagementApi: boolean; authenticationMethod: AuthenticationMethod } => {
|
||||||
|
// V3 surveys: session cookie or x-api-key (same pattern as management storage)
|
||||||
|
if (/^\/api\/v3\/surveys(?:\/|$)/.test(url))
|
||||||
|
return { isManagementApi: true, authenticationMethod: AuthenticationMethod.Both };
|
||||||
if (url.includes("/api/v1/management/storage"))
|
if (url.includes("/api/v1/management/storage"))
|
||||||
return { isManagementApi: true, authenticationMethod: AuthenticationMethod.Both };
|
return { isManagementApi: true, authenticationMethod: AuthenticationMethod.Both };
|
||||||
if (url.includes("/api/v1/webhooks"))
|
if (url.includes("/api/v1/webhooks"))
|
||||||
|
|||||||
+47
-13
@@ -140,6 +140,7 @@ checksums:
|
|||||||
common/connect: 8778ee245078a8be4a2ce855c8c56edc
|
common/connect: 8778ee245078a8be4a2ce855c8c56edc
|
||||||
common/connect_formbricks: a9dd747575e7e035da69251366df6f95
|
common/connect_formbricks: a9dd747575e7e035da69251366df6f95
|
||||||
common/connected: aa0ceca574641de34c74b9e590664230
|
common/connected: aa0ceca574641de34c74b9e590664230
|
||||||
|
common/contact: 9afa39bc47019ee6dec6c74b6273967c
|
||||||
common/contacts: d5b6c3f890b3904eaf5754081945c03d
|
common/contacts: d5b6c3f890b3904eaf5754081945c03d
|
||||||
common/continue: 3cfba90b4600131e82fc4260c568d044
|
common/continue: 3cfba90b4600131e82fc4260c568d044
|
||||||
common/copied: 29208e06d704c4fc4b8b534dc7acc4ef
|
common/copied: 29208e06d704c4fc4b8b534dc7acc4ef
|
||||||
@@ -147,6 +148,7 @@ checksums:
|
|||||||
common/copy: 627c00d2c850b9b45f7341a6ac01b6bb
|
common/copy: 627c00d2c850b9b45f7341a6ac01b6bb
|
||||||
common/copy_code: 704c13d9bc01caad29a1cf3179baa111
|
common/copy_code: 704c13d9bc01caad29a1cf3179baa111
|
||||||
common/copy_link: 57a37acfe6d7ed71d00fbbc8079fbb35
|
common/copy_link: 57a37acfe6d7ed71d00fbbc8079fbb35
|
||||||
|
common/copy_to_environment: c482d26b8fd4962af6542bbf49e49a32
|
||||||
common/count_attributes: 48805e836a9b50f9635ad00fed953058
|
common/count_attributes: 48805e836a9b50f9635ad00fed953058
|
||||||
common/count_contacts: 9f71d503455264f1eec1ae58894cf143
|
common/count_contacts: 9f71d503455264f1eec1ae58894cf143
|
||||||
common/count_members: 31ce64ca63fdf95e02ab5543b6e2f717
|
common/count_members: 31ce64ca63fdf95e02ab5543b6e2f717
|
||||||
@@ -186,12 +188,12 @@ checksums:
|
|||||||
common/duplicate_copy_number: 083cfffd294672043dcbcc4c3dfeac6a
|
common/duplicate_copy_number: 083cfffd294672043dcbcc4c3dfeac6a
|
||||||
common/e_commerce: b9584e7d0449a6d1b0c182d7ff14061e
|
common/e_commerce: b9584e7d0449a6d1b0c182d7ff14061e
|
||||||
common/edit: eee7f39ff90b18852afc1671f21fbaa9
|
common/edit: eee7f39ff90b18852afc1671f21fbaa9
|
||||||
|
common/elements: 8cb054d952b341e5965284860d532bc7
|
||||||
common/email: e7f34943a0c2fb849db1839ff6ef5cb5
|
common/email: e7f34943a0c2fb849db1839ff6ef5cb5
|
||||||
common/ending_card: 16d30d3a36472159da8c2dbd374dfe22
|
common/ending_card: 16d30d3a36472159da8c2dbd374dfe22
|
||||||
common/enter_url: 468c2276d0f2cb971ff5a47a20fa4b97
|
common/enter_url: 468c2276d0f2cb971ff5a47a20fa4b97
|
||||||
common/enterprise_license: e81bf506f47968870c7bd07245648a0d
|
common/enterprise_license: e81bf506f47968870c7bd07245648a0d
|
||||||
common/environment: 0844e8dc1485339c8de066dc0a9bb6a1
|
common/environment: 0844e8dc1485339c8de066dc0a9bb6a1
|
||||||
common/environment_not_found: 4d7610bdb55a8b5e6131bb5b08ce04c5
|
|
||||||
common/environment_notice: 228a8668be1812e031f438d166861729
|
common/environment_notice: 228a8668be1812e031f438d166861729
|
||||||
common/error: 3c95bcb32c2104b99a46f5b3dd015248
|
common/error: 3c95bcb32c2104b99a46f5b3dd015248
|
||||||
common/error_component_description: fa9eee04f864c3fe6e6681f716caa015
|
common/error_component_description: fa9eee04f864c3fe6e6681f716caa015
|
||||||
@@ -228,11 +230,13 @@ checksums:
|
|||||||
common/inactive_surveys: 324b8e1844739cdc2a3bc71aef143a76
|
common/inactive_surveys: 324b8e1844739cdc2a3bc71aef143a76
|
||||||
common/integration: 40d02f65c4356003e0e90ffb944907d2
|
common/integration: 40d02f65c4356003e0e90ffb944907d2
|
||||||
common/integrations: 0ccce343287704cd90150c32e2fcad36
|
common/integrations: 0ccce343287704cd90150c32e2fcad36
|
||||||
common/invalid_date: 4c18c82f7317d4a02f8d5fef611e82b7
|
common/invalid_date_with_value: f7f9dbe99f25f1724367ee57572b52bf
|
||||||
common/invalid_file_name: 8243c91b898110fb15ebb24aa6a7d313
|
common/invalid_file_name: 8243c91b898110fb15ebb24aa6a7d313
|
||||||
common/invalid_file_type: f0c83e7d61dbad8250abb59869af4b9e
|
common/invalid_file_type: f0c83e7d61dbad8250abb59869af4b9e
|
||||||
common/invite: 181884cea804cbde665f160811ee7ad0
|
common/invite: 181884cea804cbde665f160811ee7ad0
|
||||||
common/invite_them: d4b7aadbd3c924b04ad4fce419709f10
|
common/invite_them: d4b7aadbd3c924b04ad4fce419709f10
|
||||||
|
common/javascript_required: d7988e5934af4d0df54fda369c0e4fb6
|
||||||
|
common/javascript_required_description: 4b65f456db79af4898888a3dd034fe2f
|
||||||
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
|
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
|
||||||
common/label: a5c71bf158481233f8215dbd38cc196b
|
common/label: a5c71bf158481233f8215dbd38cc196b
|
||||||
common/language: 277fd1a41cc237a437cd1d5e4a80463b
|
common/language: 277fd1a41cc237a437cd1d5e4a80463b
|
||||||
@@ -253,7 +257,9 @@ checksums:
|
|||||||
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||||
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||||
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
||||||
|
common/membership: 83c856bbc2af99d8c3d860959d1d2a85
|
||||||
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
||||||
|
common/meta: 842eac888f134f3525f8ea613d933687
|
||||||
common/metadata: 695d4f7da261ba76e3be4de495491028
|
common/metadata: 695d4f7da261ba76e3be4de495491028
|
||||||
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
|
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
|
||||||
common/mobile_overlay_surveys_look_good: d85169e86077738b9837647bf6d1c7d2
|
common/mobile_overlay_surveys_look_good: d85169e86077738b9837647bf6d1c7d2
|
||||||
@@ -267,6 +273,7 @@ checksums:
|
|||||||
common/new: 126d036fae5fb6b629728ecb97e6195b
|
common/new: 126d036fae5fb6b629728ecb97e6195b
|
||||||
common/new_version_available: 399ddfc4232712e18ddab2587356b3dc
|
common/new_version_available: 399ddfc4232712e18ddab2587356b3dc
|
||||||
common/next: 89ddbcf710eba274963494f312bdc8a9
|
common/next: 89ddbcf710eba274963494f312bdc8a9
|
||||||
|
common/no_actions_found: 4d92b789eb121fc76cd6868136dcbcd4
|
||||||
common/no_background_image_found: 4108a781a9022c65671a826d4e299d5b
|
common/no_background_image_found: 4108a781a9022c65671a826d4e299d5b
|
||||||
common/no_code: f602144ab7d28a5b19a446bf74b4dcc4
|
common/no_code: f602144ab7d28a5b19a446bf74b4dcc4
|
||||||
common/no_files_uploaded: c97be829e195a41b2f6b6717b87a232b
|
common/no_files_uploaded: c97be829e195a41b2f6b6717b87a232b
|
||||||
@@ -292,10 +299,9 @@ checksums:
|
|||||||
common/or: 7b133c38bec0d5ee23cc6bcf9a8de50b
|
common/or: 7b133c38bec0d5ee23cc6bcf9a8de50b
|
||||||
common/organization: 3dc8489af7e74121f65ce6d9677bc94d
|
common/organization: 3dc8489af7e74121f65ce6d9677bc94d
|
||||||
common/organization_id: ef09b71c84a25b5da02a23c77e68a335
|
common/organization_id: ef09b71c84a25b5da02a23c77e68a335
|
||||||
common/organization_not_found: 4cb8c07ec2c599b6f48750e06ffa182b
|
|
||||||
common/organization_settings: 11528aa89ae9935e55dcb54478058775
|
common/organization_settings: 11528aa89ae9935e55dcb54478058775
|
||||||
common/organization_teams_not_found: ce29fcb7a4e8b4582f92b65dea9b7d4e
|
|
||||||
common/other: 79acaa6cd481262bea4e743a422529d2
|
common/other: 79acaa6cd481262bea4e743a422529d2
|
||||||
|
common/other_filters: 20b09213c131db47eb8b23e72d0c4bea
|
||||||
common/others: 39160224ce0e35eb4eb252c997edf4d8
|
common/others: 39160224ce0e35eb4eb252c997edf4d8
|
||||||
common/overlay_color: 4b72073285d13fff93d094aabffe05ac
|
common/overlay_color: 4b72073285d13fff93d094aabffe05ac
|
||||||
common/overview: 30c54e4dc4ce599b87d94be34a8617f5
|
common/overview: 30c54e4dc4ce599b87d94be34a8617f5
|
||||||
@@ -312,6 +318,7 @@ checksums:
|
|||||||
common/please_select_at_least_one_survey: fb1cbeb670480115305e23444c347e50
|
common/please_select_at_least_one_survey: fb1cbeb670480115305e23444c347e50
|
||||||
common/please_select_at_least_one_trigger: e88e64a1010a039745e80ed2e30951fe
|
common/please_select_at_least_one_trigger: e88e64a1010a039745e80ed2e30951fe
|
||||||
common/please_upgrade_your_plan: 03d54a21ecd27723c72a13644837e5ed
|
common/please_upgrade_your_plan: 03d54a21ecd27723c72a13644837e5ed
|
||||||
|
common/powered_by_formbricks: 1c3e19894583292bfaf686cac84a4960
|
||||||
common/preview: 3173ee1f0f1d4e50665ca4a84c38e15d
|
common/preview: 3173ee1f0f1d4e50665ca4a84c38e15d
|
||||||
common/preview_survey: 7409e9c118e3e5d5f2a86201c2b354f2
|
common/preview_survey: 7409e9c118e3e5d5f2a86201c2b354f2
|
||||||
common/privacy: 7459744a63ef8af4e517a09024bd7c08
|
common/privacy: 7459744a63ef8af4e517a09024bd7c08
|
||||||
@@ -353,6 +360,7 @@ checksums:
|
|||||||
common/select: 5ac04c47a98deb85906bc02e0de91ab0
|
common/select: 5ac04c47a98deb85906bc02e0de91ab0
|
||||||
common/select_all: eedc7cdb02de467c15dc418a066a77f2
|
common/select_all: eedc7cdb02de467c15dc418a066a77f2
|
||||||
common/select_filter: c50082c3981f1161022f9787a19aed71
|
common/select_filter: c50082c3981f1161022f9787a19aed71
|
||||||
|
common/select_language: d75cf5fbce8a4c7a9055e2210af74480
|
||||||
common/select_survey: bac52e59c7847417bef6fe7b7096b475
|
common/select_survey: bac52e59c7847417bef6fe7b7096b475
|
||||||
common/select_teams: ae5d451929846ae6367562bc671a1af9
|
common/select_teams: ae5d451929846ae6367562bc671a1af9
|
||||||
common/selected: 9f09e059ba20c88ed34e2b4e8e032d56
|
common/selected: 9f09e059ba20c88ed34e2b4e8e032d56
|
||||||
@@ -385,7 +393,6 @@ checksums:
|
|||||||
common/survey_id: 08303e98b3d4134947256e494b0c829e
|
common/survey_id: 08303e98b3d4134947256e494b0c829e
|
||||||
common/survey_languages: 93e4a10ab190e6b1e1f7fe5f702df249
|
common/survey_languages: 93e4a10ab190e6b1e1f7fe5f702df249
|
||||||
common/survey_live: d1f370505c67509e7b2759952daba20d
|
common/survey_live: d1f370505c67509e7b2759952daba20d
|
||||||
common/survey_not_found: 0485ea98d13a414eeefc8f1118b9c293
|
|
||||||
common/survey_paused: c770d174d6b57e8425a54906a09c8b39
|
common/survey_paused: c770d174d6b57e8425a54906a09c8b39
|
||||||
common/survey_type: 417fcfecf8eaedefc4f11172426811f9
|
common/survey_type: 417fcfecf8eaedefc4f11172426811f9
|
||||||
common/surveys: 33f68ad4111b32a6361beb9d5c184533
|
common/surveys: 33f68ad4111b32a6361beb9d5c184533
|
||||||
@@ -400,7 +407,6 @@ checksums:
|
|||||||
common/team_name: 549d949de4b9adad4afd6427a60a329e
|
common/team_name: 549d949de4b9adad4afd6427a60a329e
|
||||||
common/team_role: 66db395781aef64ef3791417b3b67c0b
|
common/team_role: 66db395781aef64ef3791417b3b67c0b
|
||||||
common/teams: b63448c05270497973ac4407047dae02
|
common/teams: b63448c05270497973ac4407047dae02
|
||||||
common/teams_not_found: 02f333a64a83c1c014d8900ec9666345
|
|
||||||
common/text: 4ddccc1974775ed7357f9beaf9361cec
|
common/text: 4ddccc1974775ed7357f9beaf9361cec
|
||||||
common/time: b504a03d52e8001bfdc5cb6205364f42
|
common/time: b504a03d52e8001bfdc5cb6205364f42
|
||||||
common/time_to_finish: c8f6abdb886bee3619bb50b08fada5fa
|
common/time_to_finish: c8f6abdb886bee3619bb50b08fada5fa
|
||||||
@@ -424,7 +430,6 @@ checksums:
|
|||||||
common/url: ca97457614226960d41dd18c3c29c86b
|
common/url: ca97457614226960d41dd18c3c29c86b
|
||||||
common/user: 61073457a5c3901084b557d065f876be
|
common/user: 61073457a5c3901084b557d065f876be
|
||||||
common/user_id: 37f5ba37f71cb50607af32a6a203b1d4
|
common/user_id: 37f5ba37f71cb50607af32a6a203b1d4
|
||||||
common/user_not_found: 5903581136ac6c1c1351a482a6d8fdf7
|
|
||||||
common/variable: c13db5775ba9791b1522cc55c9c7acce
|
common/variable: c13db5775ba9791b1522cc55c9c7acce
|
||||||
common/variable_ids: 44bf93b70703b7699fa9f21bc6c8eed4
|
common/variable_ids: 44bf93b70703b7699fa9f21bc6c8eed4
|
||||||
common/variables: ffd3eec5497af36d7b4e4185bad1313a
|
common/variables: ffd3eec5497af36d7b4e4185bad1313a
|
||||||
@@ -440,14 +445,13 @@ checksums:
|
|||||||
common/weeks: 545de30df4f44d3f6d1d344af6a10815
|
common/weeks: 545de30df4f44d3f6d1d344af6a10815
|
||||||
common/welcome_card: 76081ebd5b2e35da9b0f080323704ae7
|
common/welcome_card: 76081ebd5b2e35da9b0f080323704ae7
|
||||||
common/workflows: b0c9c8615a9ba7d9cb73e767290a7f72
|
common/workflows: b0c9c8615a9ba7d9cb73e767290a7f72
|
||||||
|
common/workspace: b63ef0e99ee6f7fef6cbe4971ca6cf0f
|
||||||
common/workspace_configuration: d0a5812d6a97d7724d565b1017c34387
|
common/workspace_configuration: d0a5812d6a97d7724d565b1017c34387
|
||||||
common/workspace_created_successfully: bf401ae83da954f1db48724e2a8e40f1
|
common/workspace_created_successfully: bf401ae83da954f1db48724e2a8e40f1
|
||||||
common/workspace_creation_description: aea2f480ba0c54c5cabac72c9c900ddf
|
common/workspace_creation_description: aea2f480ba0c54c5cabac72c9c900ddf
|
||||||
common/workspace_id: bafef925e1b57b52a69844fdf47aac3c
|
common/workspace_id: bafef925e1b57b52a69844fdf47aac3c
|
||||||
common/workspace_name: 14c04a902a874ab5ddbe9cf369ef0414
|
common/workspace_name: 14c04a902a874ab5ddbe9cf369ef0414
|
||||||
common/workspace_name_placeholder: 8a9e30ab01666af13c44a73b82c37ec1
|
common/workspace_name_placeholder: 8a9e30ab01666af13c44a73b82c37ec1
|
||||||
common/workspace_not_found: 038fb0aaf3570610f4377b9eaed13752
|
|
||||||
common/workspace_permission_not_found: e94bdff8af51175c5767714f82bb4833
|
|
||||||
common/workspaces: 8ba082a84aa35cf851af1cf874b853e2
|
common/workspaces: 8ba082a84aa35cf851af1cf874b853e2
|
||||||
common/years: eb4f5fdd2b320bf13e200fd6a6c1abff
|
common/years: eb4f5fdd2b320bf13e200fd6a6c1abff
|
||||||
common/you: db2a4a796b70cc1430d1b21f6ffb6dcb
|
common/you: db2a4a796b70cc1430d1b21f6ffb6dcb
|
||||||
@@ -623,7 +627,6 @@ checksums:
|
|||||||
environments/contacts/attributes_msg_new_attribute_created: 5cba6158c4305c05104814ec1479267c
|
environments/contacts/attributes_msg_new_attribute_created: 5cba6158c4305c05104814ec1479267c
|
||||||
environments/contacts/attributes_msg_userid_already_exists: 9c695538befc152806c460f52a73821a
|
environments/contacts/attributes_msg_userid_already_exists: 9c695538befc152806c460f52a73821a
|
||||||
environments/contacts/contact_deleted_successfully: c5b64a42a50e055f9e27ec49e20e03fa
|
environments/contacts/contact_deleted_successfully: c5b64a42a50e055f9e27ec49e20e03fa
|
||||||
environments/contacts/contact_not_found: 045396f0b13fafd43612a286263737c0
|
|
||||||
environments/contacts/contacts_table_refresh: 6a959475991dd4ab28ad881bae569a09
|
environments/contacts/contacts_table_refresh: 6a959475991dd4ab28ad881bae569a09
|
||||||
environments/contacts/contacts_table_refresh_success: 40951396e88e5c8fdafa0b3bb4fadca8
|
environments/contacts/contacts_table_refresh_success: 40951396e88e5c8fdafa0b3bb4fadca8
|
||||||
environments/contacts/create_attribute: 87320615901f95b4f35ee83c290a3a6c
|
environments/contacts/create_attribute: 87320615901f95b4f35ee83c290a3a6c
|
||||||
@@ -803,9 +806,16 @@ checksums:
|
|||||||
environments/integrations/webhooks/created_by_third_party: b40197eabbbce500b80b44268b8b1ee9
|
environments/integrations/webhooks/created_by_third_party: b40197eabbbce500b80b44268b8b1ee9
|
||||||
environments/integrations/webhooks/discord_webhook_not_supported: 23432534f908b2ba63a517fb1f9bbe0e
|
environments/integrations/webhooks/discord_webhook_not_supported: 23432534f908b2ba63a517fb1f9bbe0e
|
||||||
environments/integrations/webhooks/empty_webhook_message: 4c4d8709576a38cb8eb59866331d2405
|
environments/integrations/webhooks/empty_webhook_message: 4c4d8709576a38cb8eb59866331d2405
|
||||||
|
environments/integrations/webhooks/endpoint_bad_gateway_error: 48ab17e9a77030b289ec22f497f50b63
|
||||||
|
environments/integrations/webhooks/endpoint_gateway_timeout_error: 5da45e2f6933927d1f8b0aaa9566e6a6
|
||||||
|
environments/integrations/webhooks/endpoint_internal_server_error: 6773fc34349febf95475cde88d8ee072
|
||||||
|
environments/integrations/webhooks/endpoint_method_not_allowed_error: 9963b503311393f4d7bffae9df46d422
|
||||||
|
environments/integrations/webhooks/endpoint_not_found_error: 607b75b7b7aa92ca81fe44e466f7c318
|
||||||
environments/integrations/webhooks/endpoint_pinged: 3b1fce00e61d4b9d2bdca390649c58b6
|
environments/integrations/webhooks/endpoint_pinged: 3b1fce00e61d4b9d2bdca390649c58b6
|
||||||
environments/integrations/webhooks/endpoint_pinged_error: 96c312fe8214757c4a934cdfbe177027
|
environments/integrations/webhooks/endpoint_pinged_error: 96c312fe8214757c4a934cdfbe177027
|
||||||
|
environments/integrations/webhooks/endpoint_service_unavailable_error: f9d4874c322f2963f5afaede354c9416
|
||||||
environments/integrations/webhooks/learn_to_verify: 25b2a035e2109170b28f4e16db76ad39
|
environments/integrations/webhooks/learn_to_verify: 25b2a035e2109170b28f4e16db76ad39
|
||||||
|
environments/integrations/webhooks/no_triggers: 6b68cddfc45b3f7e20644a24a1bbea69
|
||||||
environments/integrations/webhooks/please_check_console: 7b1787e82a0d762df02c011ebb1650ea
|
environments/integrations/webhooks/please_check_console: 7b1787e82a0d762df02c011ebb1650ea
|
||||||
environments/integrations/webhooks/please_enter_a_url: c24c74d0ce7ed3a6b858aadbc82108fe
|
environments/integrations/webhooks/please_enter_a_url: c24c74d0ce7ed3a6b858aadbc82108fe
|
||||||
environments/integrations/webhooks/response_created: 8c43b1b6d748f6096f6f8d9232a3c469
|
environments/integrations/webhooks/response_created: 8c43b1b6d748f6096f6f8d9232a3c469
|
||||||
@@ -1012,6 +1022,25 @@ checksums:
|
|||||||
environments/settings/enterprise/enterprise_features: 3271476140733924b2a2477c4fdf3d12
|
environments/settings/enterprise/enterprise_features: 3271476140733924b2a2477c4fdf3d12
|
||||||
environments/settings/enterprise/get_an_enterprise_license_to_get_access_to_all_features: afd3c00f19097e88ed051800979eea44
|
environments/settings/enterprise/get_an_enterprise_license_to_get_access_to_all_features: afd3c00f19097e88ed051800979eea44
|
||||||
environments/settings/enterprise/keep_full_control_over_your_data_privacy_and_security: 43aa041cc3e2b2fdd35d2d34659a6b7a
|
environments/settings/enterprise/keep_full_control_over_your_data_privacy_and_security: 43aa041cc3e2b2fdd35d2d34659a6b7a
|
||||||
|
environments/settings/enterprise/license_feature_access_control: bdc5ce7e88ad724d4abd3e8a07a9de5d
|
||||||
|
environments/settings/enterprise/license_feature_audit_logs: e93f59c176cfc8460d2bd56551ed78b8
|
||||||
|
environments/settings/enterprise/license_feature_contacts: fd76522bc82324ac914e124cdf9935b0
|
||||||
|
environments/settings/enterprise/license_feature_projects: 8ba082a84aa35cf851af1cf874b853e2
|
||||||
|
environments/settings/enterprise/license_feature_quotas: e6afead11b5b8ae627885ce2b84a548f
|
||||||
|
environments/settings/enterprise/license_feature_remove_branding: a5c71d43cd3ed25e6e48bca64e8ffc9f
|
||||||
|
environments/settings/enterprise/license_feature_saml: 86b76024524fc585b2c3950126ef6f62
|
||||||
|
environments/settings/enterprise/license_feature_spam_protection: e1fb0dd0723044bf040b92d8fc58015d
|
||||||
|
environments/settings/enterprise/license_feature_sso: 8c029b7dd2cb3aa1393d2814aba6cd7b
|
||||||
|
environments/settings/enterprise/license_feature_two_factor_auth: bc68ddd9c3c82225ef641f097e0940db
|
||||||
|
environments/settings/enterprise/license_feature_whitelabel: 81e9ec1d4230419f4230e6f5a318497c
|
||||||
|
environments/settings/enterprise/license_features_table_access: 550606d4a12bdf108c1b12b925ca1b3a
|
||||||
|
environments/settings/enterprise/license_features_table_description: d6260830d0703f5a2c9ed59c9da462e3
|
||||||
|
environments/settings/enterprise/license_features_table_disabled: 0889a3dfd914a7ef638611796b17bf72
|
||||||
|
environments/settings/enterprise/license_features_table_enabled: 20236664b7e62df0e767921b4450205f
|
||||||
|
environments/settings/enterprise/license_features_table_feature: 58f5f3f37862b6312a2f20ec1a1fd0e8
|
||||||
|
environments/settings/enterprise/license_features_table_title: 82d1d7b30d876cf4312f78140a90e394
|
||||||
|
environments/settings/enterprise/license_features_table_unlimited: e1a92523172cd1bdde5550689840e42d
|
||||||
|
environments/settings/enterprise/license_features_table_value: 34b0eaa85808b15cbc4be94c64d0146b
|
||||||
environments/settings/enterprise/license_instance_mismatch_description: 00f47e33ff54fca52ce9b125cd77fda5
|
environments/settings/enterprise/license_instance_mismatch_description: 00f47e33ff54fca52ce9b125cd77fda5
|
||||||
environments/settings/enterprise/license_invalid_description: b500c22ab17893fdf9532d2bd94aa526
|
environments/settings/enterprise/license_invalid_description: b500c22ab17893fdf9532d2bd94aa526
|
||||||
environments/settings/enterprise/license_status: f6f85c59074ca2455321bd5288d94be8
|
environments/settings/enterprise/license_status: f6f85c59074ca2455321bd5288d94be8
|
||||||
@@ -1321,7 +1350,6 @@ checksums:
|
|||||||
environments/surveys/edit/custom_hostname: bc2b1c8de3f9b8ef145b45aeba6ab429
|
environments/surveys/edit/custom_hostname: bc2b1c8de3f9b8ef145b45aeba6ab429
|
||||||
environments/surveys/edit/customize_survey_logo: 7f7e26026c88a727228f2d7a00d914e2
|
environments/surveys/edit/customize_survey_logo: 7f7e26026c88a727228f2d7a00d914e2
|
||||||
environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f
|
environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f
|
||||||
environments/surveys/edit/date_format: e95dfc41ac944874868487457ddc057a
|
|
||||||
environments/surveys/edit/days_before_showing_this_survey_again: 9ee757e5c3a07844b12ceb406dc65b04
|
environments/surveys/edit/days_before_showing_this_survey_again: 9ee757e5c3a07844b12ceb406dc65b04
|
||||||
environments/surveys/edit/delete_anyways: cc8683ab625280eefc9776bd381dc2e8
|
environments/surveys/edit/delete_anyways: cc8683ab625280eefc9776bd381dc2e8
|
||||||
environments/surveys/edit/delete_block: c00617cb0724557e486304276063807a
|
environments/surveys/edit/delete_block: c00617cb0724557e486304276063807a
|
||||||
@@ -1359,6 +1387,7 @@ checksums:
|
|||||||
environments/surveys/edit/error_saving_changes: b75aa9e4e42e1d43c8f9c33c2b7dc9a7
|
environments/surveys/edit/error_saving_changes: b75aa9e4e42e1d43c8f9c33c2b7dc9a7
|
||||||
environments/surveys/edit/even_after_they_submitted_a_response_e_g_feedback_box: 7b99f30397dcde76f65e1ab64bdbd113
|
environments/surveys/edit/even_after_they_submitted_a_response_e_g_feedback_box: 7b99f30397dcde76f65e1ab64bdbd113
|
||||||
environments/surveys/edit/everyone: 2112aa71b568773e8e8a792c63f4d413
|
environments/surveys/edit/everyone: 2112aa71b568773e8e8a792c63f4d413
|
||||||
|
environments/surveys/edit/expand_preview: 6b694829e05432b9b54e7da53bc5be2f
|
||||||
environments/surveys/edit/external_urls_paywall_tooltip: 427f29bbbec18ebf8b3ea8d0253ddd66
|
environments/surveys/edit/external_urls_paywall_tooltip: 427f29bbbec18ebf8b3ea8d0253ddd66
|
||||||
environments/surveys/edit/fallback_missing: 43dbedbe1a178d455e5f80783a7b6722
|
environments/surveys/edit/fallback_missing: 43dbedbe1a178d455e5f80783a7b6722
|
||||||
environments/surveys/edit/fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first: ad4afe2980e1dfeffb20aa78eb892350
|
environments/surveys/edit/fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first: ad4afe2980e1dfeffb20aa78eb892350
|
||||||
@@ -1582,6 +1611,8 @@ checksums:
|
|||||||
environments/surveys/edit/response_limit_needs_to_exceed_number_of_received_responses: 9a9c223c0918ded716ddfaa84fbaa8d9
|
environments/surveys/edit/response_limit_needs_to_exceed_number_of_received_responses: 9a9c223c0918ded716ddfaa84fbaa8d9
|
||||||
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
|
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
|
||||||
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
|
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
|
||||||
|
environments/surveys/edit/reverse_order_occasionally: 170fd50de940f382fa2e605228e4e088
|
||||||
|
environments/surveys/edit/reverse_order_occasionally_except_last: 1c833001b940f1419dd7534b199a0b4a
|
||||||
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
|
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
|
||||||
environments/surveys/edit/roundness_description: 03940a6871ae43efa4810cba7cadb74b
|
environments/surveys/edit/roundness_description: 03940a6871ae43efa4810cba7cadb74b
|
||||||
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
|
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
|
||||||
@@ -1610,6 +1641,7 @@ checksums:
|
|||||||
environments/surveys/edit/show_survey_maximum_of: 721ed61b01a9fc8ce4becb72823bb72e
|
environments/surveys/edit/show_survey_maximum_of: 721ed61b01a9fc8ce4becb72823bb72e
|
||||||
environments/surveys/edit/show_survey_to_users: d5e90fd17babfea978fce826e9df89b0
|
environments/surveys/edit/show_survey_to_users: d5e90fd17babfea978fce826e9df89b0
|
||||||
environments/surveys/edit/show_to_x_percentage_of_targeted_users: b745169011fa7e8ca475baa5500c5197
|
environments/surveys/edit/show_to_x_percentage_of_targeted_users: b745169011fa7e8ca475baa5500c5197
|
||||||
|
environments/surveys/edit/shrink_preview: 42567389520b226f211f94f052197ad8
|
||||||
environments/surveys/edit/simple: 65575bd903091299bc4a94b7517a6288
|
environments/surveys/edit/simple: 65575bd903091299bc4a94b7517a6288
|
||||||
environments/surveys/edit/six_points: c6c09b3f07171dc388cb5a610ea79af7
|
environments/surveys/edit/six_points: c6c09b3f07171dc388cb5a610ea79af7
|
||||||
environments/surveys/edit/smiley: e68e3b28fc3c04255e236c6a0feb662b
|
environments/surveys/edit/smiley: e68e3b28fc3c04255e236c6a0feb662b
|
||||||
@@ -1625,10 +1657,12 @@ checksums:
|
|||||||
environments/surveys/edit/styling_set_to_theme_styles: f2c108bf422372b00cf7c87f1b042f69
|
environments/surveys/edit/styling_set_to_theme_styles: f2c108bf422372b00cf7c87f1b042f69
|
||||||
environments/surveys/edit/subheading: c0f6f57155692fd8006381518ce4fef0
|
environments/surveys/edit/subheading: c0f6f57155692fd8006381518ce4fef0
|
||||||
environments/surveys/edit/subtract: 2d83b8b9ef35110f2583ddc155b6c486
|
environments/surveys/edit/subtract: 2d83b8b9ef35110f2583ddc155b6c486
|
||||||
|
environments/surveys/edit/survey_closed_message_heading_required: f7c48e324c4a5c335ec68eaa27b2d67e
|
||||||
environments/surveys/edit/survey_completed_heading: dae5ac4a02a886dc9d9fc40927091919
|
environments/surveys/edit/survey_completed_heading: dae5ac4a02a886dc9d9fc40927091919
|
||||||
environments/surveys/edit/survey_completed_subheading: db537c356c3ab6564d24de0d11a0fee2
|
environments/surveys/edit/survey_completed_subheading: db537c356c3ab6564d24de0d11a0fee2
|
||||||
environments/surveys/edit/survey_display_settings: 8ed19e6a8e1376f7a1ba037d82c4ae11
|
environments/surveys/edit/survey_display_settings: 8ed19e6a8e1376f7a1ba037d82c4ae11
|
||||||
environments/surveys/edit/survey_placement: 083c10f257337f9648bf9d435b18ec2c
|
environments/surveys/edit/survey_placement: 083c10f257337f9648bf9d435b18ec2c
|
||||||
|
environments/surveys/edit/survey_preview: 33644451073149383d3ace08be930739
|
||||||
environments/surveys/edit/survey_styling: 7f96d6563e934e65687b74374a33b1dc
|
environments/surveys/edit/survey_styling: 7f96d6563e934e65687b74374a33b1dc
|
||||||
environments/surveys/edit/survey_trigger: f0c7014a684ca566698b87074fad5579
|
environments/surveys/edit/survey_trigger: f0c7014a684ca566698b87074fad5579
|
||||||
environments/surveys/edit/switch_multi_language_on_to_get_started: cca0ef91ee49095da30cd1e3f26c406f
|
environments/surveys/edit/switch_multi_language_on_to_get_started: cca0ef91ee49095da30cd1e3f26c406f
|
||||||
@@ -2897,7 +2931,7 @@ checksums:
|
|||||||
templates/preview_survey_question_2_choice_2_label: 1af148222f327f28cf0db6513de5989e
|
templates/preview_survey_question_2_choice_2_label: 1af148222f327f28cf0db6513de5989e
|
||||||
templates/preview_survey_question_2_headline: 5cfb173d156555227fbc2c97ad921e72
|
templates/preview_survey_question_2_headline: 5cfb173d156555227fbc2c97ad921e72
|
||||||
templates/preview_survey_question_2_subheader: 2e652d8acd68d072e5a0ae686c4011c0
|
templates/preview_survey_question_2_subheader: 2e652d8acd68d072e5a0ae686c4011c0
|
||||||
templates/preview_survey_question_open_text_headline: a9509a47e0456ae98ec3ddac3d6fad2c
|
templates/preview_survey_question_open_text_headline: 573f1b04b79f672ad42ba5e54320a940
|
||||||
templates/preview_survey_question_open_text_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
templates/preview_survey_question_open_text_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
||||||
templates/preview_survey_question_open_text_subheader: 3c7bf09f3f17b02bc2fbbbdb347a5830
|
templates/preview_survey_question_open_text_subheader: 3c7bf09f3f17b02bc2fbbbdb347a5830
|
||||||
templates/preview_survey_welcome_card_headline: 8778dc41547a2778d0f9482da989fc00
|
templates/preview_survey_welcome_card_headline: 8778dc41547a2778d0f9482da989fc00
|
||||||
@@ -3150,7 +3184,7 @@ checksums:
|
|||||||
templates/usability_score_name: 5cbf1172d24dfcb17d979dff6dfdf7e2
|
templates/usability_score_name: 5cbf1172d24dfcb17d979dff6dfdf7e2
|
||||||
workflows/coming_soon_description: 1e0621d287924d84fb539afab7372b23
|
workflows/coming_soon_description: 1e0621d287924d84fb539afab7372b23
|
||||||
workflows/coming_soon_title: d79be80559c70c828cf20811d2ed5039
|
workflows/coming_soon_title: d79be80559c70c828cf20811d2ed5039
|
||||||
workflows/follow_up_label: 8cafe669370271035aeac8e8cab0f123
|
workflows/follow_up_label: ead918852c5840636a14baabfe94821e
|
||||||
workflows/follow_up_placeholder: f680918bec28192282e229c3d4b5e80a
|
workflows/follow_up_placeholder: f680918bec28192282e229c3d4b5e80a
|
||||||
workflows/generate_button: b194b6172a49af8374a19dd2cf39cfdc
|
workflows/generate_button: b194b6172a49af8374a19dd2cf39cfdc
|
||||||
workflows/heading: a98a6b14d3e955f38cc16386df9a4111
|
workflows/heading: a98a6b14d3e955f38cc16386df9a4111
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import { type Instrumentation } from "next";
|
||||||
|
import { isExpectedError } from "@formbricks/types/errors";
|
||||||
import { IS_PRODUCTION, PROMETHEUS_ENABLED, SENTRY_DSN } from "@/lib/constants";
|
import { IS_PRODUCTION, PROMETHEUS_ENABLED, SENTRY_DSN } from "@/lib/constants";
|
||||||
|
|
||||||
export const onRequestError = Sentry.captureRequestError;
|
export const onRequestError: Instrumentation.onRequestError = (...args) => {
|
||||||
|
const [error] = args;
|
||||||
|
|
||||||
|
// Skip expected business-logic errors (AuthorizationError, ResourceNotFoundError, etc.)
|
||||||
|
// These are handled gracefully in the UI and don't need server-side Sentry reporting
|
||||||
|
if (error instanceof Error && isExpectedError(error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.captureRequestError(...args);
|
||||||
|
};
|
||||||
|
|
||||||
export const register = async () => {
|
export const register = async () => {
|
||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { upsertAccount } from "./service";
|
||||||
|
|
||||||
|
const { mockUpsert } = vi.hoisted(() => ({
|
||||||
|
mockUpsert: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => ({
|
||||||
|
prisma: {
|
||||||
|
account: {
|
||||||
|
upsert: mockUpsert,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("account service", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("upsertAccount keeps user ownership immutable on update", async () => {
|
||||||
|
const accountData = {
|
||||||
|
userId: "user-1",
|
||||||
|
type: "oauth",
|
||||||
|
provider: "google",
|
||||||
|
providerAccountId: "provider-1",
|
||||||
|
access_token: "access-token",
|
||||||
|
refresh_token: "refresh-token",
|
||||||
|
expires_at: 123,
|
||||||
|
scope: "openid email",
|
||||||
|
token_type: "Bearer",
|
||||||
|
id_token: "id-token",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockUpsert.mockResolvedValue({
|
||||||
|
id: "account-1",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
...accountData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await upsertAccount(accountData);
|
||||||
|
|
||||||
|
expect(mockUpsert).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
provider_providerAccountId: {
|
||||||
|
provider: "google",
|
||||||
|
providerAccountId: "provider-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: accountData,
|
||||||
|
update: {
|
||||||
|
access_token: "access-token",
|
||||||
|
refresh_token: "refresh-token",
|
||||||
|
expires_at: 123,
|
||||||
|
scope: "openid email",
|
||||||
|
token_type: "Bearer",
|
||||||
|
id_token: "id-token",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("upsertAccount wraps Prisma known request errors", async () => {
|
||||||
|
const prismaError = Object.assign(Object.create(Prisma.PrismaClientKnownRequestError.prototype), {
|
||||||
|
message: "duplicate account",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockUpsert.mockRejectedValue(prismaError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
upsertAccount({
|
||||||
|
userId: "user-1",
|
||||||
|
type: "oauth",
|
||||||
|
provider: "google",
|
||||||
|
providerAccountId: "provider-1",
|
||||||
|
})
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
name: "DatabaseError",
|
||||||
|
message: "duplicate account",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("upsertAccount rethrows non-Prisma errors", async () => {
|
||||||
|
const error = new Error("unexpected failure");
|
||||||
|
mockUpsert.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
upsertAccount({
|
||||||
|
userId: "user-1",
|
||||||
|
type: "oauth",
|
||||||
|
provider: "google",
|
||||||
|
providerAccountId: "provider-1",
|
||||||
|
})
|
||||||
|
).rejects.toThrow("unexpected failure");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,3 +20,36 @@ export const createAccount = async (accountData: TAccountInput): Promise<TAccoun
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const upsertAccount = async (accountData: TAccountInput): Promise<TAccount> => {
|
||||||
|
const [validatedAccountData] = validateInputs([accountData, ZAccountInput]);
|
||||||
|
const updateAccountData: Omit<TAccountInput, "userId" | "type" | "provider" | "providerAccountId"> = {
|
||||||
|
access_token: validatedAccountData.access_token,
|
||||||
|
refresh_token: validatedAccountData.refresh_token,
|
||||||
|
expires_at: validatedAccountData.expires_at,
|
||||||
|
scope: validatedAccountData.scope,
|
||||||
|
token_type: validatedAccountData.token_type,
|
||||||
|
id_token: validatedAccountData.id_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const account = await prisma.account.upsert({
|
||||||
|
where: {
|
||||||
|
provider_providerAccountId: {
|
||||||
|
provider: validatedAccountData.provider,
|
||||||
|
providerAccountId: validatedAccountData.providerAccountId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: validatedAccountData,
|
||||||
|
update: updateAccountData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return account;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
throw new DatabaseError(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const TERMS_URL = env.TERMS_URL;
|
|||||||
export const IMPRINT_URL = env.IMPRINT_URL;
|
export const IMPRINT_URL = env.IMPRINT_URL;
|
||||||
export const IMPRINT_ADDRESS = env.IMPRINT_ADDRESS;
|
export const IMPRINT_ADDRESS = env.IMPRINT_ADDRESS;
|
||||||
|
|
||||||
|
export const DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS = env.DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS === "1";
|
||||||
export const PASSWORD_RESET_DISABLED = env.PASSWORD_RESET_DISABLED === "1";
|
export const PASSWORD_RESET_DISABLED = env.PASSWORD_RESET_DISABLED === "1";
|
||||||
export const EMAIL_VERIFICATION_DISABLED = env.EMAIL_VERIFICATION_DISABLED === "1";
|
export const EMAIL_VERIFICATION_DISABLED = env.EMAIL_VERIFICATION_DISABLED === "1";
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const env = createEnv({
|
|||||||
BREVO_API_KEY: z.string().optional(),
|
BREVO_API_KEY: z.string().optional(),
|
||||||
BREVO_LIST_ID: z.string().optional(),
|
BREVO_LIST_ID: z.string().optional(),
|
||||||
DATABASE_URL: z.url(),
|
DATABASE_URL: z.url(),
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: z.enum(["1", "0"]).optional(),
|
||||||
DEBUG: z.enum(["1", "0"]).optional(),
|
DEBUG: z.enum(["1", "0"]).optional(),
|
||||||
AUTH_DEFAULT_TEAM_ID: z.string().optional(),
|
AUTH_DEFAULT_TEAM_ID: z.string().optional(),
|
||||||
AUTH_SKIP_INVITE_FOR_SSO: z.enum(["1", "0"]).optional(),
|
AUTH_SKIP_INVITE_FOR_SSO: z.enum(["1", "0"]).optional(),
|
||||||
@@ -141,6 +142,7 @@ export const env = createEnv({
|
|||||||
BREVO_LIST_ID: process.env.BREVO_LIST_ID,
|
BREVO_LIST_ID: process.env.BREVO_LIST_ID,
|
||||||
CRON_SECRET: process.env.CRON_SECRET,
|
CRON_SECRET: process.env.CRON_SECRET,
|
||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: process.env.DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS,
|
||||||
DEBUG: process.env.DEBUG,
|
DEBUG: process.env.DEBUG,
|
||||||
AUTH_DEFAULT_TEAM_ID: process.env.AUTH_SSO_DEFAULT_TEAM_ID,
|
AUTH_DEFAULT_TEAM_ID: process.env.AUTH_SSO_DEFAULT_TEAM_ID,
|
||||||
AUTH_SKIP_INVITE_FOR_SSO: process.env.AUTH_SKIP_INVITE_FOR_SSO,
|
AUTH_SKIP_INVITE_FOR_SSO: process.env.AUTH_SKIP_INVITE_FOR_SSO,
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ export const extractLanguageIds = (languages: TLanguage[]): string[] => {
|
|||||||
|
|
||||||
export const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => {
|
export const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => {
|
||||||
if (!surveyLanguages?.length || !languageCode) return "default";
|
if (!surveyLanguages?.length || !languageCode) return "default";
|
||||||
const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode);
|
const language = surveyLanguages.find(
|
||||||
|
(surveyLanguage) => surveyLanguage.language.code.toLowerCase() === languageCode.toLowerCase()
|
||||||
|
);
|
||||||
return language?.default ? "default" : language?.language.code || "default";
|
return language?.default ? "default" : language?.language.code || "default";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import "server-only";
|
import "server-only";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getOrganizationByEnvironmentId } from "../../organization/service";
|
import { getOrganizationByEnvironmentId } from "../../organization/service";
|
||||||
import { getMembershipByUserIdOrganizationId } from "../service";
|
import { getMembershipByUserIdOrganizationId } from "../service";
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export const getMembershipByUserIdOrganizationIdAction = async (environmentId: s
|
|||||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error("Organization not found");
|
throw new ResourceNotFoundError("Organization", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserMembership = await getMembershipRole(userId, organization.id);
|
const currentUserMembership = await getMembershipRole(userId, organization.id);
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export const getResponseDownloadFile = async (
|
|||||||
|
|
||||||
const organizationId = await getOrganizationIdFromEnvironmentId(survey.environmentId);
|
const organizationId = await getOrganizationIdFromEnvironmentId(survey.environmentId);
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
throw new Error("Organization ID not found");
|
throw new ResourceNotFoundError("Organization", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationBilling = await getOrganizationBilling(organizationId);
|
const organizationBilling = await getOrganizationBilling(organizationId);
|
||||||
|
|||||||
+28
-55
@@ -1,62 +1,13 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import {
|
import {
|
||||||
convertDateString,
|
|
||||||
convertDateTimeString,
|
|
||||||
convertDateTimeStringShort,
|
|
||||||
convertDatesInObject,
|
convertDatesInObject,
|
||||||
convertTimeString,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
getTodaysDateFormatted,
|
|
||||||
getTodaysDateTimeFormatted,
|
getTodaysDateTimeFormatted,
|
||||||
timeSince,
|
timeSince,
|
||||||
timeSinceDate,
|
timeSinceDate,
|
||||||
} from "./time";
|
} from "./time";
|
||||||
|
|
||||||
describe("Time Utilities", () => {
|
describe("Time Utilities", () => {
|
||||||
describe("convertDateString", () => {
|
|
||||||
test("should format date string correctly", () => {
|
|
||||||
expect(convertDateString("2024-03-20:12:30:00")).toBe("Mar 20, 2024");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return empty string for empty input", () => {
|
|
||||||
expect(convertDateString("")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return null for null input", () => {
|
|
||||||
expect(convertDateString(null as any)).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle invalid date strings", () => {
|
|
||||||
expect(convertDateString("not-a-date")).toBe("Invalid Date");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("convertDateTimeString", () => {
|
|
||||||
test("should format date and time string correctly", () => {
|
|
||||||
expect(convertDateTimeString("2024-03-20T15:30:00")).toBe("Wednesday, March 20, 2024 at 3:30 PM");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return empty string for empty input", () => {
|
|
||||||
expect(convertDateTimeString("")).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("convertDateTimeStringShort", () => {
|
|
||||||
test("should format date and time string in short format", () => {
|
|
||||||
expect(convertDateTimeStringShort("2024-03-20T15:30:00")).toBe("March 20, 2024 at 3:30 PM");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return empty string for empty input", () => {
|
|
||||||
expect(convertDateTimeStringShort("")).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("convertTimeString", () => {
|
|
||||||
test("should format time string correctly", () => {
|
|
||||||
expect(convertTimeString("2024-03-20T15:30:45")).toBe("3:30:45 PM");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("timeSince", () => {
|
describe("timeSince", () => {
|
||||||
test("should format time since in English", () => {
|
test("should format time since in English", () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -75,6 +26,18 @@ describe("Time Utilities", () => {
|
|||||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
expect(timeSince(oneHourAgo.toISOString(), "sv-SE")).toBe("ungefär en timme sedan");
|
expect(timeSince(oneHourAgo.toISOString(), "sv-SE")).toBe("ungefär en timme sedan");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should format time since in Brazilian Portuguese", () => {
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
|
expect(timeSince(oneHourAgo.toISOString(), "pt-BR")).toBe("há cerca de 1 hora");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should format time since in European Portuguese", () => {
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
|
expect(timeSince(oneHourAgo.toISOString(), "pt-PT")).toBe("há aproximadamente 1 hora");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("timeSinceDate", () => {
|
describe("timeSinceDate", () => {
|
||||||
@@ -83,6 +46,12 @@ describe("Time Utilities", () => {
|
|||||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
expect(timeSinceDate(oneHourAgo)).toBe("about 1 hour ago");
|
expect(timeSinceDate(oneHourAgo)).toBe("about 1 hour ago");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should format time since from Date object in the provided locale", () => {
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||||
|
expect(timeSinceDate(oneHourAgo, "de-DE")).toBe("vor etwa 1 Stunde");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("formatDate", () => {
|
describe("formatDate", () => {
|
||||||
@@ -90,13 +59,17 @@ describe("Time Utilities", () => {
|
|||||||
const date = new Date(2024, 2, 20); // March is month 2 (0-based)
|
const date = new Date(2024, 2, 20); // March is month 2 (0-based)
|
||||||
expect(formatDate(date)).toBe("March 20, 2024");
|
expect(formatDate(date)).toBe("March 20, 2024");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("getTodaysDateFormatted", () => {
|
test("should format date with the provided locale", () => {
|
||||||
test("should format today's date with specified separator", () => {
|
const date = new Date(2024, 2, 20);
|
||||||
const today = new Date();
|
|
||||||
const expected = today.toISOString().split("T")[0].split("-").join(".");
|
expect(formatDate(date, "de-DE")).toBe(
|
||||||
expect(getTodaysDateFormatted(".")).toBe(expected);
|
new Intl.DateTimeFormat("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(date)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+27
-120
@@ -1,120 +1,33 @@
|
|||||||
import { formatDistance, intlFormat } from "date-fns";
|
import { type Locale, formatDistance } from "date-fns";
|
||||||
import { de, enUS, es, fr, hu, ja, nl, pt, ptBR, ro, ru, sv, zhCN, zhTW } from "date-fns/locale";
|
import { de, enUS, es, fr, hu, ja, nl, pt, ptBR, ro, ru, sv, zhCN, zhTW } from "date-fns/locale";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
import { formatDateForDisplay } from "./utils/datetime";
|
||||||
|
|
||||||
export const convertDateString = (dateString: string | null) => {
|
const DEFAULT_LOCALE: TUserLocale = "en-US";
|
||||||
if (dateString === null) return null;
|
const TIME_SINCE_LOCALES: Record<TUserLocale, Locale> = {
|
||||||
if (!dateString) {
|
"de-DE": de,
|
||||||
return dateString;
|
"en-US": enUS,
|
||||||
}
|
"es-ES": es,
|
||||||
|
"fr-FR": fr,
|
||||||
const date = new Date(dateString);
|
"hu-HU": hu,
|
||||||
if (isNaN(date.getTime())) {
|
"ja-JP": ja,
|
||||||
return "Invalid Date";
|
"nl-NL": nl,
|
||||||
}
|
"pt-BR": ptBR,
|
||||||
return intlFormat(
|
"pt-PT": pt,
|
||||||
date,
|
"ro-RO": ro,
|
||||||
{
|
"ru-RU": ru,
|
||||||
year: "numeric",
|
"sv-SE": sv,
|
||||||
month: "short",
|
"zh-Hans-CN": zhCN,
|
||||||
day: "numeric",
|
"zh-Hant-TW": zhTW,
|
||||||
},
|
|
||||||
{
|
|
||||||
locale: "en",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertDateTimeString = (dateString: string) => {
|
const isUserLocale = (locale: string): locale is TUserLocale => Object.hasOwn(TIME_SINCE_LOCALES, locale);
|
||||||
if (!dateString) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return intlFormat(
|
|
||||||
date,
|
|
||||||
{
|
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
locale: "en",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertDateTimeStringShort = (dateString: string) => {
|
/** Maps locale strings to date-fns locales and falls back to English for unsupported inputs. */
|
||||||
if (!dateString) {
|
const getLocaleForTimeSince = (locale: string): Locale =>
|
||||||
return dateString;
|
isUserLocale(locale) ? TIME_SINCE_LOCALES[locale] : enUS;
|
||||||
}
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return intlFormat(
|
|
||||||
date,
|
|
||||||
{
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
locale: "en",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertTimeString = (dateString: string) => {
|
export const timeSince = (dateString: string, locale: string = DEFAULT_LOCALE) => {
|
||||||
const date = new Date(dateString);
|
|
||||||
return intlFormat(
|
|
||||||
date,
|
|
||||||
{
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
locale: "en",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLocaleForTimeSince = (locale: TUserLocale) => {
|
|
||||||
switch (locale) {
|
|
||||||
case "de-DE":
|
|
||||||
return de;
|
|
||||||
case "en-US":
|
|
||||||
return enUS;
|
|
||||||
case "es-ES":
|
|
||||||
return es;
|
|
||||||
case "fr-FR":
|
|
||||||
return fr;
|
|
||||||
case "hu-HU":
|
|
||||||
return hu;
|
|
||||||
case "ja-JP":
|
|
||||||
return ja;
|
|
||||||
case "nl-NL":
|
|
||||||
return nl;
|
|
||||||
case "pt-BR":
|
|
||||||
return ptBR;
|
|
||||||
case "pt-PT":
|
|
||||||
return pt;
|
|
||||||
case "ro-RO":
|
|
||||||
return ro;
|
|
||||||
case "ru-RU":
|
|
||||||
return ru;
|
|
||||||
case "sv-SE":
|
|
||||||
return sv;
|
|
||||||
case "zh-Hans-CN":
|
|
||||||
return zhCN;
|
|
||||||
case "zh-Hant-TW":
|
|
||||||
return zhTW;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const timeSince = (dateString: string, locale: TUserLocale) => {
|
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return formatDistance(date, new Date(), {
|
return formatDistance(date, new Date(), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
@@ -122,27 +35,21 @@ export const timeSince = (dateString: string, locale: TUserLocale) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const timeSinceDate = (date: Date) => {
|
export const timeSinceDate = (date: Date, locale: string = DEFAULT_LOCALE) => {
|
||||||
return formatDistance(date, new Date(), {
|
return formatDistance(date, new Date(), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
|
locale: getLocaleForTimeSince(locale),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDate = (date: Date) => {
|
export const formatDate = (date: Date, locale: string = DEFAULT_LOCALE) => {
|
||||||
return intlFormat(date, {
|
return formatDateForDisplay(date, locale, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTodaysDateFormatted = (seperator: string) => {
|
|
||||||
const date = new Date();
|
|
||||||
const formattedDate = date.toISOString().split("T")[0].split("-").join(seperator);
|
|
||||||
|
|
||||||
return formattedDate;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTodaysDateTimeFormatted = (seperator: string) => {
|
export const getTodaysDateTimeFormatted = (seperator: string) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const formattedDate = date.toISOString().split("T")[0].split("-").join(seperator);
|
const formattedDate = date.toISOString().split("T")[0].split("-").join(seperator);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { type TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
|
import { formatStoredDateForDisplay, getSurveyDateFormatMap, parseStoredDateValue } from "./date-display";
|
||||||
|
|
||||||
|
describe("date display utils", () => {
|
||||||
|
test("parses ISO stored dates", () => {
|
||||||
|
const parsedDate = parseStoredDateValue("2025-05-06");
|
||||||
|
|
||||||
|
expect(parsedDate).not.toBeNull();
|
||||||
|
expect(parsedDate?.getFullYear()).toBe(2025);
|
||||||
|
expect(parsedDate?.getMonth()).toBe(4);
|
||||||
|
expect(parsedDate?.getDate()).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses legacy stored dates using the element format", () => {
|
||||||
|
const parsedDate = parseStoredDateValue("5-6-2025", "M-d-y");
|
||||||
|
|
||||||
|
expect(parsedDate).not.toBeNull();
|
||||||
|
expect(parsedDate?.getFullYear()).toBe(2025);
|
||||||
|
expect(parsedDate?.getMonth()).toBe(4);
|
||||||
|
expect(parsedDate?.getDate()).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses day-first stored dates when no format is provided", () => {
|
||||||
|
const parsedDate = parseStoredDateValue("06-05-2025");
|
||||||
|
|
||||||
|
expect(parsedDate).not.toBeNull();
|
||||||
|
expect(parsedDate?.getFullYear()).toBe(2025);
|
||||||
|
expect(parsedDate?.getMonth()).toBe(4);
|
||||||
|
expect(parsedDate?.getDate()).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formats stored dates using the selected locale", () => {
|
||||||
|
const date = new Date(2025, 4, 6);
|
||||||
|
|
||||||
|
expect(formatStoredDateForDisplay("2025-05-06", undefined, "de-DE")).toBe(
|
||||||
|
new Intl.DateTimeFormat("de-DE", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(date)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for invalid stored dates", () => {
|
||||||
|
expect(formatStoredDateForDisplay("2025-02-30", "y-M-d")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds a date format map for survey date elements", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
id: "dateQuestion",
|
||||||
|
type: "date",
|
||||||
|
format: "d-M-y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "textQuestion",
|
||||||
|
type: "openText",
|
||||||
|
},
|
||||||
|
] as TSurveyElement[];
|
||||||
|
|
||||||
|
expect(getSurveyDateFormatMap(elements)).toEqual({
|
||||||
|
dateQuestion: "d-M-y",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { TSurveyDateElement, TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
|
import { formatDateWithOrdinal } from "./datetime";
|
||||||
|
|
||||||
|
export type TSurveyDateFormatMap = Partial<Record<string, TSurveyDateElement["format"]>>;
|
||||||
|
|
||||||
|
const ISO_STORED_DATE_PATTERN = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
|
||||||
|
|
||||||
|
const buildDate = (year: number, month: number, day: number): Date | null => {
|
||||||
|
if ([year, month, day].some((value) => Number.isNaN(value))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDate = new Date(year, month - 1, day);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedDate.getFullYear() !== year ||
|
||||||
|
parsedDate.getMonth() !== month - 1 ||
|
||||||
|
parsedDate.getDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLegacyStoredDateValue = (value: string, format: TSurveyDateElement["format"]): Date | null => {
|
||||||
|
const parts = value.split("-");
|
||||||
|
|
||||||
|
if (parts.length !== 3 || parts.some((part) => !/^\d{1,4}$/.test(part))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [first, second, third] = parts.map(Number);
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case "M-d-y":
|
||||||
|
return buildDate(third, first, second);
|
||||||
|
case "d-M-y":
|
||||||
|
return buildDate(third, second, first);
|
||||||
|
case "y-M-d":
|
||||||
|
return buildDate(first, second, third);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseStoredDateValue = (value: string, format?: TSurveyDateElement["format"]): Date | null => {
|
||||||
|
const isoMatch = ISO_STORED_DATE_PATTERN.exec(value);
|
||||||
|
|
||||||
|
if (isoMatch) {
|
||||||
|
return buildDate(Number(isoMatch[1]), Number(isoMatch[2]), Number(isoMatch[3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format) {
|
||||||
|
return parseLegacyStoredDateValue(value, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d{1,2}-\d{1,2}-\d{4}$/.test(value)) {
|
||||||
|
return parseLegacyStoredDateValue(value, "d-M-y");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatStoredDateForDisplay = (
|
||||||
|
value: string,
|
||||||
|
format: TSurveyDateElement["format"] | undefined,
|
||||||
|
locale: string = "en-US"
|
||||||
|
): string | null => {
|
||||||
|
const parsedDate = parseStoredDateValue(value, format);
|
||||||
|
|
||||||
|
if (!parsedDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatDateWithOrdinal(parsedDate, locale);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSurveyDateFormatMap = (elements: TSurveyElement[]): TSurveyDateFormatMap => {
|
||||||
|
return elements.reduce<TSurveyDateFormatMap>((dateFormats, element) => {
|
||||||
|
if (element.type === "date") {
|
||||||
|
dateFormats[element.id] = element.format;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateFormats;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { diffInDays, formatDateWithOrdinal, getFormattedDateTimeString, isValidDateString } from "./datetime";
|
import {
|
||||||
|
diffInDays,
|
||||||
|
formatDateForDisplay,
|
||||||
|
formatDateTimeForDisplay,
|
||||||
|
formatDateWithOrdinal,
|
||||||
|
getFormattedDateTimeString,
|
||||||
|
isValidDateString,
|
||||||
|
} from "./datetime";
|
||||||
|
|
||||||
describe("datetime utils", () => {
|
describe("datetime utils", () => {
|
||||||
test("diffInDays calculates the difference in days between two dates", () => {
|
test("diffInDays calculates the difference in days between two dates", () => {
|
||||||
@@ -8,13 +15,45 @@ describe("datetime utils", () => {
|
|||||||
expect(diffInDays(date1, date2)).toBe(5);
|
expect(diffInDays(date1, date2)).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("formatDateWithOrdinal formats a date with ordinal suffix", () => {
|
test("formatDateWithOrdinal formats a date using the provided locale", () => {
|
||||||
// Create a date that's fixed to May 6, 2025 at noon UTC
|
// Create a date that's fixed to May 6, 2025 at noon UTC
|
||||||
// Using noon ensures the date won't change in most timezones
|
// Using noon ensures the date won't change in most timezones
|
||||||
const date = new Date(Date.UTC(2025, 4, 6, 12, 0, 0));
|
const date = new Date(Date.UTC(2025, 4, 6, 12, 0, 0));
|
||||||
|
|
||||||
// Test the function
|
expect(formatDateWithOrdinal(date)).toBe(
|
||||||
expect(formatDateWithOrdinal(date)).toBe("Tuesday, May 6th, 2025");
|
new Intl.DateTimeFormat("en-US", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(date)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatDateForDisplay uses the provided locale", () => {
|
||||||
|
const date = new Date(Date.UTC(2025, 4, 6, 12, 0, 0));
|
||||||
|
|
||||||
|
expect(formatDateForDisplay(date, "de-DE")).toBe(
|
||||||
|
new Intl.DateTimeFormat("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(date)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatDateTimeForDisplay uses the provided locale", () => {
|
||||||
|
const date = new Date(Date.UTC(2025, 4, 6, 12, 30, 0));
|
||||||
|
|
||||||
|
expect(formatDateTimeForDisplay(date, "fr-FR")).toBe(
|
||||||
|
new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(date)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isValidDateString validates correct date strings", () => {
|
test("isValidDateString validates correct date strings", () => {
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
const getOrdinalSuffix = (day: number) => {
|
const DEFAULT_LOCALE = "en-US";
|
||||||
const suffixes = ["th", "st", "nd", "rd"];
|
|
||||||
const relevantDigits = day < 30 ? day % 20 : day % 30;
|
const DEFAULT_DATE_DISPLAY_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||||
return suffixes[relevantDigits <= 3 ? relevantDigits : 0];
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DATE_TIME_DISPLAY_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to calculate difference in days between two dates
|
// Helper function to calculate difference in days between two dates
|
||||||
@@ -10,23 +20,44 @@ export const diffInDays = (date1: Date, date2: Date) => {
|
|||||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDateWithOrdinal = (date: Date, locale: string = "en-US"): string => {
|
export const formatDateForDisplay = (
|
||||||
const dayOfWeek = new Intl.DateTimeFormat(locale, { weekday: "long" }).format(date);
|
date: Date,
|
||||||
const day = date.getDate();
|
locale: string = DEFAULT_LOCALE,
|
||||||
const month = new Intl.DateTimeFormat(locale, { month: "long" }).format(date);
|
options: Intl.DateTimeFormatOptions = DEFAULT_DATE_DISPLAY_OPTIONS
|
||||||
const year = date.getFullYear();
|
): string => {
|
||||||
return `${dayOfWeek}, ${month} ${day}${getOrdinalSuffix(day)}, ${year}`;
|
return new Intl.DateTimeFormat(locale, options).format(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDateTimeForDisplay = (
|
||||||
|
date: Date,
|
||||||
|
locale: string = DEFAULT_LOCALE,
|
||||||
|
options: Intl.DateTimeFormatOptions = DEFAULT_DATE_TIME_DISPLAY_OPTIONS
|
||||||
|
): string => {
|
||||||
|
return new Intl.DateTimeFormat(locale, options).format(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDateWithOrdinal = (date: Date, locale: string = DEFAULT_LOCALE): string => {
|
||||||
|
return formatDateForDisplay(date, locale, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidDateString = (value: string) => {
|
export const isValidDateString = (value: string) => {
|
||||||
const regex = /^(?:\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4})$/;
|
const regex = /^(?:\d{4}-\d{1,2}-\d{1,2}|\d{1,2}-\d{1,2}-\d{4})$/;
|
||||||
|
|
||||||
if (!regex.test(value)) {
|
if (!regex.test(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(value);
|
const normalizedValue = /^\d{1,2}-\d{1,2}-\d{4}$/.test(value)
|
||||||
return date;
|
? value.replace(/(\d{1,2})-(\d{1,2})-(\d{4})/, "$3-$2-$1")
|
||||||
|
: value;
|
||||||
|
|
||||||
|
const date = new Date(normalizedValue);
|
||||||
|
return !Number.isNaN(date.getTime());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormattedDateTimeString = (date: Date): string => {
|
export const getFormattedDateTimeString = (date: Date): string => {
|
||||||
|
|||||||
@@ -32,16 +32,17 @@ vi.mock("@/lib/pollyfills/structuredClone", () => ({
|
|||||||
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
|
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/utils/datetime", () => ({
|
vi.mock("@/lib/utils/date-display", () => ({
|
||||||
isValidDateString: vi.fn((value) => {
|
formatStoredDateForDisplay: vi.fn((value: string, format: string | undefined, locale: string) => {
|
||||||
try {
|
if (value === "2023-01-01") {
|
||||||
return !isNaN(new Date(value as string).getTime());
|
return `formatted-${locale}-${format ?? "iso"}`;
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
formatDateWithOrdinal: vi.fn(() => {
|
if (value === "01-02-2023" && format === "M-d-y") {
|
||||||
return "January 1st, 2023";
|
return `legacy-${locale}-${format}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -477,7 +478,20 @@ describe("recall utility functions", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = parseRecallInfo(text, responseData);
|
const result = parseRecallInfo(text, responseData);
|
||||||
expect(result).toBe("You joined on January 1st, 2023");
|
expect(result).toBe("You joined on formatted-en-US-iso");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formats legacy date values using the provided locale and stored format", () => {
|
||||||
|
const text = "You joined on #recall:joinDate/fallback:an-unknown-date#";
|
||||||
|
const responseData: TResponseData = {
|
||||||
|
joinDate: "01-02-2023",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseRecallInfo(text, responseData, undefined, false, "fr-FR", {
|
||||||
|
joinDate: "M-d-y",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("You joined on legacy-fr-FR-M-d-y");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("formats array values as comma-separated list", () => {
|
test("formats array values as comma-separated list", () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getTextContent } from "@formbricks/types/surveys/validation";
|
|||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
import { formatDateWithOrdinal, isValidDateString } from "./datetime";
|
import { type TSurveyDateFormatMap, formatStoredDateForDisplay } from "./date-display";
|
||||||
|
|
||||||
export interface fallbacks {
|
export interface fallbacks {
|
||||||
[id: string]: string;
|
[id: string]: string;
|
||||||
@@ -224,7 +224,9 @@ export const parseRecallInfo = (
|
|||||||
text: string,
|
text: string,
|
||||||
responseData?: TResponseData,
|
responseData?: TResponseData,
|
||||||
variables?: TResponseVariables,
|
variables?: TResponseVariables,
|
||||||
withSlash: boolean = false
|
withSlash: boolean = false,
|
||||||
|
locale: string = "en-US",
|
||||||
|
dateFormats?: TSurveyDateFormatMap
|
||||||
) => {
|
) => {
|
||||||
let modifiedText = text;
|
let modifiedText = text;
|
||||||
const questionIds = responseData ? Object.keys(responseData) : [];
|
const questionIds = responseData ? Object.keys(responseData) : [];
|
||||||
@@ -254,12 +256,14 @@ export const parseRecallInfo = (
|
|||||||
value = responseData[recallItemId];
|
value = responseData[recallItemId];
|
||||||
|
|
||||||
// Apply formatting for special value types
|
// Apply formatting for special value types
|
||||||
if (value) {
|
if (typeof value === "string") {
|
||||||
if (isValidDateString(value as string)) {
|
const formattedDate = formatStoredDateForDisplay(value, dateFormats?.[recallItemId], locale);
|
||||||
value = formatDateWithOrdinal(new Date(value as string));
|
|
||||||
} else if (Array.isArray(value)) {
|
if (formattedDate) {
|
||||||
value = value.filter((item) => item).join(", ");
|
value = formattedDate;
|
||||||
}
|
}
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
value = value.filter((item) => item).join(", ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ vi.mock("node:dns", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: false,
|
||||||
|
}));
|
||||||
|
|
||||||
const mockResolve = vi.mocked(dns.resolve);
|
const mockResolve = vi.mocked(dns.resolve);
|
||||||
const mockResolve6 = vi.mocked(dns.resolve6);
|
const mockResolve6 = vi.mocked(dns.resolve6);
|
||||||
|
|
||||||
@@ -294,4 +298,78 @@ describe("validateWebhookUrl", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS", () => {
|
||||||
|
test("allows private IP URLs when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("http://127.0.0.1/")).resolves.toBeUndefined();
|
||||||
|
await expect(validateWithFlag("http://192.168.1.1/test")).resolves.toBeUndefined();
|
||||||
|
await expect(validateWithFlag("http://10.0.0.1/webhook")).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows localhost when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("http://localhost/webhook")).resolves.toBeUndefined();
|
||||||
|
await expect(validateWithFlag("http://localhost:3333/webhook")).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows localhost.localdomain when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("http://localhost.localdomain/path")).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows hostname resolving to private IP when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setupDnsResolution(["192.168.1.1"]);
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("https://internal.company.com/webhook")).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("still rejects unresolvable hostnames when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setupDnsResolution(null, null);
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("https://typo-gibberish.invalid/hook")).rejects.toThrow(
|
||||||
|
"Could not resolve webhook URL hostname"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("still rejects invalid URL format when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("not-a-url")).rejects.toThrow("Invalid webhook URL format");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("still rejects non-HTTP protocols when enabled", async () => {
|
||||||
|
vi.doMock("../constants", () => ({
|
||||||
|
DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { validateWebhookUrl: validateWithFlag } = await import("./validate-webhook-url");
|
||||||
|
await expect(validateWithFlag("ftp://192.168.1.1/")).rejects.toThrow(
|
||||||
|
"Webhook URL must use HTTPS or HTTP protocol"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import dns from "node:dns";
|
import dns from "node:dns";
|
||||||
import { InvalidInputError } from "@formbricks/types/errors";
|
import { InvalidInputError } from "@formbricks/types/errors";
|
||||||
|
import { DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS } from "../constants";
|
||||||
|
|
||||||
const BLOCKED_HOSTNAMES = new Set([
|
const BLOCKED_HOSTNAMES = new Set([
|
||||||
"localhost",
|
"localhost",
|
||||||
@@ -139,8 +140,10 @@ export const validateWebhookUrl = async (url: string): Promise<void> => {
|
|||||||
|
|
||||||
const hostname = parsed.hostname;
|
const hostname = parsed.hostname;
|
||||||
|
|
||||||
if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {
|
if (!DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS) {
|
||||||
throw new InvalidInputError("Webhook URL must not point to localhost or internal services");
|
if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {
|
||||||
|
throw new InvalidInputError("Webhook URL must not point to localhost or internal services");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct IP literal — validate without DNS resolution
|
// Direct IP literal — validate without DNS resolution
|
||||||
@@ -149,12 +152,17 @@ export const validateWebhookUrl = async (url: string): Promise<void> => {
|
|||||||
|
|
||||||
if (isIPv4Literal || isIPv6Literal) {
|
if (isIPv4Literal || isIPv6Literal) {
|
||||||
const ip = isIPv6Literal ? stripIPv6Brackets(hostname) : hostname;
|
const ip = isIPv6Literal ? stripIPv6Brackets(hostname) : hostname;
|
||||||
if (isPrivateIP(ip)) {
|
if (!DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS && isPrivateIP(ip)) {
|
||||||
throw new InvalidInputError("Webhook URL must not point to private or internal IP addresses");
|
throw new InvalidInputError("Webhook URL must not point to private or internal IP addresses");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip DNS resolution for localhost-like hostnames when internal URLs are allowed since these are resolved via /etc/hosts and not DNS
|
||||||
|
if (DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS && BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Domain name — resolve DNS and validate every resolved IP
|
// Domain name — resolve DNS and validate every resolved IP
|
||||||
let resolvedIPs: string[];
|
let resolvedIPs: string[];
|
||||||
try {
|
try {
|
||||||
@@ -168,9 +176,11 @@ export const validateWebhookUrl = async (url: string): Promise<void> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ip of resolvedIPs) {
|
if (!DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS) {
|
||||||
if (isPrivateIP(ip)) {
|
for (const ip of resolvedIPs) {
|
||||||
throw new InvalidInputError("Webhook URL must not point to private or internal IP addresses");
|
if (isPrivateIP(ip)) {
|
||||||
|
throw new InvalidInputError("Webhook URL must not point to private or internal IP addresses");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Verbinden",
|
"connect": "Verbinden",
|
||||||
"connect_formbricks": "Formbricks verbinden",
|
"connect_formbricks": "Formbricks verbinden",
|
||||||
"connected": "Verbunden",
|
"connected": "Verbunden",
|
||||||
|
"contact": "Kontakt",
|
||||||
"contacts": "Kontakte",
|
"contacts": "Kontakte",
|
||||||
"continue": "Weitermachen",
|
"continue": "Weitermachen",
|
||||||
"copied": "Kopiert",
|
"copied": "Kopiert",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"copy_code": "Code kopieren",
|
"copy_code": "Code kopieren",
|
||||||
"copy_link": "Link kopieren",
|
"copy_link": "Link kopieren",
|
||||||
|
"copy_to_environment": "In {{environment}} kopieren",
|
||||||
"count_attributes": "{count, plural, one {{count} Attribut} other {{count} Attribute}}",
|
"count_attributes": "{count, plural, one {{count} Attribut} other {{count} Attribute}}",
|
||||||
"count_contacts": "{count, plural, one {{count} Kontakt} other {{count} Kontakte}}",
|
"count_contacts": "{count, plural, one {{count} Kontakt} other {{count} Kontakte}}",
|
||||||
"count_members": "{count, plural, one {{count} Mitglied} other {{count} Mitglieder}}",
|
"count_members": "{count, plural, one {{count} Mitglied} other {{count} Mitglieder}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(Kopie {copyNumber})",
|
"duplicate_copy_number": "(Kopie {copyNumber})",
|
||||||
"e_commerce": "E-Commerce",
|
"e_commerce": "E-Commerce",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
|
"elements": "Elemente",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"ending_card": "Abschluss-Karte",
|
"ending_card": "Abschluss-Karte",
|
||||||
"enter_url": "URL eingeben",
|
"enter_url": "URL eingeben",
|
||||||
"enterprise_license": "Enterprise Lizenz",
|
"enterprise_license": "Enterprise Lizenz",
|
||||||
"environment": "Umgebung",
|
"environment": "Umgebung",
|
||||||
"environment_not_found": "Umgebung nicht gefunden",
|
|
||||||
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
|
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
|
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Inaktive Umfragen",
|
"inactive_surveys": "Inaktive Umfragen",
|
||||||
"integration": "Integration",
|
"integration": "Integration",
|
||||||
"integrations": "Integrationen",
|
"integrations": "Integrationen",
|
||||||
"invalid_date": "Ungültiges Datum",
|
"invalid_date_with_value": "Ungültiges Datum: {value}",
|
||||||
"invalid_file_name": "Ungültiger Dateiname, bitte benennen Sie Ihre Datei um und versuchen Sie es erneut",
|
"invalid_file_name": "Ungültiger Dateiname, bitte benennen Sie Ihre Datei um und versuchen Sie es erneut",
|
||||||
"invalid_file_type": "Ungültiger Dateityp",
|
"invalid_file_type": "Ungültiger Dateityp",
|
||||||
"invite": "Einladen",
|
"invite": "Einladen",
|
||||||
"invite_them": "Lade sie ein",
|
"invite_them": "Lade sie ein",
|
||||||
|
"javascript_required": "JavaScript erforderlich",
|
||||||
|
"javascript_required_description": "Formbricks benötigt JavaScript, um ordnungsgemäß zu funktionieren. Bitte aktiviere JavaScript in deinen Browsereinstellungen, um fortzufahren.",
|
||||||
"key": "Schlüssel",
|
"key": "Schlüssel",
|
||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"members_and_teams": "Mitglieder & Teams",
|
"members_and_teams": "Mitglieder & Teams",
|
||||||
|
"membership": "Mitgliedschaft",
|
||||||
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metadaten",
|
"metadata": "Metadaten",
|
||||||
"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_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_surveys_look_good": "Keine Sorge – deine Umfragen sehen auf jedem Gerät und jeder Bildschirmgröße großartig aus!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Neu",
|
"new": "Neu",
|
||||||
"new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!",
|
"new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!",
|
||||||
"next": "Weiter",
|
"next": "Weiter",
|
||||||
|
"no_actions_found": "Keine Aktionen gefunden",
|
||||||
"no_background_image_found": "Kein Hintergrundbild gefunden.",
|
"no_background_image_found": "Kein Hintergrundbild gefunden.",
|
||||||
"no_code": "No Code",
|
"no_code": "No Code",
|
||||||
"no_files_uploaded": "Keine Dateien hochgeladen",
|
"no_files_uploaded": "Keine Dateien hochgeladen",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "oder",
|
"or": "oder",
|
||||||
"organization": "Organisation",
|
"organization": "Organisation",
|
||||||
"organization_id": "Organisations-ID",
|
"organization_id": "Organisations-ID",
|
||||||
"organization_not_found": "Organisation nicht gefunden",
|
|
||||||
"organization_settings": "Organisationseinstellungen",
|
"organization_settings": "Organisationseinstellungen",
|
||||||
"organization_teams_not_found": "Organisations-Teams nicht gefunden",
|
|
||||||
"other": "Andere",
|
"other": "Andere",
|
||||||
|
"other_filters": "Weitere Filter",
|
||||||
"others": "Andere",
|
"others": "Andere",
|
||||||
"overlay_color": "Overlay-Farbe",
|
"overlay_color": "Overlay-Farbe",
|
||||||
"overview": "Überblick",
|
"overview": "Überblick",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Bitte wähle mindestens eine Umfrage aus",
|
"please_select_at_least_one_survey": "Bitte wähle mindestens eine Umfrage aus",
|
||||||
"please_select_at_least_one_trigger": "Bitte wähle mindestens einen Auslöser aus",
|
"please_select_at_least_one_trigger": "Bitte wähle mindestens einen Auslöser aus",
|
||||||
"please_upgrade_your_plan": "Bitte aktualisieren Sie Ihren Plan",
|
"please_upgrade_your_plan": "Bitte aktualisieren Sie Ihren Plan",
|
||||||
|
"powered_by_formbricks": "Bereitgestellt von Formbricks",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"preview_survey": "Umfragevorschau",
|
"preview_survey": "Umfragevorschau",
|
||||||
"privacy": "Datenschutz",
|
"privacy": "Datenschutz",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Auswählen",
|
"select": "Auswählen",
|
||||||
"select_all": "Alles auswählen",
|
"select_all": "Alles auswählen",
|
||||||
"select_filter": "Filter auswählen",
|
"select_filter": "Filter auswählen",
|
||||||
|
"select_language": "Sprache auswählen",
|
||||||
"select_survey": "Umfrage auswählen",
|
"select_survey": "Umfrage auswählen",
|
||||||
"select_teams": "Teams auswählen",
|
"select_teams": "Teams auswählen",
|
||||||
"selected": "Ausgewählt",
|
"selected": "Ausgewählt",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "Umfrage-ID",
|
"survey_id": "Umfrage-ID",
|
||||||
"survey_languages": "Umfragesprachen",
|
"survey_languages": "Umfragesprachen",
|
||||||
"survey_live": "Umfrage live",
|
"survey_live": "Umfrage live",
|
||||||
"survey_not_found": "Umfrage nicht gefunden",
|
|
||||||
"survey_paused": "Umfrage pausiert.",
|
"survey_paused": "Umfrage pausiert.",
|
||||||
"survey_type": "Umfragetyp",
|
"survey_type": "Umfragetyp",
|
||||||
"surveys": "Umfragen",
|
"surveys": "Umfragen",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Teamname",
|
"team_name": "Teamname",
|
||||||
"team_role": "Team-Rolle",
|
"team_role": "Team-Rolle",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"teams_not_found": "Teams nicht gefunden",
|
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"time": "Zeit",
|
"time": "Zeit",
|
||||||
"time_to_finish": "Zeit zum Fertigstellen",
|
"time_to_finish": "Zeit zum Fertigstellen",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Benutzer",
|
"user": "Benutzer",
|
||||||
"user_id": "Benutzer-ID",
|
"user_id": "Benutzer-ID",
|
||||||
"user_not_found": "Benutzer nicht gefunden",
|
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_ids": "Variablen-IDs",
|
"variable_ids": "Variablen-IDs",
|
||||||
"variables": "Variablen",
|
"variables": "Variablen",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "Wochen",
|
"weeks": "Wochen",
|
||||||
"welcome_card": "Willkommenskarte",
|
"welcome_card": "Willkommenskarte",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
"workspace": "Arbeitsbereich",
|
||||||
"workspace_configuration": "Projektkonfiguration",
|
"workspace_configuration": "Projektkonfiguration",
|
||||||
"workspace_created_successfully": "Projekt erfolgreich erstellt",
|
"workspace_created_successfully": "Projekt erfolgreich erstellt",
|
||||||
"workspace_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
|
"workspace_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
|
||||||
"workspace_id": "Projekt-ID",
|
"workspace_id": "Projekt-ID",
|
||||||
"workspace_name": "Projektname",
|
"workspace_name": "Projektname",
|
||||||
"workspace_name_placeholder": "z. B. Formbricks",
|
"workspace_name_placeholder": "z. B. Formbricks",
|
||||||
"workspace_not_found": "Projekt nicht gefunden",
|
|
||||||
"workspace_permission_not_found": "Projektberechtigung nicht gefunden",
|
|
||||||
"workspaces": "Projekte",
|
"workspaces": "Projekte",
|
||||||
"years": "Jahre",
|
"years": "Jahre",
|
||||||
"you": "Du",
|
"you": "Du",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Neues Attribut “{key}” mit Typ “{dataType}” erstellt",
|
"attributes_msg_new_attribute_created": "Neues Attribut “{key}” mit Typ “{dataType}” erstellt",
|
||||||
"attributes_msg_userid_already_exists": "Die Benutzer-ID existiert bereits für diese Umgebung und wurde nicht aktualisiert.",
|
"attributes_msg_userid_already_exists": "Die Benutzer-ID existiert bereits für diese Umgebung und wurde nicht aktualisiert.",
|
||||||
"contact_deleted_successfully": "Kontakt erfolgreich gelöscht",
|
"contact_deleted_successfully": "Kontakt erfolgreich gelöscht",
|
||||||
"contact_not_found": "Kein solcher Kontakt gefunden",
|
|
||||||
"contacts_table_refresh": "Kontakte aktualisieren",
|
"contacts_table_refresh": "Kontakte aktualisieren",
|
||||||
"contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert",
|
"contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert",
|
||||||
"create_attribute": "Attribut erstellen",
|
"create_attribute": "Attribut erstellen",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Erstellt von einer dritten Partei",
|
"created_by_third_party": "Erstellt von einer dritten Partei",
|
||||||
"discord_webhook_not_supported": "Discord-Webhooks werden derzeit nicht unterstützt.",
|
"discord_webhook_not_supported": "Discord-Webhooks werden derzeit nicht unterstützt.",
|
||||||
"empty_webhook_message": "Deine Webhooks werden hier angezeigt, sobald Du sie hinzufügst ⏲️",
|
"empty_webhook_message": "Deine Webhooks werden hier angezeigt, sobald Du sie hinzufügst ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Ungültiges Gateway (502): Proxy-/Gateway-Fehler, Dienst nicht erreichbar",
|
||||||
|
"endpoint_gateway_timeout_error": "Gateway-Zeitüberschreitung (504): Gateway-Zeitüberschreitung, Dienst nicht erreichbar",
|
||||||
|
"endpoint_internal_server_error": "Interner Serverfehler (500): Der Dienst ist auf einen unerwarteten Fehler gestoßen",
|
||||||
|
"endpoint_method_not_allowed_error": "Methode nicht erlaubt (405): Der Endpoint existiert, akzeptiert aber keine POST-Anfragen",
|
||||||
|
"endpoint_not_found_error": "Nicht gefunden (404): Der Endpoint existiert nicht",
|
||||||
"endpoint_pinged": "Juhu! Wir können den Webhook anpingen!",
|
"endpoint_pinged": "Juhu! Wir können den Webhook anpingen!",
|
||||||
"endpoint_pinged_error": "Kann den Webhook nicht anpingen!",
|
"endpoint_pinged_error": "Kann den Webhook nicht anpingen!",
|
||||||
|
"endpoint_service_unavailable_error": "Dienst nicht verfügbar (503): Dienst ist vorübergehend nicht verfügbar",
|
||||||
"learn_to_verify": "Erfahren Sie, wie Sie Webhook-Signaturen verifizieren",
|
"learn_to_verify": "Erfahren Sie, wie Sie Webhook-Signaturen verifizieren",
|
||||||
|
"no_triggers": "Keine Trigger",
|
||||||
"please_check_console": "Bitte überprüfe die Konsole für weitere Details",
|
"please_check_console": "Bitte überprüfe die Konsole für weitere Details",
|
||||||
"please_enter_a_url": "Bitte gib eine URL ein",
|
"please_enter_a_url": "Bitte gib eine URL ein",
|
||||||
"response_created": "Antwort erstellt",
|
"response_created": "Antwort erstellt",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Unternehmensfunktionen",
|
"enterprise_features": "Unternehmensfunktionen",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Hol dir eine Enterprise-Lizenz, um Zugriff auf alle Funktionen zu erhalten.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Hol dir eine Enterprise-Lizenz, um Zugriff auf alle Funktionen zu erhalten.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Behalte die volle Kontrolle über deine Daten, Privatsphäre und Sicherheit.",
|
"keep_full_control_over_your_data_privacy_and_security": "Behalte die volle Kontrolle über deine Daten, Privatsphäre und Sicherheit.",
|
||||||
|
"license_feature_access_control": "Zugriffskontrolle (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Audit-Protokolle",
|
||||||
|
"license_feature_contacts": "Kontakte & Segmente",
|
||||||
|
"license_feature_projects": "Arbeitsbereiche",
|
||||||
|
"license_feature_quotas": "Kontingente",
|
||||||
|
"license_feature_remove_branding": "Branding entfernen",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Spam-Schutz",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Zwei-Faktor-Authentifizierung",
|
||||||
|
"license_feature_whitelabel": "White-Label-E-Mails",
|
||||||
|
"license_features_table_access": "Zugriff",
|
||||||
|
"license_features_table_description": "Enterprise-Funktionen und Limits, die für diese Instanz aktuell verfügbar sind.",
|
||||||
|
"license_features_table_disabled": "Deaktiviert",
|
||||||
|
"license_features_table_enabled": "Aktiviert",
|
||||||
|
"license_features_table_feature": "Funktion",
|
||||||
|
"license_features_table_title": "Lizenzierte Funktionen",
|
||||||
|
"license_features_table_unlimited": "Unbegrenzt",
|
||||||
|
"license_features_table_value": "Wert",
|
||||||
"license_instance_mismatch_description": "Diese Lizenz ist derzeit an eine andere Formbricks-Instanz gebunden. Falls diese Installation neu aufgebaut oder verschoben wurde, bitte den Formbricks-Support, die vorherige Instanzbindung zu entfernen.",
|
"license_instance_mismatch_description": "Diese Lizenz ist derzeit an eine andere Formbricks-Instanz gebunden. Falls diese Installation neu aufgebaut oder verschoben wurde, bitte den Formbricks-Support, die vorherige Instanzbindung zu entfernen.",
|
||||||
"license_invalid_description": "Der Lizenzschlüssel in deiner ENTERPRISE_LICENSE_KEY-Umgebungsvariable ist nicht gültig. Bitte überprüfe auf Tippfehler oder fordere einen neuen Schlüssel an.",
|
"license_invalid_description": "Der Lizenzschlüssel in deiner ENTERPRISE_LICENSE_KEY-Umgebungsvariable ist nicht gültig. Bitte überprüfe auf Tippfehler oder fordere einen neuen Schlüssel an.",
|
||||||
"license_status": "Lizenzstatus",
|
"license_status": "Lizenzstatus",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Benutzerdefinierter Hostname",
|
"custom_hostname": "Benutzerdefinierter Hostname",
|
||||||
"customize_survey_logo": "Umfragelogo anpassen",
|
"customize_survey_logo": "Umfragelogo anpassen",
|
||||||
"darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.",
|
"darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.",
|
||||||
"date_format": "Datumsformat",
|
|
||||||
"days_before_showing_this_survey_again": "oder mehr Tage müssen zwischen der zuletzt angezeigten Umfrage und der Anzeige dieser Umfrage vergehen.",
|
"days_before_showing_this_survey_again": "oder mehr Tage müssen zwischen der zuletzt angezeigten Umfrage und der Anzeige dieser Umfrage vergehen.",
|
||||||
"delete_anyways": "Trotzdem löschen",
|
"delete_anyways": "Trotzdem löschen",
|
||||||
"delete_block": "Block löschen",
|
"delete_block": "Block löschen",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Fehler beim Speichern der Änderungen",
|
"error_saving_changes": "Fehler beim Speichern der Änderungen",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Mehrfachantworten erlauben; weiterhin anzeigen, auch nach einer Antwort (z.B. Feedback-Box).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Mehrfachantworten erlauben; weiterhin anzeigen, auch nach einer Antwort (z.B. Feedback-Box).",
|
||||||
"everyone": "Jeder",
|
"everyone": "Jeder",
|
||||||
|
"expand_preview": "Vorschau erweitern",
|
||||||
"external_urls_paywall_tooltip": "Bitte upgrade auf einen kostenpflichtigen Tarif, um externe URLs anzupassen. So helfen wir, Phishing zu verhindern.",
|
"external_urls_paywall_tooltip": "Bitte upgrade auf einen kostenpflichtigen Tarif, um externe URLs anzupassen. So helfen wir, Phishing zu verhindern.",
|
||||||
"fallback_missing": "Fehlender Fallback",
|
"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_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.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "Antwortlimit muss die Anzahl der erhaltenen Antworten ({responseCount}) überschreiten.",
|
"response_limit_needs_to_exceed_number_of_received_responses": "Antwortlimit muss die Anzahl der erhaltenen Antworten ({responseCount}) überschreiten.",
|
||||||
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
|
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
|
||||||
"response_options": "Antwortoptionen",
|
"response_options": "Antwortoptionen",
|
||||||
|
"reverse_order_occasionally": "Reihenfolge gelegentlich umkehren",
|
||||||
|
"reverse_order_occasionally_except_last": "Reihenfolge gelegentlich umkehren, außer letzter",
|
||||||
"roundness": "Rundheit",
|
"roundness": "Rundheit",
|
||||||
"roundness_description": "Steuert, wie abgerundet die Ecken sind.",
|
"roundness_description": "Steuert, wie abgerundet die Ecken sind.",
|
||||||
"row_used_in_logic_error": "Diese Zeile wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
"row_used_in_logic_error": "Diese Zeile wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Umfrage maximal anzeigen von",
|
"show_survey_maximum_of": "Umfrage maximal anzeigen von",
|
||||||
"show_survey_to_users": "Umfrage % der Nutzer anzeigen",
|
"show_survey_to_users": "Umfrage % der Nutzer anzeigen",
|
||||||
"show_to_x_percentage_of_targeted_users": "Zeige {percentage}% der Zielbenutzer",
|
"show_to_x_percentage_of_targeted_users": "Zeige {percentage}% der Zielbenutzer",
|
||||||
|
"shrink_preview": "Vorschau verkleinern",
|
||||||
"simple": "Einfach",
|
"simple": "Einfach",
|
||||||
"six_points": "6 Punkte",
|
"six_points": "6 Punkte",
|
||||||
"smiley": "Smiley",
|
"smiley": "Smiley",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Styling auf Themenstile eingestellt",
|
"styling_set_to_theme_styles": "Styling auf Themenstile eingestellt",
|
||||||
"subheading": "Zwischenüberschrift",
|
"subheading": "Zwischenüberschrift",
|
||||||
"subtract": "Subtrahieren -",
|
"subtract": "Subtrahieren -",
|
||||||
|
"survey_closed_message_heading_required": "Füge der benutzerdefinierten Nachricht für geschlossene Umfragen eine Überschrift hinzu.",
|
||||||
"survey_completed_heading": "Umfrage abgeschlossen",
|
"survey_completed_heading": "Umfrage abgeschlossen",
|
||||||
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
|
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
|
||||||
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
|
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
|
||||||
"survey_placement": "Platzierung der Umfrage",
|
"survey_placement": "Platzierung der Umfrage",
|
||||||
|
"survey_preview": "Umfragevorschau 👀",
|
||||||
"survey_styling": "Umfrage Styling",
|
"survey_styling": "Umfrage Styling",
|
||||||
"survey_trigger": "Auslöser der Umfrage",
|
"survey_trigger": "Auslöser der Umfrage",
|
||||||
"switch_multi_language_on_to_get_started": "Aktiviere Mehrsprachigkeit, um loszulegen 👉",
|
"switch_multi_language_on_to_get_started": "Aktiviere Mehrsprachigkeit, um loszulegen 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Nein, danke!",
|
"preview_survey_question_2_choice_2_label": "Nein, danke!",
|
||||||
"preview_survey_question_2_headline": "Möchtest Du auf dem Laufenden bleiben?",
|
"preview_survey_question_2_headline": "Möchtest Du auf dem Laufenden bleiben?",
|
||||||
"preview_survey_question_2_subheader": "Dies ist eine Beispielbeschreibung.",
|
"preview_survey_question_2_subheader": "Dies ist eine Beispielbeschreibung.",
|
||||||
"preview_survey_question_open_text_headline": "Möchtest Du noch etwas teilen?",
|
"preview_survey_question_open_text_headline": "Möchten Sie noch etwas mitteilen?",
|
||||||
"preview_survey_question_open_text_placeholder": "Tippe deine Antwort hier...",
|
"preview_survey_question_open_text_placeholder": "Tippe deine Antwort hier...",
|
||||||
"preview_survey_question_open_text_subheader": "Dein Feedback hilft uns, besser zu werden.",
|
"preview_survey_question_open_text_subheader": "Dein Feedback hilft uns, besser zu werden.",
|
||||||
"preview_survey_welcome_card_headline": "Willkommen!",
|
"preview_survey_welcome_card_headline": "Willkommen!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Danke, dass du deine Workflow-Idee mit uns geteilt hast! Wir arbeiten gerade an diesem Feature und dein Feedback hilft uns dabei, genau das zu entwickeln, was du brauchst.",
|
"coming_soon_description": "Danke, dass du deine Workflow-Idee mit uns geteilt hast! Wir arbeiten gerade an diesem Feature und dein Feedback hilft uns dabei, genau das zu entwickeln, was du brauchst.",
|
||||||
"coming_soon_title": "Wir sind fast da!",
|
"coming_soon_title": "Wir sind fast da!",
|
||||||
"follow_up_label": "Gibt es noch etwas, das du hinzufügen möchtest?",
|
"follow_up_label": "Möchten Sie noch etwas hinzufügen?",
|
||||||
"follow_up_placeholder": "Welche konkreten Aufgaben möchten Sie automatisieren? Gibt es Tools oder Integrationen, die Sie einbinden möchten?",
|
"follow_up_placeholder": "Welche konkreten Aufgaben möchten Sie automatisieren? Gibt es Tools oder Integrationen, die Sie einbinden möchten?",
|
||||||
"generate_button": "Workflow generieren",
|
"generate_button": "Workflow generieren",
|
||||||
"heading": "Welchen Workflow möchtest du erstellen?",
|
"heading": "Welchen Workflow möchtest du erstellen?",
|
||||||
|
|||||||
+91
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
"connect_formbricks": "Connect Formbricks",
|
"connect_formbricks": "Connect Formbricks",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
"contact": "Contact",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copy_code": "Copy code",
|
"copy_code": "Copy code",
|
||||||
"copy_link": "Copy Link",
|
"copy_link": "Copy Link",
|
||||||
|
"copy_to_environment": "Copy to {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} attribute} other {{count} attributes}}",
|
"count_attributes": "{count, plural, one {{count} attribute} other {{count} attributes}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
||||||
"count_members": "{count, plural, one {{count} member} other {{count} members}}",
|
"count_members": "{count, plural, one {{count} member} other {{count} members}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(copy {copyNumber})",
|
"duplicate_copy_number": "(copy {copyNumber})",
|
||||||
"e_commerce": "E-Commerce",
|
"e_commerce": "E-Commerce",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
"elements": "Elements",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Ending card",
|
"ending_card": "Ending card",
|
||||||
"enter_url": "Enter URL",
|
"enter_url": "Enter URL",
|
||||||
"enterprise_license": "Enterprise License",
|
"enterprise_license": "Enterprise License",
|
||||||
"environment": "Environment",
|
"environment": "Environment",
|
||||||
"environment_not_found": "Environment not found",
|
|
||||||
"environment_notice": "You are currently in the {environment} environment.",
|
"environment_notice": "You are currently in the {environment} environment.",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_component_description": "This resource does not exist or you do not have the necessary rights to access it.",
|
"error_component_description": "This resource does not exist or you do not have the necessary rights to access it.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Inactive surveys",
|
"inactive_surveys": "Inactive surveys",
|
||||||
"integration": "integration",
|
"integration": "integration",
|
||||||
"integrations": "Integrations",
|
"integrations": "Integrations",
|
||||||
"invalid_date": "Invalid date",
|
"invalid_date_with_value": "Invalid date: {value}",
|
||||||
"invalid_file_name": "Invalid file name, please rename your file and try again",
|
"invalid_file_name": "Invalid file name, please rename your file and try again",
|
||||||
"invalid_file_type": "Invalid file type",
|
"invalid_file_type": "Invalid file type",
|
||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"invite_them": "Invite them",
|
"invite_them": "Invite them",
|
||||||
|
"javascript_required": "JavaScript Required",
|
||||||
|
"javascript_required_description": "Formbricks requires JavaScript to function properly. Please enable JavaScript in your browser settings to continue.",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"members_and_teams": "Members & Teams",
|
"members_and_teams": "Members & Teams",
|
||||||
|
"membership": "Membership",
|
||||||
"membership_not_found": "Membership not found",
|
"membership_not_found": "Membership not found",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"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_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": "Do not worry – your surveys look great on every device and screen size!",
|
"mobile_overlay_surveys_look_good": "Do not worry – your surveys look great on every device and screen size!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "New",
|
"new": "New",
|
||||||
"new_version_available": "Formbricks {version} is here. Upgrade now!",
|
"new_version_available": "Formbricks {version} is here. Upgrade now!",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
"no_actions_found": "No actions found",
|
||||||
"no_background_image_found": "No background image found.",
|
"no_background_image_found": "No background image found.",
|
||||||
"no_code": "No code",
|
"no_code": "No code",
|
||||||
"no_files_uploaded": "No files were uploaded",
|
"no_files_uploaded": "No files were uploaded",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "or",
|
"or": "or",
|
||||||
"organization": "Organization",
|
"organization": "Organization",
|
||||||
"organization_id": "Organization ID",
|
"organization_id": "Organization ID",
|
||||||
"organization_not_found": "Organization not found",
|
|
||||||
"organization_settings": "Organization settings",
|
"organization_settings": "Organization settings",
|
||||||
"organization_teams_not_found": "Organization teams not found",
|
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
|
"other_filters": "Other Filters",
|
||||||
"others": "Others",
|
"others": "Others",
|
||||||
"overlay_color": "Overlay color",
|
"overlay_color": "Overlay color",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Please select at least one survey",
|
"please_select_at_least_one_survey": "Please select at least one survey",
|
||||||
"please_select_at_least_one_trigger": "Please select at least one trigger",
|
"please_select_at_least_one_trigger": "Please select at least one trigger",
|
||||||
"please_upgrade_your_plan": "Please upgrade your plan",
|
"please_upgrade_your_plan": "Please upgrade your plan",
|
||||||
|
"powered_by_formbricks": "Powered by Formbricks",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"preview_survey": "Preview Survey",
|
"preview_survey": "Preview Survey",
|
||||||
"privacy": "Privacy Policy",
|
"privacy": "Privacy Policy",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Select",
|
"select": "Select",
|
||||||
"select_all": "Select all",
|
"select_all": "Select all",
|
||||||
"select_filter": "Select filter",
|
"select_filter": "Select filter",
|
||||||
|
"select_language": "Select Language",
|
||||||
"select_survey": "Select Survey",
|
"select_survey": "Select Survey",
|
||||||
"select_teams": "Select teams",
|
"select_teams": "Select teams",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "Survey ID",
|
"survey_id": "Survey ID",
|
||||||
"survey_languages": "Survey Languages",
|
"survey_languages": "Survey Languages",
|
||||||
"survey_live": "Survey live",
|
"survey_live": "Survey live",
|
||||||
"survey_not_found": "Survey not found",
|
|
||||||
"survey_paused": "Survey paused.",
|
"survey_paused": "Survey paused.",
|
||||||
"survey_type": "Survey Type",
|
"survey_type": "Survey Type",
|
||||||
"surveys": "Surveys",
|
"surveys": "Surveys",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Team name",
|
"team_name": "Team name",
|
||||||
"team_role": "Team role",
|
"team_role": "Team role",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"teams_not_found": "Teams not found",
|
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"time": "Time",
|
"time": "Time",
|
||||||
"time_to_finish": "Time to finish",
|
"time_to_finish": "Time to finish",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"user_id": "User ID",
|
"user_id": "User ID",
|
||||||
"user_not_found": "User not found",
|
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_ids": "Variable IDs",
|
"variable_ids": "Variable IDs",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "weeks",
|
"weeks": "weeks",
|
||||||
"welcome_card": "Welcome card",
|
"welcome_card": "Welcome card",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
"workspace": "Workspace",
|
||||||
"workspace_configuration": "Workspace Configuration",
|
"workspace_configuration": "Workspace Configuration",
|
||||||
"workspace_created_successfully": "Workspace created successfully",
|
"workspace_created_successfully": "Workspace created successfully",
|
||||||
"workspace_creation_description": "Organize surveys in workspaces for better access control.",
|
"workspace_creation_description": "Organize surveys in workspaces for better access control.",
|
||||||
"workspace_id": "Workspace ID",
|
"workspace_id": "Workspace ID",
|
||||||
"workspace_name": "Workspace Name",
|
"workspace_name": "Workspace Name",
|
||||||
"workspace_name_placeholder": "e.g. Formbricks",
|
"workspace_name_placeholder": "e.g. Formbricks",
|
||||||
"workspace_not_found": "Workspace not found",
|
|
||||||
"workspace_permission_not_found": "Workspace permission not found",
|
|
||||||
"workspaces": "Workspaces",
|
"workspaces": "Workspaces",
|
||||||
"years": "years",
|
"years": "years",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Created new attribute “{key}” with type “{dataType}”",
|
"attributes_msg_new_attribute_created": "Created new attribute “{key}” with type “{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "The user ID already exists for this environment and was not updated.",
|
"attributes_msg_userid_already_exists": "The user ID already exists for this environment and was not updated.",
|
||||||
"contact_deleted_successfully": "Contact deleted successfully",
|
"contact_deleted_successfully": "Contact deleted successfully",
|
||||||
"contact_not_found": "No such contact found",
|
|
||||||
"contacts_table_refresh": "Refresh contacts",
|
"contacts_table_refresh": "Refresh contacts",
|
||||||
"contacts_table_refresh_success": "Contacts refreshed successfully",
|
"contacts_table_refresh_success": "Contacts refreshed successfully",
|
||||||
"create_attribute": "Create attribute",
|
"create_attribute": "Create attribute",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Created by a Third Party",
|
"created_by_third_party": "Created by a Third Party",
|
||||||
"discord_webhook_not_supported": "Discord webhooks are currently not supported.",
|
"discord_webhook_not_supported": "Discord webhooks are currently not supported.",
|
||||||
"empty_webhook_message": "Your webhooks will appear here as soon as you add them. ⏲️",
|
"empty_webhook_message": "Your webhooks will appear here as soon as you add them. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Bad Gateway (502): Proxy/gateway error, service not reachable",
|
||||||
|
"endpoint_gateway_timeout_error": "Gateway Timeout (504): Gateway timeout, service not reachable",
|
||||||
|
"endpoint_internal_server_error": "Internal Server Error (500): The service encountered an unexpected error",
|
||||||
|
"endpoint_method_not_allowed_error": "Method Not Allowed (405): The endpoint exists, but doesn't accept POST requests",
|
||||||
|
"endpoint_not_found_error": "Not Found (404): The endpoint doesn't exist",
|
||||||
"endpoint_pinged": "Yay! We are able to ping the webhook!",
|
"endpoint_pinged": "Yay! We are able to ping the webhook!",
|
||||||
"endpoint_pinged_error": "Unable to ping the webhook!",
|
"endpoint_pinged_error": "Unable to ping the webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "Service Unavailable (503): Service is temporarily down",
|
||||||
"learn_to_verify": "Learn how to verify webhook signatures",
|
"learn_to_verify": "Learn how to verify webhook signatures",
|
||||||
|
"no_triggers": "No Triggers",
|
||||||
"please_check_console": "Please check the console for more details",
|
"please_check_console": "Please check the console for more details",
|
||||||
"please_enter_a_url": "Please enter a URL",
|
"please_enter_a_url": "Please enter a URL",
|
||||||
"response_created": "Response Created",
|
"response_created": "Response Created",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Enterprise Features",
|
"enterprise_features": "Enterprise Features",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Get an Enterprise license to get access to all features.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Get an Enterprise license to get access to all features.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Keep full control over your data privacy and security.",
|
"keep_full_control_over_your_data_privacy_and_security": "Keep full control over your data privacy and security.",
|
||||||
|
"license_feature_access_control": "Access control (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Audit logs",
|
||||||
|
"license_feature_contacts": "Contacts & Segments",
|
||||||
|
"license_feature_projects": "Workspaces",
|
||||||
|
"license_feature_quotas": "Quotas",
|
||||||
|
"license_feature_remove_branding": "Remove branding",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Spam protection",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Two-factor authentication",
|
||||||
|
"license_feature_whitelabel": "White-label emails",
|
||||||
|
"license_features_table_access": "Access",
|
||||||
|
"license_features_table_description": "Enterprise features and limits currently available to this instance.",
|
||||||
|
"license_features_table_disabled": "Disabled",
|
||||||
|
"license_features_table_enabled": "Enabled",
|
||||||
|
"license_features_table_feature": "Feature",
|
||||||
|
"license_features_table_title": "Licensed Features",
|
||||||
|
"license_features_table_unlimited": "Unlimited",
|
||||||
|
"license_features_table_value": "Value",
|
||||||
"license_instance_mismatch_description": "This license is currently bound to a different Formbricks instance. If this installation was rebuilt or moved, ask Formbricks support to disconnect the previous instance binding.",
|
"license_instance_mismatch_description": "This license is currently bound to a different Formbricks instance. If this installation was rebuilt or moved, ask Formbricks support to disconnect the previous instance binding.",
|
||||||
"license_invalid_description": "The license key in your ENTERPRISE_LICENSE_KEY environment variable is not valid. Please check for typos or request a new key.",
|
"license_invalid_description": "The license key in your ENTERPRISE_LICENSE_KEY environment variable is not valid. Please check for typos or request a new key.",
|
||||||
"license_status": "License Status",
|
"license_status": "License Status",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Custom hostname",
|
"custom_hostname": "Custom hostname",
|
||||||
"customize_survey_logo": "Customize the survey logo",
|
"customize_survey_logo": "Customize the survey logo",
|
||||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||||
"date_format": "Date format",
|
|
||||||
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
|
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
|
||||||
"delete_anyways": "Delete anyways",
|
"delete_anyways": "Delete anyways",
|
||||||
"delete_block": "Delete block",
|
"delete_block": "Delete block",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Error saving changes",
|
"error_saving_changes": "Error saving changes",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Allow multiple responses; continue showing even after a response (e.g., Feedback Box).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Allow multiple responses; continue showing even after a response (e.g., Feedback Box).",
|
||||||
"everyone": "Everyone",
|
"everyone": "Everyone",
|
||||||
|
"expand_preview": "Expand Preview",
|
||||||
"external_urls_paywall_tooltip": "Please upgrade to a paid plan to customize external URLs. This helps us prevent phishing.",
|
"external_urls_paywall_tooltip": "Please upgrade to a paid plan to customize external URLs. This helps us prevent phishing.",
|
||||||
"fallback_missing": "Fallback missing",
|
"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_logic_of_question_please_remove_it_from_logic_first": "{fieldId} is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||||
@@ -1503,6 +1532,28 @@
|
|||||||
"ignore_global_waiting_time": "Ignore Cooldown Period",
|
"ignore_global_waiting_time": "Ignore Cooldown Period",
|
||||||
"ignore_global_waiting_time_description": "This survey can show whenever its conditions are met, even if another survey was shown recently.",
|
"ignore_global_waiting_time_description": "This survey can show whenever its conditions are met, even if another survey was shown recently.",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
|
"import_error_invalid_json": "Invalid JSON file",
|
||||||
|
"import_error_validation": "Survey validation failed",
|
||||||
|
"import_info_quotas": "Due to the complexity of quotas, they are not being imported. Please create them manually after import.",
|
||||||
|
"import_info_triggers": "Triggers will be automatically matched or created in your environment.",
|
||||||
|
"import_survey": "Import Survey",
|
||||||
|
"import_survey_description": "Import a survey from a JSON file",
|
||||||
|
"import_survey_error": "Failed to import survey",
|
||||||
|
"import_survey_errors": "Errors",
|
||||||
|
"import_survey_file_label": "Select JSON file",
|
||||||
|
"import_survey_import": "Import Survey",
|
||||||
|
"import_survey_name_label": "Survey Name",
|
||||||
|
"import_survey_new_id": "New Survey ID",
|
||||||
|
"import_survey_success": "Survey imported successfully",
|
||||||
|
"import_survey_upload": "Upload File",
|
||||||
|
"import_survey_validate": "Validating...",
|
||||||
|
"import_survey_warnings": "Warnings",
|
||||||
|
"import_warning_action_classes": "Action classes will be matched or created in the target environment.",
|
||||||
|
"import_warning_follow_ups": "Survey follow-ups require an enterprise plan and might be removed.",
|
||||||
|
"import_warning_images": "Images detected in survey. You'll need to re-upload images after import.",
|
||||||
|
"import_warning_multi_language": "Multi-language surveys require an enterprise plan and might be removed.",
|
||||||
|
"import_warning_recaptcha": "Spam protection requires an enterprise plan and might be disabled.",
|
||||||
|
"import_warning_segments": "Segment targeting cannot be imported. Configure targeting after import.",
|
||||||
"includes_all_of": "Includes all of",
|
"includes_all_of": "Includes all of",
|
||||||
"includes_one_of": "Includes one of",
|
"includes_one_of": "Includes one of",
|
||||||
"initial_value": "Initial value",
|
"initial_value": "Initial value",
|
||||||
@@ -1655,6 +1706,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "Response limit needs to exceed number of received responses ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "Response limit needs to exceed number of received responses ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Response limits, redirections and more.",
|
"response_limits_redirections_and_more": "Response limits, redirections and more.",
|
||||||
"response_options": "Response Options",
|
"response_options": "Response Options",
|
||||||
|
"reverse_order_occasionally": "Reverse order occasionally",
|
||||||
|
"reverse_order_occasionally_except_last": "Reverse order occasionally except last",
|
||||||
"roundness": "Roundness",
|
"roundness": "Roundness",
|
||||||
"roundness_description": "Controls how rounded corners are.",
|
"roundness_description": "Controls how rounded corners are.",
|
||||||
"row_used_in_logic_error": "This row is used in logic of question {questionIndex}. Please remove it from logic first.",
|
"row_used_in_logic_error": "This row is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||||
@@ -1683,6 +1736,7 @@
|
|||||||
"show_survey_maximum_of": "Show survey maximum of",
|
"show_survey_maximum_of": "Show survey maximum of",
|
||||||
"show_survey_to_users": "Show survey to % of users",
|
"show_survey_to_users": "Show survey to % of users",
|
||||||
"show_to_x_percentage_of_targeted_users": "Show to {percentage}% of targeted users",
|
"show_to_x_percentage_of_targeted_users": "Show to {percentage}% of targeted users",
|
||||||
|
"shrink_preview": "Shrink Preview",
|
||||||
"simple": "Simple",
|
"simple": "Simple",
|
||||||
"six_points": "6 points",
|
"six_points": "6 points",
|
||||||
"smiley": "Smiley",
|
"smiley": "Smiley",
|
||||||
@@ -1698,10 +1752,12 @@
|
|||||||
"styling_set_to_theme_styles": "Styling set to theme styles",
|
"styling_set_to_theme_styles": "Styling set to theme styles",
|
||||||
"subheading": "Subheading",
|
"subheading": "Subheading",
|
||||||
"subtract": "Subtract -",
|
"subtract": "Subtract -",
|
||||||
|
"survey_closed_message_heading_required": "Add a heading to the custom survey closed message.",
|
||||||
"survey_completed_heading": "Survey Completed",
|
"survey_completed_heading": "Survey Completed",
|
||||||
"survey_completed_subheading": "This free & open-source survey has been closed",
|
"survey_completed_subheading": "This free & open-source survey has been closed",
|
||||||
"survey_display_settings": "Survey Display Settings",
|
"survey_display_settings": "Survey Display Settings",
|
||||||
"survey_placement": "Survey Placement",
|
"survey_placement": "Survey Placement",
|
||||||
|
"survey_preview": "Survey Preview 👀",
|
||||||
"survey_styling": "Survey styling",
|
"survey_styling": "Survey styling",
|
||||||
"survey_trigger": "Survey Trigger",
|
"survey_trigger": "Survey Trigger",
|
||||||
"switch_multi_language_on_to_get_started": "Switch multi-language on to get started 👉",
|
"switch_multi_language_on_to_get_started": "Switch multi-language on to get started 👉",
|
||||||
@@ -1803,6 +1859,28 @@
|
|||||||
"complete_responses": "Complete responses",
|
"complete_responses": "Complete responses",
|
||||||
"partial_responses": "Partial responses"
|
"partial_responses": "Partial responses"
|
||||||
},
|
},
|
||||||
|
"import_error_invalid_json": "Invalid JSON file",
|
||||||
|
"import_error_validation": "Survey validation failed",
|
||||||
|
"import_info_quotas": "Due to the complexity of quotas, they are not being imported. Please create them manually after import.",
|
||||||
|
"import_info_triggers": "Triggers will be automatically matched or created in your environment.",
|
||||||
|
"import_survey": "Import Survey",
|
||||||
|
"import_survey_description": "Import a survey from a JSON file",
|
||||||
|
"import_survey_error": "Failed to import survey",
|
||||||
|
"import_survey_errors": "Errors",
|
||||||
|
"import_survey_file_label": "Select JSON file",
|
||||||
|
"import_survey_import": "Import Survey",
|
||||||
|
"import_survey_name_label": "Survey Name",
|
||||||
|
"import_survey_new_id": "New Survey ID",
|
||||||
|
"import_survey_success": "Survey imported successfully",
|
||||||
|
"import_survey_upload": "Upload File",
|
||||||
|
"import_survey_validate": "Validating...",
|
||||||
|
"import_survey_warnings": "Warnings",
|
||||||
|
"import_warning_action_classes": "Action classes will be matched or created in the target environment.",
|
||||||
|
"import_warning_follow_ups": "Survey follow-ups require an enterprise plan. Follow-ups will be removed.",
|
||||||
|
"import_warning_images": "Images detected in survey. You'll need to re-upload images after import.",
|
||||||
|
"import_warning_multi_language": "Multi-language surveys require an enterprise plan. Languages will be removed.",
|
||||||
|
"import_warning_recaptcha": "Spam protection requires an enterprise plan. reCAPTCHA will be disabled.",
|
||||||
|
"import_warning_segments": "Segment targeting cannot be imported. Configure targeting after import.",
|
||||||
"new_survey": "New Survey",
|
"new_survey": "New Survey",
|
||||||
"no_surveys_created_yet": "No surveys created yet",
|
"no_surveys_created_yet": "No surveys created yet",
|
||||||
"open_options": "Open options",
|
"open_options": "Open options",
|
||||||
@@ -3052,7 +3130,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "No, thank you!",
|
"preview_survey_question_2_choice_2_label": "No, thank you!",
|
||||||
"preview_survey_question_2_headline": "Want to stay in the loop?",
|
"preview_survey_question_2_headline": "Want to stay in the loop?",
|
||||||
"preview_survey_question_2_subheader": "This is an example description.",
|
"preview_survey_question_2_subheader": "This is an example description.",
|
||||||
"preview_survey_question_open_text_headline": "Anything else you'd like to share?",
|
"preview_survey_question_open_text_headline": "Anything else you would like to share?",
|
||||||
"preview_survey_question_open_text_placeholder": "Type your answer here…",
|
"preview_survey_question_open_text_placeholder": "Type your answer here…",
|
||||||
"preview_survey_question_open_text_subheader": "Your feedback helps us improve.",
|
"preview_survey_question_open_text_subheader": "Your feedback helps us improve.",
|
||||||
"preview_survey_welcome_card_headline": "Welcome!",
|
"preview_survey_welcome_card_headline": "Welcome!",
|
||||||
@@ -3307,7 +3385,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Thank you for sharing your workflow idea with us! We are currently designing this feature and your feedback will help us build exactly what you need.",
|
"coming_soon_description": "Thank you for sharing your workflow idea with us! We are currently designing this feature and your feedback will help us build exactly what you need.",
|
||||||
"coming_soon_title": "We are almost there!",
|
"coming_soon_title": "We are almost there!",
|
||||||
"follow_up_label": "Is there anything else you'd like to add?",
|
"follow_up_label": "Is there anything else you would like to add?",
|
||||||
"follow_up_placeholder": "What specific tasks would you like to automate? Any tools or integrations you would want included?",
|
"follow_up_placeholder": "What specific tasks would you like to automate? Any tools or integrations you would want included?",
|
||||||
"generate_button": "Generate workflow",
|
"generate_button": "Generate workflow",
|
||||||
"heading": "What workflow do you want to create?",
|
"heading": "What workflow do you want to create?",
|
||||||
|
|||||||
+45
-11
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Conectar",
|
"connect": "Conectar",
|
||||||
"connect_formbricks": "Conectar Formbricks",
|
"connect_formbricks": "Conectar Formbricks",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"contact": "Contacto",
|
||||||
"contacts": "Contactos",
|
"contacts": "Contactos",
|
||||||
"continue": "Continuar",
|
"continue": "Continuar",
|
||||||
"copied": "Copiado",
|
"copied": "Copiado",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copy_code": "Copiar código",
|
"copy_code": "Copiar código",
|
||||||
"copy_link": "Copiar enlace",
|
"copy_link": "Copiar enlace",
|
||||||
|
"copy_to_environment": "Copiar a {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
||||||
"count_members": "{count, plural, one {{count} miembro} other {{count} miembros}}",
|
"count_members": "{count, plural, one {{count} miembro} other {{count} miembros}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(copia {copyNumber})",
|
"duplicate_copy_number": "(copia {copyNumber})",
|
||||||
"e_commerce": "Comercio electrónico",
|
"e_commerce": "Comercio electrónico",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
|
"elements": "Elementos",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Tarjeta final",
|
"ending_card": "Tarjeta final",
|
||||||
"enter_url": "Introducir URL",
|
"enter_url": "Introducir URL",
|
||||||
"enterprise_license": "Licencia empresarial",
|
"enterprise_license": "Licencia empresarial",
|
||||||
"environment": "Entorno",
|
"environment": "Entorno",
|
||||||
"environment_not_found": "Entorno no encontrado",
|
|
||||||
"environment_notice": "Actualmente estás en el entorno {environment}.",
|
"environment_notice": "Actualmente estás en el entorno {environment}.",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_component_description": "Este recurso no existe o no tienes los derechos necesarios para acceder a él.",
|
"error_component_description": "Este recurso no existe o no tienes los derechos necesarios para acceder a él.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Encuestas inactivas",
|
"inactive_surveys": "Encuestas inactivas",
|
||||||
"integration": "integración",
|
"integration": "integración",
|
||||||
"integrations": "Integraciones",
|
"integrations": "Integraciones",
|
||||||
"invalid_date": "Fecha no válida",
|
"invalid_date_with_value": "Fecha no válida: {value}",
|
||||||
"invalid_file_name": "Nombre de archivo no válido, por favor renombre su archivo e inténtelo de nuevo",
|
"invalid_file_name": "Nombre de archivo no válido, por favor renombre su archivo e inténtelo de nuevo",
|
||||||
"invalid_file_type": "Tipo de archivo no válido",
|
"invalid_file_type": "Tipo de archivo no válido",
|
||||||
"invite": "Invitar",
|
"invite": "Invitar",
|
||||||
"invite_them": "Invítales",
|
"invite_them": "Invítales",
|
||||||
|
"javascript_required": "Se requiere JavaScript",
|
||||||
|
"javascript_required_description": "Formbricks requiere JavaScript para funcionar correctamente. Por favor, activa JavaScript en la configuración de tu navegador para continuar.",
|
||||||
"key": "Clave",
|
"key": "Clave",
|
||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Miembros",
|
"members": "Miembros",
|
||||||
"members_and_teams": "Miembros y equipos",
|
"members_and_teams": "Miembros y equipos",
|
||||||
|
"membership": "Membresía",
|
||||||
"membership_not_found": "Membresía no encontrada",
|
"membership_not_found": "Membresía no encontrada",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metadatos",
|
"metadata": "Metadatos",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona mejor en una pantalla más grande. Para gestionar o crear encuestas, cambia a otro dispositivo.",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona mejor en una pantalla más grande. Para gestionar o crear encuestas, cambia a otro dispositivo.",
|
||||||
"mobile_overlay_surveys_look_good": "No te preocupes – ¡tus encuestas se ven geniales en todos los dispositivos y tamaños de pantalla!",
|
"mobile_overlay_surveys_look_good": "No te preocupes – ¡tus encuestas se ven geniales en todos los dispositivos y tamaños de pantalla!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Nuevo",
|
"new": "Nuevo",
|
||||||
"new_version_available": "Formbricks {version} está aquí. ¡Actualiza ahora!",
|
"new_version_available": "Formbricks {version} está aquí. ¡Actualiza ahora!",
|
||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
|
"no_actions_found": "No se encontraron acciones",
|
||||||
"no_background_image_found": "No se encontró imagen de fondo.",
|
"no_background_image_found": "No se encontró imagen de fondo.",
|
||||||
"no_code": "Sin código",
|
"no_code": "Sin código",
|
||||||
"no_files_uploaded": "No se subieron archivos",
|
"no_files_uploaded": "No se subieron archivos",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "o",
|
"or": "o",
|
||||||
"organization": "Organización",
|
"organization": "Organización",
|
||||||
"organization_id": "ID de organización",
|
"organization_id": "ID de organización",
|
||||||
"organization_not_found": "Organización no encontrada",
|
|
||||||
"organization_settings": "Ajustes de la organización",
|
"organization_settings": "Ajustes de la organización",
|
||||||
"organization_teams_not_found": "Equipos de la organización no encontrados",
|
|
||||||
"other": "Otro",
|
"other": "Otro",
|
||||||
|
"other_filters": "Otros Filtros",
|
||||||
"others": "Otros",
|
"others": "Otros",
|
||||||
"overlay_color": "Color de superposición",
|
"overlay_color": "Color de superposición",
|
||||||
"overview": "Resumen",
|
"overview": "Resumen",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Por favor, selecciona al menos una encuesta",
|
"please_select_at_least_one_survey": "Por favor, selecciona al menos una encuesta",
|
||||||
"please_select_at_least_one_trigger": "Por favor, selecciona al menos un disparador",
|
"please_select_at_least_one_trigger": "Por favor, selecciona al menos un disparador",
|
||||||
"please_upgrade_your_plan": "Por favor, actualiza tu plan",
|
"please_upgrade_your_plan": "Por favor, actualiza tu plan",
|
||||||
|
"powered_by_formbricks": "Desarrollado por Formbricks",
|
||||||
"preview": "Vista previa",
|
"preview": "Vista previa",
|
||||||
"preview_survey": "Vista previa de la encuesta",
|
"preview_survey": "Vista previa de la encuesta",
|
||||||
"privacy": "Política de privacidad",
|
"privacy": "Política de privacidad",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Seleccionar",
|
"select": "Seleccionar",
|
||||||
"select_all": "Seleccionar todo",
|
"select_all": "Seleccionar todo",
|
||||||
"select_filter": "Seleccionar filtro",
|
"select_filter": "Seleccionar filtro",
|
||||||
|
"select_language": "Seleccionar idioma",
|
||||||
"select_survey": "Seleccionar encuesta",
|
"select_survey": "Seleccionar encuesta",
|
||||||
"select_teams": "Seleccionar equipos",
|
"select_teams": "Seleccionar equipos",
|
||||||
"selected": "Seleccionado",
|
"selected": "Seleccionado",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "ID de encuesta",
|
"survey_id": "ID de encuesta",
|
||||||
"survey_languages": "Idiomas de la encuesta",
|
"survey_languages": "Idiomas de la encuesta",
|
||||||
"survey_live": "Encuesta activa",
|
"survey_live": "Encuesta activa",
|
||||||
"survey_not_found": "Encuesta no encontrada",
|
|
||||||
"survey_paused": "Encuesta pausada.",
|
"survey_paused": "Encuesta pausada.",
|
||||||
"survey_type": "Tipo de encuesta",
|
"survey_type": "Tipo de encuesta",
|
||||||
"surveys": "Encuestas",
|
"surveys": "Encuestas",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Nombre del equipo",
|
"team_name": "Nombre del equipo",
|
||||||
"team_role": "Rol del equipo",
|
"team_role": "Rol del equipo",
|
||||||
"teams": "Equipos",
|
"teams": "Equipos",
|
||||||
"teams_not_found": "Equipos no encontrados",
|
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
"time": "Hora",
|
"time": "Hora",
|
||||||
"time_to_finish": "Tiempo para finalizar",
|
"time_to_finish": "Tiempo para finalizar",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Usuario",
|
"user": "Usuario",
|
||||||
"user_id": "ID de usuario",
|
"user_id": "ID de usuario",
|
||||||
"user_not_found": "Usuario no encontrado",
|
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_ids": "IDs de variables",
|
"variable_ids": "IDs de variables",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "semanas",
|
"weeks": "semanas",
|
||||||
"welcome_card": "Tarjeta de bienvenida",
|
"welcome_card": "Tarjeta de bienvenida",
|
||||||
"workflows": "Flujos de trabajo",
|
"workflows": "Flujos de trabajo",
|
||||||
|
"workspace": "Espacio de trabajo",
|
||||||
"workspace_configuration": "Configuración del proyecto",
|
"workspace_configuration": "Configuración del proyecto",
|
||||||
"workspace_created_successfully": "Proyecto creado correctamente",
|
"workspace_created_successfully": "Proyecto creado correctamente",
|
||||||
"workspace_creation_description": "Organiza las encuestas en proyectos para un mejor control de acceso.",
|
"workspace_creation_description": "Organiza las encuestas en proyectos para un mejor control de acceso.",
|
||||||
"workspace_id": "ID del proyecto",
|
"workspace_id": "ID del proyecto",
|
||||||
"workspace_name": "Nombre del proyecto",
|
"workspace_name": "Nombre del proyecto",
|
||||||
"workspace_name_placeholder": "p. ej. Formbricks",
|
"workspace_name_placeholder": "p. ej. Formbricks",
|
||||||
"workspace_not_found": "Proyecto no encontrado",
|
|
||||||
"workspace_permission_not_found": "Permiso del proyecto no encontrado",
|
|
||||||
"workspaces": "Proyectos",
|
"workspaces": "Proyectos",
|
||||||
"years": "años",
|
"years": "años",
|
||||||
"you": "Tú",
|
"you": "Tú",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Se creó el atributo nuevo “{key}” con el tipo “{dataType}”",
|
"attributes_msg_new_attribute_created": "Se creó el atributo nuevo “{key}” con el tipo “{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "El ID de usuario ya existe para este entorno y no se actualizó.",
|
"attributes_msg_userid_already_exists": "El ID de usuario ya existe para este entorno y no se actualizó.",
|
||||||
"contact_deleted_successfully": "Contacto eliminado correctamente",
|
"contact_deleted_successfully": "Contacto eliminado correctamente",
|
||||||
"contact_not_found": "No se ha encontrado dicho contacto",
|
|
||||||
"contacts_table_refresh": "Actualizar contactos",
|
"contacts_table_refresh": "Actualizar contactos",
|
||||||
"contacts_table_refresh_success": "Contactos actualizados correctamente",
|
"contacts_table_refresh_success": "Contactos actualizados correctamente",
|
||||||
"create_attribute": "Crear atributo",
|
"create_attribute": "Crear atributo",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Creado por un tercero",
|
"created_by_third_party": "Creado por un tercero",
|
||||||
"discord_webhook_not_supported": "Los webhooks de Discord no son compatibles actualmente.",
|
"discord_webhook_not_supported": "Los webhooks de Discord no son compatibles actualmente.",
|
||||||
"empty_webhook_message": "Tus webhooks aparecerán aquí tan pronto como los añadas. ⏲️",
|
"empty_webhook_message": "Tus webhooks aparecerán aquí tan pronto como los añadas. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Puerta de enlace incorrecta (502): Error de proxy o puerta de enlace, servicio no accesible",
|
||||||
|
"endpoint_gateway_timeout_error": "Tiempo de espera de la puerta de enlace agotado (504): Tiempo de espera de la puerta de enlace agotado, servicio no accesible",
|
||||||
|
"endpoint_internal_server_error": "Error interno del servidor (500): El servicio encontró un error inesperado",
|
||||||
|
"endpoint_method_not_allowed_error": "Método no permitido (405): El endpoint existe, pero no acepta solicitudes POST",
|
||||||
|
"endpoint_not_found_error": "No encontrado (404): El endpoint no existe",
|
||||||
"endpoint_pinged": "¡Genial! ¡Podemos hacer ping al webhook!",
|
"endpoint_pinged": "¡Genial! ¡Podemos hacer ping al webhook!",
|
||||||
"endpoint_pinged_error": "¡No se puede hacer ping al webhook!",
|
"endpoint_pinged_error": "¡No se puede hacer ping al webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "Servicio no disponible (503): El servicio está temporalmente caído",
|
||||||
"learn_to_verify": "Aprende a verificar las firmas de webhook",
|
"learn_to_verify": "Aprende a verificar las firmas de webhook",
|
||||||
|
"no_triggers": "Sin activadores",
|
||||||
"please_check_console": "Por favor, consulta la consola para más detalles",
|
"please_check_console": "Por favor, consulta la consola para más detalles",
|
||||||
"please_enter_a_url": "Por favor, introduce una URL",
|
"please_enter_a_url": "Por favor, introduce una URL",
|
||||||
"response_created": "Respuesta creada",
|
"response_created": "Respuesta creada",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Características empresariales",
|
"enterprise_features": "Características empresariales",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obtén una licencia empresarial para acceder a todas las características.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Obtén una licencia empresarial para acceder a todas las características.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Mantén el control total sobre la privacidad y seguridad de tus datos.",
|
"keep_full_control_over_your_data_privacy_and_security": "Mantén el control total sobre la privacidad y seguridad de tus datos.",
|
||||||
|
"license_feature_access_control": "Control de acceso (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Registros de auditoría",
|
||||||
|
"license_feature_contacts": "Contactos y segmentos",
|
||||||
|
"license_feature_projects": "Espacios de trabajo",
|
||||||
|
"license_feature_quotas": "Cuotas",
|
||||||
|
"license_feature_remove_branding": "Eliminar marca",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Protección contra spam",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Autenticación de dos factores",
|
||||||
|
"license_feature_whitelabel": "Correos sin marca",
|
||||||
|
"license_features_table_access": "Acceso",
|
||||||
|
"license_features_table_description": "Funciones y límites empresariales disponibles actualmente para esta instancia.",
|
||||||
|
"license_features_table_disabled": "Desactivado",
|
||||||
|
"license_features_table_enabled": "Activado",
|
||||||
|
"license_features_table_feature": "Función",
|
||||||
|
"license_features_table_title": "Funciones con licencia",
|
||||||
|
"license_features_table_unlimited": "Ilimitado",
|
||||||
|
"license_features_table_value": "Valor",
|
||||||
"license_instance_mismatch_description": "Esta licencia está actualmente vinculada a una instancia diferente de Formbricks. Si esta instalación fue reconstruida o migrada, solicita al soporte de Formbricks que desconecte la vinculación de la instancia anterior.",
|
"license_instance_mismatch_description": "Esta licencia está actualmente vinculada a una instancia diferente de Formbricks. Si esta instalación fue reconstruida o migrada, solicita al soporte de Formbricks que desconecte la vinculación de la instancia anterior.",
|
||||||
"license_invalid_description": "La clave de licencia en tu variable de entorno ENTERPRISE_LICENSE_KEY no es válida. Por favor, comprueba si hay errores tipográficos o solicita una clave nueva.",
|
"license_invalid_description": "La clave de licencia en tu variable de entorno ENTERPRISE_LICENSE_KEY no es válida. Por favor, comprueba si hay errores tipográficos o solicita una clave nueva.",
|
||||||
"license_status": "Estado de la licencia",
|
"license_status": "Estado de la licencia",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Nombre de host personalizado",
|
"custom_hostname": "Nombre de host personalizado",
|
||||||
"customize_survey_logo": "Personalizar el logotipo de la encuesta",
|
"customize_survey_logo": "Personalizar el logotipo de la encuesta",
|
||||||
"darken_or_lighten_background_of_your_choice": "Oscurece o aclara el fondo de tu elección.",
|
"darken_or_lighten_background_of_your_choice": "Oscurece o aclara el fondo de tu elección.",
|
||||||
"date_format": "Formato de fecha",
|
|
||||||
"days_before_showing_this_survey_again": "o más días deben transcurrir entre la última encuesta mostrada y la visualización de esta encuesta.",
|
"days_before_showing_this_survey_again": "o más días deben transcurrir entre la última encuesta mostrada y la visualización de esta encuesta.",
|
||||||
"delete_anyways": "Eliminar de todos modos",
|
"delete_anyways": "Eliminar de todos modos",
|
||||||
"delete_block": "Eliminar bloque",
|
"delete_block": "Eliminar bloque",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Error al guardar los cambios",
|
"error_saving_changes": "Error al guardar los cambios",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir respuestas múltiples; seguir mostrando incluso después de una respuesta (p. ej., cuadro de comentarios).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir respuestas múltiples; seguir mostrando incluso después de una respuesta (p. ej., cuadro de comentarios).",
|
||||||
"everyone": "Todos",
|
"everyone": "Todos",
|
||||||
|
"expand_preview": "Expandir vista previa",
|
||||||
"external_urls_paywall_tooltip": "Por favor, actualiza a un plan de pago para personalizar URLs externas. Esto nos ayuda a prevenir el phishing.",
|
"external_urls_paywall_tooltip": "Por favor, actualiza a un plan de pago para personalizar URLs externas. Esto nos ayuda a prevenir el phishing.",
|
||||||
"fallback_missing": "Falta respaldo",
|
"fallback_missing": "Falta respaldo",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} se usa en la lógica de la pregunta {questionIndex}. Por favor, elimínalo primero de la lógica.",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} se usa en la lógica de la pregunta {questionIndex}. Por favor, elimínalo primero de la lógica.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "El límite de respuestas debe superar el número de respuestas recibidas ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "El límite de respuestas debe superar el número de respuestas recibidas ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Límites de respuestas, redirecciones y más.",
|
"response_limits_redirections_and_more": "Límites de respuestas, redirecciones y más.",
|
||||||
"response_options": "Opciones de respuesta",
|
"response_options": "Opciones de respuesta",
|
||||||
|
"reverse_order_occasionally": "Invertir orden ocasionalmente",
|
||||||
|
"reverse_order_occasionally_except_last": "Invertir orden ocasionalmente excepto el último",
|
||||||
"roundness": "Redondez",
|
"roundness": "Redondez",
|
||||||
"roundness_description": "Controla qué tan redondeadas están las esquinas.",
|
"roundness_description": "Controla qué tan redondeadas están las esquinas.",
|
||||||
"row_used_in_logic_error": "Esta fila se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
"row_used_in_logic_error": "Esta fila se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Mostrar encuesta un máximo de",
|
"show_survey_maximum_of": "Mostrar encuesta un máximo de",
|
||||||
"show_survey_to_users": "Mostrar encuesta al % de usuarios",
|
"show_survey_to_users": "Mostrar encuesta al % de usuarios",
|
||||||
"show_to_x_percentage_of_targeted_users": "Mostrar al {percentage} % de usuarios objetivo",
|
"show_to_x_percentage_of_targeted_users": "Mostrar al {percentage} % de usuarios objetivo",
|
||||||
|
"shrink_preview": "Contraer vista previa",
|
||||||
"simple": "Simple",
|
"simple": "Simple",
|
||||||
"six_points": "6 puntos",
|
"six_points": "6 puntos",
|
||||||
"smiley": "Emoticono",
|
"smiley": "Emoticono",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Estilo configurado según los estilos del tema",
|
"styling_set_to_theme_styles": "Estilo configurado según los estilos del tema",
|
||||||
"subheading": "Subtítulo",
|
"subheading": "Subtítulo",
|
||||||
"subtract": "Restar -",
|
"subtract": "Restar -",
|
||||||
|
"survey_closed_message_heading_required": "Añade un encabezado al mensaje personalizado de encuesta cerrada.",
|
||||||
"survey_completed_heading": "Encuesta completada",
|
"survey_completed_heading": "Encuesta completada",
|
||||||
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
|
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
|
||||||
"survey_display_settings": "Ajustes de visualización de la encuesta",
|
"survey_display_settings": "Ajustes de visualización de la encuesta",
|
||||||
"survey_placement": "Ubicación de la encuesta",
|
"survey_placement": "Ubicación de la encuesta",
|
||||||
|
"survey_preview": "Vista previa de la encuesta 👀",
|
||||||
"survey_styling": "Estilo del formulario",
|
"survey_styling": "Estilo del formulario",
|
||||||
"survey_trigger": "Activador de la encuesta",
|
"survey_trigger": "Activador de la encuesta",
|
||||||
"switch_multi_language_on_to_get_started": "Activa el modo multiidioma para comenzar 👉",
|
"switch_multi_language_on_to_get_started": "Activa el modo multiidioma para comenzar 👉",
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Connecter",
|
"connect": "Connecter",
|
||||||
"connect_formbricks": "Connecter Formbricks",
|
"connect_formbricks": "Connecter Formbricks",
|
||||||
"connected": "Connecté",
|
"connected": "Connecté",
|
||||||
|
"contact": "Contact",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"continue": "Continuer",
|
"continue": "Continuer",
|
||||||
"copied": "Copié",
|
"copied": "Copié",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Copier",
|
"copy": "Copier",
|
||||||
"copy_code": "Copier le code",
|
"copy_code": "Copier le code",
|
||||||
"copy_link": "Copier le lien",
|
"copy_link": "Copier le lien",
|
||||||
|
"copy_to_environment": "Copier vers {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attributs}}",
|
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attributs}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
||||||
"count_members": "{count, plural, one {{count} membre} other {{count} membres}}",
|
"count_members": "{count, plural, one {{count} membre} other {{count} membres}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(copie {copyNumber})",
|
"duplicate_copy_number": "(copie {copyNumber})",
|
||||||
"e_commerce": "E-commerce",
|
"e_commerce": "E-commerce",
|
||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
|
"elements": "Éléments",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Carte de fin",
|
"ending_card": "Carte de fin",
|
||||||
"enter_url": "Saisir l'URL",
|
"enter_url": "Saisir l'URL",
|
||||||
"enterprise_license": "Licence d'entreprise",
|
"enterprise_license": "Licence d'entreprise",
|
||||||
"environment": "Environnement",
|
"environment": "Environnement",
|
||||||
"environment_not_found": "Environnement non trouvé",
|
|
||||||
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
|
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
|
||||||
"error": "Erreur",
|
"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_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Sondages inactifs",
|
"inactive_surveys": "Sondages inactifs",
|
||||||
"integration": "intégration",
|
"integration": "intégration",
|
||||||
"integrations": "Intégrations",
|
"integrations": "Intégrations",
|
||||||
"invalid_date": "Date invalide",
|
"invalid_date_with_value": "Date invalide: {value}",
|
||||||
"invalid_file_name": "Nom de fichier invalide, veuillez renommer votre fichier et réessayer",
|
"invalid_file_name": "Nom de fichier invalide, veuillez renommer votre fichier et réessayer",
|
||||||
"invalid_file_type": "Type de fichier invalide",
|
"invalid_file_type": "Type de fichier invalide",
|
||||||
"invite": "Inviter",
|
"invite": "Inviter",
|
||||||
"invite_them": "Invitez-les",
|
"invite_them": "Invitez-les",
|
||||||
|
"javascript_required": "JavaScript requis",
|
||||||
|
"javascript_required_description": "Formbricks nécessite JavaScript pour fonctionner correctement. Veuillez activer JavaScript dans les paramètres de votre navigateur pour continuer.",
|
||||||
"key": "Clé",
|
"key": "Clé",
|
||||||
"label": "Étiquette",
|
"label": "Étiquette",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Membres",
|
"members": "Membres",
|
||||||
"members_and_teams": "Membres & Équipes",
|
"members_and_teams": "Membres & Équipes",
|
||||||
|
"membership": "Adhésion",
|
||||||
"membership_not_found": "Abonnement non trouvé",
|
"membership_not_found": "Abonnement non trouvé",
|
||||||
|
"meta": "Méta",
|
||||||
"metadata": "Métadonnées",
|
"metadata": "Métadonnées",
|
||||||
"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_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_surveys_look_good": "Ne t'inquiète pas – tes enquêtes sont superbes sur tous les appareils et tailles d'écran!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Nouveau",
|
"new": "Nouveau",
|
||||||
"new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !",
|
"new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !",
|
||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
|
"no_actions_found": "Aucune action trouvée",
|
||||||
"no_background_image_found": "Aucune image de fond trouvée.",
|
"no_background_image_found": "Aucune image de fond trouvée.",
|
||||||
"no_code": "Sans code",
|
"no_code": "Sans code",
|
||||||
"no_files_uploaded": "Aucun fichier n'a été téléchargé.",
|
"no_files_uploaded": "Aucun fichier n'a été téléchargé.",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "ou",
|
"or": "ou",
|
||||||
"organization": "Organisation",
|
"organization": "Organisation",
|
||||||
"organization_id": "Identifiant de l'organisation",
|
"organization_id": "Identifiant de l'organisation",
|
||||||
"organization_not_found": "Organisation non trouvée",
|
|
||||||
"organization_settings": "Paramètres de l'organisation",
|
"organization_settings": "Paramètres de l'organisation",
|
||||||
"organization_teams_not_found": "Équipes d'organisation non trouvées",
|
|
||||||
"other": "Autre",
|
"other": "Autre",
|
||||||
|
"other_filters": "Autres filtres",
|
||||||
"others": "Autres",
|
"others": "Autres",
|
||||||
"overlay_color": "Couleur de superposition",
|
"overlay_color": "Couleur de superposition",
|
||||||
"overview": "Aperçu",
|
"overview": "Aperçu",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Veuillez sélectionner au moins une enquête.",
|
"please_select_at_least_one_survey": "Veuillez sélectionner au moins une enquête.",
|
||||||
"please_select_at_least_one_trigger": "Veuillez sélectionner au moins un déclencheur.",
|
"please_select_at_least_one_trigger": "Veuillez sélectionner au moins un déclencheur.",
|
||||||
"please_upgrade_your_plan": "Veuillez mettre à niveau votre plan",
|
"please_upgrade_your_plan": "Veuillez mettre à niveau votre plan",
|
||||||
|
"powered_by_formbricks": "Propulsé par Formbricks",
|
||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"preview_survey": "Aperçu de l'enquête",
|
"preview_survey": "Aperçu de l'enquête",
|
||||||
"privacy": "Politique de confidentialité",
|
"privacy": "Politique de confidentialité",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Sélectionner",
|
"select": "Sélectionner",
|
||||||
"select_all": "Sélectionner tout",
|
"select_all": "Sélectionner tout",
|
||||||
"select_filter": "Sélectionner un filtre",
|
"select_filter": "Sélectionner un filtre",
|
||||||
|
"select_language": "Sélectionner la langue",
|
||||||
"select_survey": "Sélectionner l'enquête",
|
"select_survey": "Sélectionner l'enquête",
|
||||||
"select_teams": "Sélectionner les équipes",
|
"select_teams": "Sélectionner les équipes",
|
||||||
"selected": "Sélectionné",
|
"selected": "Sélectionné",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "ID de l'enquête",
|
"survey_id": "ID de l'enquête",
|
||||||
"survey_languages": "Langues de l'enquête",
|
"survey_languages": "Langues de l'enquête",
|
||||||
"survey_live": "Sondage en direct",
|
"survey_live": "Sondage en direct",
|
||||||
"survey_not_found": "Sondage non trouvé",
|
|
||||||
"survey_paused": "Sondage en pause.",
|
"survey_paused": "Sondage en pause.",
|
||||||
"survey_type": "Type de sondage",
|
"survey_type": "Type de sondage",
|
||||||
"surveys": "Enquêtes",
|
"surveys": "Enquêtes",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Nom de l'équipe",
|
"team_name": "Nom de l'équipe",
|
||||||
"team_role": "Rôle dans l'équipe",
|
"team_role": "Rôle dans l'équipe",
|
||||||
"teams": "Équipes",
|
"teams": "Équipes",
|
||||||
"teams_not_found": "Équipes non trouvées",
|
|
||||||
"text": "Texte",
|
"text": "Texte",
|
||||||
"time": "Temps",
|
"time": "Temps",
|
||||||
"time_to_finish": "Temps de finir",
|
"time_to_finish": "Temps de finir",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Utilisateur",
|
"user": "Utilisateur",
|
||||||
"user_id": "Identifiant d'utilisateur",
|
"user_id": "Identifiant d'utilisateur",
|
||||||
"user_not_found": "Utilisateur non trouvé",
|
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_ids": "Identifiants variables",
|
"variable_ids": "Identifiants variables",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "semaines",
|
"weeks": "semaines",
|
||||||
"welcome_card": "Carte de bienvenue",
|
"welcome_card": "Carte de bienvenue",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
"workspace": "Espace de travail",
|
||||||
"workspace_configuration": "Configuration du projet",
|
"workspace_configuration": "Configuration du projet",
|
||||||
"workspace_created_successfully": "Projet créé avec succès",
|
"workspace_created_successfully": "Projet créé avec succès",
|
||||||
"workspace_creation_description": "Organisez les enquêtes dans des projets pour un meilleur contrôle d'accès.",
|
"workspace_creation_description": "Organisez les enquêtes dans des projets pour un meilleur contrôle d'accès.",
|
||||||
"workspace_id": "ID du projet",
|
"workspace_id": "ID du projet",
|
||||||
"workspace_name": "Nom du projet",
|
"workspace_name": "Nom du projet",
|
||||||
"workspace_name_placeholder": "par ex. Formbricks",
|
"workspace_name_placeholder": "par ex. Formbricks",
|
||||||
"workspace_not_found": "Projet introuvable",
|
|
||||||
"workspace_permission_not_found": "Permission du projet introuvable",
|
|
||||||
"workspaces": "Projets",
|
"workspaces": "Projets",
|
||||||
"years": "années",
|
"years": "années",
|
||||||
"you": "Vous",
|
"you": "Vous",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Nouvel attribut “{key}” créé avec le type “{dataType}”",
|
"attributes_msg_new_attribute_created": "Nouvel attribut “{key}” créé avec le type “{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "L'identifiant utilisateur existe déjà pour cet environnement et n'a pas été mis à jour.",
|
"attributes_msg_userid_already_exists": "L'identifiant utilisateur existe déjà pour cet environnement et n'a pas été mis à jour.",
|
||||||
"contact_deleted_successfully": "Contact supprimé avec succès",
|
"contact_deleted_successfully": "Contact supprimé avec succès",
|
||||||
"contact_not_found": "Aucun contact trouvé",
|
|
||||||
"contacts_table_refresh": "Actualiser les contacts",
|
"contacts_table_refresh": "Actualiser les contacts",
|
||||||
"contacts_table_refresh_success": "Contacts rafraîchis avec succès",
|
"contacts_table_refresh_success": "Contacts rafraîchis avec succès",
|
||||||
"create_attribute": "Créer un attribut",
|
"create_attribute": "Créer un attribut",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Créé par un tiers",
|
"created_by_third_party": "Créé par un tiers",
|
||||||
"discord_webhook_not_supported": "Les webhooks Discord ne sont actuellement pas pris en charge.",
|
"discord_webhook_not_supported": "Les webhooks Discord ne sont actuellement pas pris en charge.",
|
||||||
"empty_webhook_message": "Vos webhooks apparaîtront ici dès que vous les ajouterez. ⏲️",
|
"empty_webhook_message": "Vos webhooks apparaîtront ici dès que vous les ajouterez. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Mauvaise passerelle (502) : Erreur de proxy/passerelle, service inaccessible",
|
||||||
|
"endpoint_gateway_timeout_error": "Délai d'attente de la passerelle dépassé (504) : Le délai d'attente de la passerelle a expiré, service inaccessible",
|
||||||
|
"endpoint_internal_server_error": "Erreur interne du serveur (500) : Le service a rencontré une erreur inattendue",
|
||||||
|
"endpoint_method_not_allowed_error": "Méthode non autorisée (405) : Le point de terminaison existe, mais n'accepte pas les requêtes POST",
|
||||||
|
"endpoint_not_found_error": "Introuvable (404) : Le point de terminaison n'existe pas",
|
||||||
"endpoint_pinged": "Yay ! Nous pouvons pinger le webhook !",
|
"endpoint_pinged": "Yay ! Nous pouvons pinger le webhook !",
|
||||||
"endpoint_pinged_error": "Impossible de pinger le webhook !",
|
"endpoint_pinged_error": "Impossible de pinger le webhook !",
|
||||||
|
"endpoint_service_unavailable_error": "Service indisponible (503) : Le service est temporairement indisponible",
|
||||||
"learn_to_verify": "Découvrez comment vérifier les signatures de webhook",
|
"learn_to_verify": "Découvrez comment vérifier les signatures de webhook",
|
||||||
|
"no_triggers": "Aucun déclencheur",
|
||||||
"please_check_console": "Veuillez vérifier la console pour plus de détails.",
|
"please_check_console": "Veuillez vérifier la console pour plus de détails.",
|
||||||
"please_enter_a_url": "Veuillez entrer une URL.",
|
"please_enter_a_url": "Veuillez entrer une URL.",
|
||||||
"response_created": "Réponse créée",
|
"response_created": "Réponse créée",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Fonctionnalités d'entreprise",
|
"enterprise_features": "Fonctionnalités d'entreprise",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenez une licence Entreprise pour accéder à toutes les fonctionnalités.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenez une licence Entreprise pour accéder à toutes les fonctionnalités.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Gardez un contrôle total sur la confidentialité et la sécurité de vos données.",
|
"keep_full_control_over_your_data_privacy_and_security": "Gardez un contrôle total sur la confidentialité et la sécurité de vos données.",
|
||||||
|
"license_feature_access_control": "Contrôle d'accès (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Journaux d'audit",
|
||||||
|
"license_feature_contacts": "Contacts et segments",
|
||||||
|
"license_feature_projects": "Espaces de travail",
|
||||||
|
"license_feature_quotas": "Quotas",
|
||||||
|
"license_feature_remove_branding": "Retirer l'image de marque",
|
||||||
|
"license_feature_saml": "SSO SAML",
|
||||||
|
"license_feature_spam_protection": "Protection anti-spam",
|
||||||
|
"license_feature_sso": "SSO OIDC",
|
||||||
|
"license_feature_two_factor_auth": "Authentification à deux facteurs",
|
||||||
|
"license_feature_whitelabel": "E-mails en marque blanche",
|
||||||
|
"license_features_table_access": "Accès",
|
||||||
|
"license_features_table_description": "Fonctionnalités Enterprise et limites actuellement disponibles pour cette instance.",
|
||||||
|
"license_features_table_disabled": "Désactivé",
|
||||||
|
"license_features_table_enabled": "Activé",
|
||||||
|
"license_features_table_feature": "Fonctionnalité",
|
||||||
|
"license_features_table_title": "Fonctionnalités sous licence",
|
||||||
|
"license_features_table_unlimited": "Illimité",
|
||||||
|
"license_features_table_value": "Valeur",
|
||||||
"license_instance_mismatch_description": "Cette licence est actuellement liée à une autre instance Formbricks. Si cette installation a été reconstruite ou déplacée, demande au support Formbricks de déconnecter la liaison de l'instance précédente.",
|
"license_instance_mismatch_description": "Cette licence est actuellement liée à une autre instance Formbricks. Si cette installation a été reconstruite ou déplacée, demande au support Formbricks de déconnecter la liaison de l'instance précédente.",
|
||||||
"license_invalid_description": "La clé de licence dans votre variable d'environnement ENTERPRISE_LICENSE_KEY n'est pas valide. Veuillez vérifier les fautes de frappe ou demander une nouvelle clé.",
|
"license_invalid_description": "La clé de licence dans votre variable d'environnement ENTERPRISE_LICENSE_KEY n'est pas valide. Veuillez vérifier les fautes de frappe ou demander une nouvelle clé.",
|
||||||
"license_status": "Statut de la licence",
|
"license_status": "Statut de la licence",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Nom d'hôte personnalisé",
|
"custom_hostname": "Nom d'hôte personnalisé",
|
||||||
"customize_survey_logo": "Personnaliser le logo de l'enquête",
|
"customize_survey_logo": "Personnaliser le logo de l'enquête",
|
||||||
"darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.",
|
"darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.",
|
||||||
"date_format": "Format de date",
|
|
||||||
"days_before_showing_this_survey_again": "ou plus de jours doivent s'écouler entre le dernier sondage affiché et l'affichage de ce sondage.",
|
"days_before_showing_this_survey_again": "ou plus de jours doivent s'écouler entre le dernier sondage affiché et l'affichage de ce sondage.",
|
||||||
"delete_anyways": "Supprimer quand même",
|
"delete_anyways": "Supprimer quand même",
|
||||||
"delete_block": "Supprimer le bloc",
|
"delete_block": "Supprimer le bloc",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Erreur lors de l'enregistrement des modifications",
|
"error_saving_changes": "Erreur lors de l'enregistrement des modifications",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Autoriser plusieurs réponses ; continuer à afficher même après une réponse (par exemple, boîte de commentaires).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Autoriser plusieurs réponses ; continuer à afficher même après une réponse (par exemple, boîte de commentaires).",
|
||||||
"everyone": "Tout le monde",
|
"everyone": "Tout le monde",
|
||||||
|
"expand_preview": "Agrandir l'aperçu",
|
||||||
"external_urls_paywall_tooltip": "Merci de passer à une offre payante pour personnaliser les URLs externes. Cela nous aide à empêcher l’hameçonnage.",
|
"external_urls_paywall_tooltip": "Merci de passer à une offre payante pour personnaliser les URLs externes. Cela nous aide à empêcher l’hameçonnage.",
|
||||||
"fallback_missing": "Fallback manquant",
|
"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_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.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "La limite de réponses doit dépasser le nombre de réponses reçues ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "La limite de réponses doit dépasser le nombre de réponses reçues ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Limites de réponse, redirections et plus.",
|
"response_limits_redirections_and_more": "Limites de réponse, redirections et plus.",
|
||||||
"response_options": "Options de réponse",
|
"response_options": "Options de réponse",
|
||||||
|
"reverse_order_occasionally": "Inverser l'ordre occasionnellement",
|
||||||
|
"reverse_order_occasionally_except_last": "Inverser l'ordre occasionnellement sauf le dernier",
|
||||||
"roundness": "Rondeur",
|
"roundness": "Rondeur",
|
||||||
"roundness_description": "Contrôle l'arrondi des coins.",
|
"roundness_description": "Contrôle l'arrondi des coins.",
|
||||||
"row_used_in_logic_error": "Cette ligne est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
"row_used_in_logic_error": "Cette ligne est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Afficher le maximum du sondage de",
|
"show_survey_maximum_of": "Afficher le maximum du sondage de",
|
||||||
"show_survey_to_users": "Afficher l'enquête à % des utilisateurs",
|
"show_survey_to_users": "Afficher l'enquête à % des utilisateurs",
|
||||||
"show_to_x_percentage_of_targeted_users": "Afficher à {percentage}% des utilisateurs ciblés",
|
"show_to_x_percentage_of_targeted_users": "Afficher à {percentage}% des utilisateurs ciblés",
|
||||||
|
"shrink_preview": "Réduire l'aperçu",
|
||||||
"simple": "Simple",
|
"simple": "Simple",
|
||||||
"six_points": "6 points",
|
"six_points": "6 points",
|
||||||
"smiley": "Sourire",
|
"smiley": "Sourire",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Style défini sur les styles du thème",
|
"styling_set_to_theme_styles": "Style défini sur les styles du thème",
|
||||||
"subheading": "Sous-titre",
|
"subheading": "Sous-titre",
|
||||||
"subtract": "Soustraire -",
|
"subtract": "Soustraire -",
|
||||||
|
"survey_closed_message_heading_required": "Ajoute un titre au message personnalisé de sondage fermé.",
|
||||||
"survey_completed_heading": "Enquête terminée",
|
"survey_completed_heading": "Enquête terminée",
|
||||||
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
|
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
|
||||||
"survey_display_settings": "Paramètres d'affichage de l'enquête",
|
"survey_display_settings": "Paramètres d'affichage de l'enquête",
|
||||||
"survey_placement": "Placement de l'enquête",
|
"survey_placement": "Placement de l'enquête",
|
||||||
|
"survey_preview": "Aperçu du sondage 👀",
|
||||||
"survey_styling": "Style de formulaire",
|
"survey_styling": "Style de formulaire",
|
||||||
"survey_trigger": "Déclencheur d'enquête",
|
"survey_trigger": "Déclencheur d'enquête",
|
||||||
"switch_multi_language_on_to_get_started": "Activez le mode multilingue pour commencer 👉",
|
"switch_multi_language_on_to_get_started": "Activez le mode multilingue pour commencer 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Non, merci !",
|
"preview_survey_question_2_choice_2_label": "Non, merci !",
|
||||||
"preview_survey_question_2_headline": "Souhaitez-vous être informé ?",
|
"preview_survey_question_2_headline": "Souhaitez-vous être informé ?",
|
||||||
"preview_survey_question_2_subheader": "Ceci est un exemple de description.",
|
"preview_survey_question_2_subheader": "Ceci est un exemple de description.",
|
||||||
"preview_survey_question_open_text_headline": "Autre chose que vous aimeriez partager ?",
|
"preview_survey_question_open_text_headline": "Souhaitez-vous partager autre chose ?",
|
||||||
"preview_survey_question_open_text_placeholder": "Entrez votre réponse ici...",
|
"preview_survey_question_open_text_placeholder": "Entrez votre réponse ici...",
|
||||||
"preview_survey_question_open_text_subheader": "Vos commentaires nous aident à nous améliorer.",
|
"preview_survey_question_open_text_subheader": "Vos commentaires nous aident à nous améliorer.",
|
||||||
"preview_survey_welcome_card_headline": "Bienvenue !",
|
"preview_survey_welcome_card_headline": "Bienvenue !",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Merci d'avoir partagé votre idée de workflow avec nous ! Nous concevons actuellement cette fonctionnalité et vos retours nous aideront à créer exactement ce dont vous avez besoin.",
|
"coming_soon_description": "Merci d'avoir partagé votre idée de workflow avec nous ! Nous concevons actuellement cette fonctionnalité et vos retours nous aideront à créer exactement ce dont vous avez besoin.",
|
||||||
"coming_soon_title": "Nous y sommes presque !",
|
"coming_soon_title": "Nous y sommes presque !",
|
||||||
"follow_up_label": "Y a-t-il autre chose que vous aimeriez ajouter ?",
|
"follow_up_label": "Souhaitez-vous ajouter quelque chose ?",
|
||||||
"follow_up_placeholder": "Quelles tâches spécifiques souhaitez-vous automatiser ? Y a-t-il des outils ou intégrations que vous aimeriez inclure ?",
|
"follow_up_placeholder": "Quelles tâches spécifiques souhaitez-vous automatiser ? Y a-t-il des outils ou intégrations que vous aimeriez inclure ?",
|
||||||
"generate_button": "Générer le workflow",
|
"generate_button": "Générer le workflow",
|
||||||
"heading": "Quel workflow souhaitez-vous créer ?",
|
"heading": "Quel workflow souhaitez-vous créer ?",
|
||||||
|
|||||||
+127
-93
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Kapcsolódás",
|
"connect": "Kapcsolódás",
|
||||||
"connect_formbricks": "Kapcsolódás a Formbrickshez",
|
"connect_formbricks": "Kapcsolódás a Formbrickshez",
|
||||||
"connected": "Kapcsolódva",
|
"connected": "Kapcsolódva",
|
||||||
|
"contact": "Kapcsolat",
|
||||||
"contacts": "Partnerek",
|
"contacts": "Partnerek",
|
||||||
"continue": "Folytatás",
|
"continue": "Folytatás",
|
||||||
"copied": "Másolva",
|
"copied": "Másolva",
|
||||||
@@ -174,8 +175,9 @@
|
|||||||
"copy": "Másolás",
|
"copy": "Másolás",
|
||||||
"copy_code": "Kód másolása",
|
"copy_code": "Kód másolása",
|
||||||
"copy_link": "Hivatkozás másolása",
|
"copy_link": "Hivatkozás másolása",
|
||||||
|
"copy_to_environment": "Másolás ide: {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} attribútum} other {{count} attribútum}}",
|
"count_attributes": "{count, plural, one {{count} attribútum} other {{count} attribútum}}",
|
||||||
"count_contacts": "{count, plural, one {{count} kontakt}} other {{count} kontakt}}",
|
"count_contacts": "{count, plural, one {{count} partner} other {{count} partner}}",
|
||||||
"count_members": "{count, plural, one {{count} tag} other {{count} tag}}",
|
"count_members": "{count, plural, one {{count} tag} other {{count} tag}}",
|
||||||
"count_questions": "{count, plural, one {{count} kérdés} other {{count} kérdés}}",
|
"count_questions": "{count, plural, one {{count} kérdés} other {{count} kérdés}}",
|
||||||
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
|
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "({copyNumber}. másolat)",
|
"duplicate_copy_number": "({copyNumber}. másolat)",
|
||||||
"e_commerce": "E-kereskedelem",
|
"e_commerce": "E-kereskedelem",
|
||||||
"edit": "Szerkesztés",
|
"edit": "Szerkesztés",
|
||||||
|
"elements": "Elemek",
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
"ending_card": "Befejező kártya",
|
"ending_card": "Befejező kártya",
|
||||||
"enter_url": "URL megadása",
|
"enter_url": "URL megadása",
|
||||||
"enterprise_license": "Vállalati licenc",
|
"enterprise_license": "Vállalati licenc",
|
||||||
"environment": "Környezet",
|
"environment": "Környezet",
|
||||||
"environment_not_found": "A környezet nem található",
|
|
||||||
"environment_notice": "Ön jelenleg a(z) {environment} környezetben van.",
|
"environment_notice": "Ön jelenleg a(z) {environment} környezetben van.",
|
||||||
"error": "Hiba",
|
"error": "Hiba",
|
||||||
"error_component_description": "Ez az erőforrás nem létezik, vagy nem rendelkezik a hozzáféréshez szükséges jogosultságokkal.",
|
"error_component_description": "Ez az erőforrás nem létezik, vagy nem rendelkezik a hozzáféréshez szükséges jogosultságokkal.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Inaktív kérdőívek",
|
"inactive_surveys": "Inaktív kérdőívek",
|
||||||
"integration": "integráció",
|
"integration": "integráció",
|
||||||
"integrations": "Integrációk",
|
"integrations": "Integrációk",
|
||||||
"invalid_date": "Érvénytelen dátum",
|
"invalid_date_with_value": "Érvénytelen dátum: {value}",
|
||||||
"invalid_file_name": "Érvénytelen fájlnév, nevezze át a fájlt, és próbálja újra",
|
"invalid_file_name": "Érvénytelen fájlnév, nevezze át a fájlt, és próbálja újra",
|
||||||
"invalid_file_type": "Érvénytelen fájltípus",
|
"invalid_file_type": "Érvénytelen fájltípus",
|
||||||
"invite": "Meghívás",
|
"invite": "Meghívás",
|
||||||
"invite_them": "Meghívó nekik",
|
"invite_them": "Meghívó nekik",
|
||||||
|
"javascript_required": "JavaScript szükséges",
|
||||||
|
"javascript_required_description": "A Formbricks használatához JavaScript szükséges. Kérjük, engedélyezze a JavaScriptet a böngésző beállításaiban a folytatáshoz.",
|
||||||
"key": "Kulcs",
|
"key": "Kulcs",
|
||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Tagok",
|
"members": "Tagok",
|
||||||
"members_and_teams": "Tagok és csapatok",
|
"members_and_teams": "Tagok és csapatok",
|
||||||
|
"membership": "Tagság",
|
||||||
"membership_not_found": "A tagság nem található",
|
"membership_not_found": "A tagság nem található",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metaadatok",
|
"metadata": "Metaadatok",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "A Formbricks nagyobb képernyőn működik a legjobban. A kérdőívek kezeléséhez vagy összeállításához váltson másik eszközre.",
|
"mobile_overlay_app_works_best_on_desktop": "A Formbricks nagyobb képernyőn működik a legjobban. A kérdőívek kezeléséhez vagy összeállításához váltson másik eszközre.",
|
||||||
"mobile_overlay_surveys_look_good": "Ne aggódjon – a kérdőívei minden eszközön és képernyőméretnél remekül néznek ki!",
|
"mobile_overlay_surveys_look_good": "Ne aggódjon – a kérdőívei minden eszközön és képernyőméretnél remekül néznek ki!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Új",
|
"new": "Új",
|
||||||
"new_version_available": "A Formbricks {version} megérkezett. Frissítsen most!",
|
"new_version_available": "A Formbricks {version} megérkezett. Frissítsen most!",
|
||||||
"next": "Következő",
|
"next": "Következő",
|
||||||
|
"no_actions_found": "Nem találhatók műveletek",
|
||||||
"no_background_image_found": "Nem található háttérkép.",
|
"no_background_image_found": "Nem található háttérkép.",
|
||||||
"no_code": "Kód nélkül",
|
"no_code": "Kód nélkül",
|
||||||
"no_files_uploaded": "Nem lettek fájlok feltöltve",
|
"no_files_uploaded": "Nem lettek fájlok feltöltve",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "vagy",
|
"or": "vagy",
|
||||||
"organization": "Szervezet",
|
"organization": "Szervezet",
|
||||||
"organization_id": "Szervezetazonosító",
|
"organization_id": "Szervezetazonosító",
|
||||||
"organization_not_found": "A szervezet nem található",
|
|
||||||
"organization_settings": "Szervezet beállításai",
|
"organization_settings": "Szervezet beállításai",
|
||||||
"organization_teams_not_found": "A szervezeti csapatok nem találhatók",
|
|
||||||
"other": "Egyéb",
|
"other": "Egyéb",
|
||||||
|
"other_filters": "Egyéb szűrők",
|
||||||
"others": "Mások",
|
"others": "Mások",
|
||||||
"overlay_color": "Rávetítés színe",
|
"overlay_color": "Rávetítés színe",
|
||||||
"overview": "Áttekintés",
|
"overview": "Áttekintés",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Válasszon legalább egy kérdőívet",
|
"please_select_at_least_one_survey": "Válasszon legalább egy kérdőívet",
|
||||||
"please_select_at_least_one_trigger": "Válasszon legalább egy aktiválót",
|
"please_select_at_least_one_trigger": "Válasszon legalább egy aktiválót",
|
||||||
"please_upgrade_your_plan": "Váltson magasabb csomagra",
|
"please_upgrade_your_plan": "Váltson magasabb csomagra",
|
||||||
|
"powered_by_formbricks": "A gépházban: Formbricks",
|
||||||
"preview": "Előnézet",
|
"preview": "Előnézet",
|
||||||
"preview_survey": "Kérdőív előnézete",
|
"preview_survey": "Kérdőív előnézete",
|
||||||
"privacy": "Adatvédelmi irányelvek",
|
"privacy": "Adatvédelmi irányelvek",
|
||||||
@@ -360,7 +367,7 @@
|
|||||||
"reorder_and_hide_columns": "Oszlopok átrendezése és elrejtése",
|
"reorder_and_hide_columns": "Oszlopok átrendezése és elrejtése",
|
||||||
"replace": "Csere",
|
"replace": "Csere",
|
||||||
"report_survey": "Kérdőív jelentése",
|
"report_survey": "Kérdőív jelentése",
|
||||||
"request_trial_license": "Próbalicenc kérése",
|
"request_trial_license": "Próbaidőszaki licenc kérése",
|
||||||
"reset_to_default": "Visszaállítás az alapértelmezettre",
|
"reset_to_default": "Visszaállítás az alapértelmezettre",
|
||||||
"response": "Válasz",
|
"response": "Válasz",
|
||||||
"response_id": "Válaszazonosító",
|
"response_id": "Válaszazonosító",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Kiválasztás",
|
"select": "Kiválasztás",
|
||||||
"select_all": "Összes kiválasztása",
|
"select_all": "Összes kiválasztása",
|
||||||
"select_filter": "Szűrő kiválasztása",
|
"select_filter": "Szűrő kiválasztása",
|
||||||
|
"select_language": "Nyelv kiválasztása",
|
||||||
"select_survey": "Kérdőív kiválasztása",
|
"select_survey": "Kérdőív kiválasztása",
|
||||||
"select_teams": "Csapatok kiválasztása",
|
"select_teams": "Csapatok kiválasztása",
|
||||||
"selected": "Kiválasztva",
|
"selected": "Kiválasztva",
|
||||||
@@ -399,7 +407,7 @@
|
|||||||
"something_went_wrong": "Valami probléma történt",
|
"something_went_wrong": "Valami probléma történt",
|
||||||
"something_went_wrong_please_try_again": "Valami probléma történt. Próbálja meg újra.",
|
"something_went_wrong_please_try_again": "Valami probléma történt. Próbálja meg újra.",
|
||||||
"sort_by": "Rendezési sorrend",
|
"sort_by": "Rendezési sorrend",
|
||||||
"start_free_trial": "Ingyenes próbaverzió indítása",
|
"start_free_trial": "Ingyenes próbaidőszak indítása",
|
||||||
"status": "Állapot",
|
"status": "Állapot",
|
||||||
"step_by_step_manual": "Lépésenkénti kézikönyv",
|
"step_by_step_manual": "Lépésenkénti kézikönyv",
|
||||||
"storage_not_configured": "A fájltároló nincs beállítva, a feltöltések valószínűleg sikertelenek lesznek",
|
"storage_not_configured": "A fájltároló nincs beállítva, a feltöltések valószínűleg sikertelenek lesznek",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "Kérdőív-azonosító",
|
"survey_id": "Kérdőív-azonosító",
|
||||||
"survey_languages": "Kérdőív nyelvei",
|
"survey_languages": "Kérdőív nyelvei",
|
||||||
"survey_live": "A kérdőív élő",
|
"survey_live": "A kérdőív élő",
|
||||||
"survey_not_found": "A kérdőív nem található",
|
|
||||||
"survey_paused": "A kérdőív szüneteltetve.",
|
"survey_paused": "A kérdőív szüneteltetve.",
|
||||||
"survey_type": "Kérdőív típusa",
|
"survey_type": "Kérdőív típusa",
|
||||||
"surveys": "Kérdőívek",
|
"surveys": "Kérdőívek",
|
||||||
@@ -427,16 +434,15 @@
|
|||||||
"team_name": "Csapat neve",
|
"team_name": "Csapat neve",
|
||||||
"team_role": "Csapatszerep",
|
"team_role": "Csapatszerep",
|
||||||
"teams": "Csapatok",
|
"teams": "Csapatok",
|
||||||
"teams_not_found": "A csapatok nem találhatók",
|
|
||||||
"text": "Szöveg",
|
"text": "Szöveg",
|
||||||
"time": "Idő",
|
"time": "Idő",
|
||||||
"time_to_finish": "Idő a befejezésig",
|
"time_to_finish": "Idő a befejezésig",
|
||||||
"title": "Cím",
|
"title": "Cím",
|
||||||
"top_left": "Balra fent",
|
"top_left": "Balra fent",
|
||||||
"top_right": "Jobbra fent",
|
"top_right": "Jobbra fent",
|
||||||
"trial_days_remaining": "{count} nap van hátra a próbaidőszakból",
|
"trial_days_remaining": "{count} nap van hátra a próbaidőszakából",
|
||||||
"trial_expired": "A próbaidőszak lejárt",
|
"trial_expired": "A próbaidőszaka lejárt",
|
||||||
"trial_one_day_remaining": "1 nap van hátra a próbaidőszakból",
|
"trial_one_day_remaining": "1 nap van hátra a próbaidőszakából",
|
||||||
"try_again": "Próbálja újra",
|
"try_again": "Próbálja újra",
|
||||||
"type": "Típus",
|
"type": "Típus",
|
||||||
"unknown_survey": "Ismeretlen kérdőív",
|
"unknown_survey": "Ismeretlen kérdőív",
|
||||||
@@ -444,14 +450,13 @@
|
|||||||
"update": "Frissítés",
|
"update": "Frissítés",
|
||||||
"updated": "Frissítve",
|
"updated": "Frissítve",
|
||||||
"updated_at": "Frissítve",
|
"updated_at": "Frissítve",
|
||||||
"upgrade_plan": "Csomag frissítése",
|
"upgrade_plan": "Magasabb csomagra váltás",
|
||||||
"upload": "Feltöltés",
|
"upload": "Feltöltés",
|
||||||
"upload_failed": "A feltöltés nem sikerült. Próbálja meg újra.",
|
"upload_failed": "A feltöltés nem sikerült. Próbálja meg újra.",
|
||||||
"upload_input_description": "Kattintson vagy húzza ide a fájlok feltöltéséhez.",
|
"upload_input_description": "Kattintson vagy húzza ide a fájlok feltöltéséhez.",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Felhasználó",
|
"user": "Felhasználó",
|
||||||
"user_id": "Felhasználó-azonosító",
|
"user_id": "Felhasználó-azonosító",
|
||||||
"user_not_found": "A felhasználó nem található",
|
|
||||||
"variable": "Változó",
|
"variable": "Változó",
|
||||||
"variable_ids": "Változóazonosítók",
|
"variable_ids": "Változóazonosítók",
|
||||||
"variables": "Változók",
|
"variables": "Változók",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "hét",
|
"weeks": "hét",
|
||||||
"welcome_card": "Üdvözlő kártya",
|
"welcome_card": "Üdvözlő kártya",
|
||||||
"workflows": "Munkafolyamatok",
|
"workflows": "Munkafolyamatok",
|
||||||
|
"workspace": "Munkaterület",
|
||||||
"workspace_configuration": "Munkaterület beállítása",
|
"workspace_configuration": "Munkaterület beállítása",
|
||||||
"workspace_created_successfully": "A munkaterület sikeresen létrehozva",
|
"workspace_created_successfully": "A munkaterület sikeresen létrehozva",
|
||||||
"workspace_creation_description": "Kérdőívek munkaterületekre szervezése a jobb hozzáférés-vezérlés érdekében.",
|
"workspace_creation_description": "Kérdőívek munkaterületekre szervezése a jobb hozzáférés-vezérlés érdekében.",
|
||||||
"workspace_id": "Munkaterület-azonosító",
|
"workspace_id": "Munkaterület-azonosító",
|
||||||
"workspace_name": "Munkaterület neve",
|
"workspace_name": "Munkaterület neve",
|
||||||
"workspace_name_placeholder": "például Formbricks",
|
"workspace_name_placeholder": "például Formbricks",
|
||||||
"workspace_not_found": "A munkaterület nem található",
|
|
||||||
"workspace_permission_not_found": "A munkaterület-jogosultság nem található",
|
|
||||||
"workspaces": "Munkaterületek",
|
"workspaces": "Munkaterületek",
|
||||||
"years": "év",
|
"years": "év",
|
||||||
"you": "Ön",
|
"you": "Ön",
|
||||||
@@ -537,7 +541,7 @@
|
|||||||
"survey_response_finished_email_view_survey_summary": "Kérdőív összegzésének megtekintése",
|
"survey_response_finished_email_view_survey_summary": "Kérdőív összegzésének megtekintése",
|
||||||
"text_variable": "Szöveg változó",
|
"text_variable": "Szöveg változó",
|
||||||
"verification_email_click_on_this_link": "Erre a hivatkozásra is kattinthat:",
|
"verification_email_click_on_this_link": "Erre a hivatkozásra is kattinthat:",
|
||||||
"verification_email_heading": "Már majdnem megvagyunk!",
|
"verification_email_heading": "Már majdnem kész vagyunk!",
|
||||||
"verification_email_hey": "Helló 👋",
|
"verification_email_hey": "Helló 👋",
|
||||||
"verification_email_if_expired_request_new_token": "Ha lejárt, kérjen új tokent itt:",
|
"verification_email_if_expired_request_new_token": "Ha lejárt, kérjen új tokent itt:",
|
||||||
"verification_email_link_valid_for_24_hours": "A hivatkozás 24 órán keresztül érvényes.",
|
"verification_email_link_valid_for_24_hours": "A hivatkozás 24 órán keresztül érvényes.",
|
||||||
@@ -605,15 +609,15 @@
|
|||||||
"test_match": "Illeszkedés tesztelése",
|
"test_match": "Illeszkedés tesztelése",
|
||||||
"test_your_url": "URL tesztelése",
|
"test_your_url": "URL tesztelése",
|
||||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Ez a művelet automatikusan lett létrehozva. Nem végezhet változtatásokat rajta.",
|
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Ez a művelet automatikusan lett létrehozva. Nem végezhet változtatásokat rajta.",
|
||||||
"this_action_will_be_triggered_after_user_stays_on_page": "Ez a művelet akkor fog aktiválódni, miután a felhasználó a megadott ideig az oldalon tartózkodik.",
|
"this_action_will_be_triggered_after_user_stays_on_page": "Ez a művelet azután lesz aktiválva, hogy a felhasználó az oldalon marad a megadott időtartamig.",
|
||||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Ez a művelet akkor lesz aktiválva, ha az oldal betöltődik.",
|
"this_action_will_be_triggered_when_the_page_is_loaded": "Ez a művelet akkor lesz aktiválva, ha az oldal betöltődik.",
|
||||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Ez a művelet akkor lesz aktiválva, ha a felhasználó az oldal 50%-áig görget.",
|
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Ez a művelet akkor lesz aktiválva, ha a felhasználó az oldal 50%-áig görget.",
|
||||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Ez a művelet akkor lesz aktiválva, ha a felhasználó megpróbálja elhagyni az oldalt.",
|
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Ez a művelet akkor lesz aktiválva, ha a felhasználó megpróbálja elhagyni az oldalt.",
|
||||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Ez egy kódművelet. A változtatásokat a kódbázisban hajtsa végre.",
|
"this_is_a_code_action_please_make_changes_in_your_code_base": "Ez egy kódművelet. A változtatásokat a kódbázisban hajtsa végre.",
|
||||||
"time_in_seconds": "Idő másodpercben",
|
"time_in_seconds": "Idő másodpercben",
|
||||||
"time_in_seconds_placeholder": "pl. 10",
|
"time_in_seconds_placeholder": "például 10",
|
||||||
"time_in_seconds_with_unit": "{seconds} mp",
|
"time_in_seconds_with_unit": "{seconds} mp",
|
||||||
"time_on_page": "Oldalon töltött idő",
|
"time_on_page": "Idő az oldalon",
|
||||||
"track_new_user_action": "Új felhasználói művelet követése",
|
"track_new_user_action": "Új felhasználói művelet követése",
|
||||||
"track_user_action_to_display_surveys_or_create_user_segment": "Felhasználói művelet követése a kérdőívek megjelenítéséhez vagy felhasználói szakasz létrehozásához.",
|
"track_user_action_to_display_surveys_or_create_user_segment": "Felhasználói művelet követése a kérdőívek megjelenítéséhez vagy felhasználói szakasz létrehozásához.",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Az új „{dataType}” típusú „{key}” attribútum létrehozva",
|
"attributes_msg_new_attribute_created": "Az új „{dataType}” típusú „{key}” attribútum létrehozva",
|
||||||
"attributes_msg_userid_already_exists": "A felhasználó-azonosító már létezik ennél a környezetnél, és nem lett frissítve.",
|
"attributes_msg_userid_already_exists": "A felhasználó-azonosító már létezik ennél a környezetnél, és nem lett frissítve.",
|
||||||
"contact_deleted_successfully": "A partner sikeresen törölve",
|
"contact_deleted_successfully": "A partner sikeresen törölve",
|
||||||
"contact_not_found": "Nem található ilyen partner",
|
|
||||||
"contacts_table_refresh": "Partnerek frissítése",
|
"contacts_table_refresh": "Partnerek frissítése",
|
||||||
"contacts_table_refresh_success": "A partnerek sikeresen frissítve",
|
"contacts_table_refresh_success": "A partnerek sikeresen frissítve",
|
||||||
"create_attribute": "Attribútum létrehozása",
|
"create_attribute": "Attribútum létrehozása",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Harmadik fél által létrehozva",
|
"created_by_third_party": "Harmadik fél által létrehozva",
|
||||||
"discord_webhook_not_supported": "A Discord webhorgok jelenleg nem támogatottak.",
|
"discord_webhook_not_supported": "A Discord webhorgok jelenleg nem támogatottak.",
|
||||||
"empty_webhook_message": "A webhorgai itt fognak megjelenni, amint hozzáadja azokat. ⏲️",
|
"empty_webhook_message": "A webhorgai itt fognak megjelenni, amint hozzáadja azokat. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Hibás átjáró (502): Proxy-/átjáróhiba, a szolgáltatás nem érhető el",
|
||||||
|
"endpoint_gateway_timeout_error": "Átjáró időtúllépés (504): Átjáró időtúllépés, a szolgáltatás nem érhető el",
|
||||||
|
"endpoint_internal_server_error": "Belső szerverhiba (500): A szolgáltatás váratlan hibába ütközött",
|
||||||
|
"endpoint_method_not_allowed_error": "A metódus nem engedélyezett (405): A végpont létezik, de nem fogad POST kéréseket",
|
||||||
|
"endpoint_not_found_error": "Nem található (404): A végpont nem létezik",
|
||||||
"endpoint_pinged": "Hurrá! Képesek vagyunk pingelni a webhorgot!",
|
"endpoint_pinged": "Hurrá! Képesek vagyunk pingelni a webhorgot!",
|
||||||
"endpoint_pinged_error": "Nem lehet pingelni a webhorgot!",
|
"endpoint_pinged_error": "Nem lehet pingelni a webhorgot!",
|
||||||
|
"endpoint_service_unavailable_error": "A szolgáltatás nem érhető el (503): A szolgáltatás átmenetileg nem elérhető",
|
||||||
"learn_to_verify": "Tudja meg, hogy kell ellenőrizni a webhorog aláírásait",
|
"learn_to_verify": "Tudja meg, hogy kell ellenőrizni a webhorog aláírásait",
|
||||||
|
"no_triggers": "Nincsenek Triggerek",
|
||||||
"please_check_console": "További részletekért nézze meg a konzolt",
|
"please_check_console": "További részletekért nézze meg a konzolt",
|
||||||
"please_enter_a_url": "Adjon meg egy URL-t",
|
"please_enter_a_url": "Adjon meg egy URL-t",
|
||||||
"response_created": "Válasz létrehozva",
|
"response_created": "Válasz létrehozva",
|
||||||
@@ -973,79 +983,79 @@
|
|||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"add_payment_method": "Fizetési mód hozzáadása",
|
"add_payment_method": "Fizetési mód hozzáadása",
|
||||||
"add_payment_method_to_upgrade_tooltip": "Kérjük, adjon hozzá egy fizetési módot fent a fizetős csomagra való frissítéshez",
|
"add_payment_method_to_upgrade_tooltip": "Adjon hozzá fizetési módot fent, hogy fizetős csomagra váltson",
|
||||||
"billing_interval_toggle": "Számlázási időszak",
|
"billing_interval_toggle": "Számlázási időköz",
|
||||||
"current_plan_badge": "Jelenlegi",
|
"current_plan_badge": "Jelenlegi",
|
||||||
"current_plan_cta": "Jelenlegi csomag",
|
"current_plan_cta": "Jelenlegi csomag",
|
||||||
"custom_plan_description": "A szervezete egyedi számlázási beállítással rendelkezik. Továbbra is válthat az alábbi standard csomagok egyikére.",
|
"custom_plan_description": "A szervezete egyéni számlázási beállítással rendelkezik. Ugyanakkor áttérhet az alábbi szabványos csomagok egyikére.",
|
||||||
"custom_plan_title": "Egyedi csomag",
|
"custom_plan_title": "Egyéni csomag",
|
||||||
"failed_to_start_trial": "A próbaidőszak indítása sikertelen. Kérjük, próbálja meg újra.",
|
"failed_to_start_trial": "Nem sikerült a próbaidőszak indítása. Próbálja meg újra.",
|
||||||
"keep_current_plan": "Jelenlegi csomag megtartása",
|
"keep_current_plan": "Jelenlegi csomag megtartása",
|
||||||
"manage_billing_details": "Kártyaadatok és számlák kezelése",
|
"manage_billing_details": "Kártyarészletek és számlák kezelése",
|
||||||
"monthly": "Havi",
|
"monthly": "Havi",
|
||||||
"most_popular": "Legnépszerűbb",
|
"most_popular": "Legnépszerűbb",
|
||||||
"pending_change_removed": "Az ütemezett csomagváltás eltávolítva.",
|
"pending_change_removed": "Az ütemezett csomagváltoztatás eltávolítva.",
|
||||||
"pending_plan_badge": "Ütemezett",
|
"pending_plan_badge": "Ütemezett",
|
||||||
"pending_plan_change_description": "A csomagja {{date}}-án átvált erre: {{plan}}.",
|
"pending_plan_change_description": "A csomagja {{plan}} csomagra fog váltani ekkor: {{date}}.",
|
||||||
"pending_plan_change_title": "Ütemezett csomagváltás",
|
"pending_plan_change_title": "Ütemezett csomagváltoztatás",
|
||||||
"pending_plan_cta": "Ütemezett",
|
"pending_plan_cta": "Ütemezett",
|
||||||
"per_month": "havonta",
|
"per_month": "havonta",
|
||||||
"per_year": "évente",
|
"per_year": "évente",
|
||||||
"plan_change_applied": "A csomag sikeresen frissítve.",
|
"plan_change_applied": "A csomag sikeresen frissítve.",
|
||||||
"plan_change_scheduled": "A csomagváltás sikeresen ütemezve.",
|
"plan_change_scheduled": "A csomagváltoztatás sikeresen ütemezve.",
|
||||||
"plan_custom": "Custom",
|
"plan_custom": "Egyéni",
|
||||||
"plan_feature_everything_in_hobby": "Minden, ami a Hobby csomagban",
|
"plan_feature_everything_in_hobby": "Minden a Hobbi csomagban",
|
||||||
"plan_feature_everything_in_pro": "Minden, ami a Pro csomagban",
|
"plan_feature_everything_in_pro": "Minden a Pro csomagban",
|
||||||
"plan_hobby": "Hobby",
|
"plan_hobby": "Hobbi",
|
||||||
"plan_hobby_description": "Magánszemélyek és kisebb csapatok számára, akik most kezdik a Formbricks Cloud használatát.",
|
"plan_hobby_description": "Magánszemélyeknek és kis csapatoknak, akik most teszik meg a kezdeti lépéseket a Formbricks Cloud szolgáltatással.",
|
||||||
"plan_hobby_feature_responses": "250 válasz / hó",
|
"plan_hobby_feature_responses": "250 válasz/hónap",
|
||||||
"plan_hobby_feature_workspaces": "1 munkaterület",
|
"plan_hobby_feature_workspaces": "1 munkaterület",
|
||||||
"plan_pro": "Pro",
|
"plan_pro": "Pro",
|
||||||
"plan_pro_description": "Növekvő csapatok számára, amelyeknek magasabb korlátokra, automatizálásokra és dinamikus túlhasználatra van szükségük.",
|
"plan_pro_description": "Növekvő csapatoknak, akiknek magasabb korlátokra, automatizálásra és dinamikus túllépési lehetőségekre van szükségük.",
|
||||||
"plan_pro_feature_responses": "2 000 válasz / hó (dinamikus túlhasználat)",
|
"plan_pro_feature_responses": "2000 válasz/hónap (dinamikus túllépés)",
|
||||||
"plan_pro_feature_workspaces": "3 munkaterület",
|
"plan_pro_feature_workspaces": "3 munkaterület",
|
||||||
"plan_scale": "Scale",
|
"plan_scale": "Méretezés",
|
||||||
"plan_scale_description": "Nagyobb csapatok számára, amelyeknek nagyobb kapacitásra, erősebb irányításra és magasabb válaszmennyiségre van szükségük.",
|
"plan_scale_description": "Nagyobb csapatoknak, amelyeknek több kapacitásra, erősebb irányításra és nagyobb válaszmennyiségre van szükségük.",
|
||||||
"plan_scale_feature_responses": "5000 válasz / hónap (dinamikus túllépés)",
|
"plan_scale_feature_responses": "5000 válasz/hónap (dinamikus túllépés)",
|
||||||
"plan_scale_feature_workspaces": "5 munkaterület",
|
"plan_scale_feature_workspaces": "5 munkaterület",
|
||||||
"plan_selection_description": "Hasonlítsa össze a Hobby, Pro és Scale csomagokat, majd váltson csomagot közvetlenül a Formbricks alkalmazásból.",
|
"plan_selection_description": "Hobbi, Pro és Méretezés csomagok összehasonlítása, majd csomagok közötti váltás közvetlenül a Formbricksben.",
|
||||||
"plan_selection_title": "Válassza ki az Ön csomagját",
|
"plan_selection_title": "Csomag kiválasztása",
|
||||||
"plan_unknown": "Ismeretlen",
|
"plan_unknown": "Ismeretlen",
|
||||||
"remove_branding": "Márkajel eltávolítása",
|
"remove_branding": "Márkajel eltávolítása",
|
||||||
"retry_setup": "Újrapróbálkozás a beállítással",
|
"retry_setup": "Beállítás újrapróbálása",
|
||||||
"select_plan_header_subtitle": "Nincs szükség bankkártyára, nincsenek rejtett feltételek.",
|
"select_plan_header_subtitle": "Nincs szükség hitelkártyára, nincs kötöttség.",
|
||||||
"select_plan_header_title": "Zökkenőmentesen integrált felmérések, 100%-ban az Ön márkája.",
|
"select_plan_header_title": "Zökkenőmentesen integrált kérdőívek, 100%-ban az Ön márkájához igazítva.",
|
||||||
"status_trialing": "Próbaverzió",
|
"status_trialing": "Próbaidőszak",
|
||||||
"stay_on_hobby_plan": "A Hobby csomagnál szeretnék maradni",
|
"stay_on_hobby_plan": "A Hobbi csomagnál szeretnék maradni",
|
||||||
"stripe_setup_incomplete": "Számlázás beállítása nem teljes",
|
"stripe_setup_incomplete": "A számlázási beállítás befejezetlen",
|
||||||
"stripe_setup_incomplete_description": "A számlázás beállítása nem sikerült teljesen. Aktiválja előfizetését az újrapróbálkozással.",
|
"stripe_setup_incomplete_description": "A számlázási beállítás nem fejeződött be sikeresen. Próbálja meg újra aktiválni az előfizetését.",
|
||||||
"subscription": "Előfizetés",
|
"subscription": "Előfizetés",
|
||||||
"subscription_description": "Kezelje előfizetését és kövesse nyomon a használatot",
|
"subscription_description": "Az előfizetési csomag kezelése és a használat felügyelete",
|
||||||
"switch_at_period_end": "Váltás az időszak végén",
|
"switch_at_period_end": "Váltás az időszak végén",
|
||||||
"switch_plan_now": "Csomag váltása most",
|
"switch_plan_now": "Csomag váltása most",
|
||||||
"this_includes": "Ez tartalmazza",
|
"this_includes": "Ezeket tartalmazza",
|
||||||
"trial_alert_description": "Adjon hozzá fizetési módot, hogy megtarthassa a hozzáférést az összes funkcióhoz.",
|
"trial_alert_description": "Fizetési mód hozzáadása az összes funkcióhoz való hozzáférés megtartásához.",
|
||||||
"trial_already_used": "Ehhez az e-mail címhez már igénybe vettek ingyenes próbaidőszakot. Kérjük, válasszon helyette fizetős csomagot.",
|
"trial_already_used": "Ehhez az e-mail-címhez már használatban van egy ingyenes próbaidőszak. Váltson inkább fizetős csomagra.",
|
||||||
"trial_feature_api_access": "API-hozzáférés",
|
"trial_feature_api_access": "API-hozzáférés",
|
||||||
"trial_feature_attribute_segmentation": "Attribútumalapú szegmentálás",
|
"trial_feature_attribute_segmentation": "Attribútumalapú szakaszolás",
|
||||||
"trial_feature_contact_segment_management": "Kapcsolat- és szegmenskezelés",
|
"trial_feature_contact_segment_management": "Partner- és szakaszkezelés",
|
||||||
"trial_feature_email_followups": "E-mail követések",
|
"trial_feature_email_followups": "E-mailes utókövetések",
|
||||||
"trial_feature_hide_branding": "Formbricks márkajelzés elrejtése",
|
"trial_feature_hide_branding": "Formbricks márkajel elrejtése",
|
||||||
"trial_feature_mobile_sdks": "iOS és Android SDK-k",
|
"trial_feature_mobile_sdks": "iOS és Android SDK-k",
|
||||||
"trial_feature_respondent_identification": "Válaszadó-azonosítás",
|
"trial_feature_respondent_identification": "Válaszadó-azonosítás",
|
||||||
"trial_feature_unlimited_seats": "Korlátlan számú felhasználói hely",
|
"trial_feature_unlimited_seats": "Korlátlan számú hely",
|
||||||
"trial_feature_webhooks": "Egyéni webhookok",
|
"trial_feature_webhooks": "Egyéni webhorgok",
|
||||||
"trial_no_credit_card": "14 napos próbaidőszak, bankkártya nélkül",
|
"trial_no_credit_card": "14 napos próbaidőszak, nincs szükség hitelkártyára",
|
||||||
"trial_payment_method_added_description": "Minden rendben! A Pro csomag automatikusan folytatódik a próbaidőszak lejárta után.",
|
"trial_payment_method_added_description": "Mindent beállított! A Pro csomagja a próbaidőszak vége után automatikusan folytatódik.",
|
||||||
"trial_title": "Szerezze meg a Formbricks Pro-t ingyen!",
|
"trial_title": "Szerezze meg a Formbricks Pro csomagot ingyen!",
|
||||||
"unlimited_responses": "Korlátlan válaszok",
|
"unlimited_responses": "Korlátlan válaszok",
|
||||||
"unlimited_workspaces": "Korlátlan munkaterület",
|
"unlimited_workspaces": "Korlátlan munkaterület",
|
||||||
"upgrade": "Frissítés",
|
"upgrade": "Frissítés",
|
||||||
"upgrade_now": "Frissítés most",
|
"upgrade_now": "Frissítés most",
|
||||||
"usage_cycle": "Usage cycle",
|
"usage_cycle": "Használati ciklus",
|
||||||
"used": "felhasználva",
|
"used": "használva",
|
||||||
"yearly": "Éves",
|
"yearly": "Évente",
|
||||||
"yearly_checkout_unavailable": "Az éves fizetés még nem érhető el. Kérjük, adjon hozzá fizetési módot egy havi előfizetéshez, vagy vegye fel a kapcsolatot az ügyfélszolgálattal.",
|
"yearly_checkout_unavailable": "Az éves fizetési lehetőség még nem érhető el. Először adjon hozzá fizetési módot egy havi csomaghoz, vagy vegye fel a kapcsolatot az ügyfélszolgálattal.",
|
||||||
"your_plan": "Az Ön csomagja"
|
"your_plan": "Az Ön csomagja"
|
||||||
},
|
},
|
||||||
"domain": {
|
"domain": {
|
||||||
@@ -1071,29 +1081,48 @@
|
|||||||
"enterprise_features": "Vállalati funkciók",
|
"enterprise_features": "Vállalati funkciók",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Vállalati licenc megszerzése az összes funkcióhoz való hozzáféréshez.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Vállalati licenc megszerzése az összes funkcióhoz való hozzáféréshez.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Az adatvédelem és biztonság fölötti rendelkezés teljes kézben tartása.",
|
"keep_full_control_over_your_data_privacy_and_security": "Az adatvédelem és biztonság fölötti rendelkezés teljes kézben tartása.",
|
||||||
"license_instance_mismatch_description": "Ez a licenc jelenleg egy másik Formbricks példányhoz van kötve. Amennyiben ez a telepítés újra lett építve vagy áthelyezésre került, kérje a Formbricks ügyfélszolgálatát, hogy bontsa fel az előző példány kötését.",
|
"license_feature_access_control": "Hozzáférés-vezérlés (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Auditálási naplók",
|
||||||
|
"license_feature_contacts": "Partnerek és szakaszok",
|
||||||
|
"license_feature_projects": "Munkaterületek",
|
||||||
|
"license_feature_quotas": "Kvóták",
|
||||||
|
"license_feature_remove_branding": "Márkajel eltávolítása",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Szemét elleni védekezés",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Kétfaktoros hitelesítés",
|
||||||
|
"license_feature_whitelabel": "Fehér címkés e-mailek",
|
||||||
|
"license_features_table_access": "Hozzáférés",
|
||||||
|
"license_features_table_description": "Az példányhoz jelenleg elérhető vállalati funkciók és korlátok.",
|
||||||
|
"license_features_table_disabled": "Letiltva",
|
||||||
|
"license_features_table_enabled": "Engedélyezve",
|
||||||
|
"license_features_table_feature": "Funkció",
|
||||||
|
"license_features_table_title": "Licencelt funkciók",
|
||||||
|
"license_features_table_unlimited": "Korlátlan",
|
||||||
|
"license_features_table_value": "Érték",
|
||||||
|
"license_instance_mismatch_description": "Ez a licenc jelenleg egy másik Formbricks-példányhoz van kötve. Ha ezt a telepítést újraépítették vagy áthelyezték, akkor kérje meg a Formbricks ügyfélszolgálatát, hogy szüntessék meg a korábbi példányhoz való kötést.",
|
||||||
"license_invalid_description": "Az ENTERPRISE_LICENSE_KEY környezeti változóban lévő licenckulcs nem érvényes. Ellenőrizze, hogy nem gépelte-e el, vagy kérjen új kulcsot.",
|
"license_invalid_description": "Az ENTERPRISE_LICENSE_KEY környezeti változóban lévő licenckulcs nem érvényes. Ellenőrizze, hogy nem gépelte-e el, vagy kérjen új kulcsot.",
|
||||||
"license_status": "Licencállapot",
|
"license_status": "Licencállapot",
|
||||||
"license_status_active": "Aktív",
|
"license_status_active": "Aktív",
|
||||||
"license_status_description": "A vállalati licenc állapota.",
|
"license_status_description": "A vállalati licenc állapota.",
|
||||||
"license_status_expired": "Lejárt",
|
"license_status_expired": "Lejárt",
|
||||||
"license_status_instance_mismatch": "Másik Példányhoz Kötve",
|
"license_status_instance_mismatch": "Másik példányhoz kötve",
|
||||||
"license_status_invalid": "Érvénytelen licenc",
|
"license_status_invalid": "Érvénytelen licenc",
|
||||||
"license_status_unreachable": "Nem érhető el",
|
"license_status_unreachable": "Nem érhető el",
|
||||||
"license_unreachable_grace_period": "A licenckiszolgálót nem lehet elérni. A vállalati funkciók egy 3 napos türelmi időszak alatt aktívak maradnak, egészen eddig: {gracePeriodEnd}.",
|
"license_unreachable_grace_period": "A licenckiszolgálót nem lehet elérni. A vállalati funkciók egy 3 napos türelmi időszak alatt aktívak maradnak, egészen eddig: {gracePeriodEnd}.",
|
||||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Nincs szükség telefonálásra, nincs feltételekhez kötöttség: kérjen 30 napos ingyenes próbalicencet az összes funkció kipróbálásához az alábbi űrlap kitöltésével:",
|
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Nincs szükség telefonálásra, nincs feltételekhez kötöttség: kérjen 30 napos próbaidőszaki licencet az összes funkció kipróbálásához az alábbi űrlap kitöltésével:",
|
||||||
"no_credit_card_no_sales_call_just_test_it": "Nem kell hitelkártya. Nincsenek értékesítési hívások. Egyszerűen csak próbálja ki :)",
|
"no_credit_card_no_sales_call_just_test_it": "Nem kell hitelkártya. Nincsenek értékesítési hívások. Egyszerűen csak próbálja ki :)",
|
||||||
"on_request": "Kérésre",
|
"on_request": "Kérésre",
|
||||||
"organization_roles": "Szervezeti szerepek (adminisztrátor, szerkesztő, fejlesztő stb.)",
|
"organization_roles": "Szervezeti szerepek (adminisztrátor, szerkesztő, fejlesztő stb.)",
|
||||||
"questions_please_reach_out_to": "Kérdése van? Írjon nekünk erre az e-mail-címre:",
|
"questions_please_reach_out_to": "Kérdése van? Írjon nekünk erre az e-mail-címre:",
|
||||||
"recheck_license": "Licenc újraellenőrzése",
|
"recheck_license": "Licenc újraellenőrzése",
|
||||||
"recheck_license_failed": "A licencellenőrzés nem sikerült. Lehet, hogy a licenckiszolgáló nem érhető el.",
|
"recheck_license_failed": "A licencellenőrzés nem sikerült. Lehet, hogy a licenckiszolgáló nem érhető el.",
|
||||||
"recheck_license_instance_mismatch": "Ez a licenc egy másik Formbricks példányhoz van kötve. Kérje a Formbricks ügyfélszolgálatát, hogy bontsa fel az előző kötést.",
|
"recheck_license_instance_mismatch": "Ez a licenc egy másik Formbricks-példányhoz van kötve. Kérje meg a Formbricks ügyfélszolgálatát, hogy szüntessék meg a korábbi kötést.",
|
||||||
"recheck_license_invalid": "A licenckulcs érvénytelen. Ellenőrizze az ENTERPRISE_LICENSE_KEY értékét.",
|
"recheck_license_invalid": "A licenckulcs érvénytelen. Ellenőrizze az ENTERPRISE_LICENSE_KEY értékét.",
|
||||||
"recheck_license_success": "A licencellenőrzés sikeres",
|
"recheck_license_success": "A licencellenőrzés sikeres",
|
||||||
"recheck_license_unreachable": "A licenckiszolgáló nem érhető el. Próbálja meg később újra.",
|
"recheck_license_unreachable": "A licenckiszolgáló nem érhető el. Próbálja meg később újra.",
|
||||||
"rechecking": "Újraellenőrzés…",
|
"rechecking": "Újraellenőrzés…",
|
||||||
"request_30_day_trial_license": "30 napos ingyenes licenc kérése",
|
"request_30_day_trial_license": "30 napos próbaidőszaki licenc kérése",
|
||||||
"saml_sso": "SAML SSO",
|
"saml_sso": "SAML SSO",
|
||||||
"service_level_agreement": "Szolgáltatási megállapodás",
|
"service_level_agreement": "Szolgáltatási megállapodás",
|
||||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 megfelelőségi ellenőrzés",
|
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 megfelelőségi ellenőrzés",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Egyéni gépnév",
|
"custom_hostname": "Egyéni gépnév",
|
||||||
"customize_survey_logo": "A kérdőív logójának személyre szabása",
|
"customize_survey_logo": "A kérdőív logójának személyre szabása",
|
||||||
"darken_or_lighten_background_of_your_choice": "A választási lehetőség hátterének sötétítése vagy világosítása.",
|
"darken_or_lighten_background_of_your_choice": "A választási lehetőség hátterének sötétítése vagy világosítása.",
|
||||||
"date_format": "Dátumformátum",
|
|
||||||
"days_before_showing_this_survey_again": "vagy több napnak kell eltelnie az utolsó megjelenített kérdőív és ezen kérdőív megjelenése között.",
|
"days_before_showing_this_survey_again": "vagy több napnak kell eltelnie az utolsó megjelenített kérdőív és ezen kérdőív megjelenése között.",
|
||||||
"delete_anyways": "Törlés mindenképp",
|
"delete_anyways": "Törlés mindenképp",
|
||||||
"delete_block": "Blokk törlése",
|
"delete_block": "Blokk törlése",
|
||||||
@@ -1430,21 +1458,22 @@
|
|||||||
"error_saving_changes": "Hiba a változtatások mentésekor",
|
"error_saving_changes": "Hiba a változtatások mentésekor",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Több válasz lehetővé tétele. Még válasz után is látható marad (például visszajelző doboz).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Több válasz lehetővé tétele. Még válasz után is látható marad (például visszajelző doboz).",
|
||||||
"everyone": "Mindenki",
|
"everyone": "Mindenki",
|
||||||
"external_urls_paywall_tooltip": "Kérjük, váltson fizetős csomagra, hogy testre szabhassa a külső URL-eket. Ez segít megelőzni az adathalászatot.",
|
"expand_preview": "Előnézet kinyitása",
|
||||||
|
"external_urls_paywall_tooltip": "Váltson a magasabb fizetős csomagra a külső URL-ek személyre szabásához. Ez segít nekünk megelőzni az adathalászatot.",
|
||||||
"fallback_missing": "Tartalék hiányzik",
|
"fallback_missing": "Tartalék hiányzik",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "A(z) {fieldId} használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "A(z) {fieldId} használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
||||||
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "A(z) „{fieldId}” rejtett mező használatban van a(z) „{quotaName}” kvótában",
|
"fieldId_is_used_in_quota_please_remove_it_from_quota_first": "A(z) „{fieldId}” rejtett mező használatban van a(z) „{quotaName}” kvótában",
|
||||||
"field_name_eg_score_price": "Mező neve, például pontszám, ár",
|
"field_name_eg_score_price": "Mező neve, például pontszám, ár",
|
||||||
"first_name": "Keresztnév",
|
"first_name": "Keresztnév",
|
||||||
"five_points_recommended": "5 pont (ajánlott)",
|
"five_points_recommended": "5 pont (ajánlott)",
|
||||||
"follow_ups": "Követések",
|
"follow_ups": "Utókövetések",
|
||||||
"follow_ups_delete_modal_text": "Biztosan törölni szeretné ezt a követést?",
|
"follow_ups_delete_modal_text": "Biztosan törölni szeretné ezt az utókövetést?",
|
||||||
"follow_ups_delete_modal_title": "Törli a követést?",
|
"follow_ups_delete_modal_title": "Törli az utókövetést?",
|
||||||
"follow_ups_empty_description": "Üzenetek küldése a válaszadóknak, önmagának vagy csapattársaknak.",
|
"follow_ups_empty_description": "Üzenetek küldése a válaszadóknak, önmagának vagy csapattársaknak.",
|
||||||
"follow_ups_empty_heading": "Automatikus követések küldése",
|
"follow_ups_empty_heading": "Automatikus utókövetések küldése",
|
||||||
"follow_ups_ending_card_delete_modal_text": "Ez a befejező kártya használatban van a követésekben. A törlése eltávolítja az összes követésből. Biztosan törölni szeretné?",
|
"follow_ups_ending_card_delete_modal_text": "Ez a befejező kártya használatban van az utókövetésekben. A törlése eltávolítja az összes utókövetésből. Biztosan törölni szeretné?",
|
||||||
"follow_ups_ending_card_delete_modal_title": "Törli a befejező kártyát?",
|
"follow_ups_ending_card_delete_modal_title": "Törli a befejező kártyát?",
|
||||||
"follow_ups_hidden_field_error": "A rejtett mező használatban van egy követésben. Először távolítsa el a követésből.",
|
"follow_ups_hidden_field_error": "A rejtett mező használatban van egy utókövetésben. Először távolítsa el az utókövetésből.",
|
||||||
"follow_ups_include_hidden_fields": "Rejtett mezők értékeinek felvétele",
|
"follow_ups_include_hidden_fields": "Rejtett mezők értékeinek felvétele",
|
||||||
"follow_ups_include_variables": "Változó értékeinek felvétele",
|
"follow_ups_include_variables": "Változó értékeinek felvétele",
|
||||||
"follow_ups_item_ending_tag": "Befejezések",
|
"follow_ups_item_ending_tag": "Befejezések",
|
||||||
@@ -1468,21 +1497,21 @@
|
|||||||
"follow_ups_modal_action_to_description": "Az az e-mail-cím, ahova az e-mail elküldésre kerül",
|
"follow_ups_modal_action_to_description": "Az az e-mail-cím, ahova az e-mail elküldésre kerül",
|
||||||
"follow_ups_modal_action_to_label": "Címzett",
|
"follow_ups_modal_action_to_label": "Címzett",
|
||||||
"follow_ups_modal_action_to_warning": "Nem találhatók érvényes beállítások az e-mailek küldéséhez, adjon hozzá néhány szabad szöveges vagy kapcsolatfelvételi információkat tartalmazó kérdést vagy rejtett mezőt",
|
"follow_ups_modal_action_to_warning": "Nem találhatók érvényes beállítások az e-mailek küldéséhez, adjon hozzá néhány szabad szöveges vagy kapcsolatfelvételi információkat tartalmazó kérdést vagy rejtett mezőt",
|
||||||
"follow_ups_modal_create_heading": "Új követés létrehozása",
|
"follow_ups_modal_create_heading": "Új utókövetés létrehozása",
|
||||||
"follow_ups_modal_created_successfull_toast": "A követés létrehozva, és akkor lesz elmentve, ha elmenti a kérdőívet.",
|
"follow_ups_modal_created_successfull_toast": "Az utókövetés létrehozva, és akkor lesz elmentve, ha elmenti a kérdőívet.",
|
||||||
"follow_ups_modal_edit_heading": "A követés szerkesztése",
|
"follow_ups_modal_edit_heading": "Az utókövetés szerkesztése",
|
||||||
"follow_ups_modal_edit_no_id": "Nincs kérdőívkövetési azonosító megadva, nem lehet frissíteni a kérdőívkövetést",
|
"follow_ups_modal_edit_no_id": "Nincs kérdőív-utókövetési azonosító megadva, nem lehet frissíteni a kérdőív utókövetését",
|
||||||
"follow_ups_modal_name_label": "Követés neve",
|
"follow_ups_modal_name_label": "Utókövetés neve",
|
||||||
"follow_ups_modal_name_placeholder": "A követés elnevezése",
|
"follow_ups_modal_name_placeholder": "Az utókövetés elnevezése",
|
||||||
"follow_ups_modal_subheading": "Üzenetek küldése a válaszadóknak, önmagának vagy csapattársaknak",
|
"follow_ups_modal_subheading": "Üzenetek küldése a válaszadóknak, önmagának vagy csapattársaknak",
|
||||||
"follow_ups_modal_trigger_description": "Mikor kell ezt a követést aktiválni?",
|
"follow_ups_modal_trigger_description": "Mikor kell ezt az utókövetést aktiválni?",
|
||||||
"follow_ups_modal_trigger_label": "Aktiváló",
|
"follow_ups_modal_trigger_label": "Aktiváló",
|
||||||
"follow_ups_modal_trigger_type_ending": "A válaszadó egy adott befejezést lát",
|
"follow_ups_modal_trigger_type_ending": "A válaszadó egy adott befejezést lát",
|
||||||
"follow_ups_modal_trigger_type_ending_select": "Befejezések kiválasztása: ",
|
"follow_ups_modal_trigger_type_ending_select": "Befejezések kiválasztása: ",
|
||||||
"follow_ups_modal_trigger_type_ending_warning": "Válasszon legalább egy befejezést, vagy változtassa meg az aktiváló típusát",
|
"follow_ups_modal_trigger_type_ending_warning": "Válasszon legalább egy befejezést, vagy változtassa meg az aktiváló típusát",
|
||||||
"follow_ups_modal_trigger_type_response": "A válaszadó kitölti a kérdőívet",
|
"follow_ups_modal_trigger_type_response": "A válaszadó kitölti a kérdőívet",
|
||||||
"follow_ups_modal_updated_successfull_toast": "A követés frissítve, és akkor lesz elmentve, ha elmenti a kérdőívet.",
|
"follow_ups_modal_updated_successfull_toast": "Az utókövetés frissítve, és akkor lesz elmentve, ha elmenti a kérdőívet.",
|
||||||
"follow_ups_new": "Új követés",
|
"follow_ups_new": "Új utókövetés",
|
||||||
"formbricks_sdk_is_not_connected": "A Formbricks SDK nincs csatlakoztatva",
|
"formbricks_sdk_is_not_connected": "A Formbricks SDK nincs csatlakoztatva",
|
||||||
"four_points": "4 pont",
|
"four_points": "4 pont",
|
||||||
"heading": "Címsor",
|
"heading": "Címsor",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "A válaszkorlátnak meg kell haladnia a kapott válaszok számát ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "A válaszkorlátnak meg kell haladnia a kapott válaszok számát ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Válaszkorlátok, átirányítások és egyebek.",
|
"response_limits_redirections_and_more": "Válaszkorlátok, átirányítások és egyebek.",
|
||||||
"response_options": "Válasz beállításai",
|
"response_options": "Válasz beállításai",
|
||||||
|
"reverse_order_occasionally": "Sorrend alkalmi megfordítása",
|
||||||
|
"reverse_order_occasionally_except_last": "Sorrend alkalmi megfordítása az utolsó kivételével",
|
||||||
"roundness": "Kerekesség",
|
"roundness": "Kerekesség",
|
||||||
"roundness_description": "Annak vezérlése, hogy a sarkok mennyire legyenek lekerekítve.",
|
"roundness_description": "Annak vezérlése, hogy a sarkok mennyire legyenek lekerekítve.",
|
||||||
"row_used_in_logic_error": "Ez a sor használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
"row_used_in_logic_error": "Ez a sor használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Kérdőív megjelenítése legfeljebb:",
|
"show_survey_maximum_of": "Kérdőív megjelenítése legfeljebb:",
|
||||||
"show_survey_to_users": "Kérdőív megjelenítése a felhasználók ennyi százalékának",
|
"show_survey_to_users": "Kérdőív megjelenítése a felhasználók ennyi százalékának",
|
||||||
"show_to_x_percentage_of_targeted_users": "Megjelenítés a célzott felhasználók {percentage}%-ának",
|
"show_to_x_percentage_of_targeted_users": "Megjelenítés a célzott felhasználók {percentage}%-ának",
|
||||||
|
"shrink_preview": "Előnézet összecsukása",
|
||||||
"simple": "Egyszerű",
|
"simple": "Egyszerű",
|
||||||
"six_points": "6 pont",
|
"six_points": "6 pont",
|
||||||
"smiley": "Hangulatjel",
|
"smiley": "Hangulatjel",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "A stílus a téma stílusaira állítva",
|
"styling_set_to_theme_styles": "A stílus a téma stílusaira állítva",
|
||||||
"subheading": "Alcím",
|
"subheading": "Alcím",
|
||||||
"subtract": "Kivonás -",
|
"subtract": "Kivonás -",
|
||||||
|
"survey_closed_message_heading_required": "Címsor hozzáadása az egyéni kérdőív záró üzenetéhez.",
|
||||||
"survey_completed_heading": "A kérdőív kitöltve",
|
"survey_completed_heading": "A kérdőív kitöltve",
|
||||||
"survey_completed_subheading": "Ez a szabad és nyílt forráskódú kérdőív le lett zárva",
|
"survey_completed_subheading": "Ez a szabad és nyílt forráskódú kérdőív le lett zárva",
|
||||||
"survey_display_settings": "Kérdőív megjelenítésének beállításai",
|
"survey_display_settings": "Kérdőív megjelenítésének beállításai",
|
||||||
"survey_placement": "Kérdőív elhelyezése",
|
"survey_placement": "Kérdőív elhelyezése",
|
||||||
|
"survey_preview": "Kérdőív előnézete 👀",
|
||||||
"survey_styling": "Kérdőív stílusának beállítása",
|
"survey_styling": "Kérdőív stílusának beállítása",
|
||||||
"survey_trigger": "Kérdőív aktiválója",
|
"survey_trigger": "Kérdőív aktiválója",
|
||||||
"switch_multi_language_on_to_get_started": "Kapcsolja be a többnyelvűséget a kezdéshez 👉",
|
"switch_multi_language_on_to_get_started": "Kapcsolja be a többnyelvűséget a kezdéshez 👉",
|
||||||
@@ -2764,8 +2798,8 @@
|
|||||||
"evaluate_content_quality_question_2_placeholder": "Írja be ide a válaszát…",
|
"evaluate_content_quality_question_2_placeholder": "Írja be ide a válaszát…",
|
||||||
"evaluate_content_quality_question_3_headline": "Csodálatos! Van még valami, amit szeretne, hogy kitárgyaljunk?",
|
"evaluate_content_quality_question_3_headline": "Csodálatos! Van még valami, amit szeretne, hogy kitárgyaljunk?",
|
||||||
"evaluate_content_quality_question_3_placeholder": "Témák, trendek, oktatóanyagok…",
|
"evaluate_content_quality_question_3_placeholder": "Témák, trendek, oktatóanyagok…",
|
||||||
"fake_door_follow_up_description": "Követés olyan felhasználókkal, akik belefutottak az egyik „fake door” kísérletébe.",
|
"fake_door_follow_up_description": "Utókövetés olyan felhasználókkal, akik belefutottak az egyik „fake door” kísérletébe.",
|
||||||
"fake_door_follow_up_name": "„Fake door” követés",
|
"fake_door_follow_up_name": "„Fake door” utókövetés",
|
||||||
"fake_door_follow_up_question_1_headline": "Mennyire fontos ez a funkció az Ön számára?",
|
"fake_door_follow_up_question_1_headline": "Mennyire fontos ez a funkció az Ön számára?",
|
||||||
"fake_door_follow_up_question_1_lower_label": "Nem fontos",
|
"fake_door_follow_up_question_1_lower_label": "Nem fontos",
|
||||||
"fake_door_follow_up_question_1_upper_label": "Nagyon fontos",
|
"fake_door_follow_up_question_1_upper_label": "Nagyon fontos",
|
||||||
@@ -2774,7 +2808,7 @@
|
|||||||
"fake_door_follow_up_question_2_choice_3": "3. szempont",
|
"fake_door_follow_up_question_2_choice_3": "3. szempont",
|
||||||
"fake_door_follow_up_question_2_choice_4": "4. szempont",
|
"fake_door_follow_up_question_2_choice_4": "4. szempont",
|
||||||
"fake_door_follow_up_question_2_headline": "Mit kell feltétlenül tartalmaznia ennek összeállításakor?",
|
"fake_door_follow_up_question_2_headline": "Mit kell feltétlenül tartalmaznia ennek összeállításakor?",
|
||||||
"feature_chaser_description": "Követés olyan felhasználókkal, akik épp most használtak egy bizonyos funkciót.",
|
"feature_chaser_description": "Utókövetés olyan felhasználókkal, akik épp most használtak egy bizonyos funkciót.",
|
||||||
"feature_chaser_name": "Funkcióvadász",
|
"feature_chaser_name": "Funkcióvadász",
|
||||||
"feature_chaser_question_1_headline": "Mennyire fontos a [FUNKCIÓ HOZZÁADÁSA] az Ön számára?",
|
"feature_chaser_question_1_headline": "Mennyire fontos a [FUNKCIÓ HOZZÁADÁSA] az Ön számára?",
|
||||||
"feature_chaser_question_1_lower_label": "Nem fontos",
|
"feature_chaser_question_1_lower_label": "Nem fontos",
|
||||||
|
|||||||
+45
-11
@@ -167,6 +167,7 @@
|
|||||||
"connect": "接続",
|
"connect": "接続",
|
||||||
"connect_formbricks": "Formbricksを接続",
|
"connect_formbricks": "Formbricksを接続",
|
||||||
"connected": "接続済み",
|
"connected": "接続済み",
|
||||||
|
"contact": "連絡先",
|
||||||
"contacts": "連絡先",
|
"contacts": "連絡先",
|
||||||
"continue": "続行",
|
"continue": "続行",
|
||||||
"copied": "コピーしました",
|
"copied": "コピーしました",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "コピー",
|
"copy": "コピー",
|
||||||
"copy_code": "コードをコピー",
|
"copy_code": "コードをコピー",
|
||||||
"copy_link": "リンクをコピー",
|
"copy_link": "リンクをコピー",
|
||||||
|
"copy_to_environment": "{{environment}} にコピー",
|
||||||
"count_attributes": "{count, plural, other {{count} 個の属性}}",
|
"count_attributes": "{count, plural, other {{count} 個の属性}}",
|
||||||
"count_contacts": "{count, plural, other {{count} 件の連絡先}}",
|
"count_contacts": "{count, plural, other {{count} 件の連絡先}}",
|
||||||
"count_members": "{count, plural, other {{count} 名のメンバー}}",
|
"count_members": "{count, plural, other {{count} 名のメンバー}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(コピー {copyNumber})",
|
"duplicate_copy_number": "(コピー {copyNumber})",
|
||||||
"e_commerce": "Eコマース",
|
"e_commerce": "Eコマース",
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
|
"elements": "要素",
|
||||||
"email": "メールアドレス",
|
"email": "メールアドレス",
|
||||||
"ending_card": "終了カード",
|
"ending_card": "終了カード",
|
||||||
"enter_url": "URLを入力",
|
"enter_url": "URLを入力",
|
||||||
"enterprise_license": "エンタープライズライセンス",
|
"enterprise_license": "エンタープライズライセンス",
|
||||||
"environment": "環境",
|
"environment": "環境",
|
||||||
"environment_not_found": "環境が見つかりません",
|
|
||||||
"environment_notice": "現在、{environment} 環境にいます。",
|
"environment_notice": "現在、{environment} 環境にいます。",
|
||||||
"error": "エラー",
|
"error": "エラー",
|
||||||
"error_component_description": "この リソース は 存在 しない か、アクセス する ための 必要な 権限 が ありません。",
|
"error_component_description": "この リソース は 存在 しない か、アクセス する ための 必要な 権限 が ありません。",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "非アクティブなフォーム",
|
"inactive_surveys": "非アクティブなフォーム",
|
||||||
"integration": "連携",
|
"integration": "連携",
|
||||||
"integrations": "連携",
|
"integrations": "連携",
|
||||||
"invalid_date": "無効な日付です",
|
"invalid_date_with_value": "無効な日付です: {value}",
|
||||||
"invalid_file_name": "ファイル名が無効です。ファイル名を変更して再試行してください",
|
"invalid_file_name": "ファイル名が無効です。ファイル名を変更して再試行してください",
|
||||||
"invalid_file_type": "無効なファイルタイプです",
|
"invalid_file_type": "無効なファイルタイプです",
|
||||||
"invite": "招待",
|
"invite": "招待",
|
||||||
"invite_them": "招待する",
|
"invite_them": "招待する",
|
||||||
|
"javascript_required": "JavaScriptが必要です",
|
||||||
|
"javascript_required_description": "Formbricksを正常に動作させるには、JavaScriptが必要です。続行するには、ブラウザの設定でJavaScriptを有効にしてください。",
|
||||||
"key": "キー",
|
"key": "キー",
|
||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "マーケティング",
|
"marketing": "マーケティング",
|
||||||
"members": "メンバー",
|
"members": "メンバー",
|
||||||
"members_and_teams": "メンバー&チーム",
|
"members_and_teams": "メンバー&チーム",
|
||||||
|
"membership": "メンバーシップ",
|
||||||
"membership_not_found": "メンバーシップが見つかりません",
|
"membership_not_found": "メンバーシップが見つかりません",
|
||||||
|
"meta": "メタ",
|
||||||
"metadata": "メタデータ",
|
"metadata": "メタデータ",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
|
||||||
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
|
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "新規",
|
"new": "新規",
|
||||||
"new_version_available": "Formbricks {version} が利用可能です。今すぐアップグレード!",
|
"new_version_available": "Formbricks {version} が利用可能です。今すぐアップグレード!",
|
||||||
"next": "次へ",
|
"next": "次へ",
|
||||||
|
"no_actions_found": "アクションが見つかりません",
|
||||||
"no_background_image_found": "背景画像が見つかりません。",
|
"no_background_image_found": "背景画像が見つかりません。",
|
||||||
"no_code": "ノーコード",
|
"no_code": "ノーコード",
|
||||||
"no_files_uploaded": "ファイルがアップロードされていません",
|
"no_files_uploaded": "ファイルがアップロードされていません",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "または",
|
"or": "または",
|
||||||
"organization": "組織",
|
"organization": "組織",
|
||||||
"organization_id": "組織ID",
|
"organization_id": "組織ID",
|
||||||
"organization_not_found": "組織が見つかりません",
|
|
||||||
"organization_settings": "組織設定",
|
"organization_settings": "組織設定",
|
||||||
"organization_teams_not_found": "組織のチームが見つかりません",
|
|
||||||
"other": "その他",
|
"other": "その他",
|
||||||
|
"other_filters": "その他のフィルター",
|
||||||
"others": "その他",
|
"others": "その他",
|
||||||
"overlay_color": "オーバーレイの色",
|
"overlay_color": "オーバーレイの色",
|
||||||
"overview": "概要",
|
"overview": "概要",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "少なくとも1つのフォームを選択してください",
|
"please_select_at_least_one_survey": "少なくとも1つのフォームを選択してください",
|
||||||
"please_select_at_least_one_trigger": "少なくとも1つのトリガーを選択してください",
|
"please_select_at_least_one_trigger": "少なくとも1つのトリガーを選択してください",
|
||||||
"please_upgrade_your_plan": "プランをアップグレードしてください",
|
"please_upgrade_your_plan": "プランをアップグレードしてください",
|
||||||
|
"powered_by_formbricks": "Powered by Formbricks",
|
||||||
"preview": "プレビュー",
|
"preview": "プレビュー",
|
||||||
"preview_survey": "フォームをプレビュー",
|
"preview_survey": "フォームをプレビュー",
|
||||||
"privacy": "プライバシーポリシー",
|
"privacy": "プライバシーポリシー",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "選択",
|
"select": "選択",
|
||||||
"select_all": "すべて選択",
|
"select_all": "すべて選択",
|
||||||
"select_filter": "フィルターを選択",
|
"select_filter": "フィルターを選択",
|
||||||
|
"select_language": "言語を選択",
|
||||||
"select_survey": "フォームを選択",
|
"select_survey": "フォームを選択",
|
||||||
"select_teams": "チームを選択",
|
"select_teams": "チームを選択",
|
||||||
"selected": "選択済み",
|
"selected": "選択済み",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "フォームID",
|
"survey_id": "フォームID",
|
||||||
"survey_languages": "フォームの言語",
|
"survey_languages": "フォームの言語",
|
||||||
"survey_live": "フォーム公開中",
|
"survey_live": "フォーム公開中",
|
||||||
"survey_not_found": "フォームが見つかりません",
|
|
||||||
"survey_paused": "フォームは一時停止中です。",
|
"survey_paused": "フォームは一時停止中です。",
|
||||||
"survey_type": "フォームの種類",
|
"survey_type": "フォームの種類",
|
||||||
"surveys": "フォーム",
|
"surveys": "フォーム",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "チーム名",
|
"team_name": "チーム名",
|
||||||
"team_role": "チームの役割",
|
"team_role": "チームの役割",
|
||||||
"teams": "チーム",
|
"teams": "チーム",
|
||||||
"teams_not_found": "チームが見つかりません",
|
|
||||||
"text": "テキスト",
|
"text": "テキスト",
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
"time_to_finish": "所要時間",
|
"time_to_finish": "所要時間",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "ユーザー",
|
"user": "ユーザー",
|
||||||
"user_id": "ユーザーID",
|
"user_id": "ユーザーID",
|
||||||
"user_not_found": "ユーザーが見つかりません",
|
|
||||||
"variable": "変数",
|
"variable": "変数",
|
||||||
"variable_ids": "変数ID",
|
"variable_ids": "変数ID",
|
||||||
"variables": "変数",
|
"variables": "変数",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "週間",
|
"weeks": "週間",
|
||||||
"welcome_card": "ウェルカムカード",
|
"welcome_card": "ウェルカムカード",
|
||||||
"workflows": "ワークフロー",
|
"workflows": "ワークフロー",
|
||||||
|
"workspace": "ワークスペース",
|
||||||
"workspace_configuration": "ワークスペース設定",
|
"workspace_configuration": "ワークスペース設定",
|
||||||
"workspace_created_successfully": "ワークスペースが正常に作成されました",
|
"workspace_created_successfully": "ワークスペースが正常に作成されました",
|
||||||
"workspace_creation_description": "アクセス制御を改善するために、フォームをワークスペースで整理します。",
|
"workspace_creation_description": "アクセス制御を改善するために、フォームをワークスペースで整理します。",
|
||||||
"workspace_id": "ワークスペースID",
|
"workspace_id": "ワークスペースID",
|
||||||
"workspace_name": "ワークスペース名",
|
"workspace_name": "ワークスペース名",
|
||||||
"workspace_name_placeholder": "例: Formbricks",
|
"workspace_name_placeholder": "例: Formbricks",
|
||||||
"workspace_not_found": "ワークスペースが見つかりません",
|
|
||||||
"workspace_permission_not_found": "ワークスペースの権限が見つかりません",
|
|
||||||
"workspaces": "ワークスペース",
|
"workspaces": "ワークスペース",
|
||||||
"years": "年",
|
"years": "年",
|
||||||
"you": "あなた",
|
"you": "あなた",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "新しい属性“{key}”を型“{dataType}”で作成しました",
|
"attributes_msg_new_attribute_created": "新しい属性“{key}”を型“{dataType}”で作成しました",
|
||||||
"attributes_msg_userid_already_exists": "この環境にはすでにユーザーIDが存在するため、更新されませんでした。",
|
"attributes_msg_userid_already_exists": "この環境にはすでにユーザーIDが存在するため、更新されませんでした。",
|
||||||
"contact_deleted_successfully": "連絡先を正常に削除しました",
|
"contact_deleted_successfully": "連絡先を正常に削除しました",
|
||||||
"contact_not_found": "そのような連絡先は見つかりません",
|
|
||||||
"contacts_table_refresh": "連絡先を更新",
|
"contacts_table_refresh": "連絡先を更新",
|
||||||
"contacts_table_refresh_success": "連絡先を正常に更新しました",
|
"contacts_table_refresh_success": "連絡先を正常に更新しました",
|
||||||
"create_attribute": "属性を作成",
|
"create_attribute": "属性を作成",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "サードパーティによって作成",
|
"created_by_third_party": "サードパーティによって作成",
|
||||||
"discord_webhook_not_supported": "現在、Discord Webhook はサポートしていません。",
|
"discord_webhook_not_supported": "現在、Discord Webhook はサポートしていません。",
|
||||||
"empty_webhook_message": "Webhook は追加するとここに表示されます。⏲️",
|
"empty_webhook_message": "Webhook は追加するとここに表示されます。⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "不正なゲートウェイ (502): プロキシまたはゲートウェイのエラーにより、サービスに到達できません",
|
||||||
|
"endpoint_gateway_timeout_error": "ゲートウェイタイムアウト (504): ゲートウェイのタイムアウトにより、サービスに到達できません",
|
||||||
|
"endpoint_internal_server_error": "内部サーバーエラー (500): サービスで予期しないエラーが発生しました",
|
||||||
|
"endpoint_method_not_allowed_error": "許可されていないメソッド (405): エンドポイントは存在しますが、POST リクエストを受け付けません",
|
||||||
|
"endpoint_not_found_error": "見つかりません (404): エンドポイントが存在しません",
|
||||||
"endpoint_pinged": "成功!Webhook に ping できました。",
|
"endpoint_pinged": "成功!Webhook に ping できました。",
|
||||||
"endpoint_pinged_error": "Webhook への ping に失敗しました。",
|
"endpoint_pinged_error": "Webhook への ping に失敗しました。",
|
||||||
|
"endpoint_service_unavailable_error": "サービス利用不可 (503): サービスは一時的に停止しています",
|
||||||
"learn_to_verify": "Webhook署名の検証方法を学ぶ",
|
"learn_to_verify": "Webhook署名の検証方法を学ぶ",
|
||||||
|
"no_triggers": "トリガーなし",
|
||||||
"please_check_console": "詳細はコンソールを確認してください",
|
"please_check_console": "詳細はコンソールを確認してください",
|
||||||
"please_enter_a_url": "URL を入力してください",
|
"please_enter_a_url": "URL を入力してください",
|
||||||
"response_created": "回答作成",
|
"response_created": "回答作成",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "エンタープライズ機能",
|
"enterprise_features": "エンタープライズ機能",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "すべての機能にアクセスするには、エンタープライズライセンスを取得してください。",
|
"get_an_enterprise_license_to_get_access_to_all_features": "すべての機能にアクセスするには、エンタープライズライセンスを取得してください。",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "データのプライバシーとセキュリティを完全に制御できます。",
|
"keep_full_control_over_your_data_privacy_and_security": "データのプライバシーとセキュリティを完全に制御できます。",
|
||||||
|
"license_feature_access_control": "アクセス制御(RBAC)",
|
||||||
|
"license_feature_audit_logs": "監査ログ",
|
||||||
|
"license_feature_contacts": "連絡先とセグメント",
|
||||||
|
"license_feature_projects": "ワークスペース",
|
||||||
|
"license_feature_quotas": "クォータ",
|
||||||
|
"license_feature_remove_branding": "ブランディングの削除",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "スパム保護",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "二要素認証",
|
||||||
|
"license_feature_whitelabel": "ホワイトラベルメール",
|
||||||
|
"license_features_table_access": "アクセス",
|
||||||
|
"license_features_table_description": "このインスタンスで現在利用可能なエンタープライズ機能と制限。",
|
||||||
|
"license_features_table_disabled": "無効",
|
||||||
|
"license_features_table_enabled": "有効",
|
||||||
|
"license_features_table_feature": "機能",
|
||||||
|
"license_features_table_title": "ライセンス機能",
|
||||||
|
"license_features_table_unlimited": "無制限",
|
||||||
|
"license_features_table_value": "値",
|
||||||
"license_instance_mismatch_description": "このライセンスは現在、別のFormbricksインスタンスに紐付けられています。このインストールが再構築または移動された場合は、Formbricksサポートに連絡して、以前のインスタンスの紐付けを解除してもらってください。",
|
"license_instance_mismatch_description": "このライセンスは現在、別のFormbricksインスタンスに紐付けられています。このインストールが再構築または移動された場合は、Formbricksサポートに連絡して、以前のインスタンスの紐付けを解除してもらってください。",
|
||||||
"license_invalid_description": "ENTERPRISE_LICENSE_KEY環境変数のライセンスキーが無効です。入力ミスがないか確認するか、新しいキーをリクエストしてください。",
|
"license_invalid_description": "ENTERPRISE_LICENSE_KEY環境変数のライセンスキーが無効です。入力ミスがないか確認するか、新しいキーをリクエストしてください。",
|
||||||
"license_status": "ライセンスステータス",
|
"license_status": "ライセンスステータス",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "カスタムホスト名",
|
"custom_hostname": "カスタムホスト名",
|
||||||
"customize_survey_logo": "アンケートのロゴをカスタマイズする",
|
"customize_survey_logo": "アンケートのロゴをカスタマイズする",
|
||||||
"darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。",
|
"darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。",
|
||||||
"date_format": "日付形式",
|
|
||||||
"days_before_showing_this_survey_again": "最後に表示されたアンケートとこのアンケートを表示するまでに、この日数以上の期間を空ける必要があります。",
|
"days_before_showing_this_survey_again": "最後に表示されたアンケートとこのアンケートを表示するまでに、この日数以上の期間を空ける必要があります。",
|
||||||
"delete_anyways": "削除する",
|
"delete_anyways": "削除する",
|
||||||
"delete_block": "ブロックを削除",
|
"delete_block": "ブロックを削除",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "変更の保存中にエラーが発生しました",
|
"error_saving_changes": "変更の保存中にエラーが発生しました",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "複数の回答を許可;回答後も表示を継続(例:フィードボックス)。",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "複数の回答を許可;回答後も表示を継続(例:フィードボックス)。",
|
||||||
"everyone": "全員",
|
"everyone": "全員",
|
||||||
|
"expand_preview": "プレビューを展開",
|
||||||
"external_urls_paywall_tooltip": "外部URLをカスタマイズするには有料プランへのアップグレードが必要です。フィッシング防止のためご協力をお願いいたします。",
|
"external_urls_paywall_tooltip": "外部URLをカスタマイズするには有料プランへのアップグレードが必要です。フィッシング防止のためご協力をお願いいたします。",
|
||||||
"fallback_missing": "フォールバックがありません",
|
"fallback_missing": "フォールバックがありません",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "回答数の上限は、受信済みの回答数 ({responseCount}) を超える必要があります。",
|
"response_limit_needs_to_exceed_number_of_received_responses": "回答数の上限は、受信済みの回答数 ({responseCount}) を超える必要があります。",
|
||||||
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
|
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
|
||||||
"response_options": "回答オプション",
|
"response_options": "回答オプション",
|
||||||
|
"reverse_order_occasionally": "順序をランダムに逆転",
|
||||||
|
"reverse_order_occasionally_except_last": "最後以外の順序をランダムに逆転",
|
||||||
"roundness": "丸み",
|
"roundness": "丸み",
|
||||||
"roundness_description": "角の丸みを調整します。",
|
"roundness_description": "角の丸みを調整します。",
|
||||||
"row_used_in_logic_error": "この行は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
"row_used_in_logic_error": "この行は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "フォームの最大表示回数",
|
"show_survey_maximum_of": "フォームの最大表示回数",
|
||||||
"show_survey_to_users": "ユーザーの {percentage}% にフォームを表示",
|
"show_survey_to_users": "ユーザーの {percentage}% にフォームを表示",
|
||||||
"show_to_x_percentage_of_targeted_users": "ターゲットユーザーの {percentage}% に表示",
|
"show_to_x_percentage_of_targeted_users": "ターゲットユーザーの {percentage}% に表示",
|
||||||
|
"shrink_preview": "プレビューを縮小",
|
||||||
"simple": "シンプル",
|
"simple": "シンプル",
|
||||||
"six_points": "6点",
|
"six_points": "6点",
|
||||||
"smiley": "スマイリー",
|
"smiley": "スマイリー",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "スタイルをテーマのスタイルに設定しました",
|
"styling_set_to_theme_styles": "スタイルをテーマのスタイルに設定しました",
|
||||||
"subheading": "サブ見出し",
|
"subheading": "サブ見出し",
|
||||||
"subtract": "減算 -",
|
"subtract": "減算 -",
|
||||||
|
"survey_closed_message_heading_required": "カスタムアンケート終了メッセージに見出しを追加してください。",
|
||||||
"survey_completed_heading": "フォームが完了しました",
|
"survey_completed_heading": "フォームが完了しました",
|
||||||
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
|
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
|
||||||
"survey_display_settings": "フォーム表示設定",
|
"survey_display_settings": "フォーム表示設定",
|
||||||
"survey_placement": "フォームの配置",
|
"survey_placement": "フォームの配置",
|
||||||
|
"survey_preview": "アンケートプレビュー 👀",
|
||||||
"survey_styling": "フォームのスタイル",
|
"survey_styling": "フォームのスタイル",
|
||||||
"survey_trigger": "フォームのトリガー",
|
"survey_trigger": "フォームのトリガー",
|
||||||
"switch_multi_language_on_to_get_started": "多言語機能をオンにして開始 👉",
|
"switch_multi_language_on_to_get_started": "多言語機能をオンにして開始 👉",
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Verbinden",
|
"connect": "Verbinden",
|
||||||
"connect_formbricks": "Sluit Formbricks aan",
|
"connect_formbricks": "Sluit Formbricks aan",
|
||||||
"connected": "Aangesloten",
|
"connected": "Aangesloten",
|
||||||
|
"contact": "Contact",
|
||||||
"contacts": "Contacten",
|
"contacts": "Contacten",
|
||||||
"continue": "Doorgaan",
|
"continue": "Doorgaan",
|
||||||
"copied": "Gekopieerd",
|
"copied": "Gekopieerd",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Kopiëren",
|
"copy": "Kopiëren",
|
||||||
"copy_code": "Kopieer code",
|
"copy_code": "Kopieer code",
|
||||||
"copy_link": "Kopieer link",
|
"copy_link": "Kopieer link",
|
||||||
|
"copy_to_environment": "Kopiëren naar {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} attribuut} other {{count} attributen}}",
|
"count_attributes": "{count, plural, one {{count} attribuut} other {{count} attributen}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacten}}",
|
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacten}}",
|
||||||
"count_members": "{count, plural, one {{count} lid} other {{count} leden}}",
|
"count_members": "{count, plural, one {{count} lid} other {{count} leden}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(kopie {copyNumber})",
|
"duplicate_copy_number": "(kopie {copyNumber})",
|
||||||
"e_commerce": "E-commerce",
|
"e_commerce": "E-commerce",
|
||||||
"edit": "Bewerking",
|
"edit": "Bewerking",
|
||||||
|
"elements": "Elementen",
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
"ending_card": "Einde kaart",
|
"ending_card": "Einde kaart",
|
||||||
"enter_url": "URL invoeren",
|
"enter_url": "URL invoeren",
|
||||||
"enterprise_license": "Enterprise-licentie",
|
"enterprise_license": "Enterprise-licentie",
|
||||||
"environment": "Omgeving",
|
"environment": "Omgeving",
|
||||||
"environment_not_found": "Omgeving niet gevonden",
|
|
||||||
"environment_notice": "U bevindt zich momenteel in de {environment}-omgeving.",
|
"environment_notice": "U bevindt zich momenteel in de {environment}-omgeving.",
|
||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
"error_component_description": "Deze bron bestaat niet of u beschikt niet over de benodigde toegangsrechten.",
|
"error_component_description": "Deze bron bestaat niet of u beschikt niet over de benodigde toegangsrechten.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Inactieve enquêtes",
|
"inactive_surveys": "Inactieve enquêtes",
|
||||||
"integration": "integratie",
|
"integration": "integratie",
|
||||||
"integrations": "Integraties",
|
"integrations": "Integraties",
|
||||||
"invalid_date": "Ongeldige datum",
|
"invalid_date_with_value": "Ongeldige datum: {value}",
|
||||||
"invalid_file_name": "Ongeldige bestandsnaam. Hernoem uw bestand en probeer het opnieuw",
|
"invalid_file_name": "Ongeldige bestandsnaam. Hernoem uw bestand en probeer het opnieuw",
|
||||||
"invalid_file_type": "Ongeldig bestandstype",
|
"invalid_file_type": "Ongeldig bestandstype",
|
||||||
"invite": "Uitnodiging",
|
"invite": "Uitnodiging",
|
||||||
"invite_them": "Nodig ze uit",
|
"invite_them": "Nodig ze uit",
|
||||||
|
"javascript_required": "JavaScript vereist",
|
||||||
|
"javascript_required_description": "Formbricks heeft JavaScript nodig om correct te functioneren. Schakel JavaScript in je browserinstellingen in om door te gaan.",
|
||||||
"key": "Sleutel",
|
"key": "Sleutel",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Leden",
|
"members": "Leden",
|
||||||
"members_and_teams": "Leden & teams",
|
"members_and_teams": "Leden & teams",
|
||||||
|
"membership": "Lidmaatschap",
|
||||||
"membership_not_found": "Lidmaatschap niet gevonden",
|
"membership_not_found": "Lidmaatschap niet gevonden",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metagegevens",
|
"metadata": "Metagegevens",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks werkt het beste op een groter scherm. Schakel over naar een ander apparaat om enquêtes te beheren of samen te stellen.",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks werkt het beste op een groter scherm. Schakel over naar een ander apparaat om enquêtes te beheren of samen te stellen.",
|
||||||
"mobile_overlay_surveys_look_good": "Maakt u zich geen zorgen: uw enquêtes zien er geweldig uit op elk apparaat en schermformaat!",
|
"mobile_overlay_surveys_look_good": "Maakt u zich geen zorgen: uw enquêtes zien er geweldig uit op elk apparaat en schermformaat!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Nieuw",
|
"new": "Nieuw",
|
||||||
"new_version_available": "Formbricks {version} is hier. Upgrade nu!",
|
"new_version_available": "Formbricks {version} is hier. Upgrade nu!",
|
||||||
"next": "Volgende",
|
"next": "Volgende",
|
||||||
|
"no_actions_found": "Geen acties gevonden",
|
||||||
"no_background_image_found": "Geen achtergrondafbeelding gevonden.",
|
"no_background_image_found": "Geen achtergrondafbeelding gevonden.",
|
||||||
"no_code": "Geen code",
|
"no_code": "Geen code",
|
||||||
"no_files_uploaded": "Er zijn geen bestanden geüpload",
|
"no_files_uploaded": "Er zijn geen bestanden geüpload",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "of",
|
"or": "of",
|
||||||
"organization": "Organisatie",
|
"organization": "Organisatie",
|
||||||
"organization_id": "Organisatie-ID",
|
"organization_id": "Organisatie-ID",
|
||||||
"organization_not_found": "Organisatie niet gevonden",
|
|
||||||
"organization_settings": "Organisatie-instellingen",
|
"organization_settings": "Organisatie-instellingen",
|
||||||
"organization_teams_not_found": "Organisatieteams niet gevonden",
|
|
||||||
"other": "Ander",
|
"other": "Ander",
|
||||||
|
"other_filters": "Overige filters",
|
||||||
"others": "Anderen",
|
"others": "Anderen",
|
||||||
"overlay_color": "Overlaykleur",
|
"overlay_color": "Overlaykleur",
|
||||||
"overview": "Overzicht",
|
"overview": "Overzicht",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Selecteer ten minste één enquête",
|
"please_select_at_least_one_survey": "Selecteer ten minste één enquête",
|
||||||
"please_select_at_least_one_trigger": "Selecteer ten minste één trigger",
|
"please_select_at_least_one_trigger": "Selecteer ten minste één trigger",
|
||||||
"please_upgrade_your_plan": "Upgrade je abonnement",
|
"please_upgrade_your_plan": "Upgrade je abonnement",
|
||||||
|
"powered_by_formbricks": "Mogelijk gemaakt door Formbricks",
|
||||||
"preview": "Voorbeeld",
|
"preview": "Voorbeeld",
|
||||||
"preview_survey": "Voorbeeld van enquête",
|
"preview_survey": "Voorbeeld van enquête",
|
||||||
"privacy": "Privacybeleid",
|
"privacy": "Privacybeleid",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Selecteer",
|
"select": "Selecteer",
|
||||||
"select_all": "Selecteer alles",
|
"select_all": "Selecteer alles",
|
||||||
"select_filter": "Filter selecteren",
|
"select_filter": "Filter selecteren",
|
||||||
|
"select_language": "Selecteer taal",
|
||||||
"select_survey": "Selecteer Enquête",
|
"select_survey": "Selecteer Enquête",
|
||||||
"select_teams": "Selecteer teams",
|
"select_teams": "Selecteer teams",
|
||||||
"selected": "Gekozen",
|
"selected": "Gekozen",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "Enquête-ID",
|
"survey_id": "Enquête-ID",
|
||||||
"survey_languages": "Enquêtetalen",
|
"survey_languages": "Enquêtetalen",
|
||||||
"survey_live": "Enquête live",
|
"survey_live": "Enquête live",
|
||||||
"survey_not_found": "Enquête niet gevonden",
|
|
||||||
"survey_paused": "Enquête onderbroken.",
|
"survey_paused": "Enquête onderbroken.",
|
||||||
"survey_type": "Enquêtetype",
|
"survey_type": "Enquêtetype",
|
||||||
"surveys": "Enquêtes",
|
"surveys": "Enquêtes",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Teamnaam",
|
"team_name": "Teamnaam",
|
||||||
"team_role": "Teamrol",
|
"team_role": "Teamrol",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"teams_not_found": "Teams niet gevonden",
|
|
||||||
"text": "Tekst",
|
"text": "Tekst",
|
||||||
"time": "Tijd",
|
"time": "Tijd",
|
||||||
"time_to_finish": "Tijd om af te ronden",
|
"time_to_finish": "Tijd om af te ronden",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Gebruiker",
|
"user": "Gebruiker",
|
||||||
"user_id": "Gebruikers-ID",
|
"user_id": "Gebruikers-ID",
|
||||||
"user_not_found": "Gebruiker niet gevonden",
|
|
||||||
"variable": "Variabel",
|
"variable": "Variabel",
|
||||||
"variable_ids": "Variabele ID's",
|
"variable_ids": "Variabele ID's",
|
||||||
"variables": "Variabelen",
|
"variables": "Variabelen",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "weken",
|
"weeks": "weken",
|
||||||
"welcome_card": "Welkomstkaart",
|
"welcome_card": "Welkomstkaart",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
"workspace": "Werkruimte",
|
||||||
"workspace_configuration": "Werkruimte-configuratie",
|
"workspace_configuration": "Werkruimte-configuratie",
|
||||||
"workspace_created_successfully": "Project succesvol aangemaakt",
|
"workspace_created_successfully": "Project succesvol aangemaakt",
|
||||||
"workspace_creation_description": "Organiseer enquêtes in werkruimtes voor beter toegangsbeheer.",
|
"workspace_creation_description": "Organiseer enquêtes in werkruimtes voor beter toegangsbeheer.",
|
||||||
"workspace_id": "Werkruimte-ID",
|
"workspace_id": "Werkruimte-ID",
|
||||||
"workspace_name": "Werkruimtenaam",
|
"workspace_name": "Werkruimtenaam",
|
||||||
"workspace_name_placeholder": "bijv. Formbricks",
|
"workspace_name_placeholder": "bijv. Formbricks",
|
||||||
"workspace_not_found": "Werkruimte niet gevonden",
|
|
||||||
"workspace_permission_not_found": "Werkruimte-machtiging niet gevonden",
|
|
||||||
"workspaces": "Werkruimtes",
|
"workspaces": "Werkruimtes",
|
||||||
"years": "jaren",
|
"years": "jaren",
|
||||||
"you": "Jij",
|
"you": "Jij",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Nieuw attribuut “{key}” aangemaakt met type “{dataType}”",
|
"attributes_msg_new_attribute_created": "Nieuw attribuut “{key}” aangemaakt met type “{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "De gebruikers-ID bestaat al voor deze omgeving en is niet bijgewerkt.",
|
"attributes_msg_userid_already_exists": "De gebruikers-ID bestaat al voor deze omgeving en is niet bijgewerkt.",
|
||||||
"contact_deleted_successfully": "Contact succesvol verwijderd",
|
"contact_deleted_successfully": "Contact succesvol verwijderd",
|
||||||
"contact_not_found": "Er is geen dergelijk contact gevonden",
|
|
||||||
"contacts_table_refresh": "Vernieuw contacten",
|
"contacts_table_refresh": "Vernieuw contacten",
|
||||||
"contacts_table_refresh_success": "Contacten zijn vernieuwd",
|
"contacts_table_refresh_success": "Contacten zijn vernieuwd",
|
||||||
"create_attribute": "Attribuut aanmaken",
|
"create_attribute": "Attribuut aanmaken",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Gemaakt door een derde partij",
|
"created_by_third_party": "Gemaakt door een derde partij",
|
||||||
"discord_webhook_not_supported": "Discord-webhooks worden momenteel niet ondersteund.",
|
"discord_webhook_not_supported": "Discord-webhooks worden momenteel niet ondersteund.",
|
||||||
"empty_webhook_message": "Uw webhooks verschijnen hier zodra u ze toevoegt. ⏲️",
|
"empty_webhook_message": "Uw webhooks verschijnen hier zodra u ze toevoegt. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Ongeldige gateway (502): Proxy-/gatewayfout, service niet bereikbaar",
|
||||||
|
"endpoint_gateway_timeout_error": "Gateway-time-out (504): Gateway-time-out, service niet bereikbaar",
|
||||||
|
"endpoint_internal_server_error": "Interne serverfout (500): De service is een onverwachte fout tegengekomen",
|
||||||
|
"endpoint_method_not_allowed_error": "Methode niet toegestaan (405): Het endpoint bestaat, maar accepteert geen POST-verzoeken",
|
||||||
|
"endpoint_not_found_error": "Niet gevonden (404): Het endpoint bestaat niet",
|
||||||
"endpoint_pinged": "Jawel! We kunnen de webhook pingen!",
|
"endpoint_pinged": "Jawel! We kunnen de webhook pingen!",
|
||||||
"endpoint_pinged_error": "Kan de webhook niet pingen!",
|
"endpoint_pinged_error": "Kan de webhook niet pingen!",
|
||||||
|
"endpoint_service_unavailable_error": "Service niet beschikbaar (503): De service is tijdelijk niet beschikbaar",
|
||||||
"learn_to_verify": "Leer hoe je webhook-handtekeningen kunt verifiëren",
|
"learn_to_verify": "Leer hoe je webhook-handtekeningen kunt verifiëren",
|
||||||
|
"no_triggers": "Geen triggers",
|
||||||
"please_check_console": "Controleer de console voor meer details",
|
"please_check_console": "Controleer de console voor meer details",
|
||||||
"please_enter_a_url": "Voer een URL in",
|
"please_enter_a_url": "Voer een URL in",
|
||||||
"response_created": "Reactie gemaakt",
|
"response_created": "Reactie gemaakt",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Enterprise-functies",
|
"enterprise_features": "Enterprise-functies",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Ontvang een Enterprise-licentie om toegang te krijgen tot alle functies.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Ontvang een Enterprise-licentie om toegang te krijgen tot alle functies.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Houd de volledige controle over de privacy en beveiliging van uw gegevens.",
|
"keep_full_control_over_your_data_privacy_and_security": "Houd de volledige controle over de privacy en beveiliging van uw gegevens.",
|
||||||
|
"license_feature_access_control": "Toegangscontrole (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Auditlogboeken",
|
||||||
|
"license_feature_contacts": "Contacten & Segmenten",
|
||||||
|
"license_feature_projects": "Werkruimtes",
|
||||||
|
"license_feature_quotas": "Quota's",
|
||||||
|
"license_feature_remove_branding": "Branding verwijderen",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Spambescherming",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Tweefactorauthenticatie",
|
||||||
|
"license_feature_whitelabel": "Whitelabel-e-mails",
|
||||||
|
"license_features_table_access": "Toegang",
|
||||||
|
"license_features_table_description": "Enterprise-functies en limieten die momenteel beschikbaar zijn voor deze instantie.",
|
||||||
|
"license_features_table_disabled": "Uitgeschakeld",
|
||||||
|
"license_features_table_enabled": "Ingeschakeld",
|
||||||
|
"license_features_table_feature": "Functie",
|
||||||
|
"license_features_table_title": "Gelicentieerde Functies",
|
||||||
|
"license_features_table_unlimited": "Onbeperkt",
|
||||||
|
"license_features_table_value": "Waarde",
|
||||||
"license_instance_mismatch_description": "Deze licentie is momenteel gekoppeld aan een andere Formbricks-instantie. Als deze installatie is herbouwd of verplaatst, vraag dan Formbricks-support om de vorige instantiekoppeling te verbreken.",
|
"license_instance_mismatch_description": "Deze licentie is momenteel gekoppeld aan een andere Formbricks-instantie. Als deze installatie is herbouwd of verplaatst, vraag dan Formbricks-support om de vorige instantiekoppeling te verbreken.",
|
||||||
"license_invalid_description": "De licentiesleutel in je ENTERPRISE_LICENSE_KEY omgevingsvariabele is niet geldig. Controleer op typefouten of vraag een nieuwe sleutel aan.",
|
"license_invalid_description": "De licentiesleutel in je ENTERPRISE_LICENSE_KEY omgevingsvariabele is niet geldig. Controleer op typefouten of vraag een nieuwe sleutel aan.",
|
||||||
"license_status": "Licentiestatus",
|
"license_status": "Licentiestatus",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Aangepaste hostnaam",
|
"custom_hostname": "Aangepaste hostnaam",
|
||||||
"customize_survey_logo": "Pas het enquêtelogo aan",
|
"customize_survey_logo": "Pas het enquêtelogo aan",
|
||||||
"darken_or_lighten_background_of_your_choice": "Maak de achtergrond naar keuze donkerder of lichter.",
|
"darken_or_lighten_background_of_your_choice": "Maak de achtergrond naar keuze donkerder of lichter.",
|
||||||
"date_format": "Datumformaat",
|
|
||||||
"days_before_showing_this_survey_again": "of meer dagen moeten verstrijken tussen de laatst getoonde enquête en het tonen van deze enquête.",
|
"days_before_showing_this_survey_again": "of meer dagen moeten verstrijken tussen de laatst getoonde enquête en het tonen van deze enquête.",
|
||||||
"delete_anyways": "Toch verwijderen",
|
"delete_anyways": "Toch verwijderen",
|
||||||
"delete_block": "Blok verwijderen",
|
"delete_block": "Blok verwijderen",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Fout bij het opslaan van wijzigingen",
|
"error_saving_changes": "Fout bij het opslaan van wijzigingen",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Meerdere reacties toestaan; blijf tonen, zelfs na een reactie (bijv. feedbackbox).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Meerdere reacties toestaan; blijf tonen, zelfs na een reactie (bijv. feedbackbox).",
|
||||||
"everyone": "Iedereen",
|
"everyone": "Iedereen",
|
||||||
|
"expand_preview": "Voorbeeld uitvouwen",
|
||||||
"external_urls_paywall_tooltip": "Upgrade naar een betaald abonnement om externe URL's aan te passen. Dit helpt om phishing te voorkomen.",
|
"external_urls_paywall_tooltip": "Upgrade naar een betaald abonnement om externe URL's aan te passen. Dit helpt om phishing te voorkomen.",
|
||||||
"fallback_missing": "Terugval ontbreekt",
|
"fallback_missing": "Terugval ontbreekt",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "De responslimiet moet groter zijn dan het aantal ontvangen reacties ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "De responslimiet moet groter zijn dan het aantal ontvangen reacties ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Reactielimieten, omleidingen en meer.",
|
"response_limits_redirections_and_more": "Reactielimieten, omleidingen en meer.",
|
||||||
"response_options": "Reactieopties",
|
"response_options": "Reactieopties",
|
||||||
|
"reverse_order_occasionally": "Volgorde af en toe omkeren",
|
||||||
|
"reverse_order_occasionally_except_last": "Volgorde af en toe omkeren behalve laatste",
|
||||||
"roundness": "Rondheid",
|
"roundness": "Rondheid",
|
||||||
"roundness_description": "Bepaalt hoe afgerond de hoeken zijn.",
|
"roundness_description": "Bepaalt hoe afgerond de hoeken zijn.",
|
||||||
"row_used_in_logic_error": "Deze rij wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
"row_used_in_logic_error": "Deze rij wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Toon onderzoek maximaal",
|
"show_survey_maximum_of": "Toon onderzoek maximaal",
|
||||||
"show_survey_to_users": "Enquête tonen aan % van de gebruikers",
|
"show_survey_to_users": "Enquête tonen aan % van de gebruikers",
|
||||||
"show_to_x_percentage_of_targeted_users": "Toon aan {percentage}% van de getargete gebruikers",
|
"show_to_x_percentage_of_targeted_users": "Toon aan {percentage}% van de getargete gebruikers",
|
||||||
|
"shrink_preview": "Voorbeeld invouwen",
|
||||||
"simple": "Eenvoudig",
|
"simple": "Eenvoudig",
|
||||||
"six_points": "6 punten",
|
"six_points": "6 punten",
|
||||||
"smiley": "Smiley",
|
"smiley": "Smiley",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Styling ingesteld op themastijlen",
|
"styling_set_to_theme_styles": "Styling ingesteld op themastijlen",
|
||||||
"subheading": "Ondertitel",
|
"subheading": "Ondertitel",
|
||||||
"subtract": "Aftrekken -",
|
"subtract": "Aftrekken -",
|
||||||
|
"survey_closed_message_heading_required": "Voeg een kop toe aan het aangepaste bericht voor gesloten enquêtes.",
|
||||||
"survey_completed_heading": "Enquête voltooid",
|
"survey_completed_heading": "Enquête voltooid",
|
||||||
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
|
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
|
||||||
"survey_display_settings": "Enquêteweergave-instellingen",
|
"survey_display_settings": "Enquêteweergave-instellingen",
|
||||||
"survey_placement": "Enquête plaatsing",
|
"survey_placement": "Enquête plaatsing",
|
||||||
|
"survey_preview": "Enquêtevoorbeeld 👀",
|
||||||
"survey_styling": "Vorm styling",
|
"survey_styling": "Vorm styling",
|
||||||
"survey_trigger": "Enquêtetrigger",
|
"survey_trigger": "Enquêtetrigger",
|
||||||
"switch_multi_language_on_to_get_started": "Schakel meertaligheid in om te beginnen 👉",
|
"switch_multi_language_on_to_get_started": "Schakel meertaligheid in om te beginnen 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Nee, dank je!",
|
"preview_survey_question_2_choice_2_label": "Nee, dank je!",
|
||||||
"preview_survey_question_2_headline": "Wil je op de hoogte blijven?",
|
"preview_survey_question_2_headline": "Wil je op de hoogte blijven?",
|
||||||
"preview_survey_question_2_subheader": "Dit is een voorbeeldbeschrijving.",
|
"preview_survey_question_2_subheader": "Dit is een voorbeeldbeschrijving.",
|
||||||
"preview_survey_question_open_text_headline": "Wil je nog iets delen?",
|
"preview_survey_question_open_text_headline": "Wilt u nog iets anders delen?",
|
||||||
"preview_survey_question_open_text_placeholder": "Typ hier je antwoord...",
|
"preview_survey_question_open_text_placeholder": "Typ hier je antwoord...",
|
||||||
"preview_survey_question_open_text_subheader": "Je feedback helpt ons verbeteren.",
|
"preview_survey_question_open_text_subheader": "Je feedback helpt ons verbeteren.",
|
||||||
"preview_survey_welcome_card_headline": "Welkom!",
|
"preview_survey_welcome_card_headline": "Welkom!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Bedankt voor het delen van je workflow-idee met ons! We zijn momenteel bezig met het ontwerpen van deze functie en jouw feedback helpt ons om precies te bouwen wat je nodig hebt.",
|
"coming_soon_description": "Bedankt voor het delen van je workflow-idee met ons! We zijn momenteel bezig met het ontwerpen van deze functie en jouw feedback helpt ons om precies te bouwen wat je nodig hebt.",
|
||||||
"coming_soon_title": "We zijn er bijna!",
|
"coming_soon_title": "We zijn er bijna!",
|
||||||
"follow_up_label": "Is er nog iets dat je wilt toevoegen?",
|
"follow_up_label": "Is er nog iets dat u wilt toevoegen?",
|
||||||
"follow_up_placeholder": "Welke specifieke taken wil je automatiseren? Zijn er tools of integraties die je wilt meenemen?",
|
"follow_up_placeholder": "Welke specifieke taken wil je automatiseren? Zijn er tools of integraties die je wilt meenemen?",
|
||||||
"generate_button": "Genereer workflow",
|
"generate_button": "Genereer workflow",
|
||||||
"heading": "Welke workflow wil je maken?",
|
"heading": "Welke workflow wil je maken?",
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Conectar",
|
"connect": "Conectar",
|
||||||
"connect_formbricks": "Conectar Formbricks",
|
"connect_formbricks": "Conectar Formbricks",
|
||||||
"connected": "conectado",
|
"connected": "conectado",
|
||||||
|
"contact": "Contato",
|
||||||
"contacts": "Contatos",
|
"contacts": "Contatos",
|
||||||
"continue": "Continuar",
|
"continue": "Continuar",
|
||||||
"copied": "Copiado",
|
"copied": "Copiado",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copy_code": "Copiar código",
|
"copy_code": "Copiar código",
|
||||||
"copy_link": "Copiar Link",
|
"copy_link": "Copiar Link",
|
||||||
|
"copy_to_environment": "Copiar para {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contato} other {{count} contatos}}",
|
"count_contacts": "{count, plural, one {{count} contato} other {{count} contatos}}",
|
||||||
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(cópia {copyNumber})",
|
"duplicate_copy_number": "(cópia {copyNumber})",
|
||||||
"e_commerce": "comércio eletrônico",
|
"e_commerce": "comércio eletrônico",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
|
"elements": "Elementos",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Cartão de encerramento",
|
"ending_card": "Cartão de encerramento",
|
||||||
"enter_url": "Inserir URL",
|
"enter_url": "Inserir URL",
|
||||||
"enterprise_license": "Licença Empresarial",
|
"enterprise_license": "Licença Empresarial",
|
||||||
"environment": "Ambiente",
|
"environment": "Ambiente",
|
||||||
"environment_not_found": "Ambiente não encontrado",
|
|
||||||
"environment_notice": "Você está atualmente no ambiente {environment}.",
|
"environment_notice": "Você está atualmente no ambiente {environment}.",
|
||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
|
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Pesquisas inativas",
|
"inactive_surveys": "Pesquisas inativas",
|
||||||
"integration": "integração",
|
"integration": "integração",
|
||||||
"integrations": "Integrações",
|
"integrations": "Integrações",
|
||||||
"invalid_date": "Data inválida",
|
"invalid_date_with_value": "Data inválida: {value}",
|
||||||
"invalid_file_name": "Nome de arquivo inválido, por favor renomeie seu arquivo e tente novamente",
|
"invalid_file_name": "Nome de arquivo inválido, por favor renomeie seu arquivo e tente novamente",
|
||||||
"invalid_file_type": "Tipo de arquivo inválido",
|
"invalid_file_type": "Tipo de arquivo inválido",
|
||||||
"invite": "convidar",
|
"invite": "convidar",
|
||||||
"invite_them": "Convida eles",
|
"invite_them": "Convida eles",
|
||||||
|
"javascript_required": "JavaScript Necessário",
|
||||||
|
"javascript_required_description": "O Formbricks precisa do JavaScript para funcionar corretamente. Por favor, ative o JavaScript nas configurações do seu navegador para continuar.",
|
||||||
"key": "Chave",
|
"key": "Chave",
|
||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Língua",
|
"language": "Língua",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "marketing",
|
"marketing": "marketing",
|
||||||
"members": "Membros",
|
"members": "Membros",
|
||||||
"members_and_teams": "Membros e equipes",
|
"members_and_teams": "Membros e equipes",
|
||||||
|
"membership": "Associação",
|
||||||
"membership_not_found": "Assinatura não encontrada",
|
"membership_not_found": "Assinatura não encontrada",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "metadados",
|
"metadata": "metadados",
|
||||||
"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_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_surveys_look_good": "Não se preocupe – suas pesquisas ficam ótimas em qualquer dispositivo e tamanho de tela!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Novo",
|
"new": "Novo",
|
||||||
"new_version_available": "Formbricks {version} chegou. Atualize agora!",
|
"new_version_available": "Formbricks {version} chegou. Atualize agora!",
|
||||||
"next": "Próximo",
|
"next": "Próximo",
|
||||||
|
"no_actions_found": "Nenhuma ação encontrada",
|
||||||
"no_background_image_found": "Imagem de fundo não encontrada.",
|
"no_background_image_found": "Imagem de fundo não encontrada.",
|
||||||
"no_code": "Sem código",
|
"no_code": "Sem código",
|
||||||
"no_files_uploaded": "Nenhum arquivo foi enviado",
|
"no_files_uploaded": "Nenhum arquivo foi enviado",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "ou",
|
"or": "ou",
|
||||||
"organization": "organização",
|
"organization": "organização",
|
||||||
"organization_id": "ID da Organização",
|
"organization_id": "ID da Organização",
|
||||||
"organization_not_found": "Organização não encontrada",
|
|
||||||
"organization_settings": "Configurações da Organização",
|
"organization_settings": "Configurações da Organização",
|
||||||
"organization_teams_not_found": "Equipes da organização não encontradas",
|
|
||||||
"other": "outro",
|
"other": "outro",
|
||||||
|
"other_filters": "Outros Filtros",
|
||||||
"others": "Outros",
|
"others": "Outros",
|
||||||
"overlay_color": "Cor da sobreposição",
|
"overlay_color": "Cor da sobreposição",
|
||||||
"overview": "Visão Geral",
|
"overview": "Visão Geral",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Por favor, selecione pelo menos uma pesquisa",
|
"please_select_at_least_one_survey": "Por favor, selecione pelo menos uma pesquisa",
|
||||||
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
|
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
|
||||||
"please_upgrade_your_plan": "Por favor, atualize seu plano",
|
"please_upgrade_your_plan": "Por favor, atualize seu plano",
|
||||||
|
"powered_by_formbricks": "Desenvolvido por Formbricks",
|
||||||
"preview": "Prévia",
|
"preview": "Prévia",
|
||||||
"preview_survey": "Prévia da Pesquisa",
|
"preview_survey": "Prévia da Pesquisa",
|
||||||
"privacy": "Política de Privacidade",
|
"privacy": "Política de Privacidade",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Selecionar",
|
"select": "Selecionar",
|
||||||
"select_all": "Selecionar tudo",
|
"select_all": "Selecionar tudo",
|
||||||
"select_filter": "Selecionar filtro",
|
"select_filter": "Selecionar filtro",
|
||||||
|
"select_language": "Selecionar Idioma",
|
||||||
"select_survey": "Selecionar Pesquisa",
|
"select_survey": "Selecionar Pesquisa",
|
||||||
"select_teams": "Selecionar times",
|
"select_teams": "Selecionar times",
|
||||||
"selected": "Selecionado",
|
"selected": "Selecionado",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "ID da Pesquisa",
|
"survey_id": "ID da Pesquisa",
|
||||||
"survey_languages": "Idiomas da Pesquisa",
|
"survey_languages": "Idiomas da Pesquisa",
|
||||||
"survey_live": "Pesquisa ao vivo",
|
"survey_live": "Pesquisa ao vivo",
|
||||||
"survey_not_found": "Pesquisa não encontrada",
|
|
||||||
"survey_paused": "Pesquisa pausada.",
|
"survey_paused": "Pesquisa pausada.",
|
||||||
"survey_type": "Tipo de Pesquisa",
|
"survey_type": "Tipo de Pesquisa",
|
||||||
"surveys": "Pesquisas",
|
"surveys": "Pesquisas",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Nome da equipe",
|
"team_name": "Nome da equipe",
|
||||||
"team_role": "Função na equipe",
|
"team_role": "Função na equipe",
|
||||||
"teams": "Equipes",
|
"teams": "Equipes",
|
||||||
"teams_not_found": "Equipes não encontradas",
|
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
"time": "tempo",
|
"time": "tempo",
|
||||||
"time_to_finish": "Hora de terminar",
|
"time_to_finish": "Hora de terminar",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Usuário",
|
"user": "Usuário",
|
||||||
"user_id": "ID do usuário",
|
"user_id": "ID do usuário",
|
||||||
"user_not_found": "Usuário não encontrado",
|
|
||||||
"variable": "variável",
|
"variable": "variável",
|
||||||
"variable_ids": "IDs de variáveis",
|
"variable_ids": "IDs de variáveis",
|
||||||
"variables": "Variáveis",
|
"variables": "Variáveis",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "semanas",
|
"weeks": "semanas",
|
||||||
"welcome_card": "Cartão de boas-vindas",
|
"welcome_card": "Cartão de boas-vindas",
|
||||||
"workflows": "Fluxos de trabalho",
|
"workflows": "Fluxos de trabalho",
|
||||||
|
"workspace": "Espaço de trabalho",
|
||||||
"workspace_configuration": "Configuração do projeto",
|
"workspace_configuration": "Configuração do projeto",
|
||||||
"workspace_created_successfully": "Projeto criado com sucesso",
|
"workspace_created_successfully": "Projeto criado com sucesso",
|
||||||
"workspace_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
|
"workspace_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
|
||||||
"workspace_id": "ID do projeto",
|
"workspace_id": "ID do projeto",
|
||||||
"workspace_name": "Nome do projeto",
|
"workspace_name": "Nome do projeto",
|
||||||
"workspace_name_placeholder": "ex: Formbricks",
|
"workspace_name_placeholder": "ex: Formbricks",
|
||||||
"workspace_not_found": "Projeto não encontrado",
|
|
||||||
"workspace_permission_not_found": "Permissão do projeto não encontrada",
|
|
||||||
"workspaces": "Projetos",
|
"workspaces": "Projetos",
|
||||||
"years": "anos",
|
"years": "anos",
|
||||||
"you": "Você",
|
"you": "Você",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Novo atributo “{key}” criado com tipo “{dataType}”",
|
"attributes_msg_new_attribute_created": "Novo atributo “{key}” criado com tipo “{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "O ID de usuário já existe para este ambiente e não foi atualizado.",
|
"attributes_msg_userid_already_exists": "O ID de usuário já existe para este ambiente e não foi atualizado.",
|
||||||
"contact_deleted_successfully": "Contato excluído com sucesso",
|
"contact_deleted_successfully": "Contato excluído com sucesso",
|
||||||
"contact_not_found": "Nenhum contato encontrado",
|
|
||||||
"contacts_table_refresh": "Atualizar contatos",
|
"contacts_table_refresh": "Atualizar contatos",
|
||||||
"contacts_table_refresh_success": "Contatos atualizados com sucesso",
|
"contacts_table_refresh_success": "Contatos atualizados com sucesso",
|
||||||
"create_attribute": "Criar atributo",
|
"create_attribute": "Criar atributo",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Criado por um Terceiro",
|
"created_by_third_party": "Criado por um Terceiro",
|
||||||
"discord_webhook_not_supported": "Webhooks do Discord não são suportados no momento.",
|
"discord_webhook_not_supported": "Webhooks do Discord não são suportados no momento.",
|
||||||
"empty_webhook_message": "Seus webhooks vão aparecer aqui assim que você adicioná-los. ⏲️",
|
"empty_webhook_message": "Seus webhooks vão aparecer aqui assim que você adicioná-los. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Gateway inválido (502): Erro de proxy/gateway, serviço inacessível",
|
||||||
|
"endpoint_gateway_timeout_error": "Tempo limite do gateway esgotado (504): Tempo limite do gateway esgotado, serviço inacessível",
|
||||||
|
"endpoint_internal_server_error": "Erro interno do servidor (500): O serviço encontrou um erro inesperado",
|
||||||
|
"endpoint_method_not_allowed_error": "Método não permitido (405): O endpoint existe, mas não aceita solicitações POST",
|
||||||
|
"endpoint_not_found_error": "Não encontrado (404): O endpoint não existe",
|
||||||
"endpoint_pinged": "Uhul! Conseguimos pingar o webhook!",
|
"endpoint_pinged": "Uhul! Conseguimos pingar o webhook!",
|
||||||
"endpoint_pinged_error": "Não consegui pingar o webhook!",
|
"endpoint_pinged_error": "Não consegui pingar o webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "Serviço indisponível (503): O serviço está temporariamente indisponível",
|
||||||
"learn_to_verify": "Aprenda como verificar assinaturas de webhook",
|
"learn_to_verify": "Aprenda como verificar assinaturas de webhook",
|
||||||
|
"no_triggers": "Nenhum Gatilho",
|
||||||
"please_check_console": "Por favor, verifica o console para mais detalhes",
|
"please_check_console": "Por favor, verifica o console para mais detalhes",
|
||||||
"please_enter_a_url": "Por favor, insira uma URL",
|
"please_enter_a_url": "Por favor, insira uma URL",
|
||||||
"response_created": "Resposta Criada",
|
"response_created": "Resposta Criada",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Recursos Empresariais",
|
"enterprise_features": "Recursos Empresariais",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Adquira uma licença Enterprise para ter acesso a todos os recursos.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Adquira uma licença Enterprise para ter acesso a todos os recursos.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controle total sobre a privacidade e segurança dos seus dados.",
|
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controle total sobre a privacidade e segurança dos seus dados.",
|
||||||
|
"license_feature_access_control": "Controle de acesso (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Logs de auditoria",
|
||||||
|
"license_feature_contacts": "Contatos e Segmentos",
|
||||||
|
"license_feature_projects": "Workspaces",
|
||||||
|
"license_feature_quotas": "Cotas",
|
||||||
|
"license_feature_remove_branding": "Remover identidade visual",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Proteção contra spam",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Autenticação de dois fatores",
|
||||||
|
"license_feature_whitelabel": "E-mails white-label",
|
||||||
|
"license_features_table_access": "Acesso",
|
||||||
|
"license_features_table_description": "Recursos empresariais e limites disponíveis atualmente para esta instância.",
|
||||||
|
"license_features_table_disabled": "Desabilitado",
|
||||||
|
"license_features_table_enabled": "Habilitado",
|
||||||
|
"license_features_table_feature": "Recurso",
|
||||||
|
"license_features_table_title": "Recursos Licenciados",
|
||||||
|
"license_features_table_unlimited": "Ilimitado",
|
||||||
|
"license_features_table_value": "Valor",
|
||||||
"license_instance_mismatch_description": "Esta licença está atualmente vinculada a uma instância diferente do Formbricks. Se esta instalação foi reconstruída ou movida, peça ao suporte do Formbricks para desconectar a vinculação da instância anterior.",
|
"license_instance_mismatch_description": "Esta licença está atualmente vinculada a uma instância diferente do Formbricks. Se esta instalação foi reconstruída ou movida, peça ao suporte do Formbricks para desconectar a vinculação da instância anterior.",
|
||||||
"license_invalid_description": "A chave de licença na sua variável de ambiente ENTERPRISE_LICENSE_KEY não é válida. Verifique se há erros de digitação ou solicite uma nova chave.",
|
"license_invalid_description": "A chave de licença na sua variável de ambiente ENTERPRISE_LICENSE_KEY não é válida. Verifique se há erros de digitação ou solicite uma nova chave.",
|
||||||
"license_status": "Status da licença",
|
"license_status": "Status da licença",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Hostname personalizado",
|
"custom_hostname": "Hostname personalizado",
|
||||||
"customize_survey_logo": "Personalizar o logo da pesquisa",
|
"customize_survey_logo": "Personalizar o logo da pesquisa",
|
||||||
"darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.",
|
"darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.",
|
||||||
"date_format": "Formato de data",
|
|
||||||
"days_before_showing_this_survey_again": "ou mais dias devem passar entre a última pesquisa exibida e a exibição desta pesquisa.",
|
"days_before_showing_this_survey_again": "ou mais dias devem passar entre a última pesquisa exibida e a exibição desta pesquisa.",
|
||||||
"delete_anyways": "Excluir mesmo assim",
|
"delete_anyways": "Excluir mesmo assim",
|
||||||
"delete_block": "Excluir bloco",
|
"delete_block": "Excluir bloco",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Erro ao salvar alterações",
|
"error_saving_changes": "Erro ao salvar alterações",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir múltiplas respostas; continuar mostrando mesmo após uma resposta (ex.: caixa de feedback).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir múltiplas respostas; continuar mostrando mesmo após uma resposta (ex.: caixa de feedback).",
|
||||||
"everyone": "Todo mundo",
|
"everyone": "Todo mundo",
|
||||||
|
"expand_preview": "Expandir prévia",
|
||||||
"external_urls_paywall_tooltip": "Faça upgrade para um plano pago para personalizar URLs externas. Isso nos ajuda a prevenir phishing.",
|
"external_urls_paywall_tooltip": "Faça upgrade para um plano pago para personalizar URLs externas. Isso nos ajuda a prevenir phishing.",
|
||||||
"fallback_missing": "Faltando alternativa",
|
"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_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.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||||
"response_options": "Opções de Resposta",
|
"response_options": "Opções de Resposta",
|
||||||
|
"reverse_order_occasionally": "Inverter ordem ocasionalmente",
|
||||||
|
"reverse_order_occasionally_except_last": "Inverter ordem ocasionalmente exceto o último",
|
||||||
"roundness": "Circularidade",
|
"roundness": "Circularidade",
|
||||||
"roundness_description": "Controla o arredondamento dos cantos.",
|
"roundness_description": "Controla o arredondamento dos cantos.",
|
||||||
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Mostrar no máximo",
|
"show_survey_maximum_of": "Mostrar no máximo",
|
||||||
"show_survey_to_users": "Mostrar pesquisa para % dos usuários",
|
"show_survey_to_users": "Mostrar pesquisa para % dos usuários",
|
||||||
"show_to_x_percentage_of_targeted_users": "Mostrar para {percentage}% dos usuários segmentados",
|
"show_to_x_percentage_of_targeted_users": "Mostrar para {percentage}% dos usuários segmentados",
|
||||||
|
"shrink_preview": "Recolher prévia",
|
||||||
"simple": "Simples",
|
"simple": "Simples",
|
||||||
"six_points": "6 pontos",
|
"six_points": "6 pontos",
|
||||||
"smiley": "Sorridente",
|
"smiley": "Sorridente",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Estilo definido para os estilos do tema",
|
"styling_set_to_theme_styles": "Estilo definido para os estilos do tema",
|
||||||
"subheading": "Subtítulo",
|
"subheading": "Subtítulo",
|
||||||
"subtract": "Subtrair -",
|
"subtract": "Subtrair -",
|
||||||
|
"survey_closed_message_heading_required": "Adicione um título à mensagem personalizada de pesquisa encerrada.",
|
||||||
"survey_completed_heading": "Pesquisa Concluída",
|
"survey_completed_heading": "Pesquisa Concluída",
|
||||||
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
|
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
|
||||||
"survey_display_settings": "Configurações de Exibição da Pesquisa",
|
"survey_display_settings": "Configurações de Exibição da Pesquisa",
|
||||||
"survey_placement": "Posicionamento da Pesquisa",
|
"survey_placement": "Posicionamento da Pesquisa",
|
||||||
|
"survey_preview": "Prévia da pesquisa 👀",
|
||||||
"survey_styling": "Estilização de Formulários",
|
"survey_styling": "Estilização de Formulários",
|
||||||
"survey_trigger": "Gatilho de Pesquisa",
|
"survey_trigger": "Gatilho de Pesquisa",
|
||||||
"switch_multi_language_on_to_get_started": "Ative o modo multilíngue para começar 👉",
|
"switch_multi_language_on_to_get_started": "Ative o modo multilíngue para começar 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||||
"preview_survey_question_2_headline": "Quer ficar por dentro?",
|
"preview_survey_question_2_headline": "Quer ficar por dentro?",
|
||||||
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
||||||
"preview_survey_question_open_text_headline": "Tem mais alguma coisa que você gostaria de compartilhar?",
|
"preview_survey_question_open_text_headline": "Há algo mais que você gostaria de compartilhar?",
|
||||||
"preview_survey_question_open_text_placeholder": "Digite sua resposta aqui...",
|
"preview_survey_question_open_text_placeholder": "Digite sua resposta aqui...",
|
||||||
"preview_survey_question_open_text_subheader": "Seu feedback nos ajuda a melhorar.",
|
"preview_survey_question_open_text_subheader": "Seu feedback nos ajuda a melhorar.",
|
||||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Obrigado por compartilhar sua ideia de fluxo de trabalho conosco! Estamos atualmente projetando este recurso e seu feedback nos ajudará a construir exatamente o que você precisa.",
|
"coming_soon_description": "Obrigado por compartilhar sua ideia de fluxo de trabalho conosco! Estamos atualmente projetando este recurso e seu feedback nos ajudará a construir exatamente o que você precisa.",
|
||||||
"coming_soon_title": "Estamos quase lá!",
|
"coming_soon_title": "Estamos quase lá!",
|
||||||
"follow_up_label": "Há algo mais que você gostaria de adicionar?",
|
"follow_up_label": "Há algo mais que você gostaria de acrescentar?",
|
||||||
"follow_up_placeholder": "Quais tarefas específicas você gostaria de automatizar? Alguma ferramenta ou integração que gostaria de incluir?",
|
"follow_up_placeholder": "Quais tarefas específicas você gostaria de automatizar? Alguma ferramenta ou integração que gostaria de incluir?",
|
||||||
"generate_button": "Gerar fluxo de trabalho",
|
"generate_button": "Gerar fluxo de trabalho",
|
||||||
"heading": "Qual fluxo de trabalho você quer criar?",
|
"heading": "Qual fluxo de trabalho você quer criar?",
|
||||||
|
|||||||
+46
-12
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Conectar",
|
"connect": "Conectar",
|
||||||
"connect_formbricks": "Ligar Formbricks",
|
"connect_formbricks": "Ligar Formbricks",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"contact": "Contacto",
|
||||||
"contacts": "Contactos",
|
"contacts": "Contactos",
|
||||||
"continue": "Continuar",
|
"continue": "Continuar",
|
||||||
"copied": "Copiado",
|
"copied": "Copiado",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copy_code": "Copiar código",
|
"copy_code": "Copiar código",
|
||||||
"copy_link": "Copiar Link",
|
"copy_link": "Copiar Link",
|
||||||
|
"copy_to_environment": "Copiar para {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
||||||
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(cópia {copyNumber})",
|
"duplicate_copy_number": "(cópia {copyNumber})",
|
||||||
"e_commerce": "Comércio Eletrónico",
|
"e_commerce": "Comércio Eletrónico",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
|
"elements": "Elementos",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Cartão de encerramento",
|
"ending_card": "Cartão de encerramento",
|
||||||
"enter_url": "Introduzir URL",
|
"enter_url": "Introduzir URL",
|
||||||
"enterprise_license": "Licença Enterprise",
|
"enterprise_license": "Licença Enterprise",
|
||||||
"environment": "Ambiente",
|
"environment": "Ambiente",
|
||||||
"environment_not_found": "Ambiente não encontrado",
|
|
||||||
"environment_notice": "Está atualmente no ambiente {environment}.",
|
"environment_notice": "Está atualmente no ambiente {environment}.",
|
||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
|
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Inquéritos inativos",
|
"inactive_surveys": "Inquéritos inativos",
|
||||||
"integration": "integração",
|
"integration": "integração",
|
||||||
"integrations": "Integrações",
|
"integrations": "Integrações",
|
||||||
"invalid_date": "Data inválida",
|
"invalid_date_with_value": "Data inválida: {value}",
|
||||||
"invalid_file_name": "Nome de ficheiro inválido, por favor renomeie o seu ficheiro e tente novamente",
|
"invalid_file_name": "Nome de ficheiro inválido, por favor renomeie o seu ficheiro e tente novamente",
|
||||||
"invalid_file_type": "Tipo de ficheiro inválido",
|
"invalid_file_type": "Tipo de ficheiro inválido",
|
||||||
"invite": "Convidar",
|
"invite": "Convidar",
|
||||||
"invite_them": "Convide-os",
|
"invite_them": "Convide-os",
|
||||||
|
"javascript_required": "JavaScript Necessário",
|
||||||
|
"javascript_required_description": "O Formbricks necessita de JavaScript para funcionar corretamente. Por favor, ativa o JavaScript nas definições do teu navegador para continuar.",
|
||||||
"key": "Chave",
|
"key": "Chave",
|
||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Membros",
|
"members": "Membros",
|
||||||
"members_and_teams": "Membros e equipas",
|
"members_and_teams": "Membros e equipas",
|
||||||
|
"membership": "Subscrição",
|
||||||
"membership_not_found": "Associação não encontrada",
|
"membership_not_found": "Associação não encontrada",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metadados",
|
"metadata": "Metadados",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor num ecrã maior. Para gerir ou criar inquéritos, mude de dispositivo.",
|
"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_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ã!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Novo",
|
"new": "Novo",
|
||||||
"new_version_available": "Formbricks {version} está aqui. Atualize agora!",
|
"new_version_available": "Formbricks {version} está aqui. Atualize agora!",
|
||||||
"next": "Seguinte",
|
"next": "Seguinte",
|
||||||
|
"no_actions_found": "Nenhuma ação encontrada",
|
||||||
"no_background_image_found": "Nenhuma imagem de fundo encontrada.",
|
"no_background_image_found": "Nenhuma imagem de fundo encontrada.",
|
||||||
"no_code": "Sem código",
|
"no_code": "Sem código",
|
||||||
"no_files_uploaded": "Nenhum ficheiro foi carregado",
|
"no_files_uploaded": "Nenhum ficheiro foi carregado",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "ou",
|
"or": "ou",
|
||||||
"organization": "Organização",
|
"organization": "Organização",
|
||||||
"organization_id": "ID da Organização",
|
"organization_id": "ID da Organização",
|
||||||
"organization_not_found": "Organização não encontrada",
|
|
||||||
"organization_settings": "Configurações da Organização",
|
"organization_settings": "Configurações da Organização",
|
||||||
"organization_teams_not_found": "Equipas da organização não encontradas",
|
|
||||||
"other": "Outro",
|
"other": "Outro",
|
||||||
|
"other_filters": "Outros Filtros",
|
||||||
"others": "Outros",
|
"others": "Outros",
|
||||||
"overlay_color": "Cor da sobreposição",
|
"overlay_color": "Cor da sobreposição",
|
||||||
"overview": "Visão geral",
|
"overview": "Visão geral",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Por favor, selecione pelo menos um inquérito",
|
"please_select_at_least_one_survey": "Por favor, selecione pelo menos um inquérito",
|
||||||
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
|
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
|
||||||
"please_upgrade_your_plan": "Por favor, atualize o seu plano",
|
"please_upgrade_your_plan": "Por favor, atualize o seu plano",
|
||||||
|
"powered_by_formbricks": "Desenvolvido por Formbricks",
|
||||||
"preview": "Pré-visualização",
|
"preview": "Pré-visualização",
|
||||||
"preview_survey": "Pré-visualização do inquérito",
|
"preview_survey": "Pré-visualização do inquérito",
|
||||||
"privacy": "Política de Privacidade",
|
"privacy": "Política de Privacidade",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Selecionar",
|
"select": "Selecionar",
|
||||||
"select_all": "Selecionar tudo",
|
"select_all": "Selecionar tudo",
|
||||||
"select_filter": "Selecionar filtro",
|
"select_filter": "Selecionar filtro",
|
||||||
|
"select_language": "Selecionar Idioma",
|
||||||
"select_survey": "Selecionar Inquérito",
|
"select_survey": "Selecionar Inquérito",
|
||||||
"select_teams": "Selecionar equipas",
|
"select_teams": "Selecionar equipas",
|
||||||
"selected": "Selecionado",
|
"selected": "Selecionado",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "ID do Inquérito",
|
"survey_id": "ID do Inquérito",
|
||||||
"survey_languages": "Idiomas da Pesquisa",
|
"survey_languages": "Idiomas da Pesquisa",
|
||||||
"survey_live": "Inquérito ao vivo",
|
"survey_live": "Inquérito ao vivo",
|
||||||
"survey_not_found": "Inquérito não encontrado",
|
|
||||||
"survey_paused": "Inquérito pausado.",
|
"survey_paused": "Inquérito pausado.",
|
||||||
"survey_type": "Tipo de Inquérito",
|
"survey_type": "Tipo de Inquérito",
|
||||||
"surveys": "Inquéritos",
|
"surveys": "Inquéritos",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Nome da equipa",
|
"team_name": "Nome da equipa",
|
||||||
"team_role": "Função na equipa",
|
"team_role": "Função na equipa",
|
||||||
"teams": "Equipas",
|
"teams": "Equipas",
|
||||||
"teams_not_found": "Equipas não encontradas",
|
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
"time": "Tempo",
|
"time": "Tempo",
|
||||||
"time_to_finish": "Tempo para concluir",
|
"time_to_finish": "Tempo para concluir",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Utilizador",
|
"user": "Utilizador",
|
||||||
"user_id": "ID do Utilizador",
|
"user_id": "ID do Utilizador",
|
||||||
"user_not_found": "Utilizador não encontrado",
|
|
||||||
"variable": "Variável",
|
"variable": "Variável",
|
||||||
"variable_ids": "IDs de variáveis",
|
"variable_ids": "IDs de variáveis",
|
||||||
"variables": "Variáveis",
|
"variables": "Variáveis",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "semanas",
|
"weeks": "semanas",
|
||||||
"welcome_card": "Cartão de boas-vindas",
|
"welcome_card": "Cartão de boas-vindas",
|
||||||
"workflows": "Fluxos de trabalho",
|
"workflows": "Fluxos de trabalho",
|
||||||
|
"workspace": "Espaço de trabalho",
|
||||||
"workspace_configuration": "Configuração do projeto",
|
"workspace_configuration": "Configuração do projeto",
|
||||||
"workspace_created_successfully": "Projeto criado com sucesso",
|
"workspace_created_successfully": "Projeto criado com sucesso",
|
||||||
"workspace_creation_description": "Organize inquéritos em projetos para melhor controlo de acesso.",
|
"workspace_creation_description": "Organize inquéritos em projetos para melhor controlo de acesso.",
|
||||||
"workspace_id": "ID do projeto",
|
"workspace_id": "ID do projeto",
|
||||||
"workspace_name": "Nome do projeto",
|
"workspace_name": "Nome do projeto",
|
||||||
"workspace_name_placeholder": "ex. Formbricks",
|
"workspace_name_placeholder": "ex. Formbricks",
|
||||||
"workspace_not_found": "Projeto não encontrado",
|
|
||||||
"workspace_permission_not_found": "Permissão do projeto não encontrada",
|
|
||||||
"workspaces": "Projetos",
|
"workspaces": "Projetos",
|
||||||
"years": "anos",
|
"years": "anos",
|
||||||
"you": "Você",
|
"you": "Você",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Criado novo atributo “{key}” com tipo “{dataType}”",
|
"attributes_msg_new_attribute_created": "Criado novo atributo “{key}” com tipo “{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "O ID de utilizador já existe para este ambiente e não foi atualizado.",
|
"attributes_msg_userid_already_exists": "O ID de utilizador já existe para este ambiente e não foi atualizado.",
|
||||||
"contact_deleted_successfully": "Contacto eliminado com sucesso",
|
"contact_deleted_successfully": "Contacto eliminado com sucesso",
|
||||||
"contact_not_found": "Nenhum contacto encontrado",
|
|
||||||
"contacts_table_refresh": "Atualizar contactos",
|
"contacts_table_refresh": "Atualizar contactos",
|
||||||
"contacts_table_refresh_success": "Contactos atualizados com sucesso",
|
"contacts_table_refresh_success": "Contactos atualizados com sucesso",
|
||||||
"create_attribute": "Criar atributo",
|
"create_attribute": "Criar atributo",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Criado por um Terceiro",
|
"created_by_third_party": "Criado por um Terceiro",
|
||||||
"discord_webhook_not_supported": "Os webhooks do Discord não são atualmente suportados.",
|
"discord_webhook_not_supported": "Os webhooks do Discord não são atualmente suportados.",
|
||||||
"empty_webhook_message": "Os seus webhooks aparecerão aqui assim que os adicionar. ⏲️",
|
"empty_webhook_message": "Os seus webhooks aparecerão aqui assim que os adicionar. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Gateway inválido (502): Erro de proxy/gateway, serviço inacessível",
|
||||||
|
"endpoint_gateway_timeout_error": "Tempo limite do gateway excedido (504): Tempo limite do gateway excedido, serviço inacessível",
|
||||||
|
"endpoint_internal_server_error": "Erro interno do servidor (500): O serviço encontrou um erro inesperado",
|
||||||
|
"endpoint_method_not_allowed_error": "Método não permitido (405): O endpoint existe, mas não aceita pedidos POST",
|
||||||
|
"endpoint_not_found_error": "Não encontrado (404): O endpoint não existe",
|
||||||
"endpoint_pinged": "Yay! Conseguimos aceder ao webhook!",
|
"endpoint_pinged": "Yay! Conseguimos aceder ao webhook!",
|
||||||
"endpoint_pinged_error": "Não foi possível aceder ao webhook!",
|
"endpoint_pinged_error": "Não foi possível aceder ao webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "Serviço indisponível (503): O serviço está temporariamente indisponível",
|
||||||
"learn_to_verify": "Aprenda a verificar assinaturas de webhook",
|
"learn_to_verify": "Aprenda a verificar assinaturas de webhook",
|
||||||
|
"no_triggers": "Sem Acionadores",
|
||||||
"please_check_console": "Por favor, verifique a consola para mais detalhes",
|
"please_check_console": "Por favor, verifique a consola para mais detalhes",
|
||||||
"please_enter_a_url": "Por favor, insira um URL",
|
"please_enter_a_url": "Por favor, insira um URL",
|
||||||
"response_created": "Resposta Criada",
|
"response_created": "Resposta Criada",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Funcionalidades da Empresa",
|
"enterprise_features": "Funcionalidades da Empresa",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenha uma licença Enterprise para ter acesso a todas as funcionalidades.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenha uma licença Enterprise para ter acesso a todas as funcionalidades.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controlo total sobre a privacidade e segurança dos seus dados.",
|
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controlo total sobre a privacidade e segurança dos seus dados.",
|
||||||
|
"license_feature_access_control": "Controlo de acesso (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Registos de auditoria",
|
||||||
|
"license_feature_contacts": "Contactos e Segmentos",
|
||||||
|
"license_feature_projects": "Áreas de trabalho",
|
||||||
|
"license_feature_quotas": "Quotas",
|
||||||
|
"license_feature_remove_branding": "Remover marca",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Proteção contra spam",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Autenticação de dois fatores",
|
||||||
|
"license_feature_whitelabel": "E-mails personalizados",
|
||||||
|
"license_features_table_access": "Acesso",
|
||||||
|
"license_features_table_description": "Funcionalidades e limites empresariais atualmente disponíveis para esta instância.",
|
||||||
|
"license_features_table_disabled": "Desativado",
|
||||||
|
"license_features_table_enabled": "Ativado",
|
||||||
|
"license_features_table_feature": "Funcionalidade",
|
||||||
|
"license_features_table_title": "Funcionalidades Licenciadas",
|
||||||
|
"license_features_table_unlimited": "Ilimitado",
|
||||||
|
"license_features_table_value": "Valor",
|
||||||
"license_instance_mismatch_description": "Esta licença está atualmente associada a uma instância Formbricks diferente. Se esta instalação foi reconstruída ou movida, pede ao suporte da Formbricks para desconectar a associação da instância anterior.",
|
"license_instance_mismatch_description": "Esta licença está atualmente associada a uma instância Formbricks diferente. Se esta instalação foi reconstruída ou movida, pede ao suporte da Formbricks para desconectar a associação da instância anterior.",
|
||||||
"license_invalid_description": "A chave de licença na sua variável de ambiente ENTERPRISE_LICENSE_KEY não é válida. Por favor, verifique se existem erros de digitação ou solicite uma nova chave.",
|
"license_invalid_description": "A chave de licença na sua variável de ambiente ENTERPRISE_LICENSE_KEY não é válida. Por favor, verifique se existem erros de digitação ou solicite uma nova chave.",
|
||||||
"license_status": "Estado da licença",
|
"license_status": "Estado da licença",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Nome do host personalizado",
|
"custom_hostname": "Nome do host personalizado",
|
||||||
"customize_survey_logo": "Personalizar o logótipo do inquérito",
|
"customize_survey_logo": "Personalizar o logótipo do inquérito",
|
||||||
"darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.",
|
"darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.",
|
||||||
"date_format": "Formato da data",
|
|
||||||
"days_before_showing_this_survey_again": "ou mais dias a decorrer entre o último inquérito apresentado e a apresentação deste inquérito.",
|
"days_before_showing_this_survey_again": "ou mais dias a decorrer entre o último inquérito apresentado e a apresentação deste inquérito.",
|
||||||
"delete_anyways": "Eliminar mesmo assim",
|
"delete_anyways": "Eliminar mesmo assim",
|
||||||
"delete_block": "Eliminar bloco",
|
"delete_block": "Eliminar bloco",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Erro ao guardar alterações",
|
"error_saving_changes": "Erro ao guardar alterações",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir múltiplas respostas; continuar a mostrar mesmo após uma resposta (por exemplo, Caixa de Feedback).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Permitir múltiplas respostas; continuar a mostrar mesmo após uma resposta (por exemplo, Caixa de Feedback).",
|
||||||
"everyone": "Todos",
|
"everyone": "Todos",
|
||||||
|
"expand_preview": "Expandir pré-visualização",
|
||||||
"external_urls_paywall_tooltip": "Por favor, faz o upgrade para um plano pago para personalizar URLs externos. Isto ajuda-nos a prevenir phishing.",
|
"external_urls_paywall_tooltip": "Por favor, faz o upgrade para um plano pago para personalizar URLs externos. Isto ajuda-nos a prevenir phishing.",
|
||||||
"fallback_missing": "Substituição em falta",
|
"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_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.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "O limite de respostas precisa exceder o número de respostas recebidas ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||||
"response_options": "Opções de Resposta",
|
"response_options": "Opções de Resposta",
|
||||||
|
"reverse_order_occasionally": "Inverter ordem ocasionalmente",
|
||||||
|
"reverse_order_occasionally_except_last": "Inverter ordem ocasionalmente exceto o último",
|
||||||
"roundness": "Arredondamento",
|
"roundness": "Arredondamento",
|
||||||
"roundness_description": "Controla o arredondamento dos cantos.",
|
"roundness_description": "Controla o arredondamento dos cantos.",
|
||||||
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Mostrar inquérito máximo de",
|
"show_survey_maximum_of": "Mostrar inquérito máximo de",
|
||||||
"show_survey_to_users": "Mostrar inquérito a % dos utilizadores",
|
"show_survey_to_users": "Mostrar inquérito a % dos utilizadores",
|
||||||
"show_to_x_percentage_of_targeted_users": "Mostrar a {percentage}% dos utilizadores alvo",
|
"show_to_x_percentage_of_targeted_users": "Mostrar a {percentage}% dos utilizadores alvo",
|
||||||
|
"shrink_preview": "Reduzir pré-visualização",
|
||||||
"simple": "Simples",
|
"simple": "Simples",
|
||||||
"six_points": "6 pontos",
|
"six_points": "6 pontos",
|
||||||
"smiley": "Sorridente",
|
"smiley": "Sorridente",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Estilo definido para estilos do tema",
|
"styling_set_to_theme_styles": "Estilo definido para estilos do tema",
|
||||||
"subheading": "Subtítulo",
|
"subheading": "Subtítulo",
|
||||||
"subtract": "Subtrair -",
|
"subtract": "Subtrair -",
|
||||||
|
"survey_closed_message_heading_required": "Adiciona um título à mensagem personalizada de inquérito encerrado.",
|
||||||
"survey_completed_heading": "Inquérito Concluído",
|
"survey_completed_heading": "Inquérito Concluído",
|
||||||
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
|
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
|
||||||
"survey_display_settings": "Configurações de Exibição do Inquérito",
|
"survey_display_settings": "Configurações de Exibição do Inquérito",
|
||||||
"survey_placement": "Colocação do Inquérito",
|
"survey_placement": "Colocação do Inquérito",
|
||||||
|
"survey_preview": "Pré-visualização do questionário 👀",
|
||||||
"survey_styling": "Estilo do formulário",
|
"survey_styling": "Estilo do formulário",
|
||||||
"survey_trigger": "Desencadeador de Inquérito",
|
"survey_trigger": "Desencadeador de Inquérito",
|
||||||
"switch_multi_language_on_to_get_started": "Ative o modo multilingue para começar 👉",
|
"switch_multi_language_on_to_get_started": "Ative o modo multilingue para começar 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||||
"preview_survey_question_2_headline": "Quer manter-se atualizado?",
|
"preview_survey_question_2_headline": "Quer manter-se atualizado?",
|
||||||
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
||||||
"preview_survey_question_open_text_headline": "Mais alguma coisa que gostaria de partilhar?",
|
"preview_survey_question_open_text_headline": "Há mais alguma coisa que gostaria de partilhar?",
|
||||||
"preview_survey_question_open_text_placeholder": "Escreva a sua resposta aqui...",
|
"preview_survey_question_open_text_placeholder": "Escreva a sua resposta aqui...",
|
||||||
"preview_survey_question_open_text_subheader": "O seu feedback ajuda-nos a melhorar.",
|
"preview_survey_question_open_text_subheader": "O seu feedback ajuda-nos a melhorar.",
|
||||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Conectează",
|
"connect": "Conectează",
|
||||||
"connect_formbricks": "Conectează Formbricks",
|
"connect_formbricks": "Conectează Formbricks",
|
||||||
"connected": "Conectat",
|
"connected": "Conectat",
|
||||||
|
"contact": "Contact",
|
||||||
"contacts": "Contacte",
|
"contacts": "Contacte",
|
||||||
"continue": "Continuă",
|
"continue": "Continuă",
|
||||||
"copied": "Copiat",
|
"copied": "Copiat",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Copiază",
|
"copy": "Copiază",
|
||||||
"copy_code": "Copiază codul",
|
"copy_code": "Copiază codul",
|
||||||
"copy_link": "Copiază legătura",
|
"copy_link": "Copiază legătura",
|
||||||
|
"copy_to_environment": "Copiază în {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} atribut} few {{count} atribute} other {{count} de atribute}}",
|
"count_attributes": "{count, plural, one {{count} atribut} few {{count} atribute} other {{count} de atribute}}",
|
||||||
"count_contacts": "{count, plural, one {{count} contact} few {{count} contacte} other {{count} de contacte}}",
|
"count_contacts": "{count, plural, one {{count} contact} few {{count} contacte} other {{count} de contacte}}",
|
||||||
"count_members": "{count, plural, one {{count} membru} few {{count} membri} other {{count} de membri}}",
|
"count_members": "{count, plural, one {{count} membru} few {{count} membri} other {{count} de membri}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(copie {copyNumber})",
|
"duplicate_copy_number": "(copie {copyNumber})",
|
||||||
"e_commerce": "Comerț electronic",
|
"e_commerce": "Comerț electronic",
|
||||||
"edit": "Editare",
|
"edit": "Editare",
|
||||||
|
"elements": "Elemente",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Cardul de finalizare",
|
"ending_card": "Cardul de finalizare",
|
||||||
"enter_url": "Introduceți URL-ul",
|
"enter_url": "Introduceți URL-ul",
|
||||||
"enterprise_license": "Licență Întreprindere",
|
"enterprise_license": "Licență Întreprindere",
|
||||||
"environment": "Mediu",
|
"environment": "Mediu",
|
||||||
"environment_not_found": "Mediul nu a fost găsit",
|
|
||||||
"environment_notice": "Te afli în prezent în mediul {environment}",
|
"environment_notice": "Te afli în prezent în mediul {environment}",
|
||||||
"error": "Eroare",
|
"error": "Eroare",
|
||||||
"error_component_description": "Această resursă nu există sau nu aveți drepturile necesare pentru a o accesa.",
|
"error_component_description": "Această resursă nu există sau nu aveți drepturile necesare pentru a o accesa.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Sondaje inactive",
|
"inactive_surveys": "Sondaje inactive",
|
||||||
"integration": "integrare",
|
"integration": "integrare",
|
||||||
"integrations": "Integrări",
|
"integrations": "Integrări",
|
||||||
"invalid_date": "Dată invalidă",
|
"invalid_date_with_value": "Dată invalidă: {value}",
|
||||||
"invalid_file_name": "Nume de fișier invalid, vă rugăm să redenumiți fișierul și să încercați din nou",
|
"invalid_file_name": "Nume de fișier invalid, vă rugăm să redenumiți fișierul și să încercați din nou",
|
||||||
"invalid_file_type": "Tip de fișier nevalid",
|
"invalid_file_type": "Tip de fișier nevalid",
|
||||||
"invite": "Invită",
|
"invite": "Invită",
|
||||||
"invite_them": "Invită-i",
|
"invite_them": "Invită-i",
|
||||||
|
"javascript_required": "JavaScript necesar",
|
||||||
|
"javascript_required_description": "Formbricks necesită JavaScript pentru a funcționa corect. Te rugăm să activezi JavaScript în setările browserului tău pentru a continua.",
|
||||||
"key": "Cheie",
|
"key": "Cheie",
|
||||||
"label": "Etichetă",
|
"label": "Etichetă",
|
||||||
"language": "Limba",
|
"language": "Limba",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"members": "Membri",
|
"members": "Membri",
|
||||||
"members_and_teams": "Membri și echipe",
|
"members_and_teams": "Membri și echipe",
|
||||||
|
"membership": "Abonament",
|
||||||
"membership_not_found": "Apartenența nu a fost găsită",
|
"membership_not_found": "Apartenența nu a fost găsită",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metadate",
|
"metadata": "Metadate",
|
||||||
"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_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_surveys_look_good": "Nu vă faceți griji – chestionarele dumneavoastră arată grozav pe orice dispozitiv și dimensiune a ecranului!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Nou",
|
"new": "Nou",
|
||||||
"new_version_available": "Formbricks {version} este disponibil. Actualizați acum!",
|
"new_version_available": "Formbricks {version} este disponibil. Actualizați acum!",
|
||||||
"next": "Următorul",
|
"next": "Următorul",
|
||||||
|
"no_actions_found": "Nu au fost găsite acțiuni",
|
||||||
"no_background_image_found": "Nu a fost găsită nicio imagine de fundal.",
|
"no_background_image_found": "Nu a fost găsită nicio imagine de fundal.",
|
||||||
"no_code": "Fără Cod",
|
"no_code": "Fără Cod",
|
||||||
"no_files_uploaded": "Nu au fost încărcate fișiere",
|
"no_files_uploaded": "Nu au fost încărcate fișiere",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "sau",
|
"or": "sau",
|
||||||
"organization": "Organizație",
|
"organization": "Organizație",
|
||||||
"organization_id": "ID Organizație",
|
"organization_id": "ID Organizație",
|
||||||
"organization_not_found": "Organizația nu a fost găsită",
|
|
||||||
"organization_settings": "Setări Organizație",
|
"organization_settings": "Setări Organizație",
|
||||||
"organization_teams_not_found": "Echipele organizației nu au fost găsite",
|
|
||||||
"other": "Altele",
|
"other": "Altele",
|
||||||
|
"other_filters": "Alte Filtre",
|
||||||
"others": "Altele",
|
"others": "Altele",
|
||||||
"overlay_color": "Culoare overlay",
|
"overlay_color": "Culoare overlay",
|
||||||
"overview": "Prezentare generală",
|
"overview": "Prezentare generală",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Vă rugăm să selectați cel puțin un sondaj",
|
"please_select_at_least_one_survey": "Vă rugăm să selectați cel puțin un sondaj",
|
||||||
"please_select_at_least_one_trigger": "Vă rugăm să selectați cel puțin un declanșator",
|
"please_select_at_least_one_trigger": "Vă rugăm să selectați cel puțin un declanșator",
|
||||||
"please_upgrade_your_plan": "Vă rugăm să faceți upgrade la planul dumneavoastră",
|
"please_upgrade_your_plan": "Vă rugăm să faceți upgrade la planul dumneavoastră",
|
||||||
|
"powered_by_formbricks": "Oferit de Formbricks",
|
||||||
"preview": "Previzualizare",
|
"preview": "Previzualizare",
|
||||||
"preview_survey": "Previzualizare Chestionar",
|
"preview_survey": "Previzualizare Chestionar",
|
||||||
"privacy": "Politica de Confidențialitate",
|
"privacy": "Politica de Confidențialitate",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Selectați",
|
"select": "Selectați",
|
||||||
"select_all": "Selectați toate",
|
"select_all": "Selectați toate",
|
||||||
"select_filter": "Selectați filtrul",
|
"select_filter": "Selectați filtrul",
|
||||||
|
"select_language": "Selectează limba",
|
||||||
"select_survey": "Selectați chestionar",
|
"select_survey": "Selectați chestionar",
|
||||||
"select_teams": "Selectați echipele",
|
"select_teams": "Selectați echipele",
|
||||||
"selected": "Selectat",
|
"selected": "Selectat",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "ID Chestionar",
|
"survey_id": "ID Chestionar",
|
||||||
"survey_languages": "Limbi chestionar",
|
"survey_languages": "Limbi chestionar",
|
||||||
"survey_live": "Chestionar activ",
|
"survey_live": "Chestionar activ",
|
||||||
"survey_not_found": "Sondajul nu a fost găsit",
|
|
||||||
"survey_paused": "Chestionar oprit.",
|
"survey_paused": "Chestionar oprit.",
|
||||||
"survey_type": "Tip Chestionar",
|
"survey_type": "Tip Chestionar",
|
||||||
"surveys": "Sondaje",
|
"surveys": "Sondaje",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Nume echipă",
|
"team_name": "Nume echipă",
|
||||||
"team_role": "Rol în echipă",
|
"team_role": "Rol în echipă",
|
||||||
"teams": "Echipe",
|
"teams": "Echipe",
|
||||||
"teams_not_found": "Echipele nu au fost găsite",
|
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"time": "Timp",
|
"time": "Timp",
|
||||||
"time_to_finish": "Timp până la finalizare",
|
"time_to_finish": "Timp până la finalizare",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Utilizator",
|
"user": "Utilizator",
|
||||||
"user_id": "ID Utilizator",
|
"user_id": "ID Utilizator",
|
||||||
"user_not_found": "Utilizatorul nu a fost găsit",
|
|
||||||
"variable": "Variabilă",
|
"variable": "Variabilă",
|
||||||
"variable_ids": "ID-uri variabile",
|
"variable_ids": "ID-uri variabile",
|
||||||
"variables": "Variante",
|
"variables": "Variante",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "săptămâni",
|
"weeks": "săptămâni",
|
||||||
"welcome_card": "Card de bun venit",
|
"welcome_card": "Card de bun venit",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
"workspace": "Spațiu de lucru",
|
||||||
"workspace_configuration": "Configurare workspace",
|
"workspace_configuration": "Configurare workspace",
|
||||||
"workspace_created_successfully": "Spațiul de lucru a fost creat cu succes",
|
"workspace_created_successfully": "Spațiul de lucru a fost creat cu succes",
|
||||||
"workspace_creation_description": "Organizează sondajele în workspaces pentru un control mai bun al accesului.",
|
"workspace_creation_description": "Organizează sondajele în workspaces pentru un control mai bun al accesului.",
|
||||||
"workspace_id": "ID workspace",
|
"workspace_id": "ID workspace",
|
||||||
"workspace_name": "Nume workspace",
|
"workspace_name": "Nume workspace",
|
||||||
"workspace_name_placeholder": "ex: Formbricks",
|
"workspace_name_placeholder": "ex: Formbricks",
|
||||||
"workspace_not_found": "Workspace-ul nu a fost găsit",
|
|
||||||
"workspace_permission_not_found": "Permisiunea pentru workspace nu a fost găsită",
|
|
||||||
"workspaces": "Workspaces",
|
"workspaces": "Workspaces",
|
||||||
"years": "ani",
|
"years": "ani",
|
||||||
"you": "Tu",
|
"you": "Tu",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "A fost creat un nou atribut „{key}” cu tipul „{dataType}”",
|
"attributes_msg_new_attribute_created": "A fost creat un nou atribut „{key}” cu tipul „{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "ID-ul de utilizator există deja pentru acest mediu și nu a fost actualizat.",
|
"attributes_msg_userid_already_exists": "ID-ul de utilizator există deja pentru acest mediu și nu a fost actualizat.",
|
||||||
"contact_deleted_successfully": "Contact șters cu succes",
|
"contact_deleted_successfully": "Contact șters cu succes",
|
||||||
"contact_not_found": "Nu a fost găsit niciun contact",
|
|
||||||
"contacts_table_refresh": "Reîmprospătare contacte",
|
"contacts_table_refresh": "Reîmprospătare contacte",
|
||||||
"contacts_table_refresh_success": "Contactele au fost actualizate cu succes",
|
"contacts_table_refresh_success": "Contactele au fost actualizate cu succes",
|
||||||
"create_attribute": "Creează atribut",
|
"create_attribute": "Creează atribut",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Creat de o Parte Terță",
|
"created_by_third_party": "Creat de o Parte Terță",
|
||||||
"discord_webhook_not_supported": "Webhook-urile Discord nu sunt în prezent suportate.",
|
"discord_webhook_not_supported": "Webhook-urile Discord nu sunt în prezent suportate.",
|
||||||
"empty_webhook_message": "Webhook-urile tale vor apărea aici de îndată ce le vei adăuga. ⏲️",
|
"empty_webhook_message": "Webhook-urile tale vor apărea aici de îndată ce le vei adăuga. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Gateway invalid (502): Eroare de proxy/gateway, serviciul nu este accesibil",
|
||||||
|
"endpoint_gateway_timeout_error": "Timp de așteptare gateway depășit (504): Timpul de așteptare al gateway-ului a fost depășit, serviciul nu este accesibil",
|
||||||
|
"endpoint_internal_server_error": "Eroare internă de server (500): Serviciul a întâmpinat o eroare neașteptată",
|
||||||
|
"endpoint_method_not_allowed_error": "Metodă nepermisă (405): Endpointul există, dar nu acceptă cereri POST",
|
||||||
|
"endpoint_not_found_error": "Negăsit (404): Endpointul nu există",
|
||||||
"endpoint_pinged": "Grozav! Am reușit să ping-ui webhooks-ul!",
|
"endpoint_pinged": "Grozav! Am reușit să ping-ui webhooks-ul!",
|
||||||
"endpoint_pinged_error": "Nu pot să ping-ui webhooks-ul!",
|
"endpoint_pinged_error": "Nu pot să ping-ui webhooks-ul!",
|
||||||
|
"endpoint_service_unavailable_error": "Serviciu indisponibil (503): Serviciul este temporar indisponibil",
|
||||||
"learn_to_verify": "Află cum să verifici semnăturile webhook",
|
"learn_to_verify": "Află cum să verifici semnăturile webhook",
|
||||||
|
"no_triggers": "Fără declanșatori",
|
||||||
"please_check_console": "Vă rugăm să verificați consola pentru mai multe detalii",
|
"please_check_console": "Vă rugăm să verificați consola pentru mai multe detalii",
|
||||||
"please_enter_a_url": "Vă rugăm să introduceți un URL",
|
"please_enter_a_url": "Vă rugăm să introduceți un URL",
|
||||||
"response_created": "Răspuns creat",
|
"response_created": "Răspuns creat",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Funcții Enterprise",
|
"enterprise_features": "Funcții Enterprise",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obțineți o licență Enterprise pentru a avea acces la toate funcționalitățile.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Obțineți o licență Enterprise pentru a avea acces la toate funcționalitățile.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Mențineți controlul complet asupra confidențialității și securității datelor dumneavoastră.",
|
"keep_full_control_over_your_data_privacy_and_security": "Mențineți controlul complet asupra confidențialității și securității datelor dumneavoastră.",
|
||||||
|
"license_feature_access_control": "Control acces (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Jurnale de audit",
|
||||||
|
"license_feature_contacts": "Contacte și segmente",
|
||||||
|
"license_feature_projects": "Spații de lucru",
|
||||||
|
"license_feature_quotas": "Cote",
|
||||||
|
"license_feature_remove_branding": "Elimină branding-ul",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Protecție spam",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Autentificare cu doi factori",
|
||||||
|
"license_feature_whitelabel": "E-mailuri white-label",
|
||||||
|
"license_features_table_access": "Acces",
|
||||||
|
"license_features_table_description": "Funcționalități și limite enterprise disponibile în prezent pentru această instanță.",
|
||||||
|
"license_features_table_disabled": "Dezactivat",
|
||||||
|
"license_features_table_enabled": "Activat",
|
||||||
|
"license_features_table_feature": "Funcționalitate",
|
||||||
|
"license_features_table_title": "Funcționalități licențiate",
|
||||||
|
"license_features_table_unlimited": "Nelimitat",
|
||||||
|
"license_features_table_value": "Valoare",
|
||||||
"license_instance_mismatch_description": "Această licență este în prezent asociată cu o altă instanță Formbricks. Dacă această instalare a fost reconstruită sau mutată, solicită echipei de suport Formbricks să deconecteze asocierea cu instanța anterioară.",
|
"license_instance_mismatch_description": "Această licență este în prezent asociată cu o altă instanță Formbricks. Dacă această instalare a fost reconstruită sau mutată, solicită echipei de suport Formbricks să deconecteze asocierea cu instanța anterioară.",
|
||||||
"license_invalid_description": "Cheia de licență din variabila de mediu ENTERPRISE_LICENSE_KEY nu este validă. Te rugăm să verifici dacă există greșeli de scriere sau să soliciți o cheie nouă.",
|
"license_invalid_description": "Cheia de licență din variabila de mediu ENTERPRISE_LICENSE_KEY nu este validă. Te rugăm să verifici dacă există greșeli de scriere sau să soliciți o cheie nouă.",
|
||||||
"license_status": "Stare licență",
|
"license_status": "Stare licență",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Gazdă personalizată",
|
"custom_hostname": "Gazdă personalizată",
|
||||||
"customize_survey_logo": "Personalizează logo-ul chestionarului",
|
"customize_survey_logo": "Personalizează logo-ul chestionarului",
|
||||||
"darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.",
|
"darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.",
|
||||||
"date_format": "Format dată",
|
|
||||||
"days_before_showing_this_survey_again": "sau mai multe zile să treacă între ultima afișare a sondajului și afișarea acestui sondaj.",
|
"days_before_showing_this_survey_again": "sau mai multe zile să treacă între ultima afișare a sondajului și afișarea acestui sondaj.",
|
||||||
"delete_anyways": "Șterge oricum",
|
"delete_anyways": "Șterge oricum",
|
||||||
"delete_block": "Șterge blocul",
|
"delete_block": "Șterge blocul",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Eroare la salvarea modificărilor",
|
"error_saving_changes": "Eroare la salvarea modificărilor",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Permite răspunsuri multiple; continuă afișarea chiar și după un răspuns (de exemplu, Caseta de Feedback).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Permite răspunsuri multiple; continuă afișarea chiar și după un răspuns (de exemplu, Caseta de Feedback).",
|
||||||
"everyone": "Toată lumea",
|
"everyone": "Toată lumea",
|
||||||
|
"expand_preview": "Extinde previzualizarea",
|
||||||
"external_urls_paywall_tooltip": "Te rugăm să treci la un plan plătit pentru a personaliza URL-urile externe. Asta ne ajută să prevenim phishing-ul.",
|
"external_urls_paywall_tooltip": "Te rugăm să treci la un plan plătit pentru a personaliza URL-urile externe. Asta ne ajută să prevenim phishing-ul.",
|
||||||
"fallback_missing": "Rezerva lipsă",
|
"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_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.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "Limita răspunsurilor trebuie să depășească numărul de răspunsuri primite ({responseCount}).",
|
"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_limits_redirections_and_more": "Limite de răspunsuri, redirecționări și altele.",
|
||||||
"response_options": "Opțiuni răspuns",
|
"response_options": "Opțiuni răspuns",
|
||||||
|
"reverse_order_occasionally": "Inversare ordine ocazional",
|
||||||
|
"reverse_order_occasionally_except_last": "Inversare ordine ocazional cu excepția ultimului",
|
||||||
"roundness": "Rotunjire",
|
"roundness": "Rotunjire",
|
||||||
"roundness_description": "Controlează cât de rotunjite sunt colțurile.",
|
"roundness_description": "Controlează cât de rotunjite sunt colțurile.",
|
||||||
"row_used_in_logic_error": "Această linie este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
"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.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Afișează sondajul de maxim",
|
"show_survey_maximum_of": "Afișează sondajul de maxim",
|
||||||
"show_survey_to_users": "Afișați sondajul la % din utilizatori",
|
"show_survey_to_users": "Afișați sondajul la % din utilizatori",
|
||||||
"show_to_x_percentage_of_targeted_users": "Afișați la {percentage}% din utilizatorii vizați",
|
"show_to_x_percentage_of_targeted_users": "Afișați la {percentage}% din utilizatorii vizați",
|
||||||
|
"shrink_preview": "Restrânge previzualizarea",
|
||||||
"simple": "Simplu",
|
"simple": "Simplu",
|
||||||
"six_points": "6 puncte",
|
"six_points": "6 puncte",
|
||||||
"smiley": "Smiley",
|
"smiley": "Smiley",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Stilizare setată la stilurile temei",
|
"styling_set_to_theme_styles": "Stilizare setată la stilurile temei",
|
||||||
"subheading": "Subtitlu",
|
"subheading": "Subtitlu",
|
||||||
"subtract": "Scade -",
|
"subtract": "Scade -",
|
||||||
|
"survey_closed_message_heading_required": "Adaugă un titlu la mesajul personalizat pentru sondajul închis.",
|
||||||
"survey_completed_heading": "Sondaj Completat",
|
"survey_completed_heading": "Sondaj Completat",
|
||||||
"survey_completed_subheading": "Acest sondaj gratuit și open-source a fost închis",
|
"survey_completed_subheading": "Acest sondaj gratuit și open-source a fost închis",
|
||||||
"survey_display_settings": "Setări de afișare a sondajului",
|
"survey_display_settings": "Setări de afișare a sondajului",
|
||||||
"survey_placement": "Amplasarea sondajului",
|
"survey_placement": "Amplasarea sondajului",
|
||||||
|
"survey_preview": "Previzualizare chestionar 👀",
|
||||||
"survey_styling": "Stilizare formular",
|
"survey_styling": "Stilizare formular",
|
||||||
"survey_trigger": "Declanșator sondaj",
|
"survey_trigger": "Declanșator sondaj",
|
||||||
"switch_multi_language_on_to_get_started": "Activați opțiunea multi-limbă pentru a începe 👉",
|
"switch_multi_language_on_to_get_started": "Activați opțiunea multi-limbă pentru a începe 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Nu, mulţumesc!",
|
"preview_survey_question_2_choice_2_label": "Nu, mulţumesc!",
|
||||||
"preview_survey_question_2_headline": "Vrei să fii în temă?",
|
"preview_survey_question_2_headline": "Vrei să fii în temă?",
|
||||||
"preview_survey_question_2_subheader": "Aceasta este o descriere exemplu.",
|
"preview_survey_question_2_subheader": "Aceasta este o descriere exemplu.",
|
||||||
"preview_survey_question_open_text_headline": "Mai vrei să împărtășești ceva?",
|
"preview_survey_question_open_text_headline": "Mai aveți ceva de adăugat?",
|
||||||
"preview_survey_question_open_text_placeholder": "Tastează răspunsul aici...",
|
"preview_survey_question_open_text_placeholder": "Tastează răspunsul aici...",
|
||||||
"preview_survey_question_open_text_subheader": "Feedbackul tău ne ajută să ne îmbunătățim.",
|
"preview_survey_question_open_text_subheader": "Feedbackul tău ne ajută să ne îmbunătățim.",
|
||||||
"preview_survey_welcome_card_headline": "Bun venit!",
|
"preview_survey_welcome_card_headline": "Bun venit!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Îți mulțumim că ai împărtășit cu noi ideea ta de workflow! În prezent, lucrăm la această funcționalitate, iar feedback-ul tău ne ajută să construim exact ce ai nevoie.",
|
"coming_soon_description": "Îți mulțumim că ai împărtășit cu noi ideea ta de workflow! În prezent, lucrăm la această funcționalitate, iar feedback-ul tău ne ajută să construim exact ce ai nevoie.",
|
||||||
"coming_soon_title": "Suntem aproape gata!",
|
"coming_soon_title": "Suntem aproape gata!",
|
||||||
"follow_up_label": "Mai este ceva ce ai vrea să adaugi?",
|
"follow_up_label": "Mai este ceva ce ați dori să adăugați?",
|
||||||
"follow_up_placeholder": "Ce sarcini specifice ați dori să automatizați? Există instrumente sau integrări pe care ați dori să le includem?",
|
"follow_up_placeholder": "Ce sarcini specifice ați dori să automatizați? Există instrumente sau integrări pe care ați dori să le includem?",
|
||||||
"generate_button": "Generează workflow",
|
"generate_button": "Generează workflow",
|
||||||
"heading": "Ce workflow vrei să creezi?",
|
"heading": "Ce workflow vrei să creezi?",
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Подключить",
|
"connect": "Подключить",
|
||||||
"connect_formbricks": "Подключить Formbricks",
|
"connect_formbricks": "Подключить Formbricks",
|
||||||
"connected": "Подключено",
|
"connected": "Подключено",
|
||||||
|
"contact": "Контакт",
|
||||||
"contacts": "Контакты",
|
"contacts": "Контакты",
|
||||||
"continue": "Продолжить",
|
"continue": "Продолжить",
|
||||||
"copied": "Скопировано",
|
"copied": "Скопировано",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Копировать",
|
"copy": "Копировать",
|
||||||
"copy_code": "Скопировать код",
|
"copy_code": "Скопировать код",
|
||||||
"copy_link": "Скопировать ссылку",
|
"copy_link": "Скопировать ссылку",
|
||||||
|
"copy_to_environment": "Копировать в {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} атрибут} few {{count} атрибута} many {{count} атрибутов} other {{count} атрибута}}",
|
"count_attributes": "{count, plural, one {{count} атрибут} few {{count} атрибута} many {{count} атрибутов} other {{count} атрибута}}",
|
||||||
"count_contacts": "{count, plural, one {{count} контакт} few {{count} контакта} many {{count} контактов} other {{count} контакта}}",
|
"count_contacts": "{count, plural, one {{count} контакт} few {{count} контакта} many {{count} контактов} other {{count} контакта}}",
|
||||||
"count_members": "{count, plural, one {{count} участник} few {{count} участника} many {{count} участников} other {{count} участника}}",
|
"count_members": "{count, plural, one {{count} участник} few {{count} участника} many {{count} участников} other {{count} участника}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(копия {copyNumber})",
|
"duplicate_copy_number": "(копия {copyNumber})",
|
||||||
"e_commerce": "E-Commerce",
|
"e_commerce": "E-Commerce",
|
||||||
"edit": "Редактировать",
|
"edit": "Редактировать",
|
||||||
|
"elements": "Элементы",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"ending_card": "Завершающая карточка",
|
"ending_card": "Завершающая карточка",
|
||||||
"enter_url": "Введите URL",
|
"enter_url": "Введите URL",
|
||||||
"enterprise_license": "Корпоративная лицензия",
|
"enterprise_license": "Корпоративная лицензия",
|
||||||
"environment": "Окружение",
|
"environment": "Окружение",
|
||||||
"environment_not_found": "Среда не найдена",
|
|
||||||
"environment_notice": "В данный момент вы находитесь в среде {environment}.",
|
"environment_notice": "В данный момент вы находитесь в среде {environment}.",
|
||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
"error_component_description": "Этот ресурс не существует или у вас нет необходимых прав для доступа к нему.",
|
"error_component_description": "Этот ресурс не существует или у вас нет необходимых прав для доступа к нему.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Неактивные опросы",
|
"inactive_surveys": "Неактивные опросы",
|
||||||
"integration": "интеграция",
|
"integration": "интеграция",
|
||||||
"integrations": "Интеграции",
|
"integrations": "Интеграции",
|
||||||
"invalid_date": "Неверная дата",
|
"invalid_date_with_value": "Неверная дата: {value}",
|
||||||
"invalid_file_name": "Недопустимое имя файла, переименуйте файл и попробуйте снова",
|
"invalid_file_name": "Недопустимое имя файла, переименуйте файл и попробуйте снова",
|
||||||
"invalid_file_type": "Недопустимый тип файла",
|
"invalid_file_type": "Недопустимый тип файла",
|
||||||
"invite": "Пригласить",
|
"invite": "Пригласить",
|
||||||
"invite_them": "Пригласить их",
|
"invite_them": "Пригласить их",
|
||||||
|
"javascript_required": "Требуется JavaScript",
|
||||||
|
"javascript_required_description": "Для корректной работы Formbricks необходим JavaScript. Пожалуйста, включите JavaScript в настройках вашего браузера, чтобы продолжить.",
|
||||||
"key": "Ключ",
|
"key": "Ключ",
|
||||||
"label": "Метка",
|
"label": "Метка",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Маркетинг",
|
"marketing": "Маркетинг",
|
||||||
"members": "Участники",
|
"members": "Участники",
|
||||||
"members_and_teams": "Участники и команды",
|
"members_and_teams": "Участники и команды",
|
||||||
|
"membership": "Членство",
|
||||||
"membership_not_found": "Участие не найдено",
|
"membership_not_found": "Участие не найдено",
|
||||||
|
"meta": "Мета",
|
||||||
"metadata": "Метаданные",
|
"metadata": "Метаданные",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
|
||||||
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
|
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Новый",
|
"new": "Новый",
|
||||||
"new_version_available": "Formbricks {version} уже здесь. Обновитесь сейчас!",
|
"new_version_available": "Formbricks {version} уже здесь. Обновитесь сейчас!",
|
||||||
"next": "Далее",
|
"next": "Далее",
|
||||||
|
"no_actions_found": "Действия не найдены",
|
||||||
"no_background_image_found": "Фоновое изображение не найдено.",
|
"no_background_image_found": "Фоновое изображение не найдено.",
|
||||||
"no_code": "Нет кода",
|
"no_code": "Нет кода",
|
||||||
"no_files_uploaded": "Файлы не были загружены",
|
"no_files_uploaded": "Файлы не были загружены",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "или",
|
"or": "или",
|
||||||
"organization": "Организация",
|
"organization": "Организация",
|
||||||
"organization_id": "ID организации",
|
"organization_id": "ID организации",
|
||||||
"organization_not_found": "Организация не найдена",
|
|
||||||
"organization_settings": "Настройки организации",
|
"organization_settings": "Настройки организации",
|
||||||
"organization_teams_not_found": "Команды организации не найдены",
|
|
||||||
"other": "Другое",
|
"other": "Другое",
|
||||||
|
"other_filters": "Другие фильтры",
|
||||||
"others": "Другие",
|
"others": "Другие",
|
||||||
"overlay_color": "Цвет наложения",
|
"overlay_color": "Цвет наложения",
|
||||||
"overview": "Обзор",
|
"overview": "Обзор",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Пожалуйста, выберите хотя бы один опрос",
|
"please_select_at_least_one_survey": "Пожалуйста, выберите хотя бы один опрос",
|
||||||
"please_select_at_least_one_trigger": "Пожалуйста, выберите хотя бы один триггер",
|
"please_select_at_least_one_trigger": "Пожалуйста, выберите хотя бы один триггер",
|
||||||
"please_upgrade_your_plan": "Пожалуйста, обновите ваш тарифный план",
|
"please_upgrade_your_plan": "Пожалуйста, обновите ваш тарифный план",
|
||||||
|
"powered_by_formbricks": "Работает на Formbricks",
|
||||||
"preview": "Предпросмотр",
|
"preview": "Предпросмотр",
|
||||||
"preview_survey": "Предпросмотр опроса",
|
"preview_survey": "Предпросмотр опроса",
|
||||||
"privacy": "Политика конфиденциальности",
|
"privacy": "Политика конфиденциальности",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Выбрать",
|
"select": "Выбрать",
|
||||||
"select_all": "Выбрать все",
|
"select_all": "Выбрать все",
|
||||||
"select_filter": "Выбрать фильтр",
|
"select_filter": "Выбрать фильтр",
|
||||||
|
"select_language": "Выберите язык",
|
||||||
"select_survey": "Выбрать опрос",
|
"select_survey": "Выбрать опрос",
|
||||||
"select_teams": "Выбрать команды",
|
"select_teams": "Выбрать команды",
|
||||||
"selected": "Выбрано",
|
"selected": "Выбрано",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "ID опроса",
|
"survey_id": "ID опроса",
|
||||||
"survey_languages": "Языки опроса",
|
"survey_languages": "Языки опроса",
|
||||||
"survey_live": "Опрос активен",
|
"survey_live": "Опрос активен",
|
||||||
"survey_not_found": "Опрос не найден",
|
|
||||||
"survey_paused": "Опрос приостановлен.",
|
"survey_paused": "Опрос приостановлен.",
|
||||||
"survey_type": "Тип опроса",
|
"survey_type": "Тип опроса",
|
||||||
"surveys": "Опросы",
|
"surveys": "Опросы",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Название команды",
|
"team_name": "Название команды",
|
||||||
"team_role": "Роль в команде",
|
"team_role": "Роль в команде",
|
||||||
"teams": "Команды",
|
"teams": "Команды",
|
||||||
"teams_not_found": "Команды не найдены",
|
|
||||||
"text": "Текст",
|
"text": "Текст",
|
||||||
"time": "Время",
|
"time": "Время",
|
||||||
"time_to_finish": "Время до завершения",
|
"time_to_finish": "Время до завершения",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Пользователь",
|
"user": "Пользователь",
|
||||||
"user_id": "ID пользователя",
|
"user_id": "ID пользователя",
|
||||||
"user_not_found": "Пользователь не найден",
|
|
||||||
"variable": "Переменная",
|
"variable": "Переменная",
|
||||||
"variable_ids": "ID переменных",
|
"variable_ids": "ID переменных",
|
||||||
"variables": "Переменные",
|
"variables": "Переменные",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "недели",
|
"weeks": "недели",
|
||||||
"welcome_card": "Приветственная карточка",
|
"welcome_card": "Приветственная карточка",
|
||||||
"workflows": "Воркфлоу",
|
"workflows": "Воркфлоу",
|
||||||
|
"workspace": "Рабочее пространство",
|
||||||
"workspace_configuration": "Настройка рабочего пространства",
|
"workspace_configuration": "Настройка рабочего пространства",
|
||||||
"workspace_created_successfully": "Рабочий проект успешно создан",
|
"workspace_created_successfully": "Рабочий проект успешно создан",
|
||||||
"workspace_creation_description": "Организуйте опросы в рабочих пространствах для лучшего контроля доступа.",
|
"workspace_creation_description": "Организуйте опросы в рабочих пространствах для лучшего контроля доступа.",
|
||||||
"workspace_id": "ID рабочего пространства",
|
"workspace_id": "ID рабочего пространства",
|
||||||
"workspace_name": "Название рабочего пространства",
|
"workspace_name": "Название рабочего пространства",
|
||||||
"workspace_name_placeholder": "например, Formbricks",
|
"workspace_name_placeholder": "например, Formbricks",
|
||||||
"workspace_not_found": "Рабочее пространство не найдено",
|
|
||||||
"workspace_permission_not_found": "Разрешение на рабочее пространство не найдено",
|
|
||||||
"workspaces": "Рабочие пространства",
|
"workspaces": "Рабочие пространства",
|
||||||
"years": "годы",
|
"years": "годы",
|
||||||
"you": "Вы",
|
"you": "Вы",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Создан новый атрибут «{key}» с типом «{dataType}»",
|
"attributes_msg_new_attribute_created": "Создан новый атрибут «{key}» с типом «{dataType}»",
|
||||||
"attributes_msg_userid_already_exists": "Этот user ID уже существует в данной среде и не был обновлён.",
|
"attributes_msg_userid_already_exists": "Этот user ID уже существует в данной среде и не был обновлён.",
|
||||||
"contact_deleted_successfully": "Контакт успешно удалён",
|
"contact_deleted_successfully": "Контакт успешно удалён",
|
||||||
"contact_not_found": "Такой контакт не найден",
|
|
||||||
"contacts_table_refresh": "Обновить контакты",
|
"contacts_table_refresh": "Обновить контакты",
|
||||||
"contacts_table_refresh_success": "Контакты успешно обновлены",
|
"contacts_table_refresh_success": "Контакты успешно обновлены",
|
||||||
"create_attribute": "Создать атрибут",
|
"create_attribute": "Создать атрибут",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Создано сторонней организацией",
|
"created_by_third_party": "Создано сторонней организацией",
|
||||||
"discord_webhook_not_supported": "В настоящее время webhooks Discord не поддерживаются.",
|
"discord_webhook_not_supported": "В настоящее время webhooks Discord не поддерживаются.",
|
||||||
"empty_webhook_message": "Ваши webhooks появятся здесь, как только вы их добавите. ⏲️",
|
"empty_webhook_message": "Ваши webhooks появятся здесь, как только вы их добавите. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Ошибка шлюза (502): Ошибка прокси/шлюза, сервис недоступен",
|
||||||
|
"endpoint_gateway_timeout_error": "Тайм-аут шлюза (504): Тайм-аут шлюза, сервис недоступен",
|
||||||
|
"endpoint_internal_server_error": "Внутренняя ошибка сервера (500): Сервис столкнулся с непредвиденной ошибкой",
|
||||||
|
"endpoint_method_not_allowed_error": "Метод не разрешен (405): Конечная точка существует, но не принимает POST-запросы",
|
||||||
|
"endpoint_not_found_error": "Не найдено (404): Конечная точка не существует",
|
||||||
"endpoint_pinged": "Ура! Нам удалось отправить ping на webhook!",
|
"endpoint_pinged": "Ура! Нам удалось отправить ping на webhook!",
|
||||||
"endpoint_pinged_error": "Не удалось отправить ping на webhook!",
|
"endpoint_pinged_error": "Не удалось отправить ping на webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "Сервис недоступен (503): Сервис временно недоступен",
|
||||||
"learn_to_verify": "Узнайте, как проверить подписи вебхуков",
|
"learn_to_verify": "Узнайте, как проверить подписи вебхуков",
|
||||||
|
"no_triggers": "Нет триггеров",
|
||||||
"please_check_console": "Пожалуйста, проверьте консоль для получения подробностей",
|
"please_check_console": "Пожалуйста, проверьте консоль для получения подробностей",
|
||||||
"please_enter_a_url": "Пожалуйста, введите URL",
|
"please_enter_a_url": "Пожалуйста, введите URL",
|
||||||
"response_created": "Ответ создан",
|
"response_created": "Ответ создан",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Функции для предприятий",
|
"enterprise_features": "Функции для предприятий",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Получите корпоративную лицензию для доступа ко всем функциям.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Получите корпоративную лицензию для доступа ко всем функциям.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Полный контроль над конфиденциальностью и безопасностью ваших данных.",
|
"keep_full_control_over_your_data_privacy_and_security": "Полный контроль над конфиденциальностью и безопасностью ваших данных.",
|
||||||
|
"license_feature_access_control": "Управление доступом (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Журналы аудита",
|
||||||
|
"license_feature_contacts": "Контакты и сегменты",
|
||||||
|
"license_feature_projects": "Рабочие пространства",
|
||||||
|
"license_feature_quotas": "Квоты",
|
||||||
|
"license_feature_remove_branding": "Удаление брендирования",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Защита от спама",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Двухфакторная аутентификация",
|
||||||
|
"license_feature_whitelabel": "Электронные письма без брендирования",
|
||||||
|
"license_features_table_access": "Доступ",
|
||||||
|
"license_features_table_description": "Корпоративные функции и ограничения, доступные для этого экземпляра.",
|
||||||
|
"license_features_table_disabled": "Отключено",
|
||||||
|
"license_features_table_enabled": "Включено",
|
||||||
|
"license_features_table_feature": "Функция",
|
||||||
|
"license_features_table_title": "Лицензированные функции",
|
||||||
|
"license_features_table_unlimited": "Без ограничений",
|
||||||
|
"license_features_table_value": "Значение",
|
||||||
"license_instance_mismatch_description": "Эта лицензия в данный момент привязана к другому экземпляру Formbricks. Если эта установка была пересобрана или перемещена, обратитесь в службу поддержки Formbricks для отключения предыдущей привязки экземпляра.",
|
"license_instance_mismatch_description": "Эта лицензия в данный момент привязана к другому экземпляру Formbricks. Если эта установка была пересобрана или перемещена, обратитесь в службу поддержки Formbricks для отключения предыдущей привязки экземпляра.",
|
||||||
"license_invalid_description": "Ключ лицензии в переменной окружения ENTERPRISE_LICENSE_KEY недействителен. Проверь, нет ли опечаток, или запроси новый ключ.",
|
"license_invalid_description": "Ключ лицензии в переменной окружения ENTERPRISE_LICENSE_KEY недействителен. Проверь, нет ли опечаток, или запроси новый ключ.",
|
||||||
"license_status": "Статус лицензии",
|
"license_status": "Статус лицензии",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Пользовательский хостнейм",
|
"custom_hostname": "Пользовательский хостнейм",
|
||||||
"customize_survey_logo": "Настроить логотип опроса",
|
"customize_survey_logo": "Настроить логотип опроса",
|
||||||
"darken_or_lighten_background_of_your_choice": "Затемните или осветлите выбранный фон.",
|
"darken_or_lighten_background_of_your_choice": "Затемните или осветлите выбранный фон.",
|
||||||
"date_format": "Формат даты",
|
|
||||||
"days_before_showing_this_survey_again": "или больше дней должно пройти между последним показом опроса и показом этого опроса.",
|
"days_before_showing_this_survey_again": "или больше дней должно пройти между последним показом опроса и показом этого опроса.",
|
||||||
"delete_anyways": "Удалить в любом случае",
|
"delete_anyways": "Удалить в любом случае",
|
||||||
"delete_block": "Удалить блок",
|
"delete_block": "Удалить блок",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Ошибка при сохранении изменений",
|
"error_saving_changes": "Ошибка при сохранении изменений",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Разрешить несколько ответов; продолжать показывать даже после ответа (например, окно обратной связи).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Разрешить несколько ответов; продолжать показывать даже после ответа (например, окно обратной связи).",
|
||||||
"everyone": "Все",
|
"everyone": "Все",
|
||||||
|
"expand_preview": "Развернуть предпросмотр",
|
||||||
"external_urls_paywall_tooltip": "Пожалуйста, перейдите на платный тариф, чтобы настраивать внешние ссылки. Это помогает нам предотвращать фишинг.",
|
"external_urls_paywall_tooltip": "Пожалуйста, перейдите на платный тариф, чтобы настраивать внешние ссылки. Это помогает нам предотвращать фишинг.",
|
||||||
"fallback_missing": "Запасное значение отсутствует",
|
"fallback_missing": "Запасное значение отсутствует",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "Лимит ответов должен превышать количество полученных ответов ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "Лимит ответов должен превышать количество полученных ответов ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Лимиты ответов, перенаправления и другое.",
|
"response_limits_redirections_and_more": "Лимиты ответов, перенаправления и другое.",
|
||||||
"response_options": "Параметры ответа",
|
"response_options": "Параметры ответа",
|
||||||
|
"reverse_order_occasionally": "Иногда обращать порядок",
|
||||||
|
"reverse_order_occasionally_except_last": "Иногда обращать порядок кроме последнего",
|
||||||
"roundness": "Скругление",
|
"roundness": "Скругление",
|
||||||
"roundness_description": "Определяет степень скругления углов.",
|
"roundness_description": "Определяет степень скругления углов.",
|
||||||
"row_used_in_logic_error": "Эта строка используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите её из логики.",
|
"row_used_in_logic_error": "Эта строка используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите её из логики.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Показать опрос максимум",
|
"show_survey_maximum_of": "Показать опрос максимум",
|
||||||
"show_survey_to_users": "Показать опрос % пользователей",
|
"show_survey_to_users": "Показать опрос % пользователей",
|
||||||
"show_to_x_percentage_of_targeted_users": "Показать {percentage}% целевых пользователей",
|
"show_to_x_percentage_of_targeted_users": "Показать {percentage}% целевых пользователей",
|
||||||
|
"shrink_preview": "Свернуть предпросмотр",
|
||||||
"simple": "Простой",
|
"simple": "Простой",
|
||||||
"six_points": "6 баллов",
|
"six_points": "6 баллов",
|
||||||
"smiley": "Смайлик",
|
"smiley": "Смайлик",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Оформление установлено в соответствии с темой",
|
"styling_set_to_theme_styles": "Оформление установлено в соответствии с темой",
|
||||||
"subheading": "Подзаголовок",
|
"subheading": "Подзаголовок",
|
||||||
"subtract": "Вычесть -",
|
"subtract": "Вычесть -",
|
||||||
|
"survey_closed_message_heading_required": "Добавьте заголовок к сообщению о закрытом опросе.",
|
||||||
"survey_completed_heading": "Опрос завершён",
|
"survey_completed_heading": "Опрос завершён",
|
||||||
"survey_completed_subheading": "Этот бесплатный и открытый опрос был закрыт",
|
"survey_completed_subheading": "Этот бесплатный и открытый опрос был закрыт",
|
||||||
"survey_display_settings": "Настройки отображения опроса",
|
"survey_display_settings": "Настройки отображения опроса",
|
||||||
"survey_placement": "Размещение опроса",
|
"survey_placement": "Размещение опроса",
|
||||||
|
"survey_preview": "Предпросмотр опроса 👀",
|
||||||
"survey_styling": "Оформление формы",
|
"survey_styling": "Оформление формы",
|
||||||
"survey_trigger": "Триггер опроса",
|
"survey_trigger": "Триггер опроса",
|
||||||
"switch_multi_language_on_to_get_started": "Включите многоязычный режим, чтобы начать 👉",
|
"switch_multi_language_on_to_get_started": "Включите многоязычный режим, чтобы начать 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Нет, спасибо!",
|
"preview_survey_question_2_choice_2_label": "Нет, спасибо!",
|
||||||
"preview_survey_question_2_headline": "Хотите быть в курсе событий?",
|
"preview_survey_question_2_headline": "Хотите быть в курсе событий?",
|
||||||
"preview_survey_question_2_subheader": "Это пример описания.",
|
"preview_survey_question_2_subheader": "Это пример описания.",
|
||||||
"preview_survey_question_open_text_headline": "Есть ли ещё что-то, чем хочешь поделиться?",
|
"preview_survey_question_open_text_headline": "Хотите ли вы чем-то ещё поделиться?",
|
||||||
"preview_survey_question_open_text_placeholder": "Введи свой ответ здесь...",
|
"preview_survey_question_open_text_placeholder": "Введи свой ответ здесь...",
|
||||||
"preview_survey_question_open_text_subheader": "Твой отзыв помогает нам становиться лучше.",
|
"preview_survey_question_open_text_subheader": "Твой отзыв помогает нам становиться лучше.",
|
||||||
"preview_survey_welcome_card_headline": "Добро пожаловать!",
|
"preview_survey_welcome_card_headline": "Добро пожаловать!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Спасибо, что поделился своей идеей воркфлоу с нами! Сейчас мы разрабатываем эту функцию, и твой отзыв поможет нам сделать именно то, что тебе нужно.",
|
"coming_soon_description": "Спасибо, что поделился своей идеей воркфлоу с нами! Сейчас мы разрабатываем эту функцию, и твой отзыв поможет нам сделать именно то, что тебе нужно.",
|
||||||
"coming_soon_title": "Мы почти готовы!",
|
"coming_soon_title": "Мы почти готовы!",
|
||||||
"follow_up_label": "Хочешь что-то ещё добавить?",
|
"follow_up_label": "Хотите ли вы что-нибудь добавить?",
|
||||||
"follow_up_placeholder": "Какие конкретные задачи вы хотите автоматизировать? Какие инструменты или интеграции вам хотелось бы добавить?",
|
"follow_up_placeholder": "Какие конкретные задачи вы хотите автоматизировать? Какие инструменты или интеграции вам хотелось бы добавить?",
|
||||||
"generate_button": "Сгенерировать воркфлоу",
|
"generate_button": "Сгенерировать воркфлоу",
|
||||||
"heading": "Какой воркфлоу ты хочешь создать?",
|
"heading": "Какой воркфлоу ты хочешь создать?",
|
||||||
|
|||||||
+47
-13
@@ -167,6 +167,7 @@
|
|||||||
"connect": "Anslut",
|
"connect": "Anslut",
|
||||||
"connect_formbricks": "Anslut Formbricks",
|
"connect_formbricks": "Anslut Formbricks",
|
||||||
"connected": "Ansluten",
|
"connected": "Ansluten",
|
||||||
|
"contact": "Kontakt",
|
||||||
"contacts": "Kontakter",
|
"contacts": "Kontakter",
|
||||||
"continue": "Fortsätt",
|
"continue": "Fortsätt",
|
||||||
"copied": "Kopierad",
|
"copied": "Kopierad",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "Kopiera",
|
"copy": "Kopiera",
|
||||||
"copy_code": "Kopiera kod",
|
"copy_code": "Kopiera kod",
|
||||||
"copy_link": "Kopiera länk",
|
"copy_link": "Kopiera länk",
|
||||||
|
"copy_to_environment": "Kopiera till {{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attribut}}",
|
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attribut}}",
|
||||||
"count_contacts": "{count, plural, one {{count} kontakt} other {{count} kontakter}}",
|
"count_contacts": "{count, plural, one {{count} kontakt} other {{count} kontakter}}",
|
||||||
"count_members": "{count, plural, one {{count} medlem} other {{count} medlemmar}}",
|
"count_members": "{count, plural, one {{count} medlem} other {{count} medlemmar}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(kopia {copyNumber})",
|
"duplicate_copy_number": "(kopia {copyNumber})",
|
||||||
"e_commerce": "E-handel",
|
"e_commerce": "E-handel",
|
||||||
"edit": "Redigera",
|
"edit": "Redigera",
|
||||||
|
"elements": "Element",
|
||||||
"email": "E-post",
|
"email": "E-post",
|
||||||
"ending_card": "Avslutningskort",
|
"ending_card": "Avslutningskort",
|
||||||
"enter_url": "Ange URL",
|
"enter_url": "Ange URL",
|
||||||
"enterprise_license": "Företagslicens",
|
"enterprise_license": "Företagslicens",
|
||||||
"environment": "Miljö",
|
"environment": "Miljö",
|
||||||
"environment_not_found": "Miljö hittades inte",
|
|
||||||
"environment_notice": "Du är för närvarande i {environment}-miljön.",
|
"environment_notice": "Du är för närvarande i {environment}-miljön.",
|
||||||
"error": "Fel",
|
"error": "Fel",
|
||||||
"error_component_description": "Denna resurs finns inte eller så har du inte de nödvändiga rättigheterna för att komma åt den.",
|
"error_component_description": "Denna resurs finns inte eller så har du inte de nödvändiga rättigheterna för att komma åt den.",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "Inaktiva enkäter",
|
"inactive_surveys": "Inaktiva enkäter",
|
||||||
"integration": "integration",
|
"integration": "integration",
|
||||||
"integrations": "Integrationer",
|
"integrations": "Integrationer",
|
||||||
"invalid_date": "Ogiltigt datum",
|
"invalid_date_with_value": "Ogiltigt datum: {value}",
|
||||||
"invalid_file_name": "Ogiltigt filnamn, vänligen byt namn på din fil och försök igen",
|
"invalid_file_name": "Ogiltigt filnamn, vänligen byt namn på din fil och försök igen",
|
||||||
"invalid_file_type": "Ogiltig filtyp",
|
"invalid_file_type": "Ogiltig filtyp",
|
||||||
"invite": "Bjud in",
|
"invite": "Bjud in",
|
||||||
"invite_them": "Bjud in dem",
|
"invite_them": "Bjud in dem",
|
||||||
|
"javascript_required": "JavaScript krävs",
|
||||||
|
"javascript_required_description": "Formbricks kräver JavaScript för att fungera korrekt. Vänligen aktivera JavaScript i dina webbläsarinställningar för att fortsätta.",
|
||||||
"key": "Nyckel",
|
"key": "Nyckel",
|
||||||
"label": "Etikett",
|
"label": "Etikett",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "Marknadsföring",
|
"marketing": "Marknadsföring",
|
||||||
"members": "Medlemmar",
|
"members": "Medlemmar",
|
||||||
"members_and_teams": "Medlemmar och team",
|
"members_and_teams": "Medlemmar och team",
|
||||||
|
"membership": "Medlemskap",
|
||||||
"membership_not_found": "Medlemskap hittades inte",
|
"membership_not_found": "Medlemskap hittades inte",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks fungerar bäst på en större skärm. Byt till en annan enhet för att hantera eller bygga enkäter.",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks fungerar bäst på en större skärm. Byt till en annan enhet för att hantera eller bygga enkäter.",
|
||||||
"mobile_overlay_surveys_look_good": "Oroa dig inte – dina enkäter ser bra ut på alla enheter och skärmstorlekar!",
|
"mobile_overlay_surveys_look_good": "Oroa dig inte – dina enkäter ser bra ut på alla enheter och skärmstorlekar!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "Ny",
|
"new": "Ny",
|
||||||
"new_version_available": "Formbricks {version} är här. Uppgradera nu!",
|
"new_version_available": "Formbricks {version} är här. Uppgradera nu!",
|
||||||
"next": "Nästa",
|
"next": "Nästa",
|
||||||
|
"no_actions_found": "Inga åtgärder hittades",
|
||||||
"no_background_image_found": "Ingen bakgrundsbild hittades.",
|
"no_background_image_found": "Ingen bakgrundsbild hittades.",
|
||||||
"no_code": "Ingen kod",
|
"no_code": "Ingen kod",
|
||||||
"no_files_uploaded": "Inga filer laddades upp",
|
"no_files_uploaded": "Inga filer laddades upp",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "eller",
|
"or": "eller",
|
||||||
"organization": "Organisation",
|
"organization": "Organisation",
|
||||||
"organization_id": "Organisations-ID",
|
"organization_id": "Organisations-ID",
|
||||||
"organization_not_found": "Organisation hittades inte",
|
|
||||||
"organization_settings": "Organisationsinställningar",
|
"organization_settings": "Organisationsinställningar",
|
||||||
"organization_teams_not_found": "Organisationsteam hittades inte",
|
|
||||||
"other": "Annat",
|
"other": "Annat",
|
||||||
|
"other_filters": "Andra filter",
|
||||||
"others": "Andra",
|
"others": "Andra",
|
||||||
"overlay_color": "Overlay-färg",
|
"overlay_color": "Overlay-färg",
|
||||||
"overview": "Översikt",
|
"overview": "Översikt",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "Vänligen välj minst en enkät",
|
"please_select_at_least_one_survey": "Vänligen välj minst en enkät",
|
||||||
"please_select_at_least_one_trigger": "Vänligen välj minst en utlösare",
|
"please_select_at_least_one_trigger": "Vänligen välj minst en utlösare",
|
||||||
"please_upgrade_your_plan": "Vänligen uppgradera din plan",
|
"please_upgrade_your_plan": "Vänligen uppgradera din plan",
|
||||||
|
"powered_by_formbricks": "Drivs av Formbricks",
|
||||||
"preview": "Förhandsgranska",
|
"preview": "Förhandsgranska",
|
||||||
"preview_survey": "Förhandsgranska enkät",
|
"preview_survey": "Förhandsgranska enkät",
|
||||||
"privacy": "Integritetspolicy",
|
"privacy": "Integritetspolicy",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "Välj",
|
"select": "Välj",
|
||||||
"select_all": "Välj alla",
|
"select_all": "Välj alla",
|
||||||
"select_filter": "Välj filter",
|
"select_filter": "Välj filter",
|
||||||
|
"select_language": "Välj språk",
|
||||||
"select_survey": "Välj enkät",
|
"select_survey": "Välj enkät",
|
||||||
"select_teams": "Välj team",
|
"select_teams": "Välj team",
|
||||||
"selected": "Vald",
|
"selected": "Vald",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "Enkät-ID",
|
"survey_id": "Enkät-ID",
|
||||||
"survey_languages": "Enkätspråk",
|
"survey_languages": "Enkätspråk",
|
||||||
"survey_live": "Enkät live",
|
"survey_live": "Enkät live",
|
||||||
"survey_not_found": "Enkät hittades inte",
|
|
||||||
"survey_paused": "Enkät pausad.",
|
"survey_paused": "Enkät pausad.",
|
||||||
"survey_type": "Enkättyp",
|
"survey_type": "Enkättyp",
|
||||||
"surveys": "Enkäter",
|
"surveys": "Enkäter",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "Teamnamn",
|
"team_name": "Teamnamn",
|
||||||
"team_role": "Teamroll",
|
"team_role": "Teamroll",
|
||||||
"teams": "Åtkomstkontroll",
|
"teams": "Åtkomstkontroll",
|
||||||
"teams_not_found": "Team hittades inte",
|
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"time": "Tid",
|
"time": "Tid",
|
||||||
"time_to_finish": "Tid att slutföra",
|
"time_to_finish": "Tid att slutföra",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "Användare",
|
"user": "Användare",
|
||||||
"user_id": "Användar-ID",
|
"user_id": "Användar-ID",
|
||||||
"user_not_found": "Användare hittades inte",
|
|
||||||
"variable": "Variabel",
|
"variable": "Variabel",
|
||||||
"variable_ids": "Variabel-ID:n",
|
"variable_ids": "Variabel-ID:n",
|
||||||
"variables": "Variabler",
|
"variables": "Variabler",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "veckor",
|
"weeks": "veckor",
|
||||||
"welcome_card": "Välkomstkort",
|
"welcome_card": "Välkomstkort",
|
||||||
"workflows": "Arbetsflöden",
|
"workflows": "Arbetsflöden",
|
||||||
|
"workspace": "Arbetsyta",
|
||||||
"workspace_configuration": "Arbetsytans konfiguration",
|
"workspace_configuration": "Arbetsytans konfiguration",
|
||||||
"workspace_created_successfully": "Arbetsytan har skapats",
|
"workspace_created_successfully": "Arbetsytan har skapats",
|
||||||
"workspace_creation_description": "Organisera enkäter i arbetsytor för bättre åtkomstkontroll.",
|
"workspace_creation_description": "Organisera enkäter i arbetsytor för bättre åtkomstkontroll.",
|
||||||
"workspace_id": "Arbetsyte-ID",
|
"workspace_id": "Arbetsyte-ID",
|
||||||
"workspace_name": "Arbetsytans namn",
|
"workspace_name": "Arbetsytans namn",
|
||||||
"workspace_name_placeholder": "t.ex. Formbricks",
|
"workspace_name_placeholder": "t.ex. Formbricks",
|
||||||
"workspace_not_found": "Arbetsyta hittades inte",
|
|
||||||
"workspace_permission_not_found": "Arbetsytebehörighet hittades inte",
|
|
||||||
"workspaces": "Arbetsytor",
|
"workspaces": "Arbetsytor",
|
||||||
"years": "år",
|
"years": "år",
|
||||||
"you": "Du",
|
"you": "Du",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "Nytt attribut ”{key}” med typen ”{dataType}” har skapats",
|
"attributes_msg_new_attribute_created": "Nytt attribut ”{key}” med typen ”{dataType}” har skapats",
|
||||||
"attributes_msg_userid_already_exists": "Användar-ID finns redan för denna miljö och uppdaterades inte.",
|
"attributes_msg_userid_already_exists": "Användar-ID finns redan för denna miljö och uppdaterades inte.",
|
||||||
"contact_deleted_successfully": "Kontakt borttagen",
|
"contact_deleted_successfully": "Kontakt borttagen",
|
||||||
"contact_not_found": "Ingen sådan kontakt hittades",
|
|
||||||
"contacts_table_refresh": "Uppdatera kontakter",
|
"contacts_table_refresh": "Uppdatera kontakter",
|
||||||
"contacts_table_refresh_success": "Kontakter uppdaterade",
|
"contacts_table_refresh_success": "Kontakter uppdaterade",
|
||||||
"create_attribute": "Skapa attribut",
|
"create_attribute": "Skapa attribut",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "Skapad av tredje part",
|
"created_by_third_party": "Skapad av tredje part",
|
||||||
"discord_webhook_not_supported": "Discord-webhooks stöds för närvarande inte.",
|
"discord_webhook_not_supported": "Discord-webhooks stöds för närvarande inte.",
|
||||||
"empty_webhook_message": "Dina webhooks visas här så snart du lägger till dem. ⏲️",
|
"empty_webhook_message": "Dina webhooks visas här så snart du lägger till dem. ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "Felaktig gateway (502): Proxy-/gatewayfel, tjänsten kan inte nås",
|
||||||
|
"endpoint_gateway_timeout_error": "Gateway-timeout (504): Gateway-timeout, tjänsten kan inte nås",
|
||||||
|
"endpoint_internal_server_error": "Internt serverfel (500): Tjänsten stötte på ett oväntat fel",
|
||||||
|
"endpoint_method_not_allowed_error": "Metoden tillåts inte (405): Endpointen finns, men accepterar inte POST-förfrågningar",
|
||||||
|
"endpoint_not_found_error": "Hittades inte (404): Endpointen finns inte",
|
||||||
"endpoint_pinged": "Ja! Vi kan nå webhooken!",
|
"endpoint_pinged": "Ja! Vi kan nå webhooken!",
|
||||||
"endpoint_pinged_error": "Kunde inte nå webhooken!",
|
"endpoint_pinged_error": "Kunde inte nå webhooken!",
|
||||||
|
"endpoint_service_unavailable_error": "Tjänsten är inte tillgänglig (503): Tjänsten är tillfälligt nere",
|
||||||
"learn_to_verify": "Lär dig hur du verifierar webhook-signaturer",
|
"learn_to_verify": "Lär dig hur du verifierar webhook-signaturer",
|
||||||
|
"no_triggers": "Inga utlösare",
|
||||||
"please_check_console": "Vänligen kontrollera konsolen för mer information",
|
"please_check_console": "Vänligen kontrollera konsolen för mer information",
|
||||||
"please_enter_a_url": "Vänligen ange en URL",
|
"please_enter_a_url": "Vänligen ange en URL",
|
||||||
"response_created": "Svar skapat",
|
"response_created": "Svar skapat",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "Enterprise-funktioner",
|
"enterprise_features": "Enterprise-funktioner",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "Skaffa en Enterprise-licens för att få tillgång till alla funktioner.",
|
"get_an_enterprise_license_to_get_access_to_all_features": "Skaffa en Enterprise-licens för att få tillgång till alla funktioner.",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "Behåll full kontroll över din datasekretess och säkerhet.",
|
"keep_full_control_over_your_data_privacy_and_security": "Behåll full kontroll över din datasekretess och säkerhet.",
|
||||||
|
"license_feature_access_control": "Åtkomstkontroll (RBAC)",
|
||||||
|
"license_feature_audit_logs": "Granskningsloggar",
|
||||||
|
"license_feature_contacts": "Kontakter & Segment",
|
||||||
|
"license_feature_projects": "Arbetsytor",
|
||||||
|
"license_feature_quotas": "Kvoter",
|
||||||
|
"license_feature_remove_branding": "Ta bort varumärkning",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "Skräppostskydd",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "Tvåfaktorsautentisering",
|
||||||
|
"license_feature_whitelabel": "White-label-mejl",
|
||||||
|
"license_features_table_access": "Åtkomst",
|
||||||
|
"license_features_table_description": "Företagsfunktioner och begränsningar som för närvarande är tillgängliga för den här instansen.",
|
||||||
|
"license_features_table_disabled": "Inaktiverad",
|
||||||
|
"license_features_table_enabled": "Aktiverad",
|
||||||
|
"license_features_table_feature": "Funktion",
|
||||||
|
"license_features_table_title": "Licensierade funktioner",
|
||||||
|
"license_features_table_unlimited": "Obegränsad",
|
||||||
|
"license_features_table_value": "Värde",
|
||||||
"license_instance_mismatch_description": "Den här licensen är för närvarande kopplad till en annan Formbricks-instans. Om den här installationen har återuppbyggts eller flyttats, be Formbricks support att koppla bort den tidigare instansbindningen.",
|
"license_instance_mismatch_description": "Den här licensen är för närvarande kopplad till en annan Formbricks-instans. Om den här installationen har återuppbyggts eller flyttats, be Formbricks support att koppla bort den tidigare instansbindningen.",
|
||||||
"license_invalid_description": "Licensnyckeln i din ENTERPRISE_LICENSE_KEY-miljövariabel är ogiltig. Kontrollera om det finns stavfel eller begär en ny nyckel.",
|
"license_invalid_description": "Licensnyckeln i din ENTERPRISE_LICENSE_KEY-miljövariabel är ogiltig. Kontrollera om det finns stavfel eller begär en ny nyckel.",
|
||||||
"license_status": "Licensstatus",
|
"license_status": "Licensstatus",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "Anpassat värdnamn",
|
"custom_hostname": "Anpassat värdnamn",
|
||||||
"customize_survey_logo": "Anpassa undersökningens logotyp",
|
"customize_survey_logo": "Anpassa undersökningens logotyp",
|
||||||
"darken_or_lighten_background_of_your_choice": "Gör bakgrunden mörkare eller ljusare efter eget val.",
|
"darken_or_lighten_background_of_your_choice": "Gör bakgrunden mörkare eller ljusare efter eget val.",
|
||||||
"date_format": "Datumformat",
|
|
||||||
"days_before_showing_this_survey_again": "eller fler dagar måste gå mellan den senaste visade enkäten och att visa denna enkät.",
|
"days_before_showing_this_survey_again": "eller fler dagar måste gå mellan den senaste visade enkäten och att visa denna enkät.",
|
||||||
"delete_anyways": "Ta bort ändå",
|
"delete_anyways": "Ta bort ändå",
|
||||||
"delete_block": "Ta bort block",
|
"delete_block": "Ta bort block",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "Fel vid sparande av ändringar",
|
"error_saving_changes": "Fel vid sparande av ändringar",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "Tillåt flera svar; fortsätt visa även efter ett svar (t.ex. feedbackruta).",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "Tillåt flera svar; fortsätt visa även efter ett svar (t.ex. feedbackruta).",
|
||||||
"everyone": "Alla",
|
"everyone": "Alla",
|
||||||
|
"expand_preview": "Expandera förhandsgranskning",
|
||||||
"external_urls_paywall_tooltip": "Uppgradera till ett betalt abonnemang för att anpassa externa URL:er. Detta hjälper oss att förhindra nätfiske.",
|
"external_urls_paywall_tooltip": "Uppgradera till ett betalt abonnemang för att anpassa externa URL:er. Detta hjälper oss att förhindra nätfiske.",
|
||||||
"fallback_missing": "Reservvärde saknas",
|
"fallback_missing": "Reservvärde saknas",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{fieldId} används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "Svarsgränsen måste överstiga antalet mottagna svar ({responseCount}).",
|
"response_limit_needs_to_exceed_number_of_received_responses": "Svarsgränsen måste överstiga antalet mottagna svar ({responseCount}).",
|
||||||
"response_limits_redirections_and_more": "Svarsgränser, omdirigeringar och mer.",
|
"response_limits_redirections_and_more": "Svarsgränser, omdirigeringar och mer.",
|
||||||
"response_options": "Svarsalternativ",
|
"response_options": "Svarsalternativ",
|
||||||
|
"reverse_order_occasionally": "Vänd ordning ibland",
|
||||||
|
"reverse_order_occasionally_except_last": "Vänd ordning ibland utom sista",
|
||||||
"roundness": "Rundhet",
|
"roundness": "Rundhet",
|
||||||
"roundness_description": "Styr hur rundade hörnen är.",
|
"roundness_description": "Styr hur rundade hörnen är.",
|
||||||
"row_used_in_logic_error": "Denna rad används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
|
"row_used_in_logic_error": "Denna rad används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "Visa enkät maximalt",
|
"show_survey_maximum_of": "Visa enkät maximalt",
|
||||||
"show_survey_to_users": "Visa enkät för % av användare",
|
"show_survey_to_users": "Visa enkät för % av användare",
|
||||||
"show_to_x_percentage_of_targeted_users": "Visa för {percentage}% av målgruppens användare",
|
"show_to_x_percentage_of_targeted_users": "Visa för {percentage}% av målgruppens användare",
|
||||||
|
"shrink_preview": "Minimera förhandsgranskning",
|
||||||
"simple": "Enkel",
|
"simple": "Enkel",
|
||||||
"six_points": "6 poäng",
|
"six_points": "6 poäng",
|
||||||
"smiley": "Smiley",
|
"smiley": "Smiley",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "Styling inställd på temastil",
|
"styling_set_to_theme_styles": "Styling inställd på temastil",
|
||||||
"subheading": "Underrubrik",
|
"subheading": "Underrubrik",
|
||||||
"subtract": "Subtrahera -",
|
"subtract": "Subtrahera -",
|
||||||
|
"survey_closed_message_heading_required": "Lägg till en rubrik för det anpassade meddelandet när undersökningen är stängd.",
|
||||||
"survey_completed_heading": "Enkät slutförd",
|
"survey_completed_heading": "Enkät slutförd",
|
||||||
"survey_completed_subheading": "Denna gratis och öppenkällkodsenkät har stängts",
|
"survey_completed_subheading": "Denna gratis och öppenkällkodsenkät har stängts",
|
||||||
"survey_display_settings": "Visningsinställningar för enkät",
|
"survey_display_settings": "Visningsinställningar för enkät",
|
||||||
"survey_placement": "Enkätplacering",
|
"survey_placement": "Enkätplacering",
|
||||||
|
"survey_preview": "Enkätförhandsgranskning 👀",
|
||||||
"survey_styling": "Formulärstil",
|
"survey_styling": "Formulärstil",
|
||||||
"survey_trigger": "Enkätutlösare",
|
"survey_trigger": "Enkätutlösare",
|
||||||
"switch_multi_language_on_to_get_started": "Slå på flerspråkighet för att komma igång 👉",
|
"switch_multi_language_on_to_get_started": "Slå på flerspråkighet för att komma igång 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "Nej, tack!",
|
"preview_survey_question_2_choice_2_label": "Nej, tack!",
|
||||||
"preview_survey_question_2_headline": "Vill du hållas uppdaterad?",
|
"preview_survey_question_2_headline": "Vill du hållas uppdaterad?",
|
||||||
"preview_survey_question_2_subheader": "Det här är ett exempel på en beskrivning.",
|
"preview_survey_question_2_subheader": "Det här är ett exempel på en beskrivning.",
|
||||||
"preview_survey_question_open_text_headline": "Något mer du vill dela med dig av?",
|
"preview_survey_question_open_text_headline": "Finns det något annat du vill dela med dig av?",
|
||||||
"preview_survey_question_open_text_placeholder": "Skriv ditt svar här...",
|
"preview_survey_question_open_text_placeholder": "Skriv ditt svar här...",
|
||||||
"preview_survey_question_open_text_subheader": "Din feedback hjälper oss att bli bättre.",
|
"preview_survey_question_open_text_subheader": "Din feedback hjälper oss att bli bättre.",
|
||||||
"preview_survey_welcome_card_headline": "Välkommen!",
|
"preview_survey_welcome_card_headline": "Välkommen!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "Tack för att du delade din arbetsflödesidé med oss! Vi håller just nu på att designa den här funktionen och din feedback hjälper oss att bygga precis det du behöver.",
|
"coming_soon_description": "Tack för att du delade din arbetsflödesidé med oss! Vi håller just nu på att designa den här funktionen och din feedback hjälper oss att bygga precis det du behöver.",
|
||||||
"coming_soon_title": "Vi är nästan där!",
|
"coming_soon_title": "Vi är nästan där!",
|
||||||
"follow_up_label": "Är det något mer du vill lägga till?",
|
"follow_up_label": "Finns det något annat du vill lägga till?",
|
||||||
"follow_up_placeholder": "Vilka specifika uppgifter vill du automatisera? Några verktyg eller integrationer du vill ha med?",
|
"follow_up_placeholder": "Vilka specifika uppgifter vill du automatisera? Några verktyg eller integrationer du vill ha med?",
|
||||||
"generate_button": "Skapa arbetsflöde",
|
"generate_button": "Skapa arbetsflöde",
|
||||||
"heading": "Vilket arbetsflöde vill du skapa?",
|
"heading": "Vilket arbetsflöde vill du skapa?",
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"connect": "连接",
|
"connect": "连接",
|
||||||
"connect_formbricks": "连接 Formbricks",
|
"connect_formbricks": "连接 Formbricks",
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
|
"contact": "联系人",
|
||||||
"contacts": "联系人",
|
"contacts": "联系人",
|
||||||
"continue": "继续",
|
"continue": "继续",
|
||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"copy_code": "复制 代码",
|
"copy_code": "复制 代码",
|
||||||
"copy_link": "复制 链接",
|
"copy_link": "复制 链接",
|
||||||
|
"copy_to_environment": "复制到{{environment}}",
|
||||||
"count_attributes": "{count, plural, one {{count} 个属性} other {{count} 个属性}}",
|
"count_attributes": "{count, plural, one {{count} 个属性} other {{count} 个属性}}",
|
||||||
"count_contacts": "{count, plural, other {{count} 联系人} }",
|
"count_contacts": "{count, plural, other {{count} 联系人} }",
|
||||||
"count_members": "{count, plural, one {{count} 位成员} other {{count} 位成员}}",
|
"count_members": "{count, plural, one {{count} 位成员} other {{count} 位成员}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(副本 {copyNumber})",
|
"duplicate_copy_number": "(副本 {copyNumber})",
|
||||||
"e_commerce": "电子商务",
|
"e_commerce": "电子商务",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
|
"elements": "元素",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"ending_card": "结尾卡片",
|
"ending_card": "结尾卡片",
|
||||||
"enter_url": "输入 URL",
|
"enter_url": "输入 URL",
|
||||||
"enterprise_license": "企业 许可证",
|
"enterprise_license": "企业 许可证",
|
||||||
"environment": "环境",
|
"environment": "环境",
|
||||||
"environment_not_found": "环境 未找到",
|
|
||||||
"environment_notice": "你 目前 位于 {environment} 环境。",
|
"environment_notice": "你 目前 位于 {environment} 环境。",
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
"error_component_description": "这个资源不存在或您没有权限访问它。",
|
"error_component_description": "这个资源不存在或您没有权限访问它。",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "不 活跃 调查",
|
"inactive_surveys": "不 活跃 调查",
|
||||||
"integration": "集成",
|
"integration": "集成",
|
||||||
"integrations": "集成",
|
"integrations": "集成",
|
||||||
"invalid_date": "无效 日期",
|
"invalid_date_with_value": "无效 日期: {value}",
|
||||||
"invalid_file_name": "文件名无效,请重命名文件后重试",
|
"invalid_file_name": "文件名无效,请重命名文件后重试",
|
||||||
"invalid_file_type": "无效 的 文件 类型",
|
"invalid_file_type": "无效 的 文件 类型",
|
||||||
"invite": "邀请",
|
"invite": "邀请",
|
||||||
"invite_them": "邀请 他们",
|
"invite_them": "邀请 他们",
|
||||||
|
"javascript_required": "需要启用 JavaScript",
|
||||||
|
"javascript_required_description": "Formbricks 需要 JavaScript 才能正常运行。请在浏览器设置中启用 JavaScript 以继续。",
|
||||||
"key": "键",
|
"key": "键",
|
||||||
"label": "标签",
|
"label": "标签",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "市场营销",
|
"marketing": "市场营销",
|
||||||
"members": "成员",
|
"members": "成员",
|
||||||
"members_and_teams": "成员和团队",
|
"members_and_teams": "成员和团队",
|
||||||
|
"membership": "会员资格",
|
||||||
"membership_not_found": "未找到会员资格",
|
"membership_not_found": "未找到会员资格",
|
||||||
|
"meta": "元数据",
|
||||||
"metadata": "元数据",
|
"metadata": "元数据",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
|
||||||
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
|
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "新建",
|
"new": "新建",
|
||||||
"new_version_available": "Formbricks {version} 在 这里。立即 升级!",
|
"new_version_available": "Formbricks {version} 在 这里。立即 升级!",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
|
"no_actions_found": "未找到操作",
|
||||||
"no_background_image_found": "未找到 背景 图片。",
|
"no_background_image_found": "未找到 背景 图片。",
|
||||||
"no_code": "无代码",
|
"no_code": "无代码",
|
||||||
"no_files_uploaded": "没有 文件 被 上传",
|
"no_files_uploaded": "没有 文件 被 上传",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "或",
|
"or": "或",
|
||||||
"organization": "组织",
|
"organization": "组织",
|
||||||
"organization_id": "组织 ID",
|
"organization_id": "组织 ID",
|
||||||
"organization_not_found": "组织 未找到",
|
|
||||||
"organization_settings": "组织 设置",
|
"organization_settings": "组织 设置",
|
||||||
"organization_teams_not_found": "未找到 组织 团队",
|
|
||||||
"other": "其他",
|
"other": "其他",
|
||||||
|
"other_filters": "其他筛选条件",
|
||||||
"others": "其他",
|
"others": "其他",
|
||||||
"overlay_color": "覆盖层颜色",
|
"overlay_color": "覆盖层颜色",
|
||||||
"overview": "概览",
|
"overview": "概览",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "请选择至少 一个调查",
|
"please_select_at_least_one_survey": "请选择至少 一个调查",
|
||||||
"please_select_at_least_one_trigger": "请选择至少 一个触发条件",
|
"please_select_at_least_one_trigger": "请选择至少 一个触发条件",
|
||||||
"please_upgrade_your_plan": "请升级您的计划",
|
"please_upgrade_your_plan": "请升级您的计划",
|
||||||
|
"powered_by_formbricks": "由 Formbricks 提供支持",
|
||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"preview_survey": "预览 Survey",
|
"preview_survey": "预览 Survey",
|
||||||
"privacy": "隐私政策",
|
"privacy": "隐私政策",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "选择",
|
"select": "选择",
|
||||||
"select_all": "选择 全部",
|
"select_all": "选择 全部",
|
||||||
"select_filter": "选择过滤器",
|
"select_filter": "选择过滤器",
|
||||||
|
"select_language": "选择语言",
|
||||||
"select_survey": "选择 调查",
|
"select_survey": "选择 调查",
|
||||||
"select_teams": "选择 团队",
|
"select_teams": "选择 团队",
|
||||||
"selected": "已选择",
|
"selected": "已选择",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "调查 ID",
|
"survey_id": "调查 ID",
|
||||||
"survey_languages": "调查 语言",
|
"survey_languages": "调查 语言",
|
||||||
"survey_live": "调查 运行中",
|
"survey_live": "调查 运行中",
|
||||||
"survey_not_found": "调查 未找到",
|
|
||||||
"survey_paused": "调查 暂停。",
|
"survey_paused": "调查 暂停。",
|
||||||
"survey_type": "调查 类型",
|
"survey_type": "调查 类型",
|
||||||
"surveys": "调查",
|
"surveys": "调查",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "团队 名称",
|
"team_name": "团队 名称",
|
||||||
"team_role": "团队角色",
|
"team_role": "团队角色",
|
||||||
"teams": "团队",
|
"teams": "团队",
|
||||||
"teams_not_found": "未找到 团队",
|
|
||||||
"text": "文本",
|
"text": "文本",
|
||||||
"time": "时间",
|
"time": "时间",
|
||||||
"time_to_finish": "完成 时间",
|
"time_to_finish": "完成 时间",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "用户",
|
"user": "用户",
|
||||||
"user_id": "用户 ID",
|
"user_id": "用户 ID",
|
||||||
"user_not_found": "用户 不存在",
|
|
||||||
"variable": "变量",
|
"variable": "变量",
|
||||||
"variable_ids": "变量 ID",
|
"variable_ids": "变量 ID",
|
||||||
"variables": "变量",
|
"variables": "变量",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "周",
|
"weeks": "周",
|
||||||
"welcome_card": "欢迎 卡片",
|
"welcome_card": "欢迎 卡片",
|
||||||
"workflows": "工作流",
|
"workflows": "工作流",
|
||||||
|
"workspace": "工作区",
|
||||||
"workspace_configuration": "工作区配置",
|
"workspace_configuration": "工作区配置",
|
||||||
"workspace_created_successfully": "工作区创建成功",
|
"workspace_created_successfully": "工作区创建成功",
|
||||||
"workspace_creation_description": "在工作区中组织调查,以便更好地进行访问控制。",
|
"workspace_creation_description": "在工作区中组织调查,以便更好地进行访问控制。",
|
||||||
"workspace_id": "工作区 ID",
|
"workspace_id": "工作区 ID",
|
||||||
"workspace_name": "工作区名称",
|
"workspace_name": "工作区名称",
|
||||||
"workspace_name_placeholder": "例如:Formbricks",
|
"workspace_name_placeholder": "例如:Formbricks",
|
||||||
"workspace_not_found": "未找到工作区",
|
|
||||||
"workspace_permission_not_found": "未找到工作区权限",
|
|
||||||
"workspaces": "工作区",
|
"workspaces": "工作区",
|
||||||
"years": "年",
|
"years": "年",
|
||||||
"you": "你 ",
|
"you": "你 ",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "已创建新属性“{key}”,类型为“{dataType}”",
|
"attributes_msg_new_attribute_created": "已创建新属性“{key}”,类型为“{dataType}”",
|
||||||
"attributes_msg_userid_already_exists": "该环境下的用户ID已存在,未进行更新。",
|
"attributes_msg_userid_already_exists": "该环境下的用户ID已存在,未进行更新。",
|
||||||
"contact_deleted_successfully": "联系人 删除 成功",
|
"contact_deleted_successfully": "联系人 删除 成功",
|
||||||
"contact_not_found": "未找到此 联系人",
|
|
||||||
"contacts_table_refresh": "刷新 联系人",
|
"contacts_table_refresh": "刷新 联系人",
|
||||||
"contacts_table_refresh_success": "联系人 已成功刷新",
|
"contacts_table_refresh_success": "联系人 已成功刷新",
|
||||||
"create_attribute": "创建属性",
|
"create_attribute": "创建属性",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "由 第三方 创建",
|
"created_by_third_party": "由 第三方 创建",
|
||||||
"discord_webhook_not_supported": "Discord webhooks 目前不 支持。",
|
"discord_webhook_not_supported": "Discord webhooks 目前不 支持。",
|
||||||
"empty_webhook_message": "您的 Webhooks 会在您 添加 后 出现在这里。 ⏲️",
|
"empty_webhook_message": "您的 Webhooks 会在您 添加 后 出现在这里。 ⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "错误网关 (502):代理/网关错误,服务不可达",
|
||||||
|
"endpoint_gateway_timeout_error": "网关超时 (504):网关超时,服务不可达",
|
||||||
|
"endpoint_internal_server_error": "内部服务器错误 (500):服务遇到了意外错误",
|
||||||
|
"endpoint_method_not_allowed_error": "方法不被允许 (405):该端点存在,但不接受 POST 请求",
|
||||||
|
"endpoint_not_found_error": "未找到 (404):该端点不存在",
|
||||||
"endpoint_pinged": "太好了! 我们能 ping 该 webhook!",
|
"endpoint_pinged": "太好了! 我们能 ping 该 webhook!",
|
||||||
"endpoint_pinged_error": "无法 ping 该 webhook!",
|
"endpoint_pinged_error": "无法 ping 该 webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "服务不可用 (503):服务暂时不可用",
|
||||||
"learn_to_verify": "了解如何验证 webhook 签名",
|
"learn_to_verify": "了解如何验证 webhook 签名",
|
||||||
|
"no_triggers": "无触发器",
|
||||||
"please_check_console": "请查看控制台以获取更多详情",
|
"please_check_console": "请查看控制台以获取更多详情",
|
||||||
"please_enter_a_url": "请输入一个 URL",
|
"please_enter_a_url": "请输入一个 URL",
|
||||||
"response_created": "创建 响应",
|
"response_created": "创建 响应",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "企业 功能",
|
"enterprise_features": "企业 功能",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "获取 企业 许可证 来 访问 所有 功能。",
|
"get_an_enterprise_license_to_get_access_to_all_features": "获取 企业 许可证 来 访问 所有 功能。",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "保持 对 您 的 数据 隐私 和 安全 的 完全 控制。",
|
"keep_full_control_over_your_data_privacy_and_security": "保持 对 您 的 数据 隐私 和 安全 的 完全 控制。",
|
||||||
|
"license_feature_access_control": "访问控制(RBAC)",
|
||||||
|
"license_feature_audit_logs": "审计日志",
|
||||||
|
"license_feature_contacts": "联系人与细分",
|
||||||
|
"license_feature_projects": "工作空间",
|
||||||
|
"license_feature_quotas": "配额",
|
||||||
|
"license_feature_remove_branding": "移除品牌标识",
|
||||||
|
"license_feature_saml": "SAML 单点登录",
|
||||||
|
"license_feature_spam_protection": "垃圾信息防护",
|
||||||
|
"license_feature_sso": "OIDC 单点登录",
|
||||||
|
"license_feature_two_factor_auth": "双因素认证",
|
||||||
|
"license_feature_whitelabel": "白标电子邮件",
|
||||||
|
"license_features_table_access": "访问权限",
|
||||||
|
"license_features_table_description": "此实例当前可用的企业功能和限制。",
|
||||||
|
"license_features_table_disabled": "已禁用",
|
||||||
|
"license_features_table_enabled": "已启用",
|
||||||
|
"license_features_table_feature": "功能",
|
||||||
|
"license_features_table_title": "许可功能",
|
||||||
|
"license_features_table_unlimited": "无限制",
|
||||||
|
"license_features_table_value": "值",
|
||||||
"license_instance_mismatch_description": "此许可证目前绑定到另一个 Formbricks 实例。如果此安装已重建或迁移,请联系 Formbricks 支持团队解除先前的实例绑定。",
|
"license_instance_mismatch_description": "此许可证目前绑定到另一个 Formbricks 实例。如果此安装已重建或迁移,请联系 Formbricks 支持团队解除先前的实例绑定。",
|
||||||
"license_invalid_description": "你在 ENTERPRISE_LICENSE_KEY 环境变量中填写的许可证密钥无效。请检查是否有拼写错误,或者申请一个新的密钥。",
|
"license_invalid_description": "你在 ENTERPRISE_LICENSE_KEY 环境变量中填写的许可证密钥无效。请检查是否有拼写错误,或者申请一个新的密钥。",
|
||||||
"license_status": "许可证状态",
|
"license_status": "许可证状态",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "自 定 义 主 机 名",
|
"custom_hostname": "自 定 义 主 机 名",
|
||||||
"customize_survey_logo": "自定义调查 logo",
|
"customize_survey_logo": "自定义调查 logo",
|
||||||
"darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。",
|
"darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。",
|
||||||
"date_format": "日期格式",
|
|
||||||
"days_before_showing_this_survey_again": "距离上次显示问卷后需间隔不少于指定天数,才能再次显示此问卷。",
|
"days_before_showing_this_survey_again": "距离上次显示问卷后需间隔不少于指定天数,才能再次显示此问卷。",
|
||||||
"delete_anyways": "仍然删除",
|
"delete_anyways": "仍然删除",
|
||||||
"delete_block": "删除区块",
|
"delete_block": "删除区块",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "保存 更改 时 出错",
|
"error_saving_changes": "保存 更改 时 出错",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "允许多次回应;即使已提交回应,仍会继续显示(例如,反馈框)。",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "允许多次回应;即使已提交回应,仍会继续显示(例如,反馈框)。",
|
||||||
"everyone": "所有 人",
|
"everyone": "所有 人",
|
||||||
|
"expand_preview": "展开预览",
|
||||||
"external_urls_paywall_tooltip": "请升级到付费套餐以自定义外部链接。这样有助于我们防范网络钓鱼。",
|
"external_urls_paywall_tooltip": "请升级到付费套餐以自定义外部链接。这样有助于我们防范网络钓鱼。",
|
||||||
"fallback_missing": "备用 缺失",
|
"fallback_missing": "备用 缺失",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{fieldId} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{fieldId} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "限制 响应 需要 超过 收到 的 响应 数量 ({responseCount})。",
|
"response_limit_needs_to_exceed_number_of_received_responses": "限制 响应 需要 超过 收到 的 响应 数量 ({responseCount})。",
|
||||||
"response_limits_redirections_and_more": "响应 限制 、 重定向 和 更多 。",
|
"response_limits_redirections_and_more": "响应 限制 、 重定向 和 更多 。",
|
||||||
"response_options": "响应 选项",
|
"response_options": "响应 选项",
|
||||||
|
"reverse_order_occasionally": "偶尔反转顺序",
|
||||||
|
"reverse_order_occasionally_except_last": "偶尔反转顺序(最后一项除外)",
|
||||||
"roundness": "圆度",
|
"roundness": "圆度",
|
||||||
"roundness_description": "控制圆角的弧度。",
|
"roundness_description": "控制圆角的弧度。",
|
||||||
"row_used_in_logic_error": "\"这个 行 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
"row_used_in_logic_error": "\"这个 行 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "显示 调查 最大 一次",
|
"show_survey_maximum_of": "显示 调查 最大 一次",
|
||||||
"show_survey_to_users": "显示 问卷 给 % 的 用户",
|
"show_survey_to_users": "显示 问卷 给 % 的 用户",
|
||||||
"show_to_x_percentage_of_targeted_users": "显示 给 {percentage}% 的 目标 用户",
|
"show_to_x_percentage_of_targeted_users": "显示 给 {percentage}% 的 目标 用户",
|
||||||
|
"shrink_preview": "收起预览",
|
||||||
"simple": "简单",
|
"simple": "简单",
|
||||||
"six_points": "6 分",
|
"six_points": "6 分",
|
||||||
"smiley": "笑脸",
|
"smiley": "笑脸",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "样式 设置 为 主题 风格",
|
"styling_set_to_theme_styles": "样式 设置 为 主题 风格",
|
||||||
"subheading": "子标题",
|
"subheading": "子标题",
|
||||||
"subtract": "减 -",
|
"subtract": "减 -",
|
||||||
|
"survey_closed_message_heading_required": "请为自定义调查关闭消息添加标题。",
|
||||||
"survey_completed_heading": "调查 完成",
|
"survey_completed_heading": "调查 完成",
|
||||||
"survey_completed_subheading": "此 免费 & 开源 调查 已 关闭",
|
"survey_completed_subheading": "此 免费 & 开源 调查 已 关闭",
|
||||||
"survey_display_settings": "调查显示设置",
|
"survey_display_settings": "调查显示设置",
|
||||||
"survey_placement": "调查 放置",
|
"survey_placement": "调查 放置",
|
||||||
|
"survey_preview": "问卷预览 👀",
|
||||||
"survey_styling": "表单 样式",
|
"survey_styling": "表单 样式",
|
||||||
"survey_trigger": "调查 触发",
|
"survey_trigger": "调查 触发",
|
||||||
"switch_multi_language_on_to_get_started": "开启多语言以开始使用 👉",
|
"switch_multi_language_on_to_get_started": "开启多语言以开始使用 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "不,谢谢!",
|
"preview_survey_question_2_choice_2_label": "不,谢谢!",
|
||||||
"preview_survey_question_2_headline": "想 了解 最新信息吗?",
|
"preview_survey_question_2_headline": "想 了解 最新信息吗?",
|
||||||
"preview_survey_question_2_subheader": "这是一个示例描述。",
|
"preview_survey_question_2_subheader": "这是一个示例描述。",
|
||||||
"preview_survey_question_open_text_headline": "还有什么想和我们分享的吗?",
|
"preview_survey_question_open_text_headline": "还有其他想分享的内容吗?",
|
||||||
"preview_survey_question_open_text_placeholder": "请在这里输入你的答案...",
|
"preview_survey_question_open_text_placeholder": "请在这里输入你的答案...",
|
||||||
"preview_survey_question_open_text_subheader": "你的反馈能帮助我们改进。",
|
"preview_survey_question_open_text_subheader": "你的反馈能帮助我们改进。",
|
||||||
"preview_survey_welcome_card_headline": "欢迎!",
|
"preview_survey_welcome_card_headline": "欢迎!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "感谢你与我们分享你的工作流想法!我们目前正在设计这个功能,你的反馈将帮助我们打造真正适合你的工具。",
|
"coming_soon_description": "感谢你与我们分享你的工作流想法!我们目前正在设计这个功能,你的反馈将帮助我们打造真正适合你的工具。",
|
||||||
"coming_soon_title": "我们快完成啦!",
|
"coming_soon_title": "我们快完成啦!",
|
||||||
"follow_up_label": "你还有其他想补充的吗?",
|
"follow_up_label": "还有其他想补充的内容吗?",
|
||||||
"follow_up_placeholder": "您希望自动化哪些具体任务?是否需要包含特定工具或集成?",
|
"follow_up_placeholder": "您希望自动化哪些具体任务?是否需要包含特定工具或集成?",
|
||||||
"generate_button": "生成工作流",
|
"generate_button": "生成工作流",
|
||||||
"heading": "你想创建什么样的工作流?",
|
"heading": "你想创建什么样的工作流?",
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"connect": "連線",
|
"connect": "連線",
|
||||||
"connect_formbricks": "連線 Formbricks",
|
"connect_formbricks": "連線 Formbricks",
|
||||||
"connected": "已連線",
|
"connected": "已連線",
|
||||||
|
"contact": "聯絡人",
|
||||||
"contacts": "聯絡人",
|
"contacts": "聯絡人",
|
||||||
"continue": "繼續",
|
"continue": "繼續",
|
||||||
"copied": "已 複製",
|
"copied": "已 複製",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"copy": "複製",
|
"copy": "複製",
|
||||||
"copy_code": "複製程式碼",
|
"copy_code": "複製程式碼",
|
||||||
"copy_link": "複製連結",
|
"copy_link": "複製連結",
|
||||||
|
"copy_to_environment": "複製到{{environment}}",
|
||||||
"count_attributes": "{count, plural, other {{count} 個屬性}}",
|
"count_attributes": "{count, plural, other {{count} 個屬性}}",
|
||||||
"count_contacts": "{count, plural, other {{count} 位聯絡人}}",
|
"count_contacts": "{count, plural, other {{count} 位聯絡人}}",
|
||||||
"count_members": "{count, plural, other {{count} 位成員}}",
|
"count_members": "{count, plural, other {{count} 位成員}}",
|
||||||
@@ -213,12 +215,12 @@
|
|||||||
"duplicate_copy_number": "(複製 {copyNumber})",
|
"duplicate_copy_number": "(複製 {copyNumber})",
|
||||||
"e_commerce": "電子商務",
|
"e_commerce": "電子商務",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
|
"elements": "元素",
|
||||||
"email": "電子郵件",
|
"email": "電子郵件",
|
||||||
"ending_card": "結尾卡片",
|
"ending_card": "結尾卡片",
|
||||||
"enter_url": "輸入 URL",
|
"enter_url": "輸入 URL",
|
||||||
"enterprise_license": "企業授權",
|
"enterprise_license": "企業授權",
|
||||||
"environment": "環境",
|
"environment": "環境",
|
||||||
"environment_not_found": "找不到環境",
|
|
||||||
"environment_notice": "您目前在 '{'environment'}' 環境中。",
|
"environment_notice": "您目前在 '{'environment'}' 環境中。",
|
||||||
"error": "錯誤",
|
"error": "錯誤",
|
||||||
"error_component_description": "此資源不存在或您沒有存取權限。",
|
"error_component_description": "此資源不存在或您沒有存取權限。",
|
||||||
@@ -255,11 +257,13 @@
|
|||||||
"inactive_surveys": "停用中的問卷",
|
"inactive_surveys": "停用中的問卷",
|
||||||
"integration": "整合",
|
"integration": "整合",
|
||||||
"integrations": "整合",
|
"integrations": "整合",
|
||||||
"invalid_date": "無效日期",
|
"invalid_date_with_value": "無效日期: {value}",
|
||||||
"invalid_file_name": "檔案名稱無效,請重新命名檔案後再試一次",
|
"invalid_file_name": "檔案名稱無效,請重新命名檔案後再試一次",
|
||||||
"invalid_file_type": "無效的檔案類型",
|
"invalid_file_type": "無效的檔案類型",
|
||||||
"invite": "邀請",
|
"invite": "邀請",
|
||||||
"invite_them": "邀請他們",
|
"invite_them": "邀請他們",
|
||||||
|
"javascript_required": "需要 JavaScript",
|
||||||
|
"javascript_required_description": "Formbricks 需要 JavaScript 才能正常運作。請在瀏覽器設定中啟用 JavaScript 以繼續使用。",
|
||||||
"key": "金鑰",
|
"key": "金鑰",
|
||||||
"label": "標籤",
|
"label": "標籤",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
@@ -280,7 +284,9 @@
|
|||||||
"marketing": "行銷",
|
"marketing": "行銷",
|
||||||
"members": "成員",
|
"members": "成員",
|
||||||
"members_and_teams": "成員與團隊",
|
"members_and_teams": "成員與團隊",
|
||||||
|
"membership": "會員資格",
|
||||||
"membership_not_found": "找不到成員資格",
|
"membership_not_found": "找不到成員資格",
|
||||||
|
"meta": "Meta",
|
||||||
"metadata": "元數據",
|
"metadata": "元數據",
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
|
||||||
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
|
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
|
||||||
@@ -294,6 +300,7 @@
|
|||||||
"new": "新增",
|
"new": "新增",
|
||||||
"new_version_available": "Formbricks '{'version'}' 已推出。立即升級!",
|
"new_version_available": "Formbricks '{'version'}' 已推出。立即升級!",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
|
"no_actions_found": "找不到動作",
|
||||||
"no_background_image_found": "找不到背景圖片。",
|
"no_background_image_found": "找不到背景圖片。",
|
||||||
"no_code": "無程式碼",
|
"no_code": "無程式碼",
|
||||||
"no_files_uploaded": "沒有上傳任何檔案",
|
"no_files_uploaded": "沒有上傳任何檔案",
|
||||||
@@ -319,10 +326,9 @@
|
|||||||
"or": "或",
|
"or": "或",
|
||||||
"organization": "組織",
|
"organization": "組織",
|
||||||
"organization_id": "組織 ID",
|
"organization_id": "組織 ID",
|
||||||
"organization_not_found": "找不到組織",
|
|
||||||
"organization_settings": "組織設定",
|
"organization_settings": "組織設定",
|
||||||
"organization_teams_not_found": "找不到組織團隊",
|
|
||||||
"other": "其他",
|
"other": "其他",
|
||||||
|
"other_filters": "其他篩選條件",
|
||||||
"others": "其他",
|
"others": "其他",
|
||||||
"overlay_color": "覆蓋層顏色",
|
"overlay_color": "覆蓋層顏色",
|
||||||
"overview": "概覽",
|
"overview": "概覽",
|
||||||
@@ -339,6 +345,7 @@
|
|||||||
"please_select_at_least_one_survey": "請選擇至少一個問卷",
|
"please_select_at_least_one_survey": "請選擇至少一個問卷",
|
||||||
"please_select_at_least_one_trigger": "請選擇至少一個觸發器",
|
"please_select_at_least_one_trigger": "請選擇至少一個觸發器",
|
||||||
"please_upgrade_your_plan": "請升級您的方案",
|
"please_upgrade_your_plan": "請升級您的方案",
|
||||||
|
"powered_by_formbricks": "由 Formbricks 提供技術支援",
|
||||||
"preview": "預覽",
|
"preview": "預覽",
|
||||||
"preview_survey": "預覽問卷",
|
"preview_survey": "預覽問卷",
|
||||||
"privacy": "隱私權政策",
|
"privacy": "隱私權政策",
|
||||||
@@ -380,6 +387,7 @@
|
|||||||
"select": "選擇",
|
"select": "選擇",
|
||||||
"select_all": "全選",
|
"select_all": "全選",
|
||||||
"select_filter": "選擇篩選器",
|
"select_filter": "選擇篩選器",
|
||||||
|
"select_language": "選擇語言",
|
||||||
"select_survey": "選擇問卷",
|
"select_survey": "選擇問卷",
|
||||||
"select_teams": "選擇 團隊",
|
"select_teams": "選擇 團隊",
|
||||||
"selected": "已選取",
|
"selected": "已選取",
|
||||||
@@ -412,7 +420,6 @@
|
|||||||
"survey_id": "問卷 ID",
|
"survey_id": "問卷 ID",
|
||||||
"survey_languages": "問卷語言",
|
"survey_languages": "問卷語言",
|
||||||
"survey_live": "問卷已上線",
|
"survey_live": "問卷已上線",
|
||||||
"survey_not_found": "找不到問卷",
|
|
||||||
"survey_paused": "問卷已暫停。",
|
"survey_paused": "問卷已暫停。",
|
||||||
"survey_type": "問卷類型",
|
"survey_type": "問卷類型",
|
||||||
"surveys": "問卷",
|
"surveys": "問卷",
|
||||||
@@ -427,7 +434,6 @@
|
|||||||
"team_name": "團隊名稱",
|
"team_name": "團隊名稱",
|
||||||
"team_role": "團隊角色",
|
"team_role": "團隊角色",
|
||||||
"teams": "團隊",
|
"teams": "團隊",
|
||||||
"teams_not_found": "找不到團隊",
|
|
||||||
"text": "文字",
|
"text": "文字",
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
"time_to_finish": "完成時間",
|
"time_to_finish": "完成時間",
|
||||||
@@ -451,7 +457,6 @@
|
|||||||
"url": "網址",
|
"url": "網址",
|
||||||
"user": "使用者",
|
"user": "使用者",
|
||||||
"user_id": "使用者 ID",
|
"user_id": "使用者 ID",
|
||||||
"user_not_found": "找不到使用者",
|
|
||||||
"variable": "變數",
|
"variable": "變數",
|
||||||
"variable_ids": "變數 ID",
|
"variable_ids": "變數 ID",
|
||||||
"variables": "變數",
|
"variables": "變數",
|
||||||
@@ -467,14 +472,13 @@
|
|||||||
"weeks": "週",
|
"weeks": "週",
|
||||||
"welcome_card": "歡迎卡片",
|
"welcome_card": "歡迎卡片",
|
||||||
"workflows": "工作流程",
|
"workflows": "工作流程",
|
||||||
|
"workspace": "工作區",
|
||||||
"workspace_configuration": "工作區設定",
|
"workspace_configuration": "工作區設定",
|
||||||
"workspace_created_successfully": "工作區已成功建立",
|
"workspace_created_successfully": "工作區已成功建立",
|
||||||
"workspace_creation_description": "將問卷組織在工作區中,以便更好地控管存取權限。",
|
"workspace_creation_description": "將問卷組織在工作區中,以便更好地控管存取權限。",
|
||||||
"workspace_id": "工作區 ID",
|
"workspace_id": "工作區 ID",
|
||||||
"workspace_name": "工作區名稱",
|
"workspace_name": "工作區名稱",
|
||||||
"workspace_name_placeholder": "例如:Formbricks",
|
"workspace_name_placeholder": "例如:Formbricks",
|
||||||
"workspace_not_found": "找不到工作區",
|
|
||||||
"workspace_permission_not_found": "找不到工作區權限",
|
|
||||||
"workspaces": "工作區",
|
"workspaces": "工作區",
|
||||||
"years": "年",
|
"years": "年",
|
||||||
"you": "您",
|
"you": "您",
|
||||||
@@ -659,7 +663,6 @@
|
|||||||
"attributes_msg_new_attribute_created": "已建立新屬性「{key}」,型別為「{dataType}」",
|
"attributes_msg_new_attribute_created": "已建立新屬性「{key}」,型別為「{dataType}」",
|
||||||
"attributes_msg_userid_already_exists": "此環境已存在該使用者 ID,未進行更新。",
|
"attributes_msg_userid_already_exists": "此環境已存在該使用者 ID,未進行更新。",
|
||||||
"contact_deleted_successfully": "聯絡人已成功刪除",
|
"contact_deleted_successfully": "聯絡人已成功刪除",
|
||||||
"contact_not_found": "找不到此聯絡人",
|
|
||||||
"contacts_table_refresh": "重新整理聯絡人",
|
"contacts_table_refresh": "重新整理聯絡人",
|
||||||
"contacts_table_refresh_success": "聯絡人已成功重新整理",
|
"contacts_table_refresh_success": "聯絡人已成功重新整理",
|
||||||
"create_attribute": "建立屬性",
|
"create_attribute": "建立屬性",
|
||||||
@@ -850,9 +853,16 @@
|
|||||||
"created_by_third_party": "由第三方建立",
|
"created_by_third_party": "由第三方建立",
|
||||||
"discord_webhook_not_supported": "目前不支援 Discord webhooks。",
|
"discord_webhook_not_supported": "目前不支援 Discord webhooks。",
|
||||||
"empty_webhook_message": "您的 Webhook 將在您新增後立即顯示在此處。⏲️",
|
"empty_webhook_message": "您的 Webhook 將在您新增後立即顯示在此處。⏲️",
|
||||||
|
"endpoint_bad_gateway_error": "錯誤的閘道 (502):代理/閘道錯誤,服務無法連線",
|
||||||
|
"endpoint_gateway_timeout_error": "閘道逾時 (504):閘道逾時,服務無法連線",
|
||||||
|
"endpoint_internal_server_error": "內部伺服器錯誤 (500):服務遇到了未預期的錯誤",
|
||||||
|
"endpoint_method_not_allowed_error": "不允許的方法 (405):該端點存在,但不接受 POST 請求",
|
||||||
|
"endpoint_not_found_error": "找不到 (404):該端點不存在",
|
||||||
"endpoint_pinged": "耶!我們能夠 ping Webhook!",
|
"endpoint_pinged": "耶!我們能夠 ping Webhook!",
|
||||||
"endpoint_pinged_error": "無法 ping Webhook!",
|
"endpoint_pinged_error": "無法 ping Webhook!",
|
||||||
|
"endpoint_service_unavailable_error": "服務無法使用 (503):服務暫時無法使用",
|
||||||
"learn_to_verify": "了解如何驗證 webhook 簽章",
|
"learn_to_verify": "了解如何驗證 webhook 簽章",
|
||||||
|
"no_triggers": "無觸發條件",
|
||||||
"please_check_console": "請檢查主控台以取得更多詳細資料",
|
"please_check_console": "請檢查主控台以取得更多詳細資料",
|
||||||
"please_enter_a_url": "請輸入網址",
|
"please_enter_a_url": "請輸入網址",
|
||||||
"response_created": "已建立回應",
|
"response_created": "已建立回應",
|
||||||
@@ -1071,6 +1081,25 @@
|
|||||||
"enterprise_features": "企業版功能",
|
"enterprise_features": "企業版功能",
|
||||||
"get_an_enterprise_license_to_get_access_to_all_features": "取得企業授權以存取所有功能。",
|
"get_an_enterprise_license_to_get_access_to_all_features": "取得企業授權以存取所有功能。",
|
||||||
"keep_full_control_over_your_data_privacy_and_security": "完全掌控您的資料隱私權和安全性。",
|
"keep_full_control_over_your_data_privacy_and_security": "完全掌控您的資料隱私權和安全性。",
|
||||||
|
"license_feature_access_control": "存取控制 (RBAC)",
|
||||||
|
"license_feature_audit_logs": "稽核日誌",
|
||||||
|
"license_feature_contacts": "聯絡人與區隔",
|
||||||
|
"license_feature_projects": "工作區",
|
||||||
|
"license_feature_quotas": "配額",
|
||||||
|
"license_feature_remove_branding": "移除品牌標識",
|
||||||
|
"license_feature_saml": "SAML SSO",
|
||||||
|
"license_feature_spam_protection": "垃圾訊息防護",
|
||||||
|
"license_feature_sso": "OIDC SSO",
|
||||||
|
"license_feature_two_factor_auth": "雙重驗證",
|
||||||
|
"license_feature_whitelabel": "白標電子郵件",
|
||||||
|
"license_features_table_access": "存取權限",
|
||||||
|
"license_features_table_description": "此執行個體目前可使用的企業功能與限制。",
|
||||||
|
"license_features_table_disabled": "已停用",
|
||||||
|
"license_features_table_enabled": "已啟用",
|
||||||
|
"license_features_table_feature": "功能",
|
||||||
|
"license_features_table_title": "授權功能",
|
||||||
|
"license_features_table_unlimited": "無限制",
|
||||||
|
"license_features_table_value": "值",
|
||||||
"license_instance_mismatch_description": "此授權目前綁定至不同的 Formbricks 執行個體。如果此安裝已重建或移動,請聯繫 Formbricks 支援以解除先前執行個體的綁定。",
|
"license_instance_mismatch_description": "此授權目前綁定至不同的 Formbricks 執行個體。如果此安裝已重建或移動,請聯繫 Formbricks 支援以解除先前執行個體的綁定。",
|
||||||
"license_invalid_description": "你在 ENTERPRISE_LICENSE_KEY 環境變數中填寫的授權金鑰無效。請檢查是否有輸入錯誤,或申請新的金鑰。",
|
"license_invalid_description": "你在 ENTERPRISE_LICENSE_KEY 環境變數中填寫的授權金鑰無效。請檢查是否有輸入錯誤,或申請新的金鑰。",
|
||||||
"license_status": "授權狀態",
|
"license_status": "授權狀態",
|
||||||
@@ -1392,7 +1421,6 @@
|
|||||||
"custom_hostname": "自訂主機名稱",
|
"custom_hostname": "自訂主機名稱",
|
||||||
"customize_survey_logo": "自訂問卷標誌",
|
"customize_survey_logo": "自訂問卷標誌",
|
||||||
"darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。",
|
"darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。",
|
||||||
"date_format": "日期格式",
|
|
||||||
"days_before_showing_this_survey_again": "距離上次顯示問卷後,需間隔指定天數才能再次顯示此問卷。",
|
"days_before_showing_this_survey_again": "距離上次顯示問卷後,需間隔指定天數才能再次顯示此問卷。",
|
||||||
"delete_anyways": "仍要刪除",
|
"delete_anyways": "仍要刪除",
|
||||||
"delete_block": "刪除區塊",
|
"delete_block": "刪除區塊",
|
||||||
@@ -1430,6 +1458,7 @@
|
|||||||
"error_saving_changes": "儲存變更時發生錯誤",
|
"error_saving_changes": "儲存變更時發生錯誤",
|
||||||
"even_after_they_submitted_a_response_e_g_feedback_box": "允許多次回應;即使已提交回應仍繼續顯示(例如:意見回饋框)。",
|
"even_after_they_submitted_a_response_e_g_feedback_box": "允許多次回應;即使已提交回應仍繼續顯示(例如:意見回饋框)。",
|
||||||
"everyone": "所有人",
|
"everyone": "所有人",
|
||||||
|
"expand_preview": "展開預覽",
|
||||||
"external_urls_paywall_tooltip": "請升級至付費方案以自訂外部連結。這有助我們防止網路釣魚。",
|
"external_urls_paywall_tooltip": "請升級至付費方案以自訂外部連結。這有助我們防止網路釣魚。",
|
||||||
"fallback_missing": "遺失的回退",
|
"fallback_missing": "遺失的回退",
|
||||||
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'fieldId'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
"fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'fieldId'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||||
@@ -1655,6 +1684,8 @@
|
|||||||
"response_limit_needs_to_exceed_number_of_received_responses": "回應限制必須超過收到的回應數 ('{'responseCount'}')。",
|
"response_limit_needs_to_exceed_number_of_received_responses": "回應限制必須超過收到的回應數 ('{'responseCount'}')。",
|
||||||
"response_limits_redirections_and_more": "回應限制、重新導向等。",
|
"response_limits_redirections_and_more": "回應限制、重新導向等。",
|
||||||
"response_options": "回應選項",
|
"response_options": "回應選項",
|
||||||
|
"reverse_order_occasionally": "偶爾反轉順序",
|
||||||
|
"reverse_order_occasionally_except_last": "偶爾反轉順序(最後一項除外)",
|
||||||
"roundness": "圓角",
|
"roundness": "圓角",
|
||||||
"roundness_description": "調整邊角的圓潤程度。",
|
"roundness_description": "調整邊角的圓潤程度。",
|
||||||
"row_used_in_logic_error": "此 row 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
"row_used_in_logic_error": "此 row 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||||
@@ -1683,6 +1714,7 @@
|
|||||||
"show_survey_maximum_of": "最多顯示問卷",
|
"show_survey_maximum_of": "最多顯示問卷",
|
||||||
"show_survey_to_users": "將問卷顯示給 % 的使用者",
|
"show_survey_to_users": "將問卷顯示給 % 的使用者",
|
||||||
"show_to_x_percentage_of_targeted_users": "顯示給 '{'percentage'}'% 的目標使用者",
|
"show_to_x_percentage_of_targeted_users": "顯示給 '{'percentage'}'% 的目標使用者",
|
||||||
|
"shrink_preview": "收合預覽",
|
||||||
"simple": "簡單",
|
"simple": "簡單",
|
||||||
"six_points": "6 分",
|
"six_points": "6 分",
|
||||||
"smiley": "表情符號",
|
"smiley": "表情符號",
|
||||||
@@ -1698,10 +1730,12 @@
|
|||||||
"styling_set_to_theme_styles": "樣式設定為主題樣式",
|
"styling_set_to_theme_styles": "樣式設定為主題樣式",
|
||||||
"subheading": "副標題",
|
"subheading": "副標題",
|
||||||
"subtract": "減 -",
|
"subtract": "減 -",
|
||||||
|
"survey_closed_message_heading_required": "請為自訂的問卷關閉訊息新增標題。",
|
||||||
"survey_completed_heading": "問卷已完成",
|
"survey_completed_heading": "問卷已完成",
|
||||||
"survey_completed_subheading": "此免費且開源的問卷已關閉",
|
"survey_completed_subheading": "此免費且開源的問卷已關閉",
|
||||||
"survey_display_settings": "問卷顯示設定",
|
"survey_display_settings": "問卷顯示設定",
|
||||||
"survey_placement": "問卷位置",
|
"survey_placement": "問卷位置",
|
||||||
|
"survey_preview": "問卷預覽 👀",
|
||||||
"survey_styling": "表單樣式設定",
|
"survey_styling": "表單樣式設定",
|
||||||
"survey_trigger": "問卷觸發器",
|
"survey_trigger": "問卷觸發器",
|
||||||
"switch_multi_language_on_to_get_started": "請開啟多語言功能以開始使用 👉",
|
"switch_multi_language_on_to_get_started": "請開啟多語言功能以開始使用 👉",
|
||||||
@@ -3052,7 +3086,7 @@
|
|||||||
"preview_survey_question_2_choice_2_label": "不用了,謝謝!",
|
"preview_survey_question_2_choice_2_label": "不用了,謝謝!",
|
||||||
"preview_survey_question_2_headline": "想要緊跟最新動態嗎?",
|
"preview_survey_question_2_headline": "想要緊跟最新動態嗎?",
|
||||||
"preview_survey_question_2_subheader": "這是一個範例說明。",
|
"preview_survey_question_2_subheader": "這是一個範例說明。",
|
||||||
"preview_survey_question_open_text_headline": "還有什麼想和我們分享的嗎?",
|
"preview_survey_question_open_text_headline": "還有其他想分享的嗎?",
|
||||||
"preview_survey_question_open_text_placeholder": "在此輸入您的答案...",
|
"preview_survey_question_open_text_placeholder": "在此輸入您的答案...",
|
||||||
"preview_survey_question_open_text_subheader": "您的回饋能幫助我們進步。",
|
"preview_survey_question_open_text_subheader": "您的回饋能幫助我們進步。",
|
||||||
"preview_survey_welcome_card_headline": "歡迎!",
|
"preview_survey_welcome_card_headline": "歡迎!",
|
||||||
@@ -3307,7 +3341,7 @@
|
|||||||
"workflows": {
|
"workflows": {
|
||||||
"coming_soon_description": "感謝你和我們分享你的工作流程想法!我們目前正在設計這個功能,你的回饋將幫助我們打造真正符合你需求的工具。",
|
"coming_soon_description": "感謝你和我們分享你的工作流程想法!我們目前正在設計這個功能,你的回饋將幫助我們打造真正符合你需求的工具。",
|
||||||
"coming_soon_title": "快完成囉!",
|
"coming_soon_title": "快完成囉!",
|
||||||
"follow_up_label": "還有什麼想補充的嗎?",
|
"follow_up_label": "還有其他想補充的嗎?",
|
||||||
"follow_up_placeholder": "您希望自動化哪些具體任務?有沒有想要整合的工具或功能?",
|
"follow_up_placeholder": "您希望自動化哪些具體任務?有沒有想要整合的工具或功能?",
|
||||||
"generate_button": "產生工作流程",
|
"generate_button": "產生工作流程",
|
||||||
"heading": "你想建立什麼樣的工作流程?",
|
"heading": "你想建立什麼樣的工作流程?",
|
||||||
|
|||||||
+6
-1
@@ -1,4 +1,5 @@
|
|||||||
import { Languages } from "lucide-react";
|
import { Languages } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
@@ -18,6 +19,7 @@ interface LanguageDropdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdownProps) => {
|
export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdownProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const enabledLanguages = getEnabledLanguages(survey.languages ?? []);
|
const enabledLanguages = getEnabledLanguages(survey.languages ?? []);
|
||||||
|
|
||||||
if (enabledLanguages.length <= 1) {
|
if (enabledLanguages.length <= 1) {
|
||||||
@@ -27,7 +29,10 @@ export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdo
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="secondary" title="Select Language" aria-label="Select Language">
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
title={t("common.select_language")}
|
||||||
|
aria-label={t("common.select_language")}>
|
||||||
<Languages className="h-5 w-5" />
|
<Languages className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { deleteResponse, getResponse } from "@/lib/response/service";
|
import { deleteResponse, getResponse } from "@/lib/response/service";
|
||||||
import { createTag } from "@/lib/tag/service";
|
import { createTag } from "@/lib/tag/service";
|
||||||
import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service";
|
import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service";
|
||||||
@@ -68,7 +69,7 @@ export const createTagToResponseAction = authenticatedActionClient
|
|||||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||||
|
|
||||||
if (!responseEnvironmentId || !tagEnvironment) {
|
if (!responseEnvironmentId || !tagEnvironment) {
|
||||||
throw new Error("Environment not found");
|
throw new ResourceNotFoundError("Environment", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||||
@@ -113,7 +114,7 @@ export const deleteTagOnResponseAction = authenticatedActionClient
|
|||||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||||
if (!responseEnvironmentId || !tagEnvironment) {
|
if (!responseEnvironmentId || !tagEnvironment) {
|
||||||
throw new Error("Environment not found");
|
throw new ResourceNotFoundError("Environment", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { TResponseData } from "@formbricks/types/responses";
|
import { TResponseData } from "@formbricks/types/responses";
|
||||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { getSurveyDateFormatMap } from "@/lib/utils/date-display";
|
||||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ interface ElementSkipProps {
|
|||||||
elements: TSurveyElement[];
|
elements: TSurveyElement[];
|
||||||
isFirstElementAnswered?: boolean;
|
isFirstElementAnswered?: boolean;
|
||||||
responseData: TResponseData;
|
responseData: TResponseData;
|
||||||
|
locale: TUserLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ElementSkip = ({
|
export const ElementSkip = ({
|
||||||
@@ -23,8 +26,10 @@ export const ElementSkip = ({
|
|||||||
elements,
|
elements,
|
||||||
isFirstElementAnswered,
|
isFirstElementAnswered,
|
||||||
responseData,
|
responseData,
|
||||||
|
locale,
|
||||||
}: ElementSkipProps) => {
|
}: ElementSkipProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dateFormats = getSurveyDateFormatMap(elements);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{skippedElements && (
|
{skippedElements && (
|
||||||
@@ -81,7 +86,11 @@ export const ElementSkip = ({
|
|||||||
},
|
},
|
||||||
"default"
|
"default"
|
||||||
),
|
),
|
||||||
responseData
|
responseData,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
locale,
|
||||||
|
dateFormats
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,7 +129,11 @@ export const ElementSkip = ({
|
|||||||
},
|
},
|
||||||
"default"
|
"default"
|
||||||
),
|
),
|
||||||
responseData
|
responseData,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
locale,
|
||||||
|
dateFormats
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+6
-4
@@ -3,11 +3,12 @@ import React from "react";
|
|||||||
import { TResponseDataValue } from "@formbricks/types/responses";
|
import { TResponseDataValue } from "@formbricks/types/responses";
|
||||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||||
import { processResponseData } from "@/lib/responses";
|
import { processResponseData } from "@/lib/responses";
|
||||||
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
import { formatStoredDateForDisplay } from "@/lib/utils/date-display";
|
||||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||||
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
|
import { FileUploadResponse } from "@/modules/ui/components/file-upload-response";
|
||||||
@@ -21,6 +22,7 @@ interface RenderResponseProps {
|
|||||||
element: TSurveyElement;
|
element: TSurveyElement;
|
||||||
survey: TSurvey;
|
survey: TSurvey;
|
||||||
language: string | null;
|
language: string | null;
|
||||||
|
locale: TUserLocale;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
showId: boolean;
|
showId: boolean;
|
||||||
}
|
}
|
||||||
@@ -30,6 +32,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
|||||||
element,
|
element,
|
||||||
survey,
|
survey,
|
||||||
language,
|
language,
|
||||||
|
locale,
|
||||||
isExpanded = true,
|
isExpanded = true,
|
||||||
showId,
|
showId,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -63,9 +66,8 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
|||||||
break;
|
break;
|
||||||
case TSurveyElementTypeEnum.Date:
|
case TSurveyElementTypeEnum.Date:
|
||||||
if (typeof responseData === "string") {
|
if (typeof responseData === "string") {
|
||||||
const parsedDate = new Date(responseData);
|
const formattedDate =
|
||||||
|
formatStoredDateForDisplay(responseData, element.format, locale) ?? responseData;
|
||||||
const formattedDate = isNaN(parsedDate.getTime()) ? responseData : formatDateWithOrdinal(parsedDate);
|
|
||||||
|
|
||||||
return <p className="ph-no-capture my-1 truncate font-normal text-slate-700">{formattedDate}</p>;
|
return <p className="ph-no-capture my-1 truncate font-normal text-slate-700">{formattedDate}</p>;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -6,7 +6,9 @@ import { TResponseWithQuotas } from "@formbricks/types/responses";
|
|||||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { getSurveyDateFormatMap } from "@/lib/utils/date-display";
|
||||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||||
import { ResponseCardQuotas } from "@/modules/ee/quotas/components/single-response-card-quotas";
|
import { ResponseCardQuotas } from "@/modules/ee/quotas/components/single-response-card-quotas";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
@@ -21,14 +23,17 @@ interface SingleResponseCardBodyProps {
|
|||||||
survey: TSurvey;
|
survey: TSurvey;
|
||||||
response: TResponseWithQuotas;
|
response: TResponseWithQuotas;
|
||||||
skippedQuestions: string[][];
|
skippedQuestions: string[][];
|
||||||
|
locale: TUserLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SingleResponseCardBody = ({
|
export const SingleResponseCardBody = ({
|
||||||
survey,
|
survey,
|
||||||
response,
|
response,
|
||||||
skippedQuestions,
|
skippedQuestions,
|
||||||
|
locale,
|
||||||
}: SingleResponseCardBodyProps) => {
|
}: SingleResponseCardBodyProps) => {
|
||||||
const elements = getElementsFromBlocks(survey.blocks);
|
const elements = getElementsFromBlocks(survey.blocks);
|
||||||
|
const dateFormats = getSurveyDateFormatMap(elements);
|
||||||
const isFirstElementAnswered = elements[0] ? !!response.data[elements[0].id] : false;
|
const isFirstElementAnswered = elements[0] ? !!response.data[elements[0].id] : false;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formatTextWithSlashes = (text: string) => {
|
const formatTextWithSlashes = (text: string) => {
|
||||||
@@ -61,6 +66,7 @@ export const SingleResponseCardBody = ({
|
|||||||
status={"welcomeCard"}
|
status={"welcomeCard"}
|
||||||
isFirstElementAnswered={isFirstElementAnswered}
|
isFirstElementAnswered={isFirstElementAnswered}
|
||||||
responseData={response.data}
|
responseData={response.data}
|
||||||
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -98,7 +104,9 @@ export const SingleResponseCardBody = ({
|
|||||||
getLocalizedValue(question.headline, "default"),
|
getLocalizedValue(question.headline, "default"),
|
||||||
response.data,
|
response.data,
|
||||||
response.variables,
|
response.variables,
|
||||||
true
|
true,
|
||||||
|
locale,
|
||||||
|
dateFormats
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -109,6 +117,7 @@ export const SingleResponseCardBody = ({
|
|||||||
survey={survey}
|
survey={survey}
|
||||||
responseData={response.data[question.id]}
|
responseData={response.data[question.id]}
|
||||||
language={response.language}
|
language={response.language}
|
||||||
|
locale={locale}
|
||||||
showId={true}
|
showId={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +127,7 @@ export const SingleResponseCardBody = ({
|
|||||||
skippedElements={skipped}
|
skippedElements={skipped}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
responseData={response.data}
|
responseData={response.data}
|
||||||
|
locale={locale}
|
||||||
status={
|
status={
|
||||||
response.finished ||
|
response.finished ||
|
||||||
(skippedQuestions.length > 0 &&
|
(skippedQuestions.length > 0 &&
|
||||||
|
|||||||
@@ -137,7 +137,12 @@ export const SingleResponseCard = ({
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SingleResponseCardBody survey={survey} response={response} skippedQuestions={skippedQuestions} />
|
<SingleResponseCardBody
|
||||||
|
survey={survey}
|
||||||
|
response={response}
|
||||||
|
skippedQuestions={skippedQuestions}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
|
||||||
<ResponseTagsWrapper
|
<ResponseTagsWrapper
|
||||||
key={response.id}
|
key={response.id}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user