Compare commits

..

2 Commits

Author SHA1 Message Date
Cursor Agent 520c337748 test: add unit tests for safeFormRequestSubmit utility
- Test requestSubmit method when available
- Test fallback behavior for iOS Safari 15.5
- Test validation failure prevents form submission
- Ensures proper event dispatching with correct properties
2026-01-21 02:54:03 +00:00
Cursor Agent 7f5c93b629 fix: add iOS Safari 15.5 compatibility for form.requestSubmit()
- Created safeFormRequestSubmit utility function with fallback for browsers
  that don't support requestSubmit() method (iOS Safari < 16.0)
- Updated block-conditional.tsx to use safe form submission
- Updated login-form.tsx to use safe form submission
- Fallback uses reportValidity() for validation UI and dispatches submit event

Fixes TypeError: n.requestSubmit is not a function on iOS Safari 15.5
2026-01-21 02:53:04 +00:00
414 changed files with 9213 additions and 24614 deletions
+2 -7
View File
@@ -184,13 +184,8 @@ ENTERPRISE_LICENSE_KEY=
# Ignore Rate Limiting across the Formbricks app
# RATE_LIMITING_DISABLED=1
# OpenTelemetry OTLP endpoint (base URL, exporters append /v1/traces and /v1/metrics)
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# OTEL_SERVICE_NAME=formbricks
# OTEL_RESOURCE_ATTRIBUTES=deployment.environment=development
# OTEL_TRACES_SAMPLER=parentbased_traceidratio
# OTEL_TRACES_SAMPLER_ARG=1
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
# Unsplash API Key
UNSPLASH_ACCESS_KEY=
+10 -9
View File
@@ -32,20 +32,21 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js 22.x
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 22.x
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup pnpm
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
with:
version: 9.15.9
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
run: pnpm install --frozen-lockfile
- name: Validate translation keys
run: |
-3
View File
@@ -13,7 +13,6 @@
**/.next/
**/out/
**/build
**/next-env.d.ts
# node
**/dist/
@@ -64,5 +63,3 @@ packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdat
.cursorrules
i18n.cache
stats.html
# next-agents-md
.next-docs/
-15
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
"@tailwindcss/vite": "4.1.18",
"@typescript-eslint/parser": "8.53.0",
"@vitejs/plugin-react": "5.1.2",
"esbuild": "0.25.12",
"esbuild": "0.27.2",
"eslint-plugin-react-refresh": "0.4.26",
"eslint-plugin-storybook": "10.1.11",
"prop-types": "15.8.1",
+16
View File
@@ -1,4 +1,20 @@
module.exports = {
extends: ["@formbricks/eslint-config/legacy-next.js"],
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
overrides: [
{
files: ["locales/*.json"],
plugins: ["i18n-json"],
rules: {
"i18n-json/identical-keys": [
"error",
{
filePath: require("path").join(__dirname, "locales", "en-US.json"),
checkExtraKeys: false,
checkMissingKeys: true,
},
],
},
},
],
};
+18 -13
View File
@@ -1,4 +1,4 @@
FROM node:24-alpine3.23 AS base
FROM node:22-alpine3.22 AS base
#
## step 1: Prune monorepo
@@ -20,7 +20,7 @@ FROM base AS installer
# Enable corepack and prepare pnpm
RUN npm install --ignore-scripts -g corepack@latest
RUN corepack enable
RUN corepack prepare pnpm@10.28.2 --activate
RUN corepack prepare pnpm@9.15.9 --activate
# Install necessary build tools and compilers
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
@@ -69,14 +69,20 @@ RUN --mount=type=secret,id=database_url \
--mount=type=secret,id=sentry_auth_token \
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
# Extract Prisma version
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
#
## step 3: setup production runner
#
FROM base AS runner
# Update npm to latest, then create user
# Note: npm's bundled tar has a known vulnerability but npm is only used during build, not at runtime
RUN npm install --ignore-scripts -g npm@latest \
RUN npm install --ignore-scripts -g corepack@latest && \
corepack enable
RUN apk add --no-cache curl \
&& apk add --no-cache supercronic \
# && addgroup --system --gid 1001 nodejs \
&& addgroup -S nextjs \
&& adduser -S -u 1001 -G nextjs nextjs
@@ -107,28 +113,25 @@ RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./package
COPY --from=installer /app/packages/database/dist ./packages/database/dist
RUN chown -R nextjs:nextjs ./packages/database/dist && chmod -R 755 ./packages/database/dist
# Copy prisma client packages
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
COPY --from=installer /prisma_version.txt .
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
COPY --from=installer /app/node_modules/uuid ./node_modules/uuid
RUN chmod -R 755 ./node_modules/uuid
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
RUN chmod -R 755 ./node_modules/@noble/hashes
COPY --from=installer /app/node_modules/zod ./node_modules/zod
RUN chmod -R 755 ./node_modules/zod
# Install prisma CLI globally for database migrations and fix permissions for nextjs user
RUN npm install --ignore-scripts -g prisma@6 \
&& chown -R nextjs:nextjs /usr/local/lib/node_modules/prisma
RUN npm install -g prisma@6
# Create a startup script to handle the conditional logic
COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh
@@ -138,8 +141,10 @@ EXPOSE 3000
ENV HOSTNAME="0.0.0.0"
USER nextjs
# Prepare pnpm as the nextjs user to ensure it's available at runtime
# Prepare volumes for uploads and SAML connections
RUN mkdir -p /home/nextjs/apps/web/uploads/ && \
RUN corepack prepare pnpm@9.15.9 --activate && \
mkdir -p /home/nextjs/apps/web/uploads/ && \
mkdir -p /home/nextjs/apps/web/saml-connection
VOLUME /home/nextjs/apps/web/uploads/
@@ -25,7 +25,7 @@ const mockProject: TProject = {
},
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
languages: [],
logo: null,
@@ -3,7 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -17,7 +17,6 @@ import {
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { previewSurvey } from "@/app/lib/templates";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
import { buildStylingFromBrandColor } from "@/lib/styling/constants";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
@@ -65,17 +64,10 @@ export const ProjectSettings = ({
const { t } = useTranslation();
const addProject = async (data: TProjectUpdateInput) => {
try {
// Build the full styling from the chosen brand color so all derived
// colours (question, button, input, option, progress, etc.) are persisted.
// Without this, only brandColor is saved and the look-and-feel page falls
// back to STYLE_DEFAULTS computed from the default brand (#64748b).
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
const createProjectResponse = await createProjectAction({
organizationId,
data: {
...data,
styling: fullStyling,
config: { channel, industry },
teamIds: data.teamIds,
},
@@ -120,7 +112,6 @@ export const ProjectSettings = ({
const projectName = form.watch("name");
const logoUrl = form.watch("logo.url");
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
const { isSubmitting } = form.formState;
const organizationTeamsOptions = organizationTeams.map((team) => ({
@@ -235,7 +226,7 @@ export const ProjectSettings = ({
alt="Logo"
width={256}
height={56}
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/>
)}
<p className="text-sm text-slate-400">{t("common.preview")}</p>
@@ -244,7 +235,7 @@ export const ProjectSettings = ({
appUrl={publicDomain}
isPreviewMode={true}
survey={previewSurvey(projectName || "my Product", t)}
styling={previewStyling}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}
languageCode="default"
onFileUpload={async (file) => file.name}
@@ -2,7 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { ZProjectUpdateInput } from "@formbricks/types/project";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganization } from "@/lib/organization/service";
@@ -138,7 +138,7 @@ export const getProjectsForSwitcherAction = authenticatedActionClient
// Need membership for getProjectsByUserId (1 DB query)
const membership = await getMembershipByUserIdOrganizationId(ctx.user.id, parsedInput.organizationId);
if (!membership) {
throw new AuthorizationError("Membership not found");
throw new Error("Membership not found");
}
return await getProjectsByUserId(ctx.user.id, membership);
@@ -36,7 +36,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
// Calculate derived values (no queries)
const { isMember, isOwner, isManager } = getAccessFlags(membership.role);
const { features, lastChecked, isPendingDowngrade, active, status } = license;
const { features, lastChecked, isPendingDowngrade, active } = license;
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.billing.limits);
const isOwnerOrManager = isOwner || isManager;
@@ -63,7 +63,6 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
active={active}
environmentId={environment.id}
locale={user.locale}
status={status}
/>
<div className="flex h-full">
@@ -1,138 +0,0 @@
"use client";
import { TFunction } from "i18next";
import { RotateCcwIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { recheckLicenseAction } from "@/modules/ee/license-check/actions";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { SettingsCard } from "../../../components/SettingsCard";
type LicenseStatus = "active" | "expired" | "unreachable" | "invalid_license";
interface EnterpriseLicenseStatusProps {
status: LicenseStatus;
gracePeriodEnd?: Date;
environmentId: string;
}
const getBadgeConfig = (
status: LicenseStatus,
t: TFunction
): { type: "success" | "error" | "warning" | "gray"; label: string } => {
switch (status) {
case "active":
return { type: "success", label: t("environments.settings.enterprise.license_status_active") };
case "expired":
return { type: "error", label: t("environments.settings.enterprise.license_status_expired") };
case "unreachable":
return { type: "warning", label: t("environments.settings.enterprise.license_status_unreachable") };
case "invalid_license":
return { type: "error", label: t("environments.settings.enterprise.license_status_invalid") };
default:
return { type: "gray", label: t("environments.settings.enterprise.license_status") };
}
};
export const EnterpriseLicenseStatus = ({ status, gracePeriodEnd, environmentId }: EnterpriseLicenseStatusProps) => {
const { t } = useTranslation();
const router = useRouter();
const [isRechecking, setIsRechecking] = useState(false);
const handleRecheck = async () => {
setIsRechecking(true);
try {
const result = await recheckLicenseAction({ environmentId });
if (result?.serverError) {
toast.error(result.serverError || t("environments.settings.enterprise.recheck_license_failed"));
return;
}
if (result?.data) {
if (result.data.status === "unreachable") {
toast.error(t("environments.settings.enterprise.recheck_license_unreachable"));
} else if (result.data.status === "invalid_license") {
toast.error(t("environments.settings.enterprise.recheck_license_invalid"));
} else {
toast.success(t("environments.settings.enterprise.recheck_license_success"));
}
router.refresh();
} else {
toast.error(t("environments.settings.enterprise.recheck_license_failed"));
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : t("environments.settings.enterprise.recheck_license_failed")
);
} finally {
setIsRechecking(false);
}
};
const badgeConfig = getBadgeConfig(status, t);
return (
<SettingsCard
title={t("environments.settings.enterprise.license_status")}
description={t("environments.settings.enterprise.license_status_description")}>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-3">
<div className="flex flex-col gap-1.5">
<Badge type={badgeConfig.type} text={badgeConfig.label} size="normal" className="w-fit" />
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRecheck}
disabled={isRechecking}
className="shrink-0">
{isRechecking ? (
<>
<RotateCcwIcon className="mr-2 h-4 w-4 animate-spin" />
{t("environments.settings.enterprise.rechecking")}
</>
) : (
<>
<RotateCcwIcon className="mr-2 h-4 w-4" />
{t("environments.settings.enterprise.recheck_license")}
</>
)}
</Button>
</div>
{status === "unreachable" && gracePeriodEnd && (
<Alert variant="warning" size="small">
<AlertDescription className="overflow-visible whitespace-normal">
{t("environments.settings.enterprise.license_unreachable_grace_period", {
gracePeriodEnd: new Date(gracePeriodEnd).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
}),
})}
</AlertDescription>
</Alert>
)}
{status === "invalid_license" && (
<Alert variant="error" size="small">
<AlertDescription className="overflow-visible whitespace-normal">
{t("environments.settings.enterprise.license_invalid_description")}
</AlertDescription>
</Alert>
)}
<p className="border-t border-slate-100 pt-4 text-sm text-slate-500">
{t("environments.settings.enterprise.questions_please_reach_out_to")}{" "}
<a
className="font-medium text-slate-700 underline hover:text-slate-900"
href="mailto:hola@formbricks.com">
hola@formbricks.com
</a>
</p>
</div>
</SettingsCard>
);
};
@@ -2,10 +2,9 @@ import { CheckIcon } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { EnterpriseLicenseStatus } from "@/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/components/EnterpriseLicenseStatus";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { GRACE_PERIOD_MS, getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -26,8 +25,7 @@ const Page = async (props) => {
return notFound();
}
const licenseState = await getEnterpriseLicense();
const hasLicense = licenseState.status !== "no-license";
const { active: isEnterpriseEdition } = await getEnterpriseLicense();
const paidFeatures = [
{
@@ -92,22 +90,35 @@ const Page = async (props) => {
activeId="enterprise"
/>
</PageHeader>
{hasLicense ? (
<EnterpriseLicenseStatus
status={licenseState.status as "active" | "expired" | "unreachable" | "invalid_license"}
gracePeriodEnd={
licenseState.status === "unreachable"
? new Date(licenseState.lastChecked.getTime() + GRACE_PERIOD_MS)
: undefined
}
environmentId={params.environmentId}
/>
{isEnterpriseEdition ? (
<div>
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
<div className="space-y-4 p-8">
<div className="flex items-center gap-x-2">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-400" />
</div>
<p className="text-slate-800">
{t(
"environments.settings.enterprise.your_enterprise_license_is_active_all_features_unlocked"
)}
</p>
</div>
<p className="text-sm text-slate-500">
{t("environments.settings.enterprise.questions_please_reach_out_to")}{" "}
<a className="font-semibold underline" href="mailto:hola@formbricks.com">
hola@formbricks.com
</a>
</p>
</div>
</div>
</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">
<svg
viewBox="0 0 1024 1024"
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
aria-hidden="true">
<circle
cx={512}
@@ -142,8 +153,8 @@ const Page = async (props) => {
{t("environments.settings.enterprise.enterprise_features")}
</h2>
<ul className="my-4 space-y-4">
{paidFeatures.map((feature) => (
<li key={feature.title} className="flex items-center">
{paidFeatures.map((feature, index) => (
<li key={index} className="flex items-center">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-400" />
</div>
@@ -7,7 +7,6 @@ import { useTranslation } from "react-i18next";
import { TOrganization } from "@formbricks/types/organizations";
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -33,12 +32,7 @@ export const DeleteOrganization = ({
setIsDeleting(true);
try {
const result = await deleteOrganizationAction({ organizationId: organization.id });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
setIsDeleting(false);
return;
}
await deleteOrganizationAction({ organizationId: organization.id });
toast.success(t("environments.settings.general.organization_deleted_successfully"));
if (typeof localStorage !== "undefined") {
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
@@ -316,14 +316,6 @@ export const generateResponseTableColumns = (
},
};
const responseIdColumn: ColumnDef<TResponseTableData> = {
accessorKey: "responseId",
header: () => <div className="gap-x-1.5">{t("common.response_id")}</div>,
cell: ({ row }) => {
return <IdBadge id={row.original.responseId} />;
},
};
const quotasColumn: ColumnDef<TResponseTableData> = {
accessorKey: "quota",
header: t("common.quota"),
@@ -422,7 +414,6 @@ export const generateResponseTableColumns = (
const baseColumns = [
personColumn,
singleUseIdColumn,
responseIdColumn,
dateColumn,
...(showQuotasColumn ? [quotasColumn] : []),
statusColumn,
@@ -8,7 +8,7 @@ import { TSurvey, TSurveyElementSummaryFileUpload } from "@formbricks/types/surv
import { TUserLocale } from "@formbricks/types/user";
import { timeSince } from "@/lib/time";
import { getContactIdentifier } from "@/lib/utils/contact";
import { getOriginalFileNameFromUrl } from "@/modules/storage/url-helpers";
import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import { EmptyState } from "@/modules/ui/components/empty-state";
@@ -21,7 +21,6 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/BaseSelectDropdown";
import { fetchTables } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable";
import AirtableLogo from "@/images/airtableLogo.svg";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
@@ -269,14 +268,7 @@ export const AddIntegrationModal = ({
airtableIntegrationData.config?.data.push(integrationData);
}
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: airtableIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: airtableIntegrationData });
if (isEditMode) {
toast.success(t("environments.integrations.integration_updated_successfully"));
} else {
@@ -312,11 +304,7 @@ export const AddIntegrationModal = ({
const integrationData = structuredClone(airtableIntegrationData);
integrationData.config.data.splice(index, 1);
const result = await createOrUpdateIntegrationAction({ environmentId, integrationData });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData });
handleClose();
router.refresh();
@@ -165,14 +165,7 @@ export const AddIntegrationModal = ({
// create action
googleSheetIntegrationData.config.data.push(integrationData);
}
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: googleSheetIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
if (selectedIntegration) {
toast.success(t("environments.integrations.integration_updated_successfully"));
} else {
@@ -212,14 +205,7 @@ export const AddIntegrationModal = ({
googleSheetIntegrationData.config.data.splice(selectedIntegration!.index, 1);
try {
setIsDeleting(true);
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: googleSheetIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
toast.success(t("environments.integrations.integration_removed_successfully"));
setOpen(false);
} catch (error) {
@@ -280,7 +266,7 @@ export const AddIntegrationModal = ({
<div className="space-y-4">
<div>
<Label htmlFor="Surveys">{t("common.questions")}</Label>
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{surveyElements.map((question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">
@@ -22,7 +22,6 @@ import {
createEmptyMapping,
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow";
import NotionLogo from "@/images/notion.png";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { Button } from "@/modules/ui/components/button";
@@ -218,14 +217,7 @@ export const AddIntegrationModal = ({
notionIntegrationData.config.data.push(integrationData);
}
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: notionIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
if (selectedIntegration) {
toast.success(t("environments.integrations.integration_updated_successfully"));
} else {
@@ -244,14 +236,7 @@ export const AddIntegrationModal = ({
notionIntegrationData.config.data.splice(selectedIntegration!.index, 1);
try {
setIsDeleting(true);
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: notionIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
toast.success(t("environments.integrations.integration_removed_successfully"));
setOpen(false);
} catch (error) {
@@ -17,7 +17,6 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
import SlackLogo from "@/images/slacklogo.png";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
@@ -145,14 +144,7 @@ export const AddChannelMappingModal = ({
// create action
slackIntegrationData.config.data.push(integrationData);
}
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: slackIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
if (selectedIntegration) {
toast.success(t("environments.integrations.integration_updated_successfully"));
} else {
@@ -189,14 +181,7 @@ export const AddChannelMappingModal = ({
slackIntegrationData.config.data.splice(selectedIntegration!.index, 1);
try {
setIsDeleting(true);
const result = await createOrUpdateIntegrationAction({
environmentId,
integrationData: slackIntegrationData,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
toast.success(t("environments.integrations.integration_removed_successfully"));
setOpen(false);
} catch (error) {
@@ -64,7 +64,7 @@ const mockProject = {
linkSurveyBranding: true,
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
languages: [],
} as unknown as TProject;
@@ -1,314 +0,0 @@
import { Prisma } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getEnvironmentStateData } from "./data";
// Mock dependencies
vi.mock("@formbricks/database", () => ({
prisma: {
environment: {
findUnique: vi.fn(),
},
},
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
vi.mock("@/modules/survey/lib/utils", () => ({
transformPrismaSurvey: vi.fn((survey) => survey),
}));
const environmentId = "cjld2cjxh0000qzrmn831i7rn";
const mockEnvironmentData = {
id: environmentId,
type: "production",
appSetupCompleted: true,
project: {
id: "project-123",
recontactDays: 30,
clickOutsideClose: true,
overlay: "none",
placement: "bottomRight",
inAppSurveyBranding: true,
styling: { allowStyleOverwrite: false },
organization: {
id: "org-123",
billing: {
plan: "free",
limits: { monthly: { responses: 100 } },
},
},
},
actionClasses: [
{
id: "action-1",
type: "code",
name: "Test Action",
key: "test-action",
noCodeConfig: null,
},
],
surveys: [
{
id: "survey-1",
name: "Test Survey",
type: "app",
status: "inProgress",
welcomeCard: { enabled: false },
questions: [],
blocks: null,
variables: [],
showLanguageSwitch: false,
languages: [],
endings: [],
autoClose: null,
styling: null,
recaptcha: { enabled: false },
segment: null,
recontactDays: null,
displayLimit: null,
displayOption: "displayOnce",
hiddenFields: { enabled: false },
isBackButtonHidden: false,
triggers: [],
displayPercentage: null,
delay: 0,
projectOverwrites: null,
},
],
};
describe("getEnvironmentStateData", () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.resetAllMocks();
});
test("should return environment state data when environment exists", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockEnvironmentData as never);
const result = await getEnvironmentStateData(environmentId);
expect(result).toEqual({
environment: {
id: environmentId,
type: "production",
appSetupCompleted: true,
project: {
id: "project-123",
recontactDays: 30,
clickOutsideClose: true,
overlay: "none",
placement: "bottomRight",
inAppSurveyBranding: true,
styling: { allowStyleOverwrite: false },
},
},
organization: {
id: "org-123",
billing: {
plan: "free",
limits: { monthly: { responses: 100 } },
},
},
surveys: mockEnvironmentData.surveys,
actionClasses: mockEnvironmentData.actionClasses,
});
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
where: { id: environmentId },
select: expect.objectContaining({
id: true,
type: true,
appSetupCompleted: true,
project: expect.any(Object),
actionClasses: expect.any(Object),
surveys: expect.any(Object),
}),
});
});
test("should throw ResourceNotFoundError when environment is not found", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow("environment");
});
test("should throw ResourceNotFoundError when project is not found", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
project: null,
} as never);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
});
test("should throw ResourceNotFoundError when organization is not found", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
project: {
...mockEnvironmentData.project,
organization: null,
},
} as never);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
});
test("should throw DatabaseError on Prisma database errors", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Connection failed", {
code: "P2024",
clientVersion: "5.0.0",
});
vi.mocked(prisma.environment.findUnique).mockRejectedValue(prismaError);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(DatabaseError);
expect(logger.error).toHaveBeenCalled();
});
test("should rethrow unexpected errors", async () => {
const unexpectedError = new Error("Unexpected error");
vi.mocked(prisma.environment.findUnique).mockRejectedValue(unexpectedError);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow("Unexpected error");
expect(logger.error).toHaveBeenCalled();
});
test("should handle empty surveys array", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
surveys: [],
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.surveys).toEqual([]);
});
test("should handle empty actionClasses array", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
actionClasses: [],
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.actionClasses).toEqual([]);
});
test("should transform surveys using transformPrismaSurvey", async () => {
const multipleSurveys = [
...mockEnvironmentData.surveys,
{
...mockEnvironmentData.surveys[0],
id: "survey-2",
name: "Second Survey",
},
];
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
surveys: multipleSurveys,
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.surveys).toHaveLength(2);
});
test("should correctly map project properties to environment.project", async () => {
const customProject = {
...mockEnvironmentData.project,
recontactDays: 14,
clickOutsideClose: false,
overlay: "dark",
placement: "center",
inAppSurveyBranding: false,
styling: { allowStyleOverwrite: true, brandColor: "#ff0000" },
};
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
project: customProject,
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.environment.project).toEqual({
id: "project-123",
recontactDays: 14,
clickOutsideClose: false,
overlay: "dark",
placement: "center",
inAppSurveyBranding: false,
styling: { allowStyleOverwrite: true, brandColor: "#ff0000" },
});
});
test("should validate environmentId input", async () => {
// Invalid CUID should throw validation error
await expect(getEnvironmentStateData("invalid-id")).rejects.toThrow();
});
test("should handle different environment types", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
type: "development",
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.environment.type).toBe("development");
});
test("should handle appSetupCompleted false", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
appSetupCompleted: false,
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.environment.appSetupCompleted).toBe(false);
});
test("should correctly extract organization billing data", async () => {
const customBilling = {
plan: "enterprise",
stripeCustomerId: "cus_123",
limits: {
monthly: { responses: 10000, miu: 50000 },
projects: 100,
},
};
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
project: {
...mockEnvironmentData.project,
organization: {
id: "org-enterprise",
billing: customBilling,
},
},
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.organization).toEqual({
id: "org-enterprise",
billing: customBilling,
});
});
});
@@ -54,7 +54,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
id: true,
recontactDays: true,
clickOutsideClose: true,
overlay: true,
darkOverlay: true,
placement: true,
inAppSurveyBranding: true,
styling: true,
@@ -174,7 +174,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
id: environmentData.project.id,
recontactDays: environmentData.project.recontactDays,
clickOutsideClose: environmentData.project.clickOutsideClose,
overlay: environmentData.project.overlay,
darkOverlay: environmentData.project.darkOverlay,
placement: environmentData.project.placement,
inAppSurveyBranding: environmentData.project.inAppSurveyBranding,
styling: environmentData.project.styling,
@@ -58,7 +58,7 @@ const mockProject: TJsEnvironmentStateProject = {
inAppSurveyBranding: true,
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
styling: {
allowStyleOverwrite: false,
},
@@ -1,15 +1,13 @@
import { NextRequest } from "next/server";
import { logger } from "@formbricks/logger";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse, TResponseUpdateInput, ZResponseUpdateInput } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
import { ZResponseUpdateInput } from "@formbricks/types/responses";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getResponse } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
import { validateFileUploads } from "@/modules/storage/utils";
@@ -33,38 +31,6 @@ const handleDatabaseError = (error: Error, url: string, endpoint: string, respon
return responses.internalServerErrorResponse("Unknown error occurred", true);
};
const validateResponse = (
response: TResponse,
survey: TSurvey,
responseUpdateInput: TResponseUpdateInput
) => {
// Validate response data against validation rules
const mergedData = {
...response.data,
...responseUpdateInput.data,
};
const isFinished = responseUpdateInput.finished ?? false;
const validationErrors = validateResponseData(
survey.blocks,
mergedData,
responseUpdateInput.language ?? response.language ?? "en",
isFinished,
survey.questions
);
if (validationErrors) {
return {
response: responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
),
};
}
};
export const PUT = withV1ApiWrapper({
handler: async ({
req,
@@ -147,11 +113,6 @@ export const PUT = withV1ApiWrapper({
};
}
const validationResult = validateResponse(response, survey, inputValidation.data);
if (validationResult) {
return validationResult;
}
// update response with quota evaluation
let updatedResponse;
try {
@@ -6,14 +6,12 @@ import { ZEnvironmentId } from "@formbricks/types/environment";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getSurvey } from "@/lib/survey/service";
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
import { validateFileUploads } from "@/modules/storage/utils";
@@ -35,27 +33,6 @@ export const OPTIONS = async (): Promise<Response> => {
);
};
const validateResponse = (responseInputData: TResponseInput, survey: TSurvey) => {
// Validate response data against validation rules
const validationErrors = validateResponseData(
survey.blocks,
responseInputData.data,
responseInputData.language ?? "en",
responseInputData.finished,
survey.questions
);
if (validationErrors) {
return {
response: responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
),
};
}
};
export const POST = withV1ApiWrapper({
handler: async ({ req, props }: { req: NextRequest; props: Context }) => {
const params = await props.params;
@@ -146,11 +123,6 @@ export const POST = withV1ApiWrapper({
};
}
const validationResult = validateResponse(responseInputData, survey);
if (validationResult) {
return validationResult;
}
let response: TResponseWithQuotaFull;
try {
const meta: TResponseInput["meta"] = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { ImageResponse } from "next/og";
import { ImageResponse } from "@vercel/og";
import { NextRequest } from "next/server";
export const GET = async (req: NextRequest) => {
@@ -8,7 +8,6 @@ import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib
import { sendToPipeline } from "@/app/lib/pipelines";
import { deleteResponse, getResponse } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import { updateResponseWithQuotaEvaluation } from "./lib/response";
@@ -141,25 +140,6 @@ export const PUT = withV1ApiWrapper({
};
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
result.survey.blocks,
responseUpdate.data,
responseUpdate.language ?? "en",
responseUpdate.finished,
result.survey.questions
);
if (validationErrors) {
return {
response: responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
),
};
}
const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
if (!inputValidation.success) {
return {
@@ -7,7 +7,6 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getSurvey } from "@/lib/survey/service";
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import {
@@ -150,25 +149,6 @@ export const POST = withV1ApiWrapper({
};
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
surveyResult.survey.blocks,
responseInput.data,
responseInput.language ?? "en",
responseInput.finished,
surveyResult.survey.questions
);
if (validationErrors) {
return {
response: responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
),
};
}
if (responseInput.createdAt && !responseInput.updatedAt) {
responseInput.updatedAt = responseInput.createdAt;
}
@@ -11,7 +11,6 @@ import { sendToPipeline } from "@/app/lib/pipelines";
import { getSurvey } from "@/lib/survey/service";
import { getElementsFromBlocks } from "@/lib/survey/utils";
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
@@ -107,23 +106,6 @@ export const POST = async (request: Request, context: Context): Promise<Response
);
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
survey.blocks,
responseInputData.data,
responseInputData.language ?? "en",
responseInputData.finished,
survey.questions
);
if (validationErrors) {
return responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
);
}
let response: TResponseWithQuotaFull;
try {
const meta: TResponseInputV2["meta"] = {
+6 -9
View File
@@ -3,9 +3,8 @@
// Error components must be Client components
import * as Sentry from "@sentry/nextjs";
import { TFunction } from "i18next";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { type ClientErrorType, getClientErrorData, isExpectedError } from "@formbricks/types/errors";
import { type ClientErrorType, getClientErrorData } from "@formbricks/types/errors";
import { Button } from "@/modules/ui/components/button";
import { ErrorComponent } from "@/modules/ui/components/error-component";
@@ -31,13 +30,11 @@ const ErrorBoundary = ({ error, reset }: { error: Error; reset: () => void }) =>
const errorData = getClientErrorData(error);
const { title, description } = getErrorMessages(errorData.type, t);
useEffect(() => {
if (process.env.NODE_ENV === "development") {
console.error(error.message);
} else if (!isExpectedError(error)) {
Sentry.captureException(error);
}
}, [error]);
if (process.env.NODE_ENV === "development") {
console.error(error.message);
} else {
Sentry.captureException(error);
}
return (
<div className="flex h-full w-full flex-col items-center justify-center">
-2
View File
@@ -4848,14 +4848,12 @@ export const previewSurvey = (projectName: string, t: TFunction): TSurvey => {
t("templates.preview_survey_question_2_choice_2_label"),
],
headline: t("templates.preview_survey_question_2_headline"),
subheader: t("templates.preview_survey_question_2_subheader"),
required: true,
shuffleOption: "none",
}),
isDraft: true,
},
],
buttonLabel: createI18nString(t("templates.next"), []),
backButtonLabel: createI18nString(t("templates.preview_survey_question_2_back_button_label"), []),
},
{
@@ -8,7 +8,7 @@ import { authorizePrivateDownload } from "@/app/storage/[environmentId]/[accessT
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 { deleteFile, getFileStreamForDownload } from "@/modules/storage/service";
import { deleteFile, getSignedUrlForDownload } from "@/modules/storage/service";
import { getErrorResponseFromStorageError } from "@/modules/storage/utils";
import { logFileDeletion } from "./lib/audit-logs";
@@ -39,25 +39,21 @@ export const GET = async (
}
}
// Stream the file directly
const streamResult = await getFileStreamForDownload(fileName, environmentId, accessType);
const signedUrlResult = await getSignedUrlForDownload(fileName, environmentId, accessType);
if (!streamResult.ok) {
const errorResponse = getErrorResponseFromStorageError(streamResult.error, { fileName });
if (!signedUrlResult.ok) {
const errorResponse = getErrorResponseFromStorageError(signedUrlResult.error, { fileName });
return errorResponse;
}
const { body, contentType, contentLength } = streamResult.data;
return new Response(body, {
status: 200,
return new Response(null, {
status: 302,
headers: {
"Content-Type": contentType,
...(contentLength > 0 && { "Content-Length": String(contentLength) }),
Location: signedUrlResult.data,
"Cache-Control":
accessType === "private"
? "no-store, no-cache, must-revalidate"
: "public, max-age=31536000, immutable",
: "public, max-age=300, s-maxage=300, stale-while-revalidate=300",
},
});
};
+5 -6
View File
@@ -9,18 +9,17 @@
"source": "en-US",
"targets": [
"de-DE",
"es-ES",
"fr-FR",
"hu-HU",
"ja-JP",
"nl-NL",
"pt-BR",
"pt-PT",
"ro-RO",
"ru-RU",
"sv-SE",
"zh-Hans-CN",
"zh-Hant-TW"
"zh-Hant-TW",
"nl-NL",
"es-ES",
"sv-SE",
"ru-RU"
]
},
"version": 1.8
+247 -360
View File
File diff suppressed because it is too large Load Diff
+45 -191
View File
@@ -1,205 +1,59 @@
// OpenTelemetry instrumentation for Next.js - loaded via instrumentation.ts hook
// Pattern based on: ee/src/opentelemetry.ts (license server)
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
// instrumentation-node.ts
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { HostMetrics } from "@opentelemetry/host-metrics";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { RuntimeNodeInstrumentation } from "@opentelemetry/instrumentation-runtime-node";
import {
AlwaysOffSampler,
AlwaysOnSampler,
BatchSpanProcessor,
ParentBasedSampler,
type Sampler,
TraceIdRatioBasedSampler,
} from "@opentelemetry/sdk-trace-base";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
import { PrismaInstrumentation } from "@prisma/instrumentation";
detectResources,
envDetector,
hostDetector,
processDetector,
resourceFromAttributes,
} from "@opentelemetry/resources";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { logger } from "@formbricks/logger";
import { env } from "@/lib/env";
// --- Configuration from environment ---
const serviceName = process.env.OTEL_SERVICE_NAME || "formbricks";
const serviceVersion = process.env.npm_package_version || "0.0.0";
const environment = process.env.ENVIRONMENT || process.env.NODE_ENV || "development";
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
const prometheusEnabled = process.env.PROMETHEUS_ENABLED === "1";
const prometheusPort = process.env.PROMETHEUS_EXPORTER_PORT
? Number.parseInt(process.env.PROMETHEUS_EXPORTER_PORT)
: 9464;
// --- Configure OTLP exporters (conditional on endpoint being set) ---
let traceExporter: OTLPTraceExporter | undefined;
let otlpMetricExporter: OTLPMetricExporter | undefined;
if (otlpEndpoint) {
try {
// OTLPTraceExporter reads OTEL_EXPORTER_OTLP_ENDPOINT from env
// and appends /v1/traces for HTTP transport
// Uses OTEL_EXPORTER_OTLP_HEADERS from env natively (W3C OTel format: key=value,key2=value2)
traceExporter = new OTLPTraceExporter();
// OTLPMetricExporter reads OTEL_EXPORTER_OTLP_ENDPOINT from env
// and appends /v1/metrics for HTTP transport
// Uses OTEL_EXPORTER_OTLP_HEADERS from env natively
otlpMetricExporter = new OTLPMetricExporter();
} catch (error) {
logger.error(error, "Failed to create OTLP exporters. Telemetry will not be exported.");
}
}
// --- Configure Prometheus exporter (pull-based metrics for ServiceMonitor) ---
let prometheusExporter: PrometheusExporter | undefined;
if (prometheusEnabled) {
prometheusExporter = new PrometheusExporter({
port: prometheusPort,
endpoint: "/metrics",
host: "0.0.0.0",
});
}
// --- Build metric readers array ---
const metricReaders: (PeriodicExportingMetricReader | PrometheusExporter)[] = [];
if (otlpMetricExporter) {
metricReaders.push(
new PeriodicExportingMetricReader({
exporter: otlpMetricExporter,
exportIntervalMillis: 60000, // Export every 60 seconds
})
);
}
if (prometheusExporter) {
metricReaders.push(prometheusExporter);
}
// --- Resource attributes ---
const resourceAttributes: Record<string, string> = {
[ATTR_SERVICE_NAME]: serviceName,
[ATTR_SERVICE_VERSION]: serviceVersion,
"deployment.environment": environment,
};
// --- Configure sampler ---
const samplerType = process.env.OTEL_TRACES_SAMPLER || "always_on";
const parsedSamplerArg = process.env.OTEL_TRACES_SAMPLER_ARG
? Number.parseFloat(process.env.OTEL_TRACES_SAMPLER_ARG)
: undefined;
const samplerArg =
parsedSamplerArg !== undefined && !Number.isNaN(parsedSamplerArg) ? parsedSamplerArg : undefined;
let sampler: Sampler;
switch (samplerType) {
case "always_on":
sampler = new AlwaysOnSampler();
break;
case "always_off":
sampler = new AlwaysOffSampler();
break;
case "traceidratio":
sampler = new TraceIdRatioBasedSampler(samplerArg ?? 1);
break;
case "parentbased_traceidratio":
sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(samplerArg ?? 1),
});
break;
case "parentbased_always_on":
sampler = new ParentBasedSampler({
root: new AlwaysOnSampler(),
});
break;
case "parentbased_always_off":
sampler = new ParentBasedSampler({
root: new AlwaysOffSampler(),
});
break;
default:
logger.warn(`Unknown sampler type: ${samplerType}. Using always_on.`);
sampler = new AlwaysOnSampler();
}
// --- Initialize NodeSDK ---
const sdk = new NodeSDK({
sampler,
resource: resourceFromAttributes(resourceAttributes),
// When no OTLP endpoint is configured (e.g. Prometheus-only setups), pass an empty
// spanProcessors array to prevent the SDK from falling back to its default OTLP exporter
// which would attempt connections to localhost:4318 and cause noisy errors.
spanProcessors: traceExporter
? [
new BatchSpanProcessor(traceExporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
}),
]
: [],
metricReaders: metricReaders.length > 0 ? metricReaders : undefined,
instrumentations: [
getNodeAutoInstrumentations({
// Disable noisy/unnecessary instrumentations
"@opentelemetry/instrumentation-fs": {
enabled: false,
},
"@opentelemetry/instrumentation-dns": {
enabled: false,
},
"@opentelemetry/instrumentation-net": {
enabled: false,
},
// Disable pg instrumentation - PrismaInstrumentation handles DB tracing
"@opentelemetry/instrumentation-pg": {
enabled: false,
},
"@opentelemetry/instrumentation-http": {
// Ignore health/metrics endpoints to reduce noise
ignoreIncomingRequestHook: (req) => {
const url = req.url || "";
return url === "/health" || url.startsWith("/metrics") || url === "/api/v2/health";
},
},
// Enable runtime metrics for Node.js process monitoring
"@opentelemetry/instrumentation-runtime-node": {
enabled: true,
},
}),
// Prisma instrumentation for database query tracing
new PrismaInstrumentation(),
],
const exporter = new PrometheusExporter({
port: env.PROMETHEUS_EXPORTER_PORT ? parseInt(env.PROMETHEUS_EXPORTER_PORT) : 9464,
endpoint: "/metrics",
host: "0.0.0.0", // Listen on all network interfaces
});
// Start the SDK
sdk.start();
const detectedResources = detectResources({
detectors: [envDetector, processDetector, hostDetector],
});
// --- Log initialization status ---
const enabledFeatures: string[] = [];
if (traceExporter) enabledFeatures.push("traces");
if (otlpMetricExporter) enabledFeatures.push("otlp-metrics");
if (prometheusExporter) enabledFeatures.push("prometheus-metrics");
const customResources = resourceFromAttributes({});
const samplerArgStr = process.env.OTEL_TRACES_SAMPLER_ARG || "";
const samplerArgMsg = samplerArgStr ? `, samplerArg=${samplerArgStr}` : "";
const resources = detectedResources.merge(customResources);
if (enabledFeatures.length > 0) {
logger.info(
`OpenTelemetry initialized: service=${serviceName}, version=${serviceVersion}, environment=${environment}, exporters=${enabledFeatures.join("+")}, sampler=${samplerType}${samplerArgMsg}`
);
} else {
logger.info(
`OpenTelemetry initialized (no exporters): service=${serviceName}, version=${serviceVersion}, environment=${environment}`
);
}
const meterProvider = new MeterProvider({
readers: [exporter],
resource: resources,
});
// --- Graceful shutdown ---
// Run before other SIGTERM listeners (logger flush, etc.) so spans are drained first.
process.prependListener("SIGTERM", async () => {
const hostMetrics = new HostMetrics({
name: `otel-metrics`,
meterProvider,
});
registerInstrumentations({
meterProvider,
instrumentations: [new HttpInstrumentation(), new RuntimeNodeInstrumentation()],
});
hostMetrics.start();
process.on("SIGTERM", async () => {
try {
await sdk.shutdown();
// Stop collecting metrics or flush them if needed
await meterProvider.shutdown();
// Possibly close other instrumentation resources
} catch (e) {
logger.error(e, "Error during OpenTelemetry shutdown");
logger.error(e, "Error during graceful shutdown");
} finally {
process.exit(0);
}
});
+1 -4
View File
@@ -5,13 +5,10 @@ export const onRequestError = Sentry.captureRequestError;
export const register = async () => {
if (process.env.NEXT_RUNTIME === "nodejs") {
// Load OpenTelemetry instrumentation when Prometheus metrics or OTLP export is enabled
if (PROMETHEUS_ENABLED || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
if (PROMETHEUS_ENABLED) {
await import("./instrumentation-node");
}
}
// Sentry init loads after OTEL to avoid TracerProvider conflicts
// Sentry tracing is disabled (tracesSampleRate: 0) -- SigNoz handles distributed tracing
if (process.env.NEXT_RUNTIME === "nodejs" && IS_PRODUCTION && SENTRY_DSN) {
await import("./sentry.server.config");
}
+8 -9
View File
@@ -165,20 +165,19 @@ export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150;
export const DEFAULT_LOCALE = "en-US";
export const AVAILABLE_LOCALES: TUserLocale[] = [
"de-DE",
"en-US",
"es-ES",
"fr-FR",
"hu-HU",
"ja-JP",
"nl-NL",
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ru-RU",
"sv-SE",
"ja-JP",
"zh-Hans-CN",
"zh-Hant-TW",
"es-ES",
"sv-SE",
"ru-RU",
];
// Billing constants
+2
View File
@@ -55,6 +55,7 @@ export const env = createEnv({
OIDC_DISPLAY_NAME: z.string().optional(),
OIDC_ISSUER: z.string().optional(),
OIDC_SIGNING_ALGORITHM: z.string().optional(),
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
REDIS_URL:
process.env.NODE_ENV === "test"
? z.string().optional()
@@ -173,6 +174,7 @@ export const env = createEnv({
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
SENTRY_DSN: process.env.SENTRY_DSN,
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
NOTION_OAUTH_CLIENT_SECRET: process.env.NOTION_OAUTH_CLIENT_SECRET,
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
+32 -38
View File
@@ -126,12 +126,6 @@ export const addMultiLanguageLabels = (object: unknown, languageSymbols: string[
};
export const appLanguages = [
{
code: "de-DE",
label: {
"en-US": "German",
},
},
{
code: "en-US",
label: {
@@ -139,9 +133,15 @@ export const appLanguages = [
},
},
{
code: "es-ES",
code: "de-DE",
label: {
"en-US": "Spanish",
"en-US": "German",
},
},
{
code: "pt-BR",
label: {
"en-US": "Portuguese (Brazil)",
},
},
{
@@ -151,27 +151,9 @@ export const appLanguages = [
},
},
{
code: "hu-HU",
code: "zh-Hant-TW",
label: {
"en-US": "Hungarian",
},
},
{
code: "ja-JP",
label: {
"en-US": "Japanese",
},
},
{
code: "nl-NL",
label: {
"en-US": "Dutch",
},
},
{
code: "pt-BR",
label: {
"en-US": "Portuguese (Brazil)",
"en-US": "Chinese (Traditional)",
},
},
{
@@ -187,15 +169,9 @@ export const appLanguages = [
},
},
{
code: "ru-RU",
code: "ja-JP",
label: {
"en-US": "Russian",
},
},
{
code: "sv-SE",
label: {
"en-US": "Swedish",
"en-US": "Japanese",
},
},
{
@@ -205,9 +181,27 @@ export const appLanguages = [
},
},
{
code: "zh-Hant-TW",
code: "nl-NL",
label: {
"en-US": "Chinese (Traditional)",
"en-US": "Dutch",
},
},
{
code: "es-ES",
label: {
"en-US": "Spanish",
},
},
{
code: "sv-SE",
label: {
"en-US": "Swedish",
},
},
{
code: "ru-RU",
label: {
"en-US": "Russian",
},
},
];
+9 -9
View File
@@ -48,7 +48,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -106,7 +106,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -171,7 +171,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -196,7 +196,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -250,7 +250,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -324,7 +324,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -378,7 +378,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -403,7 +403,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
@@ -448,7 +448,7 @@ describe("Project Service", () => {
},
placement: WidgetPlacement.bottomRight,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [],
styling: {
allowStyleOverwrite: true,
+1 -1
View File
@@ -22,7 +22,7 @@ const selectProject = {
config: true,
placement: true,
clickOutsideClose: true,
overlay: true,
darkOverlay: true,
environments: true,
styling: true,
logo: true,
+24 -167
View File
@@ -1,6 +1,5 @@
// https://github.com/airbnb/javascript/#naming--uppercase
import { TProjectStyling } from "@formbricks/types/project";
import { isLight, mixColor } from "@/lib/utils/colors";
export const COLOR_DEFAULTS = {
brandColor: "#64748b",
@@ -12,174 +11,32 @@ export const COLOR_DEFAULTS = {
highlightBorderColor: "#64748b",
} as const;
const DEFAULT_BRAND_COLOR = "#64748b";
/**
* Derives a complete set of suggested color values from a single brand color.
*
* Used by the project-level "Suggest Colors" button **and** to build
* `STYLE_DEFAULTS` so that a fresh install always has colours that are
* visually cohesive with the default brand.
*
* The returned object is a flat map of form-field paths to values so it
* can be spread directly into form defaults or applied via `form.setValue`.
*/
export const getSuggestedColors = (brandColor: string = DEFAULT_BRAND_COLOR) => {
// Question / dark text: brand darkened with black (visible brand tint)
const questionColor = mixColor(brandColor, "#000000", 0.35);
// Input / option background: white with noticeable brand tint
const inputBg = mixColor(brandColor, "#ffffff", 0.92);
// Input border: visible brand-tinted border
const inputBorder = mixColor(brandColor, "#ffffff", 0.6);
// Card tones
const cardBg = mixColor(brandColor, "#ffffff", 0.97);
const cardBorder = mixColor(brandColor, "#ffffff", 0.8);
// Page background
const pageBg = mixColor(brandColor, "#ffffff", 0.855);
return {
// General
"brandColor.light": brandColor,
"questionColor.light": questionColor,
// Headlines & Descriptions — use questionColor to match the legacy behaviour
// where all text elements derived their color from questionColor.
"elementHeadlineColor.light": questionColor,
"elementDescriptionColor.light": questionColor,
"elementUpperLabelColor.light": questionColor,
// Buttons — use the brand color so the button matches the user's intent.
"buttonBgColor.light": brandColor,
"buttonTextColor.light": isLight(brandColor) ? "#0f172a" : "#ffffff",
// Inputs
"inputColor.light": inputBg,
"inputBorderColor.light": inputBorder,
"inputTextColor.light": questionColor,
// Options (Radio / Checkbox)
"optionBgColor.light": inputBg,
"optionLabelColor.light": questionColor,
// Card
"cardBackgroundColor.light": cardBg,
"cardBorderColor.light": cardBorder,
// Highlight / Focus
"highlightBorderColor.light": mixColor(brandColor, "#ffffff", 0.25),
// Progress Bar — indicator uses the brand color; track is a lighter tint.
"progressIndicatorBgColor.light": brandColor,
"progressTrackBgColor.light": mixColor(brandColor, "#ffffff", 0.8),
// Background
background: { bg: pageBg, bgType: "color" as const, brightness: 100 },
};
};
// Pre-compute colors derived from the default brand color.
const _colors = getSuggestedColors(DEFAULT_BRAND_COLOR);
/**
* Single source of truth for every styling default.
*
* Color values are derived from the default brand color (#64748b) via
* `getSuggestedColors()`. Non-color values (dimensions, weights, sizes)
* are hardcoded here and must be kept in sync with globals.css.
*
* Used everywhere: form defaults, preview rendering, email templates,
* and as the reset target for "Restore defaults".
*/
export const STYLE_DEFAULTS: TProjectStyling = {
export const defaultStyling: TProjectStyling = {
allowStyleOverwrite: true,
brandColor: { light: _colors["brandColor.light"] },
questionColor: { light: _colors["questionColor.light"] },
inputColor: { light: _colors["inputColor.light"] },
inputBorderColor: { light: _colors["inputBorderColor.light"] },
cardBackgroundColor: { light: _colors["cardBackgroundColor.light"] },
cardBorderColor: { light: _colors["cardBorderColor.light"] },
brandColor: {
light: COLOR_DEFAULTS.brandColor,
},
questionColor: {
light: COLOR_DEFAULTS.questionColor,
},
inputColor: {
light: COLOR_DEFAULTS.inputColor,
},
inputBorderColor: {
light: COLOR_DEFAULTS.inputBorderColor,
},
cardBackgroundColor: {
light: COLOR_DEFAULTS.cardBackgroundColor,
},
cardBorderColor: {
light: COLOR_DEFAULTS.cardBorderColor,
},
isLogoHidden: false,
highlightBorderColor: { light: _colors["highlightBorderColor.light"] },
highlightBorderColor: undefined,
isDarkModeEnabled: false,
roundness: 8,
cardArrangement: { linkSurveys: "simple", appSurveys: "simple" },
// Headlines & Descriptions
elementHeadlineColor: { light: _colors["elementHeadlineColor.light"] },
elementHeadlineFontSize: 16,
elementHeadlineFontWeight: 600,
elementDescriptionColor: { light: _colors["elementDescriptionColor.light"] },
elementDescriptionFontSize: 14,
elementDescriptionFontWeight: 400,
elementUpperLabelColor: { light: _colors["elementUpperLabelColor.light"] },
elementUpperLabelFontSize: 12,
elementUpperLabelFontWeight: 400,
// Inputs
inputTextColor: { light: _colors["inputTextColor.light"] },
inputBorderRadius: 8,
inputHeight: 40,
inputFontSize: 14,
inputPaddingX: 16,
inputPaddingY: 16,
inputPlaceholderOpacity: 0.5,
inputShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
// Buttons
buttonBgColor: { light: _colors["buttonBgColor.light"] },
buttonTextColor: { light: _colors["buttonTextColor.light"] },
buttonBorderRadius: 8,
buttonHeight: "auto",
buttonFontSize: 16,
buttonFontWeight: 500,
buttonPaddingX: 12,
buttonPaddingY: 12,
// Options
optionBgColor: { light: _colors["optionBgColor.light"] },
optionLabelColor: { light: _colors["optionLabelColor.light"] },
optionBorderRadius: 8,
optionPaddingX: 16,
optionPaddingY: 16,
optionFontSize: 14,
// Progress Bar
progressTrackHeight: 8,
progressTrackBgColor: { light: _colors["progressTrackBgColor.light"] },
progressIndicatorBgColor: { light: _colors["progressIndicatorBgColor.light"] },
};
/**
* Builds a complete TProjectStyling object from a single brand color.
*
* Uses STYLE_DEFAULTS for all non-color properties (dimensions, weights, etc.)
* and derives every color from the given brand color via getSuggestedColors().
*
* Useful when only a brand color is known (e.g. onboarding) and a fully
* coherent styling object is needed for both preview rendering and persistence.
*/
export const buildStylingFromBrandColor = (brandColor: string = DEFAULT_BRAND_COLOR): TProjectStyling => {
const colors = getSuggestedColors(brandColor);
return {
...STYLE_DEFAULTS,
brandColor: { light: colors["brandColor.light"] },
questionColor: { light: colors["questionColor.light"] },
elementHeadlineColor: { light: colors["elementHeadlineColor.light"] },
elementDescriptionColor: { light: colors["elementDescriptionColor.light"] },
elementUpperLabelColor: { light: colors["elementUpperLabelColor.light"] },
buttonBgColor: { light: colors["buttonBgColor.light"] },
buttonTextColor: { light: colors["buttonTextColor.light"] },
inputColor: { light: colors["inputColor.light"] },
inputBorderColor: { light: colors["inputBorderColor.light"] },
inputTextColor: { light: colors["inputTextColor.light"] },
optionBgColor: { light: colors["optionBgColor.light"] },
optionLabelColor: { light: colors["optionLabelColor.light"] },
cardBackgroundColor: { light: colors["cardBackgroundColor.light"] },
cardBorderColor: { light: colors["cardBorderColor.light"] },
highlightBorderColor: { light: colors["highlightBorderColor.light"] },
progressIndicatorBgColor: { light: colors["progressIndicatorBgColor.light"] },
progressTrackBgColor: { light: colors["progressTrackBgColor.light"] },
background: colors.background,
};
cardArrangement: {
linkSurveys: "straight",
appSurveys: "straight",
},
};
+1 -1
View File
@@ -85,7 +85,7 @@ export const mockProject: TProject = {
inAppSurveyBranding: false,
placement: "bottomRight",
clickOutsideClose: false,
overlay: "none",
darkOverlay: false,
environments: [],
languages: [],
config: {
-47
View File
@@ -141,52 +141,5 @@ describe("Time Utilities", () => {
expect(convertDatesInObject("string")).toBe("string");
expect(convertDatesInObject(123)).toBe(123);
});
test("should not convert dates in contactAttributes", () => {
const input = {
createdAt: "2024-03-20T15:30:00",
contactAttributes: {
createdAt: "2024-03-20T16:30:00",
email: "test@example.com",
},
};
const result = convertDatesInObject(input);
expect(result.createdAt).toBeInstanceOf(Date);
expect(result.contactAttributes.createdAt).toBe("2024-03-20T16:30:00");
expect(result.contactAttributes.email).toBe("test@example.com");
});
test("should not convert dates in variables", () => {
const input = {
updatedAt: "2024-03-20T15:30:00",
variables: {
createdAt: "2024-03-20T16:30:00",
userId: "123",
},
};
const result = convertDatesInObject(input);
expect(result.updatedAt).toBeInstanceOf(Date);
expect(result.variables.createdAt).toBe("2024-03-20T16:30:00");
expect(result.variables.userId).toBe("123");
});
test("should not convert dates in data or meta", () => {
const input = {
createdAt: "2024-03-20T15:30:00",
data: {
createdAt: "2024-03-20T16:30:00",
},
meta: {
updatedAt: "2024-03-20T17:30:00",
},
};
const result = convertDatesInObject(input);
expect(result.createdAt).toBeInstanceOf(Date);
expect(result.data.createdAt).toBe("2024-03-20T16:30:00");
expect(result.meta.updatedAt).toBe("2024-03-20T17:30:00");
});
});
});
+15 -22
View File
@@ -1,5 +1,5 @@
import { formatDistance, intlFormat } 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, ja, nl, pt, ptBR, ro, ru, sv, zhCN, zhTW } from "date-fns/locale";
import { TUserLocale } from "@formbricks/types/user";
export const convertDateString = (dateString: string | null) => {
@@ -87,30 +87,28 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
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 "fr-FR":
return fr;
case "nl-NL":
return nl;
case "sv-SE":
return sv;
case "zh-Hant-TW":
return zhTW;
case "pt-PT":
return pt;
case "ro-RO":
return ro;
case "ru-RU":
return ru;
case "sv-SE":
return sv;
case "ja-JP":
return ja;
case "zh-Hans-CN":
return zhCN;
case "zh-Hant-TW":
return zhTW;
case "es-ES":
return es;
case "ru-RU":
return ru;
}
};
@@ -160,12 +158,7 @@ export const convertDatesInObject = <T>(obj: T): T => {
return obj.map((item) => convertDatesInObject(item)) as unknown as T;
}
const newObj: any = {};
const keysToIgnore = new Set(["contactAttributes", "variables", "data", "meta"]);
for (const key in obj) {
if (keysToIgnore.has(key)) {
newObj[key] = obj[key];
continue;
}
if (
(key === "createdAt" || key === "updatedAt") &&
typeof obj[key] === "string" &&
@@ -1,261 +0,0 @@
import * as Sentry from "@sentry/nextjs";
import { getServerSession } from "next-auth";
import { DEFAULT_SERVER_ERROR_MESSAGE } from "next-safe-action";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import {
AuthenticationError,
AuthorizationError,
EXPECTED_ERROR_NAMES,
InvalidInputError,
OperationNotAllowedError,
ResourceNotFoundError,
TooManyRequestsError,
UnknownError,
ValidationError,
isExpectedError,
} from "@formbricks/types/errors";
// Mock Sentry
vi.mock("@sentry/nextjs", () => ({
captureException: vi.fn(),
}));
// Mock logger — use plain functions for chained calls so vi.resetAllMocks() doesn't break them
vi.mock("@formbricks/logger", () => ({
logger: {
withContext: () => ({ error: vi.fn() }),
warn: vi.fn(),
},
}));
// Mock next-auth
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
// Mock authOptions
vi.mock("@/modules/auth/lib/authOptions", () => ({
authOptions: {},
}));
// Mock user service
vi.mock("@/lib/user/service", () => ({
getUser: vi.fn(),
}));
// Mock client IP
vi.mock("@/lib/utils/client-ip", () => ({
getClientIpFromHeaders: vi.fn(),
}));
// Mock constants
vi.mock("@/lib/constants", () => ({
AUDIT_LOG_ENABLED: false,
AUDIT_LOG_GET_USER_IP: false,
}));
// Mock audit log types
vi.mock("@/modules/ee/audit-logs/types/audit-log", () => ({
UNKNOWN_DATA: "unknown",
}));
// ── shared helper tests (pure logic, no action client needed) ──────────
describe("isExpectedError (shared helper)", () => {
test("EXPECTED_ERROR_NAMES contains exactly the right error names", () => {
const expected = [
"ResourceNotFoundError",
"AuthorizationError",
"InvalidInputError",
"ValidationError",
"AuthenticationError",
"OperationNotAllowedError",
"TooManyRequestsError",
];
expect(EXPECTED_ERROR_NAMES.size).toBe(expected.length);
for (const name of expected) {
expect(EXPECTED_ERROR_NAMES.has(name)).toBe(true);
}
});
test.each([
{ ErrorClass: AuthorizationError, args: ["Not authorized"] },
{ ErrorClass: AuthenticationError, args: ["Not authenticated"] },
{ ErrorClass: TooManyRequestsError, args: ["Rate limit exceeded"] },
{ ErrorClass: ResourceNotFoundError, args: ["Survey", "123"] },
{ ErrorClass: InvalidInputError, args: ["Invalid input"] },
{ ErrorClass: ValidationError, args: ["Invalid data"] },
{ ErrorClass: OperationNotAllowedError, args: ["Not allowed"] },
])("returns true for $ErrorClass.name", ({ ErrorClass, args }) => {
const error = new (ErrorClass as any)(...args);
expect(isExpectedError(error)).toBe(true);
});
test("returns true for serialised errors that only have a matching name", () => {
const serialisedError = new Error("Auth failed");
serialisedError.name = "AuthorizationError";
expect(isExpectedError(serialisedError)).toBe(true);
});
test.each([
{ error: new Error("Something broke"), label: "Error" },
{ error: new TypeError("Cannot read properties"), label: "TypeError" },
{ error: new RangeError("Maximum call stack"), label: "RangeError" },
{ error: new UnknownError("Unknown"), label: "UnknownError" },
])("returns false for $label", ({ error }) => {
expect(isExpectedError(error)).toBe(false);
});
});
// ── integration tests against the real actionClient / authenticatedActionClient ──
describe("actionClient handleServerError", () => {
// Lazily import so mocks are in place first
let actionClient: (typeof import("./index"))["actionClient"];
beforeEach(async () => {
vi.clearAllMocks();
const mod = await import("./index");
actionClient = mod.actionClient;
});
afterEach(() => {
vi.clearAllMocks();
});
// Helper: create and execute an action that throws the given error
const executeThrowingAction = async (error: Error) => {
const action = actionClient.action(async () => {
throw error;
});
return action();
};
describe("expected errors should NOT be reported to Sentry", () => {
test("AuthorizationError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new AuthorizationError("Not authorized"));
expect(result?.serverError).toBe("Not authorized");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("AuthenticationError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new AuthenticationError("Not authenticated"));
expect(result?.serverError).toBe("Not authenticated");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("TooManyRequestsError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new TooManyRequestsError("Rate limit exceeded"));
expect(result?.serverError).toBe("Rate limit exceeded");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("ResourceNotFoundError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new ResourceNotFoundError("Survey", "123"));
expect(result?.serverError).toContain("Survey");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("InvalidInputError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new InvalidInputError("Invalid input"));
expect(result?.serverError).toBe("Invalid input");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("ValidationError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new ValidationError("Invalid data"));
expect(result?.serverError).toBe("Invalid data");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("OperationNotAllowedError returns its message and is not sent to Sentry", async () => {
const result = await executeThrowingAction(new OperationNotAllowedError("Not allowed"));
expect(result?.serverError).toBe("Not allowed");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
});
describe("unexpected errors SHOULD be reported to Sentry", () => {
test("generic Error is sent to Sentry and returns default message", async () => {
const error = new Error("Something broke");
const result = await executeThrowingAction(error);
expect(result?.serverError).toBe(DEFAULT_SERVER_ERROR_MESSAGE);
expect(Sentry.captureException).toHaveBeenCalledWith(
error,
expect.objectContaining({ extra: expect.any(Object) })
);
});
test("TypeError is sent to Sentry and returns default message", async () => {
const error = new TypeError("Cannot read properties of undefined");
const result = await executeThrowingAction(error);
expect(result?.serverError).toBe(DEFAULT_SERVER_ERROR_MESSAGE);
expect(Sentry.captureException).toHaveBeenCalledWith(
error,
expect.objectContaining({ extra: expect.any(Object) })
);
});
test("UnknownError is sent to Sentry (not an expected business-logic error)", async () => {
const error = new UnknownError("Unknown error");
const result = await executeThrowingAction(error);
expect(result?.serverError).toBe(DEFAULT_SERVER_ERROR_MESSAGE);
expect(Sentry.captureException).toHaveBeenCalledWith(
error,
expect.objectContaining({ extra: expect.any(Object) })
);
});
});
});
describe("authenticatedActionClient", () => {
let authenticatedActionClient: (typeof import("./index"))["authenticatedActionClient"];
let getUser: (typeof import("@/lib/user/service"))["getUser"];
beforeEach(async () => {
vi.clearAllMocks();
const mod = await import("./index");
authenticatedActionClient = mod.authenticatedActionClient;
const userService = await import("@/lib/user/service");
getUser = userService.getUser;
});
afterEach(() => {
vi.clearAllMocks();
});
test("throws AuthenticationError when there is no session", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const action = authenticatedActionClient.action(async () => "ok");
const result = await action();
// handleServerError catches AuthenticationError and returns its message
expect(result?.serverError).toBe("Not authenticated");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("throws AuthorizationError when user is not found", async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-1" } });
vi.mocked(getUser).mockResolvedValue(null as any);
const action = authenticatedActionClient.action(async () => "ok");
const result = await action();
expect(result?.serverError).toBe("User not found");
expect(Sentry.captureException).not.toHaveBeenCalled();
});
test("executes action successfully when session and user exist", async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-1" } });
vi.mocked(getUser).mockResolvedValue({ id: "user-1", name: "Test" } as any);
const action = authenticatedActionClient.action(async () => "success");
const result = await action();
expect(result?.data).toBe("success");
expect(result?.serverError).toBeUndefined();
expect(Sentry.captureException).not.toHaveBeenCalled();
});
});
+21 -7
View File
@@ -3,7 +3,15 @@ import { getServerSession } from "next-auth";
import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from "next-safe-action";
import { v4 as uuidv4 } from "uuid";
import { logger } from "@formbricks/logger";
import { AuthenticationError, AuthorizationError, isExpectedError } from "@formbricks/types/errors";
import {
AuthenticationError,
AuthorizationError,
InvalidInputError,
OperationNotAllowedError,
ResourceNotFoundError,
TooManyRequestsError,
UnknownError,
} from "@formbricks/types/errors";
import { AUDIT_LOG_ENABLED, AUDIT_LOG_GET_USER_IP } from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
@@ -14,18 +22,24 @@ import { ActionClientCtx } from "./types/context";
export const actionClient = createSafeActionClient({
handleServerError(e, utils) {
const eventId = (utils.ctx as Record<string, any>)?.auditLoggingCtx?.eventId ?? undefined; // keep explicit fallback
if (isExpectedError(e)) {
return e.message;
}
// Only capture unexpected errors to Sentry
Sentry.captureException(e, {
extra: {
eventId,
},
});
if (
e instanceof ResourceNotFoundError ||
e instanceof AuthorizationError ||
e instanceof InvalidInputError ||
e instanceof UnknownError ||
e instanceof AuthenticationError ||
e instanceof OperationNotAllowedError ||
e instanceof TooManyRequestsError
) {
return e.message;
}
// eslint-disable-next-line no-console -- This error needs to be logged for debugging server-side errors
logger.withContext({ eventId }).error(e, "SERVER ERROR");
return DEFAULT_SERVER_ERROR_MESSAGE;
+1 -1
View File
@@ -2,8 +2,8 @@ import { createInstance } from "i18next";
import ICU from "i18next-icu";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { TUserLocale } from "@formbricks/types/user";
import { DEFAULT_LOCALE } from "@/lib/constants";
import { TUserLocale } from "@formbricks/types/user";
import { getLocale } from "@/lingodotdev/language";
const initI18next = async (lng: string) => {
+39 -154
View File
@@ -243,6 +243,7 @@
"imprint": "Impressum",
"in_progress": "Im Gange",
"inactive_surveys": "Inaktive Umfragen",
"input_type": "Eingabetyp",
"integration": "Integration",
"integrations": "Integrationen",
"invalid_date": "Ungültiges Datum",
@@ -254,7 +255,6 @@
"label": "Bezeichnung",
"language": "Sprache",
"learn_more": "Mehr erfahren",
"license_expired": "License Expired",
"light_overlay": "Helle Überlagerung",
"limits_reached": "Limits erreicht",
"link": "Link",
@@ -267,11 +267,13 @@
"look_and_feel": "Darstellung",
"manage": "Verwalten",
"marketing": "Marketing",
"maximum": "Maximal",
"member": "Mitglied",
"members": "Mitglieder",
"members_and_teams": "Mitglieder & Teams",
"membership_not_found": "Mitgliedschaft nicht gefunden",
"metadata": "Metadaten",
"minimum": "Minimum",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funktioniert am besten auf einem größeren Bildschirm. Um Umfragen zu verwalten oder zu erstellen, wechsle zu einem anderen Gerät.",
"mobile_overlay_surveys_look_good": "Keine Sorge deine Umfragen sehen auf jedem Gerät und jeder Bildschirmgröße großartig aus!",
"mobile_overlay_title": "Oops, Bildschirm zu klein erkannt!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Kein Hintergrundbild gefunden.",
"no_code": "No Code",
"no_files_uploaded": "Keine Dateien hochgeladen",
"no_overlay": "Kein Overlay",
"no_quotas_found": "Keine Kontingente gefunden",
"no_result_found": "Kein Ergebnis gefunden",
"no_results": "Keine Ergebnisse",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Organisations-Teams nicht gefunden",
"other": "Andere",
"others": "Andere",
"overlay_color": "Overlay-Farbe",
"overview": "Überblick",
"password": "Passwort",
"paused": "Pausiert",
@@ -326,7 +326,7 @@
"placeholder": "Platzhalter",
"please_select_at_least_one_survey": "Bitte wähle mindestens eine Umfrage aus",
"please_select_at_least_one_trigger": "Bitte wähle mindestens einen Auslöser aus",
"please_upgrade_your_plan": "Bitte aktualisieren Sie Ihren Plan",
"please_upgrade_your_plan": "Bitte upgrade deinen Plan.",
"preview": "Vorschau",
"preview_survey": "Umfragevorschau",
"privacy": "Datenschutz",
@@ -352,7 +352,6 @@
"request_trial_license": "Testlizenz anfordern",
"reset_to_default": "Auf Standard zurücksetzen",
"response": "Antwort",
"response_id": "Antwort-ID",
"responses": "Antworten",
"restart": "Neustart",
"role": "Rolle",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Sie haben Ihr Limit von {projectLimit} Workspaces erreicht.",
"you_have_reached_your_monthly_miu_limit_of": "Du hast dein monatliches MIU-Limit erreicht",
"you_have_reached_your_monthly_response_limit_of": "Du hast dein monatliches Antwortlimit erreicht",
"you_will_be_downgraded_to_the_community_edition_on_date": "Du wirst am {date} auf die Community Edition herabgestuft.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Du wirst am {date} auf die Community Edition herabgestuft."
},
"emails": {
"accept": "Annehmen",
@@ -956,32 +954,19 @@
"enterprise_features": "Unternehmensfunktionen",
"get_an_enterprise_license_to_get_access_to_all_features": "Hol dir eine Enterprise-Lizenz, um Zugriff auf alle Funktionen zu erhalten.",
"keep_full_control_over_your_data_privacy_and_security": "Behalte die volle Kontrolle über deine Daten, Privatsphäre und Sicherheit.",
"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_active": "Aktiv",
"license_status_description": "Status deiner Enterprise-Lizenz.",
"license_status_expired": "Abgelaufen",
"license_status_invalid": "Ungültige Lizenz",
"license_status_unreachable": "Nicht erreichbar",
"license_unreachable_grace_period": "Der Lizenzserver ist nicht erreichbar. Deine Enterprise-Funktionen bleiben während einer 3-tägigen Kulanzfrist bis zum {gracePeriodEnd} aktiv.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Ganz unkompliziert: Fordere eine kostenlose 30-Tage-Testlizenz an, um alle Funktionen zu testen, indem Du dieses Formular ausfüllst:",
"no_credit_card_no_sales_call_just_test_it": "Keine Kreditkarte. Kein Verkaufsgespräch. Einfach testen :)",
"on_request": "Auf Anfrage",
"organization_roles": "Organisationsrollen (Admin, Editor, Entwickler, etc.)",
"questions_please_reach_out_to": "Fragen? Bitte melde Dich bei",
"recheck_license": "Lizenz erneut prüfen",
"recheck_license_failed": "Lizenzprüfung fehlgeschlagen. Der Lizenzserver ist möglicherweise nicht erreichbar.",
"recheck_license_invalid": "Der Lizenzschlüssel ist ungültig. Bitte überprüfe deinen ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Lizenzprüfung erfolgreich",
"recheck_license_unreachable": "Lizenzserver ist nicht erreichbar. Bitte versuche es später erneut.",
"rechecking": "Wird erneut geprüft...",
"request_30_day_trial_license": "30-Tage-Testlizenz anfordern",
"saml_sso": "SAML-SSO",
"service_level_agreement": "Service-Level-Vereinbarung",
"soc2_hipaa_iso_27001_compliance_check": "SOC2-, HIPAA- und ISO 27001-Konformitätsprüfung",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Teams & Zugriffskontrolle (Lesen, Lesen & Schreiben, Verwalten)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Schalte die volle Power von Formbricks frei. 30 Tage kostenlos."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Schalte die volle Power von Formbricks frei. 30 Tage kostenlos.",
"your_enterprise_license_is_active_all_features_unlocked": "Deine Unternehmenslizenz ist aktiv. Alle Funktionen freigeschaltet."
},
"general": {
"bulk_invite_warning_description": "Bitte beachte, dass im Free-Plan alle Organisationsmitglieder automatisch die Rolle \"Owner\" zugewiesen bekommen, unabhängig von der im CSV-File angegebenen Rolle.",
@@ -1005,7 +990,7 @@
"from_your_organization": "von deiner Organisation",
"invitation_sent_once_more": "Einladung nochmal gesendet.",
"invite_deleted_successfully": "Einladung erfolgreich gelöscht",
"invite_expires_on": "Einladung läuft ab am {date}",
"invited_on": "Eingeladen am {date}",
"invites_failed": "Einladungen fehlgeschlagen",
"leave_organization": "Organisation verlassen",
"leave_organization_description": "Du wirst diese Organisation verlassen und den Zugriff auf alle Umfragen und Antworten verlieren. Du kannst nur wieder beitreten, wenn Du erneut eingeladen wirst.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Bitte füllen Sie alle Felder aus, um einen neuen Workspace hinzuzufügen.",
"read": "Lesen",
"read_write": "Lesen & Schreiben",
"select_member": "Mitglied auswählen",
"select_workspace": "Workspace auswählen",
"team_admin": "Team-Admin",
"team_created_successfully": "Team erfolgreich erstellt.",
"team_deleted_successfully": "Team erfolgreich gelöscht.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Platzhalter hinzufügen, falls kein Wert zur Verfügung steht.",
"add_hidden_field_id": "Verstecktes Feld ID hinzufügen",
"add_highlight_border": "Rahmen hinzufügen",
"add_highlight_border_description": "Füge deiner Umfragekarte einen äußeren Rahmen hinzu.",
"add_logic": "Logik hinzufügen",
"add_none_of_the_above": "Füge \"Keine der oben genannten Optionen\" hinzu",
"add_option": "Option hinzufügen",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Ändere die Nachricht, die Besucher sehen, wenn die Umfrage geschlossen ist.",
"adjust_the_theme_in_the": "Passe das Thema an in den",
"all_other_answers_will_continue_to": "Alle anderen Antworten werden weiterhin",
"allow_file_type": "Dateityp begrenzen",
"allow_multi_select": "Mehrfachauswahl erlauben",
"allow_multiple_files": "Mehrere Dateien zulassen",
"allow_users_to_select_more_than_one_image": "Erlaube Nutzern, mehr als ein Bild auszuwählen",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Block dupliziert.",
"bold": "Fett",
"brand_color": "Markenfarbe",
"brand_color_description": "Wird auf Buttons, Links und Hervorhebungen angewendet.",
"brightness": "Helligkeit",
"bulk_edit": "Massenbearbeitung",
"bulk_edit_description": "Bearbeiten Sie alle Optionen unten, eine pro Zeile. Leere Zeilen werden übersprungen und Duplikate entfernt.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Neue Aktion erfassen",
"card_arrangement_for_survey_type_derived": "Kartenanordnung für {surveyTypeDerived} Umfragen",
"card_background_color": "Hintergrundfarbe der Karte",
"card_background_color_description": "Füllt den Bereich der Umfragekarte.",
"card_border_color": "Farbe des Kartenrandes",
"card_border_color_description": "Umrandet die Umfragekarte.",
"card_styling": "Kartengestaltung",
"casual": "Lässig",
"caution_edit_duplicate": "Duplizieren & bearbeiten",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Ältere und neuere Antworten vermischen sich, was zu irreführenden Datensummen führen kann.",
"caution_recommendation": "Dies kann im Umfrageübersicht zu Dateninkonsistenzen führen. Wir empfehlen stattdessen, die Umfrage zu duplizieren.",
"caution_text": "Änderungen werden zu Inkonsistenzen führen",
"centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe",
"change_anyway": "Trotzdem ändern",
"change_background": "Hintergrund ändern",
"change_question_type": "Fragetyp ändern",
"change_survey_type": "Die Änderung des Umfragetypen kann vorhandenen Zugriff beeinträchtigen",
"change_the_background_color_of_the_card": "Hintergrundfarbe der Karte ändern.",
"change_the_background_color_of_the_input_fields": "Hintergrundfarbe der Eingabefelder ändern.",
"change_the_background_to_a_color_image_or_animation": "Hintergrund zu einer Farbe, einem Bild oder einer Animation ändern.",
"change_the_border_color_of_the_card": "Randfarbe der Karte ändern.",
"change_the_border_color_of_the_input_fields": "Randfarbe der Eingabefelder ändern.",
"change_the_border_radius_of_the_card_and_the_inputs": "Radius der Ränder der Karte und der Eingabefelder ändern.",
"change_the_brand_color_of_the_survey": "Markenfarbe der Umfrage ändern.",
"change_the_placement_of_this_survey": "Platzierung dieser Umfrage ändern.",
"change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.",
"changes_saved": "Änderungen gespeichert.",
"changing_survey_type_will_remove_existing_distribution_channels": "\"Das Ändern des Umfragetypen beeinflusst, wie er geteilt werden kann. Wenn Teilnehmer bereits Zugriffslinks für den aktuellen Typ haben, könnten sie das Zugriffsrecht nach dem Wechsel verlieren.\"",
"character_limit_toggle_description": "Begrenzen Sie, wie kurz oder lang eine Antwort sein kann.",
"character_limit_toggle_title": "Fügen Sie Zeichenbeschränkungen hinzu",
"checkbox_label": "Checkbox-Beschriftung",
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
"choose_the_first_question_on_your_block": "Wählen sie die erste frage in ihrem block",
@@ -1263,6 +1259,7 @@
"contact_fields": "Kontaktfelder",
"contains": "enthält",
"continue_to_settings": "Weiter zu den Einstellungen",
"control_which_file_types_can_be_uploaded": "Steuere, welche Dateitypen hochgeladen werden können.",
"convert_to_multiple_choice": "In Multiple-Choice umwandeln",
"convert_to_single_choice": "In Einzelauswahl umwandeln",
"country": "Land",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Trotzdem löschen",
"delete_block": "Block löschen",
"delete_choice": "Auswahl löschen",
"disable_the_visibility_of_survey_progress": "Deaktiviere die Sichtbarkeit des Umfragefortschritts.",
"display_an_estimate_of_completion_time_for_survey": "Zeige eine Schätzung der Fertigstellungszeit für die Umfrage an",
"display_number_of_responses_for_survey": "Anzahl der Antworten für Umfrage anzeigen",
"display_type": "Anzeigetyp",
"divide": "Teilen /",
"does_not_contain": "Enthält nicht",
"does_not_end_with": "Endet nicht mit",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Enthält nicht alle von",
"does_not_include_one_of": "Enthält nicht eines von",
"does_not_start_with": "Fängt nicht an mit",
"dropdown": "Dropdown",
"duplicate_block": "Block duplizieren",
"duplicate_question": "Frage duplizieren",
"edit_link": "Bearbeitungslink",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "Fortschrittsbalken ausblenden",
"hide_question_settings": "Frageeinstellungen ausblenden",
"hostname": "Hostname",
"if_you_need_more_please": "Wenn Sie mehr benötigen, bitte",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein",
"if_you_need_more_please": "Wenn Du mehr brauchst, bitte",
"if_you_really_want_that_answer_ask_until_you_get_it": "Weiterhin anzeigen, wenn ausgelöst, bis eine Antwort abgegeben wird.",
"ignore_global_waiting_time": "Abkühlphase ignorieren",
"ignore_global_waiting_time_description": "Diese Umfrage kann angezeigt werden, wenn ihre Bedingungen erfüllt sind, auch wenn kürzlich eine andere Umfrage angezeigt wurde.",
@@ -1391,9 +1386,7 @@
"initial_value": "Anfangswert",
"inner_text": "Innerer Text",
"input_border_color": "Randfarbe des Eingabefelds",
"input_border_color_description": "Umrandet Texteingaben und Textbereiche.",
"input_color": "Farbe des Eingabefelds",
"input_color_description": "Füllt das Innere von Texteingaben.",
"insert_link": "Link einfügen",
"invalid_targeting": "Ungültiges Targeting: Bitte überprüfe deine Zielgruppenfilter",
"invalid_video_url_warning": "Bitte gib eine gültige YouTube-, Vimeo- oder Loom-URL ein. Andere Video-Plattformen werden derzeit nicht unterstützt.",
@@ -1420,10 +1413,10 @@
"key": "Schlüssel",
"last_name": "Nachname",
"let_people_upload_up_to_25_files_at_the_same_time": "Erlaube bis zu 25 Dateien gleichzeitig hochzuladen.",
"limit_the_maximum_file_size": "Begrenzen Sie die maximale Dateigröße für Uploads.",
"limit_upload_file_size_to": "Upload-Dateigröße begrenzen auf",
"limit_file_types": "Dateitypen einschränken",
"limit_the_maximum_file_size": "Maximale Dateigröße begrenzen",
"limit_upload_file_size_to": "Maximale Dateigröße für Uploads",
"link_survey_description": "Teile einen Link zu einer Umfrageseite oder bette ihn in eine Webseite oder E-Mail ein.",
"list": "Liste",
"load_segment": "Segment laden",
"logic_error_warning": "Änderungen werden zu Logikfehlern führen",
"logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage",
@@ -1434,8 +1427,8 @@
"manage_languages": "Sprachen verwalten",
"matrix_all_fields": "Alle Felder",
"matrix_rows": "Zeilen",
"max_file_size": "Maximale Dateigröße",
"max_file_size_limit_is": "Die maximale Dateigrößenbeschränkung beträgt",
"max_file_size": "Max. Dateigröße",
"max_file_size_limit_is": "Max. Dateigröße ist",
"move_question_to_block": "Frage in Block verschieben",
"multiply": "Multiplizieren *",
"needed_for_self_hosted_cal_com_instance": "Benötigt für eine selbstgehostete Cal.com-Instanz",
@@ -1467,6 +1460,7 @@
"picture_idx": "Bild {idx}",
"pin_can_only_contain_numbers": "PIN darf nur Zahlen enthalten.",
"pin_must_be_a_four_digit_number": "Die PIN muss eine vierstellige Zahl sein.",
"please_enter_a_file_extension": "Bitte gib eine Dateierweiterung ein.",
"please_enter_a_valid_url": "Bitte geben Sie eine gültige URL ein (z. B. https://beispiel.de)",
"please_set_a_survey_trigger": "Bitte richte einen Umfrage-Trigger ein",
"please_specify": "Bitte angeben",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Nur Benutzer, die die PIN haben, können auf die Umfrage zugreifen.",
"publish": "Veröffentlichen",
"question": "Frage",
"question_color": "Fragefarbe",
"question_deleted": "Frage gelöscht.",
"question_duplicated": "Frage dupliziert.",
"question_id_updated": "Frage-ID aktualisiert",
"question_used_in_logic_warning_text": "Elemente aus diesem Block werden in einer Logikregel verwendet. Möchten Sie ihn wirklich löschen?",
"question_used_in_logic_warning_title": "Logikinkonsistenz",
"question_used_in_quota": "Diese Frage wird in der “{quotaName}” Quote verwendet",
"question_used_in_logic": "Diese Frage wird in der Logik der Frage {questionIndex} verwendet.",
"question_used_in_quota": "Diese Frage wird in der \"{quotaName}\" Quote verwendet",
"question_used_in_recall": "Diese Frage wird in Frage {questionIndex} abgerufen.",
"question_used_in_recall_ending_card": "Diese Frage wird in der Abschlusskarte abgerufen.",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
"response_options": "Antwortoptionen",
"roundness": "Rundheit",
"roundness_description": "Steuert, wie abgerundet die Kartenecken sind.",
"row_used_in_logic_error": "Diese Zeile wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
"rows": "Zeilen",
"save_and_close": "Speichern & Schließen",
@@ -1546,7 +1539,6 @@
"search_for_images": "Nach Bildern suchen",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "Sekunden nach dem Auslösen wird die Umfrage geschlossen, wenn keine Antwort erfolgt.",
"seconds_before_showing_the_survey": "Sekunden, bevor die Umfrage angezeigt wird.",
"select_field": "Feld auswählen",
"select_or_type_value": "Auswählen oder Wert eingeben",
"select_ordering": "Anordnung auswählen",
"select_saved_action": "Gespeicherte Aktion auswählen",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Styling auf Themenstile eingestellt",
"subheading": "Zwischenüberschrift",
"subtract": "Subtrahieren -",
"suggest_colors": "Farben vorschlagen",
"survey_completed_heading": "Umfrage abgeschlossen",
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Einmal anzeigen, auch wenn sie nicht antworten.",
"then": "dann",
"this_action_will_remove_all_the_translations_from_this_survey": "Diese Aktion entfernt alle Übersetzungen aus dieser Umfrage.",
"this_extension_is_already_added": "Diese Erweiterung ist bereits hinzugefügt.",
"this_file_type_is_not_supported": "Dieser Dateityp wird nicht unterstützt.",
"three_points": "3 Punkte",
"times": "Zeiten",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Um die Platzierung über alle Umfragen hinweg konsistent zu halten, kannst du",
@@ -1613,51 +1608,8 @@
"upper_label": "Oberes Label",
"url_filters": "URL-Filter",
"url_not_supported": "URL nicht unterstützt",
"validation": {
"add_validation_rule": "Validierungsregel hinzufügen",
"answer_all_rows": "Alle Zeilen beantworten",
"characters": "Zeichen",
"contains": "enthält",
"delete_validation_rule": "Validierungsregel löschen",
"does_not_contain": "enthält nicht",
"email": "Ist gültige E-Mail",
"end_date": "Enddatum",
"file_extension_is": "Dateierweiterung ist",
"file_extension_is_not": "Dateierweiterung ist nicht",
"is": "ist",
"is_between": "ist zwischen",
"is_earlier_than": "ist früher als",
"is_greater_than": "ist größer als",
"is_later_than": "ist später als",
"is_less_than": "ist weniger als",
"is_not": "ist nicht",
"is_not_between": "ist nicht zwischen",
"kb": "KB",
"max_length": "Höchstens",
"max_selections": "Höchstens",
"max_value": "Höchstens",
"mb": "MB",
"min_length": "Mindestens",
"min_selections": "Mindestens",
"min_value": "Mindestens",
"minimum_options_ranked": "Mindestanzahl bewerteter Optionen",
"minimum_rows_answered": "Mindestanzahl beantworteter Zeilen",
"options_selected": "Optionen ausgewählt",
"pattern": "Entspricht Regex-Muster",
"phone": "Ist gültige Telefonnummer",
"rank_all_options": "Alle Optionen bewerten",
"select_file_extensions": "Dateierweiterungen auswählen...",
"select_option": "Option auswählen",
"start_date": "Startdatum",
"url": "Ist gültige URL"
},
"validation_logic_and": "Alle sind wahr",
"validation_logic_or": "mindestens eine ist wahr",
"validation_rules": "Validierungsregeln",
"validation_rules_description": "Nur Antworten akzeptieren, die die folgenden Kriterien erfüllen",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable {variableName} wird in der {quotaName} Quote verwendet",
"variable_name_conflicts_with_hidden_field": "Der Variablenname steht im Konflikt mit einer vorhandenen Hidden-Field-ID.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" wird in der \"{quotaName}\" Quote verwendet",
"variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.",
"variable_name_must_start_with_a_letter": "Variablenname muss mit einem Buchstaben beginnen.",
"variable_used_in_recall": "Variable \"{variable}\" wird in Frage {questionIndex} abgerufen.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Hintergrundfarbe hinzufügen",
"add_background_color_description": "Füge dem Logo-Container eine Hintergrundfarbe hinzu.",
"advanced_styling_field_border_radius": "Rahmenradius",
"advanced_styling_field_button_bg": "Button-Hintergrund",
"advanced_styling_field_button_bg_description": "Füllt den Weiter-/Absenden-Button.",
"advanced_styling_field_button_border_radius_description": "Rundet die Button-Ecken ab.",
"advanced_styling_field_button_font_size_description": "Skaliert den Text der Button-Beschriftung.",
"advanced_styling_field_button_font_weight_description": "Macht den Button-Text heller oder fetter.",
"advanced_styling_field_button_height_description": "Steuert die Button-Höhe.",
"advanced_styling_field_button_padding_x_description": "Fügt links und rechts Abstand hinzu.",
"advanced_styling_field_button_padding_y_description": "Fügt oben und unten Abstand hinzu.",
"advanced_styling_field_button_text": "Button-Text",
"advanced_styling_field_button_text_description": "Färbt die Beschriftung innerhalb von Buttons.",
"advanced_styling_field_description_color": "Beschreibungsfarbe",
"advanced_styling_field_description_color_description": "Färbt den Text unterhalb jeder Überschrift.",
"advanced_styling_field_description_size": "Schriftgröße der Beschreibung",
"advanced_styling_field_description_size_description": "Skaliert den Beschreibungstext.",
"advanced_styling_field_description_weight": "Schriftstärke der Beschreibung",
"advanced_styling_field_description_weight_description": "Macht den Beschreibungstext heller oder fetter.",
"advanced_styling_field_font_size": "Schriftgröße",
"advanced_styling_field_font_weight": "Schriftstärke",
"advanced_styling_field_headline_color": "Überschriftsfarbe",
"advanced_styling_field_headline_color_description": "Färbt den Hauptfragetext.",
"advanced_styling_field_headline_size": "Schriftgröße der Überschrift",
"advanced_styling_field_headline_size_description": "Skaliert den Überschriftentext.",
"advanced_styling_field_headline_weight": "Schriftstärke der Überschrift",
"advanced_styling_field_headline_weight_description": "Macht den Überschriftentext heller oder fetter.",
"advanced_styling_field_height": "Höhe",
"advanced_styling_field_indicator_bg": "Indikator-Hintergrund",
"advanced_styling_field_indicator_bg_description": "Färbt den gefüllten Teil des Balkens.",
"advanced_styling_field_input_border_radius_description": "Rundet die Eingabeecken ab.",
"advanced_styling_field_input_font_size_description": "Skaliert den eingegebenen Text in Eingabefeldern.",
"advanced_styling_field_input_height_description": "Steuert die Höhe des Eingabefelds.",
"advanced_styling_field_input_padding_x_description": "Fügt links und rechts Abstand hinzu.",
"advanced_styling_field_input_padding_y_description": "Fügt oben und unten Abstand hinzu.",
"advanced_styling_field_input_placeholder_opacity_description": "Blendet den Platzhaltertext aus.",
"advanced_styling_field_input_shadow_description": "Fügt einen Schlagschatten um Eingabefelder hinzu.",
"advanced_styling_field_input_text": "Eingabetext",
"advanced_styling_field_input_text_description": "Färbt den eingegebenen Text in Eingabefeldern.",
"advanced_styling_field_option_bg": "Hintergrund",
"advanced_styling_field_option_bg_description": "Füllt die Optionselemente.",
"advanced_styling_field_option_border_radius_description": "Rundet die Ecken der Optionen ab.",
"advanced_styling_field_option_font_size_description": "Skaliert den Text der Optionsbeschriftung.",
"advanced_styling_field_option_label": "Label-Farbe",
"advanced_styling_field_option_label_description": "Färbt den Text der Optionsbeschriftung.",
"advanced_styling_field_option_padding_x_description": "Fügt links und rechts Abstand hinzu.",
"advanced_styling_field_option_padding_y_description": "Fügt oben und unten Abstand hinzu.",
"advanced_styling_field_padding_x": "Innenabstand X",
"advanced_styling_field_padding_y": "Innenabstand Y",
"advanced_styling_field_placeholder_opacity": "Platzhalter-Deckkraft",
"advanced_styling_field_shadow": "Schatten",
"advanced_styling_field_track_bg": "Track-Hintergrund",
"advanced_styling_field_track_bg_description": "Färbt den nicht ausgefüllten Teil des Balkens.",
"advanced_styling_field_track_height": "Track-Höhe",
"advanced_styling_field_track_height_description": "Steuert die Dicke des Fortschrittsbalkens.",
"advanced_styling_field_upper_label_color": "Farbe des oberen Labels",
"advanced_styling_field_upper_label_color_description": "Färbt die kleine Beschriftung über Eingabefeldern.",
"advanced_styling_field_upper_label_size": "Schriftgröße des oberen Labels",
"advanced_styling_field_upper_label_size_description": "Skaliert die kleine Beschriftung über Eingabefeldern.",
"advanced_styling_field_upper_label_weight": "Schriftstärke des oberen Labels",
"advanced_styling_field_upper_label_weight_description": "Macht die Beschriftung leichter oder fetter.",
"advanced_styling_section_buttons": "Buttons",
"advanced_styling_section_headlines": "Überschriften & Beschreibungen",
"advanced_styling_section_inputs": "Eingabefelder",
"advanced_styling_section_options": "Optionen (Radio/Checkbox)",
"app_survey_placement": "Platzierung der App-Umfrage",
"app_survey_placement_settings_description": "Ändere, wo Umfragen in deiner Web-App oder Website angezeigt werden.",
"centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe",
"email_customization": "E-Mail-Anpassung",
"email_customization_description": "Ändere das Aussehen und die Gestaltung von E-Mails, die Formbricks in deinem Namen versendet.",
"enable_custom_styling": "Benutzerdefiniertes Styling aktivieren",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Formbricks-Branding ist ausgeblendet.",
"formbricks_branding_settings_description": "Wir freuen uns über deine Unterstützung, haben aber Verständnis, wenn du es ausschaltest.",
"formbricks_branding_shown": "Formbricks-Branding wird angezeigt.",
"generate_theme_btn": "Generieren",
"generate_theme_confirmation": "Möchtest du ein passendes Farbschema basierend auf deiner Markenfarbe generieren? Dies überschreibt deine aktuellen Farbeinstellungen.",
"generate_theme_header": "Farbschema generieren?",
"logo_removed_successfully": "Logo erfolgreich entfernt",
"logo_settings_description": "Lade dein Firmenlogo hoch, um Umfragen und Link-Vorschauen zu branden.",
"logo_updated_successfully": "Logo erfolgreich aktualisiert",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Formbricks-Branding in {type}-Umfragen anzeigen",
"show_powered_by_formbricks": "\"Powered by Formbricks\"-Signatur anzeigen",
"styling_updated_successfully": "Styling erfolgreich aktualisiert",
"suggest_colors": "Farben vorschlagen",
"theme": "Theme",
"theme_settings_description": "Erstelle ein Style-Theme für alle Umfragen. Du kannst für jede Umfrage individuelles Styling aktivieren."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Ja, halte mich auf dem Laufenden.",
"preview_survey_question_2_choice_2_label": "Nein, danke!",
"preview_survey_question_2_headline": "Möchtest Du auf dem Laufenden bleiben?",
"preview_survey_question_2_subheader": "Dies ist eine Beispielbeschreibung.",
"preview_survey_welcome_card_headline": "Willkommen!",
"prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.",
"prioritize_features_name": "Funktionen priorisieren",
File diff suppressed because it is too large Load Diff
+36 -151
View File
@@ -243,6 +243,7 @@
"imprint": "Aviso legal",
"in_progress": "En progreso",
"inactive_surveys": "Encuestas inactivas",
"input_type": "Tipo de entrada",
"integration": "integración",
"integrations": "Integraciones",
"invalid_date": "Fecha no válida",
@@ -254,7 +255,6 @@
"label": "Etiqueta",
"language": "Idioma",
"learn_more": "Saber más",
"license_expired": "License Expired",
"light_overlay": "Superposición clara",
"limits_reached": "Límites alcanzados",
"link": "Enlace",
@@ -267,11 +267,13 @@
"look_and_feel": "Apariencia",
"manage": "Gestionar",
"marketing": "Marketing",
"maximum": "Máximo",
"member": "Miembro",
"members": "Miembros",
"members_and_teams": "Miembros y equipos",
"membership_not_found": "Membresía no encontrada",
"metadata": "Metadatos",
"minimum": "Mínimo",
"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_title": "¡Ups, pantalla pequeña detectada!",
@@ -285,7 +287,6 @@
"no_background_image_found": "No se encontró imagen de fondo.",
"no_code": "Sin código",
"no_files_uploaded": "No se subieron archivos",
"no_overlay": "Sin superposición",
"no_quotas_found": "No se encontraron cuotas",
"no_result_found": "No se encontró resultado",
"no_results": "Sin resultados",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Equipos de la organización no encontrados",
"other": "Otro",
"others": "Otros",
"overlay_color": "Color de superposición",
"overview": "Resumen",
"password": "Contraseña",
"paused": "Pausado",
@@ -326,7 +326,7 @@
"placeholder": "Marcador de posición",
"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_upgrade_your_plan": "Por favor, actualiza tu plan",
"please_upgrade_your_plan": "Por favor, actualiza tu plan.",
"preview": "Vista previa",
"preview_survey": "Vista previa de la encuesta",
"privacy": "Política de privacidad",
@@ -352,7 +352,6 @@
"request_trial_license": "Solicitar licencia de prueba",
"reset_to_default": "Restablecer a valores predeterminados",
"response": "Respuesta",
"response_id": "ID de respuesta",
"responses": "Respuestas",
"restart": "Reiniciar",
"role": "Rol",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Has alcanzado tu límite de {projectLimit} espacios de trabajo.",
"you_have_reached_your_monthly_miu_limit_of": "Has alcanzado tu límite mensual de MIU de",
"you_have_reached_your_monthly_response_limit_of": "Has alcanzado tu límite mensual de respuestas de",
"you_will_be_downgraded_to_the_community_edition_on_date": "Serás degradado a la edición Community el {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Serás degradado a la edición Community el {date}."
},
"emails": {
"accept": "Aceptar",
@@ -956,32 +954,19 @@
"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.",
"keep_full_control_over_your_data_privacy_and_security": "Mantén el control total sobre la privacidad y seguridad de tus datos.",
"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_active": "Activa",
"license_status_description": "Estado de tu licencia enterprise.",
"license_status_expired": "Caducada",
"license_status_invalid": "Licencia no válida",
"license_status_unreachable": "Inaccesible",
"license_unreachable_grace_period": "No se puede acceder al servidor de licencias. Tus funciones empresariales permanecen activas durante un período de gracia de 3 días que finaliza el {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sin necesidad de llamadas, sin compromisos: solicita una licencia de prueba gratuita de 30 días para probar todas las características rellenando este formulario:",
"no_credit_card_no_sales_call_just_test_it": "Sin tarjeta de crédito. Sin llamada de ventas. Solo pruébalo :)",
"on_request": "Bajo petición",
"organization_roles": "Roles de organización (administrador, editor, desarrollador, etc.)",
"questions_please_reach_out_to": "¿Preguntas? Por favor, contacta con",
"recheck_license": "Volver a comprobar licencia",
"recheck_license_failed": "Error al comprobar la licencia. Es posible que el servidor de licencias no esté disponible.",
"recheck_license_invalid": "La clave de licencia no es válida. Por favor, verifica tu ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Comprobación de licencia correcta",
"recheck_license_unreachable": "El servidor de licencias no está disponible. Inténtalo de nuevo más tarde.",
"rechecking": "Comprobando...",
"request_30_day_trial_license": "Solicitar licencia de prueba de 30 días",
"saml_sso": "SAML SSO",
"service_level_agreement": "Acuerdo de nivel de servicio",
"soc2_hipaa_iso_27001_compliance_check": "Verificación de cumplimiento SOC2, HIPAA, ISO 27001",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Equipos y roles de acceso (lectura, lectura y escritura, gestión)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloquea todo el potencial de Formbricks. Gratis durante 30 días."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloquea todo el potencial de Formbricks. Gratis durante 30 días.",
"your_enterprise_license_is_active_all_features_unlocked": "Tu licencia empresarial está activa. Todas las características desbloqueadas."
},
"general": {
"bulk_invite_warning_description": "En el plan gratuito, a todos los miembros de la organización se les asigna siempre el rol de \"Propietario\".",
@@ -1005,7 +990,7 @@
"from_your_organization": "de tu organización",
"invitation_sent_once_more": "Invitación enviada una vez más.",
"invite_deleted_successfully": "Invitación eliminada correctamente",
"invite_expires_on": "La invitación expira el {date}",
"invited_on": "Invitado el {date}",
"invites_failed": "Las invitaciones fallaron",
"leave_organization": "Abandonar organización",
"leave_organization_description": "Abandonarás esta organización y perderás acceso a todas las encuestas y respuestas. Solo podrás volver a unirte si te invitan de nuevo.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Por favor, rellena todos los campos para añadir un proyecto nuevo.",
"read": "Lectura",
"read_write": "Lectura y escritura",
"select_member": "Seleccionar miembro",
"select_workspace": "Seleccionar proyecto",
"team_admin": "Administrador de equipo",
"team_created_successfully": "Equipo creado con éxito.",
"team_deleted_successfully": "Equipo eliminado correctamente.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Añadir un marcador de posición para mostrar si no hay valor que recuperar.",
"add_hidden_field_id": "Añadir ID de campo oculto",
"add_highlight_border": "Añadir borde destacado",
"add_highlight_border_description": "Añadir un borde exterior a tu tarjeta de encuesta.",
"add_logic": "Añadir lógica",
"add_none_of_the_above": "Añadir \"Ninguna de las anteriores\"",
"add_option": "Añadir opción",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Cambiar el mensaje que ven los visitantes cuando la encuesta está cerrada.",
"adjust_the_theme_in_the": "Ajustar el tema en el",
"all_other_answers_will_continue_to": "Todas las demás respuestas continuarán",
"allow_file_type": "Permitir tipo de archivo",
"allow_multi_select": "Permitir selección múltiple",
"allow_multiple_files": "Permitir múltiples archivos",
"allow_users_to_select_more_than_one_image": "Permitir a los usuarios seleccionar más de una imagen",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Bloque duplicado.",
"bold": "Negrita",
"brand_color": "Color de marca",
"brand_color_description": "Se aplica a botones, enlaces y resaltados.",
"brightness": "Brillo",
"bulk_edit": "Edición masiva",
"bulk_edit_description": "Edita todas las opciones a continuación, una por línea. Las líneas vacías se omitirán y los duplicados se eliminarán.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Capturar nueva acción",
"card_arrangement_for_survey_type_derived": "Disposición de tarjetas para encuestas de tipo {surveyTypeDerived}",
"card_background_color": "Color de fondo de la tarjeta",
"card_background_color_description": "Rellena el área de la tarjeta de encuesta.",
"card_border_color": "Color del borde de la tarjeta",
"card_border_color_description": "Delinea la tarjeta de encuesta.",
"card_styling": "Estilo de la tarjeta",
"casual": "Informal",
"caution_edit_duplicate": "Duplicar y editar",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Las respuestas antiguas y nuevas se mezclan, lo que puede llevar a resúmenes de datos engañosos.",
"caution_recommendation": "Esto puede causar inconsistencias de datos en el resumen de la encuesta. Recomendamos duplicar la encuesta en su lugar.",
"caution_text": "Los cambios provocarán inconsistencias",
"centered_modal_overlay_color": "Color de superposición del modal centrado",
"change_anyway": "Cambiar de todos modos",
"change_background": "Cambiar fondo",
"change_question_type": "Cambiar tipo de pregunta",
"change_survey_type": "Cambiar el tipo de encuesta afecta al acceso existente",
"change_the_background_color_of_the_card": "Cambiar el color de fondo de la tarjeta.",
"change_the_background_color_of_the_input_fields": "Cambiar el color de fondo de los campos de entrada.",
"change_the_background_to_a_color_image_or_animation": "Cambiar el fondo a un color, imagen o animación.",
"change_the_border_color_of_the_card": "Cambiar el color del borde de la tarjeta.",
"change_the_border_color_of_the_input_fields": "Cambiar el color del borde de los campos de entrada.",
"change_the_border_radius_of_the_card_and_the_inputs": "Cambiar el radio del borde de la tarjeta y las entradas.",
"change_the_brand_color_of_the_survey": "Cambiar el color de marca de la encuesta.",
"change_the_placement_of_this_survey": "Cambiar la ubicación de esta encuesta.",
"change_the_question_color_of_the_survey": "Cambiar el color de las preguntas de la encuesta.",
"changes_saved": "Cambios guardados.",
"changing_survey_type_will_remove_existing_distribution_channels": "Cambiar el tipo de encuesta afectará a cómo se puede compartir. Si los encuestados ya tienen enlaces de acceso para el tipo actual, podrían perder el acceso después del cambio.",
"character_limit_toggle_description": "Limitar lo corta o larga que puede ser una respuesta.",
"character_limit_toggle_title": "Añadir límites de caracteres",
"checkbox_label": "Etiqueta de casilla de verificación",
"choose_the_actions_which_trigger_the_survey": "Elige las acciones que activan la encuesta.",
"choose_the_first_question_on_your_block": "Elige la primera pregunta en tu bloque",
@@ -1263,6 +1259,7 @@
"contact_fields": "Campos de contacto",
"contains": "Contiene",
"continue_to_settings": "Continuar a ajustes",
"control_which_file_types_can_be_uploaded": "Controla qué tipos de archivos se pueden subir.",
"convert_to_multiple_choice": "Convertir a selección múltiple",
"convert_to_single_choice": "Convertir a selección única",
"country": "País",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Eliminar de todos modos",
"delete_block": "Eliminar bloque",
"delete_choice": "Eliminar opción",
"disable_the_visibility_of_survey_progress": "Desactivar la visibilidad del progreso de la encuesta.",
"display_an_estimate_of_completion_time_for_survey": "Mostrar una estimación del tiempo de finalización de la encuesta",
"display_number_of_responses_for_survey": "Mostrar número de respuestas para la encuesta",
"display_type": "Tipo de visualización",
"divide": "Dividir /",
"does_not_contain": "No contiene",
"does_not_end_with": "No termina con",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "No incluye todos los",
"does_not_include_one_of": "No incluye uno de",
"does_not_start_with": "No comienza con",
"dropdown": "Desplegable",
"duplicate_block": "Duplicar bloque",
"duplicate_question": "Duplicar pregunta",
"edit_link": "Editar enlace",
@@ -1381,6 +1375,7 @@
"hide_progress_bar": "Ocultar barra de progreso",
"hide_question_settings": "Ocultar ajustes de la pregunta",
"hostname": "Nombre de host",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "¿Cuánto estilo quieres darle a tus tarjetas en las encuestas de tipo {surveyTypeDerived}?",
"if_you_need_more_please": "Si necesitas más, por favor",
"if_you_really_want_that_answer_ask_until_you_get_it": "Seguir mostrando cuando se active hasta que se envíe una respuesta.",
"ignore_global_waiting_time": "Ignorar periodo de espera",
@@ -1391,9 +1386,7 @@
"initial_value": "Valor inicial",
"inner_text": "Texto interior",
"input_border_color": "Color del borde de entrada",
"input_border_color_description": "Delinea los campos de texto y áreas de texto.",
"input_color": "Color de entrada",
"input_color_description": "Rellena el interior de los campos de texto.",
"insert_link": "Insertar enlace",
"invalid_targeting": "Segmentación no válida: por favor, comprueba tus filtros de audiencia",
"invalid_video_url_warning": "Por favor, introduce una URL válida de YouTube, Vimeo o Loom. Actualmente no admitimos otros proveedores de alojamiento de vídeos.",
@@ -1420,10 +1413,10 @@
"key": "Clave",
"last_name": "Apellido",
"let_people_upload_up_to_25_files_at_the_same_time": "Permitir que las personas suban hasta 25 archivos al mismo tiempo.",
"limit_the_maximum_file_size": "Limita el tamaño máximo de archivo para las subidas.",
"limit_upload_file_size_to": "Limitar el tamaño de archivo de subida a",
"limit_file_types": "Limitar tipos de archivo",
"limit_the_maximum_file_size": "Limitar el tamaño máximo de archivo",
"limit_upload_file_size_to": "Limitar tamaño de subida de archivos a",
"link_survey_description": "Comparte un enlace a una página de encuesta o incrústala en una página web o correo electrónico.",
"list": "Lista",
"load_segment": "Cargar segmento",
"logic_error_warning": "El cambio causará errores lógicos",
"logic_error_warning_text": "Cambiar el tipo de pregunta eliminará las condiciones lógicas de esta pregunta",
@@ -1467,6 +1460,7 @@
"picture_idx": "Imagen {idx}",
"pin_can_only_contain_numbers": "El PIN solo puede contener números.",
"pin_must_be_a_four_digit_number": "El PIN debe ser un número de cuatro dígitos.",
"please_enter_a_file_extension": "Por favor, introduce una extensión de archivo.",
"please_enter_a_valid_url": "Por favor, introduce una URL válida (p. ej., https://example.com)",
"please_set_a_survey_trigger": "Establece un disparador de encuesta",
"please_specify": "Por favor, especifica",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Solo los usuarios que tengan el PIN pueden acceder a la encuesta.",
"publish": "Publicar",
"question": "Pregunta",
"question_color": "Color de la pregunta",
"question_deleted": "Pregunta eliminada.",
"question_duplicated": "Pregunta duplicada.",
"question_id_updated": "ID de pregunta actualizado",
"question_used_in_logic_warning_text": "Los elementos de este bloque se usan en una regla de lógica, ¿estás seguro de que quieres eliminarlo?",
"question_used_in_logic_warning_title": "Inconsistencia de lógica",
"question_used_in_quota": "Esta pregunta se está utilizando en la cuota “{quotaName}”",
"question_used_in_logic": "Esta pregunta se utiliza en la lógica de la pregunta {questionIndex}.",
"question_used_in_quota": "Esta pregunta se está utilizando en la cuota \"{quotaName}\"",
"question_used_in_recall": "Esta pregunta se está recordando en la pregunta {questionIndex}.",
"question_used_in_recall_ending_card": "Esta pregunta se está recordando en la Tarjeta Final",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Límites de respuestas, redirecciones y más.",
"response_options": "Opciones de respuesta",
"roundness": "Redondez",
"roundness_description": "Controla qué tan redondeadas están las esquinas de la tarjeta.",
"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.",
"rows": "Filas",
"save_and_close": "Guardar y cerrar",
@@ -1546,7 +1539,6 @@
"search_for_images": "Buscar imágenes",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos después de activarse, la encuesta se cerrará si no hay respuesta",
"seconds_before_showing_the_survey": "segundos antes de mostrar la encuesta.",
"select_field": "Seleccionar campo",
"select_or_type_value": "Selecciona o escribe un valor",
"select_ordering": "Seleccionar ordenación",
"select_saved_action": "Seleccionar acción guardada",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Estilo configurado según los estilos del tema",
"subheading": "Subtítulo",
"subtract": "Restar -",
"suggest_colors": "Sugerir colores",
"survey_completed_heading": "Encuesta completada",
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
"survey_display_settings": "Ajustes de visualización de la encuesta",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar una sola vez, incluso si no responden.",
"then": "Entonces",
"this_action_will_remove_all_the_translations_from_this_survey": "Esta acción eliminará todas las traducciones de esta encuesta.",
"this_extension_is_already_added": "Esta extensión ya está añadida.",
"this_file_type_is_not_supported": "Este tipo de archivo no es compatible.",
"three_points": "3 puntos",
"times": "veces",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para mantener la ubicación coherente en todas las encuestas, puedes",
@@ -1613,51 +1608,8 @@
"upper_label": "Etiqueta superior",
"url_filters": "Filtros de URL",
"url_not_supported": "URL no compatible",
"validation": {
"add_validation_rule": "Añadir regla de validación",
"answer_all_rows": "Responde todas las filas",
"characters": "Caracteres",
"contains": "Contiene",
"delete_validation_rule": "Eliminar regla de validación",
"does_not_contain": "No contiene",
"email": "Es un correo electrónico válido",
"end_date": "Fecha de finalización",
"file_extension_is": "La extensión del archivo es",
"file_extension_is_not": "La extensión del archivo no es",
"is": "Es",
"is_between": "Está entre",
"is_earlier_than": "Es anterior a",
"is_greater_than": "Es mayor que",
"is_later_than": "Es posterior a",
"is_less_than": "Es menor que",
"is_not": "No es",
"is_not_between": "No está entre",
"kb": "KB",
"max_length": "Como máximo",
"max_selections": "Como máximo",
"max_value": "Como máximo",
"mb": "MB",
"min_length": "Al menos",
"min_selections": "Al menos",
"min_value": "Al menos",
"minimum_options_ranked": "Opciones mínimas clasificadas",
"minimum_rows_answered": "Filas mínimas respondidas",
"options_selected": "Opciones seleccionadas",
"pattern": "Coincide con el patrón regex",
"phone": "Es un teléfono válido",
"rank_all_options": "Clasificar todas las opciones",
"select_file_extensions": "Selecciona extensiones de archivo...",
"select_option": "Seleccionar opción",
"start_date": "Fecha de inicio",
"url": "Es una URL válida"
},
"validation_logic_and": "Todas son verdaderas",
"validation_logic_or": "alguna es verdadera",
"validation_rules": "Reglas de validación",
"validation_rules_description": "Solo aceptar respuestas que cumplan los siguientes criterios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} se usa en la lógica de la pregunta {questionIndex}. Por favor, elimínala primero de la lógica.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable {variableName} se está utilizando en la cuota {quotaName}",
"variable_name_conflicts_with_hidden_field": "El nombre de la variable entra en conflicto con un ID de campo oculto existente.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" se está utilizando en la cuota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "El nombre de la variable ya está en uso, por favor elige otro.",
"variable_name_must_start_with_a_letter": "El nombre de la variable debe comenzar con una letra.",
"variable_used_in_recall": "La variable \"{variable}\" se está recuperando en la pregunta {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Añadir color de fondo",
"add_background_color_description": "Añade un color de fondo al contenedor del logotipo.",
"advanced_styling_field_border_radius": "Radio del borde",
"advanced_styling_field_button_bg": "Fondo del botón",
"advanced_styling_field_button_bg_description": "Rellena el botón siguiente / enviar.",
"advanced_styling_field_button_border_radius_description": "Redondea las esquinas del botón.",
"advanced_styling_field_button_font_size_description": "Escala el texto de la etiqueta del botón.",
"advanced_styling_field_button_font_weight_description": "Hace el texto del botón más ligero o más grueso.",
"advanced_styling_field_button_height_description": "Controla la altura del botón.",
"advanced_styling_field_button_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
"advanced_styling_field_button_padding_y_description": "Añade espacio arriba y abajo.",
"advanced_styling_field_button_text": "Texto del botón",
"advanced_styling_field_button_text_description": "Colorea la etiqueta dentro de los botones.",
"advanced_styling_field_description_color": "Color de la descripción",
"advanced_styling_field_description_color_description": "Colorea el texto debajo de cada titular.",
"advanced_styling_field_description_size": "Tamaño de fuente de la descripción",
"advanced_styling_field_description_size_description": "Escala el texto de la descripción.",
"advanced_styling_field_description_weight": "Grosor de fuente de la descripción",
"advanced_styling_field_description_weight_description": "Hace el texto de la descripción más ligero o más grueso.",
"advanced_styling_field_font_size": "Tamaño de fuente",
"advanced_styling_field_font_weight": "Grosor de fuente",
"advanced_styling_field_headline_color": "Color del titular",
"advanced_styling_field_headline_color_description": "Colorea el texto principal de la pregunta.",
"advanced_styling_field_headline_size": "Tamaño de fuente del titular",
"advanced_styling_field_headline_size_description": "Escala el texto del titular.",
"advanced_styling_field_headline_weight": "Grosor de fuente del titular",
"advanced_styling_field_headline_weight_description": "Hace el texto del titular más ligero o más grueso.",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_indicator_bg": "Fondo del indicador",
"advanced_styling_field_indicator_bg_description": "Colorea la porción rellena de la barra.",
"advanced_styling_field_input_border_radius_description": "Redondea las esquinas del campo.",
"advanced_styling_field_input_font_size_description": "Escala el texto escrito en los campos.",
"advanced_styling_field_input_height_description": "Controla la altura del campo de entrada.",
"advanced_styling_field_input_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
"advanced_styling_field_input_padding_y_description": "Añade espacio en la parte superior e inferior.",
"advanced_styling_field_input_placeholder_opacity_description": "Atenúa el texto de sugerencia del marcador de posición.",
"advanced_styling_field_input_shadow_description": "Añade una sombra alrededor de los campos de entrada.",
"advanced_styling_field_input_text": "Texto de entrada",
"advanced_styling_field_input_text_description": "Colorea el texto escrito en los campos de entrada.",
"advanced_styling_field_option_bg": "Fondo",
"advanced_styling_field_option_bg_description": "Rellena los elementos de opción.",
"advanced_styling_field_option_border_radius_description": "Redondea las esquinas de las opciones.",
"advanced_styling_field_option_font_size_description": "Escala el texto de la etiqueta de opción.",
"advanced_styling_field_option_label": "Color de la etiqueta",
"advanced_styling_field_option_label_description": "Colorea el texto de la etiqueta de opción.",
"advanced_styling_field_option_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
"advanced_styling_field_option_padding_y_description": "Añade espacio en la parte superior e inferior.",
"advanced_styling_field_padding_x": "Relleno X",
"advanced_styling_field_padding_y": "Relleno Y",
"advanced_styling_field_placeholder_opacity": "Opacidad del marcador de posición",
"advanced_styling_field_shadow": "Sombra",
"advanced_styling_field_track_bg": "Fondo de la pista",
"advanced_styling_field_track_bg_description": "Colorea la parte no rellenada de la barra.",
"advanced_styling_field_track_height": "Altura de la pista",
"advanced_styling_field_track_height_description": "Controla el grosor de la barra de progreso.",
"advanced_styling_field_upper_label_color": "Color de la etiqueta del titular",
"advanced_styling_field_upper_label_color_description": "Colorea la etiqueta pequeña sobre los campos de entrada.",
"advanced_styling_field_upper_label_size": "Tamaño de fuente de la etiqueta del titular",
"advanced_styling_field_upper_label_size_description": "Escala la etiqueta pequeña sobre los campos de entrada.",
"advanced_styling_field_upper_label_weight": "Grosor de fuente de la etiqueta del titular",
"advanced_styling_field_upper_label_weight_description": "Hace que la etiqueta sea más ligera o más gruesa.",
"advanced_styling_section_buttons": "Botones",
"advanced_styling_section_headlines": "Títulos y descripciones",
"advanced_styling_section_inputs": "Campos de entrada",
"advanced_styling_section_options": "Opciones (radio/casilla de verificación)",
"app_survey_placement": "Ubicación de encuesta de aplicación",
"app_survey_placement_settings_description": "Cambia dónde se mostrarán las encuestas en tu aplicación web o sitio web.",
"centered_modal_overlay_color": "Color de superposición del modal centrado",
"email_customization": "Personalización de correo electrónico",
"email_customization_description": "Cambia el aspecto de los correos electrónicos que Formbricks envía en tu nombre.",
"enable_custom_styling": "Habilitar estilo personalizado",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "La marca de Formbricks está oculta.",
"formbricks_branding_settings_description": "Nos encanta tu apoyo, pero lo entendemos si lo desactivas.",
"formbricks_branding_shown": "La marca de Formbricks se muestra.",
"generate_theme_btn": "Generar",
"generate_theme_confirmation": "¿Te gustaría generar un tema de colores que combine con el color de tu marca? Esto sobrescribirá tu configuración de colores actual.",
"generate_theme_header": "¿Generar tema de colores?",
"logo_removed_successfully": "Logotipo eliminado correctamente",
"logo_settings_description": "Sube el logotipo de tu empresa para personalizar las encuestas y las vistas previas de enlaces.",
"logo_updated_successfully": "Logotipo actualizado correctamente",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Mostrar marca de Formbricks en encuestas de {type}",
"show_powered_by_formbricks": "Mostrar firma 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo actualizado correctamente",
"suggest_colors": "Sugerir colores",
"theme": "Tema",
"theme_settings_description": "Crea un tema de estilo para todas las encuestas. Puedes activar el estilo personalizado para cada encuesta."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Sí, mantenme informado.",
"preview_survey_question_2_choice_2_label": "¡No, gracias!",
"preview_survey_question_2_headline": "¿Quieres estar al tanto?",
"preview_survey_question_2_subheader": "Esta es una descripción de ejemplo.",
"preview_survey_welcome_card_headline": "¡Bienvenido!",
"prioritize_features_description": "Identifica las funciones que tus usuarios necesitan más y menos.",
"prioritize_features_name": "Priorizar funciones",
+38 -153
View File
@@ -243,6 +243,7 @@
"imprint": "Empreinte",
"in_progress": "En cours",
"inactive_surveys": "Sondages inactifs",
"input_type": "Type d'entrée",
"integration": "intégration",
"integrations": "Intégrations",
"invalid_date": "Date invalide",
@@ -254,7 +255,6 @@
"label": "Étiquette",
"language": "Langue",
"learn_more": "En savoir plus",
"license_expired": "License Expired",
"light_overlay": "Claire",
"limits_reached": "Limites atteints",
"link": "Lien",
@@ -267,11 +267,13 @@
"look_and_feel": "Apparence",
"manage": "Gérer",
"marketing": "Marketing",
"maximum": "Max",
"member": "Membre",
"members": "Membres",
"members_and_teams": "Membres & Équipes",
"membership_not_found": "Abonnement non trouvé",
"metadata": "Métadonnées",
"minimum": "Min",
"mobile_overlay_app_works_best_on_desktop": "Formbricks fonctionne mieux sur un écran plus grand. Pour gérer ou créer des sondages, passez à un autre appareil.",
"mobile_overlay_surveys_look_good": "Ne t'inquiète pas tes enquêtes sont superbes sur tous les appareils et tailles d'écran!",
"mobile_overlay_title": "Oups, écran minuscule détecté!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Aucune image de fond trouvée.",
"no_code": "Sans code",
"no_files_uploaded": "Aucun fichier n'a été téléchargé.",
"no_overlay": "Aucune superposition",
"no_quotas_found": "Aucun quota trouvé",
"no_result_found": "Aucun résultat trouvé",
"no_results": "Aucun résultat",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Équipes d'organisation non trouvées",
"other": "Autre",
"others": "Autres",
"overlay_color": "Couleur de superposition",
"overview": "Aperçu",
"password": "Mot de passe",
"paused": "En pause",
@@ -326,7 +326,7 @@
"placeholder": "Remplaçant",
"please_select_at_least_one_survey": "Veuillez sélectionner au moins une enquête.",
"please_select_at_least_one_trigger": "Veuillez sélectionner au moins un déclencheur.",
"please_upgrade_your_plan": "Veuillez mettre à niveau votre plan",
"please_upgrade_your_plan": "Veuillez mettre à niveau votre plan.",
"preview": "Aperçu",
"preview_survey": "Aperçu de l'enquête",
"privacy": "Politique de confidentialité",
@@ -352,7 +352,6 @@
"request_trial_license": "Demander une licence d'essai",
"reset_to_default": "Réinitialiser par défaut",
"response": "Réponse",
"response_id": "ID de réponse",
"responses": "Réponses",
"restart": "Recommencer",
"role": "Rôle",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Vous avez atteint votre limite de {projectLimit} espaces de travail.",
"you_have_reached_your_monthly_miu_limit_of": "Vous avez atteint votre limite mensuelle de MIU de",
"you_have_reached_your_monthly_response_limit_of": "Vous avez atteint votre limite de réponses mensuelle de",
"you_will_be_downgraded_to_the_community_edition_on_date": "Vous serez rétrogradé à l'édition communautaire le {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Vous serez rétrogradé à l'édition communautaire le {date}."
},
"emails": {
"accept": "Accepter",
@@ -956,32 +954,19 @@
"enterprise_features": "Fonctionnalités d'entreprise",
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenez une licence Entreprise pour accéder à toutes les fonctionnalités.",
"keep_full_control_over_your_data_privacy_and_security": "Gardez un contrôle total sur la confidentialité et la sécurité de vos données.",
"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_active": "Active",
"license_status_description": "Statut de votre licence entreprise.",
"license_status_expired": "Expirée",
"license_status_invalid": "Licence invalide",
"license_status_unreachable": "Inaccessible",
"license_unreachable_grace_period": "Le serveur de licence est injoignable. Vos fonctionnalités entreprise restent actives pendant une période de grâce de 3 jours se terminant le {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Aucun appel nécessaire, aucune obligation : Demandez une licence d'essai gratuite de 30 jours pour tester toutes les fonctionnalités en remplissant ce formulaire :",
"no_credit_card_no_sales_call_just_test_it": "Aucune carte de crédit. Aucun appel de vente. Testez-le simplement :)",
"on_request": "Sur demande",
"organization_roles": "Rôles d'organisation (Administrateur, Éditeur, Développeur, etc.)",
"questions_please_reach_out_to": "Des questions ? Veuillez contacter",
"recheck_license": "Revérifier la licence",
"recheck_license_failed": "La vérification de la licence a échoué. Le serveur de licences est peut-être inaccessible.",
"recheck_license_invalid": "La clé de licence est invalide. Veuillez vérifier votre ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Vérification de la licence réussie",
"recheck_license_unreachable": "Le serveur de licences est inaccessible. Veuillez réessayer plus tard.",
"rechecking": "Revérification en cours...",
"request_30_day_trial_license": "Demander une licence d'essai de 30 jours",
"saml_sso": "SAML SSO",
"service_level_agreement": "Accord de niveau de service",
"soc2_hipaa_iso_27001_compliance_check": "Vérification de conformité SOC2, HIPAA, ISO 27001",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Équipes et Rôles d'Accès (Lire, Lire et Écrire, Gérer)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Débloquez tout le potentiel de Formbricks. Gratuit pendant 30 jours."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Débloquez tout le potentiel de Formbricks. Gratuit pendant 30 jours.",
"your_enterprise_license_is_active_all_features_unlocked": "Votre licence d'entreprise est active. Toutes les fonctionnalités sont déverrouillées."
},
"general": {
"bulk_invite_warning_description": "Dans le plan gratuit, tous les membres de l'organisation se voient toujours attribuer le rôle \"Owner\".",
@@ -1005,7 +990,7 @@
"from_your_organization": "de votre organisation",
"invitation_sent_once_more": "Invitation envoyée une fois de plus.",
"invite_deleted_successfully": "Invitation supprimée avec succès",
"invite_expires_on": "L'invitation expire le {date}",
"invited_on": "Invité le {date}",
"invites_failed": "Invitations échouées",
"leave_organization": "Quitter l'organisation",
"leave_organization_description": "Vous quitterez cette organisation et perdrez l'accès à toutes les enquêtes et réponses. Vous ne pourrez revenir que si vous êtes de nouveau invité.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Veuillez remplir tous les champs pour ajouter un nouvel espace de travail.",
"read": "Lire",
"read_write": "Lire et Écrire",
"select_member": "Sélectionner membre",
"select_workspace": "Sélectionner un espace de travail",
"team_admin": "Administrateur d'équipe",
"team_created_successfully": "Équipe créée avec succès.",
"team_deleted_successfully": "Équipe supprimée avec succès.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Ajouter un espace réservé à afficher s'il n'y a pas de valeur à rappeler.",
"add_hidden_field_id": "Ajouter un champ caché ID",
"add_highlight_border": "Ajouter une bordure de surlignage",
"add_highlight_border_description": "Ajoutez une bordure extérieure à votre carte d'enquête.",
"add_logic": "Ajouter de la logique",
"add_none_of_the_above": "Ajouter \"Aucun des éléments ci-dessus\"",
"add_option": "Ajouter une option",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
"adjust_the_theme_in_the": "Ajustez le thème dans le",
"all_other_answers_will_continue_to": "Toutes les autres réponses continueront à",
"allow_file_type": "Autoriser le type de fichier",
"allow_multi_select": "Autoriser la sélection multiple",
"allow_multiple_files": "Autoriser plusieurs fichiers",
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plusieurs images",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Bloc dupliqué.",
"bold": "Gras",
"brand_color": "Couleur de marque",
"brand_color_description": "Appliqué aux boutons, liens et éléments mis en évidence.",
"brightness": "Luminosité",
"bulk_edit": "Modification en masse",
"bulk_edit_description": "Modifiez toutes les options ci-dessous, une par ligne. Les lignes vides seront ignorées et les doublons supprimés.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Capturer une nouvelle action",
"card_arrangement_for_survey_type_derived": "Disposition des cartes pour les enquêtes {surveyTypeDerived}",
"card_background_color": "Couleur de fond de la carte",
"card_background_color_description": "Remplit la zone de la carte d'enquête.",
"card_border_color": "Couleur de la bordure de la carte",
"card_border_color_description": "Délimite la carte d'enquête.",
"card_styling": "Style de carte",
"casual": "Décontracté",
"caution_edit_duplicate": "Dupliquer et modifier",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Les réponses anciennes et nouvelles se mélangent, ce qui peut entraîner des résumés de données trompeurs.",
"caution_recommendation": "Cela peut entraîner des incohérences de données dans le résumé du sondage. Nous recommandons de dupliquer le sondage à la place.",
"caution_text": "Les changements entraîneront des incohérences.",
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
"change_anyway": "Changer de toute façon",
"change_background": "Changer l'arrière-plan",
"change_question_type": "Changer le type de question",
"change_survey_type": "Le changement de type de sondage affecte l'accès existant",
"change_the_background_color_of_the_card": "Changez la couleur de fond de la carte.",
"change_the_background_color_of_the_input_fields": "Vous pouvez modifier la couleur d'arrière-plan des champs de saisie.",
"change_the_background_to_a_color_image_or_animation": "Changez l'arrière-plan en une couleur, une image ou une animation.",
"change_the_border_color_of_the_card": "Changez la couleur de la bordure de la carte.",
"change_the_border_color_of_the_input_fields": "Vous pouvez modifier la couleur de la bordure des champs de saisie.",
"change_the_border_radius_of_the_card_and_the_inputs": "Vous pouvez arrondir la bordure des encadrés et des champs de saisie.",
"change_the_brand_color_of_the_survey": "Vous pouvez modifier la couleur dominante d'une enquête.",
"change_the_placement_of_this_survey": "Changez le placement de cette enquête.",
"change_the_question_color_of_the_survey": "Vous pouvez modifier la couleur des questions d'une enquête.",
"changes_saved": "Modifications enregistrées.",
"changing_survey_type_will_remove_existing_distribution_channels": "Le changement du type de sondage affectera la façon dont il peut être partagé. Si les répondants ont déjà des liens d'accès pour le type actuel, ils peuvent perdre l'accès après le changement.",
"character_limit_toggle_description": "Limitez la longueur des réponses.",
"character_limit_toggle_title": "Ajouter des limites de caractères",
"checkbox_label": "Étiquette de case à cocher",
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
"choose_the_first_question_on_your_block": "Choisissez la première question de votre bloc",
@@ -1263,6 +1259,7 @@
"contact_fields": "Champs de contact",
"contains": "Contient",
"continue_to_settings": "Continuer vers les paramètres",
"control_which_file_types_can_be_uploaded": "Contrôlez quels types de fichiers peuvent être téléchargés.",
"convert_to_multiple_choice": "Convertir en choix multiples",
"convert_to_single_choice": "Convertir en choix unique",
"country": "Pays",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Supprimer quand même",
"delete_block": "Supprimer le bloc",
"delete_choice": "Supprimer l'option",
"disable_the_visibility_of_survey_progress": "Désactiver la visibilité de la progression du sondage.",
"display_an_estimate_of_completion_time_for_survey": "Afficher une estimation du temps de complétion pour l'enquête.",
"display_number_of_responses_for_survey": "Afficher le nombre de réponses pour l'enquête",
"display_type": "Type d'affichage",
"divide": "Diviser /",
"does_not_contain": "Ne contient pas",
"does_not_end_with": "Ne se termine pas par",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "n'inclut pas tout",
"does_not_include_one_of": "n'inclut pas un de",
"does_not_start_with": "Ne commence pas par",
"dropdown": "Menu déroulant",
"duplicate_block": "Dupliquer le bloc",
"duplicate_question": "Dupliquer la question",
"edit_link": "Modifier le lien",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "Cacher la barre de progression",
"hide_question_settings": "Masquer les paramètres de la question",
"hostname": "Nom d'hôte",
"if_you_need_more_please": "Si vous avez besoin de plus, veuillez",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}",
"if_you_need_more_please": "Si vous en avez besoin de plus, s'il vous plaît",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuer à afficher à chaque déclenchement jusqu'à ce qu'une réponse soit soumise.",
"ignore_global_waiting_time": "Ignorer la période de refroidissement",
"ignore_global_waiting_time_description": "Cette enquête peut s'afficher chaque fois que ses conditions sont remplies, même si une autre enquête a été affichée récemment.",
@@ -1391,9 +1386,7 @@
"initial_value": "Valeur initiale",
"inner_text": "Texte interne",
"input_border_color": "Couleur de la bordure des champs de saisie",
"input_border_color_description": "Délimite les champs de texte et les zones de texte.",
"input_color": "Couleur d'arrière-plan des champs de saisie",
"input_color_description": "Remplit l'intérieur des champs de texte.",
"insert_link": "Insérer un lien",
"invalid_targeting": "Ciblage invalide : Veuillez vérifier vos filtres d'audience",
"invalid_video_url_warning": "Merci d'entrer une URL YouTube, Vimeo ou Loom valide. Les autres plateformes vidéo ne sont pas encore supportées.",
@@ -1420,10 +1413,10 @@
"key": "Clé",
"last_name": "Nom de famille",
"let_people_upload_up_to_25_files_at_the_same_time": "Permettre aux utilisateurs de télécharger jusqu'à 25 fichiers en même temps.",
"limit_the_maximum_file_size": "Limiter la taille maximale des fichiers pour les téléversements.",
"limit_upload_file_size_to": "Limiter la taille de téléversement des fichiers à",
"limit_file_types": "Limiter les types de fichiers",
"limit_the_maximum_file_size": "Limiter la taille maximale du fichier",
"limit_upload_file_size_to": "Limiter la taille des fichiers téléchargés à",
"link_survey_description": "Partagez un lien vers une page d'enquête ou intégrez-le dans une page web ou un e-mail.",
"list": "Liste",
"load_segment": "Segment de chargement",
"logic_error_warning": "Changer causera des erreurs logiques",
"logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.",
@@ -1435,7 +1428,7 @@
"matrix_all_fields": "Tous les champs",
"matrix_rows": "Lignes",
"max_file_size": "Taille maximale du fichier",
"max_file_size_limit_is": "La limite de taille maximale du fichier est",
"max_file_size_limit_is": "La taille maximale du fichier est",
"move_question_to_block": "Déplacer la question vers le bloc",
"multiply": "Multiplier *",
"needed_for_self_hosted_cal_com_instance": "Nécessaire pour une instance Cal.com auto-hébergée",
@@ -1467,6 +1460,7 @@
"picture_idx": "Image {idx}",
"pin_can_only_contain_numbers": "Le code PIN ne peut contenir que des chiffres.",
"pin_must_be_a_four_digit_number": "Le code PIN doit être un numéro à quatre chiffres.",
"please_enter_a_file_extension": "Veuillez entrer une extension de fichier.",
"please_enter_a_valid_url": "Veuillez entrer une URL valide (par exemple, https://example.com)",
"please_set_a_survey_trigger": "Veuillez définir un déclencheur d'enquête.",
"please_specify": "Veuillez préciser",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Seules les personnes ayant le code PIN peuvent accéder à l'enquête.",
"publish": "Publier",
"question": "Question",
"question_color": "Couleur des questions",
"question_deleted": "Question supprimée.",
"question_duplicated": "Question dupliquée.",
"question_id_updated": "ID de la question mis à jour",
"question_used_in_logic_warning_text": "Des éléments de ce bloc sont utilisés dans une règle logique, êtes-vous sûr de vouloir le supprimer?",
"question_used_in_logic_warning_title": "Incohérence de logique",
"question_used_in_quota": "Cette question est utilisée dans le quota “{quotaName}”",
"question_used_in_logic": "Cette question est utilisée dans la logique de la question '{'questionIndex'}'.",
"question_used_in_quota": "Cette question est utilisée dans le quota \"{quotaName}\"",
"question_used_in_recall": "Cette question est rappelée dans la question {questionIndex}.",
"question_used_in_recall_ending_card": "Cette question est rappelée dans la carte de fin.",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Limites de réponse, redirections et plus.",
"response_options": "Options de réponse",
"roundness": "Rondeur",
"roundness_description": "Contrôle l'arrondi des coins de la carte.",
"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.",
"rows": "Lignes",
"save_and_close": "Enregistrer et fermer",
@@ -1546,7 +1539,6 @@
"search_for_images": "Rechercher des images",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "Les secondes après le déclenchement, l'enquête sera fermée si aucune réponse n'est donnée.",
"seconds_before_showing_the_survey": "secondes avant de montrer l'enquête.",
"select_field": "Sélectionner un champ",
"select_or_type_value": "Sélectionnez ou saisissez une valeur",
"select_ordering": "Choisir l'ordre",
"select_saved_action": "Sélectionner une action enregistrée",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Style défini sur les styles du thème",
"subheading": "Sous-titre",
"subtract": "Soustraire -",
"suggest_colors": "Suggérer des couleurs",
"survey_completed_heading": "Enquête terminée",
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
"survey_display_settings": "Paramètres d'affichage de l'enquête",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Afficher une seule fois, même si la personne ne répond pas.",
"then": "Alors",
"this_action_will_remove_all_the_translations_from_this_survey": "Cette action supprimera toutes les traductions de cette enquête.",
"this_extension_is_already_added": "Cette extension est déjà ajoutée.",
"this_file_type_is_not_supported": "Ce type de fichier n'est pas pris en charge.",
"three_points": "3 points",
"times": "fois",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Pour maintenir la cohérence du placement sur tous les sondages, vous pouvez",
@@ -1613,51 +1608,8 @@
"upper_label": "Étiquette supérieure",
"url_filters": "Filtres d'URL",
"url_not_supported": "URL non supportée",
"validation": {
"add_validation_rule": "Ajouter une règle de validation",
"answer_all_rows": "Répondre à toutes les lignes",
"characters": "Caractères",
"contains": "Contient",
"delete_validation_rule": "Supprimer la règle de validation",
"does_not_contain": "Ne contient pas",
"email": "Est un e-mail valide",
"end_date": "Date de fin",
"file_extension_is": "L'extension de fichier est",
"file_extension_is_not": "L'extension de fichier n'est pas",
"is": "Est",
"is_between": "Est entre",
"is_earlier_than": "Est antérieur à",
"is_greater_than": "Est supérieur à",
"is_later_than": "Est postérieur à",
"is_less_than": "Est inférieur à",
"is_not": "N'est pas",
"is_not_between": "N'est pas entre",
"kb": "Ko",
"max_length": "Au maximum",
"max_selections": "Au maximum",
"max_value": "Au maximum",
"mb": "Mo",
"min_length": "Au moins",
"min_selections": "Au moins",
"min_value": "Au moins",
"minimum_options_ranked": "Nombre minimum d'options classées",
"minimum_rows_answered": "Nombre minimum de lignes répondues",
"options_selected": "Options sélectionnées",
"pattern": "Correspond au modèle d'expression régulière",
"phone": "Est un numéro de téléphone valide",
"rank_all_options": "Classer toutes les options",
"select_file_extensions": "Sélectionner les extensions de fichier...",
"select_option": "Sélectionner une option",
"start_date": "Date de début",
"url": "Est une URL valide"
},
"validation_logic_and": "Toutes sont vraies",
"validation_logic_or": "au moins une est vraie",
"validation_rules": "Règles de validation",
"validation_rules_description": "Accepter uniquement les réponses qui répondent aux critères suivants",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable {variableName} est utilisée dans le quota {quotaName}",
"variable_name_conflicts_with_hidden_field": "Le nom de la variable est en conflit avec un ID de champ masqué existant.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" est utilisée dans le quota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "Le nom de la variable est déjà pris, veuillez en choisir un autre.",
"variable_name_must_start_with_a_letter": "Le nom de la variable doit commencer par une lettre.",
"variable_used_in_recall": "La variable \"{variable}\" est rappelée dans la question {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Ajouter une couleur d'arrière-plan",
"add_background_color_description": "Ajoutez une couleur d'arrière-plan au conteneur du logo.",
"advanced_styling_field_border_radius": "Rayon de bordure",
"advanced_styling_field_button_bg": "Arrière-plan du bouton",
"advanced_styling_field_button_bg_description": "Remplit le bouton Suivant / Envoyer.",
"advanced_styling_field_button_border_radius_description": "Arrondit les coins du bouton.",
"advanced_styling_field_button_font_size_description": "Ajuste la taille du texte du libellé du bouton.",
"advanced_styling_field_button_font_weight_description": "Rend le texte du bouton plus léger ou plus gras.",
"advanced_styling_field_button_height_description": "Contrôle la hauteur du bouton.",
"advanced_styling_field_button_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
"advanced_styling_field_button_padding_y_description": "Ajoute de l'espace en haut et en bas.",
"advanced_styling_field_button_text": "Texte du bouton",
"advanced_styling_field_button_text_description": "Colore le libellé à l'intérieur des boutons.",
"advanced_styling_field_description_color": "Couleur de la description",
"advanced_styling_field_description_color_description": "Colore le texte sous chaque titre.",
"advanced_styling_field_description_size": "Taille de police de la description",
"advanced_styling_field_description_size_description": "Ajuste la taille du texte de description.",
"advanced_styling_field_description_weight": "Graisse de police de la description",
"advanced_styling_field_description_weight_description": "Rend le texte de description plus léger ou plus gras.",
"advanced_styling_field_font_size": "Taille de police",
"advanced_styling_field_font_weight": "Graisse de police",
"advanced_styling_field_headline_color": "Couleur du titre",
"advanced_styling_field_headline_color_description": "Colore le texte principal de la question.",
"advanced_styling_field_headline_size": "Taille de police du titre",
"advanced_styling_field_headline_size_description": "Ajuste la taille du texte du titre.",
"advanced_styling_field_headline_weight": "Graisse de police du titre",
"advanced_styling_field_headline_weight_description": "Rend le texte du titre plus léger ou plus gras.",
"advanced_styling_field_height": "Hauteur",
"advanced_styling_field_indicator_bg": "Arrière-plan de l'indicateur",
"advanced_styling_field_indicator_bg_description": "Colore la partie remplie de la barre.",
"advanced_styling_field_input_border_radius_description": "Arrondit les coins du champ de saisie.",
"advanced_styling_field_input_font_size_description": "Ajuste la taille du texte saisi dans les champs.",
"advanced_styling_field_input_height_description": "Contrôle la hauteur du champ de saisie.",
"advanced_styling_field_input_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
"advanced_styling_field_input_padding_y_description": "Ajoute de l'espace en haut et en bas.",
"advanced_styling_field_input_placeholder_opacity_description": "Atténue le texte d'indication du placeholder.",
"advanced_styling_field_input_shadow_description": "Ajoute une ombre portée autour des champs de saisie.",
"advanced_styling_field_input_text": "Texte de saisie",
"advanced_styling_field_input_text_description": "Colore le texte saisi dans les champs.",
"advanced_styling_field_option_bg": "Arrière-plan",
"advanced_styling_field_option_bg_description": "Remplit les éléments d'option.",
"advanced_styling_field_option_border_radius_description": "Arrondit les coins des options.",
"advanced_styling_field_option_font_size_description": "Ajuste la taille du texte des libellés d'option.",
"advanced_styling_field_option_label": "Couleur de l'étiquette",
"advanced_styling_field_option_label_description": "Colore le texte des libellés d'option.",
"advanced_styling_field_option_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
"advanced_styling_field_option_padding_y_description": "Ajoute de l'espace en haut et en bas.",
"advanced_styling_field_padding_x": "Marge intérieure X",
"advanced_styling_field_padding_y": "Marge intérieure Y",
"advanced_styling_field_placeholder_opacity": "Opacité du placeholder",
"advanced_styling_field_shadow": "Ombre",
"advanced_styling_field_track_bg": "Arrière-plan de la piste",
"advanced_styling_field_track_bg_description": "Colore la partie non remplie de la barre.",
"advanced_styling_field_track_height": "Hauteur de la piste",
"advanced_styling_field_track_height_description": "Contrôle l'épaisseur de la barre de progression.",
"advanced_styling_field_upper_label_color": "Couleur de l'étiquette du titre",
"advanced_styling_field_upper_label_color_description": "Colore le petit libellé au-dessus des champs de saisie.",
"advanced_styling_field_upper_label_size": "Taille de police de l'étiquette du titre",
"advanced_styling_field_upper_label_size_description": "Ajuste la taille du petit libellé au-dessus des champs de saisie.",
"advanced_styling_field_upper_label_weight": "Graisse de police de l'étiquette du titre",
"advanced_styling_field_upper_label_weight_description": "Rend le libellé plus léger ou plus gras.",
"advanced_styling_section_buttons": "Boutons",
"advanced_styling_section_headlines": "Titres et descriptions",
"advanced_styling_section_inputs": "Champs de saisie",
"advanced_styling_section_options": "Options (boutons radio/cases à cocher)",
"app_survey_placement": "Placement du sondage d'application",
"app_survey_placement_settings_description": "Modifiez l'emplacement où les sondages seront affichés dans votre application web ou site web.",
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
"email_customization": "Personnalisation des e-mails",
"email_customization_description": "Modifiez l'apparence des e-mails que Formbricks envoie en votre nom.",
"enable_custom_styling": "Activer le style personnalisé",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Le logo Formbricks est masqué.",
"formbricks_branding_settings_description": "Nous apprécions votre soutien mais comprenons si vous choisissez de le désactiver.",
"formbricks_branding_shown": "Le logo Formbricks est affiché.",
"generate_theme_btn": "Générer",
"generate_theme_confirmation": "Souhaitez-vous générer un thème de couleurs assorti basé sur votre couleur de marque? Cela écrasera vos paramètres de couleur actuels.",
"generate_theme_header": "Générer un thème de couleurs?",
"logo_removed_successfully": "Logo supprimé avec succès",
"logo_settings_description": "Téléchargez le logo de votre entreprise pour personnaliser les enquêtes et les aperçus de liens.",
"logo_updated_successfully": "Logo mis à jour avec succès",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Afficher le logo Formbricks dans les enquêtes {type}",
"show_powered_by_formbricks": "Afficher la signature « Propulsé par Formbricks »",
"styling_updated_successfully": "Style mis à jour avec succès",
"suggest_colors": "Suggérer des couleurs",
"theme": "Thème",
"theme_settings_description": "Créez un thème de style pour toutes les enquêtes. Vous pouvez activer un style personnalisé pour chaque enquête."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Oui, tenez-moi au courant.",
"preview_survey_question_2_choice_2_label": "Non, merci !",
"preview_survey_question_2_headline": "Souhaitez-vous être informé ?",
"preview_survey_question_2_subheader": "Ceci est un exemple de description.",
"preview_survey_welcome_card_headline": "Bienvenue !",
"prioritize_features_description": "Identifiez les fonctionnalités dont vos utilisateurs ont le plus et le moins besoin.",
"prioritize_features_name": "Prioriser les fonctionnalités",
File diff suppressed because it is too large Load Diff
+36 -151
View File
@@ -243,6 +243,7 @@
"imprint": "企業情報",
"in_progress": "進行中",
"inactive_surveys": "非アクティブなフォーム",
"input_type": "入力タイプ",
"integration": "連携",
"integrations": "連携",
"invalid_date": "無効な日付です",
@@ -254,7 +255,6 @@
"label": "ラベル",
"language": "言語",
"learn_more": "詳細を見る",
"license_expired": "License Expired",
"light_overlay": "明るいオーバーレイ",
"limits_reached": "上限に達しました",
"link": "リンク",
@@ -267,11 +267,13 @@
"look_and_feel": "デザイン",
"manage": "管理",
"marketing": "マーケティング",
"maximum": "最大",
"member": "メンバー",
"members": "メンバー",
"members_and_teams": "メンバー&チーム",
"membership_not_found": "メンバーシップが見つかりません",
"metadata": "メタデータ",
"minimum": "最小",
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
"mobile_overlay_title": "おっと、 小さな 画面 が 検出されました!",
@@ -285,7 +287,6 @@
"no_background_image_found": "背景画像が見つかりません。",
"no_code": "ノーコード",
"no_files_uploaded": "ファイルがアップロードされていません",
"no_overlay": "オーバーレイなし",
"no_quotas_found": "クォータが見つかりません",
"no_result_found": "結果が見つかりません",
"no_results": "結果なし",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "組織のチームが見つかりません",
"other": "その他",
"others": "その他",
"overlay_color": "オーバーレイの色",
"overview": "概要",
"password": "パスワード",
"paused": "一時停止",
@@ -326,7 +326,7 @@
"placeholder": "プレースホルダー",
"please_select_at_least_one_survey": "少なくとも1つのフォームを選択してください",
"please_select_at_least_one_trigger": "少なくとも1つのトリガーを選択してください",
"please_upgrade_your_plan": "プランをアップグレードしてください",
"please_upgrade_your_plan": "プランをアップグレードしてください",
"preview": "プレビュー",
"preview_survey": "フォームをプレビュー",
"privacy": "プライバシーポリシー",
@@ -352,7 +352,6 @@
"request_trial_license": "トライアルライセンスをリクエスト",
"reset_to_default": "デフォルトにリセット",
"response": "回答",
"response_id": "回答ID",
"responses": "回答",
"restart": "再開",
"role": "役割",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "ワークスペースの上限である{projectLimit}件に達しました。",
"you_have_reached_your_monthly_miu_limit_of": "月間MIU(月間アクティブユーザー)の上限に達しました",
"you_have_reached_your_monthly_response_limit_of": "月間回答数の上限に達しました",
"you_will_be_downgraded_to_the_community_edition_on_date": "コミュニティ版へのダウングレードは {date} に行われます。",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "コミュニティ版へのダウングレードは {date} に行われます。"
},
"emails": {
"accept": "承認",
@@ -956,32 +954,19 @@
"enterprise_features": "エンタープライズ機能",
"get_an_enterprise_license_to_get_access_to_all_features": "すべての機能にアクセスするには、エンタープライズライセンスを取得してください。",
"keep_full_control_over_your_data_privacy_and_security": "データのプライバシーとセキュリティを完全に制御できます。",
"license_invalid_description": "ENTERPRISE_LICENSE_KEY環境変数のライセンスキーが無効です。入力ミスがないか確認するか、新しいキーをリクエストしてください。",
"license_status": "ライセンスステータス",
"license_status_active": "有効",
"license_status_description": "エンタープライズライセンスのステータス。",
"license_status_expired": "期限切れ",
"license_status_invalid": "無効なライセンス",
"license_status_unreachable": "接続不可",
"license_unreachable_grace_period": "ライセンスサーバーに接続できません。エンタープライズ機能は{gracePeriodEnd}までの3日間の猶予期間中は引き続き利用できます。",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "電話不要、制約なし: このフォームに記入して、すべての機能をテストするための無料の30日間トライアルライセンスをリクエストしてください:",
"no_credit_card_no_sales_call_just_test_it": "クレジットカード不要。営業電話もありません。ただテストしてください :)",
"on_request": "リクエストに応じて",
"organization_roles": "組織ロール(管理者、編集者、開発者など)",
"questions_please_reach_out_to": "質問はありますか?こちらまでお問い合わせください",
"recheck_license": "ライセンスを再確認",
"recheck_license_failed": "ライセンスの確認に失敗しました。ライセンスサーバーに接続できない可能性があります。",
"recheck_license_invalid": "ライセンスキーが無効です。ENTERPRISE_LICENSE_KEYを確認してください。",
"recheck_license_success": "ライセンスの確認に成功しました",
"recheck_license_unreachable": "ライセンスサーバーに接続できません。後ほど再度お試しください。",
"rechecking": "再確認中...",
"request_30_day_trial_license": "30日間トライアルライセンスをリクエスト",
"saml_sso": "SAML SSO",
"service_level_agreement": "サービスレベル契約",
"soc2_hipaa_iso_27001_compliance_check": "SOC2、HIPAA、ISO 27001準拠チェック",
"sso": "SSOGoogle、Microsoft、OpenID Connect",
"teams": "チーム&アクセスロール(読み取り、読み書き、管理)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Formbricksの全機能をアンロック。30日間無料。"
"unlock_the_full_power_of_formbricks_free_for_30_days": "Formbricksの全機能をアンロック。30日間無料。",
"your_enterprise_license_is_active_all_features_unlocked": "あなたのエンタープライズライセンスは有効です。すべての機能がアンロックされました。"
},
"general": {
"bulk_invite_warning_description": "無料プランでは、すべての組織メンバーに常に「オーナー」ロールが割り当てられます。",
@@ -1005,7 +990,7 @@
"from_your_organization": "あなたの組織から",
"invitation_sent_once_more": "招待状を再度送信しました。",
"invite_deleted_successfully": "招待を正常に削除しました",
"invite_expires_on": "招待は{date}に期限切れ",
"invited_on": "{date}に招待",
"invites_failed": "招待に失敗しました",
"leave_organization": "組織を離れる",
"leave_organization_description": "この組織を離れ、すべてのフォームと回答へのアクセス権を失います。再度招待された場合にのみ再参加できます。",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "新しいワークスペースを追加するには、すべてのフィールドを入力してください。",
"read": "読み取り",
"read_write": "読み書き",
"select_member": "メンバーを選択",
"select_workspace": "ワークスペースを選択",
"team_admin": "チーム管理者",
"team_created_successfully": "チームを正常に作成しました。",
"team_deleted_successfully": "チームを正常に削除しました。",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "質問がスキップされた場合に表示するプレースホルダーを追加:",
"add_hidden_field_id": "非表示フィールドIDを追加",
"add_highlight_border": "ハイライトボーダーを追加",
"add_highlight_border_description": "フォームカードに外側のボーダーを追加します。",
"add_logic": "ロジックを追加",
"add_none_of_the_above": "\"いずれも該当しません\" を追加",
"add_option": "オプションを追加",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "フォームがクローズしたときに訪問者が見るメッセージを変更します。",
"adjust_the_theme_in_the": "テーマを",
"all_other_answers_will_continue_to": "他のすべての回答は引き続き",
"allow_file_type": "ファイルタイプを許可",
"allow_multi_select": "複数選択を許可",
"allow_multiple_files": "複数のファイルを許可",
"allow_users_to_select_more_than_one_image": "ユーザーが複数の画像を選択できるようにする",
@@ -1205,7 +1194,6 @@
"block_duplicated": "ブロックが複製されました。",
"bold": "太字",
"brand_color": "ブランドカラー",
"brand_color_description": "ボタン、リンク、ハイライトに適用されます。",
"brightness": "明るさ",
"bulk_edit": "一括編集",
"bulk_edit_description": "以下のオプションを1行ずつ編集してください。空の行はスキップされ、重複は削除されます。",
@@ -1223,9 +1211,7 @@
"capture_new_action": "新しいアクションをキャプチャ",
"card_arrangement_for_survey_type_derived": "{surveyTypeDerived} フォームのカード配置",
"card_background_color": "カードの背景色",
"card_background_color_description": "フォームカードエリアを塗りつぶします。",
"card_border_color": "カードの枠線の色",
"card_border_color_description": "フォームカードの輪郭を描きます。",
"card_styling": "カードのスタイル設定",
"casual": "カジュアル",
"caution_edit_duplicate": "複製して編集",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "古い回答と新しい回答が混ざり、データの概要が誤解を招く可能性があります。",
"caution_recommendation": "これにより、フォームの概要にデータの不整合が生じる可能性があります。代わりにフォームを複製することをお勧めします。",
"caution_text": "変更は不整合を引き起こします",
"centered_modal_overlay_color": "中央モーダルのオーバーレイ色",
"change_anyway": "とにかく変更",
"change_background": "背景を変更",
"change_question_type": "質問の種類を変更",
"change_survey_type": "フォームの種類を変更すると、既存のアクセスに影響します",
"change_the_background_color_of_the_card": "カードの背景色を変更します。",
"change_the_background_color_of_the_input_fields": "入力フィールドの背景色を変更します。",
"change_the_background_to_a_color_image_or_animation": "背景を色、画像、またはアニメーションに変更します。",
"change_the_border_color_of_the_card": "カードの枠線の色を変更します。",
"change_the_border_color_of_the_input_fields": "入力フィールドの枠線の色を変更します。",
"change_the_border_radius_of_the_card_and_the_inputs": "カードと入力の角丸を変更します。",
"change_the_brand_color_of_the_survey": "フォームのブランドカラーを変更します。",
"change_the_placement_of_this_survey": "このフォームの配置を変更します。",
"change_the_question_color_of_the_survey": "フォームの質問の色を変更します。",
"changes_saved": "変更を保存しました。",
"changing_survey_type_will_remove_existing_distribution_channels": "フォームの種類を変更すると、共有方法に影響します。回答者が現在のタイプのアクセスリンクをすでに持っている場合、切り替え後にアクセスを失う可能性があります。",
"character_limit_toggle_description": "回答の長さの上限・下限を設定します。",
"character_limit_toggle_title": "文字数制限を追加",
"checkbox_label": "チェックボックスのラベル",
"choose_the_actions_which_trigger_the_survey": "フォームをトリガーするアクションを選択してください。",
"choose_the_first_question_on_your_block": "ブロックの最初の質問を選択してください",
@@ -1263,6 +1259,7 @@
"contact_fields": "連絡先フィールド",
"contains": "を含む",
"continue_to_settings": "設定に進む",
"control_which_file_types_can_be_uploaded": "アップロードできるファイルの種類を制御します。",
"convert_to_multiple_choice": "複数選択に変換",
"convert_to_single_choice": "単一選択に変換",
"country": "国",
@@ -1275,13 +1272,11 @@
"darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。",
"date_format": "日付形式",
"days_before_showing_this_survey_again": "最後に表示されたアンケートとこのアンケートを表示するまでに、この日数以上の期間を空ける必要があります。",
"delete_anyways": "削除する",
"delete_block": "ブロックを削除",
"delete_choice": "選択肢を削除",
"disable_the_visibility_of_survey_progress": "フォームの進捗状況の表示を無効にする。",
"display_an_estimate_of_completion_time_for_survey": "フォームの完了時間の目安を表示",
"display_number_of_responses_for_survey": "フォームの回答数を表示",
"display_type": "表示タイプ",
"divide": "除算 /",
"does_not_contain": "を含まない",
"does_not_end_with": "で終わらない",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "のすべてを含まない",
"does_not_include_one_of": "のいずれも含まない",
"does_not_start_with": "で始まらない",
"dropdown": "ドロップダウン",
"duplicate_block": "ブロックを複製",
"duplicate_question": "質問を複製",
"edit_link": "編集 リンク",
@@ -1381,6 +1375,7 @@
"hide_progress_bar": "プログレスバーを非表示",
"hide_question_settings": "質問設定を非表示",
"hostname": "ホスト名",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか",
"if_you_need_more_please": "さらに必要な場合は、",
"if_you_really_want_that_answer_ask_until_you_get_it": "回答が提出されるまで、トリガーされるたびに表示し続けます。",
"ignore_global_waiting_time": "クールダウン期間を無視",
@@ -1391,9 +1386,7 @@
"initial_value": "初期値",
"inner_text": "内部テキスト",
"input_border_color": "入力の枠線の色",
"input_border_color_description": "テキスト入力とテキストエリアの輪郭を描きます。",
"input_color": "入力の色",
"input_color_description": "テキスト入力の内側を塗りつぶします。",
"insert_link": "リンク を 挿入",
"invalid_targeting": "無効なターゲティング: オーディエンスフィルターを確認してください",
"invalid_video_url_warning": "有効なYouTube、Vimeo、またはLoomのURLを入力してください。現在、他の動画ホスティングプロバイダーはサポートしていません。",
@@ -1420,10 +1413,10 @@
"key": "キー",
"last_name": "姓",
"let_people_upload_up_to_25_files_at_the_same_time": "一度に最大25個のファイルをアップロードできるようにする。",
"limit_the_maximum_file_size": "アップロードの最大ファイルサイズを制限します。",
"limit_upload_file_size_to": "アップロードファイルサイズの上限",
"limit_file_types": "ファイルタイプを制限",
"limit_the_maximum_file_size": "最大ファイルサイズを制限",
"limit_upload_file_size_to": "アップロードファイルサイズを以下に制限",
"link_survey_description": "フォームページへのリンクを共有するか、ウェブページやメールに埋め込みます。",
"list": "リスト",
"load_segment": "セグメントを読み込み",
"logic_error_warning": "変更するとロジックエラーが発生します",
"logic_error_warning_text": "質問の種類を変更すると、この質問のロジック条件が削除されます",
@@ -1467,6 +1460,7 @@
"picture_idx": "写真 {idx}",
"pin_can_only_contain_numbers": "PINは数字のみでなければなりません。",
"pin_must_be_a_four_digit_number": "PINは4桁の数字でなければなりません。",
"please_enter_a_file_extension": "ファイル拡張子を入力してください。",
"please_enter_a_valid_url": "有効な URL を入力してください (例:https://example.com)",
"please_set_a_survey_trigger": "フォームのトリガーを設定してください",
"please_specify": "具体的に指定してください",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "PINを持つユーザーのみがフォームにアクセスできます。",
"publish": "公開",
"question": "質問",
"question_color": "質問の色",
"question_deleted": "質問を削除しました。",
"question_duplicated": "質問を複製しました。",
"question_id_updated": "質問IDを更新しました",
"question_used_in_logic_warning_text": "このブロックの要素はロジックルールで使用されていますが、本当に削除しますか?",
"question_used_in_logic_warning_title": "ロジックの不整合",
"question_used_in_quota": "この質問は“{quotaName}”クォータで使用されています",
"question_used_in_logic": "この質問は質問 {questionIndex} のロジックで使用されています",
"question_used_in_quota": "この 質問 は \"{quotaName}\" の クオータ に使用されています",
"question_used_in_recall": "この 質問 は 質問 {questionIndex} で 呼び出され て います 。",
"question_used_in_recall_ending_card": "この 質問 は エンディング カード で 呼び出され て います。",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
"response_options": "回答オプション",
"roundness": "丸み",
"roundness_description": "カードの角の丸みを調整します。",
"row_used_in_logic_error": "この行は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
"rows": "行",
"save_and_close": "保存して閉じる",
@@ -1546,7 +1539,6 @@
"search_for_images": "画像を検索",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "トリガーから数秒後に回答がない場合、フォームは閉じられます",
"seconds_before_showing_the_survey": "秒後にフォームを表示します。",
"select_field": "フィールドを選択",
"select_or_type_value": "値を選択または入力",
"select_ordering": "順序を選択",
"select_saved_action": "保存済みのアクションを選択",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "スタイルをテーマのスタイルに設定しました",
"subheading": "サブ見出し",
"subtract": "減算 -",
"suggest_colors": "色を提案",
"survey_completed_heading": "フォームが完了しました",
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
"survey_display_settings": "フォーム表示設定",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "回答がなくても1回だけ表示します。",
"then": "その後",
"this_action_will_remove_all_the_translations_from_this_survey": "このアクションは、このフォームからすべての翻訳を削除します。",
"this_extension_is_already_added": "この拡張機能はすでに追加されています。",
"this_file_type_is_not_supported": "このファイルタイプはサポートされていません。",
"three_points": "3点",
"times": "回",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "すべてのフォームの配置を一貫させるために、",
@@ -1613,51 +1608,8 @@
"upper_label": "上限ラベル",
"url_filters": "URLフィルター",
"url_not_supported": "URLはサポートされていません",
"validation": {
"add_validation_rule": "検証ルールを追加",
"answer_all_rows": "すべての行に回答してください",
"characters": "文字数",
"contains": "を含む",
"delete_validation_rule": "検証ルールを削除",
"does_not_contain": "を含まない",
"email": "有効なメールアドレスである",
"end_date": "終了日",
"file_extension_is": "ファイル拡張子が次と一致",
"file_extension_is_not": "ファイル拡張子が次と一致しない",
"is": "である",
"is_between": "の間である",
"is_earlier_than": "より前である",
"is_greater_than": "より大きい",
"is_later_than": "より後である",
"is_less_than": "より小さい",
"is_not": "ではない",
"is_not_between": "の間ではない",
"kb": "KB",
"max_length": "最大",
"max_selections": "最大",
"max_value": "最大",
"mb": "MB",
"min_length": "最小",
"min_selections": "最小",
"min_value": "最小",
"minimum_options_ranked": "ランク付けされた最小オプション数",
"minimum_rows_answered": "回答された最小行数",
"options_selected": "選択されたオプション",
"pattern": "正規表現パターンに一致する",
"phone": "有効な電話番号である",
"rank_all_options": "すべてのオプションをランク付け",
"select_file_extensions": "ファイル拡張子を選択...",
"select_option": "オプションを選択",
"start_date": "開始日",
"url": "有効なURLである"
},
"validation_logic_and": "すべてが真である",
"validation_logic_or": "いずれかが真",
"validation_rules": "検証ルール",
"validation_rules_description": "次の条件を満たす回答のみを受け付ける",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "変数{variableName}”は“{quotaName}クォータで使用されています",
"variable_name_conflicts_with_hidden_field": "変数名が既存の非表示フィールドIDと競合しています。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "変数 \"{variableName}\" は \"{quotaName}\" クォータ で使用されています",
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
"variable_name_must_start_with_a_letter": "変数名はアルファベットで始まらなければなりません。",
"variable_used_in_recall": "変数 \"{variable}\" が 質問 {questionIndex} で 呼び出され て います 。",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "背景色を追加",
"add_background_color_description": "ロゴコンテナに背景色を追加します。",
"advanced_styling_field_border_radius": "境界線の丸み",
"advanced_styling_field_button_bg": "ボタンの背景",
"advanced_styling_field_button_bg_description": "次へ/送信ボタンを塗りつぶします。",
"advanced_styling_field_button_border_radius_description": "ボタンの角を丸めます。",
"advanced_styling_field_button_font_size_description": "ボタンラベルのテキストサイズを調整します。",
"advanced_styling_field_button_font_weight_description": "ボタンテキストを細くまたは太くします。",
"advanced_styling_field_button_height_description": "ボタンの高さを調整します。",
"advanced_styling_field_button_padding_x_description": "左右にスペースを追加します。",
"advanced_styling_field_button_padding_y_description": "上下にスペースを追加します。",
"advanced_styling_field_button_text": "ボタンのテキスト",
"advanced_styling_field_button_text_description": "ボタン内のラベルに色を付けます。",
"advanced_styling_field_description_color": "説明文の色",
"advanced_styling_field_description_color_description": "各見出しの下のテキストに色を付けます。",
"advanced_styling_field_description_size": "説明文のフォントサイズ",
"advanced_styling_field_description_size_description": "説明テキストのサイズを調整します。",
"advanced_styling_field_description_weight": "説明文のフォントの太さ",
"advanced_styling_field_description_weight_description": "説明テキストを細くまたは太くします。",
"advanced_styling_field_font_size": "フォントサイズ",
"advanced_styling_field_font_weight": "フォントの太さ",
"advanced_styling_field_headline_color": "見出しの色",
"advanced_styling_field_headline_color_description": "メインの質問テキストに色を付けます。",
"advanced_styling_field_headline_size": "見出しのフォントサイズ",
"advanced_styling_field_headline_size_description": "見出しテキストのサイズを調整します。",
"advanced_styling_field_headline_weight": "見出しのフォントの太さ",
"advanced_styling_field_headline_weight_description": "見出しテキストを細くまたは太くします。",
"advanced_styling_field_height": "高さ",
"advanced_styling_field_indicator_bg": "インジケーターの背景",
"advanced_styling_field_indicator_bg_description": "バーの塗りつぶし部分に色を付けます。",
"advanced_styling_field_input_border_radius_description": "入力フィールドの角を丸めます。",
"advanced_styling_field_input_font_size_description": "入力フィールド内の入力テキストのサイズを調整します。",
"advanced_styling_field_input_height_description": "入力フィールドの高さを調整します。",
"advanced_styling_field_input_padding_x_description": "左右にスペースを追加します。",
"advanced_styling_field_input_padding_y_description": "上下にスペースを追加します。",
"advanced_styling_field_input_placeholder_opacity_description": "プレースホルダーのヒントテキストを薄くします。",
"advanced_styling_field_input_shadow_description": "入力フィールドの周囲にドロップシャドウを追加します。",
"advanced_styling_field_input_text": "入力テキスト",
"advanced_styling_field_input_text_description": "入力フィールドに入力されたテキストの色を設定します。",
"advanced_styling_field_option_bg": "背景",
"advanced_styling_field_option_bg_description": "オプション項目を塗りつぶします。",
"advanced_styling_field_option_border_radius_description": "オプションの角を丸くします。",
"advanced_styling_field_option_font_size_description": "オプションラベルのテキストサイズを調整します。",
"advanced_styling_field_option_label": "ラベルの色",
"advanced_styling_field_option_label_description": "オプションラベルのテキストの色を設定します。",
"advanced_styling_field_option_padding_x_description": "左右にスペースを追加します。",
"advanced_styling_field_option_padding_y_description": "上下にスペースを追加します。",
"advanced_styling_field_padding_x": "パディングX",
"advanced_styling_field_padding_y": "パディングY",
"advanced_styling_field_placeholder_opacity": "プレースホルダーの不透明度",
"advanced_styling_field_shadow": "影",
"advanced_styling_field_track_bg": "トラックの背景",
"advanced_styling_field_track_bg_description": "バーの未入力部分の色を設定します。",
"advanced_styling_field_track_height": "トラックの高さ",
"advanced_styling_field_track_height_description": "プログレスバーの太さを調整します。",
"advanced_styling_field_upper_label_color": "見出しラベルの色",
"advanced_styling_field_upper_label_color_description": "入力フィールド上部の小さなラベルの色を設定します。",
"advanced_styling_field_upper_label_size": "見出しラベルのフォントサイズ",
"advanced_styling_field_upper_label_size_description": "入力フィールド上部の小さなラベルのサイズを調整します。",
"advanced_styling_field_upper_label_weight": "見出しラベルのフォントの太さ",
"advanced_styling_field_upper_label_weight_description": "ラベルを細くまたは太くします。",
"advanced_styling_section_buttons": "ボタン",
"advanced_styling_section_headlines": "見出しと説明",
"advanced_styling_section_inputs": "入力フィールド",
"advanced_styling_section_options": "選択肢(ラジオボタン/チェックボックス)",
"app_survey_placement": "アプリ内フォームの配置",
"app_survey_placement_settings_description": "Webアプリまたはウェブサイトでフォームを表示する場所を変更します。",
"centered_modal_overlay_color": "中央モーダルのオーバーレイ色",
"email_customization": "メールのカスタマイズ",
"email_customization_description": "Formbricksがあなたに代わって送信するメールの外観を変更します。",
"enable_custom_styling": "カスタムスタイルを有効化",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Formbricksブランディングは非表示です。",
"formbricks_branding_settings_description": "あなたのサポートに感謝していますが、オフにすることもご理解いただけます。",
"formbricks_branding_shown": "Formbricksブランディングは表示されています。",
"generate_theme_btn": "生成",
"generate_theme_confirmation": "ブランドカラーに基づいて、マッチするカラーテーマを生成しますか?現在のカラー設定は上書きされます。",
"generate_theme_header": "カラーテーマを生成しますか?",
"logo_removed_successfully": "ロゴを正常に削除しました",
"logo_settings_description": "会社のロゴをアップロードして、アンケートとリンクプレビューにブランディングを適用します。",
"logo_updated_successfully": "ロゴを正常に更新しました",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "{type}アンケートにFormbricksブランディングを表示",
"show_powered_by_formbricks": "「Powered by Formbricks」署名を表示",
"styling_updated_successfully": "スタイルを正常に更新しました",
"suggest_colors": "カラーを提案",
"theme": "テーマ",
"theme_settings_description": "すべてのアンケート用のスタイルテーマを作成します。各アンケートでカスタムスタイルを有効にできます。"
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "はい、最新情報を知りたいです。",
"preview_survey_question_2_choice_2_label": "いいえ、結構です!",
"preview_survey_question_2_headline": "最新情報を知りたいですか?",
"preview_survey_question_2_subheader": "これは説明の例です。",
"preview_survey_welcome_card_headline": "ようこそ!",
"prioritize_features_description": "ユーザーが最も必要とする機能と最も必要としない機能を特定する。",
"prioritize_features_name": "機能の優先順位付け",
+38 -153
View File
@@ -243,6 +243,7 @@
"imprint": "Afdruk",
"in_progress": "In uitvoering",
"inactive_surveys": "Inactieve enquêtes",
"input_type": "Invoertype",
"integration": "integratie",
"integrations": "Integraties",
"invalid_date": "Ongeldige datum",
@@ -254,7 +255,6 @@
"label": "Label",
"language": "Taal",
"learn_more": "Meer informatie",
"license_expired": "License Expired",
"light_overlay": "Lichte overlay",
"limits_reached": "Grenzen bereikt",
"link": "Link",
@@ -267,11 +267,13 @@
"look_and_feel": "Kijk & voel",
"manage": "Beheren",
"marketing": "Marketing",
"maximum": "Maximaal",
"member": "Lid",
"members": "Leden",
"members_and_teams": "Leden & teams",
"membership_not_found": "Lidmaatschap niet gevonden",
"metadata": "Metagegevens",
"minimum": "Minimum",
"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_title": "Oeps, klein scherm gedetecteerd!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Geen achtergrondafbeelding gevonden.",
"no_code": "Geen code",
"no_files_uploaded": "Er zijn geen bestanden geüpload",
"no_overlay": "Geen overlay",
"no_quotas_found": "Geen quota gevonden",
"no_result_found": "Geen resultaat gevonden",
"no_results": "Geen resultaten",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Organisatieteams niet gevonden",
"other": "Ander",
"others": "Anderen",
"overlay_color": "Overlaykleur",
"overview": "Overzicht",
"password": "Wachtwoord",
"paused": "Gepauzeerd",
@@ -326,7 +326,7 @@
"placeholder": "Tijdelijke aanduiding",
"please_select_at_least_one_survey": "Selecteer ten minste één enquête",
"please_select_at_least_one_trigger": "Selecteer ten minste één trigger",
"please_upgrade_your_plan": "Upgrade je abonnement",
"please_upgrade_your_plan": "Upgrade uw abonnement.",
"preview": "Voorbeeld",
"preview_survey": "Voorbeeld van enquête",
"privacy": "Privacybeleid",
@@ -352,7 +352,6 @@
"request_trial_license": "Proeflicentie aanvragen",
"reset_to_default": "Resetten naar standaard",
"response": "Antwoord",
"response_id": "Antwoord-ID",
"responses": "Reacties",
"restart": "Opnieuw opstarten",
"role": "Rol",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Je hebt je limiet van {projectLimit} werkruimtes bereikt.",
"you_have_reached_your_monthly_miu_limit_of": "U heeft uw maandelijkse MIU-limiet van bereikt",
"you_have_reached_your_monthly_response_limit_of": "U heeft uw maandelijkse responslimiet bereikt van",
"you_will_be_downgraded_to_the_community_edition_on_date": "Je wordt gedowngraded naar de Community-editie op {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Je wordt gedowngraded naar de Community-editie op {date}."
},
"emails": {
"accept": "Accepteren",
@@ -956,32 +954,19 @@
"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.",
"keep_full_control_over_your_data_privacy_and_security": "Houd de volledige controle over de privacy en beveiliging van uw gegevens.",
"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_active": "Actief",
"license_status_description": "Status van je enterprise-licentie.",
"license_status_expired": "Verlopen",
"license_status_invalid": "Ongeldige licentie",
"license_status_unreachable": "Niet bereikbaar",
"license_unreachable_grace_period": "Licentieserver is niet bereikbaar. Je enterprise functies blijven actief tijdens een respijtperiode van 3 dagen die eindigt op {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Geen telefoontje nodig, geen verplichtingen: vraag een gratis proeflicentie van 30 dagen aan om alle functies te testen door dit formulier in te vullen:",
"no_credit_card_no_sales_call_just_test_it": "Geen creditcard. Geen verkoopgesprek. Gewoon testen :)",
"on_request": "Op aanvraag",
"organization_roles": "Organisatierollen (beheerder, redacteur, ontwikkelaar, etc.)",
"questions_please_reach_out_to": "Vragen? Neem contact op met",
"recheck_license": "Licentie opnieuw controleren",
"recheck_license_failed": "Licentiecontrole mislukt. De licentieserver is mogelijk niet bereikbaar.",
"recheck_license_invalid": "De licentiesleutel is ongeldig. Controleer je ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Licentiecontrole geslaagd",
"recheck_license_unreachable": "Licentieserver is niet bereikbaar. Probeer het later opnieuw.",
"rechecking": "Opnieuw controleren...",
"request_30_day_trial_license": "Vraag een proeflicentie van 30 dagen aan",
"saml_sso": "SAML-SSO",
"service_level_agreement": "Service Level Overeenkomst",
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 Conformiteitscontrole",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Teams en toegangsrollen (lezen, lezen en schrijven, beheren)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Ontgrendel de volledige kracht van Formbricks. 30 dagen gratis."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Ontgrendel de volledige kracht van Formbricks. 30 dagen gratis.",
"your_enterprise_license_is_active_all_features_unlocked": "Uw Enterprise-licentie is actief. Alle functies ontgrendeld."
},
"general": {
"bulk_invite_warning_description": "Bij het gratis abonnement krijgen alle organisatieleden altijd de rol 'Eigenaar' toegewezen.",
@@ -1005,7 +990,7 @@
"from_your_organization": "vanuit uw organisatie",
"invitation_sent_once_more": "Uitnodiging nogmaals verzonden.",
"invite_deleted_successfully": "Uitnodiging succesvol verwijderd",
"invite_expires_on": "Uitnodiging verloopt op {date}",
"invited_on": "Uitgenodigd op {date}",
"invites_failed": "Uitnodigingen zijn mislukt",
"leave_organization": "Verlaat de organisatie",
"leave_organization_description": "U verlaat deze organisatie en verliest de toegang tot alle enquêtes en reacties. Je kunt alleen weer meedoen als je opnieuw wordt uitgenodigd.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Vul alle velden in om een nieuwe werkruimte toe te voegen.",
"read": "Lezen",
"read_write": "Lezen en schrijven",
"select_member": "Selecteer lid",
"select_workspace": "Selecteer werkruimte",
"team_admin": "Teambeheerder",
"team_created_successfully": "Team succesvol aangemaakt.",
"team_deleted_successfully": "Team succesvol verwijderd.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Voeg een tijdelijke aanduiding toe om aan te geven of er geen waarde is om te onthouden.",
"add_hidden_field_id": "Voeg een verborgen veld-ID toe",
"add_highlight_border": "Markeerrand toevoegen",
"add_highlight_border_description": "Voeg een buitenrand toe aan uw enquêtekaart.",
"add_logic": "Voeg logica toe",
"add_none_of_the_above": "Voeg 'Geen van bovenstaande' toe",
"add_option": "Optie toevoegen",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Wijzig het bericht dat bezoekers zien wanneer de enquête wordt gesloten.",
"adjust_the_theme_in_the": "Pas het thema aan in de",
"all_other_answers_will_continue_to": "Alle andere antwoorden blijven hetzelfde",
"allow_file_type": "Bestandstype toestaan",
"allow_multi_select": "Multi-select toestaan",
"allow_multiple_files": "Meerdere bestanden toestaan",
"allow_users_to_select_more_than_one_image": "Sta gebruikers toe meer dan één afbeelding te selecteren",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Blok gedupliceerd.",
"bold": "Vetgedrukt",
"brand_color": "Merk kleur",
"brand_color_description": "Toegepast op knoppen, links en highlights.",
"brightness": "Helderheid",
"bulk_edit": "Bulkbewerking",
"bulk_edit_description": "Bewerk alle opties hieronder, één per regel. Lege regels worden overgeslagen en duplicaten verwijderd.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Leg nieuwe actie vast",
"card_arrangement_for_survey_type_derived": "Kaartarrangement voor {surveyTypeDerived} enquêtes",
"card_background_color": "Achtergrondkleur van de kaart",
"card_background_color_description": "Vult het enquêtekaartgebied.",
"card_border_color": "Randkleur kaart",
"card_border_color_description": "Omlijnt de enquêtekaart.",
"card_styling": "Kaartstijl",
"casual": "Casual",
"caution_edit_duplicate": "Dupliceren en bewerken",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Oudere en nieuwere antwoorden lopen door elkaar heen, wat kan leiden tot misleidende gegevenssamenvattingen.",
"caution_recommendation": "Dit kan inconsistenties in de gegevens in de onderzoekssamenvatting veroorzaken. Wij raden u aan de enquête te dupliceren.",
"caution_text": "Veranderingen zullen tot inconsistenties leiden",
"centered_modal_overlay_color": "Gecentreerde modale overlaykleur",
"change_anyway": "Hoe dan ook veranderen",
"change_background": "Achtergrond wijzigen",
"change_question_type": "Vraagtype wijzigen",
"change_survey_type": "Als u van enquêtetype verandert, heeft dit invloed op de bestaande toegang",
"change_the_background_color_of_the_card": "Verander de achtergrondkleur van de kaart.",
"change_the_background_color_of_the_input_fields": "Verander de achtergrondkleur van de invoervelden.",
"change_the_background_to_a_color_image_or_animation": "Verander de achtergrond in een kleur, afbeelding of animatie.",
"change_the_border_color_of_the_card": "Verander de randkleur van de kaart.",
"change_the_border_color_of_the_input_fields": "Wijzig de randkleur van de invoervelden.",
"change_the_border_radius_of_the_card_and_the_inputs": "Wijzig de randradius van de kaart en de ingangen.",
"change_the_brand_color_of_the_survey": "Wijzig de merkkleur van de enquête.",
"change_the_placement_of_this_survey": "Wijzig de plaatsing van deze enquête.",
"change_the_question_color_of_the_survey": "Verander de vraagkleur van de enquête.",
"changes_saved": "Wijzigingen opgeslagen.",
"changing_survey_type_will_remove_existing_distribution_channels": "Het wijzigen van het enquêtetype heeft invloed op de manier waarop deze kan worden gedeeld. Als respondenten al toegangslinks hebben voor het huidige type, verliezen ze mogelijk de toegang na de overstap.",
"character_limit_toggle_description": "Beperk hoe kort of lang een antwoord mag zijn.",
"character_limit_toggle_title": "Tekenlimieten toevoegen",
"checkbox_label": "Selectievakje-label",
"choose_the_actions_which_trigger_the_survey": "Kies de acties die de enquête activeren.",
"choose_the_first_question_on_your_block": "Kies de eerste vraag in je blok",
@@ -1263,6 +1259,7 @@
"contact_fields": "Contactvelden",
"contains": "Bevat",
"continue_to_settings": "Ga verder naar Instellingen",
"control_which_file_types_can_be_uploaded": "Bepaal welke bestandstypen kunnen worden geüpload.",
"convert_to_multiple_choice": "Converteren naar Multi-select",
"convert_to_single_choice": "Converteren naar Enkele selectie",
"country": "Land",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Toch verwijderen",
"delete_block": "Blok verwijderen",
"delete_choice": "Keuze verwijderen",
"disable_the_visibility_of_survey_progress": "Schakel de zichtbaarheid van de voortgang van het onderzoek uit.",
"display_an_estimate_of_completion_time_for_survey": "Geef een schatting weer van de voltooiingstijd voor het onderzoek",
"display_number_of_responses_for_survey": "Weergave aantal reacties voor enquête",
"display_type": "Weergavetype",
"divide": "Verdeling /",
"does_not_contain": "Bevat niet",
"does_not_end_with": "Eindigt niet met",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Omvat niet alles",
"does_not_include_one_of": "Bevat niet een van",
"does_not_start_with": "Begint niet met",
"dropdown": "Dropdown",
"duplicate_block": "Blok dupliceren",
"duplicate_question": "Vraag dupliceren",
"edit_link": "Link bewerken",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "Voortgangsbalk verbergen",
"hide_question_settings": "Vraaginstellingen verbergen",
"hostname": "Hostnaam",
"if_you_need_more_please": "Als je meer nodig hebt,",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hoe funky wil je je kaarten hebben in {surveyTypeDerived} Enquêtes",
"if_you_need_more_please": "Als u meer nodig heeft, alstublieft",
"if_you_really_want_that_answer_ask_until_you_get_it": "Blijf tonen wanneer geactiveerd totdat een reactie is ingediend.",
"ignore_global_waiting_time": "Afkoelperiode negeren",
"ignore_global_waiting_time_description": "Deze enquête kan worden getoond wanneer aan de voorwaarden wordt voldaan, zelfs als er onlangs een andere enquête is getoond.",
@@ -1391,9 +1386,7 @@
"initial_value": "Initiële waarde",
"inner_text": "Innerlijke tekst",
"input_border_color": "Randkleur invoeren",
"input_border_color_description": "Omlijnt tekstvelden en tekstgebieden.",
"input_color": "Kleur invoeren",
"input_color_description": "Vult de binnenkant van tekstvelden.",
"insert_link": "Link invoegen",
"invalid_targeting": "Ongeldige targeting: controleer uw doelgroepfilters",
"invalid_video_url_warning": "Voer een geldige YouTube-, Vimeo- of Loom-URL in. We ondersteunen momenteel geen andere videohostingproviders.",
@@ -1420,10 +1413,10 @@
"key": "Sleutel",
"last_name": "Achternaam",
"let_people_upload_up_to_25_files_at_the_same_time": "Laat mensen maximaal 25 bestanden tegelijk uploaden.",
"limit_the_maximum_file_size": "Beperk de maximale bestandsgrootte voor uploads.",
"limit_upload_file_size_to": "Beperk uploadbestandsgrootte tot",
"limit_file_types": "Beperk bestandstypen",
"limit_the_maximum_file_size": "Beperk de maximale bestandsgrootte",
"limit_upload_file_size_to": "Beperk de uploadbestandsgrootte tot",
"link_survey_description": "Deel een link naar een enquêtepagina of sluit deze in op een webpagina of e-mail.",
"list": "Lijst",
"load_segment": "Laadsegment",
"logic_error_warning": "Wijzigen zal logische fouten veroorzaken",
"logic_error_warning_text": "Als u het vraagtype wijzigt, worden de logische voorwaarden van deze vraag verwijderd",
@@ -1435,7 +1428,7 @@
"matrix_all_fields": "Alle velden",
"matrix_rows": "Rijen",
"max_file_size": "Maximale bestandsgrootte",
"max_file_size_limit_is": "Maximale bestandsgroottelimiet is",
"max_file_size_limit_is": "De maximale bestandsgrootte is",
"move_question_to_block": "Vraag naar blok verplaatsen",
"multiply": "Vermenigvuldig *",
"needed_for_self_hosted_cal_com_instance": "Nodig voor een zelf-gehoste Cal.com-instantie",
@@ -1467,6 +1460,7 @@
"picture_idx": "Afbeelding {idx}",
"pin_can_only_contain_numbers": "De pincode kan alleen cijfers bevatten.",
"pin_must_be_a_four_digit_number": "De pincode moet uit vier cijfers bestaan.",
"please_enter_a_file_extension": "Voer een bestandsextensie in.",
"please_enter_a_valid_url": "Voer een geldige URL in (bijvoorbeeld https://example.com)",
"please_set_a_survey_trigger": "Stel een enquêtetrigger in",
"please_specify": "Gelieve te specificeren",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Alleen gebruikers die de pincode hebben, hebben toegang tot de enquête.",
"publish": "Publiceren",
"question": "Vraag",
"question_color": "Vraag kleur",
"question_deleted": "Vraag verwijderd.",
"question_duplicated": "Vraag dubbel gesteld.",
"question_id_updated": "Vraag-ID bijgewerkt",
"question_used_in_logic_warning_text": "Elementen uit dit blok worden gebruikt in een logische regel, weet je zeker dat je het wilt verwijderen?",
"question_used_in_logic_warning_title": "Logica-inconsistentie",
"question_used_in_quota": "Deze vraag wordt gebruikt in het quotum “{quotaName}”",
"question_used_in_logic": "Deze vraag wordt gebruikt in de logica van vraag {questionIndex}.",
"question_used_in_quota": "Deze vraag wordt gebruikt in het quotum '{quotaName}'",
"question_used_in_recall": "Deze vraag wordt teruggehaald in vraag {questionIndex}.",
"question_used_in_recall_ending_card": "Deze vraag wordt teruggeroepen in de Eindkaart",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Reactielimieten, omleidingen en meer.",
"response_options": "Reactieopties",
"roundness": "Rondheid",
"roundness_description": "Bepaalt hoe afgerond de kaarthoeken zijn.",
"row_used_in_logic_error": "Deze rij wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
"rows": "Rijen",
"save_and_close": "Opslaan en sluiten",
@@ -1546,7 +1539,6 @@
"search_for_images": "Zoek naar afbeeldingen",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "seconden na trigger wordt de enquête gesloten als er geen reactie is",
"seconds_before_showing_the_survey": "seconden voordat de enquête wordt weergegeven.",
"select_field": "Selecteer veld",
"select_or_type_value": "Selecteer of typ een waarde",
"select_ordering": "Selecteer bestellen",
"select_saved_action": "Selecteer opgeslagen actie",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Styling ingesteld op themastijlen",
"subheading": "Ondertitel",
"subtract": "Aftrekken -",
"suggest_colors": "Stel kleuren voor",
"survey_completed_heading": "Enquête voltooid",
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
"survey_display_settings": "Enquêteweergave-instellingen",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Toon één keer, zelfs als ze niet reageren.",
"then": "Dan",
"this_action_will_remove_all_the_translations_from_this_survey": "Met deze actie worden alle vertalingen uit deze enquête verwijderd.",
"this_extension_is_already_added": "Deze extensie is al toegevoegd.",
"this_file_type_is_not_supported": "Dit bestandstype wordt niet ondersteund.",
"three_points": "3 punten",
"times": "keer",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Om de plaatsing over alle enquêtes consistent te houden, kunt u dat doen",
@@ -1613,51 +1608,8 @@
"upper_label": "Bovenste etiket",
"url_filters": "URL-filters",
"url_not_supported": "URL niet ondersteund",
"validation": {
"add_validation_rule": "Validatieregel toevoegen",
"answer_all_rows": "Beantwoord alle rijen",
"characters": "Tekens",
"contains": "Bevat",
"delete_validation_rule": "Validatieregel verwijderen",
"does_not_contain": "Bevat niet",
"email": "Is geldig e-mailadres",
"end_date": "Einddatum",
"file_extension_is": "Bestandsextensie is",
"file_extension_is_not": "Bestandsextensie is niet",
"is": "Is",
"is_between": "Is tussen",
"is_earlier_than": "Is eerder dan",
"is_greater_than": "Is groter dan",
"is_later_than": "Is later dan",
"is_less_than": "Is minder dan",
"is_not": "Is niet",
"is_not_between": "Is niet tussen",
"kb": "KB",
"max_length": "Maximaal",
"max_selections": "Maximaal",
"max_value": "Maximaal",
"mb": "MB",
"min_length": "Minimaal",
"min_selections": "Minimaal",
"min_value": "Minimaal",
"minimum_options_ranked": "Minimaal aantal gerangschikte opties",
"minimum_rows_answered": "Minimaal aantal beantwoorde rijen",
"options_selected": "Opties geselecteerd",
"pattern": "Komt overeen met regex-patroon",
"phone": "Is geldig telefoonnummer",
"rank_all_options": "Rangschik alle opties",
"select_file_extensions": "Selecteer bestandsextensies...",
"select_option": "Optie selecteren",
"start_date": "Startdatum",
"url": "Is geldige URL"
},
"validation_logic_and": "Alle zijn waar",
"validation_logic_or": "een is waar",
"validation_rules": "Validatieregels",
"validation_rules_description": "Accepteer alleen antwoorden die voldoen aan de volgende criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabele {variableName} wordt gebruikt in het quotum “{quotaName}",
"variable_name_conflicts_with_hidden_field": "Variabelenaam conflicteert met een bestaande verborgen veld-ID.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabele \"{variableName}\" wordt gebruikt in het \"{quotaName}\" quotum",
"variable_name_is_already_taken_please_choose_another": "Variabelenaam is al in gebruik, kies een andere.",
"variable_name_must_start_with_a_letter": "Variabelenaam moet beginnen met een letter.",
"variable_used_in_recall": "Variabele \"{variable}\" wordt opgeroepen in vraag {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Achtergrondkleur toevoegen",
"add_background_color_description": "Voeg een achtergrondkleur toe aan de logocontainer.",
"advanced_styling_field_border_radius": "Hoekradius",
"advanced_styling_field_button_bg": "Knopachtergrond",
"advanced_styling_field_button_bg_description": "Vult de volgende/verzend-knop.",
"advanced_styling_field_button_border_radius_description": "Rondt de knophoeken af.",
"advanced_styling_field_button_font_size_description": "Schaalt de tekst van het knoplabel.",
"advanced_styling_field_button_font_weight_description": "Maakt knoptekst lichter of vetter.",
"advanced_styling_field_button_height_description": "Bepaalt de knophoogte.",
"advanced_styling_field_button_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
"advanced_styling_field_button_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
"advanced_styling_field_button_text": "Knoptekst",
"advanced_styling_field_button_text_description": "Kleurt het label binnen knoppen.",
"advanced_styling_field_description_color": "Beschrijvingskleur",
"advanced_styling_field_description_color_description": "Kleurt de tekst onder elke kop.",
"advanced_styling_field_description_size": "Lettergrootte beschrijving",
"advanced_styling_field_description_size_description": "Schaalt de beschrijvingstekst.",
"advanced_styling_field_description_weight": "Letterdikte beschrijving",
"advanced_styling_field_description_weight_description": "Maakt beschrijvingstekst lichter of vetter.",
"advanced_styling_field_font_size": "Lettergrootte",
"advanced_styling_field_font_weight": "Letterdikte",
"advanced_styling_field_headline_color": "Kopkleur",
"advanced_styling_field_headline_color_description": "Kleurt de hoofdvraagtekst.",
"advanced_styling_field_headline_size": "Lettergrootte kop",
"advanced_styling_field_headline_size_description": "Schaalt de koptekst.",
"advanced_styling_field_headline_weight": "Letterdikte kop",
"advanced_styling_field_headline_weight_description": "Maakt koptekst lichter of vetter.",
"advanced_styling_field_height": "Hoogte",
"advanced_styling_field_indicator_bg": "Indicatorachtergrond",
"advanced_styling_field_indicator_bg_description": "Kleurt het gevulde deel van de balk.",
"advanced_styling_field_input_border_radius_description": "Rondt de invoerhoeken af.",
"advanced_styling_field_input_font_size_description": "Schaalt de getypte tekst in invoervelden.",
"advanced_styling_field_input_height_description": "Bepaalt de hoogte van het invoerveld.",
"advanced_styling_field_input_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
"advanced_styling_field_input_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
"advanced_styling_field_input_placeholder_opacity_description": "Vervaagt de tijdelijke aanwijzingstekst.",
"advanced_styling_field_input_shadow_description": "Voegt een slagschaduw toe rond invoervelden.",
"advanced_styling_field_input_text": "Invoertekst",
"advanced_styling_field_input_text_description": "Kleurt de getypte tekst in invoervelden.",
"advanced_styling_field_option_bg": "Achtergrond",
"advanced_styling_field_option_bg_description": "Vult de optie-items.",
"advanced_styling_field_option_border_radius_description": "Rondt de hoeken van opties af.",
"advanced_styling_field_option_font_size_description": "Schaalt de tekst van optielabels.",
"advanced_styling_field_option_label": "Labelkleur",
"advanced_styling_field_option_label_description": "Kleurt de tekst van optielabels.",
"advanced_styling_field_option_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
"advanced_styling_field_option_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
"advanced_styling_field_padding_x": "Opvulling X",
"advanced_styling_field_padding_y": "Opvulling Y",
"advanced_styling_field_placeholder_opacity": "Plaatshouderopaciteit",
"advanced_styling_field_shadow": "Schaduw",
"advanced_styling_field_track_bg": "Sporachtergrond",
"advanced_styling_field_track_bg_description": "Kleurt het ongevulde gedeelte van de balk.",
"advanced_styling_field_track_height": "Spoorhoogte",
"advanced_styling_field_track_height_description": "Regelt de dikte van de voortgangsbalk.",
"advanced_styling_field_upper_label_color": "Koplabelkleur",
"advanced_styling_field_upper_label_color_description": "Kleurt het kleine label boven invoervelden.",
"advanced_styling_field_upper_label_size": "Lettergrootte koplabel",
"advanced_styling_field_upper_label_size_description": "Schaalt het kleine label boven invoervelden.",
"advanced_styling_field_upper_label_weight": "Letterdikte koplabel",
"advanced_styling_field_upper_label_weight_description": "Maakt het label lichter of vetter.",
"advanced_styling_section_buttons": "Knoppen",
"advanced_styling_section_headlines": "Koppen & beschrijvingen",
"advanced_styling_section_inputs": "Invoervelden",
"advanced_styling_section_options": "Opties (radio/checkbox)",
"app_survey_placement": "App-enquête plaatsing",
"app_survey_placement_settings_description": "Wijzig waar enquêtes worden weergegeven in uw web-app of website.",
"centered_modal_overlay_color": "Gecentreerde modale overlaykleur",
"email_customization": "E-mail aanpassing",
"email_customization_description": "Wijzig het uiterlijk van e-mails die Formbricks namens u verstuurt.",
"enable_custom_styling": "Aangepaste styling inschakelen",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Formbricks-branding is verborgen.",
"formbricks_branding_settings_description": "We waarderen uw steun, maar begrijpen het als u dit uitschakelt.",
"formbricks_branding_shown": "Formbricks-branding wordt weergegeven.",
"generate_theme_btn": "Genereren",
"generate_theme_confirmation": "Wil je een passend kleurthema genereren op basis van je merkkleur? Dit overschrijft je huidige kleurinstellingen.",
"generate_theme_header": "Kleurthema genereren?",
"logo_removed_successfully": "Logo succesvol verwijderd",
"logo_settings_description": "Upload uw bedrijfslogo om enquêtes en linkvoorbeelden te voorzien van uw huisstijl.",
"logo_updated_successfully": "Logo succesvol bijgewerkt",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Toon Formbricks-branding in {type} enquêtes",
"show_powered_by_formbricks": "Toon 'Powered by Formbricks' handtekening",
"styling_updated_successfully": "Styling succesvol bijgewerkt",
"suggest_colors": "Kleuren voorstellen",
"theme": "Thema",
"theme_settings_description": "Maak een stijlthema voor alle enquêtes. Je kunt aangepaste styling inschakelen voor elke enquête."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Ja, houd mij op de hoogte.",
"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_subheader": "Dit is een voorbeeldbeschrijving.",
"preview_survey_welcome_card_headline": "Welkom!",
"prioritize_features_description": "Identificeer functies die uw gebruikers het meest en het minst nodig hebben.",
"prioritize_features_name": "Geef prioriteit aan functies",
+37 -152
View File
@@ -243,6 +243,7 @@
"imprint": "impressão",
"in_progress": "Em andamento",
"inactive_surveys": "Pesquisas inativas",
"input_type": "Tipo de entrada",
"integration": "integração",
"integrations": "Integrações",
"invalid_date": "Data inválida",
@@ -254,7 +255,6 @@
"label": "Etiqueta",
"language": "Língua",
"learn_more": "Saiba mais",
"license_expired": "License Expired",
"light_overlay": "sobreposição leve",
"limits_reached": "Limites Atingidos",
"link": "link",
@@ -267,11 +267,13 @@
"look_and_feel": "Aparência e Experiência",
"manage": "gerenciar",
"marketing": "marketing",
"maximum": "Máximo",
"member": "Membros",
"members": "Membros",
"members_and_teams": "Membros e equipes",
"membership_not_found": "Assinatura não encontrada",
"metadata": "metadados",
"minimum": "Mínimo",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor em uma tela maior. Para gerenciar ou criar pesquisas, mude para outro dispositivo.",
"mobile_overlay_surveys_look_good": "Não se preocupe suas pesquisas ficam ótimas em qualquer dispositivo e tamanho de tela!",
"mobile_overlay_title": "Eita, tela pequena detectada!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Imagem de fundo não encontrada.",
"no_code": "Sem código",
"no_files_uploaded": "Nenhum arquivo foi enviado",
"no_overlay": "Sem sobreposição",
"no_quotas_found": "Nenhuma cota encontrada",
"no_result_found": "Nenhum resultado encontrado",
"no_results": "Nenhum resultado",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Equipes da organização não encontradas",
"other": "outro",
"others": "Outros",
"overlay_color": "Cor da sobreposição",
"overview": "Visão Geral",
"password": "Senha",
"paused": "Pausado",
@@ -326,7 +326,7 @@
"placeholder": "Espaço reservado",
"please_select_at_least_one_survey": "Por favor, selecione pelo menos uma pesquisa",
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
"please_upgrade_your_plan": "Por favor, atualize seu plano",
"please_upgrade_your_plan": "Por favor, atualize seu plano.",
"preview": "Prévia",
"preview_survey": "Prévia da Pesquisa",
"privacy": "Política de Privacidade",
@@ -352,7 +352,6 @@
"request_trial_license": "Pedir licença de teste",
"reset_to_default": "Restaurar para o padrão",
"response": "Resposta",
"response_id": "ID da resposta",
"responses": "Respostas",
"restart": "Reiniciar",
"role": "Rolê",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Você atingiu seu limite de {projectLimit} espaços de trabalho.",
"you_have_reached_your_monthly_miu_limit_of": "Você atingiu o seu limite mensal de MIU de",
"you_have_reached_your_monthly_response_limit_of": "Você atingiu o limite mensal de respostas de",
"you_will_be_downgraded_to_the_community_edition_on_date": "Você será rebaixado para a Edição Comunitária em {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Você será rebaixado para a Edição Comunitária em {date}."
},
"emails": {
"accept": "Aceitar",
@@ -956,32 +954,19 @@
"enterprise_features": "Recursos Empresariais",
"get_an_enterprise_license_to_get_access_to_all_features": "Adquira uma licença Enterprise para ter acesso a todos os recursos.",
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controle total sobre a privacidade e segurança dos seus dados.",
"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_active": "Ativa",
"license_status_description": "Status da sua licença enterprise.",
"license_status_expired": "Expirada",
"license_status_invalid": "Licença inválida",
"license_status_unreachable": "Inacessível",
"license_unreachable_grace_period": "O servidor de licenças não pode ser alcançado. Seus recursos empresariais permanecem ativos durante um período de carência de 3 dias que termina em {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sem necessidade de ligação, sem compromisso: Solicite uma licença de teste gratuita de 30 dias para testar todas as funcionalidades preenchendo este formulário:",
"no_credit_card_no_sales_call_just_test_it": "Sem cartão de crédito. Sem ligação de vendas. Só teste :)",
"on_request": "Quando solicitado",
"organization_roles": "Funções na Organização (Admin, Editor, Desenvolvedor, etc.)",
"questions_please_reach_out_to": "Perguntas? Entre em contato com",
"recheck_license": "Verificar licença novamente",
"recheck_license_failed": "Falha na verificação da licença. O servidor de licenças pode estar inacessível.",
"recheck_license_invalid": "A chave de licença é inválida. Verifique sua ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Verificação da licença bem-sucedida",
"recheck_license_unreachable": "Servidor de licenças inacessível. Por favor, tente novamente mais tarde.",
"rechecking": "Verificando novamente...",
"request_30_day_trial_license": "Pedir Licença de Teste de 30 Dias",
"saml_sso": "SSO SAML",
"service_level_agreement": "Acordo de Nível de Serviço",
"soc2_hipaa_iso_27001_compliance_check": "Verificação de conformidade SOC2, HIPAA, ISO 27001",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Equipes e Funções de Acesso (Ler, Ler e Escrever, Gerenciar)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias.",
"your_enterprise_license_is_active_all_features_unlocked": "Sua licença empresarial está ativa. Todos os recursos estão desbloqueados."
},
"general": {
"bulk_invite_warning_description": "Por favor, note que no Plano Gratuito, todos os membros da organização são automaticamente atribuídos ao papel de 'Owner', independentemente do papel especificado no arquivo CSV.",
@@ -1005,7 +990,7 @@
"from_your_organization": "da sua organização",
"invitation_sent_once_more": "Convite enviado de novo.",
"invite_deleted_successfully": "Convite deletado com sucesso",
"invite_expires_on": "O convite expira em {date}",
"invited_on": "Convidado em {date}",
"invites_failed": "Convites falharam",
"leave_organization": "Sair da organização",
"leave_organization_description": "Você vai sair dessa organização e perder acesso a todas as pesquisas e respostas. Você só pode voltar se for convidado de novo.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Preencha todos os campos para adicionar um novo espaço de trabalho.",
"read": "Leitura",
"read_write": "Leitura & Escrita",
"select_member": "Selecionar membro",
"select_workspace": "Selecionar espaço de trabalho",
"team_admin": "Administrador da equipe",
"team_created_successfully": "Equipe criada com sucesso.",
"team_deleted_successfully": "Equipe excluída com sucesso.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Adicionar um texto padrão para mostrar se a pergunta for ignorada:",
"add_hidden_field_id": "Adicionar campo oculto ID",
"add_highlight_border": "Adicionar borda de destaque",
"add_highlight_border_description": "Adicione uma borda externa ao seu cartão de pesquisa.",
"add_logic": "Adicionar lógica",
"add_none_of_the_above": "Adicionar \"Nenhuma das opções acima\"",
"add_option": "Adicionar opção",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Mude a mensagem que os visitantes veem quando a pesquisa está fechada.",
"adjust_the_theme_in_the": "Ajuste o tema no",
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
"allow_file_type": "Permitir tipo de arquivo",
"allow_multi_select": "Permitir seleção múltipla",
"allow_multiple_files": "Permitir vários arquivos",
"allow_users_to_select_more_than_one_image": "Permitir que os usuários selecionem mais de uma imagem",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Bloco duplicado.",
"bold": "Negrito",
"brand_color": "Cor da marca",
"brand_color_description": "Aplicado a botões, links e destaques.",
"brightness": "brilho",
"bulk_edit": "Edição em massa",
"bulk_edit_description": "Edite todas as opções abaixo, uma por linha. Linhas vazias serão ignoradas e duplicatas removidas.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Capturar nova ação",
"card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Pesquisas {surveyTypeDerived}",
"card_background_color": "Cor de fundo do cartão",
"card_background_color_description": "Preenche a área do cartão da pesquisa.",
"card_border_color": "Cor da borda do cartão",
"card_border_color_description": "Contorna o cartão da pesquisa.",
"card_styling": "Estilo do cartão",
"casual": "Casual",
"caution_edit_duplicate": "Duplicar e editar",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Respostas antigas e novas são misturadas, o que pode levar a resumos de dados enganosos.",
"caution_recommendation": "Isso pode causar inconsistências de dados no resumo da pesquisa. Recomendamos duplicar a pesquisa em vez disso.",
"caution_text": "Mudanças vão levar a inconsistências",
"centered_modal_overlay_color": "cor de sobreposição modal centralizada",
"change_anyway": "Mudar mesmo assim",
"change_background": "Mudar fundo",
"change_question_type": "Mudar tipo de pergunta",
"change_survey_type": "Alterar o tipo de pesquisa afeta o acesso existente",
"change_the_background_color_of_the_card": "Muda a cor de fundo do cartão.",
"change_the_background_color_of_the_input_fields": "Mude a cor de fundo dos campos de entrada.",
"change_the_background_to_a_color_image_or_animation": "Mude o fundo para uma cor, imagem ou animação.",
"change_the_border_color_of_the_card": "Muda a cor da borda do cartão.",
"change_the_border_color_of_the_input_fields": "Mude a cor da borda dos campos de entrada.",
"change_the_border_radius_of_the_card_and_the_inputs": "Muda o raio da borda do card e dos inputs.",
"change_the_brand_color_of_the_survey": "Muda a cor da marca da pesquisa.",
"change_the_placement_of_this_survey": "Muda a posição dessa pesquisa.",
"change_the_question_color_of_the_survey": "Muda a cor da pergunta da pesquisa.",
"changes_saved": "Mudanças salvas.",
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de pesquisa afetará a forma como ela pode ser compartilhada. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
"character_limit_toggle_description": "Limite o quão curta ou longa uma resposta pode ser.",
"character_limit_toggle_title": "Adicionar limites de caracteres",
"checkbox_label": "Rótulo da Caixa de Seleção",
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.",
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta do seu bloco",
@@ -1263,6 +1259,7 @@
"contact_fields": "Campos de Contato",
"contains": "contém",
"continue_to_settings": "Continuar para Configurações",
"control_which_file_types_can_be_uploaded": "Controlar quais tipos de arquivos podem ser enviados.",
"convert_to_multiple_choice": "Converter para Múltipla Escolha",
"convert_to_single_choice": "Converter para Escolha Única",
"country": "país",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Excluir mesmo assim",
"delete_block": "Excluir bloco",
"delete_choice": "Deletar opção",
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa de tempo de conclusão da pesquisa",
"display_number_of_responses_for_survey": "Mostrar número de respostas da pesquisa",
"display_type": "Tipo de exibição",
"divide": "Divida /",
"does_not_contain": "não contém",
"does_not_end_with": "Não termina com",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Não inclui todos de",
"does_not_include_one_of": "Não inclui um de",
"does_not_start_with": "Não começa com",
"dropdown": "Menu suspenso",
"duplicate_block": "Duplicar bloco",
"duplicate_question": "Duplicar pergunta",
"edit_link": "Editar link",
@@ -1381,6 +1375,7 @@
"hide_progress_bar": "Esconder barra de progresso",
"hide_question_settings": "Ocultar configurações da pergunta",
"hostname": "nome do host",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}",
"if_you_need_more_please": "Se você precisar de mais, por favor",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar mostrando sempre que acionada até que uma resposta seja enviada.",
"ignore_global_waiting_time": "Ignorar período de espera",
@@ -1391,9 +1386,7 @@
"initial_value": "Valor inicial",
"inner_text": "Texto Interno",
"input_border_color": "Cor da borda de entrada",
"input_border_color_description": "Contorna campos de texto e áreas de texto.",
"input_color": "Cor de entrada",
"input_color_description": "Preenche o interior dos campos de texto.",
"insert_link": "Inserir link",
"invalid_targeting": "Segmentação inválida: Por favor, verifique os filtros do seu público",
"invalid_video_url_warning": "Por favor, insira uma URL válida do YouTube, Vimeo ou Loom. No momento, não suportamos outros provedores de vídeo.",
@@ -1420,10 +1413,10 @@
"key": "chave",
"last_name": "Sobrenome",
"let_people_upload_up_to_25_files_at_the_same_time": "Deixe as pessoas fazerem upload de até 25 arquivos ao mesmo tempo.",
"limit_the_maximum_file_size": "Limitar o tamanho máximo de arquivo para uploads.",
"limit_upload_file_size_to": "Limitar tamanho de arquivo de upload para",
"limit_file_types": "Limitar tipos de arquivos",
"limit_the_maximum_file_size": "Limitar o tamanho máximo do arquivo",
"limit_upload_file_size_to": "Limitar tamanho do arquivo de upload para",
"link_survey_description": "Compartilhe um link para a página da pesquisa ou incorpore-a em uma página da web ou e-mail.",
"list": "Lista",
"load_segment": "segmento de carga",
"logic_error_warning": "Mudar vai causar erros de lógica",
"logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta",
@@ -1435,7 +1428,7 @@
"matrix_all_fields": "Todos os campos",
"matrix_rows": "Linhas",
"max_file_size": "Tamanho máximo do arquivo",
"max_file_size_limit_is": "O limite de tamanho máximo do arquivo é",
"max_file_size_limit_is": "Tamanho máximo do arquivo é",
"move_question_to_block": "Mover pergunta para o bloco",
"multiply": "Multiplicar *",
"needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com",
@@ -1467,6 +1460,7 @@
"picture_idx": "Imagem {idx}",
"pin_can_only_contain_numbers": "O PIN só pode conter números.",
"pin_must_be_a_four_digit_number": "O PIN deve ser um número de quatro dígitos.",
"please_enter_a_file_extension": "Por favor, insira uma extensão de arquivo.",
"please_enter_a_valid_url": "Por favor, insira uma URL válida (ex.: https://example.com)",
"please_set_a_survey_trigger": "Por favor, configure um gatilho para a pesquisa",
"please_specify": "Por favor, especifique",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Somente usuários que têm o PIN podem acessar a pesquisa.",
"publish": "Publicar",
"question": "Pergunta",
"question_color": "Cor da pergunta",
"question_deleted": "Pergunta deletada.",
"question_duplicated": "Pergunta duplicada.",
"question_id_updated": "ID da pergunta atualizado",
"question_used_in_logic_warning_text": "Elementos deste bloco são usados em uma regra de lógica, tem certeza de que deseja excluí-lo?",
"question_used_in_logic_warning_title": "Inconsistência de lógica",
"question_used_in_quota": "Esta pergunta está sendo usada na cota \"{quotaName}\"",
"question_used_in_logic": "Essa pergunta é usada na lógica da pergunta {questionIndex}.",
"question_used_in_quota": "Esta questão está sendo usada na cota \"{quotaName}\"",
"question_used_in_recall": "Esta pergunta está sendo recordada na pergunta {questionIndex}.",
"question_used_in_recall_ending_card": "Esta pergunta está sendo recordada no card de Encerramento",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
"response_options": "Opções de Resposta",
"roundness": "Circularidade",
"roundness_description": "Controla o arredondamento dos cantos do cartão.",
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
"rows": "linhas",
"save_and_close": "Salvar e Fechar",
@@ -1546,7 +1539,6 @@
"search_for_images": "Buscar imagens",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos após acionar, a pesquisa será encerrada se não houver resposta",
"seconds_before_showing_the_survey": "segundos antes de mostrar a pesquisa.",
"select_field": "Selecionar campo",
"select_or_type_value": "Selecionar ou digitar valor",
"select_ordering": "Selecionar pedido",
"select_saved_action": "Selecionar ação salva",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Estilo definido para os estilos do tema",
"subheading": "Subtítulo",
"subtract": "Subtrair -",
"suggest_colors": "Sugerir cores",
"survey_completed_heading": "Pesquisa Concluída",
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
"survey_display_settings": "Configurações de Exibição da Pesquisa",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar uma única vez, mesmo que não respondam.",
"then": "Então",
"this_action_will_remove_all_the_translations_from_this_survey": "Essa ação vai remover todas as traduções dessa pesquisa.",
"this_extension_is_already_added": "Essa extensão já foi adicionada.",
"this_file_type_is_not_supported": "Esse tipo de arquivo não é suportado.",
"three_points": "3 pontos",
"times": "times",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todas as pesquisas, você pode",
@@ -1613,51 +1608,8 @@
"upper_label": "Etiqueta Superior",
"url_filters": "Filtros de URL",
"url_not_supported": "URL não suportada",
"validation": {
"add_validation_rule": "Adicionar regra de validação",
"answer_all_rows": "Responda todas as linhas",
"characters": "Caracteres",
"contains": "Contém",
"delete_validation_rule": "Excluir regra de validação",
"does_not_contain": "Não contém",
"email": "É um e-mail válido",
"end_date": "Data final",
"file_extension_is": "A extensão do arquivo é",
"file_extension_is_not": "A extensão do arquivo não é",
"is": "É",
"is_between": "Está entre",
"is_earlier_than": "É anterior a",
"is_greater_than": "É maior que",
"is_later_than": "É posterior a",
"is_less_than": "É menor que",
"is_not": "Não é",
"is_not_between": "Não está entre",
"kb": "KB",
"max_length": "No máximo",
"max_selections": "No máximo",
"max_value": "No máximo",
"mb": "MB",
"min_length": "No mínimo",
"min_selections": "No mínimo",
"min_value": "No mínimo",
"minimum_options_ranked": "Mínimo de opções classificadas",
"minimum_rows_answered": "Mínimo de linhas respondidas",
"options_selected": "Opções selecionadas",
"pattern": "Corresponde ao padrão regex",
"phone": "É um telefone válido",
"rank_all_options": "Classificar todas as opções",
"select_file_extensions": "Selecionar extensões de arquivo...",
"select_option": "Selecionar opção",
"start_date": "Data inicial",
"url": "É uma URL válida"
},
"validation_logic_and": "Todas são verdadeiras",
"validation_logic_or": "qualquer uma é verdadeira",
"validation_rules": "Regras de validação",
"validation_rules_description": "Aceitar apenas respostas que atendam aos seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} está sendo usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "A variável \"{variableName}\" está sendo usada na cota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "O nome da variável está em conflito com um ID de campo oculto existente.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está sendo usada na cota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
"variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.",
"variable_used_in_recall": "Variável \"{variable}\" está sendo recordada na pergunta {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Adicionar cor de fundo",
"add_background_color_description": "Adicione uma cor de fundo ao container do logo.",
"advanced_styling_field_border_radius": "Raio da borda",
"advanced_styling_field_button_bg": "Fundo do botão",
"advanced_styling_field_button_bg_description": "Preenche o botão Próximo / Enviar.",
"advanced_styling_field_button_border_radius_description": "Arredonda os cantos do botão.",
"advanced_styling_field_button_font_size_description": "Ajusta o tamanho do texto do rótulo do botão.",
"advanced_styling_field_button_font_weight_description": "Torna o texto do botão mais leve ou mais negrito.",
"advanced_styling_field_button_height_description": "Controla a altura do botão.",
"advanced_styling_field_button_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_button_padding_y_description": "Adiciona espaço no topo e na base.",
"advanced_styling_field_button_text": "Texto do botão",
"advanced_styling_field_button_text_description": "Colore o rótulo dentro dos botões.",
"advanced_styling_field_description_color": "Cor da descrição",
"advanced_styling_field_description_color_description": "Colore o texto abaixo de cada título.",
"advanced_styling_field_description_size": "Tamanho da fonte da descrição",
"advanced_styling_field_description_size_description": "Ajusta o tamanho do texto da descrição.",
"advanced_styling_field_description_weight": "Peso da fonte da descrição",
"advanced_styling_field_description_weight_description": "Torna o texto da descrição mais leve ou mais negrito.",
"advanced_styling_field_font_size": "Tamanho da fonte",
"advanced_styling_field_font_weight": "Peso da fonte",
"advanced_styling_field_headline_color": "Cor do título",
"advanced_styling_field_headline_color_description": "Colore o texto principal da pergunta.",
"advanced_styling_field_headline_size": "Tamanho da fonte do título",
"advanced_styling_field_headline_size_description": "Ajusta o tamanho do texto do título.",
"advanced_styling_field_headline_weight": "Peso da fonte do título",
"advanced_styling_field_headline_weight_description": "Torna o texto do título mais leve ou mais negrito.",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_indicator_bg": "Fundo do indicador",
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
"advanced_styling_field_input_height_description": "Controla a altura do campo de entrada.",
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_input_padding_y_description": "Adiciona espaço na parte superior e inferior.",
"advanced_styling_field_input_placeholder_opacity_description": "Esmaece o texto de dica do placeholder.",
"advanced_styling_field_input_shadow_description": "Adiciona uma sombra ao redor dos campos de entrada.",
"advanced_styling_field_input_text": "Texto de entrada",
"advanced_styling_field_input_text_description": "Colore o texto digitado nos campos de entrada.",
"advanced_styling_field_option_bg": "Fundo",
"advanced_styling_field_option_bg_description": "Preenche os itens de opção.",
"advanced_styling_field_option_border_radius_description": "Arredonda os cantos das opções.",
"advanced_styling_field_option_font_size_description": "Ajusta o tamanho do texto do rótulo da opção.",
"advanced_styling_field_option_label": "Cor do rótulo",
"advanced_styling_field_option_label_description": "Colore o texto do rótulo da opção.",
"advanced_styling_field_option_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_option_padding_y_description": "Adiciona espaço na parte superior e inferior.",
"advanced_styling_field_padding_x": "Espaçamento X",
"advanced_styling_field_padding_y": "Espaçamento Y",
"advanced_styling_field_placeholder_opacity": "Opacidade do placeholder",
"advanced_styling_field_shadow": "Sombra",
"advanced_styling_field_track_bg": "Fundo da trilha",
"advanced_styling_field_track_bg_description": "Colore a porção não preenchida da barra.",
"advanced_styling_field_track_height": "Altura da trilha",
"advanced_styling_field_track_height_description": "Controla a espessura da barra de progresso.",
"advanced_styling_field_upper_label_color": "Cor do rótulo do título",
"advanced_styling_field_upper_label_color_description": "Colore o pequeno rótulo acima dos campos de entrada.",
"advanced_styling_field_upper_label_size": "Tamanho da fonte do rótulo do título",
"advanced_styling_field_upper_label_size_description": "Ajusta o tamanho do pequeno rótulo acima dos campos de entrada.",
"advanced_styling_field_upper_label_weight": "Peso da fonte do rótulo do título",
"advanced_styling_field_upper_label_weight_description": "Torna o rótulo mais leve ou mais negrito.",
"advanced_styling_section_buttons": "Botões",
"advanced_styling_section_headlines": "Títulos e descrições",
"advanced_styling_section_inputs": "Campos de entrada",
"advanced_styling_section_options": "Opções (rádio/caixa de seleção)",
"app_survey_placement": "Posicionamento da pesquisa de app",
"app_survey_placement_settings_description": "Altere onde as pesquisas serão exibidas em seu aplicativo web ou site.",
"centered_modal_overlay_color": "Cor de sobreposição modal centralizada",
"email_customization": "Personalização de e-mail",
"email_customization_description": "Altere a aparência dos e-mails que o Formbricks envia em seu nome.",
"enable_custom_styling": "Habilitar estilização personalizada",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "A marca Formbricks está oculta.",
"formbricks_branding_settings_description": "Adoramos seu apoio, mas entendemos se você desativar.",
"formbricks_branding_shown": "A marca Formbricks está visível.",
"generate_theme_btn": "Gerar",
"generate_theme_confirmation": "Gostaria de gerar um tema de cores correspondente baseado na cor da sua marca? Isso substituirá suas configurações de cores atuais.",
"generate_theme_header": "Gerar tema de cores?",
"logo_removed_successfully": "Logo removido com sucesso",
"logo_settings_description": "Faça upload do logo da sua empresa para personalizar pesquisas e pré-visualizações de links.",
"logo_updated_successfully": "Logo atualizado com sucesso",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Mostrar marca Formbricks em pesquisas {type}",
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo atualizado com sucesso",
"suggest_colors": "Sugerir cores",
"theme": "Tema",
"theme_settings_description": "Crie um tema de estilo para todas as pesquisas. Você pode ativar estilo personalizado para cada pesquisa."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Sim, me mantenha informado.",
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
"preview_survey_question_2_headline": "Quer ficar por dentro?",
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
"preview_survey_welcome_card_headline": "Bem-vindo!",
"prioritize_features_description": "Identifique os recursos que seus usuários mais e menos precisam.",
"prioritize_features_name": "Priorizar Funcionalidades",
+37 -152
View File
@@ -243,6 +243,7 @@
"imprint": "Impressão",
"in_progress": "Em Progresso",
"inactive_surveys": "Inquéritos inativos",
"input_type": "Tipo de entrada",
"integration": "integração",
"integrations": "Integrações",
"invalid_date": "Data inválida",
@@ -254,7 +255,6 @@
"label": "Etiqueta",
"language": "Idioma",
"learn_more": "Saiba mais",
"license_expired": "License Expired",
"light_overlay": "Sobreposição leve",
"limits_reached": "Limites Atingidos",
"link": "Link",
@@ -267,11 +267,13 @@
"look_and_feel": "Aparência e Sensação",
"manage": "Gerir",
"marketing": "Marketing",
"maximum": "Máximo",
"member": "Membro",
"members": "Membros",
"members_and_teams": "Membros e equipas",
"membership_not_found": "Associação não encontrada",
"metadata": "Metadados",
"minimum": "Mínimo",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor num ecrã maior. Para gerir ou criar inquéritos, mude de dispositivo.",
"mobile_overlay_surveys_look_good": "Não se preocupe os seus inquéritos têm uma ótima aparência em todos os dispositivos e tamanhos de ecrã!",
"mobile_overlay_title": "Oops, ecrã pequeno detectado!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Nenhuma imagem de fundo encontrada.",
"no_code": "Sem código",
"no_files_uploaded": "Nenhum ficheiro foi carregado",
"no_overlay": "Sem sobreposição",
"no_quotas_found": "Nenhum quota encontrado",
"no_result_found": "Nenhum resultado encontrado",
"no_results": "Nenhum resultado",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Equipas da organização não encontradas",
"other": "Outro",
"others": "Outros",
"overlay_color": "Cor da sobreposição",
"overview": "Visão geral",
"password": "Palavra-passe",
"paused": "Em pausa",
@@ -326,7 +326,7 @@
"placeholder": "Espaço reservado",
"please_select_at_least_one_survey": "Por favor, selecione pelo menos um inquérito",
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
"please_upgrade_your_plan": "Por favor, atualize o seu plano",
"please_upgrade_your_plan": "Por favor, atualize o seu plano.",
"preview": "Pré-visualização",
"preview_survey": "Pré-visualização do inquérito",
"privacy": "Política de Privacidade",
@@ -352,7 +352,6 @@
"request_trial_license": "Solicitar licença de teste",
"reset_to_default": "Repor para o padrão",
"response": "Resposta",
"response_id": "ID de resposta",
"responses": "Respostas",
"restart": "Reiniciar",
"role": "Função",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Atingiu o seu limite de {projectLimit} áreas de trabalho.",
"you_have_reached_your_monthly_miu_limit_of": "Atingiu o seu limite mensal de MIU de",
"you_have_reached_your_monthly_response_limit_of": "Atingiu o seu limite mensal de respostas de",
"you_will_be_downgraded_to_the_community_edition_on_date": "Será rebaixado para a Edição Comunitária em {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Será rebaixado para a Edição Comunitária em {date}."
},
"emails": {
"accept": "Aceitar",
@@ -956,32 +954,19 @@
"enterprise_features": "Funcionalidades da Empresa",
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenha uma licença Enterprise para ter acesso a todas as funcionalidades.",
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controlo total sobre a privacidade e segurança dos seus dados.",
"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_active": "Ativa",
"license_status_description": "Estado da sua licença empresarial.",
"license_status_expired": "Expirada",
"license_status_invalid": "Licença inválida",
"license_status_unreachable": "Inacessível",
"license_unreachable_grace_period": "Não é possível contactar o servidor de licenças. As suas funcionalidades empresariais permanecem ativas durante um período de tolerância de 3 dias que termina a {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sem necessidade de chamada, sem compromissos: Solicite uma licença de teste gratuita de 30 dias para testar todas as funcionalidades preenchendo este formulário:",
"no_credit_card_no_sales_call_just_test_it": "Sem cartão de crédito. Sem chamada de vendas. Apenas teste :)",
"on_request": "A pedido",
"organization_roles": "Funções da Organização (Administrador, Editor, Programador, etc.)",
"questions_please_reach_out_to": "Questões? Por favor entre em contacto com",
"recheck_license": "Verificar licença novamente",
"recheck_license_failed": "A verificação da licença falhou. O servidor de licenças pode estar inacessível.",
"recheck_license_invalid": "A chave de licença é inválida. Por favor, verifique a sua ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Verificação da licença bem-sucedida",
"recheck_license_unreachable": "O servidor de licenças está inacessível. Por favor, tenta novamente mais tarde.",
"rechecking": "A verificar novamente...",
"request_30_day_trial_license": "Solicitar Licença de Teste de 30 Dias",
"saml_sso": "SSO SAML",
"service_level_agreement": "Acordo de Nível de Serviço",
"soc2_hipaa_iso_27001_compliance_check": "Verificação de conformidade SOC2, HIPAA, ISO 27001",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Equipas e Funções de Acesso (Ler, Ler e Escrever, Gerir)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias.",
"your_enterprise_license_is_active_all_features_unlocked": "A sua licença Enterprise está ativa. Todas as funcionalidades desbloqueadas."
},
"general": {
"bulk_invite_warning_description": "No plano gratuito, todos os membros da organização são sempre atribuídos ao papel de \"Proprietário\".",
@@ -1005,7 +990,7 @@
"from_your_organization": "da sua organização",
"invitation_sent_once_more": "Convite enviado mais uma vez.",
"invite_deleted_successfully": "Convite eliminado com sucesso",
"invite_expires_on": "O convite expira em {date}",
"invited_on": "Convidado em {date}",
"invites_failed": "Convites falharam",
"leave_organization": "Sair da organização",
"leave_organization_description": "Vai sair desta organização e perder o acesso a todos os inquéritos e respostas. Só pode voltar a juntar-se se for convidado novamente.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Preencha todos os campos para adicionar um novo espaço de trabalho.",
"read": "Ler",
"read_write": "Ler e Escrever",
"select_member": "Selecionar membro",
"select_workspace": "Selecionar espaço de trabalho",
"team_admin": "Administrador da Equipa",
"team_created_successfully": "Equipa criada com sucesso.",
"team_deleted_successfully": "Equipa eliminada com sucesso.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Adicionar um espaço reservado para mostrar se não houver valor para recordar.",
"add_hidden_field_id": "Adicionar ID do campo oculto",
"add_highlight_border": "Adicionar borda de destaque",
"add_highlight_border_description": "Adicione uma borda externa ao seu cartão de inquérito.",
"add_logic": "Adicionar lógica",
"add_none_of_the_above": "Adicionar \"Nenhuma das Opções Acima\"",
"add_option": "Adicionar opção",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Alterar a mensagem que os visitantes veem quando o inquérito está fechado.",
"adjust_the_theme_in_the": "Ajustar o tema no",
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
"allow_file_type": "Permitir tipo de ficheiro",
"allow_multi_select": "Permitir seleção múltipla",
"allow_multiple_files": "Permitir vários ficheiros",
"allow_users_to_select_more_than_one_image": "Permitir aos utilizadores selecionar mais do que uma imagem",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Bloco duplicado.",
"bold": "Negrito",
"brand_color": "Cor da marca",
"brand_color_description": "Aplicado a botões, links e destaques.",
"brightness": "Brilho",
"bulk_edit": "Edição em massa",
"bulk_edit_description": "Edite todas as opções abaixo, uma por linha. Linhas vazias serão ignoradas e duplicados removidos.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Capturar nova ação",
"card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Inquéritos {surveyTypeDerived}",
"card_background_color": "Cor de fundo do cartão",
"card_background_color_description": "Preenche a área do cartão do inquérito.",
"card_border_color": "Cor da borda do cartão",
"card_border_color_description": "Contorna o cartão do inquérito.",
"card_styling": "Estilo de cartão",
"casual": "Casual",
"caution_edit_duplicate": "Duplicar e editar",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "As respostas mais antigas e mais recentes se misturam, o que pode levar a resumos de dados enganosos.",
"caution_recommendation": "Isso pode causar inconsistências de dados no resumo do inquérito. Recomendamos duplicar o inquérito em vez disso.",
"caution_text": "As alterações levarão a inconsistências",
"centered_modal_overlay_color": "Cor da sobreposição modal centralizada",
"change_anyway": "Alterar mesmo assim",
"change_background": "Alterar fundo",
"change_question_type": "Alterar tipo de pergunta",
"change_survey_type": "Alterar o tipo de inquérito afeta o acesso existente",
"change_the_background_color_of_the_card": "Alterar a cor de fundo do cartão",
"change_the_background_color_of_the_input_fields": "Alterar a cor de fundo dos campos de entrada",
"change_the_background_to_a_color_image_or_animation": "Altere o fundo para uma cor, imagem ou animação",
"change_the_border_color_of_the_card": "Alterar a cor da borda do cartão.",
"change_the_border_color_of_the_input_fields": "Alterar a cor da borda dos campos de entrada",
"change_the_border_radius_of_the_card_and_the_inputs": "Alterar o raio da borda do cartão e dos campos de entrada",
"change_the_brand_color_of_the_survey": "Alterar a cor da marca do inquérito",
"change_the_placement_of_this_survey": "Alterar a colocação deste inquérito.",
"change_the_question_color_of_the_survey": "Alterar a cor da pergunta do inquérito",
"changes_saved": "Alterações guardadas.",
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de inquérito afetará como ele pode ser partilhado. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
"character_limit_toggle_description": "Limitar o quão curta ou longa uma resposta pode ser.",
"character_limit_toggle_title": "Adicionar limites de caracteres",
"checkbox_label": "Rótulo da Caixa de Seleção",
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.",
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta no seu bloco",
@@ -1263,6 +1259,7 @@
"contact_fields": "Campos de Contacto",
"contains": "Contém",
"continue_to_settings": "Continuar para Definições",
"control_which_file_types_can_be_uploaded": "Controlar quais tipos de ficheiros podem ser carregados.",
"convert_to_multiple_choice": "Converter para Seleção Múltipla",
"convert_to_single_choice": "Converter para Seleção Única",
"country": "País",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Eliminar mesmo assim",
"delete_block": "Eliminar bloco",
"delete_choice": "Eliminar escolha",
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa do tempo de conclusão do inquérito",
"display_number_of_responses_for_survey": "Mostrar número de respostas do inquérito",
"display_type": "Tipo de exibição",
"divide": "Dividir /",
"does_not_contain": "Não contém",
"does_not_end_with": "Não termina com",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Não inclui todos de",
"does_not_include_one_of": "Não inclui um de",
"does_not_start_with": "Não começa com",
"dropdown": "Menu suspenso",
"duplicate_block": "Duplicar bloco",
"duplicate_question": "Duplicar pergunta",
"edit_link": "Editar link",
@@ -1381,6 +1375,7 @@
"hide_progress_bar": "Ocultar barra de progresso",
"hide_question_settings": "Ocultar definições da pergunta",
"hostname": "Nome do host",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}",
"if_you_need_more_please": "Se precisar de mais, por favor",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar a mostrar sempre que acionado até que uma resposta seja submetida.",
"ignore_global_waiting_time": "Ignorar período de espera",
@@ -1391,9 +1386,7 @@
"initial_value": "Valor inicial",
"inner_text": "Texto Interno",
"input_border_color": "Cor da borda do campo de entrada",
"input_border_color_description": "Contorna campos de texto e áreas de texto.",
"input_color": "Cor do campo de entrada",
"input_color_description": "Preenche o interior dos campos de texto.",
"insert_link": "Inserir ligação",
"invalid_targeting": "Segmentação inválida: Por favor, verifique os seus filtros de audiência",
"invalid_video_url_warning": "Por favor, insira um URL válido do YouTube, Vimeo ou Loom. Atualmente, não suportamos outros fornecedores de hospedagem de vídeo.",
@@ -1420,10 +1413,10 @@
"key": "Chave",
"last_name": "Apelido",
"let_people_upload_up_to_25_files_at_the_same_time": "Permitir que as pessoas carreguem até 25 ficheiros ao mesmo tempo.",
"limit_the_maximum_file_size": "Limitar o tamanho máximo de ficheiro para carregamentos.",
"limit_upload_file_size_to": "Limitar o tamanho de ficheiro de carregamento para",
"limit_file_types": "Limitar tipos de ficheiros",
"limit_the_maximum_file_size": "Limitar o tamanho máximo do ficheiro",
"limit_upload_file_size_to": "Limitar tamanho do ficheiro carregado a",
"link_survey_description": "Partilhe um link para uma página de inquérito ou incorpore-o numa página web ou email.",
"list": "Lista",
"load_segment": "Carregar segmento",
"logic_error_warning": "A alteração causará erros de lógica",
"logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta",
@@ -1434,8 +1427,8 @@
"manage_languages": "Gerir Idiomas",
"matrix_all_fields": "Todos os campos",
"matrix_rows": "Linhas",
"max_file_size": "Tamanho máximo de ficheiro",
"max_file_size_limit_is": "O limite de tamanho máximo de ficheiro é",
"max_file_size": "Tamanho máximo do ficheiro",
"max_file_size_limit_is": "O limite do tamanho máximo do ficheiro é",
"move_question_to_block": "Mover pergunta para o bloco",
"multiply": "Multiplicar *",
"needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com",
@@ -1467,6 +1460,7 @@
"picture_idx": "Imagem {idx}",
"pin_can_only_contain_numbers": "O PIN só pode conter números.",
"pin_must_be_a_four_digit_number": "O PIN deve ser um número de quatro dígitos.",
"please_enter_a_file_extension": "Por favor, insira uma extensão de ficheiro.",
"please_enter_a_valid_url": "Por favor, insira um URL válido (por exemplo, https://example.com)",
"please_set_a_survey_trigger": "Por favor, defina um desencadeador de inquérito",
"please_specify": "Por favor, especifique",
@@ -1477,11 +1471,11 @@
"protect_survey_with_pin_description": "Apenas utilizadores com o PIN podem aceder ao inquérito.",
"publish": "Publicar",
"question": "Pergunta",
"question_color": "Cor da pergunta",
"question_deleted": "Pergunta eliminada.",
"question_duplicated": "Pergunta duplicada.",
"question_id_updated": "ID da pergunta atualizado",
"question_used_in_logic_warning_text": "Os elementos deste bloco são utilizados numa regra de lógica, tem a certeza de que pretende eliminá-lo?",
"question_used_in_logic_warning_title": "Inconsistência de lógica",
"question_used_in_logic": "Esta pergunta é usada na lógica da pergunta {questionIndex}.",
"question_used_in_quota": "Esta pergunta está a ser usada na quota \"{quotaName}\"",
"question_used_in_recall": "Esta pergunta está a ser recordada na pergunta {questionIndex}.",
"question_used_in_recall_ending_card": "Esta pergunta está a ser recordada no Cartão de Conclusão",
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
"response_options": "Opções de Resposta",
"roundness": "Arredondamento",
"roundness_description": "Controla o arredondamento dos cantos do cartão.",
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
"rows": "Linhas",
"save_and_close": "Guardar e Fechar",
@@ -1546,7 +1539,6 @@
"search_for_images": "Procurar imagens",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos após o acionamento o inquérito será fechado se não houver resposta",
"seconds_before_showing_the_survey": "segundos antes de mostrar o inquérito.",
"select_field": "Selecionar campo",
"select_or_type_value": "Selecionar ou digitar valor",
"select_ordering": "Selecionar ordem",
"select_saved_action": "Selecionar ação guardada",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Estilo definido para estilos do tema",
"subheading": "Subtítulo",
"subtract": "Subtrair -",
"suggest_colors": "Sugerir cores",
"survey_completed_heading": "Inquérito Concluído",
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
"survey_display_settings": "Configurações de Exibição do Inquérito",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar uma única vez, mesmo que não respondam.",
"then": "Então",
"this_action_will_remove_all_the_translations_from_this_survey": "Esta ação irá remover todas as traduções deste inquérito.",
"this_extension_is_already_added": "Esta extensão já está adicionada.",
"this_file_type_is_not_supported": "Este tipo de ficheiro não é suportado.",
"three_points": "3 pontos",
"times": "tempos",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todos os questionários, pode",
@@ -1613,51 +1608,8 @@
"upper_label": "Etiqueta Superior",
"url_filters": "Filtros de URL",
"url_not_supported": "URL não suportado",
"validation": {
"add_validation_rule": "Adicionar regra de validação",
"answer_all_rows": "Responda a todas as linhas",
"characters": "Caracteres",
"contains": "Contém",
"delete_validation_rule": "Eliminar regra de validação",
"does_not_contain": "Não contém",
"email": "É um email válido",
"end_date": "Data de fim",
"file_extension_is": "A extensão do ficheiro é",
"file_extension_is_not": "A extensão do ficheiro não é",
"is": "É",
"is_between": "Está entre",
"is_earlier_than": "É anterior a",
"is_greater_than": "É maior que",
"is_later_than": "É posterior a",
"is_less_than": "É menor que",
"is_not": "Não é",
"is_not_between": "Não está entre",
"kb": "KB",
"max_length": "No máximo",
"max_selections": "No máximo",
"max_value": "No máximo",
"mb": "MB",
"min_length": "Pelo menos",
"min_selections": "Pelo menos",
"min_value": "Pelo menos",
"minimum_options_ranked": "Opções mínimas classificadas",
"minimum_rows_answered": "Linhas mínimas respondidas",
"options_selected": "Opções selecionadas",
"pattern": "Coincide com o padrão regex",
"phone": "É um telefone válido",
"rank_all_options": "Classificar todas as opções",
"select_file_extensions": "Selecionar extensões de ficheiro...",
"select_option": "Selecionar opção",
"start_date": "Data de início",
"url": "É um URL válido"
},
"validation_logic_and": "Todas são verdadeiras",
"validation_logic_or": "qualquer uma é verdadeira",
"validation_rules": "Regras de validação",
"validation_rules_description": "Aceitar apenas respostas que cumpram os seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "A variável \"{variableName}\" está a ser usada na quota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "O nome da variável está em conflito com um ID de campo oculto existente.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está a ser utilizada na quota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
"variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.",
"variable_used_in_recall": "Variável \"{variable}\" está a ser recordada na pergunta {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Adicionar cor de fundo",
"add_background_color_description": "Adicione uma cor de fundo ao contentor do logótipo.",
"advanced_styling_field_border_radius": "Raio da borda",
"advanced_styling_field_button_bg": "Fundo do botão",
"advanced_styling_field_button_bg_description": "Preenche o botão Seguinte / Submeter.",
"advanced_styling_field_button_border_radius_description": "Arredonda os cantos do botão.",
"advanced_styling_field_button_font_size_description": "Ajusta o tamanho do texto da etiqueta do botão.",
"advanced_styling_field_button_font_weight_description": "Torna o texto do botão mais leve ou mais negrito.",
"advanced_styling_field_button_height_description": "Controla a altura do botão.",
"advanced_styling_field_button_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_button_padding_y_description": "Adiciona espaço no topo e na base.",
"advanced_styling_field_button_text": "Texto do botão",
"advanced_styling_field_button_text_description": "Colore a etiqueta dentro dos botões.",
"advanced_styling_field_description_color": "Cor da descrição",
"advanced_styling_field_description_color_description": "Colore o texto abaixo de cada título.",
"advanced_styling_field_description_size": "Tamanho da fonte da descrição",
"advanced_styling_field_description_size_description": "Ajusta o tamanho do texto da descrição.",
"advanced_styling_field_description_weight": "Peso da fonte da descrição",
"advanced_styling_field_description_weight_description": "Torna o texto da descrição mais leve ou mais negrito.",
"advanced_styling_field_font_size": "Tamanho da fonte",
"advanced_styling_field_font_weight": "Peso da fonte",
"advanced_styling_field_headline_color": "Cor do título",
"advanced_styling_field_headline_color_description": "Colore o texto principal da pergunta.",
"advanced_styling_field_headline_size": "Tamanho da fonte do título",
"advanced_styling_field_headline_size_description": "Ajusta o tamanho do texto do título.",
"advanced_styling_field_headline_weight": "Peso da fonte do título",
"advanced_styling_field_headline_weight_description": "Torna o texto do título mais leve ou mais negrito.",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_indicator_bg": "Fundo do indicador",
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
"advanced_styling_field_input_height_description": "Controla a altura do campo de entrada.",
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_input_padding_y_description": "Adiciona espaço no topo e na base.",
"advanced_styling_field_input_placeholder_opacity_description": "Atenua o texto de sugestão do placeholder.",
"advanced_styling_field_input_shadow_description": "Adiciona uma sombra ao redor dos campos de entrada.",
"advanced_styling_field_input_text": "Texto de entrada",
"advanced_styling_field_input_text_description": "Colore o texto digitado nos campos de entrada.",
"advanced_styling_field_option_bg": "Fundo",
"advanced_styling_field_option_bg_description": "Preenche os itens de opção.",
"advanced_styling_field_option_border_radius_description": "Arredonda os cantos das opções.",
"advanced_styling_field_option_font_size_description": "Ajusta o tamanho do texto da etiqueta da opção.",
"advanced_styling_field_option_label": "Cor da etiqueta",
"advanced_styling_field_option_label_description": "Colore o texto da etiqueta da opção.",
"advanced_styling_field_option_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_option_padding_y_description": "Adiciona espaço no topo e na base.",
"advanced_styling_field_padding_x": "Espaçamento X",
"advanced_styling_field_padding_y": "Espaçamento Y",
"advanced_styling_field_placeholder_opacity": "Opacidade do marcador de posição",
"advanced_styling_field_shadow": "Sombra",
"advanced_styling_field_track_bg": "Fundo da faixa",
"advanced_styling_field_track_bg_description": "Colore a porção não preenchida da barra.",
"advanced_styling_field_track_height": "Altura da faixa",
"advanced_styling_field_track_height_description": "Controla a espessura da barra de progresso.",
"advanced_styling_field_upper_label_color": "Cor da etiqueta do título",
"advanced_styling_field_upper_label_color_description": "Colore a pequena etiqueta acima dos campos de entrada.",
"advanced_styling_field_upper_label_size": "Tamanho da fonte da etiqueta do título",
"advanced_styling_field_upper_label_size_description": "Ajusta o tamanho da pequena etiqueta acima dos campos de entrada.",
"advanced_styling_field_upper_label_weight": "Peso da fonte da etiqueta do título",
"advanced_styling_field_upper_label_weight_description": "Torna a etiqueta mais leve ou mais negrito.",
"advanced_styling_section_buttons": "Botões",
"advanced_styling_section_headlines": "Títulos e descrições",
"advanced_styling_section_inputs": "Campos de entrada",
"advanced_styling_section_options": "Opções (rádio/caixa de seleção)",
"app_survey_placement": "Colocação do inquérito (app)",
"app_survey_placement_settings_description": "Altere onde os inquéritos serão apresentados na sua aplicação web ou website.",
"centered_modal_overlay_color": "Cor da sobreposição modal centralizada",
"email_customization": "Personalização de e-mail",
"email_customization_description": "Altere a aparência dos e-mails que a Formbricks envia em seu nome.",
"enable_custom_styling": "Ativar estilização personalizada",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "A marca Formbricks está oculta.",
"formbricks_branding_settings_description": "Adoramos o seu apoio, mas compreendemos se preferir desativar.",
"formbricks_branding_shown": "A marca Formbricks está visível.",
"generate_theme_btn": "Gerar",
"generate_theme_confirmation": "Gostarias de gerar um tema de cores correspondente com base na cor da tua marca? Isto irá substituir as tuas definições de cor atuais.",
"generate_theme_header": "Gerar tema de cores?",
"logo_removed_successfully": "Logótipo removido com sucesso",
"logo_settings_description": "Carregue o logótipo da sua empresa para personalizar inquéritos e pré-visualizações de links.",
"logo_updated_successfully": "Logótipo atualizado com sucesso",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Mostrar marca Formbricks em inquéritos {type}",
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo atualizado com sucesso",
"suggest_colors": "Sugerir cores",
"theme": "Tema",
"theme_settings_description": "Crie um tema de estilo para todos os inquéritos. Pode ativar estilos personalizados para cada inquérito."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Sim, mantenha-me informado.",
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
"preview_survey_question_2_headline": "Quer manter-se atualizado?",
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
"preview_survey_welcome_card_headline": "Bem-vindo!",
"prioritize_features_description": "Identifique as funcionalidades que os seus utilizadores precisam mais e menos.",
"prioritize_features_name": "Priorizar Funcionalidades",
+38 -153
View File
@@ -243,6 +243,7 @@
"imprint": "Amprentă",
"in_progress": "În progres",
"inactive_surveys": "Sondaje inactive",
"input_type": "Tipul de intrare",
"integration": "integrare",
"integrations": "Integrări",
"invalid_date": "Dată invalidă",
@@ -254,7 +255,6 @@
"label": "Etichetă",
"language": "Limba",
"learn_more": "Află mai multe",
"license_expired": "License Expired",
"light_overlay": "Suprapunere ușoară",
"limits_reached": "Limite atinse",
"link": "Legătura",
@@ -267,11 +267,13 @@
"look_and_feel": "Aspect și Comportament",
"manage": "Gestionați",
"marketing": "Marketing",
"maximum": "Maximum",
"member": "Membru",
"members": "Membri",
"members_and_teams": "Membri și echipe",
"membership_not_found": "Apartenența nu a fost găsită",
"metadata": "Metadate",
"minimum": "Minim",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funcționează cel mai bine pe un ecran mai mare. Pentru a gestiona sau crea chestionare, treceți la un alt dispozitiv.",
"mobile_overlay_surveys_look_good": "Nu vă faceți griji chestionarele dumneavoastră arată grozav pe orice dispozitiv și dimensiune a ecranului!",
"mobile_overlay_title": "Ups, ecran mic detectat!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Nu a fost găsită nicio imagine de fundal.",
"no_code": "Fără Cod",
"no_files_uploaded": "Nu au fost încărcate fișiere",
"no_overlay": "Fără overlay",
"no_quotas_found": "Nicio cotă găsită",
"no_result_found": "Niciun rezultat găsit",
"no_results": "Nicio rezultat",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Echipele organizației nu au fost găsite",
"other": "Altele",
"others": "Altele",
"overlay_color": "Culoare overlay",
"overview": "Prezentare generală",
"password": "Parolă",
"paused": "Pauză",
@@ -326,7 +326,7 @@
"placeholder": "Marcaj substituent",
"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_upgrade_your_plan": "Vă rugăm să faceți upgrade la planul dumneavoastră",
"please_upgrade_your_plan": "Vă rugăm să vă actualizați planul.",
"preview": "Previzualizare",
"preview_survey": "Previzualizare Chestionar",
"privacy": "Politica de Confidențialitate",
@@ -352,7 +352,6 @@
"request_trial_license": "Solicitați o licență de încercare",
"reset_to_default": "Revino la implicit",
"response": "Răspuns",
"response_id": "ID răspuns",
"responses": "Răspunsuri",
"restart": "Repornește",
"role": "Rolul",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Ați atins limita de {projectLimit} spații de lucru.",
"you_have_reached_your_monthly_miu_limit_of": "Ați atins limita lunară MIU de",
"you_have_reached_your_monthly_response_limit_of": "Ați atins limita lunară de răspunsuri de",
"you_will_be_downgraded_to_the_community_edition_on_date": "Vei fi retrogradat la ediția Community pe {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Vei fi retrogradat la ediția Community pe {date}."
},
"emails": {
"accept": "Acceptă",
@@ -956,32 +954,19 @@
"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.",
"keep_full_control_over_your_data_privacy_and_security": "Mențineți controlul complet asupra confidențialității și securității datelor dumneavoastră.",
"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_active": "Activă",
"license_status_description": "Starea licenței tale enterprise.",
"license_status_expired": "Expirată",
"license_status_invalid": "Licență invalidă",
"license_status_unreachable": "Indisponibilă",
"license_unreachable_grace_period": "Serverul de licențe nu poate fi contactat. Funcționalitățile enterprise rămân active timp de 3 zile, până la data de {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Nicio apel necesar, fără obligații: Solicitați o licență de probă gratuită de 30 de zile pentru a testa toate funcțiile prin completarea acestui formular:",
"no_credit_card_no_sales_call_just_test_it": "Nu este nevoie de card de credit. Fără apeluri de vânzări. Doar testează-l :)",
"on_request": "La cerere",
"organization_roles": "Roluri organizaționale (Administrator, Editor, Dezvoltator, etc.)",
"questions_please_reach_out_to": "Întrebări? Vă rugăm să trimiteți mesaj către",
"recheck_license": "Verifică din nou licența",
"recheck_license_failed": "Verificarea licenței a eșuat. Serverul de licențe poate fi indisponibil.",
"recheck_license_invalid": "Cheia de licență este invalidă. Te rugăm să verifici variabila ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Licența a fost verificată cu succes",
"recheck_license_unreachable": "Serverul de licențe este indisponibil. Te rugăm să încerci din nou mai târziu.",
"rechecking": "Se verifică din nou...",
"request_30_day_trial_license": "Solicitați o licență de încercare de 30 de zile",
"saml_sso": "SAML SSO",
"service_level_agreement": "Acord privind nivelul de servicii",
"soc2_hipaa_iso_27001_compliance_check": "Verificare conformitate SOC2, HIPAA, ISO 27001",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Echipe & Roluri de Acces (Citiți, Citiți și Scrieți, Gestionați)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Deblocați puterea completă a Formbricks. Gratuit timp de 30 de zile."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Deblocați puterea completă a Formbricks. Gratuit timp de 30 de zile.",
"your_enterprise_license_is_active_all_features_unlocked": "Licența dvs. Enterprise este activă. Toate funcțiile sunt deblocate."
},
"general": {
"bulk_invite_warning_description": "În planul gratuit, toți membrii organizației sunt întotdeauna alocați rolului „Proprietar”.",
@@ -1005,7 +990,7 @@
"from_your_organization": "din organizația ta",
"invitation_sent_once_more": "Invitație trimisă din nou.",
"invite_deleted_successfully": "Invitație ștearsă cu succes",
"invite_expires_on": "Invitația expiră pe {date}",
"invited_on": "Invitat pe {date}",
"invites_failed": "Invitații eșuate",
"leave_organization": "Părăsește organizația",
"leave_organization_description": "Vei părăsi această organizație și vei pierde accesul la toate sondajele și răspunsurile. Poți să te alături din nou doar dacă ești invitat.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un nou spațiu de lucru.",
"read": "Citește",
"read_write": "Citire & Scriere",
"select_member": "Selectează membrul",
"select_workspace": "Selectați spațiul de lucru",
"team_admin": "Administrator Echipe",
"team_created_successfully": "Echipă creată cu succes",
"team_deleted_successfully": "Echipă ștearsă cu succes.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Adaugă un placeholder pentru a afișa dacă nu există valoare de reamintit",
"add_hidden_field_id": "Adăugați ID câmp ascuns",
"add_highlight_border": "Adaugă bordură evidențiată",
"add_highlight_border_description": "Adaugă o margine exterioară cardului tău de sondaj.",
"add_logic": "Adaugă logică",
"add_none_of_the_above": "Adăugați \"Niciuna dintre cele de mai sus\"",
"add_option": "Adăugați opțiune",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Schimbați mesajul pe care îl văd vizitatorii atunci când sondajul este închis.",
"adjust_the_theme_in_the": "Ajustați tema în",
"all_other_answers_will_continue_to": "Toate celelalte răspunsuri vor continua să",
"allow_file_type": "Permite tipul de fișier",
"allow_multi_select": "Permite selectare multiplă",
"allow_multiple_files": "Permite fișiere multiple",
"allow_users_to_select_more_than_one_image": "Permite utilizatorilor să selecteze mai mult de o imagine",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Bloc duplicat.",
"bold": "Îngroșat",
"brand_color": "Culoarea brandului",
"brand_color_description": "Se aplică pe butoane, linkuri și evidențieri.",
"brightness": "Luminozitate",
"bulk_edit": "Editare în bloc",
"bulk_edit_description": "Editați toate opțiunile de mai jos, câte una pe linie. Liniile goale vor fi omise, iar duplicatele vor fi eliminate.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Capturați acțiune nouă",
"card_arrangement_for_survey_type_derived": "Aranjament de carduri pentru sondaje de tip {surveyTypeDerived}",
"card_background_color": "Culoarea de fundal a cardului",
"card_background_color_description": "Umple zona cardului de sondaj.",
"card_border_color": "Culoarea bordurii cardului",
"card_border_color_description": "Conturează cardul sondajului.",
"card_styling": "Stilizare card",
"casual": "Casual",
"caution_edit_duplicate": "Duplică & editează",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Răspunsurile mai vechi și mai noi se amestecă, ceea ce poate duce la rezumate de date înșelătoare.",
"caution_recommendation": "Aceasta poate cauza inconsistențe de date în rezultatul sondajului. Vă recomandăm să duplicați sondajul în schimb.",
"caution_text": "Schimbările vor duce la inconsecvențe",
"centered_modal_overlay_color": "Culoare suprapunere modală centralizată",
"change_anyway": "Schimbă oricum",
"change_background": "Schimbați fundalul",
"change_question_type": "Schimbă tipul întrebării",
"change_survey_type": "Schimbarea tipului chestionarului afectează accesul existent",
"change_the_background_color_of_the_card": "Schimbați culoarea de fundal a cardului.",
"change_the_background_color_of_the_input_fields": "Schimbați culoarea de fundal a câmpurilor de introducere.",
"change_the_background_to_a_color_image_or_animation": "Schimbați fundalul cu o culoare, imagine sau animație.",
"change_the_border_color_of_the_card": "Schimbați culoarea bordurii cardului.",
"change_the_border_color_of_the_input_fields": "Schimbați culoarea bordurii câmpurilor de introducere.",
"change_the_border_radius_of_the_card_and_the_inputs": "Schimbați raza de rotunjire a cardului și a câmpurilor de introducere.",
"change_the_brand_color_of_the_survey": "Schimbați culoarea brandului chestionarului",
"change_the_placement_of_this_survey": "Schimbă amplasarea acestui sondaj.",
"change_the_question_color_of_the_survey": "Schimbați culoarea întrebării chestionarului.",
"changes_saved": "Modificările au fost salvate",
"changing_survey_type_will_remove_existing_distribution_channels": "Schimbarea tipului chestionarului va afecta modul în care acesta poate fi distribuit. Dacă respondenții au deja linkuri de acces pentru tipul curent, aceștia ar putea pierde accesul după schimbare.",
"character_limit_toggle_description": "Limitați cât de scurt sau lung poate fi un răspuns.",
"character_limit_toggle_title": "Adăugați limite de caractere",
"checkbox_label": "Etichetă casetă de selectare",
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
"choose_the_first_question_on_your_block": "Alege prima întrebare din blocul tău",
@@ -1263,6 +1259,7 @@
"contact_fields": "Câmpuri de contact",
"contains": "Conține",
"continue_to_settings": "Continuă către Setări",
"control_which_file_types_can_be_uploaded": "Controlează ce tipuri de fișiere pot fi încărcate.",
"convert_to_multiple_choice": "Convertiți la selectare multiplă",
"convert_to_single_choice": "Convertiți la selectare unică",
"country": "Țară",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Șterge oricum",
"delete_block": "Șterge blocul",
"delete_choice": "Șterge alegerea",
"disable_the_visibility_of_survey_progress": "Dezactivați vizibilitatea progresului sondajului",
"display_an_estimate_of_completion_time_for_survey": "Afișează o estimare a timpului de finalizare pentru sondaj",
"display_number_of_responses_for_survey": "Afișează numărul de răspunsuri pentru sondaj",
"display_type": "Tip de afișare",
"divide": "Împarte /",
"does_not_contain": "Nu conține",
"does_not_end_with": "Nu se termină cu",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Nu include toate",
"does_not_include_one_of": "Nu include una dintre",
"does_not_start_with": "Nu începe cu",
"dropdown": "Dropdown",
"duplicate_block": "Duplicați blocul",
"duplicate_question": "Duplică întrebarea",
"edit_link": "Editare legătură",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "Ascunde bara de progres",
"hide_question_settings": "Ascunde setările întrebării",
"hostname": "Nume gazdă",
"if_you_need_more_please": "Dacă aveți nevoie de mai mult, vă rugăm",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}",
"if_you_need_more_please": "Dacă aveți nevoie de mai multe, vă rugăm să",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuă afișarea ori de câte ori este declanșat până când se trimite un răspuns.",
"ignore_global_waiting_time": "Ignoră perioada de răcire",
"ignore_global_waiting_time_description": "Acest sondaj poate fi afișat ori de câte ori condițiile sale sunt îndeplinite, chiar dacă un alt sondaj a fost afișat recent.",
@@ -1391,9 +1386,7 @@
"initial_value": "Valoare inițială",
"inner_text": "Text Interior",
"input_border_color": "Culoarea graniței câmpului de introducere",
"input_border_color_description": "Conturează câmpurile de text și zonele de text.",
"input_color": "Culoarea câmpului de introducere",
"input_color_description": "Umple interiorul câmpurilor de text.",
"insert_link": "Inserează link",
"invalid_targeting": "\"Targetare nevalidă: Vă rugăm să verificați filtrele pentru audiență\"",
"invalid_video_url_warning": "Vă rugăm să introduceți un URL valid de YouTube, Vimeo sau Loom. În prezent nu susținem alți furnizori de găzduire video.",
@@ -1420,10 +1413,10 @@
"key": "Cheie",
"last_name": "Nume de familie",
"let_people_upload_up_to_25_files_at_the_same_time": "Permiteți utilizatorilor să încarce până la 25 de fișiere simultan.",
"limit_the_maximum_file_size": "Limitați dimensiunea maximă a fișierului pentru încărcări.",
"limit_upload_file_size_to": "Limitați dimensiunea fișierului încărcat la",
"limit_file_types": "Limitare tipuri de fișiere",
"limit_the_maximum_file_size": "Limitează dimensiunea maximă a fișierului",
"limit_upload_file_size_to": "Limitați dimensiunea fișierului de încărcare la",
"link_survey_description": "Partajați un link către o pagină de chestionar sau încorporați-l într-o pagină web sau email.",
"list": "Listă",
"load_segment": "Încarcă segment",
"logic_error_warning": "Schimbarea va provoca erori de logică",
"logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare",
@@ -1435,7 +1428,7 @@
"matrix_all_fields": "Toate câmpurile",
"matrix_rows": "Rânduri",
"max_file_size": "Dimensiune maximă fișier",
"max_file_size_limit_is": "Limita maximă pentru dimensiunea fișierului este",
"max_file_size_limit_is": "Limita dimensiunii maxime a fișierului este",
"move_question_to_block": "Mută întrebarea în bloc",
"multiply": "Multiplicare",
"needed_for_self_hosted_cal_com_instance": "Necesar pentru un exemplu autogăzduit Cal.com",
@@ -1467,6 +1460,7 @@
"picture_idx": "Poză {idx}",
"pin_can_only_contain_numbers": "PIN-ul poate conține doar numere.",
"pin_must_be_a_four_digit_number": "PIN-ul trebuie să fie un număr de patru cifre",
"please_enter_a_file_extension": "Vă rugăm să introduceți o extensie de fișier.",
"please_enter_a_valid_url": "Vă rugăm să introduceți un URL valid (de exemplu, https://example.com)",
"please_set_a_survey_trigger": "Vă rugăm să setați un declanșator sondaj",
"please_specify": "Vă rugăm să specificați",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Doar utilizatorii care cunosc PIN-ul pot accesa sondajul.",
"publish": "Publică",
"question": "Întrebare",
"question_color": "Culoarea întrebării",
"question_deleted": "Întrebare ștearsă.",
"question_duplicated": "Întrebare duplicată.",
"question_id_updated": "ID întrebare actualizat",
"question_used_in_logic_warning_text": "Elemente din acest bloc sunt folosite într-o regulă de logică. Sigur doriți să îl ștergeți?",
"question_used_in_logic_warning_title": "Inconsistență logică",
"question_used_in_quota": "Întrebarea aceasta este folosită în cota „{quotaName}”",
"question_used_in_logic": "Această întrebare este folosită în logica întrebării {questionIndex}.",
"question_used_in_quota": "Întrebarea aceasta este folosită în cota \"{quotaName}\"",
"question_used_in_recall": "Această întrebare este reamintită în întrebarea {questionIndex}.",
"question_used_in_recall_ending_card": "Această întrebare este reamintită în Cardul de Încheiere.",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Limite de răspunsuri, redirecționări și altele.",
"response_options": "Opțiuni răspuns",
"roundness": "Rotunjire",
"roundness_description": "Controlează cât de rotunjite sunt colțurile cardului.",
"row_used_in_logic_error": "Această linie este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
"rows": "Rânduri",
"save_and_close": "Salvează & Închide",
@@ -1546,7 +1539,6 @@
"search_for_images": "Căutare de imagini",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "secunde după declanșare sondajul va fi închis dacă nu există niciun răspuns",
"seconds_before_showing_the_survey": "secunde înainte de afișarea sondajului",
"select_field": "Selectează câmpul",
"select_or_type_value": "Selectați sau introduceți valoarea",
"select_ordering": "Selectează ordonarea",
"select_saved_action": "Selectați acțiunea salvată",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Stilizare setată la stilurile temei",
"subheading": "Subtitlu",
"subtract": "Scade -",
"suggest_colors": "Sugerați culori",
"survey_completed_heading": "Sondaj Completat",
"survey_completed_subheading": "Acest sondaj gratuit și open-source a fost închis",
"survey_display_settings": "Setări de afișare a sondajului",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Afișează o singură dată, chiar dacă persoana nu răspunde.",
"then": "Apoi",
"this_action_will_remove_all_the_translations_from_this_survey": "Această acțiune va elimina toate traducerile din acest sondaj.",
"this_extension_is_already_added": "Această extensie este deja adăugată.",
"this_file_type_is_not_supported": "Acest tip de fișier nu este acceptat.",
"three_points": "3 puncte",
"times": "ori",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Pentru a menține amplasarea consecventă pentru toate sondajele, puteți",
@@ -1613,51 +1608,8 @@
"upper_label": "Etichetă superioară",
"url_filters": "Filtre URL",
"url_not_supported": "URL nesuportat",
"validation": {
"add_validation_rule": "Adaugă regulă de validare",
"answer_all_rows": "Răspunde la toate rândurile",
"characters": "Caractere",
"contains": "Conține",
"delete_validation_rule": "Șterge regula de validare",
"does_not_contain": "Nu conține",
"email": "Este un email valid",
"end_date": "Data de sfârșit",
"file_extension_is": "Extensia fișierului este",
"file_extension_is_not": "Extensia fișierului nu este",
"is": "Este",
"is_between": "Este între",
"is_earlier_than": "Este mai devreme decât",
"is_greater_than": "Este mai mare decât",
"is_later_than": "Este mai târziu decât",
"is_less_than": "Este mai mic decât",
"is_not": "Nu este",
"is_not_between": "Nu este între",
"kb": "KB",
"max_length": "Cel mult",
"max_selections": "Cel mult",
"max_value": "Cel mult",
"mb": "MB",
"min_length": "Cel puțin",
"min_selections": "Cel puțin",
"min_value": "Cel puțin",
"minimum_options_ranked": "Număr minim de opțiuni ordonate",
"minimum_rows_answered": "Număr minim de rânduri completate",
"options_selected": "Opțiuni selectate",
"pattern": "Se potrivește cu un șablon regex",
"phone": "Este un număr de telefon valid",
"rank_all_options": "Ordonați toate opțiunile",
"select_file_extensions": "Selectați extensiile de fișier...",
"select_option": "Selectează opțiunea",
"start_date": "Data de început",
"url": "Este un URL valid"
},
"validation_logic_and": "Toate sunt adevărate",
"validation_logic_or": "oricare este adevărată",
"validation_rules": "Reguli de validare",
"validation_rules_description": "Acceptă doar răspunsurile care îndeplinesc următoarele criterii",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} este folosit în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabila {variableName} este folosită în cota {quotaName}”. Vă rugăm să o eliminați mai întâi din cotă",
"variable_name_conflicts_with_hidden_field": "Numele variabilei intră în conflict cu un ID de câmp ascuns existent.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabila \"{variableName}\" este folosită în cota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "Numele variabilei este deja utilizat, vă rugăm să alegeți altul.",
"variable_name_must_start_with_a_letter": "Numele variabilei trebuie să înceapă cu o literă.",
"variable_used_in_recall": "Variabila \"{variable}\" este reamintită în întrebarea {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Adăugați culoare de fundal",
"add_background_color_description": "Adăugați o culoare de fundal la containerul siglei.",
"advanced_styling_field_border_radius": "Raza colțurilor",
"advanced_styling_field_button_bg": "Fundal buton",
"advanced_styling_field_button_bg_description": "Umple butonul Următor / Trimite.",
"advanced_styling_field_button_border_radius_description": "Rotunjește colțurile butonului.",
"advanced_styling_field_button_font_size_description": "Scalează textul etichetei butonului.",
"advanced_styling_field_button_font_weight_description": "Face textul butonului mai subțire sau mai îngroșat.",
"advanced_styling_field_button_height_description": "Controlează înălțimea butonului.",
"advanced_styling_field_button_padding_x_description": "Adaugă spațiu la stânga și la dreapta.",
"advanced_styling_field_button_padding_y_description": "Adaugă spațiu sus și jos.",
"advanced_styling_field_button_text": "Text buton",
"advanced_styling_field_button_text_description": "Colorează eticheta din interiorul butoanelor.",
"advanced_styling_field_description_color": "Culoare descriere",
"advanced_styling_field_description_color_description": "Colorează textul de sub fiecare titlu.",
"advanced_styling_field_description_size": "Mărime font descriere",
"advanced_styling_field_description_size_description": "Scalează textul descrierii.",
"advanced_styling_field_description_weight": "Grosime font descriere",
"advanced_styling_field_description_weight_description": "Face textul descrierii mai subțire sau mai îngroșat.",
"advanced_styling_field_font_size": "Mărime font",
"advanced_styling_field_font_weight": "Grosime font",
"advanced_styling_field_headline_color": "Culoare titlu",
"advanced_styling_field_headline_color_description": "Colorează textul principal al întrebării.",
"advanced_styling_field_headline_size": "Mărime font titlu",
"advanced_styling_field_headline_size_description": "Scalează textul titlului.",
"advanced_styling_field_headline_weight": "Grosime font titlu",
"advanced_styling_field_headline_weight_description": "Face textul titlului mai subțire sau mai îngroșat.",
"advanced_styling_field_height": "Înălțime",
"advanced_styling_field_indicator_bg": "Fundal indicator",
"advanced_styling_field_indicator_bg_description": "Colorează partea umplută a barei.",
"advanced_styling_field_input_border_radius_description": "Rotunjește colțurile câmpurilor de introducere.",
"advanced_styling_field_input_font_size_description": "Scalează textul introdus în câmpuri.",
"advanced_styling_field_input_height_description": "Controlează înălțimea câmpului de introducere.",
"advanced_styling_field_input_padding_x_description": "Adaugă spațiu la stânga și la dreapta.",
"advanced_styling_field_input_padding_y_description": "Adaugă spațiu deasupra și dedesubt.",
"advanced_styling_field_input_placeholder_opacity_description": "Estompează textul de sugestie din placeholder.",
"advanced_styling_field_input_shadow_description": "Adaugă o umbră în jurul câmpurilor de introducere.",
"advanced_styling_field_input_text": "Text câmp",
"advanced_styling_field_input_text_description": "Colorează textul introdus în câmpuri.",
"advanced_styling_field_option_bg": "Fundal",
"advanced_styling_field_option_bg_description": "Umple elementele de opțiune.",
"advanced_styling_field_option_border_radius_description": "Rotunjește colțurile opțiunilor.",
"advanced_styling_field_option_font_size_description": "Redimensionează textul etichetei opțiunii.",
"advanced_styling_field_option_label": "Culoare etichetă",
"advanced_styling_field_option_label_description": "Colorează textul etichetei opțiunii.",
"advanced_styling_field_option_padding_x_description": "Adaugă spațiu în stânga și în dreapta.",
"advanced_styling_field_option_padding_y_description": "Adaugă spațiu deasupra și dedesubt.",
"advanced_styling_field_padding_x": "Spațiere X",
"advanced_styling_field_padding_y": "Spațiere Y",
"advanced_styling_field_placeholder_opacity": "Opacitate placeholder",
"advanced_styling_field_shadow": "Umbră",
"advanced_styling_field_track_bg": "Fundal track",
"advanced_styling_field_track_bg_description": "Colorează partea necompletată a barei.",
"advanced_styling_field_track_height": "Înălțime track",
"advanced_styling_field_track_height_description": "Controlează grosimea barei de progres.",
"advanced_styling_field_upper_label_color": "Culoare etichetă titlu",
"advanced_styling_field_upper_label_color_description": "Colorează eticheta mică de deasupra câmpurilor.",
"advanced_styling_field_upper_label_size": "Mărime font etichetă titlu",
"advanced_styling_field_upper_label_size_description": "Redimensionează eticheta mică de deasupra câmpurilor.",
"advanced_styling_field_upper_label_weight": "Grosime font etichetă titlu",
"advanced_styling_field_upper_label_weight_description": "Face eticheta mai subțire sau mai îngroșată.",
"advanced_styling_section_buttons": "Butoane",
"advanced_styling_section_headlines": "Titluri și descrieri",
"advanced_styling_section_inputs": "Inputuri",
"advanced_styling_section_options": "Opțiuni (Radio/Checkbox)",
"app_survey_placement": "Amplasarea sondajului în aplicație",
"app_survey_placement_settings_description": "Schimbați unde vor fi afișate sondajele în aplicația sau site-ul dvs. web.",
"centered_modal_overlay_color": "Culoare suprapunere modală centralizată",
"email_customization": "Personalizare email",
"email_customization_description": "Schimbați aspectul și stilul emailurilor trimise de Formbricks în numele dvs.",
"enable_custom_styling": "Activați stilizarea personalizată",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Brandingul Formbricks este ascuns.",
"formbricks_branding_settings_description": "Ne bucurăm de susținerea ta, dar înțelegem dacă vrei să dezactivezi această opțiune.",
"formbricks_branding_shown": "Brandingul Formbricks este afișat.",
"generate_theme_btn": "Generează",
"generate_theme_confirmation": "Vrei să generezi o temă de culori potrivită pe baza culorii brandului tău? Aceasta va suprascrie setările actuale de culoare.",
"generate_theme_header": "Generezi temă de culori?",
"logo_removed_successfully": "Sigla a fost eliminată cu succes",
"logo_settings_description": "Încarcă sigla companiei pentru a personaliza sondajele și previzualizările de linkuri.",
"logo_updated_successfully": "Sigla a fost actualizată cu succes",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Afișează brandingul Formbricks în sondajele de tip {type}",
"show_powered_by_formbricks": "Afișează semnătura „Powered by Formbricks”",
"styling_updated_successfully": "Stilizarea a fost actualizată cu succes",
"suggest_colors": "Sugerează culori",
"theme": "Temă",
"theme_settings_description": "Creează o temă de stil pentru toate sondajele. Poți activa stilizare personalizată pentru fiecare sondaj."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Da, ține-mă informat.",
"preview_survey_question_2_choice_2_label": "Nu, mulţumesc!",
"preview_survey_question_2_headline": "Vrei să fii în temă?",
"preview_survey_question_2_subheader": "Aceasta este o descriere exemplu.",
"preview_survey_welcome_card_headline": "Bun venit!",
"prioritize_features_description": "Identificați caracteristicile de care utilizatorii dumneavoastră au cel mai mult și cel mai puțin nevoie.",
"prioritize_features_name": "Prioritizați caracteristicile",
+36 -151
View File
@@ -243,6 +243,7 @@
"imprint": "Выходные данные",
"in_progress": "В процессе",
"inactive_surveys": "Неактивные опросы",
"input_type": "Тип ввода",
"integration": "интеграция",
"integrations": "Интеграции",
"invalid_date": "Неверная дата",
@@ -254,7 +255,6 @@
"label": "Метка",
"language": "Язык",
"learn_more": "Подробнее",
"license_expired": "License Expired",
"light_overlay": "Светлый оверлей",
"limits_reached": "Достигнуты лимиты",
"link": "Ссылка",
@@ -267,11 +267,13 @@
"look_and_feel": "Внешний вид",
"manage": "Управление",
"marketing": "Маркетинг",
"maximum": "Максимум",
"member": "Участник",
"members": "Участники",
"members_and_teams": "Участники и команды",
"membership_not_found": "Участие не найдено",
"metadata": "Метаданные",
"minimum": "Минимум",
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
"mobile_overlay_title": "Ой, обнаружен маленький экран!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Фоновое изображение не найдено.",
"no_code": "Нет кода",
"no_files_uploaded": "Файлы не были загружены",
"no_overlay": "Без наложения",
"no_quotas_found": "Квоты не найдены",
"no_result_found": "Результат не найден",
"no_results": "Нет результатов",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Команды организации не найдены",
"other": "Другое",
"others": "Другие",
"overlay_color": "Цвет наложения",
"overview": "Обзор",
"password": "Пароль",
"paused": "Приостановлено",
@@ -326,7 +326,7 @@
"placeholder": "Заполнитель",
"please_select_at_least_one_survey": "Пожалуйста, выберите хотя бы один опрос",
"please_select_at_least_one_trigger": "Пожалуйста, выберите хотя бы один триггер",
"please_upgrade_your_plan": "Пожалуйста, обновите ваш тарифный план",
"please_upgrade_your_plan": "Пожалуйста, обновите ваш тарифный план.",
"preview": "Предпросмотр",
"preview_survey": "Предпросмотр опроса",
"privacy": "Политика конфиденциальности",
@@ -352,7 +352,6 @@
"request_trial_license": "Запросить пробную лицензию",
"reset_to_default": "Сбросить по умолчанию",
"response": "Ответ",
"response_id": "ID ответа",
"responses": "Ответы",
"restart": "Перезапустить",
"role": "Роль",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Вы достигли лимита в {projectLimit} рабочих пространств.",
"you_have_reached_your_monthly_miu_limit_of": "Вы достигли месячного лимита MIU:",
"you_have_reached_your_monthly_response_limit_of": "Вы достигли месячного лимита ответов:",
"you_will_be_downgraded_to_the_community_edition_on_date": "Ваша версия будет понижена до Community Edition {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Ваша версия будет понижена до Community Edition {date}."
},
"emails": {
"accept": "Принять",
@@ -956,32 +954,19 @@
"enterprise_features": "Функции для предприятий",
"get_an_enterprise_license_to_get_access_to_all_features": "Получите корпоративную лицензию для доступа ко всем функциям.",
"keep_full_control_over_your_data_privacy_and_security": "Полный контроль над конфиденциальностью и безопасностью ваших данных.",
"license_invalid_description": "Ключ лицензии в переменной окружения ENTERPRISE_LICENSE_KEY недействителен. Проверь, нет ли опечаток, или запроси новый ключ.",
"license_status": "Статус лицензии",
"license_status_active": "Активна",
"license_status_description": "Статус вашей корпоративной лицензии.",
"license_status_expired": "Срок действия истёк",
"license_status_invalid": "Недействительная лицензия",
"license_status_unreachable": "Недоступна",
"license_unreachable_grace_period": "Не удаётся подключиться к серверу лицензий. Корпоративные функции останутся активными в течение 3-дневного льготного периода, который закончится {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Без звонков и обязательств: запросите бесплатную 30-дневную пробную лицензию для тестирования всех функций, заполнив эту форму:",
"no_credit_card_no_sales_call_just_test_it": "Без кредитной карты. Без звонков от отдела продаж. Просто попробуйте :)",
"on_request": "По запросу",
"organization_roles": "Роли в организации (администратор, редактор, разработчик и др.)",
"questions_please_reach_out_to": "Вопросы? Свяжитесь с",
"recheck_license": "Проверить лицензию ещё раз",
"recheck_license_failed": "Не удалось проверить лицензию. Сервер лицензий может быть недоступен.",
"recheck_license_invalid": "Ключ лицензии недействителен. Пожалуйста, проверь свою переменную ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Проверка лицензии прошла успешно",
"recheck_license_unreachable": "Сервер лицензий недоступен. Пожалуйста, попробуй позже.",
"rechecking": "Проверка...",
"request_30_day_trial_license": "Запросить 30-дневную пробную лицензию",
"saml_sso": "SAML SSO",
"service_level_agreement": "Соглашение об уровне обслуживания (SLA)",
"soc2_hipaa_iso_27001_compliance_check": "Проверка соответствия SOC2, HIPAA, ISO 27001",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Команды и роли доступа (чтение, чтение и запись, управление)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Откройте все возможности Formbricks. Бесплатно на 30 дней."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Откройте все возможности Formbricks. Бесплатно на 30 дней.",
"your_enterprise_license_is_active_all_features_unlocked": "Ваша корпоративная лицензия активна. Все функции разблокированы."
},
"general": {
"bulk_invite_warning_description": "В бесплатном тарифе всем участникам организации всегда назначается роль \"Владелец\".",
@@ -1005,7 +990,7 @@
"from_your_organization": "из вашей организации",
"invitation_sent_once_more": "Приглашение отправлено ещё раз.",
"invite_deleted_successfully": "Приглашение успешно удалено",
"invite_expires_on": "Приглашение истекает {date}",
"invited_on": "Приглашён {date}",
"invites_failed": "Не удалось отправить приглашения",
"leave_organization": "Покинуть организацию",
"leave_organization_description": "Вы покинете эту организацию и потеряете доступ ко всем опросам и ответам. Вы сможете вернуться только по новому приглашению.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Пожалуйста, заполните все поля для добавления нового рабочего пространства.",
"read": "Чтение",
"read_write": "Чтение и запись",
"select_member": "Выберите участника",
"select_workspace": "Выберите рабочее пространство",
"team_admin": "Администратор команды",
"team_created_successfully": "Команда успешно создана.",
"team_deleted_successfully": "Команда успешно удалена.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Добавить плейсхолдер, который будет показан, если нет значения для отображения.",
"add_hidden_field_id": "Добавить скрытый ID поля",
"add_highlight_border": "Добавить выделяющую рамку",
"add_highlight_border_description": "Добавьте внешнюю рамку к карточке опроса.",
"add_logic": "Добавить логику",
"add_none_of_the_above": "Добавить вариант «Ничего из вышеперечисленного»",
"add_option": "Добавить вариант",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Измените сообщение, которое видят посетители, когда опрос закрыт.",
"adjust_the_theme_in_the": "Настройте тему в",
"all_other_answers_will_continue_to": "Все остальные ответы будут продолжать",
"allow_file_type": "Разрешить тип файла",
"allow_multi_select": "Разрешить множественный выбор",
"allow_multiple_files": "Разрешить несколько файлов",
"allow_users_to_select_more_than_one_image": "Разрешить пользователям выбирать более одного изображения",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Блокировать дубликаты.",
"bold": "Жирный",
"brand_color": "Фирменный цвет",
"brand_color_description": "Применяется к кнопкам, ссылкам и выделениям.",
"brightness": "Яркость",
"bulk_edit": "Массовое редактирование",
"bulk_edit_description": "Отредактируйте все варианты ниже, по одному на строку. Пустые строки будут пропущены, а дубликаты удалены.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Захватить новое действие",
"card_arrangement_for_survey_type_derived": "Расположение карточек для опросов типа {surveyTypeDerived}",
"card_background_color": "Цвет фона карточки",
"card_background_color_description": "Заполняет область карточки опроса.",
"card_border_color": "Цвет рамки карточки",
"card_border_color_description": "Обводит карточку опроса.",
"card_styling": "Оформление карточки",
"casual": "Неформальный",
"caution_edit_duplicate": "Дублировать и редактировать",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Старые и новые ответы смешиваются, что может привести к искажённым итоговым данным.",
"caution_recommendation": "Это может привести к несоответствиям в итогах опроса. Рекомендуем вместо этого дублировать опрос.",
"caution_text": "Изменения приведут к несоответствиям",
"centered_modal_overlay_color": "Цвет оверлея центрированного модального окна",
"change_anyway": "Всё равно изменить",
"change_background": "Изменить фон",
"change_question_type": "Изменить тип вопроса",
"change_survey_type": "Смена типа опроса влияет на существующий доступ",
"change_the_background_color_of_the_card": "Изменить цвет фона карточки.",
"change_the_background_color_of_the_input_fields": "Изменить цвет фона полей ввода.",
"change_the_background_to_a_color_image_or_animation": "Изменить фон на цвет, изображение или анимацию.",
"change_the_border_color_of_the_card": "Изменить цвет рамки карточки.",
"change_the_border_color_of_the_input_fields": "Изменить цвет рамки полей ввода.",
"change_the_border_radius_of_the_card_and_the_inputs": "Изменить скругление углов карточки и полей ввода.",
"change_the_brand_color_of_the_survey": "Изменить фирменный цвет опроса.",
"change_the_placement_of_this_survey": "Изменить размещение этого опроса.",
"change_the_question_color_of_the_survey": "Изменить цвет вопросов в опросе.",
"changes_saved": "Изменения сохранены.",
"changing_survey_type_will_remove_existing_distribution_channels": "Изменение типа опроса повлияет на способы его распространения. Если у респондентов уже есть ссылки для доступа к текущему типу, после смены они могут потерять доступ.",
"character_limit_toggle_description": "Ограничьте минимальную и максимальную длину ответа.",
"character_limit_toggle_title": "Добавить ограничения на количество символов",
"checkbox_label": "Метка флажка",
"choose_the_actions_which_trigger_the_survey": "Выберите действия, которые запускают опрос.",
"choose_the_first_question_on_your_block": "Выберите первый вопрос в вашем блоке",
@@ -1263,6 +1259,7 @@
"contact_fields": "Поля контакта",
"contains": "Содержит",
"continue_to_settings": "Перейти к настройкам",
"control_which_file_types_can_be_uploaded": "Управляйте типами файлов, которые можно загружать.",
"convert_to_multiple_choice": "Преобразовать в мультивыбор",
"convert_to_single_choice": "Преобразовать в одиночный выбор",
"country": "Страна",
@@ -1275,13 +1272,11 @@
"darken_or_lighten_background_of_your_choice": "Затемните или осветлите выбранный фон.",
"date_format": "Формат даты",
"days_before_showing_this_survey_again": "или больше дней должно пройти между последним показом опроса и показом этого опроса.",
"delete_anyways": "Удалить в любом случае",
"delete_block": "Удалить блок",
"delete_choice": "Удалить вариант",
"disable_the_visibility_of_survey_progress": "Отключить отображение прогресса опроса.",
"display_an_estimate_of_completion_time_for_survey": "Показывать примерное время прохождения опроса",
"display_number_of_responses_for_survey": "Показывать количество ответов на опрос",
"display_type": "Тип отображения",
"divide": "Разделить /",
"does_not_contain": "Не содержит",
"does_not_end_with": "Не заканчивается на",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Не включает все из",
"does_not_include_one_of": "Не включает ни одного из",
"does_not_start_with": "Не начинается с",
"dropdown": "Выпадающий список",
"duplicate_block": "Дублировать блок",
"duplicate_question": "Дублировать вопрос",
"edit_link": "Редактировать ссылку",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "Скрыть индикатор прогресса",
"hide_question_settings": "Скрыть настройки вопроса",
"hostname": "Имя хоста",
"if_you_need_more_please": "Если вам нужно больше, пожалуйста",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Насколько необычными вы хотите сделать карточки в опросах типа {surveyTypeDerived}",
"if_you_need_more_please": "Если нужно больше, пожалуйста",
"if_you_really_want_that_answer_ask_until_you_get_it": "Показывать каждый раз при срабатывании, пока не будет получен ответ.",
"ignore_global_waiting_time": "Игнорировать период ожидания",
"ignore_global_waiting_time_description": "Этот опрос может отображаться при выполнении условий, даже если недавно уже был показан другой опрос.",
@@ -1391,9 +1386,7 @@
"initial_value": "Начальное значение",
"inner_text": "Внутренний текст",
"input_border_color": "Цвет рамки поля ввода",
"input_border_color_description": "Обводит текстовые поля и текстовые области.",
"input_color": "Цвет поля ввода",
"input_color_description": "Заполняет внутреннюю часть текстовых полей.",
"insert_link": "Вставить ссылку",
"invalid_targeting": "Некорректный таргетинг: проверьте фильтры аудитории",
"invalid_video_url_warning": "Пожалуйста, введите корректную ссылку на YouTube, Vimeo или Loom. В настоящее время другие видеохостинги не поддерживаются.",
@@ -1420,10 +1413,10 @@
"key": "Ключ",
"last_name": "Фамилия",
"let_people_upload_up_to_25_files_at_the_same_time": "Разрешить загружать до 25 файлов одновременно.",
"limit_the_maximum_file_size": "Ограничьте максимальный размер загружаемых файлов.",
"limit_file_types": "Ограничить типы файлов",
"limit_the_maximum_file_size": "Ограничить максимальный размер файла",
"limit_upload_file_size_to": "Ограничить размер загружаемого файла до",
"link_survey_description": "Поделитесь ссылкой на страницу опроса или вставьте её на веб-страницу или в электронное письмо.",
"list": "Список",
"load_segment": "Загрузить сегмент",
"logic_error_warning": "Изменение приведёт к логическим ошибкам",
"logic_error_warning_text": "Изменение типа вопроса удалит логические условия из этого вопроса",
@@ -1467,6 +1460,7 @@
"picture_idx": "Изображение {idx}",
"pin_can_only_contain_numbers": "PIN-код может содержать только цифры.",
"pin_must_be_a_four_digit_number": "PIN-код должен состоять из четырёх цифр.",
"please_enter_a_file_extension": "Пожалуйста, введите расширение файла.",
"please_enter_a_valid_url": "Пожалуйста, введите корректный URL (например, https://example.com)",
"please_set_a_survey_trigger": "Пожалуйста, установите триггер опроса",
"please_specify": "Пожалуйста, уточните",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Только пользователи, у которых есть PIN-код, могут получить доступ к опросу.",
"publish": "Опубликовать",
"question": "Вопрос",
"question_color": "Цвет вопроса",
"question_deleted": "Вопрос удалён.",
"question_duplicated": "Вопрос дублирован.",
"question_id_updated": "ID вопроса обновлён",
"question_used_in_logic_warning_text": "Элементы из этого блока используются в правиле логики. Вы уверены, что хотите удалить его?",
"question_used_in_logic_warning_title": "Несогласованность логики",
"question_used_in_quota": "Этот вопрос используется в квоте «{quotaName}»",
"question_used_in_logic": "Этот вопрос используется в логике вопроса {questionIndex}.",
"question_used_in_quota": "Этот вопрос используется в квоте \"{quotaName}\"",
"question_used_in_recall": "Этот вопрос используется в отзыве в вопросе {questionIndex}.",
"question_used_in_recall_ending_card": "Этот вопрос используется в отзыве на финальной карточке",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Лимиты ответов, перенаправления и другое.",
"response_options": "Параметры ответа",
"roundness": "Скругление",
"roundness_description": "Определяет степень скругления углов карточки.",
"row_used_in_logic_error": "Эта строка используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите её из логики.",
"rows": "Строки",
"save_and_close": "Сохранить и закрыть",
@@ -1546,7 +1539,6 @@
"search_for_images": "Поиск изображений",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "секунд после запуска — опрос будет закрыт, если не будет ответа",
"seconds_before_showing_the_survey": "секунд до показа опроса.",
"select_field": "Выберите поле",
"select_or_type_value": "Выберите или введите значение",
"select_ordering": "Выберите порядок",
"select_saved_action": "Выберите сохранённое действие",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Оформление установлено в соответствии с темой",
"subheading": "Подзаголовок",
"subtract": "Вычесть -",
"suggest_colors": "Предложить цвета",
"survey_completed_heading": "Опрос завершён",
"survey_completed_subheading": "Этот бесплатный и открытый опрос был закрыт",
"survey_display_settings": "Настройки отображения опроса",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Показать один раз, даже если не будет ответа.",
"then": "Затем",
"this_action_will_remove_all_the_translations_from_this_survey": "Это действие удалит все переводы из этого опроса.",
"this_extension_is_already_added": "Это расширение уже добавлено.",
"this_file_type_is_not_supported": "Этот тип файла не поддерживается.",
"three_points": "3 балла",
"times": "раз",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Чтобы сохранить единое расположение во всех опросах, вы можете",
@@ -1613,51 +1608,8 @@
"upper_label": "Верхняя метка",
"url_filters": "Фильтры URL",
"url_not_supported": "URL не поддерживается",
"validation": {
"add_validation_rule": "Добавить правило проверки",
"answer_all_rows": "Ответьте на все строки",
"characters": "Символы",
"contains": "Содержит",
"delete_validation_rule": "Удалить правило проверки",
"does_not_contain": "Не содержит",
"email": "Корректный email",
"end_date": "Дата окончания",
"file_extension_is": "Расширение файла —",
"file_extension_is_not": "Расширение файла не является",
"is": "Является",
"is_between": "Находится между",
"is_earlier_than": "Ранее чем",
"is_greater_than": "Больше чем",
"is_later_than": "Позже чем",
"is_less_than": "Меньше чем",
"is_not": "Не является",
"is_not_between": "Не находится между",
"kb": "КБ",
"max_length": "Не более",
"max_selections": "Не более",
"max_value": "Не более",
"mb": "МБ",
"min_length": "Не менее",
"min_selections": "Не менее",
"min_value": "Не менее",
"minimum_options_ranked": "Минимальное количество ранжированных вариантов",
"minimum_rows_answered": "Минимальное количество заполненных строк",
"options_selected": "Выбранные опции",
"pattern": "Соответствует шаблону regex",
"phone": "Корректный телефон",
"rank_all_options": "Ранжируйте все опции",
"select_file_extensions": "Выберите расширения файлов...",
"select_option": "Выберите вариант",
"start_date": "Дата начала",
"url": "Корректный URL"
},
"validation_logic_and": "Все условия выполняются",
"validation_logic_or": "выполняется хотя бы одно условие",
"validation_rules": "Правила валидации",
"validation_rules_description": "Принимать только ответы, соответствующие следующим критериям",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Переменная «{variableName}» используется в квоте «{quotaName}». Сначала удалите её из квоты.",
"variable_name_conflicts_with_hidden_field": "Имя переменной конфликтует с существующим ID скрытого поля.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Переменная «{variableName}» используется в квоте «{quotaName}»",
"variable_name_is_already_taken_please_choose_another": "Это имя переменной уже занято, выберите другое.",
"variable_name_must_start_with_a_letter": "Имя переменной должно начинаться с буквы.",
"variable_used_in_recall": "Переменная «{variable}» используется в вопросе {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Добавить цвет фона",
"add_background_color_description": "Добавьте цвет фона для контейнера с логотипом.",
"advanced_styling_field_border_radius": "Радиус скругления",
"advanced_styling_field_button_bg": "Фон кнопки",
"advanced_styling_field_button_bg_description": "Заполняет кнопку «Далее» / «Отправить».",
"advanced_styling_field_button_border_radius_description": "Скругляет углы кнопки.",
"advanced_styling_field_button_font_size_description": "Масштабирует текст на кнопке.",
"advanced_styling_field_button_font_weight_description": "Делает текст на кнопке тоньше или жирнее.",
"advanced_styling_field_button_height_description": "Определяет высоту кнопки.",
"advanced_styling_field_button_padding_x_description": "Добавляет отступы слева и справа.",
"advanced_styling_field_button_padding_y_description": "Добавляет отступы сверху и снизу.",
"advanced_styling_field_button_text": "Текст кнопки",
"advanced_styling_field_button_text_description": "Задаёт цвет текста на кнопках.",
"advanced_styling_field_description_color": "Цвет описания",
"advanced_styling_field_description_color_description": "Задаёт цвет текста под каждым заголовком.",
"advanced_styling_field_description_size": "Размер шрифта описания",
"advanced_styling_field_description_size_description": "Масштабирует текст описания.",
"advanced_styling_field_description_weight": "Толщина шрифта описания",
"advanced_styling_field_description_weight_description": "Делает текст описания тоньше или жирнее.",
"advanced_styling_field_font_size": "Размер шрифта",
"advanced_styling_field_font_weight": "Толщина шрифта",
"advanced_styling_field_headline_color": "Цвет заголовка",
"advanced_styling_field_headline_color_description": "Задаёт цвет основного текста вопроса.",
"advanced_styling_field_headline_size": "Размер шрифта заголовка",
"advanced_styling_field_headline_size_description": "Масштабирует текст заголовка.",
"advanced_styling_field_headline_weight": "Толщина шрифта заголовка",
"advanced_styling_field_headline_weight_description": "Делает текст заголовка тоньше или жирнее.",
"advanced_styling_field_height": "Высота",
"advanced_styling_field_indicator_bg": "Фон индикатора",
"advanced_styling_field_indicator_bg_description": "Задаёт цвет заполненной части полосы.",
"advanced_styling_field_input_border_radius_description": "Скругляет углы полей ввода.",
"advanced_styling_field_input_font_size_description": "Масштабирует введённый текст в полях ввода.",
"advanced_styling_field_input_height_description": "Определяет высоту поля ввода.",
"advanced_styling_field_input_padding_x_description": "Добавляет отступы слева и справа.",
"advanced_styling_field_input_padding_y_description": "Добавляет пространство сверху и снизу.",
"advanced_styling_field_input_placeholder_opacity_description": "Делает текст подсказки менее заметным.",
"advanced_styling_field_input_shadow_description": "Добавляет тень вокруг полей ввода.",
"advanced_styling_field_input_text": "Текст ввода",
"advanced_styling_field_input_text_description": "Задаёт цвет введённого текста в полях.",
"advanced_styling_field_option_bg": "Фон",
"advanced_styling_field_option_bg_description": "Заливает фон элементов опций.",
"advanced_styling_field_option_border_radius_description": "Скругляет углы опций.",
"advanced_styling_field_option_font_size_description": "Изменяет размер текста метки опции.",
"advanced_styling_field_option_label": "Цвет метки",
"advanced_styling_field_option_label_description": "Задаёт цвет текста метки опции.",
"advanced_styling_field_option_padding_x_description": "Добавляет пространство слева и справа.",
"advanced_styling_field_option_padding_y_description": "Добавляет пространство сверху и снизу.",
"advanced_styling_field_padding_x": "Внутренний отступ по X",
"advanced_styling_field_padding_y": "Внутренний отступ по Y",
"advanced_styling_field_placeholder_opacity": "Прозрачность плейсхолдера",
"advanced_styling_field_shadow": "Тень",
"advanced_styling_field_track_bg": "Фон трека",
"advanced_styling_field_track_bg_description": "Задаёт цвет незаполненной части полосы.",
"advanced_styling_field_track_height": "Высота трека",
"advanced_styling_field_track_height_description": "Управляет толщиной индикатора прогресса.",
"advanced_styling_field_upper_label_color": "Цвет метки заголовка",
"advanced_styling_field_upper_label_color_description": "Задаёт цвет маленькой метки над полями ввода.",
"advanced_styling_field_upper_label_size": "Размер шрифта метки заголовка",
"advanced_styling_field_upper_label_size_description": "Изменяет размер маленькой метки над полями ввода.",
"advanced_styling_field_upper_label_weight": "Толщина шрифта метки заголовка",
"advanced_styling_field_upper_label_weight_description": "Делает метку тоньше или жирнее.",
"advanced_styling_section_buttons": "Кнопки",
"advanced_styling_section_headlines": "Заголовки и описания",
"advanced_styling_section_inputs": "Поля ввода",
"advanced_styling_section_options": "Опции (радио/чекбокс)",
"app_survey_placement": "Размещение опроса в приложении",
"app_survey_placement_settings_description": "Измените, где будут отображаться опросы в вашем веб-приложении или на сайте.",
"centered_modal_overlay_color": "Цвет оверлея центрированного модального окна",
"email_customization": "Настройка email",
"email_customization_description": "Измените внешний вид писем, которые Formbricks отправляет от вашего имени.",
"enable_custom_styling": "Включить пользовательское оформление",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Брендинг Formbricks скрыт.",
"formbricks_branding_settings_description": "Мы ценим вашу поддержку, но понимаем, если вы захотите отключить это.",
"formbricks_branding_shown": "Брендинг Formbricks отображается.",
"generate_theme_btn": "Сгенерировать",
"generate_theme_confirmation": "Сгенерировать подходящую цветовую тему на основе цвета твоего бренда? Это перезапишет текущие цветовые настройки.",
"generate_theme_header": "Сгенерировать цветовую тему?",
"logo_removed_successfully": "Логотип успешно удалён",
"logo_settings_description": "Загрузите логотип вашей компании для брендирования опросов и предпросмотра ссылок.",
"logo_updated_successfully": "Логотип успешно обновлён",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Показывать брендинг Formbricks в опросах типа {type}",
"show_powered_by_formbricks": "Показывать подпись «Работает на Formbricks»",
"styling_updated_successfully": "Стили успешно обновлены",
"suggest_colors": "Предложить цвета",
"theme": "Тема",
"theme_settings_description": "Создайте стиль для всех опросов. Вы можете включить индивидуальное оформление для каждого опроса."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Да, держите меня в курсе.",
"preview_survey_question_2_choice_2_label": "Нет, спасибо!",
"preview_survey_question_2_headline": "Хотите быть в курсе событий?",
"preview_survey_question_2_subheader": "Это пример описания.",
"preview_survey_welcome_card_headline": "Добро пожаловать!",
"prioritize_features_description": "Определите, какие функции наиболее и наименее важны для ваших пользователей.",
"prioritize_features_name": "Приоритизация функций",
+38 -153
View File
@@ -243,6 +243,7 @@
"imprint": "Impressum",
"in_progress": "Pågående",
"inactive_surveys": "Inaktiva enkäter",
"input_type": "Inmatningstyp",
"integration": "integration",
"integrations": "Integrationer",
"invalid_date": "Ogiltigt datum",
@@ -254,7 +255,6 @@
"label": "Etikett",
"language": "Språk",
"learn_more": "Läs mer",
"license_expired": "License Expired",
"light_overlay": "Ljust överlägg",
"limits_reached": "Gränser nådda",
"link": "Länk",
@@ -267,11 +267,13 @@
"look_and_feel": "Utseende",
"manage": "Hantera",
"marketing": "Marknadsföring",
"maximum": "Maximum",
"member": "Medlem",
"members": "Medlemmar",
"members_and_teams": "Medlemmar och team",
"membership_not_found": "Medlemskap hittades inte",
"metadata": "Metadata",
"minimum": "Minimum",
"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_title": "Hoppsan, liten skärm upptäckt!",
@@ -285,7 +287,6 @@
"no_background_image_found": "Ingen bakgrundsbild hittades.",
"no_code": "Ingen kod",
"no_files_uploaded": "Inga filer laddades upp",
"no_overlay": "Ingen overlay",
"no_quotas_found": "Inga kvoter hittades",
"no_result_found": "Inget resultat hittades",
"no_results": "Inga resultat",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "Organisationsteam hittades inte",
"other": "Annat",
"others": "Andra",
"overlay_color": "Overlay-färg",
"overview": "Översikt",
"password": "Lösenord",
"paused": "Pausad",
@@ -326,7 +326,7 @@
"placeholder": "Platshållare",
"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_upgrade_your_plan": "Vänligen uppgradera din plan",
"please_upgrade_your_plan": "Vänligen uppgradera din plan.",
"preview": "Förhandsgranska",
"preview_survey": "Förhandsgranska enkät",
"privacy": "Integritetspolicy",
@@ -352,7 +352,6 @@
"request_trial_license": "Begär provlicens",
"reset_to_default": "Återställ till standard",
"response": "Svar",
"response_id": "Svar-ID",
"responses": "Svar",
"restart": "Starta om",
"role": "Roll",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "Du har nått din gräns på {projectLimit} arbetsytor.",
"you_have_reached_your_monthly_miu_limit_of": "Du har nått din månatliga MIU-gräns på",
"you_have_reached_your_monthly_response_limit_of": "Du har nått din månatliga svarsgräns på",
"you_will_be_downgraded_to_the_community_edition_on_date": "Du kommer att nedgraderas till Community Edition den {date}.",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "Du kommer att nedgraderas till Community Edition den {date}."
},
"emails": {
"accept": "Acceptera",
@@ -956,32 +954,19 @@
"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.",
"keep_full_control_over_your_data_privacy_and_security": "Behåll full kontroll över din datasekretess och säkerhet.",
"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_active": "Aktiv",
"license_status_description": "Status för din företagslicens.",
"license_status_expired": "Utgången",
"license_status_invalid": "Ogiltig licens",
"license_status_unreachable": "Otillgänglig",
"license_unreachable_grace_period": "Licensservern kan inte nås. Dina enterprise-funktioner är aktiva under en 3-dagars respitperiod som slutar {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Inget samtal behövs, inga åtaganden: Begär en gratis 30-dagars provlicens för att testa alla funktioner genom att fylla i detta formulär:",
"no_credit_card_no_sales_call_just_test_it": "Inget kreditkort. Inget säljsamtal. Testa bara :)",
"on_request": "På begäran",
"organization_roles": "Organisationsroller (Admin, Redaktör, Utvecklare, etc.)",
"questions_please_reach_out_to": "Frågor? Kontakta",
"recheck_license": "Kontrollera licensen igen",
"recheck_license_failed": "Licenskontrollen misslyckades. Licensservern kan vara otillgänglig.",
"recheck_license_invalid": "Licensnyckeln är ogiltig. Kontrollera din ENTERPRISE_LICENSE_KEY.",
"recheck_license_success": "Licenskontrollen lyckades",
"recheck_license_unreachable": "Licensservern är otillgänglig. Försök igen senare.",
"rechecking": "Kontrollerar igen...",
"request_30_day_trial_license": "Begär 30-dagars provlicens",
"saml_sso": "SAML SSO",
"service_level_agreement": "Servicenivåavtal",
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 efterlevnadskontroll",
"sso": "SSO (Google, Microsoft, OpenID Connect)",
"teams": "Team och åtkomstroller (Läs, Läs och skriv, Hantera)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Lås upp Formbricks fulla kraft. Gratis i 30 dagar."
"unlock_the_full_power_of_formbricks_free_for_30_days": "Lås upp Formbricks fulla kraft. Gratis i 30 dagar.",
"your_enterprise_license_is_active_all_features_unlocked": "Din Enterprise-licens är aktiv. Alla funktioner upplåsta."
},
"general": {
"bulk_invite_warning_description": "På gratisplanen tilldelas alla organisationsmedlemmar alltid rollen \"Ägare\".",
@@ -1005,7 +990,7 @@
"from_your_organization": "från din organisation",
"invitation_sent_once_more": "Inbjudan skickad igen.",
"invite_deleted_successfully": "Inbjudan borttagen",
"invite_expires_on": "Inbjudan går ut den {date}",
"invited_on": "Inbjuden den {date}",
"invites_failed": "Inbjudningar misslyckades",
"leave_organization": "Lämna organisation",
"leave_organization_description": "Du kommer att lämna denna organisation och förlora åtkomst till alla enkäter och svar. Du kan endast återansluta om du blir inbjuden igen.",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "Fyll i alla fält för att lägga till en ny arbetsyta.",
"read": "Läs",
"read_write": "Läs och skriv",
"select_member": "Välj medlem",
"select_workspace": "Välj arbetsyta",
"team_admin": "Teamadministratör",
"team_created_successfully": "Team skapat.",
"team_deleted_successfully": "Team borttaget.",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "Lägg till en platshållare att visa om det inte finns något värde att återkalla.",
"add_hidden_field_id": "Lägg till dolt fält-ID",
"add_highlight_border": "Lägg till markerad kant",
"add_highlight_border_description": "Lägg till en yttre kant till ditt enkätkort.",
"add_logic": "Lägg till logik",
"add_none_of_the_above": "Lägg till \"Inget av ovanstående\"",
"add_option": "Lägg till alternativ",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "Ändra meddelandet besökare ser när enkäten är stängd.",
"adjust_the_theme_in_the": "Justera temat i",
"all_other_answers_will_continue_to": "Alla andra svar fortsätter till",
"allow_file_type": "Tillåt filtyp",
"allow_multi_select": "Tillåt flerval",
"allow_multiple_files": "Tillåt flera filer",
"allow_users_to_select_more_than_one_image": "Tillåt användare att välja mer än en bild",
@@ -1205,7 +1194,6 @@
"block_duplicated": "Block duplicerat.",
"bold": "Fet",
"brand_color": "Varumärkesfärg",
"brand_color_description": "Används för knappar, länkar och markeringar.",
"brightness": "Ljusstyrka",
"bulk_edit": "Massredigera",
"bulk_edit_description": "Redigera alla alternativ nedan, ett per rad. Tomma rader kommer att hoppas över och dubbletter tas bort.",
@@ -1223,9 +1211,7 @@
"capture_new_action": "Fånga ny åtgärd",
"card_arrangement_for_survey_type_derived": "Kortarrangemang för {surveyTypeDerived}-enkäter",
"card_background_color": "Kortets bakgrundsfärg",
"card_background_color_description": "Fyller enkätkortets yta.",
"card_border_color": "Kortets kantfärg",
"card_border_color_description": "Markerar enkätkortets kant.",
"card_styling": "Kortstil",
"casual": "Avslappnad",
"caution_edit_duplicate": "Duplicera och redigera",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "Äldre och nyare svar blandas vilket kan leda till vilseledande datasammanfattningar.",
"caution_recommendation": "Detta kan orsaka datainkonsekvenser i enkätsammanfattningen. Vi rekommenderar att duplicera enkäten istället.",
"caution_text": "Ändringar kommer att leda till inkonsekvenser",
"centered_modal_overlay_color": "Centrerad modal överläggsfärg",
"change_anyway": "Ändra ändå",
"change_background": "Ändra bakgrund",
"change_question_type": "Ändra frågetyp",
"change_survey_type": "Byte av enkättyp påverkar befintlig åtkomst",
"change_the_background_color_of_the_card": "Ändra kortets bakgrundsfärg.",
"change_the_background_color_of_the_input_fields": "Ändra inmatningsfältens bakgrundsfärg.",
"change_the_background_to_a_color_image_or_animation": "Ändra bakgrunden till en färg, bild eller animering.",
"change_the_border_color_of_the_card": "Ändra kortets kantfärg.",
"change_the_border_color_of_the_input_fields": "Ändra inmatningsfältens kantfärg.",
"change_the_border_radius_of_the_card_and_the_inputs": "Ändra kantradie för kortet och inmatningsfälten.",
"change_the_brand_color_of_the_survey": "Ändra enkätens varumärkesfärg.",
"change_the_placement_of_this_survey": "Ändra placeringen av denna enkät.",
"change_the_question_color_of_the_survey": "Ändra enkätens frågefärg.",
"changes_saved": "Ändringar sparade.",
"changing_survey_type_will_remove_existing_distribution_channels": "Att ändra enkättypen påverkar hur den kan delas. Om respondenter redan har åtkomstlänkar för den nuvarande typen kan de förlora åtkomst efter bytet.",
"character_limit_toggle_description": "Begränsa hur kort eller långt ett svar kan vara.",
"character_limit_toggle_title": "Lägg till teckengränser",
"checkbox_label": "Kryssruteetikett",
"choose_the_actions_which_trigger_the_survey": "Välj de åtgärder som utlöser enkäten.",
"choose_the_first_question_on_your_block": "Välj den första frågan i ditt block",
@@ -1263,6 +1259,7 @@
"contact_fields": "Kontaktfält",
"contains": "Innehåller",
"continue_to_settings": "Fortsätt till inställningar",
"control_which_file_types_can_be_uploaded": "Kontrollera vilka filtyper som kan laddas upp.",
"convert_to_multiple_choice": "Konvertera till flerval",
"convert_to_single_choice": "Konvertera till enkelval",
"country": "Land",
@@ -1275,13 +1272,11 @@
"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.",
"delete_anyways": "Ta bort ändå",
"delete_block": "Ta bort block",
"delete_choice": "Ta bort val",
"disable_the_visibility_of_survey_progress": "Inaktivera synligheten av enkätens framsteg.",
"display_an_estimate_of_completion_time_for_survey": "Visa en uppskattning av tid för att slutföra enkäten",
"display_number_of_responses_for_survey": "Visa antal svar för enkäten",
"display_type": "Visningstyp",
"divide": "Dividera /",
"does_not_contain": "Innehåller inte",
"does_not_end_with": "Slutar inte med",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "Inkluderar inte alla av",
"does_not_include_one_of": "Inkluderar inte en av",
"does_not_start_with": "Börjar inte med",
"dropdown": "Rullgardinsmeny",
"duplicate_block": "Duplicera block",
"duplicate_question": "Duplicera fråga",
"edit_link": "Redigera länk",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "Dölj framstegsindikator",
"hide_question_settings": "Dölj frågeinställningar",
"hostname": "Värdnamn",
"if_you_need_more_please": "Om du behöver mer, vänligen",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hur coola vill du att dina kort ska vara i {surveyTypeDerived}-enkäter",
"if_you_need_more_please": "Om du behöver fler, vänligen",
"if_you_really_want_that_answer_ask_until_you_get_it": "Fortsätt visa när villkoren är uppfyllda tills ett svar skickas in.",
"ignore_global_waiting_time": "Ignorera väntetid",
"ignore_global_waiting_time_description": "Denna enkät kan visas när dess villkor är uppfyllda, även om en annan enkät nyligen visats.",
@@ -1391,9 +1386,7 @@
"initial_value": "Initialt värde",
"inner_text": "Inre text",
"input_border_color": "Inmatningsfältets kantfärg",
"input_border_color_description": "Markerar kanten på textfält och textområden.",
"input_color": "Inmatningsfärg",
"input_color_description": "Fyller insidan av textfält.",
"insert_link": "Infoga länk",
"invalid_targeting": "Ogiltig målgruppsinriktning: Vänligen kontrollera dina målgruppsfilter",
"invalid_video_url_warning": "Vänligen ange en giltig YouTube-, Vimeo- eller Loom-URL. Vi stöder för närvarande inte andra videohostingleverantörer.",
@@ -1420,10 +1413,10 @@
"key": "Nyckel",
"last_name": "Efternamn",
"let_people_upload_up_to_25_files_at_the_same_time": "Låt personer ladda upp upp till 25 filer samtidigt.",
"limit_the_maximum_file_size": "Begränsa den maximala filstorleken för uppladdningar.",
"limit_upload_file_size_to": "Begränsa uppladdad filstorlek till",
"limit_file_types": "Begränsa filtyper",
"limit_the_maximum_file_size": "Begränsa maximal filstorlek",
"limit_upload_file_size_to": "Begränsa uppladdningsfilstorlek till",
"link_survey_description": "Dela en länk till en enkätsida eller bädda in den på en webbsida eller i e-post.",
"list": "Lista",
"load_segment": "Ladda segment",
"logic_error_warning": "Ändring kommer att orsaka logikfel",
"logic_error_warning_text": "Att ändra frågetypen kommer att ta bort logikvillkoren från denna fråga",
@@ -1435,7 +1428,7 @@
"matrix_all_fields": "Alla fält",
"matrix_rows": "Rader",
"max_file_size": "Max filstorlek",
"max_file_size_limit_is": "Maximal filstorleksgräns är",
"max_file_size_limit_is": "Maxgräns för filstorlek är",
"move_question_to_block": "Flytta fråga till block",
"multiply": "Multiplicera *",
"needed_for_self_hosted_cal_com_instance": "Behövs för en självhostad Cal.com-instans",
@@ -1467,6 +1460,7 @@
"picture_idx": "Bild {idx}",
"pin_can_only_contain_numbers": "PIN kan endast innehålla siffror.",
"pin_must_be_a_four_digit_number": "PIN måste vara ett fyrsiffrigt nummer.",
"please_enter_a_file_extension": "Vänligen ange en filändelse.",
"please_enter_a_valid_url": "Vänligen ange en giltig URL (t.ex. https://example.com)",
"please_set_a_survey_trigger": "Vänligen ställ in en enkätutlösare",
"please_specify": "Vänligen specificera",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "Endast användare som har PIN-koden kan komma åt enkäten.",
"publish": "Publicera",
"question": "Fråga",
"question_color": "Frågefärg",
"question_deleted": "Fråga borttagen.",
"question_duplicated": "Fråga duplicerad.",
"question_id_updated": "Fråge-ID uppdaterat",
"question_used_in_logic_warning_text": "Element från det här blocket används i en logikregel. Är du säker på att du vill ta bort det?",
"question_used_in_logic_warning_title": "Logikkonflikt",
"question_used_in_quota": "Denna fråga används i kvoten “{quotaName}”",
"question_used_in_logic": "Denna fråga används i logiken för fråga {questionIndex}.",
"question_used_in_quota": "Denna fråga används i kvoten \"{quotaName}\"",
"question_used_in_recall": "Denna fråga återkallas i fråga {questionIndex}.",
"question_used_in_recall_ending_card": "Denna fråga återkallas i avslutningskortet",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "Svarsgränser, omdirigeringar och mer.",
"response_options": "Svarsalternativ",
"roundness": "Rundhet",
"roundness_description": "Styr hur rundade kortets hörn ä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.",
"rows": "Rader",
"save_and_close": "Spara och stäng",
@@ -1546,7 +1539,6 @@
"search_for_images": "Sök efter bilder",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "sekunder efter utlösning stängs enkäten om inget svar",
"seconds_before_showing_the_survey": "sekunder innan enkäten visas.",
"select_field": "Välj fält",
"select_or_type_value": "Välj eller skriv värde",
"select_ordering": "Välj ordning",
"select_saved_action": "Välj sparad åtgärd",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "Styling inställd på temastil",
"subheading": "Underrubrik",
"subtract": "Subtrahera -",
"suggest_colors": "Föreslå färger",
"survey_completed_heading": "Enkät slutförd",
"survey_completed_subheading": "Denna gratis och öppenkällkodsenkät har stängts",
"survey_display_settings": "Visningsinställningar för enkät",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Visa en enda gång, även om de inte svarar.",
"then": "Sedan",
"this_action_will_remove_all_the_translations_from_this_survey": "Denna åtgärd kommer att ta bort alla översättningar från denna enkät.",
"this_extension_is_already_added": "Denna filändelse är redan tillagd.",
"this_file_type_is_not_supported": "Denna filtyp stöds inte.",
"three_points": "3 poäng",
"times": "gånger",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "För att hålla placeringen konsekvent över alla enkäter kan du",
@@ -1613,51 +1608,8 @@
"upper_label": "Övre etikett",
"url_filters": "URL-filter",
"url_not_supported": "URL stöds inte",
"validation": {
"add_validation_rule": "Lägg till valideringsregel",
"answer_all_rows": "Svara på alla rader",
"characters": "Tecken",
"contains": "Innehåller",
"delete_validation_rule": "Ta bort valideringsregel",
"does_not_contain": "Innehåller inte",
"email": "Är en giltig e-postadress",
"end_date": "Slutdatum",
"file_extension_is": "Filändelsen är",
"file_extension_is_not": "Filändelsen är inte",
"is": "Är",
"is_between": "Är mellan",
"is_earlier_than": "Är tidigare än",
"is_greater_than": "Är större än",
"is_later_than": "Är senare än",
"is_less_than": "Är mindre än",
"is_not": "Är inte",
"is_not_between": "Är inte mellan",
"kb": "KB",
"max_length": "Högst",
"max_selections": "Högst",
"max_value": "Högst",
"mb": "MB",
"min_length": "Minst",
"min_selections": "Minst",
"min_value": "Minst",
"minimum_options_ranked": "Minsta antal rangordnade alternativ",
"minimum_rows_answered": "Minsta antal besvarade rader",
"options_selected": "Valda alternativ",
"pattern": "Matchar regexmönster",
"phone": "Är ett giltigt telefonnummer",
"rank_all_options": "Rangordna alla alternativ",
"select_file_extensions": "Välj filändelser...",
"select_option": "Välj alternativ",
"start_date": "Startdatum",
"url": "Är en giltig URL"
},
"validation_logic_and": "Alla är sanna",
"validation_logic_or": "någon är sann",
"validation_rules": "Valideringsregler",
"validation_rules_description": "Acceptera endast svar som uppfyller följande kriterier",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabeln “{variableName} används i kvoten {quotaName}",
"variable_name_conflicts_with_hidden_field": "Variabelnamnet krockar med ett befintligt dolt fält-ID.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabel \"{variableName}\" används i kvoten \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "Variabelnamnet är redan taget, vänligen välj ett annat.",
"variable_name_must_start_with_a_letter": "Variabelnamnet måste börja med en bokstav.",
"variable_used_in_recall": "Variabel \"{variable}\" återkallas i fråga {questionIndex}.",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "Lägg till bakgrundsfärg",
"add_background_color_description": "Lägg till en bakgrundsfärg i logobehållaren.",
"advanced_styling_field_border_radius": "Hörnradie",
"advanced_styling_field_button_bg": "Knappens bakgrund",
"advanced_styling_field_button_bg_description": "Fyller Nästa / Skicka-knappen.",
"advanced_styling_field_button_border_radius_description": "Rundar av knappens hörn.",
"advanced_styling_field_button_font_size_description": "Ändrar storleken på knappens text.",
"advanced_styling_field_button_font_weight_description": "Gör knapptexten tunnare eller fetare.",
"advanced_styling_field_button_height_description": "Styr knappens höjd.",
"advanced_styling_field_button_padding_x_description": "Lägger till utrymme till vänster och höger.",
"advanced_styling_field_button_padding_y_description": "Lägger till utrymme upptill och nedtill.",
"advanced_styling_field_button_text": "Knapptext",
"advanced_styling_field_button_text_description": "Färglägger texten i knappar.",
"advanced_styling_field_description_color": "Beskrivningsfärg",
"advanced_styling_field_description_color_description": "Färglägger texten under varje rubrik.",
"advanced_styling_field_description_size": "Beskrivningens teckenstorlek",
"advanced_styling_field_description_size_description": "Ändrar storleken på beskrivningstexten.",
"advanced_styling_field_description_weight": "Beskrivningens teckentjocklek",
"advanced_styling_field_description_weight_description": "Gör beskrivningstexten tunnare eller fetare.",
"advanced_styling_field_font_size": "Teckenstorlek",
"advanced_styling_field_font_weight": "Teckentjocklek",
"advanced_styling_field_headline_color": "Rubrikfärg",
"advanced_styling_field_headline_color_description": "Färglägger huvudfrågan.",
"advanced_styling_field_headline_size": "Rubrikens teckenstorlek",
"advanced_styling_field_headline_size_description": "Ändrar storleken på rubriken.",
"advanced_styling_field_headline_weight": "Rubrikens teckentjocklek",
"advanced_styling_field_headline_weight_description": "Gör rubriktexten tunnare eller fetare.",
"advanced_styling_field_height": "Höjd",
"advanced_styling_field_indicator_bg": "Indikatorns bakgrund",
"advanced_styling_field_indicator_bg_description": "Färglägger den fyllda delen av stapeln.",
"advanced_styling_field_input_border_radius_description": "Rundar av hörnen på inmatningsfält.",
"advanced_styling_field_input_font_size_description": "Ändrar storleken på texten i inmatningsfält.",
"advanced_styling_field_input_height_description": "Styr höjden på inmatningsfältet.",
"advanced_styling_field_input_padding_x_description": "Lägger till utrymme till vänster och höger.",
"advanced_styling_field_input_padding_y_description": "Lägger till utrymme upptill och nedtill.",
"advanced_styling_field_input_placeholder_opacity_description": "Tonar ut platshållartexten.",
"advanced_styling_field_input_shadow_description": "Lägger till en skugga runt inmatningsfälten.",
"advanced_styling_field_input_text": "Inmatningstext",
"advanced_styling_field_input_text_description": "Färgar den inmatade texten i fälten.",
"advanced_styling_field_option_bg": "Bakgrund",
"advanced_styling_field_option_bg_description": "Fyller alternativraderna.",
"advanced_styling_field_option_border_radius_description": "Rundar hörnen på alternativen.",
"advanced_styling_field_option_font_size_description": "Skalar textstorleken på alternativetiketten.",
"advanced_styling_field_option_label": "Etikettfärg",
"advanced_styling_field_option_label_description": "Färgar texten på alternativetiketten.",
"advanced_styling_field_option_padding_x_description": "Lägger till utrymme till vänster och höger.",
"advanced_styling_field_option_padding_y_description": "Lägger till utrymme upptill och nedtill.",
"advanced_styling_field_padding_x": "Horisontell padding",
"advanced_styling_field_padding_y": "Vertikal padding",
"advanced_styling_field_placeholder_opacity": "Platshållarens opacitet",
"advanced_styling_field_shadow": "Skugga",
"advanced_styling_field_track_bg": "Spårets bakgrund",
"advanced_styling_field_track_bg_description": "Färgar den ofyllda delen av stapeln.",
"advanced_styling_field_track_height": "Spårets höjd",
"advanced_styling_field_track_height_description": "Styr tjockleken på förloppsstapeln.",
"advanced_styling_field_upper_label_color": "Rubriketikettens färg",
"advanced_styling_field_upper_label_color_description": "Färgar den lilla etiketten ovanför fälten.",
"advanced_styling_field_upper_label_size": "Rubriketikettens teckenstorlek",
"advanced_styling_field_upper_label_size_description": "Skalar storleken på den lilla etiketten ovanför fälten.",
"advanced_styling_field_upper_label_weight": "Rubriketikettens teckentjocklek",
"advanced_styling_field_upper_label_weight_description": "Gör etiketten tunnare eller fetare.",
"advanced_styling_section_buttons": "Knappar",
"advanced_styling_section_headlines": "Rubriker & beskrivningar",
"advanced_styling_section_inputs": "Inmatningar",
"advanced_styling_section_options": "Alternativ (Radio/Checkbox)",
"app_survey_placement": "App-enkätplacering",
"app_survey_placement_settings_description": "Ändra var enkäter visas i din webbapp eller på din webbplats.",
"centered_modal_overlay_color": "Centrerad modal överläggsfärg",
"email_customization": "E-postanpassning",
"email_customization_description": "Ändra utseendet på de e-postmeddelanden som Formbricks skickar åt dig.",
"enable_custom_styling": "Aktivera anpassad styling",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Formbricks-varumärket är dolt.",
"formbricks_branding_settings_description": "Vi uppskattar ditt stöd men förstår om du vill stänga av det.",
"formbricks_branding_shown": "Formbricks-varumärket visas.",
"generate_theme_btn": "Generera",
"generate_theme_confirmation": "Vill du generera ett matchande färgtema baserat på din varumärkesfärg? Detta kommer att skriva över dina nuvarande färginställningar.",
"generate_theme_header": "Generera färgtema?",
"logo_removed_successfully": "Logotyp borttagen",
"logo_settings_description": "Ladda upp företagets logotyp för att profilera enkäter och länkförhandsvisningar.",
"logo_updated_successfully": "Logotyp uppdaterad",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "Visa Formbricks-varumärket i {type}-enkäter",
"show_powered_by_formbricks": "Visa 'Powered by Formbricks'-signatur",
"styling_updated_successfully": "Stiluppdatering lyckades",
"suggest_colors": "Föreslå färger",
"theme": "Tema",
"theme_settings_description": "Skapa ett stilmall för alla undersökningar. Du kan aktivera anpassad stil för varje undersökning."
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "Ja, håll mig informerad.",
"preview_survey_question_2_choice_2_label": "Nej, tack!",
"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_welcome_card_headline": "Välkommen!",
"prioritize_features_description": "Identifiera vilka funktioner dina användare behöver mest och minst.",
"prioritize_features_name": "Prioritera funktioner",
+39 -154
View File
@@ -243,6 +243,7 @@
"imprint": "印记",
"in_progress": "进行中",
"inactive_surveys": "不 活跃 调查",
"input_type": "输入类型",
"integration": "集成",
"integrations": "集成",
"invalid_date": "无效 日期",
@@ -254,7 +255,6 @@
"label": "标签",
"language": "语言",
"learn_more": "了解 更多",
"license_expired": "License Expired",
"light_overlay": "浅色遮罩层",
"limits_reached": "限制 达到",
"link": "链接",
@@ -267,11 +267,13 @@
"look_and_feel": "外观 & 感觉",
"manage": "管理",
"marketing": "市场营销",
"maximum": "最大值",
"member": "成员",
"members": "成员",
"members_and_teams": "成员和团队",
"membership_not_found": "未找到会员资格",
"metadata": "元数据",
"minimum": "最低",
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
"mobile_overlay_title": "噢, 检测 到 小 屏幕!",
@@ -285,7 +287,6 @@
"no_background_image_found": "未找到 背景 图片。",
"no_code": "无代码",
"no_files_uploaded": "没有 文件 被 上传",
"no_overlay": "无覆盖层",
"no_quotas_found": "未找到配额",
"no_result_found": "没有 结果",
"no_results": "没有 结果",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "未找到 组织 团队",
"other": "其他",
"others": "其他",
"overlay_color": "覆盖层颜色",
"overview": "概览",
"password": "密码",
"paused": "暂停",
@@ -326,7 +326,7 @@
"placeholder": "占位符",
"please_select_at_least_one_survey": "请选择至少 一个调查",
"please_select_at_least_one_trigger": "请选择至少 一个触发条件",
"please_upgrade_your_plan": "请升级您的计划",
"please_upgrade_your_plan": "请 升级 您的 计划",
"preview": "预览",
"preview_survey": "预览 Survey",
"privacy": "隐私政策",
@@ -352,7 +352,6 @@
"request_trial_license": "申请试用许可证",
"reset_to_default": "重置为 默认",
"response": "响应",
"response_id": "响应 ID",
"responses": "反馈",
"restart": "重新启动",
"role": "角色",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "您已达到 {projectLimit} 个工作区的上限。",
"you_have_reached_your_monthly_miu_limit_of": "您 已经 达到 每月 的 MIU 限制",
"you_have_reached_your_monthly_response_limit_of": "您 已经 达到 每月 的 响应 限制",
"you_will_be_downgraded_to_the_community_edition_on_date": "您将在 {date} 降级到社区版。",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "您将在 {date} 降级到社区版。"
},
"emails": {
"accept": "接受",
@@ -956,32 +954,19 @@
"enterprise_features": "企业 功能",
"get_an_enterprise_license_to_get_access_to_all_features": "获取 企业 许可证 来 访问 所有 功能。",
"keep_full_control_over_your_data_privacy_and_security": "保持 对 您 的 数据 隐私 和 安全 的 完全 控制。",
"license_invalid_description": "你在 ENTERPRISE_LICENSE_KEY 环境变量中填写的许可证密钥无效。请检查是否有拼写错误,或者申请一个新的密钥。",
"license_status": "许可证状态",
"license_status_active": "已激活",
"license_status_description": "你的企业许可证状态。",
"license_status_expired": "已过期",
"license_status_invalid": "许可证无效",
"license_status_unreachable": "无法访问",
"license_unreachable_grace_period": "无法连接到许可证服务器。在为期 3 天的宽限期内,你的企业功能仍然可用,宽限期将于 {gracePeriodEnd} 结束。",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "无需 电话 ,无需 附加 条件: 申请 免费 30 天 试用 授权以 通过 填写 此 表格 测试 所有 功能:",
"no_credit_card_no_sales_call_just_test_it": "无需信用卡 。无需销售电话 。只需测试一下 :)",
"on_request": "按请求",
"organization_roles": "组织角色(管理员,编辑,开发者等)",
"questions_please_reach_out_to": "问题 请 联系",
"recheck_license": "重新检查许可证",
"recheck_license_failed": "许可证检查失败。许可证服务器可能无法访问。",
"recheck_license_invalid": "许可证密钥无效。请确认你的 ENTERPRISE_LICENSE_KEY。",
"recheck_license_success": "许可证检查成功",
"recheck_license_unreachable": "许可证服务器无法访问,请稍后再试。",
"rechecking": "正在重新检查...",
"request_30_day_trial_license": "申请 30 天 的 试用许可证",
"saml_sso": "SAML SSO",
"service_level_agreement": "服务水平协议",
"soc2_hipaa_iso_27001_compliance_check": "SOC2 HIPAA ISO 27001 合规检查",
"sso": "SSO Google 、Microsoft 、OpenID Connect",
"teams": "团队 & 访问 角色(读取, 读取 & 写入, 管理)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "解锁 Formbricks 的全部功能。免费使用 30 天。"
"unlock_the_full_power_of_formbricks_free_for_30_days": "解锁 Formbricks 的全部功能。免费使用 30 天。",
"your_enterprise_license_is_active_all_features_unlocked": "您的企业许可证已激活 所有功能已解锁"
},
"general": {
"bulk_invite_warning_description": "在免费计划中,所有组织成员都会被分配为 \"Owner \"角色。",
@@ -1005,7 +990,7 @@
"from_your_organization": "来自你的组织",
"invitation_sent_once_more": "再次发送邀请。",
"invite_deleted_successfully": "邀请 删除 成功",
"invite_expires_on": "邀请将于 {date} 过期",
"invited_on": "邀于 {date}",
"invites_failed": "邀请失败",
"leave_organization": "离开 组织",
"leave_organization_description": "您将离开此组织,并失去对所有调查和响应的访问权限。只有再次被邀请后,您才能重新加入。",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "请填写所有字段以添加新工作区。",
"read": "阅读",
"read_write": "读 & 写",
"select_member": "选择成员",
"select_workspace": "选择工作区",
"team_admin": "团队管理员",
"team_created_successfully": "团队 创建 成功",
"team_deleted_successfully": "团队 删除 成功",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "添加 占位符 显示 如果 没有 值以 回忆",
"add_hidden_field_id": "添加 隐藏 字段 ID",
"add_highlight_border": "添加 高亮 边框",
"add_highlight_border_description": "在 你的 调查 卡片 添加 外 边框。",
"add_logic": "添加逻辑",
"add_none_of_the_above": "添加 “以上 都 不 是”",
"add_option": "添加 选项",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "更改 访客 看到 调查 关闭 时 的 消息。",
"adjust_the_theme_in_the": "调整主题在",
"all_other_answers_will_continue_to": "所有其他答案将继续",
"allow_file_type": "允许 文件类型",
"allow_multi_select": "允许 多选",
"allow_multiple_files": "允许 多 个 文件",
"allow_users_to_select_more_than_one_image": "允许 用户 选择 多于 一个 图片",
@@ -1205,7 +1194,6 @@
"block_duplicated": "区块已复制。",
"bold": "粗体",
"brand_color": "品牌 颜色",
"brand_color_description": "应用于按钮、链接和高亮部分。",
"brightness": "亮度",
"bulk_edit": "批量编辑",
"bulk_edit_description": "编辑以下所有选项,每行一个。空行将被跳过,重复项将被移除。",
@@ -1223,9 +1211,7 @@
"capture_new_action": "捕获 新动作",
"card_arrangement_for_survey_type_derived": "{surveyTypeDerived} 调查 的 卡片 布局",
"card_background_color": "卡片 的 背景 颜色",
"card_background_color_description": "填充调查卡区域。",
"card_border_color": "卡片 的 边框 颜色",
"card_border_color_description": "勾勒调查卡边框。",
"card_styling": "卡片样式",
"casual": "休闲",
"caution_edit_duplicate": "复制 并 编辑",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "旧 与 新 的 回复 混合 , 这 可能 导致 数据 总结 有误 。",
"caution_recommendation": "这 可能 会 导致 调查 统计 数据 的 不一致 。 我们 建议 复制 调查 。",
"caution_text": "更改 会导致 不一致",
"centered_modal_overlay_color": "居中 模态遮罩层颜色",
"change_anyway": "还是更改",
"change_background": "更改 背景",
"change_question_type": "更改 问题类型",
"change_survey_type": "更改 调查 类型 会影 响 现有 访问",
"change_the_background_color_of_the_card": "更改 卡片 的 背景 颜色",
"change_the_background_color_of_the_input_fields": "更改 输入字段 的 背景颜色",
"change_the_background_to_a_color_image_or_animation": "将 背景 更改为 颜色 、 图像 或 动画。",
"change_the_border_color_of_the_card": "更改 卡片 的 边框 颜色",
"change_the_border_color_of_the_input_fields": "更改 输入字段 的边框颜色。",
"change_the_border_radius_of_the_card_and_the_inputs": "更改 卡片 和 输入 的 边框 半径",
"change_the_brand_color_of_the_survey": "更改调查的品牌颜色",
"change_the_placement_of_this_survey": "更改 此 调查 的 放置。",
"change_the_question_color_of_the_survey": "更改调查的 问题颜色",
"changes_saved": "更改 已 保存",
"changing_survey_type_will_remove_existing_distribution_channels": "更改 调查 类型 会影 响 分享 方式 。 如果 受访者 已经 拥有 当前 类型 的 访问 链接 , 在 更改 之后 ,他们 可能 会 失去 访问 权限 。",
"character_limit_toggle_description": "限制 答案的短或长程度。",
"character_limit_toggle_title": "添加 字符限制",
"checkbox_label": "复选框 标签",
"choose_the_actions_which_trigger_the_survey": "选择 触发 调查 的 动作 。",
"choose_the_first_question_on_your_block": "选择区块中的第一个问题",
@@ -1263,6 +1259,7 @@
"contact_fields": "联络字段",
"contains": "包含",
"continue_to_settings": "继续 到 设置",
"control_which_file_types_can_be_uploaded": "控制 可以 上传的 文件 类型",
"convert_to_multiple_choice": "转换为 多选",
"convert_to_single_choice": "转换为 单选",
"country": "国家",
@@ -1275,13 +1272,11 @@
"darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。",
"date_format": "日期格式",
"days_before_showing_this_survey_again": "距离上次显示问卷后需间隔不少于指定天数,才能再次显示此问卷。",
"delete_anyways": "仍然删除",
"delete_block": "删除区块",
"delete_choice": "删除 选择",
"disable_the_visibility_of_survey_progress": "禁用问卷 进度 的可见性。",
"display_an_estimate_of_completion_time_for_survey": "显示 调查 预计 完成 时间",
"display_number_of_responses_for_survey": "显示 调查 响应 数量",
"display_type": "显示类型",
"divide": "划分 /",
"does_not_contain": "不包含",
"does_not_end_with": "不 以 结尾",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "不包括所有 ",
"does_not_include_one_of": "不包括一 个",
"does_not_start_with": "不 以 开头",
"dropdown": "下拉菜单",
"duplicate_block": "复制区块",
"duplicate_question": "复制问题",
"edit_link": "编辑 链接",
@@ -1381,7 +1375,8 @@
"hide_progress_bar": "隐藏 进度 条",
"hide_question_settings": "隐藏问题设置",
"hostname": "主 机 名",
"if_you_need_more_please": "如果您需要更多,请",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣",
"if_you_need_more_please": "如果你需要更多,请",
"if_you_really_want_that_answer_ask_until_you_get_it": "每次触发时都会显示,直到提交回应为止。",
"ignore_global_waiting_time": "忽略冷却期",
"ignore_global_waiting_time_description": "只要满足条件,此调查即可显示,即使最近刚显示过其他调查。",
@@ -1391,9 +1386,7 @@
"initial_value": "初始 值",
"inner_text": "内文",
"input_border_color": "输入 边框 颜色",
"input_border_color_description": "勾勒文本输入框和多行文本框的边框。",
"input_color": "输入颜色",
"input_color_description": "填充文本输入框内部。",
"insert_link": "插入 链接",
"invalid_targeting": "无效的目标: 请检查 您 的受众过滤器",
"invalid_video_url_warning": "请输入有效的 YouTube、Vimeo 或 Loom URL 。我们目前不支持其他 视频 托管服务提供商。",
@@ -1420,10 +1413,10 @@
"key": "键",
"last_name": "姓",
"let_people_upload_up_to_25_files_at_the_same_time": "允许 人们 同时 上传 最多 25 个 文件",
"limit_the_maximum_file_size": "限制上传文件的最大大小。",
"limit_upload_file_size_to": "将上传文件大小限制为",
"limit_file_types": "限制 文件 类型",
"limit_the_maximum_file_size": "限制 最大 文件 大小",
"limit_upload_file_size_to": "将 上传 文件 大小 限制 为",
"link_survey_description": "分享 问卷 页面 链接 或 将其 嵌入 网页 或 电子邮件 中。",
"list": "列表",
"load_segment": "载入 段落",
"logic_error_warning": "更改 将 导致 逻辑 错误",
"logic_error_warning_text": "更改问题类型 会 移除 此问题 的 逻辑条件",
@@ -1434,8 +1427,8 @@
"manage_languages": "管理 语言",
"matrix_all_fields": "所有字段",
"matrix_rows": "行",
"max_file_size": "最大文件大小",
"max_file_size_limit_is": "最大文件大小限制",
"max_file_size": "最大 文件 大小",
"max_file_size_limit_is": "最大 文件 大小 限制",
"move_question_to_block": "将问题移动到区块",
"multiply": "乘 *",
"needed_for_self_hosted_cal_com_instance": "需要用于 自建 Cal.com 实例",
@@ -1467,6 +1460,7 @@
"picture_idx": "图片 {idx}",
"pin_can_only_contain_numbers": "PIN 只能包含数字。",
"pin_must_be_a_four_digit_number": "PIN 必须是 四 位数字。",
"please_enter_a_file_extension": "请输入 文件 扩展名。",
"please_enter_a_valid_url": "请输入有效的 URL(例如, https://example.com ",
"please_set_a_survey_trigger": "请 设置 一个 调查 触发",
"please_specify": "请 指定",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "只有 拥有 PIN 的 用户 可以 访问 调查。",
"publish": "发布",
"question": "问题",
"question_color": "问题颜色",
"question_deleted": "问题 已删除",
"question_duplicated": "问题重复。",
"question_id_updated": "问题 ID 更新",
"question_used_in_logic_warning_text": "此区块中的元素已被用于逻辑规则,您确定要删除吗?",
"question_used_in_logic_warning_title": "逻辑不一致",
"question_used_in_quota": "此问题正在被“{quotaName}”配额使用",
"question_used_in_logic": "\"这个 问题 在 问题 {questionIndex} 的 逻辑 中 使用。\"",
"question_used_in_quota": "此 问题 正在 被 \"{quotaName}\" 配额 使用",
"question_used_in_recall": "此问题正在召回于问题 {questionIndex}。",
"question_used_in_recall_ending_card": "此 问题 正在召回于结束 卡片。",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "响应 限制 、 重定向 和 更多 。",
"response_options": "响应 选项",
"roundness": "圆度",
"roundness_description": "控制卡片角的圆润程度。",
"row_used_in_logic_error": "\"这个 行 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
"rows": "行",
"save_and_close": "保存 和 关闭",
@@ -1546,7 +1539,6 @@
"search_for_images": "搜索 图片",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "触发后 如果 没有 应答 将 在 几秒 后 关闭 调查",
"seconds_before_showing_the_survey": "显示问卷前 几秒",
"select_field": "选择字段",
"select_or_type_value": "选择 或 输入 值",
"select_ordering": "选择排序",
"select_saved_action": "选择 保存的 操作",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "样式 设置 为 主题 风格",
"subheading": "子标题",
"subtract": "减 -",
"suggest_colors": "建议颜色",
"survey_completed_heading": "调查 完成",
"survey_completed_subheading": "此 免费 & 开源 调查 已 关闭",
"survey_display_settings": "调查显示设置",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "仅显示一次,即使他们未回应。",
"then": "然后",
"this_action_will_remove_all_the_translations_from_this_survey": "此操作将删除该调查中的所有翻译。",
"this_extension_is_already_added": "此扩展已经添加。",
"this_file_type_is_not_supported": "此 文件 类型 不 支持。",
"three_points": "3 分",
"times": "次数",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "为了 保持 所有 调查 的 放置 一致,您 可以",
@@ -1613,51 +1608,8 @@
"upper_label": "上限标签",
"url_filters": "URL 过滤器",
"url_not_supported": "URL 不支持",
"validation": {
"add_validation_rule": "添加验证规则",
"answer_all_rows": "请填写所有行",
"characters": "字符",
"contains": "包含",
"delete_validation_rule": "删除验证规则",
"does_not_contain": "不包含",
"email": "是有效的邮箱地址",
"end_date": "结束日期",
"file_extension_is": "文件扩展名为",
"file_extension_is_not": "文件扩展名不是",
"is": "等于",
"is_between": "介于",
"is_earlier_than": "早于",
"is_greater_than": "大于",
"is_later_than": "晚于",
"is_less_than": "小于",
"is_not": "不等于",
"is_not_between": "不介于",
"kb": "KB",
"max_length": "最多",
"max_selections": "最多",
"max_value": "最多",
"mb": "MB",
"min_length": "至少",
"min_selections": "至少",
"min_value": "至少",
"minimum_options_ranked": "最少排序选项数",
"minimum_rows_answered": "最少回答行数",
"options_selected": "已选择的选项",
"pattern": "匹配正则表达式模式",
"phone": "是有效的手机号",
"rank_all_options": "对所有选项进行排序",
"select_file_extensions": "选择文件扩展名...",
"select_option": "选择选项",
"start_date": "开始日期",
"url": "是有效的URL"
},
"validation_logic_and": "全部为真",
"validation_logic_or": "任一为真",
"validation_rules": "校验规则",
"validation_rules_description": "仅接受符合以下条件的回复",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{variable} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "变量{variableName}”正在被“{quotaName}配额使用,请先将其从配额中移除",
"variable_name_conflicts_with_hidden_field": "变量名与已有的隐藏字段 ID 冲突。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "变量 \"{variableName}\" 正在 被 \"{quotaName}\" 配额 使用",
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
"variable_name_must_start_with_a_letter": "变量名 必须 以字母开头。",
"variable_used_in_recall": "变量 \"{variable}\" 正在召回于问题 {questionIndex}。",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "添加背景色",
"add_background_color_description": "为 logo 容器添加背景色。",
"advanced_styling_field_border_radius": "边框圆角",
"advanced_styling_field_button_bg": "按钮背景",
"advanced_styling_field_button_bg_description": "填充“下一步/提交”按钮。",
"advanced_styling_field_button_border_radius_description": "设置按钮圆角。",
"advanced_styling_field_button_font_size_description": "调整按钮标签文字大小。",
"advanced_styling_field_button_font_weight_description": "设置按钮文字的粗细。",
"advanced_styling_field_button_height_description": "控制按钮高度。",
"advanced_styling_field_button_padding_x_description": "增加左右间距。",
"advanced_styling_field_button_padding_y_description": "增加上下间距。",
"advanced_styling_field_button_text": "按钮文字",
"advanced_styling_field_button_text_description": "设置按钮内标签的颜色。",
"advanced_styling_field_description_color": "描述颜色",
"advanced_styling_field_description_color_description": "设置每个标题下方文字的颜色。",
"advanced_styling_field_description_size": "描述字体大小",
"advanced_styling_field_description_size_description": "调整描述文字大小。",
"advanced_styling_field_description_weight": "描述字体粗细",
"advanced_styling_field_description_weight_description": "设置描述文字的粗细。",
"advanced_styling_field_font_size": "字体大小",
"advanced_styling_field_font_weight": "字体粗细",
"advanced_styling_field_headline_color": "标题颜色",
"advanced_styling_field_headline_color_description": "设置主问题文字的颜色。",
"advanced_styling_field_headline_size": "标题字体大小",
"advanced_styling_field_headline_size_description": "调整主标题文字大小。",
"advanced_styling_field_headline_weight": "标题字体粗细",
"advanced_styling_field_headline_weight_description": "设置主标题文字的粗细。",
"advanced_styling_field_height": "高度",
"advanced_styling_field_indicator_bg": "指示器背景",
"advanced_styling_field_indicator_bg_description": "设置进度条已填充部分的颜色。",
"advanced_styling_field_input_border_radius_description": "设置输入框圆角。",
"advanced_styling_field_input_font_size_description": "调整输入框内文字大小。",
"advanced_styling_field_input_height_description": "控制输入框高度。",
"advanced_styling_field_input_padding_x_description": "增加输入框左右间距。",
"advanced_styling_field_input_padding_y_description": "为输入框上下添加间距。",
"advanced_styling_field_input_placeholder_opacity_description": "调整占位提示文字的透明度。",
"advanced_styling_field_input_shadow_description": "为输入框添加投影效果。",
"advanced_styling_field_input_text": "输入文字",
"advanced_styling_field_input_text_description": "设置输入框内已输入文字的颜色。",
"advanced_styling_field_option_bg": "背景色",
"advanced_styling_field_option_bg_description": "设置选项项的背景色。",
"advanced_styling_field_option_border_radius_description": "设置选项的圆角。",
"advanced_styling_field_option_font_size_description": "调整选项标签文字的大小。",
"advanced_styling_field_option_label": "标签颜色",
"advanced_styling_field_option_label_description": "设置选项标签文字的颜色。",
"advanced_styling_field_option_padding_x_description": "为选项左右添加间距。",
"advanced_styling_field_option_padding_y_description": "为选项上下添加间距。",
"advanced_styling_field_padding_x": "横向内边距",
"advanced_styling_field_padding_y": "纵向内边距",
"advanced_styling_field_placeholder_opacity": "占位符透明度",
"advanced_styling_field_shadow": "阴影",
"advanced_styling_field_track_bg": "轨道背景",
"advanced_styling_field_track_bg_description": "设置进度条未填充部分的颜色。",
"advanced_styling_field_track_height": "轨道高度",
"advanced_styling_field_track_height_description": "控制进度条的粗细。",
"advanced_styling_field_upper_label_color": "标题标签颜色",
"advanced_styling_field_upper_label_color_description": "设置输入框上方小标签的颜色。",
"advanced_styling_field_upper_label_size": "标题标签字体大小",
"advanced_styling_field_upper_label_size_description": "调整输入框上方小标签的大小。",
"advanced_styling_field_upper_label_weight": "标题标签字体粗细",
"advanced_styling_field_upper_label_weight_description": "设置标签文字的粗细。",
"advanced_styling_section_buttons": "按钮",
"advanced_styling_section_headlines": "标题和描述",
"advanced_styling_section_inputs": "输入项",
"advanced_styling_section_options": "选项(单选/多选)",
"app_survey_placement": "应用调查放置位置",
"app_survey_placement_settings_description": "更改调查在您的 Web 应用或网站中显示的位置。",
"centered_modal_overlay_color": "居中模态遮罩层颜色",
"email_customization": "邮件自定义",
"email_customization_description": "更改 Formbricks 代表您发送邮件的外观和风格。",
"enable_custom_styling": "启用自定义样式",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Formbricks 品牌标识已隐藏。",
"formbricks_branding_settings_description": "我们很感谢您的支持,但如果您关闭它,我们也能理解。",
"formbricks_branding_shown": "Formbricks 品牌标识已显示。",
"generate_theme_btn": "生成",
"generate_theme_confirmation": "要根据你的品牌色生成一个匹配的配色主题吗?这将覆盖你当前的颜色设置。",
"generate_theme_header": "生成配色主题?",
"logo_removed_successfully": "logo 移除成功",
"logo_settings_description": "上传您的公司 logo,用于品牌调查和链接预览。",
"logo_updated_successfully": "logo 更新成功",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "在 {type} 调查中显示 Formbricks 品牌标识",
"show_powered_by_formbricks": "显示“Powered by Formbricks”标识",
"styling_updated_successfully": "样式更新成功",
"suggest_colors": "推荐颜色",
"theme": "主题",
"theme_settings_description": "为所有问卷创建一个样式主题。你可以为每个问卷启用自定义样式。"
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "是 保持我 更新 。",
"preview_survey_question_2_choice_2_label": "不,谢谢!",
"preview_survey_question_2_headline": "想 了解 最新信息吗?",
"preview_survey_question_2_subheader": "这是一个示例描述。",
"preview_survey_welcome_card_headline": "欢迎!",
"prioritize_features_description": "确定 用户 最 需要 和 最 不 需要 的 功能。",
"prioritize_features_name": "优先 功能",
+36 -151
View File
@@ -243,6 +243,7 @@
"imprint": "版本訊息",
"in_progress": "進行中",
"inactive_surveys": "停用中的問卷",
"input_type": "輸入類型",
"integration": "整合",
"integrations": "整合",
"invalid_date": "無效日期",
@@ -254,7 +255,6 @@
"label": "標籤",
"language": "語言",
"learn_more": "瞭解更多",
"license_expired": "License Expired",
"light_overlay": "淺色覆蓋",
"limits_reached": "已達上限",
"link": "連結",
@@ -267,11 +267,13 @@
"look_and_feel": "外觀與風格",
"manage": "管理",
"marketing": "行銷",
"maximum": "最大值",
"member": "成員",
"members": "成員",
"members_and_teams": "成員與團隊",
"membership_not_found": "找不到成員資格",
"metadata": "元數據",
"minimum": "最小值",
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
"mobile_overlay_title": "糟糕 ,偵測到小螢幕!",
@@ -285,7 +287,6 @@
"no_background_image_found": "找不到背景圖片。",
"no_code": "無程式碼",
"no_files_uploaded": "沒有上傳任何檔案",
"no_overlay": "無覆蓋層",
"no_quotas_found": "找不到 配額",
"no_result_found": "找不到結果",
"no_results": "沒有結果",
@@ -312,7 +313,6 @@
"organization_teams_not_found": "找不到組織團隊",
"other": "其他",
"others": "其他",
"overlay_color": "覆蓋層顏色",
"overview": "概覽",
"password": "密碼",
"paused": "已暫停",
@@ -326,7 +326,7 @@
"placeholder": "提示文字",
"please_select_at_least_one_survey": "請選擇至少一個問卷",
"please_select_at_least_one_trigger": "請選擇至少一個觸發器",
"please_upgrade_your_plan": "請升級您的方案",
"please_upgrade_your_plan": "請升級您的方案",
"preview": "預覽",
"preview_survey": "預覽問卷",
"privacy": "隱私權政策",
@@ -352,7 +352,6 @@
"request_trial_license": "請求試用授權",
"reset_to_default": "重設為預設值",
"response": "回應",
"response_id": "回應 ID",
"responses": "回應",
"restart": "重新開始",
"role": "角色",
@@ -464,8 +463,7 @@
"you_have_reached_your_limit_of_workspace_limit": "您已達到 {projectLimit} 個工作區的上限。",
"you_have_reached_your_monthly_miu_limit_of": "您已達到每月 MIU 上限:",
"you_have_reached_your_monthly_response_limit_of": "您已達到每月回應上限:",
"you_will_be_downgraded_to_the_community_edition_on_date": "您將於 '{'date'}' 降級至社群版。",
"your_license_has_expired_please_renew": "Your enterprise license has expired. Please renew it to continue using enterprise features."
"you_will_be_downgraded_to_the_community_edition_on_date": "您將於 '{'date'}' 降級至社群版。"
},
"emails": {
"accept": "接受",
@@ -956,32 +954,19 @@
"enterprise_features": "企業版功能",
"get_an_enterprise_license_to_get_access_to_all_features": "取得企業授權以存取所有功能。",
"keep_full_control_over_your_data_privacy_and_security": "完全掌控您的資料隱私權和安全性。",
"license_invalid_description": "你在 ENTERPRISE_LICENSE_KEY 環境變數中填寫的授權金鑰無效。請檢查是否有輸入錯誤,或申請新的金鑰。",
"license_status": "授權狀態",
"license_status_active": "有效",
"license_status_description": "你的企業授權狀態。",
"license_status_expired": "已過期",
"license_status_invalid": "授權無效",
"license_status_unreachable": "無法連線",
"license_unreachable_grace_period": "無法連線至授權伺服器。在 3 天的寬限期內,你的企業功能仍可使用,寬限期將於 {gracePeriodEnd} 結束。",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "無需通話,無附加條件:填寫此表單,請求免費 30 天試用授權以測試所有功能:",
"no_credit_card_no_sales_call_just_test_it": "無需信用卡。無需銷售電話。只需測試一下 :)",
"on_request": "依要求",
"organization_roles": "組織角色(管理員、編輯者、開發人員等)",
"questions_please_reach_out_to": "有任何問題?請聯絡",
"recheck_license": "重新檢查授權",
"recheck_license_failed": "授權檢查失敗。授權伺服器可能無法連線。",
"recheck_license_invalid": "授權金鑰無效。請確認你的 ENTERPRISE_LICENSE_KEY。",
"recheck_license_success": "授權檢查成功",
"recheck_license_unreachable": "授權伺服器無法連線,請稍後再試。",
"rechecking": "正在重新檢查...",
"request_30_day_trial_license": "請求 30 天試用授權",
"saml_sso": "SAML SSO",
"service_level_agreement": "服務等級協定",
"soc2_hipaa_iso_27001_compliance_check": "SOC2、HIPAA、ISO 27001 合規性檢查",
"sso": "SSOGoogle、Microsoft、OpenID Connect",
"teams": "團隊和存取角色(讀取、讀取和寫入、管理)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "免費解鎖 Formbricks 的全部功能,為期 30 天。"
"unlock_the_full_power_of_formbricks_free_for_30_days": "免費解鎖 Formbricks 的全部功能,為期 30 天。",
"your_enterprise_license_is_active_all_features_unlocked": "您的企業授權處於活動狀態。所有功能都已解鎖。"
},
"general": {
"bulk_invite_warning_description": "在免費方案中,所有組織成員始終會被指派「擁有者」角色。",
@@ -1005,7 +990,7 @@
"from_your_organization": "來自您的組織",
"invitation_sent_once_more": "已再次發送邀請。",
"invite_deleted_successfully": "邀請已成功刪除",
"invite_expires_on": "邀請於 '{'date'}' 過期",
"invited_on": "邀請於 '{'date'}'",
"invites_failed": "邀請失敗",
"leave_organization": "離開組織",
"leave_organization_description": "您將離開此組織並失去對所有問卷和回應的存取權限。只有再次收到邀請,您才能重新加入。",
@@ -1118,6 +1103,8 @@
"please_fill_all_workspace_fields": "請填寫所有欄位以新增工作區。",
"read": "讀取",
"read_write": "讀取和寫入",
"select_member": "選擇成員",
"select_workspace": "選擇工作區",
"team_admin": "團隊管理員",
"team_created_successfully": "團隊已成功建立。",
"team_deleted_successfully": "團隊已成功刪除。",
@@ -1167,6 +1154,7 @@
"add_fallback_placeholder": "新增 預設 以顯示是否沒 有 值 可 回憶 。",
"add_hidden_field_id": "新增隱藏欄位 ID",
"add_highlight_border": "新增醒目提示邊框",
"add_highlight_border_description": "在您的問卷卡片新增外邊框。",
"add_logic": "新增邏輯",
"add_none_of_the_above": "新增 \"以上皆非\"",
"add_option": "新增選項",
@@ -1184,6 +1172,7 @@
"adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。",
"adjust_the_theme_in_the": "在",
"all_other_answers_will_continue_to": "所有其他答案將繼續",
"allow_file_type": "允許檔案類型",
"allow_multi_select": "允許多重選取",
"allow_multiple_files": "允許上傳多個檔案",
"allow_users_to_select_more_than_one_image": "允許使用者選取多張圖片",
@@ -1205,7 +1194,6 @@
"block_duplicated": "區塊已複製。",
"bold": "粗體",
"brand_color": "品牌顏色",
"brand_color_description": "應用於按鈕、連結和重點標示。",
"brightness": "亮度",
"bulk_edit": "批次編輯",
"bulk_edit_description": "在下方逐行編輯所有選項。空白行將被略過,重複項目將被移除。",
@@ -1223,9 +1211,7 @@
"capture_new_action": "擷取新操作",
"card_arrangement_for_survey_type_derived": "'{'surveyTypeDerived'}' 問卷的卡片排列",
"card_background_color": "卡片背景顏色",
"card_background_color_description": "填滿問卷卡片區域。",
"card_border_color": "卡片邊框顏色",
"card_border_color_description": "描繪問卷卡片的邊框。",
"card_styling": "卡片樣式",
"casual": "隨意",
"caution_edit_duplicate": "複製 & 編輯",
@@ -1236,14 +1222,24 @@
"caution_explanation_responses_are_safe": "較舊和較新的回應會混在一起,可能導致數據摘要失準。",
"caution_recommendation": "這可能導致調查摘要中的數據不一致。我們建議複製這個調查。",
"caution_text": "變更會導致不一致",
"centered_modal_overlay_color": "置中彈窗覆蓋顏色",
"change_anyway": "仍然變更",
"change_background": "變更背景",
"change_question_type": "變更問題類型",
"change_survey_type": "切換問卷類型會影響現有訪問",
"change_the_background_color_of_the_card": "變更卡片的背景顏色。",
"change_the_background_color_of_the_input_fields": "變更輸入欄位的背景顏色。",
"change_the_background_to_a_color_image_or_animation": "將背景變更為顏色、圖片或動畫。",
"change_the_border_color_of_the_card": "變更卡片的邊框顏色。",
"change_the_border_color_of_the_input_fields": "變更輸入欄位的邊框顏色。",
"change_the_border_radius_of_the_card_and_the_inputs": "變更卡片和輸入的邊框半徑。",
"change_the_brand_color_of_the_survey": "變更問卷的品牌顏色。",
"change_the_placement_of_this_survey": "變更此問卷的位置。",
"change_the_question_color_of_the_survey": "變更問卷的問題顏色。",
"changes_saved": "已儲存變更。",
"changing_survey_type_will_remove_existing_distribution_channels": "更改問卷類型會影響其共享方式。如果受訪者已擁有當前類型的存取連結,則在切換後可能會失去存取權限。",
"character_limit_toggle_description": "限制答案的長度或短度。",
"character_limit_toggle_title": "新增字元限制",
"checkbox_label": "核取方塊標籤",
"choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。",
"choose_the_first_question_on_your_block": "選擇此區塊的第一個問題",
@@ -1263,6 +1259,7 @@
"contact_fields": "聯絡人欄位",
"contains": "包含",
"continue_to_settings": "繼續設定",
"control_which_file_types_can_be_uploaded": "控制可以上傳哪些檔案類型。",
"convert_to_multiple_choice": "轉換為多選",
"convert_to_single_choice": "轉換為單選",
"country": "國家/地區",
@@ -1275,13 +1272,11 @@
"darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。",
"date_format": "日期格式",
"days_before_showing_this_survey_again": "距離上次顯示問卷後,需間隔指定天數才能再次顯示此問卷。",
"delete_anyways": "仍要刪除",
"delete_block": "刪除區塊",
"delete_choice": "刪除選項",
"disable_the_visibility_of_survey_progress": "停用問卷進度的可見性。",
"display_an_estimate_of_completion_time_for_survey": "顯示問卷的估計完成時間",
"display_number_of_responses_for_survey": "顯示問卷的回應數",
"display_type": "顯示類型",
"divide": "除 /",
"does_not_contain": "不包含",
"does_not_end_with": "不以...結尾",
@@ -1289,7 +1284,6 @@
"does_not_include_all_of": "不包含全部",
"does_not_include_one_of": "不包含其中之一",
"does_not_start_with": "不以...開頭",
"dropdown": "下拉選單",
"duplicate_block": "複製區塊",
"duplicate_question": "複製問題",
"edit_link": "編輯 連結",
@@ -1381,6 +1375,7 @@
"hide_progress_bar": "隱藏進度列",
"hide_question_settings": "隱藏問題設定",
"hostname": "主機名稱",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫",
"if_you_need_more_please": "如果您需要更多,請",
"if_you_really_want_that_answer_ask_until_you_get_it": "每次觸發時都顯示,直到提交回應為止。",
"ignore_global_waiting_time": "忽略冷卻期",
@@ -1391,9 +1386,7 @@
"initial_value": "初始值",
"inner_text": "內部文字",
"input_border_color": "輸入邊框顏色",
"input_border_color_description": "描繪文字輸入框和文字區域的邊框。",
"input_color": "輸入顏色",
"input_color_description": "填滿文字輸入框的內部。",
"insert_link": "插入 連結",
"invalid_targeting": "目標設定無效:請檢查您的受眾篩選器",
"invalid_video_url_warning": "請輸入有效的 YouTube、Vimeo 或 Loom 網址。我們目前不支援其他影片託管提供者。",
@@ -1420,10 +1413,10 @@
"key": "金鑰",
"last_name": "姓氏",
"let_people_upload_up_to_25_files_at_the_same_time": "允許使用者同時上傳最多 25 個檔案。",
"limit_the_maximum_file_size": "限制上傳檔案的最大大小。",
"limit_upload_file_size_to": "將上傳檔案大小限制為",
"limit_file_types": "限制檔案類型",
"limit_the_maximum_file_size": "限制最大檔案大小",
"limit_upload_file_size_to": "限制上傳檔案大小為",
"link_survey_description": "分享問卷頁面的連結或將其嵌入網頁或電子郵件中。",
"list": "清單",
"load_segment": "載入區隔",
"logic_error_warning": "變更將導致邏輯錯誤",
"logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件",
@@ -1467,6 +1460,7 @@
"picture_idx": "圖片 '{'idx'}'",
"pin_can_only_contain_numbers": "PIN 碼只能包含數字。",
"pin_must_be_a_four_digit_number": "PIN 碼必須是四位數的數字。",
"please_enter_a_file_extension": "請輸入檔案副檔名。",
"please_enter_a_valid_url": "請輸入有效的 URL(例如:https://example.com",
"please_set_a_survey_trigger": "請設定問卷觸發器",
"please_specify": "請指定",
@@ -1477,12 +1471,12 @@
"protect_survey_with_pin_description": "只有擁有 PIN 碼的使用者才能存取問卷。",
"publish": "發布",
"question": "問題",
"question_color": "問題顏色",
"question_deleted": "問題已刪除。",
"question_duplicated": "問題已複製。",
"question_id_updated": "問題 ID 已更新",
"question_used_in_logic_warning_text": "此區塊中的元素已用於邏輯規則,確定要刪除嗎?",
"question_used_in_logic_warning_title": "邏輯不一致",
"question_used_in_quota": "此問題正被使用於「{quotaName}」配額中",
"question_used_in_logic": "此問題用於問題 '{'questionIndex'}' 的邏輯中。",
"question_used_in_quota": "此問題 正被使用於 \"{quotaName}\" 配額中",
"question_used_in_recall": "此問題於問題 {questionIndex} 中被召回。",
"question_used_in_recall_ending_card": "此問題於結尾卡中被召回。",
"quotas": {
@@ -1538,7 +1532,6 @@
"response_limits_redirections_and_more": "回應限制、重新導向等。",
"response_options": "回應選項",
"roundness": "圓角",
"roundness_description": "調整卡片邊角的圓弧度。",
"row_used_in_logic_error": "此 row 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
"rows": "列",
"save_and_close": "儲存並關閉",
@@ -1546,7 +1539,6 @@
"search_for_images": "搜尋圖片",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "如果沒有回應,則在觸發後幾秒關閉問卷",
"seconds_before_showing_the_survey": "秒後顯示問卷。",
"select_field": "選擇欄位",
"select_or_type_value": "選取或輸入值",
"select_ordering": "選取排序",
"select_saved_action": "選取已儲存的操作",
@@ -1580,6 +1572,7 @@
"styling_set_to_theme_styles": "樣式設定為主題樣式",
"subheading": "副標題",
"subtract": "減 -",
"suggest_colors": "建議顏色",
"survey_completed_heading": "問卷已完成",
"survey_completed_subheading": "此免費且開源的問卷已關閉",
"survey_display_settings": "問卷顯示設定",
@@ -1593,6 +1586,8 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "僅顯示一次,即使他們未回應。",
"then": "然後",
"this_action_will_remove_all_the_translations_from_this_survey": "此操作將從此問卷中移除所有翻譯。",
"this_extension_is_already_added": "已新增此擴充功能。",
"this_file_type_is_not_supported": "不支援此檔案類型。",
"three_points": "3 分",
"times": "次",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "若要保持所有問卷的位置一致,您可以",
@@ -1613,51 +1608,8 @@
"upper_label": "上標籤",
"url_filters": "網址篩選器",
"url_not_supported": "不支援網址",
"validation": {
"add_validation_rule": "新增驗證規則",
"answer_all_rows": "請填答所有列",
"characters": "字元",
"contains": "包含",
"delete_validation_rule": "刪除驗證規則",
"does_not_contain": "不包含",
"email": "是有效的電子郵件",
"end_date": "結束日期",
"file_extension_is": "檔案副檔名為",
"file_extension_is_not": "檔案副檔名不是",
"is": "等於",
"is_between": "介於",
"is_earlier_than": "早於",
"is_greater_than": "大於",
"is_later_than": "晚於",
"is_less_than": "小於",
"is_not": "不等於",
"is_not_between": "不介於",
"kb": "KB",
"max_length": "最多",
"max_selections": "最多",
"max_value": "最多",
"mb": "MB",
"min_length": "至少",
"min_selections": "至少",
"min_value": "至少",
"minimum_options_ranked": "最少排序選項數",
"minimum_rows_answered": "最少作答列數",
"options_selected": "已選擇的選項",
"pattern": "符合正則表達式樣式",
"phone": "是有效的電話號碼",
"rank_all_options": "請為所有選項排序",
"select_file_extensions": "請選擇檔案副檔名...",
"select_option": "選擇選項",
"start_date": "開始日期",
"url": "是有效的 URL"
},
"validation_logic_and": "全部為真",
"validation_logic_or": "任一為真",
"validation_rules": "驗證規則",
"validation_rules_description": "僅接受符合下列條件的回應",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "變數{variableName}正被使用於{quotaName}配額中",
"variable_name_conflicts_with_hidden_field": "變數名稱與現有的隱藏欄位 ID 衝突。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "變數 \"{variableName}\" 正被使用於 \"{quotaName}\" 配額中",
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
"variable_name_must_start_with_a_letter": "變數名稱必須以字母開頭。",
"variable_used_in_recall": "變數 \"{variable}\" 於問題 {questionIndex} 中被召回。",
@@ -2063,71 +2015,9 @@
"look": {
"add_background_color": "新增背景顏色",
"add_background_color_description": "為標誌容器新增背景顏色。",
"advanced_styling_field_border_radius": "邊框圓角",
"advanced_styling_field_button_bg": "按鈕背景",
"advanced_styling_field_button_bg_description": "填滿「下一步」/「送出」按鈕。",
"advanced_styling_field_button_border_radius_description": "調整按鈕的圓角。",
"advanced_styling_field_button_font_size_description": "調整按鈕標籤文字的大小。",
"advanced_styling_field_button_font_weight_description": "讓按鈕文字變細或變粗。",
"advanced_styling_field_button_height_description": "調整按鈕的高度。",
"advanced_styling_field_button_padding_x_description": "在左右兩側增加間距。",
"advanced_styling_field_button_padding_y_description": "在上下兩側增加間距。",
"advanced_styling_field_button_text": "按鈕文字",
"advanced_styling_field_button_text_description": "設定按鈕內標籤的顏色。",
"advanced_styling_field_description_color": "說明文字顏色",
"advanced_styling_field_description_color_description": "設定每個標題下方文字的顏色。",
"advanced_styling_field_description_size": "說明文字大小",
"advanced_styling_field_description_size_description": "調整說明文字的大小。",
"advanced_styling_field_description_weight": "說明文字粗細",
"advanced_styling_field_description_weight_description": "讓說明文字變細或變粗。",
"advanced_styling_field_font_size": "字體大小",
"advanced_styling_field_font_weight": "字體粗細",
"advanced_styling_field_headline_color": "標題顏色",
"advanced_styling_field_headline_color_description": "設定主要問題文字的顏色。",
"advanced_styling_field_headline_size": "標題字體大小",
"advanced_styling_field_headline_size_description": "調整標題文字的大小。",
"advanced_styling_field_headline_weight": "標題字體粗細",
"advanced_styling_field_headline_weight_description": "讓標題文字變細或變粗。",
"advanced_styling_field_height": "高度",
"advanced_styling_field_indicator_bg": "指示器背景",
"advanced_styling_field_indicator_bg_description": "設定進度條已填滿部分的顏色。",
"advanced_styling_field_input_border_radius_description": "調整輸入框的圓角。",
"advanced_styling_field_input_font_size_description": "調整輸入框內輸入文字的大小。",
"advanced_styling_field_input_height_description": "調整輸入欄位的高度。",
"advanced_styling_field_input_padding_x_description": "在左右兩側增加間距。",
"advanced_styling_field_input_padding_y_description": "在上方和下方增加間距。",
"advanced_styling_field_input_placeholder_opacity_description": "讓提示文字變得更淡。",
"advanced_styling_field_input_shadow_description": "在輸入框周圍加上陰影。",
"advanced_styling_field_input_text": "輸入文字",
"advanced_styling_field_input_text_description": "設定輸入文字的顏色。",
"advanced_styling_field_option_bg": "背景",
"advanced_styling_field_option_bg_description": "填滿選項項目背景。",
"advanced_styling_field_option_border_radius_description": "讓選項的邊角變圓。",
"advanced_styling_field_option_font_size_description": "調整選項標籤文字的大小。",
"advanced_styling_field_option_label": "標籤顏色",
"advanced_styling_field_option_label_description": "設定選項標籤文字的顏色。",
"advanced_styling_field_option_padding_x_description": "在左側和右側增加間距。",
"advanced_styling_field_option_padding_y_description": "在上方和下方增加間距。",
"advanced_styling_field_padding_x": "左右內距",
"advanced_styling_field_padding_y": "上下內距",
"advanced_styling_field_placeholder_opacity": "預設文字透明度",
"advanced_styling_field_shadow": "陰影",
"advanced_styling_field_track_bg": "軌道背景",
"advanced_styling_field_track_bg_description": "設定進度條未填滿部分的顏色。",
"advanced_styling_field_track_height": "軌道高度",
"advanced_styling_field_track_height_description": "調整進度條的厚度。",
"advanced_styling_field_upper_label_color": "標題標籤顏色",
"advanced_styling_field_upper_label_color_description": "設定輸入框上方小標籤的顏色。",
"advanced_styling_field_upper_label_size": "標題標籤字體大小",
"advanced_styling_field_upper_label_size_description": "調整輸入框上方小標籤的大小。",
"advanced_styling_field_upper_label_weight": "標題標籤字體粗細",
"advanced_styling_field_upper_label_weight_description": "讓標籤字體變細或變粗。",
"advanced_styling_section_buttons": "按鈕",
"advanced_styling_section_headlines": "標題與說明",
"advanced_styling_section_inputs": "輸入欄位",
"advanced_styling_section_options": "選項(單選/複選)",
"app_survey_placement": "應用程式問卷位置",
"app_survey_placement_settings_description": "變更問卷在您的網頁應用程式或網站中顯示的位置。",
"centered_modal_overlay_color": "置中彈窗覆蓋顏色",
"email_customization": "電子郵件自訂化",
"email_customization_description": "變更 Formbricks 代表您發送的電子郵件外觀與風格。",
"enable_custom_styling": "啟用自訂樣式",
@@ -2138,9 +2028,6 @@
"formbricks_branding_hidden": "Formbricks 品牌標示已隱藏。",
"formbricks_branding_settings_description": "我們很感謝您的支持,但若您選擇關閉我們也能理解。",
"formbricks_branding_shown": "Formbricks 品牌標示已顯示。",
"generate_theme_btn": "產生",
"generate_theme_confirmation": "你想根據品牌色產生一組相符的主題色嗎?這將會覆蓋你目前的顏色設定。",
"generate_theme_header": "要產生主題色嗎?",
"logo_removed_successfully": "標誌已成功移除",
"logo_settings_description": "上傳您的公司標誌,以用於問卷和連結預覽的品牌展示。",
"logo_updated_successfully": "標誌已成功更新",
@@ -2155,7 +2042,6 @@
"show_formbricks_branding_in": "在 {type} 問卷中顯示 Formbricks 品牌標示",
"show_powered_by_formbricks": "顯示「Powered by Formbricks」標記",
"styling_updated_successfully": "樣式已成功更新",
"suggest_colors": "建議顏色",
"theme": "主題",
"theme_settings_description": "為所有調查建立樣式主題。您可以為每個調查啟用自訂樣式。"
},
@@ -2919,7 +2805,6 @@
"preview_survey_question_2_choice_1_label": "是,請保持通知我。",
"preview_survey_question_2_choice_2_label": "不用了,謝謝!",
"preview_survey_question_2_headline": "想要緊跟最新動態嗎?",
"preview_survey_question_2_subheader": "這是一個範例說明。",
"preview_survey_welcome_card_headline": "歡迎!",
"prioritize_features_description": "找出您的使用者最需要和最不需要的功能。",
"prioritize_features_name": "優先排序功能",
@@ -3,7 +3,6 @@
import { CheckCircle2Icon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TResponseWithQuotas } from "@formbricks/types/responses";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { getLocalizedValue } from "@/lib/i18n/utils";
@@ -68,16 +67,6 @@ export const SingleResponseCardBody = ({
<VerifiedEmail responseData={response.data} />
)}
{elements.map((question) => {
// Skip CTA elements without external buttons only if they have no response data
// This preserves historical data from when buttonExternal was true
if (
question.type === TSurveyElementTypeEnum.CTA &&
!question.buttonExternal &&
!response.data[question.id]
) {
return null;
}
const skipped = skippedQuestions.find((skippedQuestionElement) =>
skippedQuestionElement.includes(question.id)
);
-231
View File
@@ -1,231 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TValidationErrorMap } from "@formbricks/types/surveys/validation-rules";
import {
formatValidationErrorsForV1Api,
formatValidationErrorsForV2Api,
validateResponseData,
} from "@/modules/api/lib/validation";
const mockTransformQuestionsToBlocks = vi.fn();
const mockGetElementsFromBlocks = vi.fn();
const mockValidateBlockResponses = vi.fn();
vi.mock("@/app/lib/api/survey-transformation", () => ({
transformQuestionsToBlocks: (...args: unknown[]) => mockTransformQuestionsToBlocks(...args),
}));
vi.mock("@/lib/survey/utils", () => ({
getElementsFromBlocks: (...args: unknown[]) => mockGetElementsFromBlocks(...args),
}));
vi.mock("@formbricks/surveys/validation", () => ({
validateBlockResponses: (...args: unknown[]) => mockValidateBlockResponses(...args),
}));
describe("validateResponseData", () => {
beforeEach(() => vi.clearAllMocks());
const mockBlocks: TSurveyBlock[] = [
{
id: "block1",
name: "Block 1",
elements: [
{
id: "element1",
type: TSurveyElementTypeEnum.OpenText,
headline: { default: "Q1" },
required: false,
inputType: "text",
charLimit: { enabled: false },
},
],
},
];
const mockQuestions: TSurveyQuestion[] = [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Q1" },
required: false,
inputType: "text",
} as unknown as TSurveyQuestion,
];
const mockResponseData: TResponseData = { element1: "test" };
const mockElements = [
{
id: "element1",
type: TSurveyElementTypeEnum.OpenText,
headline: { default: "Q1" },
required: false,
inputType: "text",
charLimit: { enabled: false },
},
];
test("should use blocks when provided", () => {
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
const result = validateResponseData(mockBlocks, mockResponseData, "en");
expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(mockBlocks);
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en");
expect(result).toBeNull();
});
test("should return error map when validation fails", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
};
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue(errorMap);
expect(validateResponseData(mockBlocks, mockResponseData, "en")).toEqual(errorMap);
});
test("should transform questions to blocks when blocks are empty", () => {
const transformedBlocks = [{ ...mockBlocks[0] }];
mockTransformQuestionsToBlocks.mockReturnValue(transformedBlocks);
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData([], mockResponseData, "en", true, mockQuestions);
expect(mockTransformQuestionsToBlocks).toHaveBeenCalledWith(mockQuestions, []);
expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(transformedBlocks);
});
test("should prefer blocks over questions", () => {
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData(mockBlocks, mockResponseData, "en", true, mockQuestions);
expect(mockTransformQuestionsToBlocks).not.toHaveBeenCalled();
});
test("should return null when both blocks and questions are empty", () => {
expect(validateResponseData([], mockResponseData, "en", true, [])).toBeNull();
expect(validateResponseData(null, mockResponseData, "en", true, [])).toBeNull();
expect(validateResponseData(undefined, mockResponseData, "en", true, null)).toBeNull();
});
test("should use default language code", () => {
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData(mockBlocks, mockResponseData);
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en");
});
test("should validate only present fields when finished is false", () => {
const partialResponseData: TResponseData = { element1: "test" };
const partialElements = [mockElements[0]];
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData(mockBlocks, partialResponseData, "en", false);
expect(mockValidateBlockResponses).toHaveBeenCalledWith(partialElements, partialResponseData, "en");
});
test("should validate all fields when finished is true", () => {
const partialResponseData: TResponseData = { element1: "test" };
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData(mockBlocks, partialResponseData, "en", true);
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, partialResponseData, "en");
});
});
describe("formatValidationErrorsForV2Api", () => {
test("should convert error map to V2 API format", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
};
const result = formatValidationErrorsForV2Api(errorMap);
expect(result).toEqual([
{
field: "response.data.element1",
issue: "Min length required",
meta: { elementId: "element1", ruleId: "minLength", ruleType: "minLength" },
},
]);
});
test("should handle multiple errors per element", () => {
const errorMap: TValidationErrorMap = {
element1: [
{ ruleId: "minLength", ruleType: "minLength", message: "Min length" },
{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" },
],
};
const result = formatValidationErrorsForV2Api(errorMap);
expect(result).toHaveLength(2);
expect(result[0].field).toBe("response.data.element1");
expect(result[1].field).toBe("response.data.element1");
});
test("should handle multiple elements", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length" }],
element2: [{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" }],
};
const result = formatValidationErrorsForV2Api(errorMap);
expect(result).toHaveLength(2);
expect(result[0].field).toBe("response.data.element1");
expect(result[1].field).toBe("response.data.element2");
});
});
describe("formatValidationErrorsForV1Api", () => {
test("should convert error map to V1 API format", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
};
expect(formatValidationErrorsForV1Api(errorMap)).toEqual({
"response.data.element1": "Min length required",
});
});
test("should combine multiple errors with semicolon", () => {
const errorMap: TValidationErrorMap = {
element1: [
{ ruleId: "minLength", ruleType: "minLength", message: "Min length" },
{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" },
],
};
expect(formatValidationErrorsForV1Api(errorMap)).toEqual({
"response.data.element1": "Min length; Max length",
});
});
test("should handle multiple elements", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length" }],
element2: [{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" }],
};
expect(formatValidationErrorsForV1Api(errorMap)).toEqual({
"response.data.element1": "Min length",
"response.data.element2": "Max length",
});
});
});
-101
View File
@@ -1,101 +0,0 @@
import "server-only";
import { validateBlockResponses } from "@formbricks/surveys/validation";
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
import { TValidationErrorMap } from "@formbricks/types/surveys/validation-rules";
import { transformQuestionsToBlocks } from "@/app/lib/api/survey-transformation";
import { getElementsFromBlocks } from "@/lib/survey/utils";
import { ApiErrorDetails } from "@/modules/api/v2/types/api-error";
/**
* Validates response data against survey validation rules
* Handles partial responses (in-progress) by only validating present fields when finished is false
*
* @param blocks - Survey blocks containing elements with validation rules (preferred)
* @param responseData - Response data to validate (keyed by element ID)
* @param languageCode - Language code for error messages (defaults to "en")
* @param finished - Whether the response is finished (defaults to true for management APIs)
* @param questions - Survey questions (legacy format, used as fallback if blocks are empty)
* @returns Validation error map keyed by element ID, or null if validation passes
*/
export const validateResponseData = (
blocks: TSurveyBlock[] | undefined | null,
responseData: TResponseData,
languageCode: string = "en",
finished: boolean = true,
questions?: TSurveyQuestion[] | undefined | null
): TValidationErrorMap | null => {
// Use blocks if available, otherwise transform questions to blocks
let blocksToUse: TSurveyBlock[] = [];
if (blocks && blocks.length > 0) {
blocksToUse = blocks;
} else if (questions && questions.length > 0) {
// Transform legacy questions format to blocks for validation
blocksToUse = transformQuestionsToBlocks(questions, []);
} else {
// No blocks or questions to validate against
return null;
}
// Extract elements from blocks
const allElements = getElementsFromBlocks(blocksToUse);
// If response is not finished, only validate elements that are present in the response data
// This prevents "required" errors for fields the user hasn't reached yet
const elementsToValidate = finished
? allElements
: allElements.filter((element) => Object.keys(responseData).includes(element.id));
// Validate selected elements
const errorMap = validateBlockResponses(elementsToValidate, responseData, languageCode);
// Return null if no errors (validation passed), otherwise return error map
return Object.keys(errorMap).length === 0 ? null : errorMap;
};
/**
* Converts validation error map to V2 API error response format
*
* @param errorMap - Validation error map from validateResponseData
* @returns V2 API error response details
*/
export const formatValidationErrorsForV2Api = (errorMap: TValidationErrorMap) => {
const details: ApiErrorDetails = [];
for (const [elementId, errors] of Object.entries(errorMap)) {
// Include all error messages for each element
for (const error of errors) {
details.push({
field: `response.data.${elementId}`,
issue: error.message,
meta: {
elementId,
ruleId: error.ruleId,
ruleType: error.ruleType,
},
});
}
}
return details;
};
/**
* Converts validation error map to V1 API error response format
*
* @param errorMap - Validation error map from validateResponseData
* @returns V1 API error details as Record<string, string>
*/
export const formatValidationErrorsForV1Api = (errorMap: TValidationErrorMap): Record<string, string> => {
const details: Record<string, string> = {};
for (const [elementId, errors] of Object.entries(errorMap)) {
// Combine all error messages for each element
const errorMessages = errors.map((error) => error.message).join("; ");
details[`response.data.${elementId}`] = errorMessages;
}
return details;
};
@@ -1,6 +1,5 @@
import { z } from "zod";
import { sendToPipeline } from "@/app/lib/pipelines";
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { responses } from "@/modules/api/v2/lib/response";
@@ -193,26 +192,6 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
});
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
questionsResponse.data.blocks,
body.data,
body.language ?? "en",
body.finished,
questionsResponse.data.questions
);
if (validationErrors) {
return handleApiError(
request,
{
type: "bad_request",
details: formatValidationErrorsForV2Api(validationErrors),
},
auditLog
);
}
const response = await updateResponseWithQuotaEvaluation(params.responseId, body);
if (!response.ok) {
@@ -1,7 +1,6 @@
import { Response } from "@prisma/client";
import { NextRequest } from "next/server";
import { sendToPipeline } from "@/app/lib/pipelines";
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { responses } from "@/modules/api/v2/lib/response";
@@ -129,26 +128,6 @@ export const POST = async (request: Request) =>
});
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
surveyQuestions.data.blocks,
body.data,
body.language ?? "en",
body.finished,
surveyQuestions.data.questions
);
if (validationErrors) {
return handleApiError(
request,
{
type: "bad_request",
details: formatValidationErrorsForV2Api(validationErrors),
},
auditLog
);
}
const createResponseResult = await createResponseWithQuotaEvaluation(environmentId, body);
if (!createResponseResult.ok) {
return handleApiError(request, createResponseResult.error, auditLog);
@@ -19,6 +19,7 @@ import { TwoFactorBackup } from "@/modules/ee/two-factor-auth/components/two-fac
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { PasswordInput } from "@/modules/ui/components/password-input";
import { safeFormRequestSubmit } from "@/modules/ui/lib/utils";
const ZLoginForm = z.object({
email: z.string().email(),
@@ -236,7 +237,7 @@ export const LoginForm = ({
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => emailRef.current?.focus(), 100);
} else if (formRef.current) {
formRef.current.requestSubmit();
safeFormRequestSubmit(formRef.current);
}
}}
className="relative w-full justify-center"
@@ -73,12 +73,7 @@ describe("rateLimitConfigs", () => {
test("should have all action configurations", () => {
const actionConfigs = Object.keys(rateLimitConfigs.actions);
expect(actionConfigs).toEqual([
"emailUpdate",
"surveyFollowUp",
"sendLinkSurveyEmail",
"licenseRecheck",
]);
expect(actionConfigs).toEqual(["emailUpdate", "surveyFollowUp", "sendLinkSurveyEmail"]);
});
});
@@ -28,7 +28,6 @@ export const rateLimitConfigs = {
allowedPerInterval: 10,
namespace: "action:send-link-survey-email",
}, // 10 per hour
licenseRecheck: { interval: 60, allowedPerInterval: 5, namespace: "action:license-recheck" }, // 5 per minute
},
storage: {
@@ -109,13 +109,7 @@ export function SegmentSettings({
const handleDeleteSegment = async () => {
try {
setIsDeletingSegment(true);
const result = await deleteSegmentAction({ segmentId: segment.id });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
setIsDeletingSegment(false);
return;
}
await deleteSegmentAction({ segmentId: segment.id });
setIsDeletingSegment(false);
toast.success(t("environments.segments.segment_deleted_successfully"));
@@ -17,7 +17,6 @@ import type {
import type { TSurvey } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import {
cloneSegmentAction,
createSegmentAction,
@@ -136,11 +135,7 @@ export function TargetingCard({
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
try {
if (!segment) throw new Error(t("environments.segments.invalid_segment"));
const result = await updateSegmentAction({ segmentId: segment.id, environmentId, data });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
await updateSegmentAction({ segmentId: segment.id, environmentId, data });
toast.success(t("environments.segments.segment_saved_successfully"));
setIsSegmentEditorOpen(false);
@@ -1,100 +0,0 @@
"use server";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import {
AuthenticationError,
OperationNotAllowedError,
ResourceNotFoundError,
} from "@formbricks/types/errors";
import { cache } from "@/lib/cache";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
import {
FAILED_FETCH_TTL_MS,
FETCH_LICENSE_TTL_MS,
LicenseApiError,
clearLicenseCache,
computeFreshLicenseState,
fetchLicenseFresh,
getCacheKeys,
} from "./lib/license";
const ZRecheckLicenseAction = z.object({
environmentId: ZId,
});
export type TRecheckLicenseAction = z.infer<typeof ZRecheckLicenseAction>;
export const recheckLicenseAction = authenticatedActionClient
.schema(ZRecheckLicenseAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: TRecheckLicenseAction;
}) => {
// Rate limit: 5 rechecks per minute per user
await applyRateLimit(rateLimitConfigs.actions.licenseRecheck, ctx.user.id);
// Only allow on self-hosted instances
if (IS_FORMBRICKS_CLOUD) {
throw new OperationNotAllowedError("License recheck is only available on self-hosted instances");
}
// Get organization from environment
const organization = await getOrganizationByEnvironmentId(parsedInput.environmentId);
if (!organization) {
throw new ResourceNotFoundError("Organization", null);
}
// Check user is owner or manager (not member)
const currentUserMembership = await getMembershipByUserIdOrganizationId(ctx.user.id, organization.id);
if (!currentUserMembership) {
throw new AuthenticationError("User not a member of this organization");
}
if (currentUserMembership.role === "member") {
throw new OperationNotAllowedError("Only owners and managers can recheck license");
}
// Clear main license cache (preserves previous result cache for grace period)
// This prevents instant downgrade if the license server is temporarily unreachable
await clearLicenseCache();
const cacheKeys = getCacheKeys();
let freshLicense: Awaited<ReturnType<typeof fetchLicenseFresh>>;
try {
freshLicense = await fetchLicenseFresh();
} catch (error) {
// 400 = invalid license key — return directly so the UI shows the correct message
if (error instanceof LicenseApiError && error.status === 400) {
return { active: false, status: "invalid_license" as const };
}
throw error;
}
// Cache the fresh result (or null if failed) so getEnterpriseLicense can use it.
// Wrapped in { value: ... } so fetchLicense can distinguish cache miss from cached null.
if (freshLicense) {
await cache.set(cacheKeys.FETCH_LICENSE_CACHE_KEY, { value: freshLicense }, FETCH_LICENSE_TTL_MS);
} else {
await cache.set(cacheKeys.FETCH_LICENSE_CACHE_KEY, { value: null }, FAILED_FETCH_TTL_MS);
}
const licenseState = await computeFreshLicenseState(freshLicense);
return {
active: licenseState.active,
status: licenseState.status,
};
}
);
File diff suppressed because it is too large Load Diff
+83 -378
View File
@@ -14,14 +14,12 @@ import { getInstanceId } from "@/lib/instance";
import {
TEnterpriseLicenseDetails,
TEnterpriseLicenseFeatures,
TEnterpriseLicenseStatusReturn,
} from "@/modules/ee/license-check/types/enterprise-license";
// Configuration
const CONFIG = {
CACHE: {
FETCH_LICENSE_TTL_MS: 24 * 60 * 60 * 1000, // 24 hours
FAILED_FETCH_TTL_MS: 10 * 60 * 1000, // 10 minutes for failed/null results
PREVIOUS_RESULT_TTL_MS: 4 * 24 * 60 * 60 * 1000, // 4 days
GRACE_PERIOD_MS: 3 * 24 * 60 * 60 * 1000, // 3 days
MAX_RETRIES: 3,
@@ -32,42 +30,20 @@ const CONFIG = {
env.ENVIRONMENT === "staging"
? "https://staging.ee.formbricks.com/api/licenses/check"
: "https://ee.formbricks.com/api/licenses/check",
// ENDPOINT: "https://localhost:8080/api/licenses/check",
TIMEOUT_MS: 5000,
},
} as const;
export const GRACE_PERIOD_MS = CONFIG.CACHE.GRACE_PERIOD_MS;
/** TTL in ms for successful license fetch results (24h). Re-export for use in actions. */
export const FETCH_LICENSE_TTL_MS = CONFIG.CACHE.FETCH_LICENSE_TTL_MS;
/** TTL in ms for failed license fetch results (10 min). Re-export for use in actions. */
export const FAILED_FETCH_TTL_MS = CONFIG.CACHE.FAILED_FETCH_TTL_MS;
// Types
type FallbackLevel = "live" | "cached" | "grace" | "default";
type TEnterpriseLicenseResult = {
active: boolean;
features: TEnterpriseLicenseFeatures | null;
lastChecked: Date;
isPendingDowngrade: boolean;
fallbackLevel: FallbackLevel;
status: TEnterpriseLicenseStatusReturn;
};
type TPreviousResult = {
active: boolean;
lastChecked: Date;
features: TEnterpriseLicenseFeatures | null;
};
// Wrapper type for cached license fetch results.
// Storing { value: <result> } instead of <result> directly lets us distinguish
// "key not in cache" (get returns null) from "key exists with a null value"
// ({ value: null }) in a single cache.get call, eliminating the TOCTOU race
// that existed between separate get + exists calls.
type TCachedFetchResult = { value: TEnterpriseLicenseDetails | null };
// Validation schemas
const LicenseFeaturesSchema = z.object({
isMultiOrgEnabled: z.boolean(),
@@ -102,7 +78,7 @@ class LicenseError extends Error {
}
}
export class LicenseApiError extends LicenseError {
class LicenseApiError extends LicenseError {
constructor(
message: string,
public readonly status: number
@@ -114,7 +90,7 @@ export class LicenseApiError extends LicenseError {
// Cache keys using enterprise-grade hierarchical patterns
const getCacheIdentifier = () => {
if (globalThis.window !== undefined) {
if (typeof window !== "undefined") {
return "browser"; // Browser environment
}
if (!env.ENTERPRISE_LICENSE_KEY) {
@@ -123,16 +99,11 @@ const getCacheIdentifier = () => {
return hashString(env.ENTERPRISE_LICENSE_KEY); // Valid license key
};
const LICENSE_FETCH_LOCK_TTL_MS = 90 * 1000; // 90s lock so only one process fetches when cache is cold
const LICENSE_FETCH_POLL_MS = 2 * 1000; // Wait up to 2s for another process to populate cache
const LICENSE_FETCH_POLL_INTERVAL_MS = 400;
export const getCacheKeys = () => {
const identifier = getCacheIdentifier();
return {
FETCH_LICENSE_CACHE_KEY: createCacheKey.license.status(identifier),
PREVIOUS_RESULT_CACHE_KEY: createCacheKey.license.previous_result(identifier),
FETCH_LOCK_CACHE_KEY: createCacheKey.license.fetch_lock(identifier),
};
};
@@ -171,50 +142,36 @@ const validateConfig = () => {
};
// Cache functions with async pattern
let getPreviousResultPromise: Promise<TPreviousResult> | null = null;
const getPreviousResult = async (): Promise<TPreviousResult> => {
if (getPreviousResultPromise) return getPreviousResultPromise;
getPreviousResultPromise = (async () => {
if (globalThis.window !== undefined) {
return {
active: false,
lastChecked: new Date(0),
features: DEFAULT_FEATURES,
};
}
try {
const result = await cache.get<TPreviousResult>(getCacheKeys().PREVIOUS_RESULT_CACHE_KEY);
if (result.ok && result.data) {
return {
...result.data,
lastChecked: new Date(result.data.lastChecked),
};
}
} catch (error) {
logger.error({ error }, "Failed to get previous result from cache");
}
if (typeof window !== "undefined") {
return {
active: false,
lastChecked: new Date(0),
features: DEFAULT_FEATURES,
};
})();
}
getPreviousResultPromise
.finally(() => {
getPreviousResultPromise = null;
})
.catch(() => {});
try {
const result = await cache.get<TPreviousResult>(getCacheKeys().PREVIOUS_RESULT_CACHE_KEY);
if (result.ok && result.data) {
return {
...result.data,
lastChecked: new Date(result.data.lastChecked),
};
}
} catch (error) {
logger.error({ error }, "Failed to get previous result from cache");
}
return getPreviousResultPromise;
return {
active: false,
lastChecked: new Date(0),
features: DEFAULT_FEATURES,
};
};
const setPreviousResult = async (previousResult: TPreviousResult) => {
if (globalThis.window !== undefined) return;
if (typeof window !== "undefined") return;
try {
const result = await cache.set(
@@ -264,21 +221,12 @@ const validateLicenseDetails = (data: unknown): TEnterpriseLicenseDetails => {
};
// Fallback functions
let memoryCache: {
data: TEnterpriseLicenseResult;
timestamp: number;
} | null = null;
const MEMORY_CACHE_TTL_MS = 60 * 1000; // 1 minute memory cache to avoid stampedes and reduce load when Redis is slow
let getEnterpriseLicensePromise: Promise<TEnterpriseLicenseResult> | null = null;
const getFallbackLevel = (
liveLicense: TEnterpriseLicenseDetails | null,
previousResult: TPreviousResult,
currentTime: Date
): FallbackLevel => {
if (liveLicense?.status === "active") return "live";
if (liveLicense) return "live";
if (previousResult.active) {
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
return elapsedTime < CONFIG.CACHE.GRACE_PERIOD_MS ? "grace" : "default";
@@ -286,7 +234,7 @@ const getFallbackLevel = (
return "default";
};
const handleInitialFailure = async (currentTime: Date): Promise<TEnterpriseLicenseResult> => {
const handleInitialFailure = async (currentTime: Date) => {
const initialFailResult: TPreviousResult = {
active: false,
features: DEFAULT_FEATURES,
@@ -299,26 +247,10 @@ const handleInitialFailure = async (currentTime: Date): Promise<TEnterpriseLicen
lastChecked: currentTime,
isPendingDowngrade: false,
fallbackLevel: "default" as const,
status: "unreachable" as const,
};
};
/**
* Try to read cached license from Redis. Returns undefined on miss or error.
*/
const getCachedLicense = async (): Promise<TEnterpriseLicenseDetails | null | undefined> => {
const keys = getCacheKeys();
const result = await cache.get<TCachedFetchResult>(keys.FETCH_LICENSE_CACHE_KEY);
if (!result.ok) return undefined;
if (result.data !== null && result.data !== undefined && "value" in result.data) {
return result.data.value;
}
return undefined;
};
// API functions
let fetchLicensePromise: Promise<TEnterpriseLicenseDetails | null> | null = null;
const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpriseLicenseDetails | null> => {
if (!env.ENTERPRISE_LICENSE_KEY) return null;
@@ -334,7 +266,6 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
// first millisecond of next year => current year is fully included
const startOfNextYear = new Date(now.getFullYear() + 1, 0, 1);
const startTime = Date.now();
const [instanceId, responseCount] = await Promise.all([
// Skip instance ID during E2E tests to avoid license key conflicts
// as the instance ID changes with each test run
@@ -348,11 +279,6 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
},
}),
]);
const duration = Date.now() - startTime;
if (duration > 1000) {
logger.warn({ duration, responseCount }, "Slow license check prerequisite data fetching (DB count)");
}
// No organization exists, cannot perform license check
// (skip this check during E2E tests as we intentionally use null)
@@ -385,19 +311,7 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
if (res.ok) {
const responseJson = (await res.json()) as { data: unknown };
const licenseDetails = validateLicenseDetails(responseJson.data);
logger.debug(
{
status: licenseDetails.status,
instanceId: instanceId ?? "not-set",
responseCount,
timestamp: new Date().toISOString(),
},
"License check API response received"
);
return licenseDetails;
return validateLicenseDetails(responseJson.data);
}
const error = new LicenseApiError(`License check API responded with status: ${res.status}`, res.status);
@@ -409,23 +323,12 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
return fetchLicenseFromServerInternal(retryCount + 1);
}
// 400 = invalid license key — propagate so callers can distinguish from unreachable
if (res.status === 400) {
throw error;
}
return null;
} catch (error) {
if (error instanceof LicenseApiError) {
throw error;
}
logger.error(error, "Error while fetching license from server");
logger.warn(
{
timestamp: new Date().toISOString(),
},
"License server fetch returned null - server may be unreachable"
);
return null;
}
};
@@ -439,227 +342,80 @@ export const fetchLicense = async (): Promise<TEnterpriseLicenseDetails | null>
return null;
}
if (fetchLicensePromise) {
return fetchLicensePromise;
}
fetchLicensePromise = (async () => {
const keys = getCacheKeys();
const cached = await getCachedLicense();
if (cached !== undefined) return cached;
const lockResult = await cache.tryLock(keys.FETCH_LOCK_CACHE_KEY, "1", LICENSE_FETCH_LOCK_TTL_MS);
const acquired = lockResult.ok && lockResult.data === true;
const redisError = !lockResult.ok;
if (acquired) {
try {
const fresh = await fetchLicenseFromServerInternal();
const ttl = fresh ? CONFIG.CACHE.FETCH_LICENSE_TTL_MS : CONFIG.CACHE.FAILED_FETCH_TTL_MS;
if (!fresh) {
logger.warn(
{
ttlMinutes: Math.floor(ttl / 60000),
timestamp: new Date().toISOString(),
},
"License fetch failed, caching null result with short TTL for faster retry"
);
}
await cache.set(keys.FETCH_LICENSE_CACHE_KEY, { value: fresh }, ttl);
return fresh;
} finally {
// Lock expires automatically; no need to release
}
}
// If Redis itself is down, skip the polling loop (it would just fail repeatedly)
// and go directly to fetching from the server.
if (redisError) {
logger.warn("Redis unavailable during license fetch lock; skipping poll and fetching directly");
return await cache.withCache(
async () => {
return await fetchLicenseFromServerInternal();
}
// Another process holds the lock — poll until the cache is populated or timeout
const deadline = Date.now() + LICENSE_FETCH_POLL_MS;
while (Date.now() < deadline) {
await sleep(LICENSE_FETCH_POLL_INTERVAL_MS);
const value = await getCachedLicense();
if (value !== undefined) return value;
}
logger.warn(
{ pollMs: LICENSE_FETCH_POLL_MS },
"License cache not populated by holder within poll window; fetching in this process"
);
const fallback = await fetchLicenseFromServerInternal();
const fallbackTtl = fallback ? CONFIG.CACHE.FETCH_LICENSE_TTL_MS : CONFIG.CACHE.FAILED_FETCH_TTL_MS;
await cache.set(keys.FETCH_LICENSE_CACHE_KEY, { value: fallback }, fallbackTtl);
return fallback;
})();
fetchLicensePromise
.finally(() => {
fetchLicensePromise = null;
})
.catch(() => {});
return fetchLicensePromise;
},
getCacheKeys().FETCH_LICENSE_CACHE_KEY,
CONFIG.CACHE.FETCH_LICENSE_TTL_MS
);
};
/**
* Core license state evaluation logic.
* Accepts pre-fetched license details and applies fallback / grace-period rules.
* Sets the in-process memoryCache as a side effect so subsequent requests benefit.
*/
const computeLicenseState = async (
liveLicenseDetails: TEnterpriseLicenseDetails | null
): Promise<TEnterpriseLicenseResult> => {
validateConfig();
export const getEnterpriseLicense = reactCache(
async (): Promise<{
active: boolean;
features: TEnterpriseLicenseFeatures | null;
lastChecked: Date;
isPendingDowngrade: boolean;
fallbackLevel: FallbackLevel;
}> => {
validateConfig();
if (!env.ENTERPRISE_LICENSE_KEY || env.ENTERPRISE_LICENSE_KEY.length === 0) {
return {
active: false,
features: null,
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "default" as const,
status: "no-license" as const,
};
}
const currentTime = new Date();
const previousResult = await getPreviousResult();
const fallbackLevel = getFallbackLevel(liveLicenseDetails, previousResult, currentTime);
trackFallbackUsage(fallbackLevel);
let currentLicenseState: TPreviousResult | undefined;
switch (fallbackLevel) {
case "live": {
if (!liveLicenseDetails) throw new Error("Invalid state: live license expected");
currentLicenseState = {
active: liveLicenseDetails.status === "active",
features: liveLicenseDetails.features,
lastChecked: currentTime,
};
// Only update previous result if it's actually different or if it's old (1 hour)
// This prevents hammering Redis on every request when the license is active
if (
!previousResult.active ||
previousResult.active !== currentLicenseState.active ||
currentTime.getTime() - previousResult.lastChecked.getTime() > 60 * 60 * 1000
) {
await setPreviousResult(currentLicenseState);
}
const liveResult: TEnterpriseLicenseResult = {
active: currentLicenseState.active,
features: currentLicenseState.features,
lastChecked: currentTime,
if (!env.ENTERPRISE_LICENSE_KEY || env.ENTERPRISE_LICENSE_KEY.length === 0) {
return {
active: false,
features: null,
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "live" as const,
status: liveLicenseDetails.status,
fallbackLevel: "default" as const,
};
memoryCache = { data: liveResult, timestamp: Date.now() };
return liveResult;
}
case "grace": {
if (!validateFallback(previousResult)) {
return await handleInitialFailure(currentTime);
}
logger.warn(
{
lastChecked: previousResult.lastChecked.toISOString(),
gracePeriodEnds: new Date(
previousResult.lastChecked.getTime() + CONFIG.CACHE.GRACE_PERIOD_MS
).toISOString(),
timestamp: new Date().toISOString(),
},
"License server unreachable, using grace period. Will retry in ~10 minutes."
);
const graceResult: TEnterpriseLicenseResult = {
active: previousResult.active,
features: previousResult.features,
lastChecked: previousResult.lastChecked,
isPendingDowngrade: true,
fallbackLevel: "grace" as const,
status: (liveLicenseDetails?.status as TEnterpriseLicenseStatusReturn) ?? "unreachable",
};
memoryCache = { data: graceResult, timestamp: Date.now() };
return graceResult;
}
const currentTime = new Date();
const liveLicenseDetails = await fetchLicense();
const previousResult = await getPreviousResult();
const fallbackLevel = getFallbackLevel(liveLicenseDetails, previousResult, currentTime);
case "default": {
if (liveLicenseDetails?.status === "expired") {
const expiredResult: TEnterpriseLicenseResult = {
active: false,
features: DEFAULT_FEATURES,
trackFallbackUsage(fallbackLevel);
let currentLicenseState: TPreviousResult | undefined;
switch (fallbackLevel) {
case "live":
if (!liveLicenseDetails) throw new Error("Invalid state: live license expected");
currentLicenseState = {
active: liveLicenseDetails.status === "active",
features: liveLicenseDetails.features,
lastChecked: currentTime,
};
await setPreviousResult(currentLicenseState);
return {
active: currentLicenseState.active,
features: currentLicenseState.features,
lastChecked: currentTime,
isPendingDowngrade: false,
fallbackLevel: "default" as const,
status: "expired" as const,
fallbackLevel: "live" as const,
};
memoryCache = { data: expiredResult, timestamp: Date.now() };
return expiredResult;
}
const failResult = await handleInitialFailure(currentTime);
memoryCache = { data: failResult, timestamp: Date.now() };
return failResult;
}
}
const finalFailResult = await handleInitialFailure(currentTime);
memoryCache = { data: finalFailResult, timestamp: Date.now() };
return finalFailResult;
};
export const getEnterpriseLicense = reactCache(async (): Promise<TEnterpriseLicenseResult> => {
if (
process.env.NODE_ENV !== "test" &&
memoryCache &&
Date.now() - memoryCache.timestamp < MEMORY_CACHE_TTL_MS
) {
return memoryCache.data;
}
if (getEnterpriseLicensePromise) return getEnterpriseLicensePromise;
getEnterpriseLicensePromise = (async () => {
let liveLicenseDetails: TEnterpriseLicenseDetails | null = null;
try {
liveLicenseDetails = await fetchLicense();
} catch (error) {
if (error instanceof LicenseApiError && error.status === 400) {
const invalidResult: TEnterpriseLicenseResult = {
active: false,
features: DEFAULT_FEATURES,
lastChecked: new Date(),
isPendingDowngrade: false,
fallbackLevel: "default" as const,
status: "invalid_license" as const,
case "grace":
if (!validateFallback(previousResult)) {
return handleInitialFailure(currentTime);
}
return {
active: previousResult.active,
features: previousResult.features,
lastChecked: previousResult.lastChecked,
isPendingDowngrade: true,
fallbackLevel: "grace" as const,
};
memoryCache = { data: invalidResult, timestamp: Date.now() };
return invalidResult;
}
// Other errors: liveLicenseDetails stays null (treated as unreachable)
case "default":
return handleInitialFailure(currentTime);
}
return computeLicenseState(liveLicenseDetails);
})();
getEnterpriseLicensePromise
.finally(() => {
getEnterpriseLicensePromise = null;
})
.catch(() => {});
return getEnterpriseLicensePromise;
});
return handleInitialFailure(currentTime);
}
);
export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures | null> => {
try {
@@ -671,55 +427,4 @@ export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures |
}
};
/**
* Clear license fetch cache (but preserve previous result cache for grace period)
* Used by the recheck license action to force a fresh fetch without losing grace period
*/
export const clearLicenseCache = async (): Promise<void> => {
memoryCache = null;
const cacheKeys = getCacheKeys();
// Only clear the main fetch cache, NOT the previous result cache
// This preserves the grace period fallback if the server is unreachable
const delResult = await cache.del([cacheKeys.FETCH_LICENSE_CACHE_KEY]);
if (!delResult.ok) {
logger.warn({ error: delResult.error }, "Failed to delete license cache");
}
};
/**
* Fetch license directly from server without using cache.
* Used by the recheck license action for a fresh check.
* Concurrent callers share a single in-flight request to avoid
* hammering the license server (e.g. multiple managers rechecking).
*/
let fetchLicenseFreshPromise: Promise<TEnterpriseLicenseDetails | null> | null = null;
export const fetchLicenseFresh = async (): Promise<TEnterpriseLicenseDetails | null> => {
if (fetchLicenseFreshPromise) return fetchLicenseFreshPromise;
fetchLicenseFreshPromise = fetchLicenseFromServerInternal();
fetchLicenseFreshPromise
.finally(() => {
fetchLicenseFreshPromise = null;
})
.catch(() => {});
return fetchLicenseFreshPromise;
};
/**
* Compute license state from pre-fetched license data, bypassing React cache
* and the in-process memory cache. Used by the recheck action to guarantee
* fresh evaluation after clearing caches and fetching new data.
* Refreshes the in-process memory cache as a side effect so subsequent
* requests benefit from the fresh result.
*/
export const computeFreshLicenseState = async (
freshLicense: TEnterpriseLicenseDetails | null
): Promise<TEnterpriseLicenseResult> => {
memoryCache = null;
return computeLicenseState(freshLicense);
};
// All permission checking functions and their helpers have been moved to utils.ts
@@ -8,7 +8,6 @@ import {
getBiggerUploadFileSizePermission,
getIsContactsEnabled,
getIsMultiOrgEnabled,
getIsQuotasEnabled,
getIsSamlSsoEnabled,
getIsSpamProtectionEnabled,
getIsSsoEnabled,
@@ -49,7 +48,6 @@ const defaultFeatures: TEnterpriseLicenseFeatures = {
auditLogs: false,
multiLanguageSurveys: false,
accessControl: false,
quotas: false,
};
const defaultLicense = {
@@ -186,10 +184,10 @@ describe("License Utils", () => {
expect(result).toBe(true);
});
test("should return false if license active but accessControl feature disabled (self-hosted)", async () => {
test("should return true if license active but accessControl feature disabled because of fallback", async () => {
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
const result = await getAccessControlPermission(mockOrganization.billing.plan);
expect(result).toBe(false);
expect(result).toBe(true);
});
test("should return false if license is inactive", async () => {
@@ -275,10 +273,10 @@ describe("License Utils", () => {
expect(result).toBe(true);
});
test("should return false if license active but multiLanguageSurveys feature disabled (self-hosted)", async () => {
test("should return true if license active but multiLanguageSurveys feature disabled because of fallback", async () => {
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
expect(result).toBe(false);
expect(result).toBe(true);
});
test("should return false if license is inactive", async () => {
@@ -291,54 +289,6 @@ describe("License Utils", () => {
});
});
describe("getIsQuotasEnabled", () => {
test("should return true if license active and quotas feature enabled (self-hosted)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
features: { ...defaultFeatures, quotas: true },
});
const result = await getIsQuotasEnabled(mockOrganization.billing.plan);
expect(result).toBe(true);
});
test("should return true if license active, quotas enabled and plan is CUSTOM (cloud)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
features: { ...defaultFeatures, quotas: true },
});
const result = await getIsQuotasEnabled(constants.PROJECT_FEATURE_KEYS.CUSTOM);
expect(result).toBe(true);
});
test("should return false if license active, quotas enabled but plan is not CUSTOM (cloud)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
features: { ...defaultFeatures, quotas: true },
});
const result = await getIsQuotasEnabled(constants.PROJECT_FEATURE_KEYS.STARTUP);
expect(result).toBe(false);
});
test("should return false if license active but quotas feature disabled (self-hosted)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
const result = await getIsQuotasEnabled(mockOrganization.billing.plan);
expect(result).toBe(false);
});
test("should return false if license is inactive", async () => {
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
active: false,
});
const result = await getIsQuotasEnabled(mockOrganization.billing.plan);
expect(result).toBe(false);
});
});
describe("getIsMultiOrgEnabled", () => {
test("should return true if feature flag isMultiOrgEnabled is true", async () => {
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
+43 -40
View File
@@ -10,8 +10,6 @@ import { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/types/ent
import { getEnterpriseLicense, getLicenseFeatures } from "./license";
// Helper function for feature permissions (e.g., removeBranding, whitelabel)
// On Cloud: requires active license and non-FREE plan
// On Self-hosted: requires active license and feature enabled
const getFeaturePermission = async (
billingPlan: Organization["billing"]["plan"],
featureKey: keyof Pick<TEnterpriseLicenseFeatures, "removeBranding" | "whitelabel">
@@ -25,41 +23,6 @@ const getFeaturePermission = async (
}
};
// Helper function for enterprise features that require CUSTOM plan on Cloud
// On Cloud: requires active license AND feature enabled in license AND CUSTOM billing plan
// On Self-hosted: requires active license AND feature enabled in license
const getCustomPlanFeaturePermission = async (
billingPlan: Organization["billing"]["plan"],
featureKey: keyof Pick<TEnterpriseLicenseFeatures, "accessControl" | "multiLanguageSurveys" | "quotas">
): Promise<boolean> => {
const license = await getEnterpriseLicense();
if (!license.active) return false;
const isFeatureEnabled = license.features?.[featureKey] ?? false;
if (!isFeatureEnabled) return false;
if (IS_FORMBRICKS_CLOUD) {
return billingPlan === PROJECT_FEATURE_KEYS.CUSTOM;
}
return true;
};
// Helper function for license-only feature flags (no billing plan check)
// Returns true only if the license is active AND the specific feature is enabled in the license
// Used for features that are controlled purely by the license key, not billing plans
const getSpecificFeatureFlag = async (
featureKey: keyof Pick<
TEnterpriseLicenseFeatures,
"isMultiOrgEnabled" | "contacts" | "twoFactorAuth" | "sso" | "auditLogs"
>
): Promise<boolean> => {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false;
};
export const getRemoveBrandingPermission = async (
billingPlan: Organization["billing"]["plan"]
): Promise<boolean> => {
@@ -82,6 +45,24 @@ export const getBiggerUploadFileSizePermission = async (
return false;
};
const getSpecificFeatureFlag = async (
featureKey: keyof Pick<
TEnterpriseLicenseFeatures,
| "isMultiOrgEnabled"
| "contacts"
| "twoFactorAuth"
| "sso"
| "auditLogs"
| "multiLanguageSurveys"
| "accessControl"
| "quotas"
>
): Promise<boolean> => {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false;
};
export const getIsMultiOrgEnabled = async (): Promise<boolean> => {
return getSpecificFeatureFlag("isMultiOrgEnabled");
};
@@ -99,7 +80,12 @@ export const getIsSsoEnabled = async (): Promise<boolean> => {
};
export const getIsQuotasEnabled = async (billingPlan: Organization["billing"]["plan"]): Promise<boolean> => {
return getCustomPlanFeaturePermission(billingPlan, "quotas");
const isEnabled = await getSpecificFeatureFlag("quotas");
// If the feature is enabled in the license, return true
if (isEnabled) return true;
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
return featureFlagFallback(billingPlan);
};
export const getIsAuditLogsEnabled = async (): Promise<boolean> => {
@@ -132,16 +118,33 @@ export const getIsSpamProtectionEnabled = async (
return license.active && !!license.features?.spamProtection;
};
const featureFlagFallback = async (billingPlan: Organization["billing"]["plan"]): Promise<boolean> => {
const license = await getEnterpriseLicense();
if (IS_FORMBRICKS_CLOUD) return license.active && billingPlan === PROJECT_FEATURE_KEYS.CUSTOM;
else if (!IS_FORMBRICKS_CLOUD) return license.active;
return false;
};
export const getMultiLanguagePermission = async (
billingPlan: Organization["billing"]["plan"]
): Promise<boolean> => {
return getCustomPlanFeaturePermission(billingPlan, "multiLanguageSurveys");
const isEnabled = await getSpecificFeatureFlag("multiLanguageSurveys");
// If the feature is enabled in the license, return true
if (isEnabled) return true;
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
return featureFlagFallback(billingPlan);
};
export const getAccessControlPermission = async (
billingPlan: Organization["billing"]["plan"]
): Promise<boolean> => {
return getCustomPlanFeaturePermission(billingPlan, "accessControl");
const isEnabled = await getSpecificFeatureFlag("accessControl");
// If the feature is enabled in the license, return true
if (isEnabled) return true;
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
return featureFlagFallback(billingPlan);
};
export const getOrganizationProjectsLimit = async (
@@ -29,5 +29,3 @@ export const ZEnterpriseLicenseDetails = z.object({
});
export type TEnterpriseLicenseDetails = z.infer<typeof ZEnterpriseLicenseDetails>;
export type TEnterpriseLicenseStatusReturn = "active" | "expired" | "unreachable" | "invalid_license" | "no-license";
@@ -154,12 +154,7 @@ export function EditLanguage({
const performLanguageDeletion = async (languageId: string) => {
try {
const result = await deleteLanguageAction({ languageId, projectId: project.id });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
setConfirmationModal((prev) => ({ ...prev, isOpen: false }));
return;
}
await deleteLanguageAction({ languageId, projectId: project.id });
setLanguages((prev) => prev.filter((lang) => lang.id !== languageId));
toast.success(t("environments.workspace.languages.language_deleted_successfully"));
// Close the modal after deletion
@@ -192,7 +187,7 @@ export function EditLanguage({
const handleSaveChanges = async () => {
if (!validateLanguages(languages, t)) return;
const results = await Promise.all(
await Promise.all(
languages.map((lang) => {
return lang.id === "new"
? createLanguageAction({
@@ -206,11 +201,6 @@ export function EditLanguage({
});
})
);
const errorResult = results.find((result) => result?.serverError);
if (errorResult) {
toast.error(getFormattedErrorMessage(errorResult));
return;
}
toast.success(t("environments.workspace.languages.languages_updated_successfully"));
router.refresh();
setIsEditing(false);
@@ -249,7 +239,7 @@ export function EditLanguage({
))}
</>
) : (
<p className="text-sm italic text-slate-500">
<p className="text-sm text-slate-500 italic">
{t("environments.workspace.languages.no_language_found")}
</p>
)}
@@ -1,7 +1,7 @@
"use client";
import { useMemo, useTransition } from "react";
import type { Dispatch, SetStateAction } from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { TI18nString } from "@formbricks/types/i18n";
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
@@ -74,8 +74,6 @@ export function LocalizedEditor({
[id, isInvalid, localSurvey.languages, value]
);
const [, startTransition] = useTransition();
return (
<div className="relative w-full">
<Editor
@@ -111,45 +109,44 @@ export function LocalizedEditor({
sanitizedContent = v.replaceAll(/<a[^>]*>(.*?)<\/a>/gi, "$1");
}
// Check if the elements still exists before updating
const currentElement = elements[elementIdx];
startTransition(() => {
// if this is a card, we wanna check if the card exists in the localSurvey
if (isCard) {
const isWelcomeCard = elementIdx === -1;
const isEndingCard = elementIdx >= elements.length;
// if this is a card, we wanna check if the card exists in the localSurvey
if (isCard) {
const isWelcomeCard = elementIdx === -1;
const isEndingCard = elementIdx >= elements.length;
// For ending cards, check if the field exists before updating
if (isEndingCard) {
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
// If the field doesn't exist on the ending card, don't create it
if (!ending || ending[id] === undefined) {
return;
}
}
// For welcome cards, check if it exists
if (isWelcomeCard && !localSurvey.welcomeCard) {
// For ending cards, check if the field exists before updating
if (isEndingCard) {
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
// If the field doesn't exist on the ending card, don't create it
if (!ending || ending[id] === undefined) {
return;
}
}
const translatedContent = {
...value,
[selectedLanguageCode]: sanitizedContent,
};
updateElement({ [id]: translatedContent });
// For welcome cards, check if it exists
if (isWelcomeCard && !localSurvey.welcomeCard) {
return;
}
// Check if the field exists on the element (not just if it's not undefined)
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
const translatedContent = {
...value,
[selectedLanguageCode]: sanitizedContent,
};
updateElement(elementIdx, { [id]: translatedContent });
}
});
const translatedContent = {
...value,
[selectedLanguageCode]: sanitizedContent,
};
updateElement({ [id]: translatedContent });
return;
}
// Check if the field exists on the element (not just if it's not undefined)
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
const translatedContent = {
...value,
[selectedLanguageCode]: sanitizedContent,
};
updateElement(elementIdx, { [id]: translatedContent });
}
}}
localSurvey={localSurvey}
elementId={elementId}
@@ -62,7 +62,7 @@ export const QuotaConditionBuilder = ({
);
return (
<div className="max-h-[150px] space-y-4 overflow-y-auto">
<div className="space-y-4">
<ConditionsEditor
conditions={genericConditions}
config={config}
@@ -438,45 +438,5 @@ describe("Quota Evaluation Service", () => {
"Error evaluating quotas for response"
);
});
test("should use 'default' language when provided language matches default language", async () => {
const surveyWithLanguages = {
...mockSurvey,
languages: [
{ default: true, language: { code: "en", flag: "🇺🇸" } },
{ default: false, language: { code: "fr", flag: "🇫🇷" } },
],
};
const input: QuotaEvaluationInput = {
surveyId: mockSurveyId,
responseId: mockResponseId,
data: mockResponseData,
variables: mockVariablesData,
language: "en",
responseFinished: true,
tx: mockTx,
};
const evaluateResult = {
passedQuotas: [mockQuota],
failedQuotas: [],
};
vi.mocked(getQuotas).mockResolvedValue([mockQuota]);
vi.mocked(getSurvey).mockResolvedValue(surveyWithLanguages as unknown as TSurvey);
vi.mocked(evaluateQuotas).mockReturnValue(evaluateResult);
vi.mocked(handleQuotas).mockResolvedValue(null);
await evaluateResponseQuotas(input);
expect(evaluateQuotas).toHaveBeenCalledWith(
surveyWithLanguages,
mockResponseData,
mockVariablesData,
[mockQuota],
"default"
);
});
});
});
@@ -51,8 +51,8 @@ export const evaluateResponseQuotas = async (input: QuotaEvaluationInput): Promi
if (!survey) {
return { shouldEndSurvey: false };
}
const isDefaultLanguage = survey.languages.find((lang) => lang.default)?.language.code === language;
const result = evaluateQuotas(survey, data, variables, quotas, isDefaultLanguage ? "default" : language);
const result = evaluateQuotas(survey, data, variables, quotas, language);
const quotaFull = await handleQuotas(surveyId, responseId, result, responseFinished, prismaClient);
@@ -4,7 +4,6 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { deleteTeamAction } from "@/modules/ee/teams/team-list/actions";
import { TTeam } from "@/modules/ee/teams/team-list/types/team";
import { Button } from "@/modules/ui/components/button";
@@ -28,12 +27,6 @@ export const DeleteTeam = ({ teamId, onDelete, isOwnerOrManager }: DeleteTeamPro
setIsDeleting(true);
const deleteTeamActionResponse = await deleteTeamAction({ teamId });
if (deleteTeamActionResponse?.serverError) {
toast.error(getFormattedErrorMessage(deleteTeamActionResponse));
setIsDeleteDialogOpen(false);
setIsDeleting(false);
return;
}
if (deleteTeamActionResponse?.data) {
toast.success(t("environments.settings.teams.team_deleted_successfully"));
onDelete?.();
@@ -1,137 +0,0 @@
"use client";
import { XIcon } from "lucide-react";
import { Control } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
TOrganizationMember,
TTeamRole,
TTeamSettingsFormSchema,
ZTeamRole,
} from "@/modules/ee/teams/team-list/types/team";
import { Button } from "@/modules/ui/components/button";
import { FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
export interface MemberRowProps {
index: number;
member: { userId: string; role: TTeamRole };
memberOpts: { value: string; label: string }[];
control: Control<TTeamSettingsFormSchema>;
orgMembers: TOrganizationMember[];
watchMembers: { userId: string; role: TTeamRole }[];
initialMemberIds: Set<string>;
isOwnerOrManager: boolean;
isTeamAdminMember: boolean;
isTeamContributorMember: boolean;
currentUserId: string;
onMemberSelectionChange: (index: number, userId: string) => void;
onRemoveMember: (index: number) => void;
memberCount: number;
}
export function MemberRow(props: Readonly<MemberRowProps>) {
const {
index,
member,
memberOpts,
control,
orgMembers,
watchMembers,
initialMemberIds,
isOwnerOrManager,
isTeamAdminMember,
isTeamContributorMember,
currentUserId,
onMemberSelectionChange,
onRemoveMember,
memberCount,
} = props;
const { t } = useTranslation();
const chosenMember = orgMembers.find((m) => m.id === watchMembers[index]?.userId);
const canEditWhenNoMember = isOwnerOrManager || isTeamAdminMember;
const isRoleSelectDisabled =
chosenMember === undefined
? !canEditWhenNoMember
: chosenMember.role === "owner" ||
chosenMember.role === "manager" ||
isTeamContributorMember ||
chosenMember.id === currentUserId;
return (
<div className="flex gap-2.5">
<FormField
control={control}
name={`members.${index}.userId`}
render={({ field, fieldState: { error } }) => {
const isExistingMember = member.userId && initialMemberIds.has(member.userId);
const isSelectDisabled = isExistingMember || (!isOwnerOrManager && !isTeamAdminMember);
return (
<FormItem className="flex-1">
<div className={isSelectDisabled ? "pointer-events-none opacity-50" : undefined}>
<InputCombobox
id={`member-select-${index}`}
options={memberOpts}
value={field.value || null}
onChangeValue={(val) => {
const value = typeof val === "string" ? val : "";
field.onChange(value);
onMemberSelectionChange(index, value);
}}
showSearch
searchPlaceholder={t("common.search")}
comboboxClasses="flex-1 min-w-0 w-full"
emptyDropdownText={t("environments.surveys.edit.no_option_found")}
/>
</div>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</FormItem>
);
}}
/>
<FormField
control={control}
name={`members.${index}.role`}
render={({ field }) => {
const roleOptions = [
{ value: ZTeamRole.enum.admin, label: t("environments.settings.teams.team_admin") },
{
value: ZTeamRole.enum.contributor,
label: t("environments.settings.teams.contributor"),
},
];
return (
<FormItem className="flex-1">
<div className={isRoleSelectDisabled ? "pointer-events-none opacity-50" : undefined}>
<InputCombobox
id={`member-role-select-${index}`}
options={roleOptions}
value={field.value}
onChangeValue={(val) => field.onChange(val)}
showSearch={false}
comboboxClasses="flex-1 min-w-0 w-full"
/>
</div>
</FormItem>
);
}}
/>
{memberCount > 1 && (
<TooltipRenderer tooltipContent={t("common.remove_from_team")}>
<Button
size="icon"
type="button"
variant="destructive"
className="shrink-0"
disabled={!isOwnerOrManager && (!isTeamAdminMember || member.userId === currentUserId)}
onClick={() => onRemoveMember(index)}>
<XIcon className="h-4 w-4" />
</Button>
</TooltipRenderer>
)}
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { PlusIcon, Trash2Icon, XIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useMemo } from "react";
import { FormProvider, SubmitHandler, useForm, useWatch } from "react-hook-form";
@@ -13,8 +13,6 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { ZTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { updateTeamDetailsAction } from "@/modules/ee/teams/team-list/actions";
import { DeleteTeam } from "@/modules/ee/teams/team-list/components/team-settings/delete-team";
import { MemberRow } from "@/modules/ee/teams/team-list/components/team-settings/member-row";
import { WorkspaceRow } from "@/modules/ee/teams/team-list/components/team-settings/workspace-row";
import { TOrganizationProject } from "@/modules/ee/teams/team-list/types/project";
import {
TOrganizationMember,
@@ -38,6 +36,13 @@ import {
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { Input } from "@/modules/ui/components/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { Muted } from "@/modules/ui/components/typography";
@@ -182,16 +187,14 @@ export const TeamSettingsModal = ({
const currentMemberId = watchMembers[index]?.userId;
return orgMembers
.filter((om) => !selectedMemberIds.includes(om?.id) || om?.id === currentMemberId)
.map((om) => ({ label: om?.name ?? "", value: om?.id }))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
.map((om) => ({ label: om?.name, value: om?.id }));
};
const getProjectOptionsForIndex = (index: number) => {
const currentProjectId = watchProjects[index]?.projectId;
return orgProjects
.filter((op) => !selectedProjectIds.includes(op?.id) || op?.id === currentProjectId)
.map((op) => ({ label: op?.name ?? "", value: op?.id }))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
.map((op) => ({ label: op?.name, value: op?.id }));
};
const handleMemberSelectionChange = (index: number, userId: string) => {
@@ -259,25 +262,110 @@ export const TeamSettingsModal = ({
render={({ fieldState: { error } }) => (
<FormItem className="flex-1">
<div className="space-y-2 overflow-y-auto">
{watchMembers.map((member, index) => (
<MemberRow
key={`member-${member.userId}-${index}`}
index={index}
member={member}
memberOpts={getMemberOptionsForIndex(index)}
control={control}
orgMembers={orgMembers}
watchMembers={watchMembers}
initialMemberIds={initialMemberIds}
isOwnerOrManager={isOwnerOrManager}
isTeamAdminMember={isTeamAdminMember}
isTeamContributorMember={isTeamContributorMember}
currentUserId={currentUserId}
onMemberSelectionChange={handleMemberSelectionChange}
onRemoveMember={handleRemoveMember}
memberCount={watchMembers.length}
/>
))}
{watchMembers.map((member, index) => {
const memberOpts = getMemberOptionsForIndex(index);
return (
<div key={`member-${member.userId}-${index}`} className="flex gap-2.5">
<FormField
control={control}
name={`members.${index}.userId`}
render={({ field, fieldState: { error } }) => {
// Disable user select for existing members (can only remove or change role)
const isExistingMember =
member.userId && initialMemberIds.has(member.userId);
const isSelectDisabled =
isExistingMember || (!isOwnerOrManager && !isTeamAdminMember);
return (
<FormItem className="flex-1">
<Select
onValueChange={(val) => {
field.onChange(val);
handleMemberSelectionChange(index, val);
}}
disabled={isSelectDisabled}
value={member.userId}>
<SelectTrigger>
<SelectValue
placeholder={t("environments.settings.teams.select_member")}
/>
</SelectTrigger>
<SelectContent>
{memberOpts.map((option) => (
<SelectItem
key={option.value}
value={option.value}
id={`member-${index}-option`}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{error?.message && (
<FormError className="text-left">{error.message}</FormError>
)}
</FormItem>
);
}}
/>
<FormField
control={control}
name={`members.${index}.role`}
render={({ field }) => (
<FormItem className="flex-1">
<Select
onValueChange={field.onChange}
value={member.role}
disabled={(() => {
const chosenMember = orgMembers.find(
(m) => m.id === watchMembers[index]?.userId
);
if (!chosenMember) return !isOwnerOrManager && !isTeamAdminMember;
return (
chosenMember.role === "owner" ||
chosenMember.role === "manager" ||
isTeamContributorMember ||
chosenMember.id === currentUserId
);
})()}>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ZTeamRole.enum.admin}>
{t("environments.settings.teams.team_admin")}
</SelectItem>
<SelectItem value={ZTeamRole.enum.contributor}>
{t("environments.settings.teams.contributor")}
</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
{/* Delete Button for Member */}
{watchMembers.length > 1 && (
<TooltipRenderer tooltipContent={t("common.remove_from_team")}>
<Button
size="icon"
type="button"
variant="destructive"
className="shrink-0"
disabled={
!isOwnerOrManager &&
(!isTeamAdminMember || member.userId === currentUserId)
}
onClick={() => handleRemoveMember(index)}>
<XIcon className="h-4 w-4" />
</Button>
</TooltipRenderer>
)}
</div>
);
})}
</div>
{error?.root?.message && (
<FormError className="text-left">{error.root.message}</FormError>
@@ -323,19 +411,90 @@ export const TeamSettingsModal = ({
render={({ fieldState: { error } }) => (
<FormItem className="flex-1">
<div className="space-y-2">
{watchProjects.map((project, index) => (
<WorkspaceRow
key={`workspace-${project.projectId}-${index}`}
index={index}
project={project}
projectOpts={getProjectOptionsForIndex(index)}
control={control}
initialProjectIds={initialProjectIds}
isOwnerOrManager={isOwnerOrManager}
onRemoveProject={handleRemoveProject}
projectCount={watchProjects.length}
/>
))}
{watchProjects.map((project, index) => {
const projectOpts = getProjectOptionsForIndex(index);
return (
<div key={`project-${project.projectId}-${index}`} className="flex gap-2.5">
<FormField
control={control}
name={`projects.${index}.projectId`}
render={({ field, fieldState: { error } }) => {
// Disable project select for existing projects (can only remove or change permission)
const isExistingProject =
project.projectId && initialProjectIds.has(project.projectId);
const isSelectDisabled = isExistingProject || !isOwnerOrManager;
return (
<FormItem className="flex-1">
<Select
onValueChange={field.onChange}
value={project.projectId}
disabled={isSelectDisabled}>
<SelectTrigger>
<SelectValue
placeholder={t("environments.settings.teams.select_workspace")}
/>
</SelectTrigger>
<SelectContent>
{projectOpts.map((option) => (
<SelectItem
key={option.value}
value={option.value}
id={`project-${index}-option`}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{error?.message && (
<FormError className="text-left">{error.message}</FormError>
)}
</FormItem>
);
}}
/>
<FormField
control={control}
name={`projects.${index}.permission`}
render={({ field }) => (
<FormItem className="flex-1">
<Select
onValueChange={field.onChange}
value={project.permission}
disabled={!isOwnerOrManager}>
<SelectTrigger>
<SelectValue placeholder="Select project role" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ZTeamPermission.enum.read}>
{t("environments.settings.teams.read")}
</SelectItem>
<SelectItem value={ZTeamPermission.enum.readWrite}>
{t("environments.settings.teams.read_write")}
</SelectItem>
<SelectItem value={ZTeamPermission.enum.manage}>
{t("environments.settings.teams.manage")}
</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
{watchProjects.length > 1 && (
<Button
size="icon"
type="button"
variant="secondary"
className="shrink-0"
disabled={!isOwnerOrManager}
onClick={() => handleRemoveProject(index)}>
<Trash2Icon className="h-4 w-4" />
</Button>
)}
</div>
);
})}
</div>
{error?.root?.message && (
<FormError className="text-left">{error.root.message}</FormError>
@@ -1,115 +0,0 @@
"use client";
import { Trash2Icon } from "lucide-react";
import { Control } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { ZTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { TTeamSettingsFormSchema } from "@/modules/ee/teams/team-list/types/team";
import { Button } from "@/modules/ui/components/button";
import { FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
export interface WorkspaceRowProps {
index: number;
project: { projectId: string; permission: string };
projectOpts: { value: string; label: string }[];
control: Control<TTeamSettingsFormSchema>;
initialProjectIds: Set<string>;
isOwnerOrManager: boolean;
onRemoveProject: (index: number) => void;
projectCount: number;
}
export function WorkspaceRow(props: Readonly<WorkspaceRowProps>) {
const {
index,
project,
projectOpts,
control,
initialProjectIds,
isOwnerOrManager,
onRemoveProject,
projectCount,
} = props;
const { t } = useTranslation();
return (
<div className="flex gap-2.5">
<FormField
control={control}
name={`projects.${index}.projectId`}
render={({ field, fieldState: { error } }) => {
const isExistingProject = project.projectId && initialProjectIds.has(project.projectId);
const isSelectDisabled = isExistingProject || !isOwnerOrManager;
return (
<FormItem className="flex-1">
<div className={isSelectDisabled ? "pointer-events-none opacity-50" : undefined}>
<InputCombobox
id={`project-select-${index}`}
options={projectOpts}
value={field.value || null}
onChangeValue={(val) => {
const value = typeof val === "string" ? val : "";
field.onChange(value);
}}
showSearch
searchPlaceholder={t("common.search")}
comboboxClasses="flex-1 min-w-0 w-full"
emptyDropdownText={t("environments.surveys.edit.no_option_found")}
/>
</div>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</FormItem>
);
}}
/>
<FormField
control={control}
name={`projects.${index}.permission`}
render={({ field }) => {
const permissionOptions = [
{
value: ZTeamPermission.enum.read,
label: t("environments.settings.teams.read"),
},
{
value: ZTeamPermission.enum.readWrite,
label: t("environments.settings.teams.read_write"),
},
{
value: ZTeamPermission.enum.manage,
label: t("environments.settings.teams.manage"),
},
];
return (
<FormItem className="flex-1">
<div className={isOwnerOrManager ? undefined : "pointer-events-none opacity-50"}>
<InputCombobox
id={`project-permission-select-${index}`}
options={permissionOptions}
value={field.value}
onChangeValue={(val) => field.onChange(val)}
showSearch={false}
comboboxClasses="flex-1 min-w-0 w-full"
/>
</div>
</FormItem>
);
}}
/>
{projectCount > 1 && (
<Button
size="icon"
type="button"
variant="secondary"
className="shrink-0"
disabled={!isOwnerOrManager}
onClick={() => onRemoveProject(index)}>
<Trash2Icon className="h-4 w-4" />
</Button>
)}
</div>
);
}
@@ -24,9 +24,6 @@ vi.mock("@formbricks/database", () => ({
update: vi.fn(),
delete: vi.fn(),
},
teamUser: {
findMany: vi.fn(),
},
membership: { findUnique: vi.fn(), count: vi.fn() },
project: { count: vi.fn() },
environment: { findMany: vi.fn() },
@@ -34,16 +31,13 @@ vi.mock("@formbricks/database", () => ({
}));
const mockTeams = [
{ id: "t1", name: "Team 1", organizationId: "org1", createdAt: new Date(), updatedAt: new Date() },
{ id: "t2", name: "Team 2", organizationId: "org1", createdAt: new Date(), updatedAt: new Date() },
{ id: "t1", name: "Team 1" },
{ id: "t2", name: "Team 2" },
];
const mockUserTeams = [
{
id: "t1",
name: "Team 1",
organizationId: "org1",
createdAt: new Date(),
updatedAt: new Date(),
teamUsers: [{ role: "admin" }],
_count: { teamUsers: 2 },
},
@@ -52,24 +46,14 @@ const mockOtherTeams = [
{
id: "t2",
name: "Team 2",
organizationId: "org1",
createdAt: new Date(),
updatedAt: new Date(),
_count: { teamUsers: 3 },
},
];
const mockMembership = {
userId: "u1",
accepted: true,
role: "owner" as const,
organizationId: "org1",
};
const mockMembership = { role: "admin" };
const mockTeamDetails = {
id: "t1",
name: "Team 1",
organizationId: "org1",
createdAt: new Date(),
updatedAt: new Date(),
teamUsers: [
{ userId: "u1", role: "admin", user: { name: "User 1" } },
{ userId: "u2", role: "member", user: { name: "User 2" } },
@@ -169,13 +153,7 @@ describe("createTeam", () => {
expect(result).toBe("t1");
});
test("throws InvalidInputError if team exists", async () => {
vi.mocked(prisma.team.findFirst).mockResolvedValueOnce({
id: "t1",
name: "Team 1",
organizationId: "org1",
createdAt: new Date(),
updatedAt: new Date(),
});
vi.mocked(prisma.team.findFirst).mockResolvedValueOnce({ id: "t1" });
await expect(createTeam("org1", "Team 1")).rejects.toThrow(InvalidInputError);
});
test("throws InvalidInputError if name too short", async () => {
@@ -275,16 +253,7 @@ describe("updateTeamDetails", () => {
createdAt: new Date(),
updatedAt: new Date(),
});
vi.mocked(prisma.environment.findMany).mockResolvedValueOnce([
{
id: "env1",
type: "production" as const,
createdAt: new Date(),
updatedAt: new Date(),
projectId: "p1",
appSetupCompleted: false,
},
]);
vi.mocked(prisma.environment.findMany).mockResolvedValueOnce([{ id: "env1" }]);
const result = await updateTeamDetails("t1", data);
expect(result).toBe(true);
});
@@ -315,11 +284,9 @@ describe("updateTeamDetails", () => {
id: "t1",
name: "Team 1",
organizationId: "org1",
createdAt: new Date(),
updatedAt: new Date(),
teamUsers: [],
projectTeams: [],
} as any);
members: [],
projects: [],
});
vi.mocked(prisma.membership.count).mockResolvedValueOnce(0);
await expect(updateTeamDetails("t1", data)).rejects.toThrow();
});
@@ -335,11 +302,9 @@ describe("updateTeamDetails", () => {
id: "t1",
name: "Team 1",
organizationId: "org1",
createdAt: new Date(),
updatedAt: new Date(),
teamUsers: [],
projectTeams: [],
} as any);
members: [],
projects: [],
});
vi.mocked(prisma.membership.count).mockResolvedValueOnce(1);
vi.mocked(prisma.project.count).mockResolvedValueOnce(0);
await expect(
@@ -1,7 +1,7 @@
"use server";
import { z } from "zod";
import { ZId, ZStorageUrl } from "@formbricks/types/common";
import { ZId, ZUrl } from "@formbricks/types/common";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
@@ -11,7 +11,7 @@ import { updateOrganizationFaviconUrl } from "@/modules/ee/whitelabel/favicon-cu
const ZUpdateOrganizationFaviconUrlAction = z.object({
organizationId: ZId,
faviconUrl: ZStorageUrl,
faviconUrl: ZUrl,
});
export const updateOrganizationFaviconUrlAction = authenticatedActionClient
@@ -2,7 +2,7 @@ import "server-only";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { ZId, ZStorageUrl } from "@formbricks/types/common";
import { ZId, ZUrl } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TOrganizationWhitelabel } from "@formbricks/types/organizations";
import { validateInputs } from "@/lib/utils/validate";
@@ -11,7 +11,7 @@ export const updateOrganizationFaviconUrl = async (
organizationId: string,
faviconUrl: string | null
): Promise<boolean> => {
validateInputs([organizationId, ZId], [faviconUrl, ZStorageUrl.nullable()]);
validateInputs([organizationId, ZId], [faviconUrl, ZUrl.nullable()]);
try {
const organization = await prisma.organization.findUnique({
@@ -58,7 +58,7 @@ describe("updateProjectBranding", () => {
},
placement: "bottomRight" as const,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
environments: [{ id: "test-env-id" }],
languages: [],
logo: null,
@@ -24,7 +24,6 @@ import { getElementsFromBlocks } from "@/lib/survey/utils";
import { isLight, mixColor } from "@/lib/utils/colors";
import { parseRecallInfo } from "@/lib/utils/recall";
import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley";
import { resolveStorageUrl } from "@/modules/storage/utils";
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
interface PreviewEmailTemplateProps {
@@ -184,7 +183,7 @@ export async function PreviewEmailTemplate({
{ctaElement.buttonExternal && ctaElement.ctaButtonLabel && ctaElement.buttonUrl && (
<Container className="mx-0 mt-4 flex max-w-none items-center justify-end">
<EmailButton
className="text-question-color flex items-center rounded-md border-0 bg-transparent px-3 py-3 text-base font-medium leading-4 no-underline shadow-none"
className="text-question-color flex items-center rounded-md border-0 bg-transparent px-3 py-3 text-base leading-4 font-medium no-underline shadow-none"
href={ctaElement.buttonUrl}>
<Text className="inline">
{getLocalizedValue(ctaElement.ctaButtonLabel, defaultLanguageCode)}{" "}
@@ -214,8 +213,8 @@ export async function PreviewEmailTemplate({
{ "rounded-l-lg border-l": i === 0 },
{ "rounded-r-lg": i === firstQuestion.range - 1 },
firstQuestion.isColorCodingEnabled &&
firstQuestion.scale === "number" &&
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
firstQuestion.scale === "number" &&
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
firstQuestion.scale === "star" && "border-transparent"
)}
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
@@ -307,17 +306,17 @@ export async function PreviewEmailTemplate({
{firstQuestion.choices.map((choice) =>
firstQuestion.allowMulti ? (
<Img
className="rounded-custom mb-3 mr-3 inline-block h-[150px] w-[250px]"
className="rounded-custom mr-3 mb-3 inline-block h-[150px] w-[250px]"
key={choice.id}
src={resolveStorageUrl(choice.imageUrl)}
src={choice.imageUrl}
/>
) : (
<Link
className="rounded-custom mb-3 mr-3 inline-block h-[150px] w-[250px]"
className="rounded-custom mr-3 mb-3 inline-block h-[150px] w-[250px]"
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.id}`}
key={choice.id}
target="_blank">
<Img className="rounded-custom h-full w-full" src={resolveStorageUrl(choice.imageUrl)} />
<Img className="rounded-custom h-full w-full" src={choice.imageUrl} />
</Link>
)
)}
@@ -361,11 +360,11 @@ export async function PreviewEmailTemplate({
<Container className="mx-0">
<Section className="w-full table-auto">
<Row>
<Column className="w-40 break-words px-4 py-2" />
<Column className="w-40 px-4 py-2 break-words" />
{firstQuestion.columns.map((column) => {
return (
<Column
className="text-question-color max-w-40 break-words px-4 py-2 text-center"
className="text-question-color max-w-40 px-4 py-2 text-center break-words"
key={column.id}>
{getLocalizedValue(column.label, "default")}
</Column>
@@ -377,7 +376,7 @@ export async function PreviewEmailTemplate({
<Row
className={`${rowIndex % 2 === 0 ? "bg-input-color" : ""} rounded-custom`}
key={row.id}>
<Column className="w-40 break-words px-4 py-2">
<Column className="w-40 px-4 py-2 break-words">
{getLocalizedValue(row.label, "default")}
</Column>
{firstQuestion.columns.map((column) => {
+15 -38
View File
@@ -17,7 +17,6 @@ import { logger } from "@formbricks/logger";
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
import { InvalidInputError } from "@formbricks/types/errors";
import type { TResponse } from "@formbricks/types/responses";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import type { TSurvey } from "@formbricks/types/surveys/types";
import { TUserEmail, TUserLocale } from "@formbricks/types/user";
import {
@@ -42,7 +41,6 @@ import { createEmailChangeToken, createInviteToken, createToken, createTokenForL
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getElementResponseMapping } from "@/lib/responses";
import { getTranslate } from "@/lingodotdev/server";
import { resolveStorageUrl } from "@/modules/storage/utils";
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
@@ -73,12 +71,12 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
...(SMTP_AUTHENTICATED
? {
auth: {
type: "LOGIN",
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
}
auth: {
type: "LOGIN",
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
}
: {}),
tls: {
rejectUnauthorized: SMTP_REJECT_UNAUTHORIZED_TLS,
@@ -243,22 +241,6 @@ export const sendResponseFinishedEmail = async (
// Pre-process the element response mapping before passing to email
const elements = getElementResponseMapping(survey, response);
// Resolve relative storage URLs to absolute URLs for email rendering
const elementsWithResolvedUrls = elements.map((element) => {
if (
(element.type === TSurveyElementTypeEnum.PictureSelection ||
element.type === TSurveyElementTypeEnum.FileUpload) &&
Array.isArray(element.response)
) {
return {
...element,
response: element.response.map((url) => resolveStorageUrl(url)),
};
}
return element;
});
const html = await renderResponseFinishedEmail({
survey,
responseCount,
@@ -266,7 +248,7 @@ export const sendResponseFinishedEmail = async (
WEBAPP_URL,
environmentId,
organization,
elements: elementsWithResolvedUrls,
elements,
t,
...legalProps,
});
@@ -275,12 +257,12 @@ export const sendResponseFinishedEmail = async (
to: email,
subject: personEmail
? t("emails.response_finished_email_subject_with_email", {
personEmail,
surveyName: survey.name,
})
personEmail,
surveyName: survey.name,
})
: t("emails.response_finished_email_subject", {
surveyName: survey.name,
}),
surveyName: survey.name,
}),
replyTo: personEmail?.toString() ?? MAIL_FROM,
html,
});
@@ -294,12 +276,10 @@ export const sendEmbedSurveyPreviewEmail = async (
logoUrl?: string
): Promise<boolean> => {
const t = await getTranslate(locale);
// Resolve relative storage URLs to absolute URLs for email rendering
const resolvedLogoUrl = logoUrl ? resolveStorageUrl(logoUrl) : undefined;
const html = await renderEmbedSurveyPreviewEmail({
html: innerHtml,
environmentId,
logoUrl: resolvedLogoUrl,
logoUrl,
t,
...legalProps,
});
@@ -317,11 +297,9 @@ export const sendEmailCustomizationPreviewEmail = async (
logoUrl?: string
): Promise<boolean> => {
const t = await getTranslate(locale);
// Resolve relative storage URLs to absolute URLs for email rendering
const resolvedLogoUrl = logoUrl ? resolveStorageUrl(logoUrl) : undefined;
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
userName,
logoUrl: resolvedLogoUrl,
logoUrl,
t,
...legalProps,
});
@@ -338,8 +316,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
const email = data.email;
const surveyName = data.surveyName;
const singleUseId = data.suId;
// Resolve relative storage URLs to absolute URLs for email rendering
const logoUrl = data.logoUrl ? resolveStorageUrl(data.logoUrl) : "";
const logoUrl = data.logoUrl || "";
const token = createTokenForLinkSurvey(surveyId, email);
const t = await getTranslate(data.locale);
const getSurveyLink = (): string => {
@@ -177,9 +177,9 @@ describe("utils.ts", () => {
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.organization_not_found");
});
test("throws AuthorizationError if membership not found", async () => {
test("throws error if membership not found", async () => {
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
await expect(getEnvironmentAuth("env123")).rejects.toThrow(AuthorizationError);
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.membership_not_found");
});
});
@@ -241,7 +241,7 @@ describe("utils.ts", () => {
config: {},
placement: "bottomRight" as const,
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
styling: {},
logo: null,
environments: [
@@ -389,7 +389,7 @@ describe("utils.ts", () => {
config: {},
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
styling: {},
logo: null,
environments: [
@@ -481,7 +481,7 @@ describe("utils.ts", () => {
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthorizationError);
});
test("throws AuthorizationError if membership not found", async () => {
test("throws error if membership not found", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce({
id: "env123",
createdAt: new Date(),
@@ -502,7 +502,7 @@ describe("utils.ts", () => {
config: {},
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
styling: {},
logo: null,
environments: [],
@@ -519,7 +519,9 @@ describe("utils.ts", () => {
},
} as any);
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthorizationError);
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(
"common.membership_not_found"
);
});
test("fetches user before auth check, then environment data after authorization", async () => {
@@ -586,7 +588,7 @@ describe("utils.ts", () => {
config: {},
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
styling: {},
logo: null,
environments: [],
@@ -625,7 +627,7 @@ describe("utils.ts", () => {
config: {},
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
darkOverlay: false,
styling: {},
logo: null,
environments: [],
+4 -4
View File
@@ -61,7 +61,7 @@ export const getEnvironmentAuth = reactCache(async (environmentId: string): Prom
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
if (!currentUserMembership) {
throw new AuthorizationError(t("common.membership_not_found"));
throw new Error(t("common.membership_not_found"));
}
const { isMember, isOwner, isManager, isBilling } = getAccessFlags(currentUserMembership?.role);
@@ -150,7 +150,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
config: true,
placement: true,
clickOutsideClose: true,
overlay: true,
darkOverlay: true,
styling: true,
logo: true,
customHeadScripts: true,
@@ -220,7 +220,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
config: data.project.config,
placement: data.project.placement,
clickOutsideClose: data.project.clickOutsideClose,
overlay: data.project.overlay,
darkOverlay: data.project.darkOverlay,
styling: data.project.styling,
logo: data.project.logo,
customHeadScripts: data.project.customHeadScripts,
@@ -293,7 +293,7 @@ export const getEnvironmentLayoutData = reactCache(
// Validate user's membership was found
if (!membership) {
throw new AuthorizationError(t("common.membership_not_found"));
throw new Error(t("common.membership_not_found"));
}
// Fetch remaining data in parallel
@@ -15,7 +15,6 @@ type TEnterpriseLicense = {
lastChecked: Date;
isPendingDowngrade: boolean;
fallbackLevel: string;
status: "active" | "expired" | "unreachable" | "no-license" | "invalid_license";
};
export const ZEnvironmentAuth = z.object({
@@ -2,7 +2,6 @@
import { OrganizationRole } from "@prisma/client";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId, ZUuid } from "@formbricks/types/common";
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
@@ -24,7 +23,7 @@ import {
getMembershipsByUserId,
getOrganizationOwnerCount,
} from "@/modules/organization/settings/teams/lib/membership";
import { deleteInvite, getInvite, inviteUser, refreshInviteExpiration, resendInvite } from "./lib/invite";
import { deleteInvite, getInvite, inviteUser, resendInvite } from "./lib/invite";
const ZDeleteInviteAction = z.object({
inviteId: ZUuid,
@@ -58,57 +57,30 @@ const ZCreateInviteTokenAction = z.object({
inviteId: ZUuid,
});
export const createInviteTokenAction = authenticatedActionClient.schema(ZCreateInviteTokenAction).action(
withAuditLogging(
"updated",
"invite",
async ({
parsedInput,
ctx,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: z.infer<typeof ZCreateInviteTokenAction>;
}) => {
const organizationId = await getOrganizationIdFromInviteId(parsedInput.inviteId);
export const createInviteTokenAction = authenticatedActionClient
.schema(ZCreateInviteTokenAction)
.action(async ({ parsedInput, ctx }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromInviteId(parsedInput.inviteId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
// Get old expiresAt for audit logging before update
const oldInvite = await prisma.invite.findUnique({
where: { id: parsedInput.inviteId },
select: { email: true, expiresAt: true },
});
if (!oldInvite) {
throw new ValidationError("Invite not found");
}
// Refresh the invitation expiration
const updatedInvite = await refreshInviteExpiration(parsedInput.inviteId);
// Set audit context
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.inviteId = parsedInput.inviteId;
ctx.auditLoggingCtx.oldObject = { expiresAt: oldInvite.expiresAt };
ctx.auditLoggingCtx.newObject = { expiresAt: updatedInvite.expiresAt };
const inviteToken = createInviteToken(parsedInput.inviteId, updatedInvite.email, {
expiresIn: "7d",
});
return { inviteToken: encodeURIComponent(inviteToken) };
const invite = await getInvite(parsedInput.inviteId);
if (!invite) {
throw new ValidationError("Invite not found");
}
)
);
const inviteToken = createInviteToken(parsedInput.inviteId, invite.email, {
expiresIn: "7d",
});
return { inviteToken: encodeURIComponent(inviteToken) };
});
const ZDeleteMembershipAction = z.object({
userId: ZId,
@@ -219,7 +191,6 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
invite?.creator?.name ?? "",
updatedInvite.name ?? ""
);
return updatedInvite;
}
)
@@ -28,16 +28,18 @@ export const EditMemberships = async ({
return (
<div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 w-full max-w-full grid-cols-12 items-center gap-x-4 rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2 overflow-hidden">{t("common.full_name")}</div>
<div className="col-span-3 overflow-hidden">{t("common.email")}</div>
<div className="flex h-12 w-full max-w-full items-center gap-x-4 rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div className="w-1/2 overflow-hidden">{t("common.full_name")}</div>
<div className="w-1/2 overflow-hidden">{t("common.email")}</div>
{isAccessControlAllowed && <div className="col-span-2 whitespace-nowrap">{t("common.role")}</div>}
{isAccessControlAllowed && (
<div className="min-w-[100px] whitespace-nowrap">{t("common.role")}</div>
)}
<div className="col-span-2 whitespace-nowrap">{t("common.status")}</div>
<div className="min-w-[80px] whitespace-nowrap">{t("common.status")}</div>
{!isUserManagementDisabledFromUi && (
<div className="col-span-3 whitespace-nowrap text-center">{t("common.actions")}</div>
<div className="min-w-[125px] whitespace-nowrap">{t("common.actions")}</div>
)}
</div>
@@ -33,6 +33,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
const [isDeleteMemberModalOpen, setDeleteMemberModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [showShareInviteModal, setShowShareInviteModal] = useState(false);
const [shareInviteToken, setShareInviteToken] = useState("");
const handleDeleteMember = async () => {
@@ -41,27 +42,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
if (!member && invite) {
// This is an invite
const result = await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
setIsDeleting(false);
return;
}
await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id });
toast.success(t("environments.settings.general.invite_deleted_successfully"));
}
if (member && !invite) {
// This is a member
const result = await deleteMembershipAction({
userId: member.userId,
organizationId: organization.id,
});
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
setIsDeleting(false);
return;
}
await deleteMembershipAction({ userId: member.userId, organizationId: organization.id });
toast.success(t("environments.settings.general.member_deleted_successfully"));
}
@@ -92,7 +80,6 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
if (createInviteTokenResponse?.data) {
setShareInviteToken(createInviteTokenResponse.data.inviteToken);
setShowShareInviteModal(true);
router.refresh();
} else {
const errorMessage = getFormattedErrorMessage(createInviteTokenResponse);
toast.error(errorMessage);
@@ -112,7 +99,6 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
});
if (resendInviteResponse?.data) {
toast.success(t("environments.settings.general.invitation_sent_once_more"));
router.refresh();
} else {
const errorMessage = getFormattedErrorMessage(resendInviteResponse);
toast.error(errorMessage);
@@ -123,7 +109,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
};
return (
<div className="flex justify-end gap-2">
<div className="flex gap-2">
<TooltipRenderer tooltipContent={t("common.delete")} shouldRender={!!showDeleteButton}>
<Button
variant="destructive"

Some files were not shown because too many files have changed in this diff Show More