Compare commits

..

4 Commits

Author SHA1 Message Date
Cursor Agent 6fb754bf87 chore: update pnpm-lock.yaml for vitest dependency 2026-02-13 01:25:01 +00:00
Cursor Agent e1cdbf7bc5 style: use test instead of it in validation tests
- Fixed linting issues by following @vitest/consistent-test-it rule
- Changed all 'it' to 'test' for consistency
2026-02-13 01:24:28 +00:00
Cursor Agent b52f1a19d4 test: add comprehensive tests for findLanguageCodesForDuplicateLabels
- Added test coverage for undefined value handling
- Tests verify fix for TypeError when language keys are missing
- Added test script to types package.json
- All 7 tests passing
2026-02-13 01:21:21 +00:00
Cursor Agent f55b9de3a2 fix: prevent TypeError when accessing undefined TI18nString values in findLanguageCodesForDuplicateLabels
Fixes FORMBRICKS-JM

- Updated findLanguageCodesForDuplicateLabels in both validation.ts and elements-validation.ts
- Changed to safely filter undefined values before calling .trim()
- Now filters for string type first, then trims, then filters empty strings
- Prevents 'undefined is not an object (evaluating e[r].trim)' error when survey labels are incomplete
2026-02-13 01:19:23 +00:00
430 changed files with 13206 additions and 18081 deletions
+3 -3
View File
@@ -65,8 +65,8 @@ jobs:
set -euo pipefail
echo "Updating Chart.yaml with version: ${VERSION}"
yq -i ".version = \"${VERSION}\"" charts/formbricks/Chart.yaml
yq -i ".appVersion = \"${VERSION}\"" charts/formbricks/Chart.yaml
yq -i ".version = \"${VERSION}\"" helm-chart/Chart.yaml
yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml
echo "✅ Successfully updated Chart.yaml"
@@ -77,7 +77,7 @@ jobs:
set -euo pipefail
echo "Packaging Helm chart version: ${VERSION}"
helm package ./charts/formbricks
helm package ./helm-chart
echo "✅ Successfully packaged formbricks-${VERSION}.tgz"
-4
View File
@@ -9,7 +9,6 @@ on:
merge_group:
permissions:
contents: read
pull-requests: read
jobs:
sonarqube:
name: SonarQube
@@ -51,9 +50,6 @@ jobs:
pnpm test:coverage
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf
with:
args: >
-Dsonar.verbose=true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+22 -20
View File
@@ -6,9 +6,19 @@ permissions:
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "apps/web/**/*.ts"
- "apps/web/**/*.tsx"
- "apps/web/locales/**/*.json"
- "scan-translations.ts"
push:
branches:
- main
paths:
- "apps/web/**/*.ts"
- "apps/web/**/*.tsx"
- "apps/web/locales/**/*.json"
- "scan-translations.ts"
jobs:
validate-translations:
@@ -23,38 +33,30 @@ jobs:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check for relevant changes
id: changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
translations:
- 'apps/web/**/*.ts'
- 'apps/web/**/*.tsx'
- 'apps/web/locales/**/*.json'
- 'packages/surveys/src/**/*.{ts,tsx}'
- 'packages/surveys/locales/**/*.json'
- 'packages/email/**/*.{ts,tsx}'
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Setup Node.js 22.x
if: steps.changes.outputs.translations == 'true'
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: 22.x
- name: Install pnpm
if: steps.changes.outputs.translations == 'true'
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
if: steps.changes.outputs.translations == 'true'
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Validate translation keys
if: steps.changes.outputs.translations == 'true'
run: pnpm run scan-translations
run: |
echo ""
echo "🔍 Validating translation keys..."
echo ""
pnpm run scan-translations
- name: Skip (no translation-related changes)
if: steps.changes.outputs.translations != 'true'
run: echo "No translation-related files changed — skipping validation."
- name: Summary
if: success()
run: |
echo ""
echo "✅ Translation validation completed successfully!"
echo ""
+2
View File
@@ -0,0 +1,2 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json
prettier --write ./branch.json
+40 -1
View File
@@ -1 +1,40 @@
pnpm lint-staged
# Load environment variables from .env files
if [ -f .env ]; then
set -a
. .env
set +a
fi
pnpm lint-staged
# Run Lingo.dev i18n workflow if LINGODOTDEV_API_KEY is set
if [ -n "$LINGODOTDEV_API_KEY" ]; then
echo ""
echo "🌍 Running Lingo.dev translation workflow..."
echo ""
# Run translation generation and validation
if pnpm run i18n; then
echo ""
echo "✅ Translation validation passed"
echo ""
# Add updated locale files to git
git add apps/web/locales/*.json
else
echo ""
echo "❌ Translation validation failed!"
echo ""
echo "Please fix the translation issues above before committing:"
echo " • Add missing translation keys to your locale files"
echo " • Remove unused translation keys"
echo ""
echo "Or run 'pnpm i18n' to see the detailed report"
echo ""
exit 1
fi
else
echo ""
echo "⚠️ Skipping translation validation: LINGODOTDEV_API_KEY is not set"
echo " (This is expected for community contributors)"
echo ""
fi
+17 -12
View File
@@ -10,20 +10,25 @@
"build-storybook": "storybook build",
"clean": "rimraf .turbo node_modules dist storybook-static"
},
"dependencies": {
"@formbricks/survey-ui": "workspace:*"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@storybook/addon-a11y": "10.2.14",
"@storybook/addon-links": "10.2.14",
"@storybook/addon-onboarding": "10.2.14",
"@storybook/react-vite": "10.2.14",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@tailwindcss/vite": "4.2.1",
"@typescript-eslint/parser": "8.56.1",
"@vitejs/plugin-react": "5.1.4",
"@chromatic-com/storybook": "^5.0.0",
"@storybook/addon-a11y": "10.1.11",
"@storybook/addon-links": "10.1.11",
"@storybook/addon-onboarding": "10.1.11",
"@storybook/react-vite": "10.1.11",
"@typescript-eslint/eslint-plugin": "8.53.0",
"@tailwindcss/vite": "4.1.18",
"@typescript-eslint/parser": "8.53.0",
"@vitejs/plugin-react": "5.1.2",
"esbuild": "0.25.12",
"eslint-plugin-react-refresh": "0.4.26",
"eslint-plugin-storybook": "10.2.14",
"storybook": "10.2.14",
"eslint-plugin-storybook": "10.1.11",
"prop-types": "15.8.1",
"storybook": "10.1.11",
"vite": "7.3.1",
"@storybook/addon-docs": "10.2.14"
"@storybook/addon-docs": "10.1.11"
}
}
-6
View File
@@ -1,6 +0,0 @@
const baseConfig = require("../../.prettierrc.js");
module.exports = {
...baseConfig,
tailwindConfig: "./tailwind.config.js",
};
-19
View File
@@ -101,9 +101,6 @@ RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
# Create packages/database directory structure with proper ownership for runtime migrations
RUN mkdir -p ./packages/database/migrations && chown -R nextjs:nextjs ./packages/database
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
@@ -129,22 +126,6 @@ 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
# Pino loads transport code in worker threads via dynamic require().
# Next.js file tracing only traces static imports, missing runtime-loaded files
# (e.g. pino/lib/transport-stream.js, transport targets).
# Copy the full packages to ensure all runtime files are available.
COPY --from=installer /app/node_modules/pino ./node_modules/pino
RUN chmod -R 755 ./node_modules/pino
COPY --from=installer /app/node_modules/pino-opentelemetry-transport ./node_modules/pino-opentelemetry-transport
RUN chmod -R 755 ./node_modules/pino-opentelemetry-transport
COPY --from=installer /app/node_modules/pino-abstract-transport ./node_modules/pino-abstract-transport
RUN chmod -R 755 ./node_modules/pino-abstract-transport
COPY --from=installer /app/node_modules/otlp-logger ./node_modules/otlp-logger
RUN chmod -R 755 ./node_modules/otlp-logger
# 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
@@ -69,7 +69,7 @@ export const ConnectWithFormbricks = ({
) : (
<div className="flex animate-pulse flex-col items-center space-y-4">
<span className="relative flex h-10 w-10">
<span className="absolute inline-flex h-full w-full animate-ping-slow rounded-full bg-slate-400 opacity-75"></span>
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
</span>
<p className="pt-4 text-sm font-medium text-slate-600">
@@ -46,7 +46,7 @@ const Page = async (props: ConnectPageProps) => {
channel={channel}
/>
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}`}>
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
<XMTemplateList project={project} user={user} environmentId={environment.id} />
{projects.length >= 2 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}/surveys`}>
@@ -42,7 +42,7 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
return (
<aside
className={cn(
"z-40 flex w-sidebar-collapsed flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
"w-sidebar-collapsed z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
)}>
<Image src={FBLogo} width={160} height={30} alt={t("environments.formbricks_logo")} />
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -228,7 +228,7 @@ export const ProjectSettings = ({
</FormProvider>
</div>
<div className="relative flex w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 p-6 shadow">
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow">
{logoUrl && (
<Image
src={logoUrl}
@@ -239,16 +239,18 @@ export const ProjectSettings = ({
/>
)}
<p className="text-sm text-slate-400">{t("common.preview")}</p>
<SurveyInline
appUrl={publicDomain}
isPreviewMode={true}
survey={previewSurvey(projectName || t("common.my_product"), t)}
styling={previewStyling}
isBrandingEnabled={false}
languageCode="default"
onFileUpload={async (file) => file.name}
autoFocus={false}
/>
<div className="z-0 h-3/4 w-3/4">
<SurveyInline
appUrl={publicDomain}
isPreviewMode={true}
survey={previewSurvey(projectName || "my Product", t)}
styling={previewStyling}
isBrandingEnabled={false}
languageCode="default"
onFileUpload={async (file) => file.name}
autoFocus={false}
/>
</div>
</div>
<CreateTeamModal
open={createTeamModalOpen}
@@ -69,7 +69,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
/>
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -109,10 +109,7 @@ export const MainNavigation = ({
href: `/environments/${environment.id}/contacts`,
name: t("common.contacts"),
icon: UserIcon,
isActive:
pathname?.includes("/contacts") ||
pathname?.includes("/segments") ||
pathname?.includes("/attributes"),
isActive: pathname?.includes("/contacts") || pathname?.includes("/segments"),
},
{
name: t("common.configuration"),
@@ -188,7 +185,7 @@ export const MainNavigation = ({
size="icon"
onClick={toggleSidebar}
className={cn(
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
)}>
{isCollapsed ? (
<PanelLeftOpenIcon strokeWidth={1.5} />
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
<currentStatus.icon />
</div>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
{status === "notImplemented" && (
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
<RotateCcwIcon />
@@ -81,7 +81,7 @@ export const OrganizationBreadcrumb = ({
getOrganizationsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
if (result?.data) {
// Sort organizations by name
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
const sorted = result.data.toSorted((a, b) => a.name.localeCompare(b.name));
setOrganizations(sorted);
} else {
// Handle server errors or validation errors
@@ -82,7 +82,7 @@ export const ProjectBreadcrumb = ({
getProjectsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
if (result?.data) {
// Sort projects by name
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
const sorted = result.data.toSorted((a, b) => a.name.localeCompare(b.name));
setProjects(sorted);
} else {
// Handle server errors or validation errors
@@ -30,7 +30,7 @@ export const NotificationSwitch = ({
const isChecked =
notificationType === "unsubscribedOrganizationIds"
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)
: notificationSettings[notificationType]?.[surveyOrProjectOrOrganizationId] === true;
: notificationSettings[notificationType][surveyOrProjectOrOrganizationId] === true;
const handleSwitchChange = async () => {
setIsLoading(true);
@@ -49,11 +49,8 @@ export const NotificationSwitch = ({
];
}
} else {
updatedNotificationSettings[notificationType] = {
...updatedNotificationSettings[notificationType],
[surveyOrProjectOrOrganizationId]:
!updatedNotificationSettings[notificationType]?.[surveyOrProjectOrOrganizationId],
};
updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId] =
!updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId];
}
const updatedNotificationSettingsActionResponse = await updateNotificationSettingsAction({
@@ -81,7 +78,7 @@ export const NotificationSwitch = ({
) {
switch (notificationType) {
case "alert":
if (notificationSettings[notificationType]?.[surveyOrProjectOrOrganizationId] === true) {
if (notificationSettings[notificationType][surveyOrProjectOrOrganizationId] === true) {
handleSwitchChange();
toast.success(
t(
@@ -98,7 +98,7 @@ export const PasswordConfirmationModal = ({
aria-label="password"
aria-required="true"
required
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(password) => field.onChange(password)}
/>
@@ -38,11 +38,7 @@ const getBadgeConfig = (
}
};
export const EnterpriseLicenseStatus = ({
status,
gracePeriodEnd,
environmentId,
}: EnterpriseLicenseStatusProps) => {
export const EnterpriseLicenseStatus = ({ status, gracePeriodEnd, environmentId }: EnterpriseLicenseStatusProps) => {
const { t } = useTranslation();
const router = useRouter();
const [isRechecking, setIsRechecking] = useState(false);
@@ -9,7 +9,6 @@ import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import packageJson from "@/package.json";
import { SettingsCard } from "../../components/SettingsCard";
import { DeleteOrganization } from "./components/DeleteOrganization";
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
@@ -82,10 +81,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
</SettingsCard>
)}
<div className="space-y-2">
<IdBadge id={organization.id} label={t("common.organization_id")} variant="column" />
<IdBadge id={packageJson.version} label={t("common.formbricks_version")} variant="column" />
</div>
<IdBadge id={organization.id} label={t("common.organization_id")} variant="column" />
</PageContentWrapper>
);
};
@@ -4,7 +4,6 @@ import { revalidatePath } from "next/cache";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
import { getDisplaysBySurveyIdWithContact } from "@/lib/display/service";
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
@@ -107,31 +106,3 @@ export const getResponseCountAction = authenticatedActionClient
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
});
const ZGetDisplaysWithContactAction = z.object({
surveyId: ZId,
limit: z.number().int().min(1).max(100),
offset: z.number().int().nonnegative(),
});
export const getDisplaysWithContactAction = authenticatedActionClient
.schema(ZGetDisplaysWithContactAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
return getDisplaysBySurveyIdWithContact(parsedInput.surveyId, parsedInput.limit, parsedInput.offset);
});
@@ -77,7 +77,7 @@ export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: Matr
)}>
<button
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
className="m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline hover:outline-brand-dark"
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline"
onClick={() =>
setFilter(
elementSummary.element.id,
@@ -158,7 +158,7 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
}>
<div className="flex h-32 w-full flex-col items-center justify-end">
<div
className="w-full rounded-t-lg border border-slate-200 bg-brand-dark transition-all group-hover:brightness-110"
className="bg-brand-dark w-full rounded-t-lg border border-slate-200 transition-all group-hover:brightness-110"
style={{
height: `${Math.max(choice.percentage, 2)}%`,
opacity,
@@ -116,7 +116,7 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
)
}>
<div
className={`h-full bg-brand-dark ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
className={`bg-brand-dark h-full ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
style={{ opacity }}
/>
</ClickableBarSegment>
@@ -1,125 +0,0 @@
"use client";
import { AlertCircleIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { TDisplayWithContact } from "@formbricks/types/displays";
import { TUserLocale } from "@formbricks/types/user";
import { timeSince } from "@/lib/time";
import { Button } from "@/modules/ui/components/button";
interface SummaryImpressionsProps {
displays: TDisplayWithContact[];
isLoading: boolean;
hasMore: boolean;
displaysError: string | null;
environmentId: string;
locale: TUserLocale;
onLoadMore: () => void;
onRetry: () => void;
}
const getDisplayContactIdentifier = (display: TDisplayWithContact): string => {
if (!display.contact) return "";
return display.contact.attributes?.email || display.contact.attributes?.userId || display.contact.id;
};
export const SummaryImpressions = ({
displays,
isLoading,
hasMore,
displaysError,
environmentId,
locale,
onLoadMore,
onRetry,
}: SummaryImpressionsProps) => {
const { t } = useTranslation();
const renderContent = () => {
if (displaysError) {
return (
<div className="p-8">
<div className="flex flex-col items-center gap-4 text-center">
<div className="flex items-center gap-2 text-red-600">
<AlertCircleIcon className="h-5 w-5" />
<span className="text-sm font-medium">{t("common.error_loading_data")}</span>
</div>
<p className="text-sm text-slate-500">{displaysError}</p>
<Button onClick={onRetry} variant="secondary" size="sm">
{t("common.try_again")}
</Button>
</div>
</div>
);
}
if (displays.length === 0) {
return (
<div className="p-8 text-center text-sm text-slate-500">
{t("environments.surveys.summary.no_identified_impressions")}
</div>
);
}
return (
<>
<div className="grid min-h-10 grid-cols-4 items-center border-b border-slate-200 bg-slate-100 text-sm font-semibold text-slate-600">
<div className="col-span-2 px-4 md:px-6">{t("common.user")}</div>
<div className="col-span-2 px-4 md:px-6">{t("environments.contacts.survey_viewed_at")}</div>
</div>
<div className="max-h-[62vh] overflow-y-auto">
{displays.map((display) => (
<div
key={display.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-xs text-slate-800 last:border-transparent md:text-sm">
<div className="col-span-2 pl-4 md:pl-6">
{display.contact ? (
<Link
className="ph-no-capture break-all text-slate-600 hover:underline"
href={`/environments/${environmentId}/contacts/${display.contact.id}`}>
{getDisplayContactIdentifier(display)}
</Link>
) : (
<span className="break-all text-slate-600">{t("common.anonymous")}</span>
)}
</div>
<div className="col-span-2 px-4 text-slate-500 md:px-6">
{timeSince(display.createdAt.toString(), locale)}
</div>
</div>
))}
</div>
{hasMore && (
<div className="flex justify-center border-t border-slate-100 py-4">
<Button onClick={onLoadMore} variant="secondary" size="sm">
{t("common.load_more")}
</Button>
</div>
)}
</>
);
};
if (isLoading) {
return (
<div className="rounded-xl border border-slate-200 bg-white p-8 shadow-sm">
<div className="flex items-center justify-center">
<div className="h-6 w-32 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
);
}
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex items-center gap-2 rounded-t-xl border-b border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
<InfoIcon className="h-4 w-4 shrink-0" />
<span>{t("environments.surveys.summary.impressions_identified_only")}</span>
</div>
{renderContent()}
</div>
);
};
@@ -10,8 +10,8 @@ interface SummaryMetadataProps {
surveySummary: TSurveySummary["meta"];
quotasCount: number;
isLoading: boolean;
tab: "dropOffs" | "quotas" | "impressions" | undefined;
setTab: React.Dispatch<React.SetStateAction<"dropOffs" | "quotas" | "impressions" | undefined>>;
tab: "dropOffs" | "quotas" | undefined;
setTab: React.Dispatch<React.SetStateAction<"dropOffs" | "quotas" | undefined>>;
isQuotasAllowed: boolean;
}
@@ -53,7 +53,7 @@ export const SummaryMetadata = ({
const { t } = useTranslation();
const dropoffCountValue = dropOffCount === 0 ? <span>-</span> : dropOffCount;
const handleTabChange = (val: "dropOffs" | "quotas" | "impressions") => {
const handleTabChange = (val: "dropOffs" | "quotas") => {
const change = tab === val ? undefined : val;
setTab(change);
};
@@ -65,16 +65,12 @@ export const SummaryMetadata = ({
`grid gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-2 lg:grid-cols-3 2xl:grid-cols-5`,
isQuotasAllowed && quotasCount > 0 && "2xl:grid-cols-6"
)}>
<InteractiveCard
key="impressions"
tab="impressions"
<StatCard
label={t("environments.surveys.summary.impressions")}
percentage={null}
value={displayCount === 0 ? <span>-</span> : displayCount}
tooltipText={t("environments.surveys.summary.impressions_tooltip")}
isLoading={isLoading}
onClick={() => handleTabChange("impressions")}
isActive={tab === "impressions"}
/>
<StatCard
label={t("environments.surveys.summary.starts")}
@@ -1,31 +1,21 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TDisplayWithContact } from "@formbricks/types/displays";
import { useEffect, useMemo, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import {
getDisplaysWithContactAction,
getSurveySummaryAction,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import { getSurveySummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
import { SummaryImpressions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryImpressions";
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { QuotasSummary } from "@/modules/ee/quotas/components/quotas-summary";
import { SummaryList } from "./SummaryList";
import { SummaryMetadata } from "./SummaryMetadata";
const DISPLAYS_PER_PAGE = 15;
const defaultSurveySummary: TSurveySummary = {
meta: {
completedPercentage: 0,
@@ -61,76 +51,17 @@ export const SummaryPage = ({
initialSurveySummary,
isQuotasAllowed,
}: SummaryPageProps) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(
initialSurveySummary || defaultSurveySummary
);
const [tab, setTab] = useState<"dropOffs" | "quotas" | "impressions" | undefined>(undefined);
const [tab, setTab] = useState<"dropOffs" | "quotas" | undefined>(undefined);
const [isLoading, setIsLoading] = useState(!initialSurveySummary);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [displays, setDisplays] = useState<TDisplayWithContact[]>([]);
const [isDisplaysLoading, setIsDisplaysLoading] = useState(false);
const [hasMoreDisplays, setHasMoreDisplays] = useState(true);
const [displaysError, setDisplaysError] = useState<string | null>(null);
const displaysFetchedRef = useRef(false);
const fetchDisplays = useCallback(
async (offset: number) => {
const response = await getDisplaysWithContactAction({
surveyId,
limit: DISPLAYS_PER_PAGE,
offset,
});
if (!response?.data) {
const errorMessage = getFormattedErrorMessage(response);
throw new Error(errorMessage);
}
return response?.data ?? [];
},
[surveyId]
);
const loadInitialDisplays = useCallback(async () => {
setIsDisplaysLoading(true);
setDisplaysError(null);
try {
const data = await fetchDisplays(0);
setDisplays(data);
setHasMoreDisplays(data.length === DISPLAYS_PER_PAGE);
} catch (error) {
toast.error(error);
setDisplays([]);
setHasMoreDisplays(false);
} finally {
setIsDisplaysLoading(false);
}
}, [fetchDisplays, t]);
const handleLoadMoreDisplays = useCallback(async () => {
try {
const data = await fetchDisplays(displays.length);
setDisplays((prev) => [...prev, ...data]);
setHasMoreDisplays(data.length === DISPLAYS_PER_PAGE);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : t("common.something_went_wrong");
toast.error(errorMessage);
}
}, [fetchDisplays, displays.length, t]);
useEffect(() => {
if (tab === "impressions" && !displaysFetchedRef.current) {
displaysFetchedRef.current = true;
loadInitialDisplays();
}
}, [tab, loadInitialDisplays]);
// Only fetch data when filters change or when there's no initial data
useEffect(() => {
// If we have initial data and no filters are applied, don't fetch
@@ -190,18 +121,6 @@ export const SummaryPage = ({
setTab={setTab}
isQuotasAllowed={isQuotasAllowed}
/>
{tab === "impressions" && (
<SummaryImpressions
displays={displays}
isLoading={isDisplaysLoading}
hasMore={hasMoreDisplays}
displaysError={displaysError}
environmentId={environment.id}
locale={locale}
onLoadMore={handleLoadMoreDisplays}
onRetry={loadInitialDisplays}
/>
)}
{tab === "dropOffs" && <SummaryDropOffs dropOff={surveySummary.dropOff} survey={surveyMemoized} />}
{isQuotasAllowed && tab === "quotas" && <QuotasSummary quotas={surveySummary.quotas} />}
<div className="flex gap-1.5">
@@ -4,9 +4,9 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { BaseCard } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card";
interface InteractiveCardProps {
tab: "dropOffs" | "quotas" | "impressions";
tab: "dropOffs" | "quotas";
label: string;
percentage: number | null;
percentage: number;
value: React.ReactNode;
tooltipText: string;
isLoading: boolean;
@@ -352,7 +352,7 @@ export const AnonymousLinksTab = ({
},
{
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-block",
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-question",
},
]}
/>
@@ -105,7 +105,7 @@ export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTa
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
<pre className="whitespace-pre-wrap font-mono text-xs text-slate-600">
<pre className="font-mono text-xs whitespace-pre-wrap text-slate-600">
{projectCustomScripts}
</pre>
</div>
@@ -135,7 +135,7 @@ export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTa
rows={8}
placeholder={t("environments.surveys.share.custom_html.placeholder")}
className={cn(
"flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:border-brand-dark focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
)}
{...field}
disabled={isReadOnly}
@@ -66,7 +66,7 @@ export const SuccessView: React.FC<SuccessViewProps> = ({
className="relative flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
<UserIcon className="h-8 w-8 stroke-1 text-slate-900" />
{t("environments.surveys.summary.use_personal_links")}
<Badge size="normal" type="success" className="absolute right-3 top-3" text={t("common.new")} />
<Badge size="normal" type="success" className="absolute top-3 right-3" text={t("common.new")} />
</button>
<Link
href={`/environments/${environmentId}/settings/notifications`}
@@ -192,7 +192,7 @@ export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementCo
value={inputValue}
onValueChange={setInputValue}
placeholder={open ? `${t("common.search")}...` : t("common.select_filter")}
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none outline-none ring-offset-transparent focus:border-none focus:shadow-none focus:outline-none focus:ring-offset-0"
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none ring-offset-transparent outline-none focus:border-none focus:shadow-none focus:ring-offset-0 focus:outline-none"
/>
)}
<Button
@@ -241,7 +241,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
<Popover open={isOpen} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<PopoverTriggerButton isOpen={isOpen}>
{t("common.filter")} <b>{activeFilterCount > 0 && `(${activeFilterCount})`}</b>
Filter <b>{activeFilterCount > 0 && `(${activeFilterCount})`}</b>
</PopoverTriggerButton>
</PopoverTrigger>
<PopoverContent
@@ -329,7 +329,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
</div>
{i !== filterValue.filter.length - 1 && (
<div className="my-4 flex items-center">
<p className="mr-4 font-semibold text-slate-800">{t("common.and")}</p>
<p className="mr-4 font-semibold text-slate-800">and</p>
<hr className="w-full text-slate-600" />
</div>
)}
@@ -1,49 +1,12 @@
"use server";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import {
TIntegrationGoogleSheets,
ZIntegrationGoogleSheets,
} from "@formbricks/types/integration/google-sheet";
import { getSpreadsheetNameById, validateGoogleSheetsConnection } from "@/lib/googleSheet/service";
import { getIntegrationByType } from "@/lib/integration/service";
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { getSpreadsheetNameById } from "@/lib/googleSheet/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
const ZValidateGoogleSheetsConnectionAction = z.object({
environmentId: ZId,
});
export const validateGoogleSheetsConnectionAction = authenticatedActionClient
.schema(ZValidateGoogleSheetsConnectionAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],
});
const integration = await getIntegrationByType(parsedInput.environmentId, "googleSheets");
if (!integration) {
return { data: false };
}
await validateGoogleSheetsConnection(integration as TIntegrationGoogleSheets);
return { data: true };
});
const ZGetSpreadsheetNameByIdAction = z.object({
googleSheetIntegration: ZIntegrationGoogleSheets,
environmentId: z.string(),
@@ -20,10 +20,6 @@ import {
isValidGoogleSheetsUrl,
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/util";
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
import {
GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION,
GOOGLE_SHEET_INTEGRATION_INVALID_GRANT,
} from "@/lib/googleSheet/constants";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
@@ -122,17 +118,6 @@ export const AddIntegrationModal = ({
resetForm();
}, [selectedIntegration, surveys]);
const showErrorMessageToast = (response: Awaited<ReturnType<typeof getSpreadsheetNameByIdAction>>) => {
const errorMessage = getFormattedErrorMessage(response);
if (errorMessage === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) {
toast.error(t("environments.integrations.google_sheets.token_expired_error"));
} else if (errorMessage === GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION) {
toast.error(t("environments.integrations.google_sheets.spreadsheet_permission_error"));
} else {
toast.error(errorMessage);
}
};
const linkSheet = async () => {
try {
if (!isValidGoogleSheetsUrl(spreadsheetUrl)) {
@@ -144,7 +129,6 @@ export const AddIntegrationModal = ({
if (selectedElements.length === 0) {
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
}
setIsLinkingSheet(true);
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
googleSheetIntegration,
@@ -153,11 +137,13 @@ export const AddIntegrationModal = ({
});
if (!spreadsheetNameResponse?.data) {
showErrorMessageToast(spreadsheetNameResponse);
return;
const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse);
throw new Error(errorMessage);
}
const spreadsheetName = spreadsheetNameResponse.data;
setIsLinkingSheet(true);
integrationData.spreadsheetId = spreadsheetId;
integrationData.spreadsheetName = spreadsheetName;
integrationData.surveyId = selectedSurvey.id;
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import {
TIntegrationGoogleSheets,
@@ -8,11 +8,9 @@ import {
} from "@formbricks/types/integration/google-sheet";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { validateGoogleSheetsConnectionAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions";
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration";
import { authorize } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/google";
import googleSheetLogo from "@/images/googleSheetsLogo.png";
import { GOOGLE_SHEET_INTEGRATION_INVALID_GRANT } from "@/lib/googleSheet/constants";
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
import { AddIntegrationModal } from "./AddIntegrationModal";
@@ -37,23 +35,10 @@ export const GoogleSheetWrapper = ({
googleSheetIntegration ? googleSheetIntegration.config?.key : false
);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [showReconnectButton, setShowReconnectButton] = useState<boolean>(false);
const [selectedIntegration, setSelectedIntegration] = useState<
(TIntegrationGoogleSheetsConfigData & { index: number }) | null
>(null);
const validateConnection = useCallback(async () => {
if (!isConnected || !googleSheetIntegration) return;
const response = await validateGoogleSheetsConnectionAction({ environmentId: environment.id });
if (response?.serverError === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) {
setShowReconnectButton(true);
}
}, [environment.id, isConnected, googleSheetIntegration]);
useEffect(() => {
validateConnection();
}, [validateConnection]);
const handleGoogleAuthorization = async () => {
authorize(environment.id, webAppUrl).then((url: string) => {
if (url) {
@@ -79,8 +64,6 @@ export const GoogleSheetWrapper = ({
setOpenAddIntegrationModal={setIsModalOpen}
setIsConnected={setIsConnected}
setSelectedIntegration={setSelectedIntegration}
showReconnectButton={showReconnectButton}
handleGoogleAuthorization={handleGoogleAuthorization}
locale={locale}
/>
</>
@@ -1,6 +1,6 @@
"use client";
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
import { Trash2Icon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -12,19 +12,15 @@ import { TUserLocale } from "@formbricks/types/user";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertButton, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptyState } from "@/modules/ui/components/empty-state";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
interface ManageIntegrationProps {
googleSheetIntegration: TIntegrationGoogleSheets;
setOpenAddIntegrationModal: (v: boolean) => void;
setIsConnected: (v: boolean) => void;
setSelectedIntegration: (v: (TIntegrationGoogleSheetsConfigData & { index: number }) | null) => void;
showReconnectButton: boolean;
handleGoogleAuthorization: () => void;
locale: TUserLocale;
}
@@ -33,8 +29,6 @@ export const ManageIntegration = ({
setOpenAddIntegrationModal,
setIsConnected,
setSelectedIntegration,
showReconnectButton,
handleGoogleAuthorization,
locale,
}: ManageIntegrationProps) => {
const { t } = useTranslation();
@@ -74,17 +68,7 @@ export const ManageIntegration = ({
return (
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
{showReconnectButton && (
<Alert variant="warning" size="small" className="mb-4 w-full">
<AlertDescription>
{t("environments.integrations.google_sheets.reconnect_button_description")}
</AlertDescription>
<AlertButton onClick={handleGoogleAuthorization}>
{t("environments.integrations.google_sheets.reconnect_button")}
</AlertButton>
</Alert>
)}
<div className="flex w-full justify-end space-x-2">
<div className="flex w-full justify-end">
<div className="mr-6 flex items-center">
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">
@@ -93,19 +77,6 @@ export const ManageIntegration = ({
})}
</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" onClick={handleGoogleAuthorization}>
<RefreshCcwIcon className="mr-2 h-4 w-4" />
{t("environments.integrations.google_sheets.reconnect_button")}
</Button>
</TooltipTrigger>
<TooltipContent>
{t("environments.integrations.google_sheets.reconnect_button_tooltip")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
onClick={() => {
setSelectedIntegration(null);
@@ -10,7 +10,7 @@ const Loading = () => {
<div className="mt-6 p-6">
<GoBackButton />
<div className="mb-6 text-right">
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
{t("environments.integrations.google_sheets.link_new_sheet")}
</Button>
</div>
@@ -51,7 +51,7 @@ const Loading = () => {
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
@@ -10,7 +10,7 @@ const Loading = () => {
<div className="mt-6 p-6">
<GoBackButton />
<div className="mb-6 text-right">
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
{t("environments.integrations.notion.link_database")}
</Button>
</div>
@@ -48,7 +48,7 @@ const Loading = () => {
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
@@ -21,7 +21,6 @@ import { getElementsFromBlocks } from "@/lib/survey/utils";
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
import { parseRecallInfo } from "@/lib/utils/recall";
import { truncateText } from "@/lib/utils/strings";
import { resolveStorageUrlAuto } from "@/modules/storage/utils";
const convertMetaObjectToString = (metadata: TResponseMeta): string => {
let result: string[] = [];
@@ -257,16 +256,10 @@ const processElementResponse = (
const selectedChoiceIds = responseValue as string[];
return element.choices
.filter((choice) => selectedChoiceIds.includes(choice.id))
.map((choice) => resolveStorageUrlAuto(choice.imageUrl))
.map((choice) => choice.imageUrl)
.join("\n");
}
if (element.type === TSurveyElementTypeEnum.FileUpload && Array.isArray(responseValue)) {
return responseValue
.map((url) => (typeof url === "string" ? resolveStorageUrlAuto(url) : url))
.join("; ");
}
return processResponseData(responseValue);
};
@@ -375,7 +368,7 @@ const buildNotionPayloadProperties = (
responses[resp] = (pictureElement as any)?.choices
.filter((choice) => selectedChoiceIds.includes(choice.id))
.map((choice) => resolveStorageUrlAuto(choice.imageUrl));
.map((choice) => choice.imageUrl);
}
});
@@ -18,7 +18,6 @@ import { convertDatesInObject } from "@/lib/time";
import { queueAuditEvent } from "@/modules/ee/audit-logs/lib/handler";
import { TAuditStatus, UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
import { sendResponseFinishedEmail } from "@/modules/email";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
import { sendFollowUpsForResponse } from "@/modules/survey/follow-ups/lib/follow-ups";
import { FollowUpSendError } from "@/modules/survey/follow-ups/types/follow-up";
import { handleIntegrations } from "./lib/handleIntegrations";
@@ -31,10 +30,7 @@ export const POST = async (request: Request) => {
}
const jsonInput = await request.json();
const convertedJsonInput = convertDatesInObject(
jsonInput,
new Set(["contactAttributes", "variables", "data", "meta"])
);
const convertedJsonInput = convertDatesInObject(jsonInput);
const inputValidation = ZPipelineInput.safeParse(convertedJsonInput);
@@ -96,15 +92,12 @@ export const POST = async (request: Request) => {
]);
};
const resolvedResponseData = resolveStorageUrlsInObject(response.data);
const webhookPromises = webhooks.map((webhook) => {
const body = JSON.stringify({
webhookId: webhook.id,
event,
data: {
...response,
data: resolvedResponseData,
survey: {
title: survey.name,
type: survey.type,
+26 -45
View File
@@ -1,6 +1,4 @@
import { google } from "googleapis";
import { getServerSession } from "next-auth";
import { TIntegrationGoogleSheetsConfig } from "@formbricks/types/integration/google-sheet";
import { responses } from "@/app/lib/api/response";
import {
GOOGLE_SHEETS_CLIENT_ID,
@@ -8,29 +6,18 @@ import {
GOOGLE_SHEETS_REDIRECT_URL,
WEBAPP_URL,
} from "@/lib/constants";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { createOrUpdateIntegration } from "@/lib/integration/service";
export const GET = async (req: Request) => {
const url = new URL(req.url);
const environmentId = url.searchParams.get("state");
const code = url.searchParams.get("code");
const url = req.url;
const queryParams = new URLSearchParams(url.split("?")[1]); // Split the URL and get the query parameters
const environmentId = queryParams.get("state"); // Get the value of the 'state' parameter
const code = queryParams.get("code");
if (!environmentId) {
return responses.badRequestResponse("Invalid environmentId");
}
const session = await getServerSession(authOptions);
if (!session) {
return responses.notAuthenticatedResponse();
}
const canUserAccessEnvironment = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!canUserAccessEnvironment) {
return responses.unauthorizedResponse();
}
if (code && typeof code !== "string") {
return responses.badRequestResponse("`code` must be a string");
}
@@ -43,39 +30,33 @@ export const GET = async (req: Request) => {
if (!redirect_uri) return responses.internalServerErrorResponse("Google redirect url is missing");
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
if (!code) {
return Response.redirect(
`${WEBAPP_URL}/environments/${environmentId}/workspace/integrations/google-sheets`
);
let key;
let userEmail;
if (code) {
const token = await oAuth2Client.getToken(code);
key = token.res?.data;
// Set credentials using the provided token
oAuth2Client.setCredentials({
access_token: key.access_token,
});
// Fetch user's email
const oauth2 = google.oauth2({
auth: oAuth2Client,
version: "v2",
});
const userInfo = await oauth2.userinfo.get();
userEmail = userInfo.data.email;
}
const token = await oAuth2Client.getToken(code);
const key = token.res?.data;
if (!key) {
return Response.redirect(
`${WEBAPP_URL}/environments/${environmentId}/workspace/integrations/google-sheets`
);
}
oAuth2Client.setCredentials({ access_token: key.access_token });
const oauth2 = google.oauth2({ auth: oAuth2Client, version: "v2" });
const userInfo = await oauth2.userinfo.get();
const userEmail = userInfo.data.email;
if (!userEmail) {
return responses.internalServerErrorResponse("Failed to get user email");
}
const integrationType = "googleSheets" as const;
const existingIntegration = await getIntegrationByType(environmentId, integrationType);
const existingConfig = existingIntegration?.config as TIntegrationGoogleSheetsConfig;
const googleSheetIntegration = {
type: integrationType,
type: "googleSheets" as "googleSheets",
environment: environmentId,
config: {
key,
data: existingConfig?.data ?? [],
data: [],
email: userEmail,
},
};
@@ -0,0 +1,180 @@
// Deprecated: This api route is deprecated now and will be removed in the future.
// Deprecated: This is currently only being used for the older react native SDKs. Please upgrade to the latest SDKs.
import { NextRequest, userAgent } from "next/server";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { TJsPeopleUserIdInput, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getContactByUserId } from "@/app/api/v1/client/[environmentId]/app/sync/lib/contact";
import { getSyncSurveys } from "@/app/api/v1/client/[environmentId]/app/sync/lib/survey";
import { replaceAttributeRecall } from "@/app/api/v1/client/[environmentId]/app/sync/lib/utils";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { getActionClasses } from "@/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getEnvironment, updateEnvironment } from "@/lib/environment/service";
import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
const validateInput = (
environmentId: string,
userId: string
): { isValid: true; data: TJsPeopleUserIdInput } | { isValid: false; error: Response } => {
const inputValidation = ZJsPeopleUserIdInput.safeParse({ environmentId, userId });
if (!inputValidation.success) {
return {
isValid: false,
error: responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
),
};
}
return { isValid: true, data: inputValidation.data };
};
const checkResponseLimit = async (environmentId: string): Promise<boolean> => {
if (!IS_FORMBRICKS_CLOUD) return false;
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
logger.error({ environmentId }, "Organization does not exist");
// fail closed if the organization does not exist
return true;
}
const currentResponseCount = await getMonthlyOrganizationResponseCount(organization.id);
const monthlyResponseLimit = organization.billing.limits.monthly.responses;
const isLimitReached = monthlyResponseLimit !== null && currentResponseCount >= monthlyResponseLimit;
return isLimitReached;
};
export const OPTIONS = async (): Promise<Response> => {
return responses.successResponse({}, true);
};
export const GET = withV1ApiWrapper({
handler: async ({
req,
props,
}: {
req: NextRequest;
props: { params: Promise<{ environmentId: string; userId: string }> };
}) => {
const params = await props.params;
try {
const { device } = userAgent(req);
// validate using zod
const validation = validateInput(params.environmentId, params.userId);
if (!validation.isValid) {
return { response: validation.error };
}
const { environmentId, userId } = validation.data;
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment does not exist");
}
const project = await getProjectByEnvironmentId(environmentId);
if (!project) {
throw new Error("Project not found");
}
if (!environment.appSetupCompleted) {
await updateEnvironment(environment.id, { appSetupCompleted: true });
}
// check organization subscriptions and response limits
const isAppSurveyResponseLimitReached = await checkResponseLimit(environmentId);
let contact = await getContactByUserId(environmentId, userId);
if (!contact) {
contact = await prisma.contact.create({
data: {
attributes: {
create: {
attributeKey: {
connect: {
key_environmentId: {
key: "userId",
environmentId,
},
},
},
value: userId,
},
},
environment: { connect: { id: environmentId } },
},
select: {
id: true,
attributes: { select: { attributeKey: { select: { key: true } }, value: true } },
},
});
}
const contactAttributes = contact.attributes.reduce((acc, attribute) => {
acc[attribute.attributeKey.key] = attribute.value;
return acc;
}, {}) as Record<string, string>;
const [surveys, actionClasses] = await Promise.all([
getSyncSurveys(
environmentId,
contact.id,
contactAttributes,
device.type === "mobile" ? "phone" : "desktop"
),
getActionClasses(environmentId),
]);
const updatedProject: any = {
...project,
brandColor: project.styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor,
...(project.styling.highlightBorderColor?.light && {
highlightBorderColor: project.styling.highlightBorderColor.light,
}),
};
const language = contactAttributes["language"];
// Scenario 1: Multi language and updated trigger action classes supported.
// Use the surveys as they are.
let transformedSurveys: TSurvey[] = surveys;
// creating state object
let state = {
surveys: !isAppSurveyResponseLimitReached
? transformedSurveys.map((survey) => replaceAttributeRecall(survey, contactAttributes))
: [],
actionClasses,
language,
project: updatedProject,
};
return {
response: responses.successResponse({ ...state }, true),
};
} catch (error) {
logger.error({ error, url: req.url }, "Error in GET /api/v1/client/[environmentId]/app/sync/[userId]");
return {
response: responses.internalServerErrorResponse(
"Unable to handle the request: " + error.message,
true
),
};
}
},
});
@@ -0,0 +1,83 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TContact } from "@/modules/ee/contacts/types/contact";
import { getContactByUserId } from "./contact";
// Mock prisma
vi.mock("@formbricks/database", () => ({
prisma: {
contact: {
findFirst: vi.fn(),
},
},
}));
const environmentId = "test-environment-id";
const userId = "test-user-id";
const contactId = "test-contact-id";
const contactMock: Partial<TContact> & {
attributes: { value: string; attributeKey: { key: string } }[];
} = {
id: contactId,
attributes: [
{ attributeKey: { key: "userId" }, value: userId },
{ attributeKey: { key: "email" }, value: "test@example.com" },
],
};
describe("getContactByUserId", () => {
afterEach(() => {
vi.resetAllMocks();
});
test("should return contact if found", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(contactMock as any);
const contact = await getContactByUserId(environmentId, userId);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
attributes: {
some: {
attributeKey: {
key: "userId",
environmentId,
},
value: userId,
},
},
},
select: {
id: true,
attributes: { select: { attributeKey: { select: { key: true } }, value: true } },
},
});
expect(contact).toEqual(contactMock);
});
test("should return null if contact not found", async () => {
vi.mocked(prisma.contact.findFirst).mockResolvedValue(null);
const contact = await getContactByUserId(environmentId, userId);
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
where: {
attributes: {
some: {
attributeKey: {
key: "userId",
environmentId,
},
value: userId,
},
},
},
select: {
id: true,
attributes: { select: { attributeKey: { select: { key: true } }, value: true } },
},
});
expect(contact).toBeNull();
});
});
@@ -0,0 +1,42 @@
import "server-only";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
export const getContactByUserId = reactCache(
async (
environmentId: string,
userId: string
): Promise<{
attributes: {
value: string;
attributeKey: {
key: string;
};
}[];
id: string;
} | null> => {
const contact = await prisma.contact.findFirst({
where: {
attributes: {
some: {
attributeKey: {
key: "userId",
environmentId,
},
value: userId,
},
},
},
select: {
id: true,
attributes: { select: { attributeKey: { select: { key: true } }, value: true } },
},
});
if (!contact) {
return null;
}
return contact;
}
);
@@ -0,0 +1,323 @@
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 } from "@formbricks/types/errors";
import { TProject } from "@formbricks/types/project";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getSurveys } from "@/lib/survey/service";
import { anySurveyHasFilters } from "@/lib/survey/utils";
import { diffInDays } from "@/lib/utils/datetime";
import { evaluateSegment } from "@/modules/ee/contacts/segments/lib/segments";
import { getSyncSurveys } from "./survey";
vi.mock("@/lib/project/service", () => ({
getProjectByEnvironmentId: vi.fn(),
}));
vi.mock("@/lib/survey/service", () => ({
getSurveys: vi.fn(),
}));
vi.mock("@/lib/survey/utils", () => ({
anySurveyHasFilters: vi.fn(),
}));
vi.mock("@/lib/utils/datetime", () => ({
diffInDays: vi.fn(),
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
}));
vi.mock("@/modules/ee/contacts/segments/lib/segments", () => ({
evaluateSegment: vi.fn(),
}));
vi.mock("@formbricks/database", () => ({
prisma: {
display: {
findMany: vi.fn(),
},
response: {
findMany: vi.fn(),
},
},
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
const environmentId = "test-env-id";
const contactId = "test-contact-id";
const contactAttributes = { userId: "user1", email: "test@example.com" };
const deviceType = "desktop";
const mockProject = {
id: "proj1",
name: "Test Project",
createdAt: new Date(),
updatedAt: new Date(),
organizationId: "org1",
environments: [],
recontactDays: 10,
inAppSurveyBranding: true,
linkSurveyBranding: true,
placement: "bottomRight",
clickOutsideClose: true,
overlay: "none",
languages: [],
} as unknown as TProject;
const baseSurvey: TSurvey = {
id: "survey1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Test Survey 1",
environmentId: environmentId,
type: "app",
status: "inProgress",
questions: [],
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
delay: 0,
displayPercentage: null,
autoComplete: null,
segment: null,
surveyClosedMessage: null,
singleUse: null,
styling: null,
pin: null,
displayLimit: null,
welcomeCard: { enabled: false } as TSurvey["welcomeCard"],
endings: [],
triggers: [],
languages: [],
variables: [],
hiddenFields: { enabled: false },
createdBy: null,
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
showLanguageSwitch: false,
isBackButtonHidden: false,
followUps: [],
recaptcha: { enabled: false, threshold: 0.5 },
};
// Helper function to create mock display objects
const createMockDisplay = (id: string, surveyId: string, contactId: string, createdAt?: Date) => ({
id,
createdAt: createdAt || new Date(),
updatedAt: new Date(),
surveyId,
contactId,
responseId: null,
status: null,
});
// Helper function to create mock response objects
const createMockResponse = (id: string, surveyId: string, contactId: string, createdAt?: Date) => ({
id,
createdAt: createdAt || new Date(),
updatedAt: new Date(),
finished: false,
surveyId,
contactId,
endingId: null,
data: {},
variables: {},
ttc: {},
meta: {},
contactAttributes: null,
singleUseId: null,
language: null,
displayId: null,
});
describe("getSyncSurveys", () => {
beforeEach(() => {
vi.mocked(getProjectByEnvironmentId).mockResolvedValue(mockProject);
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
vi.mocked(prisma.response.findMany).mockResolvedValue([]);
vi.mocked(anySurveyHasFilters).mockReturnValue(false);
vi.mocked(evaluateSegment).mockResolvedValue(true);
vi.mocked(diffInDays).mockReturnValue(100); // Assume enough days passed
});
afterEach(() => {
vi.resetAllMocks();
});
test("should throw error if product not found", async () => {
vi.mocked(getProjectByEnvironmentId).mockResolvedValue(null);
await expect(getSyncSurveys(environmentId, contactId, contactAttributes, deviceType)).rejects.toThrow(
"Project not found"
);
});
test("should return empty array if no surveys found", async () => {
vi.mocked(getSurveys).mockResolvedValue([]);
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]);
});
test("should return empty array if no 'app' type surveys in progress", async () => {
const surveys: TSurvey[] = [
{ ...baseSurvey, id: "s1", type: "link", status: "inProgress" },
{ ...baseSurvey, id: "s2", type: "app", status: "paused" },
];
vi.mocked(getSurveys).mockResolvedValue(surveys);
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]);
});
test("should filter by displayOption 'displayOnce'", async () => {
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "displayOnce" }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(prisma.display.findMany).mockResolvedValue([createMockDisplay("d1", "s1", contactId)]); // Already displayed
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]);
vi.mocked(prisma.display.findMany).mockResolvedValue([]); // Not displayed yet
const result2 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result2).toEqual(surveys);
});
test("should filter by displayOption 'displayMultiple'", async () => {
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "displayMultiple" }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(prisma.response.findMany).mockResolvedValue([createMockResponse("r1", "s1", contactId)]); // Already responded
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]);
vi.mocked(prisma.response.findMany).mockResolvedValue([]); // Not responded yet
const result2 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result2).toEqual(surveys);
});
test("should filter by displayOption 'displaySome'", async () => {
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "displaySome", displayLimit: 2 }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(prisma.display.findMany).mockResolvedValue([
createMockDisplay("d1", "s1", contactId),
createMockDisplay("d2", "s1", contactId),
]); // Display limit reached
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]);
vi.mocked(prisma.display.findMany).mockResolvedValue([createMockDisplay("d1", "s1", contactId)]); // Within limit
const result2 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result2).toEqual(surveys);
// Test with response already submitted
vi.mocked(prisma.response.findMany).mockResolvedValue([createMockResponse("r1", "s1", contactId)]);
const result3 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result3).toEqual([]);
});
test("should not filter by displayOption 'respondMultiple'", async () => {
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", displayOption: "respondMultiple" }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(prisma.display.findMany).mockResolvedValue([createMockDisplay("d1", "s1", contactId)]);
vi.mocked(prisma.response.findMany).mockResolvedValue([createMockResponse("r1", "s1", contactId)]);
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual(surveys);
});
test("should filter by product recontactDays if survey recontactDays is null", async () => {
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", recontactDays: null }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
const displayDate = new Date();
vi.mocked(prisma.display.findMany).mockResolvedValue([
createMockDisplay("d1", "s2", contactId, displayDate), // Display for another survey
]);
vi.mocked(diffInDays).mockReturnValue(5); // Not enough days passed (product.recontactDays = 10)
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]);
expect(diffInDays).toHaveBeenCalledWith(expect.any(Date), displayDate);
vi.mocked(diffInDays).mockReturnValue(15); // Enough days passed
const result2 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result2).toEqual(surveys);
});
test("should return surveys if no segment filters exist", async () => {
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1" }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(anySurveyHasFilters).mockReturnValue(false);
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual(surveys);
expect(evaluateSegment).not.toHaveBeenCalled();
});
test("should evaluate segment filters if they exist", async () => {
const segment = { id: "seg1", filters: [{}] } as TSegment; // Mock filter structure
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", segment }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(anySurveyHasFilters).mockReturnValue(true);
// Case 1: Segment evaluation matches
vi.mocked(evaluateSegment).mockResolvedValue(true);
const result1 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result1).toEqual(surveys);
expect(evaluateSegment).toHaveBeenCalledWith(
{
attributes: contactAttributes,
deviceType,
environmentId,
contactId,
userId: contactAttributes.userId,
},
segment.filters
);
// Case 2: Segment evaluation does not match
vi.mocked(evaluateSegment).mockResolvedValue(false);
const result2 = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result2).toEqual([]);
});
test("should handle Prisma errors", async () => {
const prismaError = new Prisma.PrismaClientKnownRequestError("Test Prisma Error", {
code: "P2025",
clientVersion: "test",
});
vi.mocked(getSurveys).mockRejectedValue(prismaError);
await expect(getSyncSurveys(environmentId, contactId, contactAttributes, deviceType)).rejects.toThrow(
DatabaseError
);
expect(logger.error).toHaveBeenCalledWith(prismaError);
});
test("should handle general errors", async () => {
const generalError = new Error("Something went wrong");
vi.mocked(getSurveys).mockRejectedValue(generalError);
await expect(getSyncSurveys(environmentId, contactId, contactAttributes, deviceType)).rejects.toThrow(
generalError
);
});
test("should throw ResourceNotFoundError if resolved surveys are null after filtering", async () => {
const segment = { id: "seg1", filters: [{}] } as TSegment; // Mock filter structure
const surveys: TSurvey[] = [{ ...baseSurvey, id: "s1", segment }];
vi.mocked(getSurveys).mockResolvedValue(surveys);
vi.mocked(anySurveyHasFilters).mockReturnValue(true);
vi.mocked(evaluateSegment).mockResolvedValue(false); // Ensure all surveys are filtered out
// This scenario is tricky to force directly as the code checks `if (!surveys)` before returning.
// However, if `Promise.all` somehow resolved to null/undefined (highly unlikely), it should throw.
// We can simulate this by mocking `Promise.all` if needed, but the current code structure makes this hard to test.
// Let's assume the filter logic works correctly and test the intended path.
const result = await getSyncSurveys(environmentId, contactId, contactAttributes, deviceType);
expect(result).toEqual([]); // Expect empty array, not an error in this case.
});
});
@@ -0,0 +1,148 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getSurveys } from "@/lib/survey/service";
import { anySurveyHasFilters } from "@/lib/survey/utils";
import { diffInDays } from "@/lib/utils/datetime";
import { validateInputs } from "@/lib/utils/validate";
import { evaluateSegment } from "@/modules/ee/contacts/segments/lib/segments";
export const getSyncSurveys = reactCache(
async (
environmentId: string,
contactId: string,
contactAttributes: Record<string, string | number>,
deviceType: "phone" | "desktop" = "desktop"
): Promise<TSurvey[]> => {
validateInputs([environmentId, ZId]);
try {
const project = await getProjectByEnvironmentId(environmentId);
if (!project) {
throw new Error("Project not found");
}
let surveys = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "app");
// if no surveys are left, return an empty array
if (surveys.length === 0) {
return [];
}
const displays = await prisma.display.findMany({
where: {
contactId,
},
});
const responses = await prisma.response.findMany({
where: {
contactId,
},
});
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
switch (survey.displayOption) {
case "respondMultiple":
return true;
case "displayOnce":
return displays.filter((display) => display.surveyId === survey.id).length === 0;
case "displayMultiple":
if (!responses) return true;
else {
return responses.filter((response) => response.surveyId === survey.id).length === 0;
}
case "displaySome":
if (survey.displayLimit === null) {
return true;
}
if (responses && responses.filter((response) => response.surveyId === survey.id).length !== 0) {
return false;
}
return displays.filter((display) => display.surveyId === survey.id).length < survey.displayLimit;
default:
throw Error("Invalid displayOption");
}
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = surveys.filter((survey) => {
if (!latestDisplay) {
return true;
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return true;
}
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (project.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= project.recontactDays;
} else {
return true;
}
});
// if no surveys are left, return an empty array
if (surveys.length === 0) {
return [];
}
// if no surveys have segment filters, return the surveys
if (!anySurveyHasFilters(surveys)) {
return surveys;
}
// the surveys now have segment filters, so we need to evaluate them
const surveyPromises = surveys.map(async (survey) => {
const { segment } = survey;
// if the survey has no segment, or the segment has no filters, we return the survey
if (!segment || !segment.filters?.length) {
return survey;
}
// Evaluate the segment filters
const result = await evaluateSegment(
{
attributes: contactAttributes ?? {},
deviceType,
environmentId,
contactId,
userId: String(contactAttributes.userId),
},
segment.filters
);
return result ? survey : null;
});
const resolvedSurveys = await Promise.all(surveyPromises);
surveys = resolvedSurveys.filter((survey) => !!survey) as TSurvey[];
if (!surveys) {
throw new ResourceNotFoundError("Survey", environmentId);
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
logger.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
}
);
@@ -0,0 +1,245 @@
import { describe, expect, test, vi } from "vitest";
import { TAttributes } from "@formbricks/types/attributes";
import { TLanguage } from "@formbricks/types/project";
import {
TSurvey,
TSurveyEnding,
TSurveyQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { parseRecallInfo } from "@/lib/utils/recall";
import { replaceAttributeRecall } from "./utils";
vi.mock("@/lib/utils/recall", () => ({
parseRecallInfo: vi.fn((text, attributes) => {
const recallPattern = /recall:([a-zA-Z0-9_-]+)/;
const match = text.match(recallPattern);
if (match && match[1]) {
const recallKey = match[1];
const attributeValue = attributes[recallKey];
if (attributeValue !== undefined) {
return text.replace(recallPattern, `parsed-${attributeValue}`);
}
}
return text; // Return original text if no match or attribute not found
}),
}));
const baseSurvey: TSurvey = {
id: "survey1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Test Survey",
environmentId: "env1",
type: "app",
status: "inProgress",
questions: [],
endings: [],
welcomeCard: { enabled: false } as TSurvey["welcomeCard"],
languages: [
{ language: { id: "lang1", code: "en" } as unknown as TLanguage, default: true, enabled: true },
],
triggers: [],
recontactDays: null,
displayLimit: null,
singleUse: null,
styling: null,
surveyClosedMessage: null,
hiddenFields: { enabled: false },
variables: [],
createdBy: null,
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
showLanguageSwitch: false,
isBackButtonHidden: false,
followUps: [],
recaptcha: { enabled: false, threshold: 0.5 },
displayOption: "displayOnce",
autoClose: null,
delay: 0,
displayPercentage: null,
autoComplete: null,
segment: null,
pin: null,
metadata: {},
};
const attributes: TAttributes = {
name: "John Doe",
email: "john.doe@example.com",
plan: "premium",
};
describe("replaceAttributeRecall", () => {
test("should replace recall info in question headlines and subheaders", () => {
const surveyWithRecall: TSurvey = {
...baseSurvey,
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Hello recall:name!" },
subheader: { default: "Your email is recall:email" },
required: true,
buttonLabel: { default: "Next" },
placeholder: { default: "Type here..." },
longAnswer: false,
logic: [],
} as unknown as TSurveyQuestion,
],
};
const result = replaceAttributeRecall(surveyWithRecall, attributes);
expect(result.questions[0].headline.default).toBe("Hello parsed-John Doe!");
expect(result.questions[0].subheader?.default).toBe("Your email is parsed-john.doe@example.com");
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Hello recall:name!", attributes);
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Your email is recall:email", attributes);
});
test("should replace recall info in welcome card headline", () => {
const surveyWithRecall: TSurvey = {
...baseSurvey,
welcomeCard: {
enabled: true,
headline: { default: "Welcome, recall:name!" },
subheader: { default: "<p>Some content</p>" },
buttonLabel: { default: "Start" },
timeToFinish: false,
showResponseCount: false,
},
};
const result = replaceAttributeRecall(surveyWithRecall, attributes);
expect(result.welcomeCard.headline?.default).toBe("Welcome, parsed-John Doe!");
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Welcome, recall:name!", attributes);
});
test("should replace recall info in end screen headlines and subheaders", () => {
const surveyWithRecall: TSurvey = {
...baseSurvey,
endings: [
{
type: "endScreen",
headline: { default: "Thank you, recall:name!" },
subheader: { default: "Your plan: recall:plan" },
buttonLabel: { default: "Finish" },
buttonLink: "https://example.com",
} as unknown as TSurveyEnding,
],
};
const result = replaceAttributeRecall(surveyWithRecall, attributes);
expect(result.endings[0].type).toBe("endScreen");
if (result.endings[0].type === "endScreen") {
expect(result.endings[0].headline?.default).toBe("Thank you, parsed-John Doe!");
expect(result.endings[0].subheader?.default).toBe("Your plan: parsed-premium");
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Thank you, recall:name!", attributes);
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Your plan: recall:plan", attributes);
}
});
test("should handle multiple languages", () => {
const surveyMultiLang: TSurvey = {
...baseSurvey,
languages: [
{ language: { id: "lang1", code: "en" } as unknown as TLanguage, default: true, enabled: true },
{ language: { id: "lang2", code: "es" } as unknown as TLanguage, default: false, enabled: true },
],
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Hello recall:name!", es: "Hola recall:name!" },
required: true,
buttonLabel: { default: "Next", es: "Siguiente" },
placeholder: { default: "Type here...", es: "Escribe aquí..." },
longAnswer: false,
logic: [],
} as unknown as TSurveyQuestion,
],
};
const result = replaceAttributeRecall(surveyMultiLang, attributes);
expect(result.questions[0].headline.default).toBe("Hello parsed-John Doe!");
expect(result.questions[0].headline.es).toBe("Hola parsed-John Doe!");
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Hello recall:name!", attributes);
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Hola recall:name!", attributes);
});
test("should not replace if recall key is not in attributes", () => {
const surveyWithRecall: TSurvey = {
...baseSurvey,
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Your company: recall:company" },
required: true,
buttonLabel: { default: "Next" },
placeholder: { default: "Type here..." },
longAnswer: false,
logic: [],
} as unknown as TSurveyQuestion,
],
};
const result = replaceAttributeRecall(surveyWithRecall, attributes);
expect(result.questions[0].headline.default).toBe("Your company: recall:company");
expect(vi.mocked(parseRecallInfo)).toHaveBeenCalledWith("Your company: recall:company", attributes);
});
test("should handle surveys with no recall information", async () => {
const surveyNoRecall: TSurvey = {
...baseSurvey,
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Just a regular question" },
required: true,
buttonLabel: { default: "Next" },
placeholder: { default: "Type here..." },
longAnswer: false,
logic: [],
} as unknown as TSurveyQuestion,
],
welcomeCard: {
enabled: true,
headline: { default: "Welcome!" },
subheader: { default: "<p>Some content</p>" },
buttonLabel: { default: "Start" },
timeToFinish: false,
showResponseCount: false,
},
endings: [
{
type: "endScreen",
headline: { default: "Thank you!" },
buttonLabel: { default: "Finish" },
} as unknown as TSurveyEnding,
],
};
const parseRecallInfoSpy = vi.spyOn(await import("@/lib/utils/recall"), "parseRecallInfo");
const result = replaceAttributeRecall(surveyNoRecall, attributes);
expect(result).toEqual(surveyNoRecall); // Should be unchanged
expect(parseRecallInfoSpy).not.toHaveBeenCalled();
parseRecallInfoSpy.mockRestore();
});
test("should handle surveys with empty questions, endings, or disabled welcome card", async () => {
const surveyEmpty: TSurvey = {
...baseSurvey,
questions: [],
endings: [],
welcomeCard: { enabled: false } as TSurvey["welcomeCard"],
};
const parseRecallInfoSpy = vi.spyOn(await import("@/lib/utils/recall"), "parseRecallInfo");
const result = replaceAttributeRecall(surveyEmpty, attributes);
expect(result).toEqual(surveyEmpty);
expect(parseRecallInfoSpy).not.toHaveBeenCalled();
parseRecallInfoSpy.mockRestore();
});
});
@@ -0,0 +1,55 @@
import { TAttributes } from "@formbricks/types/attributes";
import { TSurvey } from "@formbricks/types/surveys/types";
import { parseRecallInfo } from "@/lib/utils/recall";
export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes): TSurvey => {
const surveyTemp = structuredClone(survey);
const languages = surveyTemp.languages
.map((surveyLanguage) => {
if (surveyLanguage.default) {
return "default";
}
if (surveyLanguage.enabled) {
return surveyLanguage.language.code;
}
return null;
})
.filter((language): language is string => language !== null);
surveyTemp.questions.forEach((question) => {
languages.forEach((language) => {
if (question.headline[language]?.includes("recall:")) {
question.headline[language] = parseRecallInfo(question.headline[language], attributes);
}
if (question.subheader && question.subheader[language]?.includes("recall:")) {
question.subheader[language] = parseRecallInfo(question.subheader[language], attributes);
}
});
});
if (surveyTemp.welcomeCard.enabled && surveyTemp.welcomeCard.headline) {
languages.forEach((language) => {
if (surveyTemp.welcomeCard.headline && surveyTemp.welcomeCard.headline[language]?.includes("recall:")) {
surveyTemp.welcomeCard.headline[language] = parseRecallInfo(
surveyTemp.welcomeCard.headline[language],
attributes
);
}
});
}
surveyTemp.endings.forEach((ending) => {
if (ending.type === "endScreen") {
languages.forEach((language) => {
if (ending.headline && ending.headline[language]?.includes("recall:")) {
ending.headline[language] = parseRecallInfo(ending.headline[language], attributes);
if (ending.subheader && ending.subheader[language]?.includes("recall:")) {
ending.subheader[language] = parseRecallInfo(ending.subheader[language], attributes);
}
}
});
}
});
return surveyTemp;
};
@@ -0,0 +1,6 @@
import {
OPTIONS,
PUT,
} from "@/modules/ee/contacts/api/v1/client/[environmentId]/contacts/[userId]/attributes/route";
export { OPTIONS, PUT };
@@ -10,7 +10,6 @@ import {
TJsEnvironmentStateSurvey,
} from "@formbricks/types/js";
import { validateInputs } from "@/lib/utils/validate";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
import { transformPrismaSurvey } from "@/modules/survey/lib/utils";
/**
@@ -178,14 +177,14 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
overlay: environmentData.project.overlay,
placement: environmentData.project.placement,
inAppSurveyBranding: environmentData.project.inAppSurveyBranding,
styling: resolveStorageUrlsInObject(environmentData.project.styling),
styling: environmentData.project.styling,
},
},
organization: {
id: environmentData.project.organization.id,
billing: environmentData.project.organization.billing,
},
surveys: resolveStorageUrlsInObject(transformedSurveys),
surveys: transformedSurveys,
actionClasses: environmentData.actionClasses as TJsEnvironmentStateActionClass[],
};
} catch (error) {
@@ -0,0 +1,6 @@
import {
GET,
OPTIONS,
} from "@/modules/ee/contacts/api/v1/client/[environmentId]/identify/contacts/[userId]/route";
export { GET, OPTIONS };
@@ -44,10 +44,13 @@ const validateResponse = (
...responseUpdateInput.data,
};
const isFinished = responseUpdateInput.finished ?? false;
const validationErrors = validateResponseData(
survey.blocks,
mergedData,
responseUpdateInput.language ?? response.language ?? "en",
isFinished,
survey.questions
);
@@ -41,6 +41,7 @@ const validateResponse = (responseInputData: TResponseInput, survey: TSurvey) =>
survey.blocks,
responseInputData.data,
responseInputData.language ?? "en",
responseInputData.finished,
survey.questions
);
+108 -106
View File
@@ -6,138 +6,140 @@ export const GET = async (req: NextRequest) => {
let brandColor = req.nextUrl.searchParams.get("brandColor");
return new ImageResponse(
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
alignItems: "center",
backgroundColor: brandColor ? brandColor + "BF" : "#0000BFBF", // /75 opacity is approximately BF in hex
borderRadius: "0.75rem",
}}>
(
<div
style={{
display: "flex",
flexDirection: "column",
width: "80%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3.25rem",
position: "absolute",
left: "3rem",
top: "0.75rem",
opacity: 0.2,
transform: "rotate(356deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "84%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3rem",
position: "absolute",
top: "1.25rem",
left: "3.25rem",
borderWidth: "2px",
opacity: 0.6,
transform: "rotate(357deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "85%",
height: "67%",
width: "100%",
height: "100%",
alignItems: "center",
backgroundColor: "white",
backgroundColor: brandColor ? brandColor + "BF" : "#0000BFBF", // /75 opacity is approximately BF in hex
borderRadius: "0.75rem",
marginTop: "2rem",
position: "absolute",
top: "2.3rem",
left: "3.5rem",
transform: "rotate(360deg)",
}}>
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
justifyContent: "space-between",
}}>
<div
style={{
display: "flex",
flexDirection: "column",
width: "80%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3.25rem",
position: "absolute",
left: "3rem",
top: "0.75rem",
opacity: 0.2,
transform: "rotate(356deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "84%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3rem",
position: "absolute",
top: "1.25rem",
left: "3.25rem",
borderWidth: "2px",
opacity: 0.6,
transform: "rotate(357deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "85%",
height: "67%",
alignItems: "center",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "2rem",
position: "absolute",
top: "2.3rem",
left: "3.5rem",
transform: "rotate(360deg)",
}}>
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
<div
style={{
display: "flex",
flexDirection: "column",
paddingLeft: "2rem",
paddingRight: "2rem",
width: "100%",
justifyContent: "space-between",
}}>
<h2
<div
style={{
display: "flex",
flexDirection: "column",
fontSize: "2rem",
fontWeight: "700",
letterSpacing: "-0.025em",
color: "#0f172a",
textAlign: "left",
marginTop: "3.75rem",
paddingLeft: "2rem",
paddingRight: "2rem",
}}>
{name}
</h2>
<h2
style={{
display: "flex",
flexDirection: "column",
fontSize: "2rem",
fontWeight: "700",
letterSpacing: "-0.025em",
color: "#0f172a",
textAlign: "left",
marginTop: "3.75rem",
}}>
{name}
</h2>
</div>
</div>
</div>
<div style={{ display: "flex", justifyContent: "flex-end", marginRight: "2.5rem" }}>
<div
style={{
display: "flex",
borderRadius: "1rem",
position: "absolute",
right: "-0.5rem",
marginTop: "0.5rem",
}}>
<div
content=""
style={{
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
height: "4.5rem",
width: "9.5rem",
opacity: 0.5,
}}></div>
</div>
<div
style={{
display: "flex",
borderRadius: "1rem",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
}}>
<div style={{ display: "flex", justifyContent: "flex-end", marginRight: "2.5rem" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
fontSize: "1.5rem",
color: "white",
height: "4.5rem",
width: "9.5rem",
borderRadius: "1rem",
position: "absolute",
right: "-0.5rem",
marginTop: "0.5rem",
}}>
Begin!
<div
content=""
style={{
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
height: "4.5rem",
width: "9.5rem",
opacity: 0.5,
}}></div>
</div>
<div
style={{
display: "flex",
borderRadius: "1rem",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
}}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
fontSize: "1.5rem",
color: "white",
height: "4.5rem",
width: "9.5rem",
}}>
Begin!
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
),
{
width: 800,
height: 400,
@@ -1,7 +1,7 @@
import { NextRequest } from "next/server";
import { TIntegrationNotionConfigData, TIntegrationNotionInput } from "@formbricks/types/integration/notion";
import { responses } from "@/app/lib/api/response";
import { TSessionAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import {
ENCRYPTION_KEY,
NOTION_OAUTH_CLIENT_ID,
@@ -10,17 +10,10 @@ import {
WEBAPP_URL,
} from "@/lib/constants";
import { symmetricEncrypt } from "@/lib/crypto";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service";
export const GET = withV1ApiWrapper({
handler: async ({
req,
authentication,
}: {
req: NextRequest;
authentication: NonNullable<TSessionAuthentication>;
}) => {
handler: async ({ req }: { req: NextRequest }) => {
const url = req.url;
const queryParams = new URLSearchParams(url.split("?")[1]); // Split the URL and get the query parameters
const environmentId = queryParams.get("state"); // Get the value of the 'state' parameter
@@ -33,13 +26,6 @@ export const GET = withV1ApiWrapper({
};
}
const canUserAccessEnvironment = await hasUserEnvironmentAccess(authentication.user.id, environmentId);
if (!canUserAccessEnvironment) {
return {
response: responses.unauthorizedResponse(),
};
}
if (code && typeof code !== "string") {
return {
response: responses.badRequestResponse("`code` must be a string"),
@@ -5,19 +5,12 @@ import {
TIntegrationSlackCredential,
} from "@formbricks/types/integration/slack";
import { responses } from "@/app/lib/api/response";
import { TSessionAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, WEBAPP_URL } from "@/lib/constants";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service";
export const GET = withV1ApiWrapper({
handler: async ({
req,
authentication,
}: {
req: NextRequest;
authentication: NonNullable<TSessionAuthentication>;
}) => {
handler: async ({ req }: { req: NextRequest }) => {
const url = req.url;
const queryParams = new URLSearchParams(url.split("?")[1]); // Split the URL and get the query parameters
const environmentId = queryParams.get("state"); // Get the value of the 'state' parameter
@@ -30,13 +23,6 @@ export const GET = withV1ApiWrapper({
};
}
const canUserAccessEnvironment = await hasUserEnvironmentAccess(authentication.user.id, environmentId);
if (!canUserAccessEnvironment) {
return {
response: responses.unauthorizedResponse(),
};
}
if (code && typeof code !== "string") {
return {
response: responses.badRequestResponse("`code` must be a string"),
@@ -56,7 +42,6 @@ export const GET = withV1ApiWrapper({
code,
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
redirect_uri: SLACK_REDIRECT_URI,
};
const formBody: string[] = [];
for (const property in formData) {
@@ -10,7 +10,7 @@ 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 { resolveStorageUrlsInObject, validateFileUploads } from "@/modules/storage/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import { updateResponseWithQuotaEvaluation } from "./lib/response";
async function fetchAndAuthorizeResponse(
@@ -57,10 +57,7 @@ export const GET = withV1ApiWrapper({
}
return {
response: responses.successResponse({
...result.response,
data: resolveStorageUrlsInObject(result.response.data),
}),
response: responses.successResponse(result.response),
};
} catch (error) {
return {
@@ -149,6 +146,7 @@ export const PUT = withV1ApiWrapper({
result.survey.blocks,
responseUpdate.data,
responseUpdate.language ?? "en",
responseUpdate.finished,
result.survey.questions
);
@@ -192,7 +190,7 @@ export const PUT = withV1ApiWrapper({
}
return {
response: responses.successResponse({ ...updated, data: resolveStorageUrlsInObject(updated.data) }),
response: responses.successResponse(updated),
};
} catch (error) {
return {
@@ -9,7 +9,7 @@ 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 { resolveStorageUrlsInObject, validateFileUploads } from "@/modules/storage/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import {
createResponseWithQuotaEvaluation,
getResponses,
@@ -54,9 +54,7 @@ export const GET = withV1ApiWrapper({
allResponses.push(...environmentResponses);
}
return {
response: responses.successResponse(
allResponses.map((r) => ({ ...r, data: resolveStorageUrlsInObject(r.data) }))
),
response: responses.successResponse(allResponses),
};
} catch (error) {
if (error instanceof DatabaseError) {
@@ -157,6 +155,7 @@ export const POST = withV1ApiWrapper({
surveyResult.survey.blocks,
responseInput.data,
responseInput.language ?? "en",
responseInput.finished,
surveyResult.survey.questions
);
@@ -16,7 +16,6 @@ import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getSurvey, updateSurvey } from "@/lib/survey/service";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
const fetchAndAuthorizeSurvey = async (
surveyId: string,
@@ -59,18 +58,16 @@ export const GET = withV1ApiWrapper({
if (shouldTransformToQuestions) {
return {
response: responses.successResponse(
resolveStorageUrlsInObject({
...result.survey,
questions: transformBlocksToQuestions(result.survey.blocks, result.survey.endings),
blocks: [],
})
),
response: responses.successResponse({
...result.survey,
questions: transformBlocksToQuestions(result.survey.blocks, result.survey.endings),
blocks: [],
}),
};
}
return {
response: responses.successResponse(resolveStorageUrlsInObject(result.survey)),
response: responses.successResponse(result.survey),
};
} catch (error) {
return {
@@ -205,12 +202,12 @@ export const PUT = withV1ApiWrapper({
};
return {
response: responses.successResponse(resolveStorageUrlsInObject(surveyWithQuestions)),
response: responses.successResponse(surveyWithQuestions),
};
}
return {
response: responses.successResponse(resolveStorageUrlsInObject(updatedSurvey)),
response: responses.successResponse(updatedSurvey),
};
} catch (error) {
return {
@@ -14,7 +14,6 @@ import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { createSurvey } from "@/lib/survey/service";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
import { getSurveys } from "./lib/surveys";
export const GET = withV1ApiWrapper({
@@ -56,7 +55,7 @@ export const GET = withV1ApiWrapper({
});
return {
response: responses.successResponse(resolveStorageUrlsInObject(surveysWithQuestions)),
response: responses.successResponse(surveysWithQuestions),
};
} catch (error) {
if (error instanceof DatabaseError) {
@@ -0,0 +1,6 @@
import {
OPTIONS,
PUT,
} from "@/modules/ee/contacts/api/v1/client/[environmentId]/contacts/[userId]/attributes/route";
export { OPTIONS, PUT };
@@ -0,0 +1,6 @@
import {
GET,
OPTIONS,
} from "@/modules/ee/contacts/api/v1/client/[environmentId]/identify/contacts/[userId]/route";
export { GET, OPTIONS };
@@ -112,6 +112,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
survey.blocks,
responseInputData.data,
responseInputData.language ?? "en",
responseInputData.finished,
survey.questions
);
+87 -31
View File
@@ -68,6 +68,7 @@ vi.mock("@/app/middleware/endpoint-validator", async () => {
isClientSideApiRoute: vi.fn().mockReturnValue({ isClientSideApi: false, isRateLimited: true }),
isManagementApiRoute: vi.fn().mockReturnValue({ isManagementApi: false, authenticationMethod: "apiKey" }),
isIntegrationRoute: vi.fn().mockReturnValue(false),
isSyncWithUserIdentificationEndpoint: vi.fn().mockReturnValue(null),
};
});
@@ -81,6 +82,7 @@ vi.mock("@/modules/core/rate-limit/rate-limit-configs", () => ({
api: {
client: { windowMs: 60000, max: 100 },
v1: { windowMs: 60000, max: 1000 },
syncUserIdentification: { windowMs: 60000, max: 50 },
},
},
}));
@@ -131,11 +133,13 @@ describe("withV1ApiWrapper", () => {
});
test("logs and audits on error response with API key authentication", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -183,11 +187,13 @@ describe("withV1ApiWrapper", () => {
});
test("does not log Sentry if not 500", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -229,11 +235,13 @@ describe("withV1ApiWrapper", () => {
});
test("logs and audits on thrown error", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -285,11 +293,13 @@ describe("withV1ApiWrapper", () => {
});
test("does not log on success response but still audits", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -339,11 +349,13 @@ describe("withV1ApiWrapper", () => {
REDIS_URL: "redis://localhost:6379",
}));
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { withV1ApiWrapper } = await import("./with-api-logging");
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
@@ -366,8 +378,9 @@ describe("withV1ApiWrapper", () => {
});
test("handles client-side API routes without authentication", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
@@ -399,8 +412,9 @@ describe("withV1ApiWrapper", () => {
});
test("returns authentication error for non-client routes without auth", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { authenticateRequest } = await import("@/app/api/v1/auth");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -423,8 +437,9 @@ describe("withV1ApiWrapper", () => {
test("handles rate limiting errors", async () => {
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { authenticateRequest } = await import("@/app/api/v1/auth");
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
@@ -448,12 +463,53 @@ describe("withV1ApiWrapper", () => {
expect(handler).not.toHaveBeenCalled();
});
test("skips audit log creation when no action/targetType provided", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
test("handles sync user identification rate limiting", async () => {
const { applyRateLimit, applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
const {
isClientSideApiRoute,
isManagementApiRoute,
isIntegrationRoute,
isSyncWithUserIdentificationEndpoint,
} = await import("@/app/middleware/endpoint-validator");
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: false,
authenticationMethod: AuthenticationMethod.None,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(isSyncWithUserIdentificationEndpoint).mockReturnValue({
userId: "user-123",
environmentId: "env-123",
});
vi.mocked(authenticateRequest).mockResolvedValue(null);
vi.mocked(applyIPRateLimit).mockResolvedValue(undefined);
const rateLimitError = new Error("Sync rate limit exceeded");
rateLimitError.message = "Sync rate limit exceeded";
vi.mocked(applyRateLimit).mockRejectedValue(rateLimitError);
const handler = vi.fn();
const req = createMockRequest({ url: "/api/v1/client/env-123/app/sync/user-123" });
const { withV1ApiWrapper } = await import("./with-api-logging");
const wrapped = withV1ApiWrapper({ handler });
const res = await wrapped(req, undefined);
expect(res.status).toBe(429);
expect(applyRateLimit).toHaveBeenCalledWith(
expect.objectContaining({ windowMs: 60000, max: 50 }),
"user-123"
);
});
test("skips audit log creation when no action/targetType provided", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
+19 -5
View File
@@ -10,6 +10,7 @@ import {
isClientSideApiRoute,
isIntegrationRoute,
isManagementApiRoute,
isSyncWithUserIdentificationEndpoint,
} from "@/app/middleware/endpoint-validator";
import { AUDIT_LOG_ENABLED, IS_PRODUCTION, SENTRY_DSN } from "@/lib/constants";
import { authOptions } from "@/modules/auth/lib/authOptions";
@@ -47,16 +48,23 @@ enum ApiV1RouteTypeEnum {
}
/**
* Apply client-side API rate limiting (IP-based)
* Apply client-side API rate limiting (IP-based or sync-specific)
*/
const applyClientRateLimit = async (customRateLimitConfig?: TRateLimitConfig): Promise<void> => {
await applyIPRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.client);
const applyClientRateLimit = async (url: string, customRateLimitConfig?: TRateLimitConfig): Promise<void> => {
const syncEndpoint = isSyncWithUserIdentificationEndpoint(url);
if (syncEndpoint) {
const syncRateLimitConfig = rateLimitConfigs.api.syncUserIdentification;
await applyRateLimit(syncRateLimitConfig, syncEndpoint.userId);
} else {
await applyIPRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.client);
}
};
/**
* Handle rate limiting based on authentication and API type
*/
const handleRateLimiting = async (
url: string,
authentication: TApiV1Authentication,
routeType: ApiV1RouteTypeEnum,
customRateLimitConfig?: TRateLimitConfig
@@ -76,7 +84,7 @@ const handleRateLimiting = async (
}
if (routeType === ApiV1RouteTypeEnum.Client) {
await applyClientRateLimit(customRateLimitConfig);
await applyClientRateLimit(url, customRateLimitConfig);
}
} catch (error) {
return responses.tooManyRequestsResponse(error.message);
@@ -247,6 +255,7 @@ const getRouteType = (
* Features:
* - Performs authentication once and passes result to handler
* - Applies API key-based rate limiting with differentiated limits for client vs management APIs
* - Includes additional sync user identification rate limiting for client-side sync endpoints
* - Sets userId and organizationId in audit log automatically when audit logging is enabled
* - System and Sentry logs are always called for non-success responses
* - Uses function overloads to provide type safety without requiring type guards
@@ -319,7 +328,12 @@ export const withV1ApiWrapper: {
// === Rate Limiting ===
if (isRateLimited) {
const rateLimitResponse = await handleRateLimiting(authentication, routeType, customRateLimitConfig);
const rateLimitResponse = await handleRateLimiting(
req.nextUrl.pathname,
authentication,
routeType,
customRateLimitConfig
);
if (rateLimitResponse) return rateLimitResponse;
}
-11
View File
@@ -4854,17 +4854,6 @@ export const previewSurvey = (projectName: string, t: TFunction): TSurvey => {
}),
isDraft: true,
},
{
...buildOpenTextElement({
id: "preview-open-text-01",
headline: t("templates.preview_survey_question_open_text_headline"),
subheader: t("templates.preview_survey_question_open_text_subheader"),
placeholder: t("templates.preview_survey_question_open_text_placeholder"),
inputType: "text",
required: false,
}),
isDraft: true,
},
],
buttonLabel: createI18nString(t("templates.next"), []),
backButtonLabel: createI18nString(t("templates.preview_survey_question_2_back_button_label"), []),
@@ -8,6 +8,7 @@ import {
isManagementApiRoute,
isPublicDomainRoute,
isRouteAllowedForDomain,
isSyncWithUserIdentificationEndpoint,
} from "./endpoint-validator";
describe("endpoint-validator", () => {
@@ -257,7 +258,6 @@ describe("endpoint-validator", () => {
expect(isAuthProtectedRoute("/api/v1/client/test")).toBe(false);
expect(isAuthProtectedRoute("/")).toBe(false);
expect(isAuthProtectedRoute("/s/survey123")).toBe(false);
expect(isAuthProtectedRoute("/p/pretty-url")).toBe(false);
expect(isAuthProtectedRoute("/c/jwt-token")).toBe(false);
expect(isAuthProtectedRoute("/health")).toBe(false);
});
@@ -270,6 +270,58 @@ describe("endpoint-validator", () => {
});
});
describe("isSyncWithUserIdentificationEndpoint", () => {
test("should return environmentId and userId for valid sync URLs", () => {
const result1 = isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/app/sync/user456");
expect(result1).toEqual({
environmentId: "env123",
userId: "user456",
});
const result2 = isSyncWithUserIdentificationEndpoint("/api/v1/client/abc-123/app/sync/xyz-789");
expect(result2).toEqual({
environmentId: "abc-123",
userId: "xyz-789",
});
const result3 = isSyncWithUserIdentificationEndpoint(
"/api/v1/client/env_123_test/app/sync/user_456_test"
);
expect(result3).toEqual({
environmentId: "env_123_test",
userId: "user_456_test",
});
});
test("should handle optional trailing slash", () => {
// Test both with and without trailing slash
const result1 = isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/app/sync/user456");
expect(result1).toEqual({
environmentId: "env123",
userId: "user456",
});
const result2 = isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/app/sync/user456/");
expect(result2).toEqual({
environmentId: "env123",
userId: "user456",
});
});
test("should return false for invalid sync URLs", () => {
expect(isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/app/sync")).toBe(false);
expect(isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/something")).toBe(false);
expect(isSyncWithUserIdentificationEndpoint("/api/something")).toBe(false);
expect(isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/app/other/user456")).toBe(false);
expect(isSyncWithUserIdentificationEndpoint("/api/v2/client/env123/app/sync/user456")).toBe(false); // only v1 supported
});
test("should handle empty or malformed IDs", () => {
expect(isSyncWithUserIdentificationEndpoint("/api/v1/client//app/sync/user456")).toBe(false);
expect(isSyncWithUserIdentificationEndpoint("/api/v1/client/env123/app/sync/")).toBe(false);
});
});
describe("isPublicDomainRoute", () => {
test("should return true for health endpoint", () => {
expect(isPublicDomainRoute("/health")).toBe(true);
@@ -314,19 +366,6 @@ describe("endpoint-validator", () => {
expect(isPublicDomainRoute("/contact/token")).toBe(false);
});
test("should return true for pretty URL survey routes", () => {
expect(isPublicDomainRoute("/p/pretty123")).toBe(true);
expect(isPublicDomainRoute("/p/pretty-name-with-dashes")).toBe(true);
expect(isPublicDomainRoute("/p/survey_id_with_underscores")).toBe(true);
expect(isPublicDomainRoute("/p/abc123def456")).toBe(true);
});
test("should return false for malformed pretty URL survey routes", () => {
expect(isPublicDomainRoute("/p/")).toBe(false);
expect(isPublicDomainRoute("/p")).toBe(false);
expect(isPublicDomainRoute("/pretty/123")).toBe(false);
});
test("should return true for client API routes", () => {
expect(isPublicDomainRoute("/api/v1/client/something")).toBe(true);
expect(isPublicDomainRoute("/api/v2/client/other")).toBe(true);
@@ -389,8 +428,6 @@ describe("endpoint-validator", () => {
expect(isAdminDomainRoute("/s/survey-id-with-dashes")).toBe(false);
expect(isAdminDomainRoute("/c/jwt-token")).toBe(false);
expect(isAdminDomainRoute("/c/very-long-jwt-token-123")).toBe(false);
expect(isAdminDomainRoute("/p/pretty123")).toBe(false);
expect(isAdminDomainRoute("/p/pretty-name-with-dashes")).toBe(false);
expect(isAdminDomainRoute("/api/v1/client/test")).toBe(false);
expect(isAdminDomainRoute("/api/v2/client/other")).toBe(false);
});
@@ -406,7 +443,6 @@ describe("endpoint-validator", () => {
test("should allow public routes on public domain", () => {
expect(isRouteAllowedForDomain("/s/survey123", true)).toBe(true);
expect(isRouteAllowedForDomain("/c/jwt-token", true)).toBe(true);
expect(isRouteAllowedForDomain("/p/pretty123", true)).toBe(true);
expect(isRouteAllowedForDomain("/api/v1/client/test", true)).toBe(true);
expect(isRouteAllowedForDomain("/api/v2/client/other", true)).toBe(true);
expect(isRouteAllowedForDomain("/health", true)).toBe(true);
@@ -443,8 +479,6 @@ describe("endpoint-validator", () => {
expect(isRouteAllowedForDomain("/s/survey-id-with-dashes", false)).toBe(false);
expect(isRouteAllowedForDomain("/c/jwt-token", false)).toBe(false);
expect(isRouteAllowedForDomain("/c/very-long-jwt-token-123", false)).toBe(false);
expect(isRouteAllowedForDomain("/p/pretty123", false)).toBe(false);
expect(isRouteAllowedForDomain("/p/pretty-name-with-dashes", false)).toBe(false);
expect(isRouteAllowedForDomain("/api/v1/client/test", false)).toBe(false);
expect(isRouteAllowedForDomain("/api/v2/client/other", false)).toBe(false);
});
@@ -459,8 +493,6 @@ describe("endpoint-validator", () => {
test("should handle paths with query parameters and fragments", () => {
expect(isRouteAllowedForDomain("/s/survey123?param=value", true)).toBe(true);
expect(isRouteAllowedForDomain("/s/survey123#section", true)).toBe(true);
expect(isRouteAllowedForDomain("/p/pretty123?param=value", true)).toBe(true);
expect(isRouteAllowedForDomain("/p/pretty123#section", true)).toBe(true);
expect(isRouteAllowedForDomain("/environments/123?tab=settings", true)).toBe(false);
expect(isRouteAllowedForDomain("/environments/123?tab=settings", false)).toBe(true);
});
@@ -471,7 +503,6 @@ describe("endpoint-validator", () => {
describe("URL parsing edge cases", () => {
test("should handle paths with query parameters", () => {
expect(isPublicDomainRoute("/s/survey123?param=value&other=test")).toBe(true);
expect(isPublicDomainRoute("/p/pretty123?param=value&other=test")).toBe(true);
expect(isPublicDomainRoute("/api/v1/client/test?query=data")).toBe(true);
expect(isPublicDomainRoute("/environments/123?tab=settings")).toBe(false);
expect(isAuthProtectedRoute("/environments/123?tab=overview")).toBe(true);
@@ -480,14 +511,12 @@ describe("endpoint-validator", () => {
test("should handle paths with fragments", () => {
expect(isPublicDomainRoute("/s/survey123#section")).toBe(true);
expect(isPublicDomainRoute("/c/jwt-token#top")).toBe(true);
expect(isPublicDomainRoute("/p/pretty123#section")).toBe(true);
expect(isPublicDomainRoute("/environments/123#overview")).toBe(false);
expect(isAuthProtectedRoute("/organizations/456#settings")).toBe(true);
});
test("should handle trailing slashes", () => {
expect(isPublicDomainRoute("/s/survey123/")).toBe(true);
expect(isPublicDomainRoute("/p/pretty123/")).toBe(true);
expect(isPublicDomainRoute("/api/v1/client/test/")).toBe(true);
expect(isManagementApiRoute("/api/v1/management/test/")).toEqual({
isManagementApi: true,
@@ -502,9 +531,6 @@ describe("endpoint-validator", () => {
expect(isPublicDomainRoute("/s/survey123/preview")).toBe(true);
expect(isPublicDomainRoute("/s/survey123/embed")).toBe(true);
expect(isPublicDomainRoute("/s/survey123/thank-you")).toBe(true);
expect(isPublicDomainRoute("/p/pretty123/preview")).toBe(true);
expect(isPublicDomainRoute("/p/pretty123/embed")).toBe(true);
expect(isPublicDomainRoute("/p/pretty123/thank-you")).toBe(true);
});
test("should handle nested client API routes", () => {
@@ -556,7 +582,12 @@ describe("endpoint-validator", () => {
test("should handle special characters in survey IDs", () => {
expect(isPublicDomainRoute("/s/survey-123_test.v2")).toBe(true);
expect(isPublicDomainRoute("/c/jwt.token.with.dots")).toBe(true);
expect(isPublicDomainRoute("/p/pretty-123_test.v2")).toBe(true);
expect(
isSyncWithUserIdentificationEndpoint("/api/v1/client/env-123_test/app/sync/user-456_test")
).toEqual({
environmentId: "env-123_test",
userId: "user-456_test",
});
});
});
@@ -564,7 +595,6 @@ describe("endpoint-validator", () => {
test("should properly validate malicious or injection-like URLs", () => {
// SQL injection-like attempts
expect(isPublicDomainRoute("/s/'; DROP TABLE users; --")).toBe(true); // Still valid survey ID format
expect(isPublicDomainRoute("/p/'; DROP TABLE users; --")).toBe(true);
expect(isManagementApiRoute("/api/v1/management/'; DROP TABLE users; --")).toEqual({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
@@ -572,12 +602,10 @@ describe("endpoint-validator", () => {
// Path traversal attempts
expect(isPublicDomainRoute("/s/../../../etc/passwd")).toBe(true); // Still matches pattern
expect(isPublicDomainRoute("/p/../../../etc/passwd")).toBe(true);
expect(isAuthProtectedRoute("/environments/../../../etc/passwd")).toBe(true);
// XSS-like attempts
expect(isPublicDomainRoute("/s/<script>alert('xss')</script>")).toBe(true);
expect(isPublicDomainRoute("/p/<script>alert('xss')</script>")).toBe(true);
expect(isClientSideApiRoute("/api/v1/client/<script>alert('xss')</script>")).toEqual({
isClientSideApi: true,
isRateLimited: true,
@@ -587,7 +615,6 @@ describe("endpoint-validator", () => {
test("should handle URL encoding", () => {
expect(isPublicDomainRoute("/s/survey%20123")).toBe(true);
expect(isPublicDomainRoute("/c/jwt%2Etoken")).toBe(true);
expect(isPublicDomainRoute("/p/pretty%20123")).toBe(true);
expect(isAuthProtectedRoute("/environments%2F123")).toBe(true);
expect(isManagementApiRoute("/api/v1/management/test%20route")).toEqual({
isManagementApi: true,
@@ -601,6 +628,15 @@ describe("endpoint-validator", () => {
const longSurveyId = "a".repeat(1000);
const longPath = `s/${longSurveyId}`;
expect(isPublicDomainRoute(`/${longPath}`)).toBe(true);
const longEnvironmentId = "env" + "a".repeat(1000);
const longUserId = "user" + "b".repeat(1000);
expect(
isSyncWithUserIdentificationEndpoint(`/api/v1/client/${longEnvironmentId}/app/sync/${longUserId}`)
).toEqual({
environmentId: longEnvironmentId,
userId: longUserId,
});
});
test("should handle empty and minimal inputs", () => {
@@ -615,6 +651,7 @@ describe("endpoint-validator", () => {
});
expect(isIntegrationRoute("")).toBe(false);
expect(isAuthProtectedRoute("")).toBe(false);
expect(isSyncWithUserIdentificationEndpoint("")).toBe(false);
});
});
@@ -623,7 +660,6 @@ describe("endpoint-validator", () => {
// These should not match due to case sensitivity
expect(isPublicDomainRoute("/S/survey123")).toBe(false);
expect(isPublicDomainRoute("/C/jwt-token")).toBe(false);
expect(isPublicDomainRoute("/P/pretty123")).toBe(false);
expect(isClientSideApiRoute("/API/V1/CLIENT/test")).toEqual({
isClientSideApi: false,
isRateLimited: true,
@@ -43,6 +43,14 @@ export const isAuthProtectedRoute = (url: string): boolean => {
return protectedRoutes.some((route) => url.startsWith(route));
};
export const isSyncWithUserIdentificationEndpoint = (
url: string
): { environmentId: string; userId: string } | false => {
const regex = /\/api\/v1\/client\/(?<environmentId>[^/]+)\/app\/sync\/(?<userId>[^/]+)/;
const match = url.match(regex);
return match ? { environmentId: match.groups!.environmentId, userId: match.groups!.userId } : false;
};
/**
* Check if the route should be accessible on the public domain (PUBLIC_URL)
* Uses whitelist approach - only explicitly allowed routes are accessible
-1
View File
@@ -7,7 +7,6 @@ const PUBLIC_ROUTES = {
SURVEY_ROUTES: [
/^\/s\/[^/]+/, // /s/[surveyId] - survey pages
/^\/c\/[^/]+/, // /c/[jwt] - contact survey pages
/^\/p\/[^/]+/, // /p/[prettyUrl] - pretty URL pages
],
// API routes accessible from public domain
+9 -100
View File
@@ -161,10 +161,8 @@ checksums:
common/customer_success: 2b0c99a5f57e1d16cf0a998f9bb116c4
common/dark_overlay: 173e84b526414dbc70dbf9737e443b60
common/date: 56f41c5d30a76295bb087b20b7bee4c3
common/days: c95fe8aedde21a0b5653dbd0b3c58b48
common/default: d9c6dc5c412fe94143dfd1d332ec81d4
common/delete: 8bcf303dd10a645b5baacb02b47d72c9
common/delete_what: 718ddfcc1dec7f3e8b67856fba838267
common/description: e17686a22ffad04cc7bb70524ed4478b
common/dev_env: e650911d5e19ba256358e0cda154c005
common/development: 85211dbb918bda7a6e87649dcfc1b17a
@@ -180,8 +178,6 @@ checksums:
common/download: 56b7d0834952b39ee394b44bd8179178
common/draft: e8a92958ad300aacfe46c2bf6644927e
common/duplicate: 27756566785c2b8463e21582c4bb619b
common/duplicate_copy: 68d2201918610ca87c2914b61dc8010f
common/duplicate_copy_number: 083cfffd294672043dcbcc4c3dfeac6a
common/e_commerce: b9584e7d0449a6d1b0c182d7ff14061e
common/edit: eee7f39ff90b18852afc1671f21fbaa9
common/email: e7f34943a0c2fb849db1839ff6ef5cb5
@@ -194,16 +190,13 @@ checksums:
common/error: 3c95bcb32c2104b99a46f5b3dd015248
common/error_component_description: fa9eee04f864c3fe6e6681f716caa015
common/error_component_title: ae68fa341a143aaa13a5ea30dd57a63e
common/error_loading_data: aaeffbfe4a2c2145442a57de524494be
common/error_rate_limit_description: 37791a33a947204662ee9c6544e90f51
common/error_rate_limit_title: 23ac9419e267e610e1bfd38e1dc35dc0
common/expand_rows: b6e06327cb8718dfd6651720843e4dad
common/failed_to_copy_to_clipboard: de836a7d628d36c832809252f188f784
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4
common/filter: 626325a05e4c8800f7ede7012b0cadaf
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/first_name: cf040a5d6a9fd696be400380cc99f54b
common/follow_these: 3a730b242bb17a3f95e01bf0dae86885
common/formbricks_version: d9967c797f3e49ca0cae78bc0ebd19cb
common/full_name: f45991923345e8322c9ff8cd6b7e2b16
@@ -216,7 +209,6 @@ checksums:
common/hidden_field: 3ed5c58d0ed359e558cdf7bd33606d2d
common/hidden_fields: 3de6cfd308293a826cb8679fd1d49972
common/hide_column: 23ce94db148f2d8e4a0923defead6cf1
common/id: c8886d38aeea2ed5f785aba4fc96784b
common/image: 048ba7a239de0fbd883ade8558415830
common/images: 9305827c28694866f49db42b4c51831f
common/import: 348b8ab981de5b7f1fca6d7302263bbd
@@ -234,7 +226,6 @@ checksums:
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
common/label: a5c71bf158481233f8215dbd38cc196b
common/language: 277fd1a41cc237a437cd1d5e4a80463b
common/last_name: 2c9a7de7738ca007ba9023c385149c26
common/learn_more: e598091d132f890c37a6d4ed94f6d794
common/license_expired: 7af13535e320e4197989472c01387d2c
common/light_overlay: 0499907ea7b8405f4267b117998b5a78
@@ -257,11 +248,9 @@ checksums:
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
common/mobile_overlay_surveys_look_good: d85169e86077738b9837647bf6d1c7d2
common/mobile_overlay_title: 42f52259b7527989fb3a3240f5352a8b
common/months: da74749fbe80394fa0f72973d7b0964a
common/move_down: 4f4de55743043355ad4a839aff2c48ff
common/move_up: 69f25b205c677abdb26cbb69d97cd10b
common/multiple_languages: 7d8ddd4b40d32fcd7bd6f7bac6485b1f
common/my_product: ad022177062f9ef6e9acf33b13e889aa
common/name: 9368b5a047572b6051f334af5aa76819
common/new: 126d036fae5fb6b629728ecb97e6195b
common/new_version_available: 399ddfc4232712e18ddab2587356b3dc
@@ -377,7 +366,6 @@ checksums:
common/status: 4e1fcce15854d824919b4a582c697c90
common/step_by_step_manual: 2894a07952a4fd11d98d5d8f1088690c
common/storage_not_configured: b0c3e339f6d71f23fdd189e7bcb076f6
common/string: 4ddccc1974775ed7357f9beaf9361cec
common/styling: 240fc91eb03c52d46b137f82e7aec2a1
common/submit: 7c91ef5f747eea9f77a9c4f23e19fb2e
common/summary: 13eb7b8a239fb4702dfdaee69100a220
@@ -410,7 +398,6 @@ checksums:
common/top_right: 241f95c923846911aaf13af6109333e5
common/try_again: 33dd8820e743e35a66e6977f69e9d3b5
common/type: f04471a7ddac844b9ad145eb9911ef75
common/unknown_survey: dd8f6985e17ccf19fac1776e18b2c498
common/unlock_more_workspaces_with_a_higher_plan: fe1590075b855bb4306c9388b65143b0
common/update: 079fc039262fd31b10532929685c2d1b
common/updated: 8aa8ff2dc2977ca4b269e80a513100b4
@@ -434,7 +421,6 @@ checksums:
common/website_and_app_connection: 60fea5cff5bddb4db3c8a1b0a2f9ec63
common/website_app_survey: 258579927ed3955dcc8e1cbd7f0df17f
common/website_survey: 17513d25a07b6361768a15ec622b021b
common/weeks: 545de30df4f44d3f6d1d344af6a10815
common/welcome_card: 76081ebd5b2e35da9b0f080323704ae7
common/workspace_configuration: d0a5812d6a97d7724d565b1017c34387
common/workspace_created_successfully: bf401ae83da954f1db48724e2a8e40f1
@@ -445,7 +431,6 @@ checksums:
common/workspace_not_found: 038fb0aaf3570610f4377b9eaed13752
common/workspace_permission_not_found: e94bdff8af51175c5767714f82bb4833
common/workspaces: 8ba082a84aa35cf851af1cf874b853e2
common/years: eb4f5fdd2b320bf13e200fd6a6c1abff
common/you: db2a4a796b70cc1430d1b21f6ffb6dcb
common/you_are_downgraded_to_the_community_edition: e3ae56502ff787109cae0997519f628e
common/you_are_not_authorized_to_perform_this_action: 1b3255ab740582ddff016a399f8bf302
@@ -608,45 +593,28 @@ checksums:
environments/contacts/attribute_updated_successfully: 0e64422156c29940cd4dab2f9d1f40b2
environments/contacts/attribute_value: 34b0eaa85808b15cbc4be94c64d0146b
environments/contacts/attribute_value_placeholder: 90fb17015de807031304d7a650a6cb8c
environments/contacts/attributes_msg_attribute_limit_exceeded: a6c430860f307f9cc90c449f96a1284f
environments/contacts/attributes_msg_attribute_type_validation_error: ed177ce83bd174ed6be7e889664f93a1
environments/contacts/attributes_msg_email_already_exists: a3ea1265e3db885f53d0e589aecf6260
environments/contacts/attributes_msg_email_or_userid_required: 3be0e745cd3500c9a23bad2e25ad3147
environments/contacts/attributes_msg_new_attribute_created: c4c7b27523058f43b70411d7aa6510e5
environments/contacts/attributes_msg_userid_already_exists: d2d95ece4b06507be18c9ba240b0a26b
environments/contacts/contact_deleted_successfully: c5b64a42a50e055f9e27ec49e20e03fa
environments/contacts/contact_not_found: 045396f0b13fafd43612a286263737c0
environments/contacts/contacts_table_refresh: 6a959475991dd4ab28ad881bae569a09
environments/contacts/contacts_table_refresh_success: 40951396e88e5c8fdafa0b3bb4fadca8
environments/contacts/create_attribute: 87320615901f95b4f35ee83c290a3a6c
environments/contacts/create_key: 0d385c354af8963acbe35cd646710f86
environments/contacts/create_new_attribute: c17d407dacd0b90f360f9f5e899d662f
environments/contacts/create_new_attribute_description: cc19d76bb6940537bbe3461191f25d26
environments/contacts/custom_attributes: fffc7722742d1291b102dc737cf2fc9e
environments/contacts/data_type: 1ea127ba2c18d0d91fb0361cc6747e2b
environments/contacts/data_type_cannot_be_changed: 22603f6193fdac3784eeef8315df70de
environments/contacts/data_type_description: 800bf4935df15e6cb14269e1b60c506e
environments/contacts/date_value_required: e0e75b75ae4e8c02f03284954756adc9
environments/contacts/delete_attribute_confirmation: 01d99b89eb3d27ff468d0db1b4aeb394
environments/contacts/delete_contact_confirmation: 2d45579e0bb4bc40fb1ee75b43c0e7a4
environments/contacts/delete_contact_confirmation_with_quotas: d3d17f13ae46ce04c126c82bf01299ac
environments/contacts/displays: fcc4527002bd045021882be463b8ac72
environments/contacts/edit_attribute: 92a83c96a5d850e7d39002e8fd5898f4
environments/contacts/edit_attribute_description: 073a3084bb2f3b34ed1320ed1cd6db3c
environments/contacts/edit_attribute_values: 44e4e7a661cc1b59200bb07c710072a7
environments/contacts/edit_attribute_values_description: 21593dfaf4cad965ffc17685bc005509
environments/contacts/edit_attributes: a5c3b540441d34b4c0b7faab8f0f0c89
environments/contacts/edit_attributes_success: 39f93b1a6f1605bc5951f4da5847bb22
environments/contacts/generate_personal_link: 9ac0865f6876d40fe858f94eae781eb8
environments/contacts/generate_personal_link_description: b9dbaf9e2d8362505b7e3cfa40f415a6
environments/contacts/invalid_csv_column_names: dcb8534e7d4c00b9ea7bdaf389f72328
environments/contacts/invalid_date_format: 5bad9730ac5a5bacd0792098f712b1c4
environments/contacts/invalid_number_format: bd0422507385f671c3046730a6febc64
environments/contacts/no_activity_yet: f88897ac05afd6bf8af0d4834ad24ffc
environments/contacts/no_published_link_surveys_available: 9c1abc5b21aba827443cdf87dd6c8bfe
environments/contacts/no_published_surveys: bd945b0e2e2328c17615c94143bdd62b
environments/contacts/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
environments/contacts/not_provided: a09e4d61bbeb04b927406a50116445e2
environments/contacts/number_value_required: d82a198a378eb120f3329e4d3fd4d3f7
environments/contacts/personal_link_generated: efb7a0420bd459847eb57bca41a4ab0d
environments/contacts/personal_link_generated_but_clipboard_failed: 4eb1e208e729bd5ac00c33f72fc38d53
environments/contacts/personal_survey_link: 5b3f1afc53733718c4ed5b1443b6a604
@@ -655,24 +623,13 @@ checksums:
environments/contacts/search_contact: 020205a93846ab3e12c203ac4fa97c12
environments/contacts/select_a_survey: 1f49086dfb874307aae1136e88c3d514
environments/contacts/select_attribute: d93fb60eb4fbb42bf13a22f6216fbd79
environments/contacts/select_attribute_key: 673a6683fab41b387d921841cded7e38
environments/contacts/survey_viewed: 646d413218626787b0373ffd71cb7451
environments/contacts/survey_viewed_at: 2ab535237af5c3c3f33acc792a7e70a4
environments/contacts/system_attributes: eadb6a8888c7b32c0e68881f945ae9b6
environments/contacts/unlock_contacts_description: c5572047f02b4c39e5109f9de715499d
environments/contacts/unlock_contacts_title: a8b3d7db03eb404d9267fd5cdd6d5ddb
environments/contacts/upload_contacts_error_attribute_type_mismatch: 70a60f0886ce767c00defa7d4aad0f93
environments/contacts/upload_contacts_error_duplicate_mappings: 9c1e1f07e476226bad98ccfa07979fec
environments/contacts/upload_contacts_error_file_too_large: 0c1837286c55d18049277465bc2444c1
environments/contacts/upload_contacts_error_generic: 3a8d35a421b377198361af9972392693
environments/contacts/upload_contacts_error_invalid_file_type: 15ef4fa7c2d5273b05a042f398655e81
environments/contacts/upload_contacts_error_no_valid_contacts: 27fbd24ed2d2fa3b6ed7b3a8c1dad343
environments/contacts/upload_contacts_modal_attribute_header: 263246ad2a76f8e2f80f0ed175d7629a
environments/contacts/upload_contacts_modal_attributes_description: e2cedbd4a043423002cbb2048e2145ac
environments/contacts/upload_contacts_modal_attributes_new: 9829382598c681de74130440a37b560f
environments/contacts/upload_contacts_modal_attributes_search_or_add: 1874839e465650d353282b43b00247a9
environments/contacts/upload_contacts_modal_attributes_should_be_mapped_to: 693dfe5836e90b1c4c7c65b015418174
environments/contacts/upload_contacts_modal_attributes_title: 86d0ae6fea0fbb119722ed3841f8385a
environments/contacts/upload_contacts_modal_csv_column_header: f181add48fb8325efaa40579fe8c343e
environments/contacts/upload_contacts_modal_description: 41566d40d25cc882aa9f82d87b4e2f03
environments/contacts/upload_contacts_modal_download_example_csv: 7a186fc4941b264452ee6c9e785769de
environments/contacts/upload_contacts_modal_duplicates_description: 112ce4641088520469a83a0bd740b073
@@ -724,12 +681,7 @@ checksums:
environments/integrations/google_sheets/link_google_sheet: fa78146ae26ce5b1d2aaf2678f628943
environments/integrations/google_sheets/link_new_sheet: 8ad2ea8708f50ed184c00b84577b325e
environments/integrations/google_sheets/no_integrations_yet: ea46f7747937baf48a47a4c1b1776aee
environments/integrations/google_sheets/reconnect_button: 8992a0f250278c116cb26be448b68ba2
environments/integrations/google_sheets/reconnect_button_description: 851fd2fda57211293090f371d5b2c734
environments/integrations/google_sheets/reconnect_button_tooltip: 210dd97470fde8264d2c076db3c98fde
environments/integrations/google_sheets/spreadsheet_permission_error: 94f0007a187d3b9a7ab8200fe26aad20
environments/integrations/google_sheets/spreadsheet_url: b1665f96e6ecce23ea2d9196f4a3e5dd
environments/integrations/google_sheets/token_expired_error: 555d34c18c554ec8ac66614f21bd44fc
environments/integrations/include_created_at: 8011355b13e28e638d74e6f3d68a2bbf
environments/integrations/include_hidden_fields: 25f0ea5ca1c6ead2cd121f8754cb8d72
environments/integrations/include_metadata: 750091d965d7cc8d02468b5239816dc5
@@ -844,40 +796,6 @@ checksums:
environments/segments/no_attributes_yet: 57beecc917dcd598ccdd0ccfb364a960
environments/segments/no_filters_yet: d885a68516840e15dd27f1c17d9a8975
environments/segments/no_segments_yet: 6307a4163a5bd553bb2aba074d24be9c
environments/segments/operator_contains: 06dd606c0a8f81f9a03b414e9ae89440
environments/segments/operator_does_not_contain: 854da2bdf10613ce62fb454bab16c58b
environments/segments/operator_ends_with: 2bd866369766c6a2ef74bb9fa74b1d7e
environments/segments/operator_is_after: f9d9296eb9a5a7d168cc4e65a4095a87
environments/segments/operator_is_before: 2462480cf4e8d2832b64004fbd463e55
environments/segments/operator_is_between: 41ff45044d8a017a8a74f72be57916b8
environments/segments/operator_is_newer_than: c41e03366623caed6b2c224e50387614
environments/segments/operator_is_not_set: 906801489132487ef457652af4835142
environments/segments/operator_is_older_than: acca6b309da507bbc5973c4b56b698b0
environments/segments/operator_is_same_day: c06506b6eb9f6491f15685baccd68897
environments/segments/operator_is_set: 9850468156356f95884bbaf56b6687aa
environments/segments/operator_starts_with: 37e55e9080c84a1855956161e7885c21
environments/segments/operator_title_contains: 41c8c25407527a5336404313f4c8d650
environments/segments/operator_title_does_not_contain: d618eb0f854f7efa0d7c644e6628fa42
environments/segments/operator_title_ends_with: c8a5f60f1bd1d8fa018dbbf49806fb5b
environments/segments/operator_title_equals: 73439e2839b8049e68079b1b6f2e3c41
environments/segments/operator_title_greater_equal: 556b342cee0ac7055171e41be80f49e4
environments/segments/operator_title_greater_than: e06dabbbf3a9c527502c997101edab40
environments/segments/operator_title_is_after: bd4cf644e442fca330cb483528485e5f
environments/segments/operator_title_is_before: a47ce3825c5c7cea7ed7eb8d5505a2d5
environments/segments/operator_title_is_between: 5721c877c60f0005dc4ce78d4c0d3fdc
environments/segments/operator_title_is_newer_than: 133731671413c702a55cdfb9134d63f8
environments/segments/operator_title_is_not_set: c1a6fd89387686d3a5426a768bb286e9
environments/segments/operator_title_is_older_than: 9064cd482f2312c8b10aee4937d0278d
environments/segments/operator_title_is_same_day: 9340bf7bd6ab504d71b0e957ca9fcf4c
environments/segments/operator_title_is_set: 1c66019bd162201db83aef305ab2a161
environments/segments/operator_title_less_equal: 235dbef60cd0af5ff1d319aab24a1109
environments/segments/operator_title_less_than: e9f3c9742143760b28bf4e326f63a97b
environments/segments/operator_title_not_equals: a186482f46739c9fe8683826a1cab723
environments/segments/operator_title_starts_with: f6673c17475708313c6a0f245b561781
environments/segments/operator_title_user_is_in: 33ecd1bc30f56d97133368f1b244ee4b
environments/segments/operator_title_user_is_not_in: 99d576a3611d171947fd88c317aaf5f3
environments/segments/operator_user_is_in: 33ecd1bc30f56d97133368f1b244ee4b
environments/segments/operator_user_is_not_in: 99d576a3611d171947fd88c317aaf5f3
environments/segments/person_and_attributes: 507023d577326a6326dd9603dcdc589d
environments/segments/phone: b9537ee90fc5b0116942e0af29d926cc
environments/segments/please_remove_the_segment_from_these_surveys_in_order_to_delete_it: 1858a8ae40bed3a8c06c3bb518e0b8aa
@@ -902,7 +820,6 @@ checksums:
environments/segments/user_targeting_is_currently_only_available_when: 9785f159fb045607b62461f38e8d3aee
environments/segments/value_cannot_be_empty: 99efd449ec19f1ecc5cf0b6807d4f315
environments/segments/value_must_be_a_number: 87516b5c69e08741fa8a6ddf64d60deb
environments/segments/value_must_be_positive: d17ad009f7845a6fbeddeb2aef532e10
environments/segments/view_filters: 791cd4bacb11e3eb0ffccee131270561
environments/segments/where: 23aecda7d27f26121b057ec7f7327069
environments/segments/with_the_formbricks_sdk: 2b185e6242edb69e1bc6e64e10dfc02a
@@ -1024,7 +941,7 @@ checksums:
environments/settings/general/email_customization_preview_email_heading: 8b798cb8438b3dd356c02dab33b4c897
environments/settings/general/email_customization_preview_email_text: fa6ae92403cc8f3c35c03e6c94cbde51
environments/settings/general/error_deleting_organization_please_try_again: 7f0fe257d4a0b40bff025408a7766706
environments/settings/general/from_your_organization: 9ebd6dcd79f7bfad3fea46ed2e3133d2
environments/settings/general/from_your_organization: 4b7970431edb3d0f13c394dbd755a055
environments/settings/general/invitation_sent_once_more: e6e5ea066810f9dcb65788aa4f05d6e2
environments/settings/general/invite_deleted_successfully: 1c7dca6d0f6870d945288e38cfd2f943
environments/settings/general/invite_expires_on: 6fd2356ad91a5f189070c43855904bb4
@@ -1179,7 +1096,6 @@ checksums:
environments/surveys/edit/add_fallback_placeholder: 0e77ea487ddd7bc7fc2f1574b018dc08
environments/surveys/edit/add_hidden_field_id: a8f55b51b790cf5f4d898af7770ad1ed
environments/surveys/edit/add_highlight_border: 66f52b21fbb9aa6561c98a090abaaf8f
environments/surveys/edit/add_highlight_border_description: fe548fe03ea10ef5cd9e553d6812b3c2
environments/surveys/edit/add_logic: f234c9f1393a9ed4792dfbd15838c951
environments/surveys/edit/add_none_of_the_above: dbe1ada4512d6c3f80c54c8fac107ec6
environments/surveys/edit/add_option: 143c54f0b201067fe5159284d6daeca2
@@ -1378,6 +1294,7 @@ checksums:
environments/surveys/edit/follow_ups_modal_updated_successfull_toast: 61204fada3231f4f1fe3866e87e1130a
environments/surveys/edit/follow_ups_new: 224c779d252b3e75086e4ed456ba2548
environments/surveys/edit/follow_ups_upgrade_button_text: 4cd167527fc6cdb5b0bfc9b486b142a8
environments/surveys/edit/form_styling: 1278a2db4257b5500474161133acc857
environments/surveys/edit/formbricks_sdk_is_not_connected: 35165b0cac182a98408007a378cc677e
environments/surveys/edit/four_points: b289628a6b8a6cd0f7d17a14ca6cd7bf
environments/surveys/edit/heading: 79e9dfa461f38a239d34b9833ca103f1
@@ -1548,7 +1465,7 @@ checksums:
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
environments/surveys/edit/roundness_description: 03940a6871ae43efa4810cba7cadb74b
environments/surveys/edit/roundness_description: bde131aa5674836416dcdf2ff517d899
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
environments/surveys/edit/rows: 8f41f34e6ca28221cf1ebd948af4c151
environments/surveys/edit/save_and_close: 6ede705b3f82f30269ff3054a5049e34
@@ -1594,7 +1511,6 @@ checksums:
environments/surveys/edit/survey_completed_subheading: db537c356c3ab6564d24de0d11a0fee2
environments/surveys/edit/survey_display_settings: 8ed19e6a8e1376f7a1ba037d82c4ae11
environments/surveys/edit/survey_placement: 083c10f257337f9648bf9d435b18ec2c
environments/surveys/edit/survey_styling: 7f96d6563e934e65687b74374a33b1dc
environments/surveys/edit/survey_trigger: f0c7014a684ca566698b87074fad5579
environments/surveys/edit/switch_multi_language_on_to_get_started: cca0ef91ee49095da30cd1e3f26c406f
environments/surveys/edit/target_block_not_found: 0a0c401017ab32364fec2fcbf815d832
@@ -1683,6 +1599,7 @@ checksums:
environments/surveys/edit/welcome_message: 986a434e3895c8ee0b267df95cc40051
environments/surveys/edit/without_a_filter_all_of_your_users_can_be_surveyed: 451990569c61f25d01044cc45b1ce122
environments/surveys/edit/you_have_not_created_a_segment_yet: c6658bd1cee9c5c957c675db044708dd
environments/surveys/edit/you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations: 04241177ba989ef4c1d8c01e1a7b8541
environments/surveys/edit/your_description_here_recall_information_with: 60f73a3cc9bdb9afea2166a7db8fd618
environments/surveys/edit/your_question_here_recall_information_with: 6395bd54f5167830c9d662ba403da167
environments/surveys/edit/your_web_app: 07234bed03a33330dc50ae9fcf0174f3
@@ -1864,7 +1781,6 @@ checksums:
environments/surveys/summary/filtered_responses_excel: 06e57bae9e41979fd7fc4b8bfe3466f9
environments/surveys/summary/generating_qr_code: 5026d4a76f995db458195e5215d9bbd9
environments/surveys/summary/impressions: 7fe38d42d68a64d3fd8436a063751584
environments/surveys/summary/impressions_identified_only: 10f8c491463c73b8e6534314ee00d165
environments/surveys/summary/impressions_tooltip: 4d0823cbf360304770c7c5913e33fdc8
environments/surveys/summary/in_app/connection_description: 9710bbf8048a8a5c3b2b56db9d946b73
environments/surveys/summary/in_app/connection_title: 29e8a40ad6a7fdb5af5ee9451a70a9aa
@@ -1905,7 +1821,6 @@ checksums:
environments/surveys/summary/last_quarter: 2e565a81de9b3d7b1ee709ebb6f6eda1
environments/surveys/summary/last_year: fe7c268a48bf85bc40da000e6e437637
environments/surveys/summary/limit: 347051f1a068e01e8c4e4f6744d8e727
environments/surveys/summary/no_identified_impressions: c3bc42e6feb9010ced905ded51c5afc4
environments/surveys/summary/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
environments/surveys/summary/other_values_found: 48a74ee68c05f7fb162072b50c683b6a
environments/surveys/summary/overall: 6c6d6533013d4739766af84b2871bca6
@@ -2047,7 +1962,7 @@ checksums:
environments/workspace/look/advanced_styling_field_description_size: a0d51c3ab7dc56320ecedc2b27917842
environments/workspace/look/advanced_styling_field_description_size_description: ff880ea1beddd1b1ec7416d0b8a69cf3
environments/workspace/look/advanced_styling_field_description_weight: 514680cc7202ad29835c1cbcde3def1c
environments/workspace/look/advanced_styling_field_description_weight_description: aa95bc81b5336a548e256bce49350683
environments/workspace/look/advanced_styling_field_description_weight_description: 441ac8db1a32557813eb68fbfd759061
environments/workspace/look/advanced_styling_field_font_size: ca44d14429b2175a1b194793b4ab8f6b
environments/workspace/look/advanced_styling_field_font_weight: bfef83778146cf40550df9650d8a07da
environments/workspace/look/advanced_styling_field_headline_color: 4ccf3935ad90c88ad4add24f498673ce
@@ -2056,12 +1971,12 @@ checksums:
environments/workspace/look/advanced_styling_field_headline_size_description: 13debc3855e4edae992c7a1ebff599c3
environments/workspace/look/advanced_styling_field_headline_weight: 0c8b8262945c61f8e2978502362e0a42
environments/workspace/look/advanced_styling_field_headline_weight_description: 1a9c40bd76ff5098b1e48b1d3893171b
environments/workspace/look/advanced_styling_field_height: 40ca2224bb2936ad1329091b35a9ffe2
environments/workspace/look/advanced_styling_field_height: f4da6d7ecd26e3fa75cfea03abb60c00
environments/workspace/look/advanced_styling_field_indicator_bg: 00febda2901af0f1b0c17e44f9917c38
environments/workspace/look/advanced_styling_field_indicator_bg_description: 7eb3b54a8b331354ec95c0dc1545c620
environments/workspace/look/advanced_styling_field_input_border_radius_description: 0007f1bb572b35d9a3720daeb7a55617
environments/workspace/look/advanced_styling_field_input_font_size_description: 5311f95dcbd083623e35c98ea5374c3b
environments/workspace/look/advanced_styling_field_input_height_description: bb7439d42ec3848a8fa9edb8b001b69a
environments/workspace/look/advanced_styling_field_input_height_description: b704fc67e805223992c811d6f86a9c00
environments/workspace/look/advanced_styling_field_input_padding_x_description: 10e14296468321c13fda77fd1ba58dfd
environments/workspace/look/advanced_styling_field_input_padding_y_description: 98b4aeff2940516d05ea61bdc1211d0d
environments/workspace/look/advanced_styling_field_input_placeholder_opacity_description: f55a6700884d24014404e58876121ddf
@@ -2070,8 +1985,6 @@ checksums:
environments/workspace/look/advanced_styling_field_input_text_description: 460450df24ea0cc902710118a5000feb
environments/workspace/look/advanced_styling_field_option_bg: 0ceaed10d99ed4ad83cb0934ab970174
environments/workspace/look/advanced_styling_field_option_bg_description: 6cd6ccecbbb9f2f19439d7c682eb67c1
environments/workspace/look/advanced_styling_field_option_border: aa478eb148515b6a2637fb144ff72028
environments/workspace/look/advanced_styling_field_option_border_description: 8f75b740e8dcb7f6cfeff2e5d5ca7c92
environments/workspace/look/advanced_styling_field_option_border_radius_description: 23f81c25b2681a7c9e2c4f2e7d2e0656
environments/workspace/look/advanced_styling_field_option_font_size_description: 5430fd9b08819972f0a613bf3fa659da
environments/workspace/look/advanced_styling_field_option_label: 2767a5db32742073a01aac16488e93dc
@@ -2126,7 +2039,6 @@ checksums:
environments/workspace/look/show_powered_by_formbricks: a0e96edadec8ef326423feccc9d06be7
environments/workspace/look/styling_updated_successfully: b8b74b50dde95abcd498633e9d0c891f
environments/workspace/look/suggest_colors: ddc4543b416ab774007b10a3434343cd
environments/workspace/look/suggested_colors_applied_please_save: 226fa70af5efc8ffa0a3755909c8163e
environments/workspace/look/theme: 21fe00b7a518089576fb83c08631107a
environments/workspace/look/theme_settings_description: 9fc45322818c3774ab4a44ea14d7836e
environments/workspace/tags/add: 87c4a663507f2bcbbf79934af8164e13
@@ -2853,9 +2765,6 @@ checksums:
templates/preview_survey_question_2_choice_2_label: 1af148222f327f28cf0db6513de5989e
templates/preview_survey_question_2_headline: 5cfb173d156555227fbc2c97ad921e72
templates/preview_survey_question_2_subheader: 2e652d8acd68d072e5a0ae686c4011c0
templates/preview_survey_question_open_text_headline: a9509a47e0456ae98ec3ddac3d6fad2c
templates/preview_survey_question_open_text_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/preview_survey_question_open_text_subheader: 3c7bf09f3f17b02bc2fbbbdb347a5830
templates/preview_survey_welcome_card_headline: 8778dc41547a2778d0f9482da989fc00
templates/prioritize_features_description: 1eae41fad0e3947f803d8539081e59ec
templates/prioritize_features_name: 4ca59ff1f9c319aaa68c3106d820fd6a
+1 -2
View File
@@ -63,8 +63,7 @@ export const INVITE_DISABLED = env.INVITE_DISABLED === "1";
export const SLACK_CLIENT_SECRET = env.SLACK_CLIENT_SECRET;
export const SLACK_CLIENT_ID = env.SLACK_CLIENT_ID;
export const SLACK_REDIRECT_URI = `${WEBAPP_URL}/api/v1/integrations/slack/callback`;
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read&redirect_uri=${SLACK_REDIRECT_URI}`;
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read`;
export const GOOGLE_SHEETS_CLIENT_ID = env.GOOGLE_SHEETS_CLIENT_ID;
export const GOOGLE_SHEETS_CLIENT_SECRET = env.GOOGLE_SHEETS_CLIENT_SECRET;
+8 -99
View File
@@ -1,10 +1,9 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { TDisplay, TDisplayFilters, TDisplayWithContact } from "@formbricks/types/displays";
import { TDisplay, TDisplayFilters } from "@formbricks/types/displays";
import { DatabaseError } from "@formbricks/types/errors";
import { validateInputs } from "../utils/validate";
@@ -24,12 +23,13 @@ export const getDisplayCountBySurveyId = reactCache(
const displayCount = await prisma.display.count({
where: {
surveyId: surveyId,
...(filters?.createdAt && {
createdAt: {
gte: filters.createdAt.min,
lte: filters.createdAt.max,
},
}),
...(filters &&
filters.createdAt && {
createdAt: {
gte: filters.createdAt.min,
lte: filters.createdAt.max,
},
}),
},
});
return displayCount;
@@ -42,97 +42,6 @@ export const getDisplayCountBySurveyId = reactCache(
}
);
export const getDisplaysByContactId = reactCache(
async (contactId: string): Promise<Pick<TDisplay, "id" | "createdAt" | "surveyId">[]> => {
validateInputs([contactId, ZId]);
try {
const displays = await prisma.display.findMany({
where: { contactId },
select: {
id: true,
createdAt: true,
surveyId: true,
},
orderBy: { createdAt: "desc" },
});
return displays;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
}
);
export const getDisplaysBySurveyIdWithContact = reactCache(
async (surveyId: string, limit?: number, offset?: number): Promise<TDisplayWithContact[]> => {
validateInputs(
[surveyId, ZId],
[limit, z.number().int().min(1).optional()],
[offset, z.number().int().nonnegative().optional()]
);
try {
const displays = await prisma.display.findMany({
where: {
surveyId,
contactId: { not: null },
},
select: {
id: true,
createdAt: true,
surveyId: true,
contact: {
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: { in: ["email", "userId"] },
},
},
select: {
attributeKey: { select: { key: true } },
value: true,
},
},
},
},
},
orderBy: { createdAt: "desc" },
take: limit,
skip: offset,
});
return displays.map((display) => ({
id: display.id,
createdAt: display.createdAt,
surveyId: display.surveyId,
contact: display.contact
? {
id: display.contact.id,
attributes: display.contact.attributes.reduce(
(acc, attr) => {
acc[attr.attributeKey.key] = attr.value;
return acc;
},
{} as Record<string, string>
),
}
: null,
}));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
}
);
export const deleteDisplay = async (displayId: string, tx?: Prisma.TransactionClient): Promise<TDisplay> => {
validateInputs([displayId, ZId]);
try {
@@ -1,219 +0,0 @@
import { mockDisplayId, mockSurveyId } from "./__mocks__/data.mock";
import { prisma } from "@/lib/__mocks__/database";
import { Prisma } from "@prisma/client";
import { describe, expect, test, vi } from "vitest";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { getDisplaysByContactId, getDisplaysBySurveyIdWithContact } from "../service";
const mockContactId = "clqnj99r9000008lebgf8734j";
const mockDisplaysForContact = [
{
id: mockDisplayId,
createdAt: new Date("2024-01-15T10:00:00Z"),
surveyId: mockSurveyId,
},
{
id: "clqkr5smu000208jy50v6g5k5",
createdAt: new Date("2024-01-14T10:00:00Z"),
surveyId: "clqkr8dlv000308jybb08evgs",
},
];
const mockDisplaysWithContact = [
{
id: mockDisplayId,
createdAt: new Date("2024-01-15T10:00:00Z"),
surveyId: mockSurveyId,
contact: {
id: mockContactId,
attributes: [
{ attributeKey: { key: "email" }, value: "test@example.com" },
{ attributeKey: { key: "userId" }, value: "user-123" },
],
},
},
{
id: "clqkr5smu000208jy50v6g5k5",
createdAt: new Date("2024-01-14T10:00:00Z"),
surveyId: "clqkr8dlv000308jybb08evgs",
contact: {
id: "clqnj99r9000008lebgf8734k",
attributes: [{ attributeKey: { key: "userId" }, value: "user-456" }],
},
},
];
describe("getDisplaysByContactId", () => {
describe("Happy Path", () => {
test("returns displays for a contact ordered by createdAt desc", async () => {
vi.mocked(prisma.display.findMany).mockResolvedValue(mockDisplaysForContact as any);
const result = await getDisplaysByContactId(mockContactId);
expect(result).toEqual(mockDisplaysForContact);
expect(prisma.display.findMany).toHaveBeenCalledWith({
where: { contactId: mockContactId },
select: {
id: true,
createdAt: true,
surveyId: true,
},
orderBy: { createdAt: "desc" },
});
});
test("returns empty array when contact has no displays", async () => {
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
const result = await getDisplaysByContactId(mockContactId);
expect(result).toEqual([]);
});
});
describe("Sad Path", () => {
test("throws a ValidationError if the contactId is invalid", async () => {
await expect(getDisplaysByContactId("not-a-cuid")).rejects.toThrow(ValidationError);
});
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.display.findMany).mockRejectedValue(errToThrow);
await expect(getDisplaysByContactId(mockContactId)).rejects.toThrow(DatabaseError);
});
test("throws generic Error for other exceptions", async () => {
vi.mocked(prisma.display.findMany).mockRejectedValue(new Error("Mock error"));
await expect(getDisplaysByContactId(mockContactId)).rejects.toThrow(Error);
});
});
});
describe("getDisplaysBySurveyIdWithContact", () => {
describe("Happy Path", () => {
test("returns displays with contact attributes transformed", async () => {
vi.mocked(prisma.display.findMany).mockResolvedValue(mockDisplaysWithContact as any);
const result = await getDisplaysBySurveyIdWithContact(mockSurveyId, 15, 0);
expect(result).toEqual([
{
id: mockDisplayId,
createdAt: new Date("2024-01-15T10:00:00Z"),
surveyId: mockSurveyId,
contact: {
id: mockContactId,
attributes: { email: "test@example.com", userId: "user-123" },
},
},
{
id: "clqkr5smu000208jy50v6g5k5",
createdAt: new Date("2024-01-14T10:00:00Z"),
surveyId: "clqkr8dlv000308jybb08evgs",
contact: {
id: "clqnj99r9000008lebgf8734k",
attributes: { userId: "user-456" },
},
},
]);
});
test("calls prisma with correct where clause and pagination", async () => {
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
await getDisplaysBySurveyIdWithContact(mockSurveyId, 15, 0);
expect(prisma.display.findMany).toHaveBeenCalledWith({
where: {
surveyId: mockSurveyId,
contactId: { not: null },
},
select: {
id: true,
createdAt: true,
surveyId: true,
contact: {
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: { in: ["email", "userId"] },
},
},
select: {
attributeKey: { select: { key: true } },
value: true,
},
},
},
},
},
orderBy: { createdAt: "desc" },
take: 15,
skip: 0,
});
});
test("returns empty array when no displays found", async () => {
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
const result = await getDisplaysBySurveyIdWithContact(mockSurveyId);
expect(result).toEqual([]);
});
test("handles display with null contact", async () => {
vi.mocked(prisma.display.findMany).mockResolvedValue([
{
id: mockDisplayId,
createdAt: new Date("2024-01-15T10:00:00Z"),
surveyId: mockSurveyId,
contact: null,
},
] as any);
const result = await getDisplaysBySurveyIdWithContact(mockSurveyId);
expect(result).toEqual([
{
id: mockDisplayId,
createdAt: new Date("2024-01-15T10:00:00Z"),
surveyId: mockSurveyId,
contact: null,
},
]);
});
});
describe("Sad Path", () => {
test("throws a ValidationError if the surveyId is invalid", async () => {
await expect(getDisplaysBySurveyIdWithContact("not-a-cuid")).rejects.toThrow(ValidationError);
});
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.display.findMany).mockRejectedValue(errToThrow);
await expect(getDisplaysBySurveyIdWithContact(mockSurveyId)).rejects.toThrow(DatabaseError);
});
test("throws generic Error for other exceptions", async () => {
vi.mocked(prisma.display.findMany).mockRejectedValue(new Error("Mock error"));
await expect(getDisplaysBySurveyIdWithContact(mockSurveyId)).rejects.toThrow(Error);
});
});
});
-6
View File
@@ -167,12 +167,6 @@ export const createEnvironment = async (
description: "Your contact's last name",
type: "default",
},
{
key: "language",
name: "Language",
description: "The language preference of a contact",
type: "default",
},
],
},
},
+26 -48
View File
@@ -1,66 +1,44 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock constants module
const envMock = {
WEBAPP_URL: undefined as string | undefined,
VERCEL_URL: undefined as string | undefined,
PUBLIC_URL: undefined as string | undefined,
env: {
WEBAPP_URL: "http://localhost:3000",
PUBLIC_URL: undefined as string | undefined,
},
};
vi.mock("./env", () => ({
env: envMock,
}));
const loadGetPublicDomain = async () => {
vi.resetModules();
const { getPublicDomain } = await import("./getPublicUrl");
return getPublicDomain;
};
vi.mock("@/lib/env", () => envMock);
describe("getPublicDomain", () => {
beforeEach(() => {
envMock.WEBAPP_URL = undefined;
envMock.VERCEL_URL = undefined;
envMock.PUBLIC_URL = undefined;
vi.resetModules();
});
test("returns trimmed WEBAPP_URL when configured", async () => {
envMock.WEBAPP_URL = " https://app.formbricks.com ";
const getPublicDomain = await loadGetPublicDomain();
expect(getPublicDomain()).toBe("https://app.formbricks.com");
test("should return WEBAPP_URL when PUBLIC_URL is not set", async () => {
const { getPublicDomain } = await import("./getPublicUrl");
const domain = getPublicDomain();
expect(domain).toBe("http://localhost:3000");
});
test("falls back to VERCEL_URL when WEBAPP_URL is empty", async () => {
envMock.WEBAPP_URL = " ";
envMock.VERCEL_URL = "preview.formbricks.com";
const getPublicDomain = await loadGetPublicDomain();
expect(getPublicDomain()).toBe("https://preview.formbricks.com");
test("should return PUBLIC_URL when it is set", async () => {
envMock.env.PUBLIC_URL = "https://surveys.example.com";
const { getPublicDomain } = await import("./getPublicUrl");
const domain = getPublicDomain();
expect(domain).toBe("https://surveys.example.com");
});
test("falls back to localhost when WEBAPP_URL and VERCEL_URL are not set", async () => {
const getPublicDomain = await loadGetPublicDomain();
expect(getPublicDomain()).toBe("http://localhost:3000");
test("should handle empty string PUBLIC_URL by returning WEBAPP_URL", async () => {
envMock.env.PUBLIC_URL = "";
const { getPublicDomain } = await import("./getPublicUrl");
const domain = getPublicDomain();
expect(domain).toBe("http://localhost:3000");
});
test("returns PUBLIC_URL when set", async () => {
envMock.WEBAPP_URL = "https://app.formbricks.com";
envMock.PUBLIC_URL = "https://surveys.formbricks.com";
const getPublicDomain = await loadGetPublicDomain();
expect(getPublicDomain()).toBe("https://surveys.formbricks.com");
});
test("falls back to WEBAPP_URL when PUBLIC_URL is empty", async () => {
envMock.WEBAPP_URL = "https://app.formbricks.com";
envMock.PUBLIC_URL = " ";
const getPublicDomain = await loadGetPublicDomain();
expect(getPublicDomain()).toBe("https://app.formbricks.com");
test("should handle undefined PUBLIC_URL by returning WEBAPP_URL", async () => {
envMock.env.PUBLIC_URL = undefined;
const { getPublicDomain } = await import("./getPublicUrl");
const domain = getPublicDomain();
expect(domain).toBe("http://localhost:3000");
});
});
+2 -12
View File
@@ -1,18 +1,8 @@
import "server-only";
import { env } from "./env";
const configuredWebappUrl = env.WEBAPP_URL?.trim() ?? "";
const WEBAPP_URL = (() => {
if (configuredWebappUrl !== "") {
return configuredWebappUrl;
}
if (env.VERCEL_URL) {
return `https://${env.VERCEL_URL}`;
}
return "http://localhost:3000";
})();
const WEBAPP_URL =
env.WEBAPP_URL ?? (env.VERCEL_URL ? `https://${env.VERCEL_URL}` : "") ?? "http://localhost:3000";
/**
* Returns the public domain URL
-6
View File
@@ -1,6 +0,0 @@
/**
* Error codes returned by Google Sheets integration.
* Use these constants when comparing error responses to avoid typos and enable reuse.
*/
export const GOOGLE_SHEET_INTEGRATION_INVALID_GRANT = "invalid_grant";
export const GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION = "insufficient_permission";
+18 -92
View File
@@ -2,12 +2,7 @@ import "server-only";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import { ZString } from "@formbricks/types/common";
import {
AuthenticationError,
DatabaseError,
OperationNotAllowedError,
UnknownError,
} from "@formbricks/types/errors";
import { DatabaseError, UnknownError } from "@formbricks/types/errors";
import {
TIntegrationGoogleSheets,
ZIntegrationGoogleSheets,
@@ -16,12 +11,8 @@ import {
GOOGLE_SHEETS_CLIENT_ID,
GOOGLE_SHEETS_CLIENT_SECRET,
GOOGLE_SHEETS_REDIRECT_URL,
GOOGLE_SHEET_MESSAGE_LIMIT,
} from "@/lib/constants";
import {
GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION,
GOOGLE_SHEET_INTEGRATION_INVALID_GRANT,
} from "@/lib/googleSheet/constants";
import { GOOGLE_SHEET_MESSAGE_LIMIT } from "@/lib/constants";
import { createOrUpdateIntegration } from "@/lib/integration/service";
import { truncateText } from "../utils/strings";
import { validateInputs } from "../utils/validate";
@@ -90,17 +81,6 @@ export const writeData = async (
}
};
export const validateGoogleSheetsConnection = async (
googleSheetIntegrationData: TIntegrationGoogleSheets
): Promise<void> => {
validateInputs([googleSheetIntegrationData, ZIntegrationGoogleSheets]);
const integrationData = structuredClone(googleSheetIntegrationData);
integrationData.config.data.forEach((data) => {
data.createdAt = new Date(data.createdAt);
});
await authorize(integrationData);
};
export const getSpreadsheetNameById = async (
googleSheetIntegrationData: TIntegrationGoogleSheets,
spreadsheetId: string
@@ -114,17 +94,7 @@ export const getSpreadsheetNameById = async (
return new Promise((resolve, reject) => {
sheets.spreadsheets.get({ spreadsheetId }, (err, response) => {
if (err) {
const msg = err.message?.toLowerCase() ?? "";
const isPermissionError =
msg.includes("permission") ||
msg.includes("caller does not have") ||
msg.includes("insufficient permission") ||
msg.includes("access denied");
if (isPermissionError) {
reject(new OperationNotAllowedError(GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION));
} else {
reject(new UnknownError(`Error while fetching spreadsheet data: ${err.message}`));
}
reject(new UnknownError(`Error while fetching spreadsheet data: ${err.message}`));
return;
}
const spreadsheetTitle = response.data.properties.title;
@@ -139,70 +109,26 @@ export const getSpreadsheetNameById = async (
}
};
const isInvalidGrantError = (error: unknown): boolean => {
const err = error as { message?: string; response?: { data?: { error?: string } } };
return (
typeof err?.message === "string" &&
err.message.toLowerCase().includes(GOOGLE_SHEET_INTEGRATION_INVALID_GRANT)
);
};
/** Buffer in ms before expiry_date to consider token near-expired (5 minutes). */
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
const GOOGLE_TOKENINFO_URL = "https://www.googleapis.com/oauth2/v1/tokeninfo";
/**
* Verifies that the access token is still valid and not revoked (e.g. user removed app access).
* Returns true if token is valid, false if invalid/revoked.
*/
const isAccessTokenValid = async (accessToken: string): Promise<boolean> => {
try {
const res = await fetch(`${GOOGLE_TOKENINFO_URL}?access_token=${encodeURIComponent(accessToken)}`);
return res.ok;
} catch {
return false;
}
};
const authorize = async (googleSheetIntegrationData: TIntegrationGoogleSheets) => {
const client_id = GOOGLE_SHEETS_CLIENT_ID;
const client_secret = GOOGLE_SHEETS_CLIENT_SECRET;
const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const key = googleSheetIntegrationData.config.key;
const refresh_token = googleSheetIntegrationData.config.key.refresh_token;
oAuth2Client.setCredentials({
refresh_token,
});
const { credentials } = await oAuth2Client.refreshAccessToken();
await createOrUpdateIntegration(googleSheetIntegrationData.environmentId, {
type: "googleSheets",
config: {
data: googleSheetIntegrationData.config?.data ?? [],
email: googleSheetIntegrationData.config?.email ?? "",
key: credentials,
},
});
const hasStoredCredentials =
key.access_token && key.expiry_date && key.expiry_date > Date.now() + TOKEN_EXPIRY_BUFFER_MS;
oAuth2Client.setCredentials(credentials);
if (hasStoredCredentials && (await isAccessTokenValid(key.access_token))) {
oAuth2Client.setCredentials(key);
return oAuth2Client;
}
oAuth2Client.setCredentials({ refresh_token: key.refresh_token });
try {
const { credentials } = await oAuth2Client.refreshAccessToken();
const mergedCredentials = {
...credentials,
refresh_token: credentials.refresh_token ?? key.refresh_token,
};
await createOrUpdateIntegration(googleSheetIntegrationData.environmentId, {
type: "googleSheets",
config: {
data: googleSheetIntegrationData.config?.data ?? [],
email: googleSheetIntegrationData.config?.email ?? "",
key: mergedCredentials,
},
});
oAuth2Client.setCredentials(mergedCredentials);
return oAuth2Client;
} catch (error) {
if (isInvalidGrantError(error)) {
throw new AuthenticationError(GOOGLE_SHEET_INTEGRATION_INVALID_GRANT);
}
throw error;
}
return oAuth2Client;
};
+2 -3
View File
@@ -22,7 +22,6 @@ import { getElementsFromBlocks } from "@/lib/survey/utils";
import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
import { reduceQuotaLimits } from "@/modules/ee/quotas/lib/quotas";
import { deleteFile } from "@/modules/storage/service";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
import { getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
import { ITEMS_PER_PAGE } from "../constants";
@@ -397,6 +396,7 @@ export const getResponseDownloadFile = async (
"Survey ID",
"Formbricks ID (internal)",
"User ID",
"Notes",
"Tags",
...metaDataFields,
...elements.flat(),
@@ -408,10 +408,9 @@ export const getResponseDownloadFile = async (
if (survey.isVerifyEmailEnabled) {
headers.push("Verified Email");
}
const resolvedResponses = responses.map((r) => ({ ...r, data: resolveStorageUrlsInObject(r.data) }));
const jsonData = getResponsesJson(
survey,
resolvedResponses,
responses,
elements,
userAttributes,
hiddenFields,
+3 -43
View File
@@ -60,7 +60,6 @@ export const getSuggestedColors = (brandColor: string = DEFAULT_BRAND_COLOR) =>
// Options (Radio / Checkbox)
"optionBgColor.light": inputBg,
"optionLabelColor.light": questionColor,
"optionBorderColor.light": inputBorder,
// Card
"cardBackgroundColor.light": cardBg,
@@ -119,10 +118,10 @@ export const STYLE_DEFAULTS: TProjectStyling = {
// Inputs
inputTextColor: { light: _colors["inputTextColor.light"] },
inputBorderRadius: 8,
inputHeight: 20,
inputHeight: 40,
inputFontSize: 14,
inputPaddingX: 8,
inputPaddingY: 8,
inputPaddingX: 16,
inputPaddingY: 16,
inputPlaceholderOpacity: 0.5,
inputShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
@@ -139,7 +138,6 @@ export const STYLE_DEFAULTS: TProjectStyling = {
// Options
optionBgColor: { light: _colors["optionBgColor.light"] },
optionLabelColor: { light: _colors["optionLabelColor.light"] },
optionBorderColor: { light: _colors["optionBorderColor.light"] },
optionBorderRadius: 8,
optionPaddingX: 16,
optionPaddingY: 16,
@@ -151,43 +149,6 @@ export const STYLE_DEFAULTS: TProjectStyling = {
progressIndicatorBgColor: { light: _colors["progressIndicatorBgColor.light"] },
};
/**
* Fills in new v4.7 color fields from legacy v4.6 fields when they are missing.
*
* v4.6 stored: brandColor, questionColor, inputColor, inputBorderColor.
* v4.7 adds: elementHeadlineColor, buttonBgColor, optionBgColor, etc.
*
* When loading v4.6 data the new fields are absent. Without this helper the
* form would fall back to STYLE_DEFAULTS (derived from the *default* brand
* colour), causing a visible mismatch. This function derives the new fields
* from the actually-saved legacy fields so the preview and form stay coherent.
*
* Only sets a field when the legacy source exists AND the new field is absent.
*/
export const deriveNewFieldsFromLegacy = (saved: Record<string, unknown>): Record<string, unknown> => {
const light = (key: string): string | undefined =>
(saved[key] as { light?: string } | null | undefined)?.light;
const q = light("questionColor");
const b = light("brandColor");
const i = light("inputColor");
const inputBorder = light("inputBorderColor");
return {
...(q && !saved.elementHeadlineColor && { elementHeadlineColor: { light: q } }),
...(q && !saved.elementDescriptionColor && { elementDescriptionColor: { light: q } }),
...(q && !saved.elementUpperLabelColor && { elementUpperLabelColor: { light: q } }),
...(q && !saved.inputTextColor && { inputTextColor: { light: q } }),
...(q && !saved.optionLabelColor && { optionLabelColor: { light: q } }),
...(b && !saved.buttonBgColor && { buttonBgColor: { light: b } }),
...(b && !saved.buttonTextColor && { buttonTextColor: { light: isLight(b) ? "#0f172a" : "#ffffff" } }),
...(i && !saved.optionBgColor && { optionBgColor: { light: i } }),
...(inputBorder && !saved.optionBorderColor && { optionBorderColor: { light: inputBorder } }),
...(b && !saved.progressIndicatorBgColor && { progressIndicatorBgColor: { light: b } }),
...(b && !saved.progressTrackBgColor && { progressTrackBgColor: { light: mixColor(b, "#ffffff", 0.8) } }),
};
};
/**
* Builds a complete TProjectStyling object from a single brand color.
*
@@ -214,7 +175,6 @@ export const buildStylingFromBrandColor = (brandColor: string = DEFAULT_BRAND_CO
inputTextColor: { light: colors["inputTextColor.light"] },
optionBgColor: { light: colors["optionBgColor.light"] },
optionLabelColor: { light: colors["optionLabelColor.light"] },
optionBorderColor: { light: colors["optionBorderColor.light"] },
cardBackgroundColor: { light: colors["cardBackgroundColor.light"] },
cardBorderColor: { light: colors["cardBorderColor.light"] },
highlightBorderColor: { light: colors["highlightBorderColor.light"] },
@@ -168,7 +168,6 @@ export const mockContactAttributeKey: TContactAttributeKey = {
type: "custom",
description: "mock action class",
isUnique: false,
dataType: "string",
...commonMockProperties,
};
-1
View File
@@ -508,7 +508,6 @@ export const updateSurveyInternal = async (
newFollowUps.length > 0
? {
data: newFollowUps.map((followUp) => ({
id: followUp.id,
name: followUp.name,
trigger: followUp.trigger,
action: followUp.action,
+6 -22
View File
@@ -142,8 +142,7 @@ describe("Time Utilities", () => {
expect(convertDatesInObject(123)).toBe(123);
});
test("should not convert dates in ignored keys when keysToIgnore is provided", () => {
const keysToIgnore = new Set(["contactAttributes", "variables", "data", "meta"]);
test("should not convert dates in contactAttributes", () => {
const input = {
createdAt: "2024-03-20T15:30:00",
contactAttributes: {
@@ -152,14 +151,13 @@ describe("Time Utilities", () => {
},
};
const result = convertDatesInObject(input, keysToIgnore);
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 when keysToIgnore is provided", () => {
const keysToIgnore = new Set(["contactAttributes", "variables", "data", "meta"]);
test("should not convert dates in variables", () => {
const input = {
updatedAt: "2024-03-20T15:30:00",
variables: {
@@ -168,14 +166,13 @@ describe("Time Utilities", () => {
},
};
const result = convertDatesInObject(input, keysToIgnore);
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 when keysToIgnore is provided", () => {
const keysToIgnore = new Set(["contactAttributes", "variables", "data", "meta"]);
test("should not convert dates in data or meta", () => {
const input = {
createdAt: "2024-03-20T15:30:00",
data: {
@@ -186,23 +183,10 @@ describe("Time Utilities", () => {
},
};
const result = convertDatesInObject(input, keysToIgnore);
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");
});
test("should recurse into all keys when keysToIgnore is not provided", () => {
const input = {
createdAt: "2024-03-20T15:30:00",
contactAttributes: {
createdAt: "2024-03-20T16:30:00",
},
};
const result = convertDatesInObject(input);
expect(result.createdAt).toBeInstanceOf(Date);
expect(result.contactAttributes.createdAt).toBeInstanceOf(Date);
});
});
});
+7 -6
View File
@@ -151,17 +151,18 @@ export const getTodaysDateTimeFormatted = (seperator: string) => {
return [formattedDate, formattedTime].join(seperator);
};
export const convertDatesInObject = <T>(obj: T, keysToIgnore?: Set<string>): T => {
export const convertDatesInObject = <T>(obj: T): T => {
if (obj === null || typeof obj !== "object") {
return obj; // Return if obj is not an object
}
if (Array.isArray(obj)) {
// Handle arrays by mapping each element through the function
return obj.map((item) => convertDatesInObject(item, keysToIgnore)) as unknown as T;
return obj.map((item) => convertDatesInObject(item)) as unknown as T;
}
const newObj: Record<string, unknown> = {};
const newObj: any = {};
const keysToIgnore = new Set(["contactAttributes", "variables", "data", "meta"]);
for (const key in obj) {
if (keysToIgnore?.has(key)) {
if (keysToIgnore.has(key)) {
newObj[key] = obj[key];
continue;
}
@@ -172,10 +173,10 @@ export const convertDatesInObject = <T>(obj: T, keysToIgnore?: Set<string>): T =
) {
newObj[key] = new Date(obj[key] as unknown as string);
} else if (typeof obj[key] === "object" && obj[key] !== null) {
newObj[key] = convertDatesInObject(obj[key], keysToIgnore);
newObj[key] = convertDatesInObject(obj[key]);
} else {
newObj[key] = obj[key];
}
}
return newObj as T;
return newObj;
};
-13
View File
@@ -11,16 +11,3 @@ export const isSafeIdentifier = (value: string): boolean => {
// Can only contain lowercase letters, numbers, and underscores
return /^[a-z0-9_]+$/.test(value);
};
/**
* Converts a snake_case string to Title Case for display as a label.
* Example: "job_description" -> "Job Description"
* "api_key" -> "Api Key"
* "signup_date" -> "Signup Date"
*/
export const formatSnakeCaseToTitleCase = (key: string): string => {
return key
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
+1 -8
View File
@@ -12,18 +12,11 @@ export function validateInputs<T extends ValidationPair<any>[]>(
for (const [value, schema] of pairs) {
const inputValidation = schema.safeParse(value);
if (!inputValidation.success) {
const zodDetails = inputValidation.error.issues
.map((issue) => {
const path = issue?.path?.join(".") ?? "";
return `${path}${issue.message}`;
})
.join("; ");
logger.error(
inputValidation.error,
`Validation failed for ${JSON.stringify(value).substring(0, 100)} and ${JSON.stringify(schema)}`
);
throw new ValidationError(`Validation failed: ${zodDetails}`);
throw new ValidationError("Validation failed");
}
parsedData.push(inputValidation.data);
}
+9 -100
View File
@@ -188,10 +188,8 @@
"customer_success": "Kundenerfolg",
"dark_overlay": "Dunkle Überlagerung",
"date": "Datum",
"days": "Tage",
"default": "Standard",
"delete": "Löschen",
"delete_what": "{deleteWhat} löschen",
"description": "Beschreibung",
"dev_env": "Entwicklungsumgebung",
"development": "Entwicklung",
@@ -207,8 +205,6 @@
"download": "Herunterladen",
"draft": "Entwurf",
"duplicate": "Duplikat",
"duplicate_copy": "(Kopie)",
"duplicate_copy_number": "(Kopie {copyNumber})",
"e_commerce": "E-Commerce",
"edit": "Bearbeiten",
"email": "E-Mail",
@@ -221,16 +217,13 @@
"error": "Fehler",
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
"error_component_title": "Fehler beim Laden der Ressourcen",
"error_loading_data": "Fehler beim Laden der Daten",
"error_rate_limit_description": "Maximale Anzahl an Anfragen erreicht. Bitte später erneut versuchen.",
"error_rate_limit_title": "Rate Limit Überschritten",
"expand_rows": "Zeilen erweitern",
"failed_to_copy_to_clipboard": "Fehler beim Kopieren in die Zwischenablage",
"failed_to_load_organizations": "Fehler beim Laden der Organisationen",
"failed_to_load_workspaces": "Projekte konnten nicht geladen werden",
"filter": "Filter",
"finish": "Fertigstellen",
"first_name": "Vorname",
"follow_these": "Folge diesen",
"formbricks_version": "Formbricks Version",
"full_name": "Name",
@@ -243,7 +236,6 @@
"hidden_field": "Verstecktes Feld",
"hidden_fields": "Versteckte Felder",
"hide_column": "Spalte ausblenden",
"id": "ID",
"image": "Bild",
"images": "Bilder",
"import": "Importieren",
@@ -261,7 +253,6 @@
"key": "Schlüssel",
"label": "Bezeichnung",
"language": "Sprache",
"last_name": "Nachname",
"learn_more": "Mehr erfahren",
"license_expired": "License Expired",
"light_overlay": "Helle Überlagerung",
@@ -284,11 +275,9 @@
"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!",
"months": "Monate",
"move_down": "Nach unten bewegen",
"move_up": "Nach oben bewegen",
"multiple_languages": "Mehrsprachigkeit",
"my_product": "mein Produkt",
"name": "Name",
"new": "Neu",
"new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!",
@@ -404,7 +393,6 @@
"status": "Status",
"step_by_step_manual": "Schritt-für-Schritt-Anleitung",
"storage_not_configured": "Dateispeicher nicht eingerichtet, Uploads werden wahrscheinlich fehlschlagen",
"string": "Text",
"styling": "Styling",
"submit": "Abschicken",
"summary": "Zusammenfassung",
@@ -437,7 +425,6 @@
"top_right": "Oben rechts",
"try_again": "Versuch's nochmal",
"type": "Typ",
"unknown_survey": "Unbekannte Umfrage",
"unlock_more_workspaces_with_a_higher_plan": "Schalten Sie mehr Projekte mit einem höheren Tarif frei.",
"update": "Aktualisierung",
"updated": "Aktualisiert",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Website & App Verbindung",
"website_app_survey": "Website- & App-Umfrage",
"website_survey": "Website-Umfrage",
"weeks": "Wochen",
"welcome_card": "Willkommenskarte",
"workspace_configuration": "Projektkonfiguration",
"workspace_created_successfully": "Projekt erfolgreich erstellt",
@@ -472,7 +458,6 @@
"workspace_not_found": "Projekt nicht gefunden",
"workspace_permission_not_found": "Projektberechtigung nicht gefunden",
"workspaces": "Projekte",
"years": "Jahre",
"you": "Du",
"you_are_downgraded_to_the_community_edition": "Du wurdest auf die Community Edition herabgestuft.",
"you_are_not_authorized_to_perform_this_action": "Du bist nicht berechtigt, diese Aktion durchzuführen.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Attribut erfolgreich aktualisiert",
"attribute_value": "Wert",
"attribute_value_placeholder": "Attributwert",
"attributes_msg_attribute_limit_exceeded": "Es konnten {count} neue Attribute nicht erstellt werden, da dies das maximale Limit von {limit} Attributklassen überschreiten würde. Bestehende Attribute wurden erfolgreich aktualisiert.",
"attributes_msg_attribute_type_validation_error": "{error} (Attribut '{key}' hat dataType: {dataType})",
"attributes_msg_email_already_exists": "Die E-Mail existiert bereits für diese Umgebung und wurde nicht aktualisiert.",
"attributes_msg_email_or_userid_required": "Entweder E-Mail oder userId ist erforderlich. Die bestehenden Werte wurden beibehalten.",
"attributes_msg_new_attribute_created": "Neues Attribut '{key}' mit Typ '{dataType}' erstellt",
"attributes_msg_userid_already_exists": "Die userId existiert bereits für diese Umgebung und wurde nicht aktualisiert.",
"contact_deleted_successfully": "Kontakt erfolgreich gelöscht",
"contact_not_found": "Kein solcher Kontakt gefunden",
"contacts_table_refresh": "Kontakte aktualisieren",
"contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert",
"create_attribute": "Attribut erstellen",
"create_key": "Schlüssel erstellen",
"create_new_attribute": "Neues Attribut erstellen",
"create_new_attribute_description": "Erstellen Sie ein neues Attribut für Segmentierungszwecke.",
"custom_attributes": "Benutzerdefinierte Attribute",
"data_type": "Datentyp",
"data_type_cannot_be_changed": "Der Datentyp kann nach der Erstellung nicht mehr geändert werden",
"data_type_description": "Wähle aus, wie dieses Attribut gespeichert und gefiltert werden soll",
"date_value_required": "Ein Datumswert ist erforderlich. Verwende die Löschen-Schaltfläche, um dieses Attribut zu entfernen, wenn du kein Datum festlegen möchtest.",
"delete_attribute_confirmation": "{value, plural, one {Dadurch wird das ausgewählte Attribut gelöscht. Alle mit diesem Attribut verknüpften Kontaktdaten gehen verloren.} other {Dadurch werden die ausgewählten Attribute gelöscht. Alle mit diesen Attributen verknüpften Kontaktdaten gehen verloren.}}",
"delete_contact_confirmation": "Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren.",
"delete_contact_confirmation_with_quotas": "{value, plural, one {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn dieser Kontakt Antworten hat, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.} other {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesen Kontakten verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn diesen Kontakten Antworten haben, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.}}",
"displays": "Anzeigen",
"edit_attribute": "Attribut bearbeiten",
"edit_attribute_description": "Aktualisieren Sie die Bezeichnung und Beschreibung für dieses Attribut.",
"edit_attribute_values": "Attribute bearbeiten",
"edit_attribute_values_description": "Ändern Sie die Werte für bestimmte Attribute dieses Kontakts.",
"edit_attributes": "Attribute bearbeiten",
"edit_attributes_success": "Kontaktattribute erfolgreich aktualisiert",
"generate_personal_link": "Persönlichen Link generieren",
"generate_personal_link_description": "Wähle eine veröffentlichte Umfrage aus, um einen personalisierten Link für diesen Kontakt zu generieren.",
"invalid_csv_column_names": "Ungültige CSV-Spaltennamen: {columns}. Spaltennamen, die zu neuen Attributen werden, dürfen nur Kleinbuchstaben, Zahlen und Unterstriche enthalten und müssen mit einem Buchstaben beginnen.",
"invalid_date_format": "Ungültiges Datumsformat. Bitte verwende ein gültiges Datum.",
"invalid_number_format": "Ungültiges Zahlenformat. Bitte gib eine gültige Zahl ein.",
"no_activity_yet": "Noch keine Aktivität",
"no_published_link_surveys_available": "Keine veröffentlichten Link-Umfragen verfügbar. Bitte veröffentliche zuerst eine Link-Umfrage.",
"no_published_surveys": "Keine veröffentlichten Umfragen",
"no_responses_found": "Keine Antworten gefunden",
"not_provided": "Nicht angegeben",
"number_value_required": "Zahlenwert ist erforderlich. Verwende die Löschen-Schaltfläche, um dieses Attribut zu entfernen.",
"personal_link_generated": "Persönlicher Link erfolgreich generiert",
"personal_link_generated_but_clipboard_failed": "Persönlicher Link wurde generiert, konnte aber nicht in die Zwischenablage kopiert werden: {url}",
"personal_survey_link": "Link zur persönlichen Umfrage",
@@ -691,24 +659,13 @@
"search_contact": "Kontakt suchen",
"select_a_survey": "Wähle eine Umfrage aus",
"select_attribute": "Attribut auswählen",
"select_attribute_key": "Attributschlüssel auswählen",
"survey_viewed": "Umfrage angesehen",
"survey_viewed_at": "Angesehen am",
"system_attributes": "Systemattribute",
"unlock_contacts_description": "Verwalte Kontakte und sende gezielte Umfragen",
"unlock_contacts_title": "Kontakte mit einem höheren Plan freischalten",
"upload_contacts_error_attribute_type_mismatch": "Attribut \"{key}\" ist als \"{dataType}\" definiert, aber die CSV-Datei enthält ungültige Werte: {values}",
"upload_contacts_error_duplicate_mappings": "Doppelte Zuordnungen für folgende Attribute gefunden: {attributes}",
"upload_contacts_error_file_too_large": "Dateigröße überschreitet das maximale Limit von 800KB",
"upload_contacts_error_generic": "Beim Hochladen der Kontakte ist ein Fehler aufgetreten. Bitte versuche es später erneut.",
"upload_contacts_error_invalid_file_type": "Bitte lade eine CSV-Datei hoch",
"upload_contacts_error_no_valid_contacts": "Die hochgeladene CSV-Datei enthält keine gültigen Kontakte. Bitte schaue dir die Beispiel-CSV-Datei für das richtige Format an.",
"upload_contacts_modal_attribute_header": "Formbricks-Attribut",
"upload_contacts_modal_attributes_description": "Ordne die Spalten in deiner CSV den Attributen in Formbricks zu.",
"upload_contacts_modal_attributes_new": "Neues Attribut",
"upload_contacts_modal_attributes_search_or_add": "Attribut suchen oder hinzufügen",
"upload_contacts_modal_attributes_should_be_mapped_to": "sollte zugeordnet werden zu",
"upload_contacts_modal_attributes_title": "Attribute",
"upload_contacts_modal_csv_column_header": "CSV-Spalte",
"upload_contacts_modal_description": "Lade eine CSV hoch, um Kontakte mit Attributen schnell zu importieren",
"upload_contacts_modal_download_example_csv": "Beispiel-CSV herunterladen",
"upload_contacts_modal_duplicates_description": "Wie sollen wir vorgehen, wenn ein Kontakt bereits existiert?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Tabelle verlinken",
"link_new_sheet": "Neues Blatt verknüpfen",
"no_integrations_yet": "Deine verknüpften Tabellen werden hier angezeigt, sobald Du sie hinzufügst ⏲️",
"reconnect_button": "Erneut verbinden",
"reconnect_button_description": "Deine Google Sheets-Verbindung ist abgelaufen. Bitte verbinde dich erneut, um weiterhin Antworten zu synchronisieren. Deine bestehenden Tabellen-Links und Daten bleiben erhalten.",
"reconnect_button_tooltip": "Verbinde die Integration erneut, um deinen Zugriff zu aktualisieren. Deine bestehenden Tabellen-Links und Daten bleiben erhalten.",
"spreadsheet_permission_error": "Du hast keine Berechtigung, auf diese Tabelle zuzugreifen. Bitte stelle sicher, dass die Tabelle mit deinem Google-Konto geteilt ist und du Schreibzugriff auf die Tabelle hast.",
"spreadsheet_url": "Tabellen-URL",
"token_expired_error": "Das Google Sheets-Aktualisierungstoken ist abgelaufen oder wurde widerrufen. Bitte verbinde die Integration erneut."
"spreadsheet_url": "Tabellen-URL"
},
"include_created_at": "Erstellungsdatum einbeziehen",
"include_hidden_fields": "Versteckte Felder (hidden fields) einbeziehen",
@@ -894,40 +846,6 @@
"no_attributes_yet": "Noch keine Attribute",
"no_filters_yet": "Es gibt noch keine Filter",
"no_segments_yet": "Du hast momentan keine gespeicherten Segmente.",
"operator_contains": "enthält",
"operator_does_not_contain": "enthält nicht",
"operator_ends_with": "endet mit",
"operator_is_after": "ist nach",
"operator_is_before": "ist vor",
"operator_is_between": "ist zwischen",
"operator_is_newer_than": "ist neuer als",
"operator_is_not_set": "ist nicht festgelegt",
"operator_is_older_than": "ist älter als",
"operator_is_same_day": "ist am selben Tag",
"operator_is_set": "ist festgelegt",
"operator_starts_with": "fängt an mit",
"operator_title_contains": "Enthält",
"operator_title_does_not_contain": "Enthält nicht",
"operator_title_ends_with": "Endet mit",
"operator_title_equals": "Gleich",
"operator_title_greater_equal": "Größer als oder gleich",
"operator_title_greater_than": "Größer als",
"operator_title_is_after": "Ist nach",
"operator_title_is_before": "Ist vor",
"operator_title_is_between": "Ist zwischen",
"operator_title_is_newer_than": "Ist neuer als",
"operator_title_is_not_set": "Ist nicht festgelegt",
"operator_title_is_older_than": "Ist älter als",
"operator_title_is_same_day": "Ist am selben Tag",
"operator_title_is_set": "Ist festgelegt",
"operator_title_less_equal": "Kleiner oder gleich",
"operator_title_less_than": "Kleiner als",
"operator_title_not_equals": "Ist nicht gleich",
"operator_title_starts_with": "Fängt an mit",
"operator_title_user_is_in": "Nutzer ist in",
"operator_title_user_is_not_in": "Nutzer ist nicht in",
"operator_user_is_in": "Nutzer ist in",
"operator_user_is_not_in": "Nutzer ist nicht in",
"person_and_attributes": "Person & Attribute",
"phone": "Handy",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Bitte entferne das Segment aus diesen Umfragen, um es zu löschen.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "Benutzerzielgruppen sind derzeit nur verfügbar, wenn",
"value_cannot_be_empty": "Wert darf nicht leer sein.",
"value_must_be_a_number": "Wert muss eine Zahl sein.",
"value_must_be_positive": "Wert muss eine positive Zahl sein.",
"view_filters": "Filter anzeigen",
"where": "Wo",
"with_the_formbricks_sdk": "mit dem Formbricks SDK"
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Hey {userName}",
"email_customization_preview_email_text": "Dies ist eine E-Mail-Vorschau, um dir zu zeigen, welches Logo in den E-Mails gerendert wird.",
"error_deleting_organization_please_try_again": "Fehler beim Löschen der Organisation. Bitte versuche es erneut.",
"from_your_organization": "{memberName} aus Ihrer Organisation",
"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}",
@@ -1250,7 +1167,6 @@
"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": "Gilt nur für In-Product-Umfragen.",
"add_logic": "Logik hinzufügen",
"add_none_of_the_above": "Füge \"Keine der oben genannten Optionen\" hinzu",
"add_option": "Option hinzufügen",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "Nachverfolgung aktualisiert und wird gespeichert, sobald du die Umfrage speicherst.",
"follow_ups_new": "Neues Follow-up",
"follow_ups_upgrade_button_text": "Upgrade, um Follow-ups zu aktivieren",
"form_styling": "Umfrage Styling",
"formbricks_sdk_is_not_connected": "Formbricks SDK ist nicht verbunden",
"four_points": "4 Punkte",
"heading": "Überschrift",
@@ -1621,7 +1538,7 @@
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
"response_options": "Antwortoptionen",
"roundness": "Rundheit",
"roundness_description": "Steuert, wie abgerundet die Ecken sind.",
"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",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
"survey_placement": "Platzierung der Umfrage",
"survey_styling": "Umfrage Styling",
"survey_trigger": "Auslöser der Umfrage",
"switch_multi_language_on_to_get_started": "Aktiviere Mehrsprachigkeit, um loszulegen 👉",
"target_block_not_found": "Zielblock nicht gefunden",
@@ -1758,6 +1674,7 @@
"welcome_message": "Willkommensnachricht",
"without_a_filter_all_of_your_users_can_be_surveyed": "Ohne Filter können alle deine Nutzer befragt werden.",
"you_have_not_created_a_segment_yet": "Du hast noch keinen Segment erstellt.",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Sie müssen zwei oder mehr Sprachen in Ihrem Workspace eingerichtet haben, um mit Übersetzungen zu arbeiten.",
"your_description_here_recall_information_with": "Deine Beschreibung hier. Informationen abrufen mit @",
"your_question_here_recall_information_with": "Deine Frage hier. Informationen abrufen mit @",
"your_web_app": "Deine Web-App",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Gefilterte Antworten (Excel)",
"generating_qr_code": "QR-Code wird generiert",
"impressions": "Eindrücke",
"impressions_identified_only": "Zeigt nur Impressionen von identifizierten Kontakten",
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
"in_app": {
"connection_description": "Die Umfrage wird den Nutzern Ihrer Website angezeigt, die den unten aufgeführten Kriterien entsprechen",
@@ -2008,7 +1924,6 @@
"last_quarter": "Letztes Quartal",
"last_year": "Letztes Jahr",
"limit": "Limit",
"no_identified_impressions": "Keine Impressionen von identifizierten Kontakten",
"no_responses_found": "Keine Antworten gefunden",
"other_values_found": "Andere Werte gefunden",
"overall": "Insgesamt",
@@ -2173,12 +2088,12 @@
"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": "Mindesthöhe",
"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 Mindesthöhe der Eingabe.",
"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.",
@@ -2187,8 +2102,6 @@
"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": "Rahmenfarbe",
"advanced_styling_field_option_border_description": "Umrandet Radio- und Checkbox-Optionen.",
"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",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "\"Powered by Formbricks\"-Signatur anzeigen",
"styling_updated_successfully": "Styling erfolgreich aktualisiert",
"suggest_colors": "Farben vorschlagen",
"suggested_colors_applied_please_save": "Vorgeschlagene Farben erfolgreich generiert. Drücke \"Speichern\", um die Änderungen zu übernehmen.",
"theme": "Theme",
"theme_settings_description": "Erstelle ein Style-Theme für alle Umfragen. Du kannst für jede Umfrage individuelles Styling aktivieren."
},
@@ -3008,9 +2920,6 @@
"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_question_open_text_headline": "Möchtest Du noch etwas teilen?",
"preview_survey_question_open_text_placeholder": "Tippe deine Antwort hier...",
"preview_survey_question_open_text_subheader": "Dein Feedback hilft uns, besser zu werden.",
"preview_survey_welcome_card_headline": "Willkommen!",
"prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.",
"prioritize_features_name": "Funktionen priorisieren",
+27 -118
View File
@@ -39,14 +39,14 @@
},
"invite": {
"create_account": "Create an account",
"email_does_not_match": "Ooops! Wrong email 🤦",
"email_does_not_match": "Ooops! Wrong email \uD83E\uDD26",
"email_does_not_match_description": "The email in the invitation does not match yours.",
"go_to_app": "Go to app",
"happy_to_have_you": "Happy to have you 🤗",
"happy_to_have_you": "Happy to have you \uD83E\uDD17",
"happy_to_have_you_description": "Please create an account or login.",
"invite_expired": "Invite expired 😥",
"invite_expired": "Invite expired \uD83D\uDE25",
"invite_expired_description": "Invites are valid for 7 days. Please request a new invite.",
"invite_not_found": "Invite not found 😥",
"invite_not_found": "Invite not found \uD83D\uDE25",
"invite_not_found_description": "The invitation code cannot be found or has already been used.",
"login": "Login",
"welcome_to_organization": "You are in \uD83C\uDF89",
@@ -188,10 +188,8 @@
"customer_success": "Customer Success",
"dark_overlay": "Dark overlay",
"date": "Date",
"days": "days",
"default": "Default",
"delete": "Delete",
"delete_what": "Delete {deleteWhat}",
"description": "Description",
"dev_env": "Dev Environment",
"development": "Development",
@@ -207,8 +205,6 @@
"download": "Download",
"draft": "Draft",
"duplicate": "Duplicate",
"duplicate_copy": "(copy)",
"duplicate_copy_number": "(copy {copyNumber})",
"e_commerce": "E-Commerce",
"edit": "Edit",
"email": "Email",
@@ -221,16 +217,13 @@
"error": "Error",
"error_component_description": "This resource does not exist or you do not have the necessary rights to access it.",
"error_component_title": "Error loading resources",
"error_loading_data": "Error loading data",
"error_rate_limit_description": "Maximum number of requests reached. Please try again later.",
"error_rate_limit_title": "Rate Limit Exceeded",
"expand_rows": "Expand rows",
"failed_to_copy_to_clipboard": "Failed to copy to clipboard",
"failed_to_load_organizations": "Failed to load organizations",
"failed_to_load_workspaces": "Failed to load workspaces",
"filter": "Filter",
"finish": "Finish",
"first_name": "First Name",
"follow_these": "Follow these",
"formbricks_version": "Formbricks Version",
"full_name": "Full name",
@@ -243,7 +236,6 @@
"hidden_field": "Hidden field",
"hidden_fields": "Hidden fields",
"hide_column": "Hide column",
"id": "ID",
"image": "Image",
"images": "Images",
"import": "Import",
@@ -261,7 +253,6 @@
"key": "Key",
"label": "Label",
"language": "Language",
"last_name": "Last Name",
"learn_more": "Learn more",
"license_expired": "License Expired",
"light_overlay": "Light overlay",
@@ -284,11 +275,9 @@
"mobile_overlay_app_works_best_on_desktop": "Formbricks works best on a bigger screen. To manage or build surveys, switch to another device.",
"mobile_overlay_surveys_look_good": "Do not worry your surveys look great on every device and screen size!",
"mobile_overlay_title": "Oops, tiny screen detected!",
"months": "months",
"move_down": "Move down",
"move_up": "Move up",
"multiple_languages": "Multiple languages",
"my_product": "my Product",
"name": "Name",
"new": "New",
"new_version_available": "Formbricks {version} is here. Upgrade now!",
@@ -404,7 +393,6 @@
"status": "Status",
"step_by_step_manual": "Step by step manual",
"storage_not_configured": "File storage not set up, uploads will likely fail",
"string": "Text",
"styling": "Styling",
"submit": "Submit",
"summary": "Summary",
@@ -437,7 +425,6 @@
"top_right": "Top Right",
"try_again": "Try again",
"type": "Type",
"unknown_survey": "Unknown survey",
"unlock_more_workspaces_with_a_higher_plan": "Unlock more workspaces with a higher plan.",
"update": "Update",
"updated": "Updated",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Website & App Connection",
"website_app_survey": "Website & App Survey",
"website_survey": "Website Survey",
"weeks": "weeks",
"welcome_card": "Welcome card",
"workspace_configuration": "Workspace Configuration",
"workspace_created_successfully": "Workspace created successfully",
@@ -472,7 +458,6 @@
"workspace_not_found": "Workspace not found",
"workspace_permission_not_found": "Workspace permission not found",
"workspaces": "Workspaces",
"years": "years",
"you": "You",
"you_are_downgraded_to_the_community_edition": "You are downgraded to the Community Edition.",
"you_are_not_authorized_to_perform_this_action": "You are not authorized to perform this action.",
@@ -535,7 +520,7 @@
"text_variable": "Text variable",
"verification_email_click_on_this_link": "You can also click on this link:",
"verification_email_heading": "Almost there!",
"verification_email_hey": "Hey 👋",
"verification_email_hey": "Hey \uD83D\uDC4B",
"verification_email_if_expired_request_new_token": "If it has expired please request a new token here:",
"verification_email_link_valid_for_24_hours": "The link is valid for 24 hours.",
"verification_email_request_new_verification": "Request new verification",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Attribute updated successfully",
"attribute_value": "Value",
"attribute_value_placeholder": "Attribute Value",
"attributes_msg_attribute_limit_exceeded": "Could not create {count} new attribute(s) as it would exceed the maximum limit of {limit} attribute classes. Existing attributes were updated successfully.",
"attributes_msg_attribute_type_validation_error": "{error} (attribute '{key}' has dataType: {dataType})",
"attributes_msg_email_already_exists": "The email already exists for this environment and was not updated.",
"attributes_msg_email_or_userid_required": "Either email or userId is required. The existing values were preserved.",
"attributes_msg_new_attribute_created": "Created new attribute '{key}' with type '{dataType}'",
"attributes_msg_userid_already_exists": "The userId already exists for this environment and was not updated.",
"contact_deleted_successfully": "Contact deleted successfully",
"contact_not_found": "No such contact found",
"contacts_table_refresh": "Refresh contacts",
"contacts_table_refresh_success": "Contacts refreshed successfully",
"create_attribute": "Create attribute",
"create_key": "Create Key",
"create_new_attribute": "Create new attribute",
"create_new_attribute_description": "Create a new attribute for segmentation purposes.",
"custom_attributes": "Custom Attributes",
"data_type": "Data Type",
"data_type_cannot_be_changed": "Data type cannot be changed after creation",
"data_type_description": "Choose how this attribute should be stored and filtered",
"date_value_required": "Date value is required. Use the delete button to remove this attribute if you don't want to set a date.",
"delete_attribute_confirmation": "{value, plural, one {This will delete the selected attribute. Any contact data associated with this attribute will be lost.} other {This will delete the selected attributes. Any contact data associated with these attributes will be lost.}}",
"delete_contact_confirmation": "This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contacts data will be lost.",
"delete_contact_confirmation_with_quotas": "{value, plural, one {This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contacts data will be lost. If this contact has responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.} other {This will delete all survey responses and contact attributes associated with these contacts. Any targeting and personalization based on these contacts data will be lost. If these contacts have responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.}}",
"displays": "Displays",
"edit_attribute": "Edit attribute",
"edit_attribute_description": "Update the label and description for this attribute.",
"edit_attribute_values": "Edit attributes",
"edit_attribute_values_description": "Change the values for specific attributes for this contact.",
"edit_attributes": "Edit Attributes",
"edit_attributes_success": "Contact attributes updated successfully",
"generate_personal_link": "Generate Personal Link",
"generate_personal_link_description": "Select a published survey to generate a personalized link for this contact.",
"invalid_csv_column_names": "Invalid CSV column name(s): {columns}. Column names that will become new attributes must only contain lowercase letters, numbers, and underscores, and must start with a letter.",
"invalid_date_format": "Invalid date format. Please use a valid date.",
"invalid_number_format": "Invalid number format. Please enter a valid number.",
"no_activity_yet": "No activity yet",
"no_published_link_surveys_available": "No published link surveys available. Please publish a link survey first.",
"no_published_surveys": "No published surveys",
"no_responses_found": "No responses found",
"not_provided": "Not provided",
"number_value_required": "Number value is required. Use the delete button to remove this attribute.",
"personal_link_generated": "Personal link generated successfully",
"personal_link_generated_but_clipboard_failed": "Personal link generated but failed to copy to clipboard: {url}",
"personal_survey_link": "Personal Survey Link",
@@ -691,24 +659,13 @@
"search_contact": "Search contact",
"select_a_survey": "Select a survey",
"select_attribute": "Select Attribute",
"select_attribute_key": "Select attribute key",
"survey_viewed": "Survey viewed",
"survey_viewed_at": "Viewed At",
"system_attributes": "System Attributes",
"unlock_contacts_description": "Manage contacts and send out targeted surveys",
"unlock_contacts_title": "Unlock contacts with a higher plan",
"upload_contacts_error_attribute_type_mismatch": "Attribute \"{key}\" is typed as \"{dataType}\" but CSV contains invalid values: {values}",
"upload_contacts_error_duplicate_mappings": "Duplicate mappings found for the following attributes: {attributes}",
"upload_contacts_error_file_too_large": "File size exceeds the maximum limit of 800KB",
"upload_contacts_error_generic": "An error occurred while uploading the contacts. Please try again later.",
"upload_contacts_error_invalid_file_type": "Please upload a CSV file",
"upload_contacts_error_no_valid_contacts": "The uploaded CSV file does not contain any valid contacts, please see the sample CSV file for the correct format.",
"upload_contacts_modal_attribute_header": "Formbricks Attribute",
"upload_contacts_modal_attributes_description": "Map the columns in your CSV to the attributes in Formbricks.",
"upload_contacts_modal_attributes_new": "New attribute",
"upload_contacts_modal_attributes_search_or_add": "Search or add attribute",
"upload_contacts_modal_attributes_should_be_mapped_to": "should be mapped to",
"upload_contacts_modal_attributes_title": "Attributes",
"upload_contacts_modal_csv_column_header": "CSV Column",
"upload_contacts_modal_description": "Upload a CSV to quickly import contacts with attributes",
"upload_contacts_modal_download_example_csv": "Download example CSV",
"upload_contacts_modal_duplicates_description": "How should we handle if a contact already exists in your contacts?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Link Google Sheet",
"link_new_sheet": "Link new Sheet",
"no_integrations_yet": "Your google sheet integrations will appear here as soon as you add them. ⏲️",
"reconnect_button": "Reconnect",
"reconnect_button_description": "Your Google Sheets connection has expired. Please reconnect to continue syncing responses. Your existing spreadsheet links and data will be preserved.",
"reconnect_button_tooltip": "Reconnect the integration to refresh your access. Your existing spreadsheet links and data will be preserved.",
"spreadsheet_permission_error": "You don't have permission to access this spreadsheet. Please ensure the spreadsheet is shared with your Google account and you have write access to the spreadsheet.",
"spreadsheet_url": "Spreadsheet URL",
"token_expired_error": "Google Sheets refresh token has expired or been revoked. Please reconnect the integration."
"spreadsheet_url": "Spreadsheet URL"
},
"include_created_at": "Include Created At",
"include_hidden_fields": "Include Hidden Fields",
@@ -894,40 +846,6 @@
"no_attributes_yet": "No attributes yet!",
"no_filters_yet": "There are no filters yet!",
"no_segments_yet": "You currently have no saved segments.",
"operator_contains": "contains",
"operator_does_not_contain": "does not contain",
"operator_ends_with": "ends with",
"operator_is_after": "is after",
"operator_is_before": "is before",
"operator_is_between": "is between",
"operator_is_newer_than": "is newer than",
"operator_is_not_set": "is not set",
"operator_is_older_than": "is older than",
"operator_is_same_day": "is same day",
"operator_is_set": "is set",
"operator_starts_with": "starts with",
"operator_title_contains": "Contains",
"operator_title_does_not_contain": "Does not contain",
"operator_title_ends_with": "Ends with",
"operator_title_equals": "Equals",
"operator_title_greater_equal": "Greater than or equal to",
"operator_title_greater_than": "Greater than",
"operator_title_is_after": "Is after",
"operator_title_is_before": "Is before",
"operator_title_is_between": "Is between",
"operator_title_is_newer_than": "Is newer than",
"operator_title_is_not_set": "Is not set",
"operator_title_is_older_than": "Is older than",
"operator_title_is_same_day": "Is same day",
"operator_title_is_set": "Is set",
"operator_title_less_equal": "Less than or equal to",
"operator_title_less_than": "Less than",
"operator_title_not_equals": "Not equals to",
"operator_title_starts_with": "Starts with",
"operator_title_user_is_in": "User is in",
"operator_title_user_is_not_in": "User is not in",
"operator_user_is_in": "User is in",
"operator_user_is_not_in": "User is not in",
"person_and_attributes": "Person & Attributes",
"phone": "Phone",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Please remove the segment from these surveys in order to delete it.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "User targeting is currently only available when",
"value_cannot_be_empty": "Value cannot be empty.",
"value_must_be_a_number": "Value must be a number.",
"value_must_be_positive": "Value must be a positive number.",
"view_filters": "View filters",
"where": "Where",
"with_the_formbricks_sdk": "with the Formbricks SDK"
@@ -1039,13 +956,13 @@
"enterprise_features": "Enterprise Features",
"get_an_enterprise_license_to_get_access_to_all_features": "Get an Enterprise license to get access to all features.",
"keep_full_control_over_your_data_privacy_and_security": "Keep full control over your data privacy and security.",
"license_invalid_description": "The license key in your ENTERPRISE_LICENSE_KEY environment variable is not valid. Please check for typos or request a new key.",
"license_status": "License Status",
"license_status_active": "Active",
"license_status_description": "Status of your enterprise license.",
"license_status_expired": "Expired",
"license_status_invalid": "Invalid License",
"license_status_unreachable": "Unreachable",
"license_invalid_description": "The license key in your ENTERPRISE_LICENSE_KEY environment variable is not valid. Please check for typos or request a new key.",
"license_unreachable_grace_period": "License server cannot be reached. Your enterprise features remain active during a 3-day grace period ending {gracePeriodEnd}.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "No call needed, no strings attached: Request a free 30-day trial license to test all features by filling out this form:",
"no_credit_card_no_sales_call_just_test_it": "No credit card. No sales call. Just test it :)",
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Hey {userName}",
"email_customization_preview_email_text": "This is an email preview to show you which logo will be rendered in the emails.",
"error_deleting_organization_please_try_again": "Error deleting organization. Please try again.",
"from_your_organization": "{memberName} from your organization",
"from_your_organization": "from your organization",
"invitation_sent_once_more": "Invitation sent once more.",
"invite_deleted_successfully": "Invite deleted successfully",
"invite_expires_on": "Invite expires on {date}",
@@ -1250,7 +1167,6 @@
"add_fallback_placeholder": "Add a placeholder to show if there is no value to recall.",
"add_hidden_field_id": "Add hidden field ID",
"add_highlight_border": "Add highlight border",
"add_highlight_border_description": "Only applies to in-product surveys.",
"add_logic": "Add logic",
"add_none_of_the_above": "Add “None of the Above”",
"add_option": "Add option",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "Follow-up updated and will be saved once you save the survey.",
"follow_ups_new": "New follow-up",
"follow_ups_upgrade_button_text": "Upgrade to enable follow-ups",
"form_styling": "Form styling",
"formbricks_sdk_is_not_connected": "Formbricks SDK is not connected",
"four_points": "4 points",
"heading": "Heading",
@@ -1621,7 +1538,7 @@
"response_limits_redirections_and_more": "Response limits, redirections and more.",
"response_options": "Response Options",
"roundness": "Roundness",
"roundness_description": "Controls how rounded corners are.",
"roundness_description": "Controls how rounded the card corners are.",
"row_used_in_logic_error": "This row is used in logic of question {questionIndex}. Please remove it from logic first.",
"rows": "Rows",
"save_and_close": "Save & Close",
@@ -1667,9 +1584,8 @@
"survey_completed_subheading": "This free & open-source survey has been closed",
"survey_display_settings": "Survey Display Settings",
"survey_placement": "Survey Placement",
"survey_styling": "Survey styling",
"survey_trigger": "Survey Trigger",
"switch_multi_language_on_to_get_started": "Switch multi-language on to get started 👉",
"switch_multi_language_on_to_get_started": "Switch multi-language on to get started \uD83D\uDC49",
"target_block_not_found": "Target block not found",
"targeted": "Targeted",
"ten_points": "10 points",
@@ -1758,6 +1674,7 @@
"welcome_message": "Welcome message",
"without_a_filter_all_of_your_users_can_be_surveyed": "Without a filter, all of your users can be surveyed.",
"you_have_not_created_a_segment_yet": "You have not created a segment yet",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "You need to have two or more languages set up in your workspace to work with translations.",
"your_description_here_recall_information_with": "Your description here. Recall information with @",
"your_question_here_recall_information_with": "Your question here. Recall information with @",
"your_web_app": "Your web app",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Filtered responses (Excel)",
"generating_qr_code": "Generating QR code",
"impressions": "Impressions",
"impressions_identified_only": "Only showing impressions from identified contacts",
"impressions_tooltip": "Number of times the survey has been viewed.",
"in_app": {
"connection_description": "The survey will be shown to users of your website, that match the criteria listed below",
@@ -2008,7 +1924,6 @@
"last_quarter": "Last quarter",
"last_year": "Last year",
"limit": "Limit",
"no_identified_impressions": "No impressions from identified contacts",
"no_responses_found": "No responses found",
"other_values_found": "Other values found",
"overall": "Overall",
@@ -2088,7 +2003,7 @@
"formbricks_sdk_not_connected_description": "Add the Formbricks SDK to your website or app to connect it with Formbricks",
"how_to_setup": "How to setup",
"how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.",
"receiving_data": "Receiving data 💃🕺",
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
"recheck": "Re-check",
"sdk_connection_details": "SDK Connection Details",
"sdk_connection_details_description": "Your unique environment ID and SDK connection URL for integrating Formbricks with your application.",
@@ -2164,7 +2079,7 @@
"advanced_styling_field_description_size": "Description Font Size",
"advanced_styling_field_description_size_description": "Scales the description text.",
"advanced_styling_field_description_weight": "Description Font Weight",
"advanced_styling_field_description_weight_description": "Makes descr. text lighter or bolder.",
"advanced_styling_field_description_weight_description": "Makes description text lighter or bolder.",
"advanced_styling_field_font_size": "Font Size",
"advanced_styling_field_font_weight": "Font Weight",
"advanced_styling_field_headline_color": "Headline Color",
@@ -2173,12 +2088,12 @@
"advanced_styling_field_headline_size_description": "Scales the headline text.",
"advanced_styling_field_headline_weight": "Headline Font Weight",
"advanced_styling_field_headline_weight_description": "Makes headline text lighter or bolder.",
"advanced_styling_field_height": "Minimum Height",
"advanced_styling_field_height": "Height",
"advanced_styling_field_indicator_bg": "Indicator Background",
"advanced_styling_field_indicator_bg_description": "Colors the filled portion of the bar.",
"advanced_styling_field_input_border_radius_description": "Rounds the input corners.",
"advanced_styling_field_input_font_size_description": "Scales the typed text in inputs.",
"advanced_styling_field_input_height_description": "Controls the min. height of the input.",
"advanced_styling_field_input_height_description": "Controls the input field height.",
"advanced_styling_field_input_padding_x_description": "Adds space on the left and right.",
"advanced_styling_field_input_padding_y_description": "Adds space on the top and bottom.",
"advanced_styling_field_input_placeholder_opacity_description": "Fades the placeholder hint text.",
@@ -2187,8 +2102,6 @@
"advanced_styling_field_input_text_description": "Colors the typed text in inputs.",
"advanced_styling_field_option_bg": "Background",
"advanced_styling_field_option_bg_description": "Fills the option items.",
"advanced_styling_field_option_border": "Border Color",
"advanced_styling_field_option_border_description": "Outlines radio and checkbox options.",
"advanced_styling_field_option_border_radius_description": "Rounds the option corners.",
"advanced_styling_field_option_font_size_description": "Scales the option label text.",
"advanced_styling_field_option_label": "Label Color",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "Show “Powered by Formbricks” Signature",
"styling_updated_successfully": "Styling updated successfully",
"suggest_colors": "Suggest colors",
"suggested_colors_applied_please_save": "Suggested colors generated successfully. Press \"Save\" to persist the changes.",
"theme": "Theme",
"theme_settings_description": "Create a style theme for all surveys. You can enable custom styling for each survey."
},
@@ -2349,7 +2261,7 @@
"setup": {
"intro": {
"get_started": "Get started",
"made_with_love_in_kiel": "Made with 🤍 in Germany",
"made_with_love_in_kiel": "Made with \uD83E\uDD0D in Germany",
"paragraph_1": "Formbricks is an Experience Management Suite built on the <b>fastest growing open-source survey platform</b> worldwide.",
"paragraph_2": "Run targeted surveys on websites, in apps or anywhere online. Gather valuable insights to <b>craft irresistible experiences</b> for customers, users and employees.",
"paragraph_3": "We are committed to the highest degree of data privacy. Self-host to keep <b>full control over your data</b>.",
@@ -2499,7 +2411,7 @@
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
"churn_survey_question_4_headline": "What features are you missing?",
"churn_survey_question_5_button_label": "Send email to CEO",
"churn_survey_question_5_headline": "So sorry to hear 😔 Talk to our CEO directly!",
"churn_survey_question_5_headline": "So sorry to hear \uD83D\uDE14 Talk to our CEO directly!",
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
"collect_feedback_description": "Gather comprehensive feedback on your product or service.",
"collect_feedback_name": "Collect Feedback",
@@ -2627,8 +2539,8 @@
"default_welcome_card_html": "Thanks for providing your feedback - lets go!",
"docs_feedback_description": "Measure how clear each page of your developer documentation is.",
"docs_feedback_name": "Docs Feedback",
"docs_feedback_question_1_choice_1": "Yes 👍",
"docs_feedback_question_1_choice_2": "No 👎",
"docs_feedback_question_1_choice_1": "Yes \uD83D\uDC4D",
"docs_feedback_question_1_choice_2": "No \uD83D\uDC4E",
"docs_feedback_question_1_headline": "Was this page helpful?",
"docs_feedback_question_2_headline": "Please elaborate:",
"docs_feedback_question_3_headline": "Page URL",
@@ -2758,7 +2670,7 @@
"file_upload": "File Upload",
"file_upload_description": "Enable respondents to upload documents, images, or other files",
"finish": "Finish",
"follow_ups_modal_action_body": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Hey 👋</span><br><br><span style=\"white-space: pre-wrap;\">Thanks for taking the time to respond, we will be in touch shortly.</span><br><br><span style=\"white-space: pre-wrap;\">Have a great day!</span></p>",
"follow_ups_modal_action_body": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Hey \uD83D\uDC4B</span><br><br><span style=\"white-space: pre-wrap;\">Thanks for taking the time to respond, we will be in touch shortly.</span><br><br><span style=\"white-space: pre-wrap;\">Have a great day!</span></p>",
"free_text": "Free text",
"free_text_description": "Collect open-ended feedback",
"free_text_placeholder": "Type your answer here…",
@@ -2796,7 +2708,7 @@
"identify_sign_up_barriers_question_8_placeholder": "Type your answer here…",
"identify_sign_up_barriers_question_9_button_label": "Sign Up",
"identify_sign_up_barriers_question_9_headline": "Thanks! Here is your code: SIGNUPNOW10",
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Thanks a lot for taking the time to share feedback 🙏</span></p>",
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Thanks a lot for taking the time to share feedback \uD83D\uDE4F</span></p>",
"identify_upsell_opportunities_description": "Find out how much time your product saves your user. Use it to upsell.",
"identify_upsell_opportunities_name": "Identify Upsell Opportunities",
"identify_upsell_opportunities_question_1_choice_1": "Less than 1 hour",
@@ -3008,9 +2920,6 @@
"preview_survey_question_2_choice_2_label": "No, thank you!",
"preview_survey_question_2_headline": "Want to stay in the loop?",
"preview_survey_question_2_subheader": "This is an example description.",
"preview_survey_question_open_text_headline": "Anything else you'd like to share?",
"preview_survey_question_open_text_placeholder": "Type your answer here…",
"preview_survey_question_open_text_subheader": "Your feedback helps us improve.",
"preview_survey_welcome_card_headline": "Welcome!",
"prioritize_features_description": "Identify features your users need most and least.",
"prioritize_features_name": "Prioritize Features",
@@ -3128,7 +3037,7 @@
"review_prompt_question_1_lower_label": "Not good",
"review_prompt_question_1_upper_label": "Very satisfied",
"review_prompt_question_2_button_label": "Write review",
"review_prompt_question_2_headline": "Happy to hear 🙏 Please write a review for us!",
"review_prompt_question_2_headline": "Happy to hear \uD83D\uDE4F Please write a review for us!",
"review_prompt_question_2_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>This helps us a lot.</span></p>",
"review_prompt_question_3_button_label": "Send",
"review_prompt_question_3_headline": "Sorry to hear! What is ONE thing we can do better?",
@@ -3171,7 +3080,7 @@
"smileys_survey_question_1_lower_label": "Not good",
"smileys_survey_question_1_upper_label": "Very satisfied",
"smileys_survey_question_2_button_label": "Write review",
"smileys_survey_question_2_headline": "Happy to hear 🙏 Please write a review for us!",
"smileys_survey_question_2_headline": "Happy to hear \uD83D\uDE4F Please write a review for us!",
"smileys_survey_question_2_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>This helps us a lot.</span></p>",
"smileys_survey_question_3_button_label": "Send",
"smileys_survey_question_3_headline": "Sorry to hear! What is ONE thing we can do better?",
@@ -3182,7 +3091,7 @@
"star_rating_survey_question_1_lower_label": "Extremely dissatisfied",
"star_rating_survey_question_1_upper_label": "Extremely satisfied",
"star_rating_survey_question_2_button_label": "Write review",
"star_rating_survey_question_2_headline": "Happy to hear 🙏 Please write a review for us!",
"star_rating_survey_question_2_headline": "Happy to hear \uD83D\uDE4F Please write a review for us!",
"star_rating_survey_question_2_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>This helps us a lot.</span></p>",
"star_rating_survey_question_3_button_label": "Send",
"star_rating_survey_question_3_headline": "Sorry to hear! What is ONE thing we can do better?",
+10 -101
View File
@@ -188,10 +188,8 @@
"customer_success": "Éxito del cliente",
"dark_overlay": "Superposición oscura",
"date": "Fecha",
"days": "días",
"default": "Predeterminado",
"delete": "Eliminar",
"delete_what": "Eliminar {deleteWhat}",
"description": "Descripción",
"dev_env": "Entorno de desarrollo",
"development": "Desarrollo",
@@ -207,8 +205,6 @@
"download": "Descargar",
"draft": "Borrador",
"duplicate": "Duplicar",
"duplicate_copy": "(copia)",
"duplicate_copy_number": "(copia {copyNumber})",
"e_commerce": "Comercio electrónico",
"edit": "Editar",
"email": "Email",
@@ -221,16 +217,13 @@
"error": "Error",
"error_component_description": "Este recurso no existe o no tienes los derechos necesarios para acceder a él.",
"error_component_title": "Error al cargar recursos",
"error_loading_data": "Error al cargar los datos",
"error_rate_limit_description": "Número máximo de solicitudes alcanzado. Por favor, inténtalo de nuevo más tarde.",
"error_rate_limit_title": "Límite de frecuencia excedido",
"expand_rows": "Expandir filas",
"failed_to_copy_to_clipboard": "Error al copiar al portapapeles",
"failed_to_load_organizations": "Error al cargar organizaciones",
"failed_to_load_workspaces": "Error al cargar los proyectos",
"filter": "Filtro",
"finish": "Finalizar",
"first_name": "Nombre",
"follow_these": "Sigue estos",
"formbricks_version": "Versión de Formbricks",
"full_name": "Nombre completo",
@@ -243,7 +236,6 @@
"hidden_field": "Campo oculto",
"hidden_fields": "Campos ocultos",
"hide_column": "Ocultar columna",
"id": "ID",
"image": "Imagen",
"images": "Imágenes",
"import": "Importar",
@@ -261,7 +253,6 @@
"key": "Clave",
"label": "Etiqueta",
"language": "Idioma",
"last_name": "Apellido",
"learn_more": "Saber más",
"license_expired": "License Expired",
"light_overlay": "Superposición clara",
@@ -284,11 +275,9 @@
"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!",
"months": "meses",
"move_down": "Mover hacia abajo",
"move_up": "Mover hacia arriba",
"multiple_languages": "Múltiples idiomas",
"my_product": "mi producto",
"name": "Nombre",
"new": "Nuevo",
"new_version_available": "Formbricks {version} está aquí. ¡Actualiza ahora!",
@@ -404,7 +393,6 @@
"status": "Estado",
"step_by_step_manual": "Manual paso a paso",
"storage_not_configured": "Almacenamiento de archivos no configurado, es probable que fallen las subidas",
"string": "Texto",
"styling": "Estilo",
"submit": "Enviar",
"summary": "Resumen",
@@ -437,7 +425,6 @@
"top_right": "Superior derecha",
"try_again": "Intentar de nuevo",
"type": "Tipo",
"unknown_survey": "Encuesta desconocida",
"unlock_more_workspaces_with_a_higher_plan": "Desbloquea más proyectos con un plan superior.",
"update": "Actualizar",
"updated": "Actualizado",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Conexión de sitio web y aplicación",
"website_app_survey": "Encuesta de sitio web y aplicación",
"website_survey": "Encuesta de sitio web",
"weeks": "semanas",
"welcome_card": "Tarjeta de bienvenida",
"workspace_configuration": "Configuración del proyecto",
"workspace_created_successfully": "Proyecto creado correctamente",
@@ -472,7 +458,6 @@
"workspace_not_found": "Proyecto no encontrado",
"workspace_permission_not_found": "Permiso del proyecto no encontrado",
"workspaces": "Proyectos",
"years": "años",
"you": "Tú",
"you_are_downgraded_to_the_community_edition": "Has sido degradado a la edición Community.",
"you_are_not_authorized_to_perform_this_action": "No tienes autorización para realizar esta acción.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Atributo actualizado con éxito",
"attribute_value": "Valor",
"attribute_value_placeholder": "Valor del atributo",
"attributes_msg_attribute_limit_exceeded": "No se pudieron crear {count} atributo(s) nuevo(s) ya que se excedería el límite máximo de {limit} clases de atributos. Los atributos existentes se actualizaron correctamente.",
"attributes_msg_attribute_type_validation_error": "{error} (el atributo '{key}' tiene dataType: {dataType})",
"attributes_msg_email_already_exists": "El email ya existe para este entorno y no se actualizó.",
"attributes_msg_email_or_userid_required": "Se requiere email o userId. Se conservaron los valores existentes.",
"attributes_msg_new_attribute_created": "Se creó el atributo nuevo '{key}' con tipo '{dataType}'",
"attributes_msg_userid_already_exists": "El userId ya existe para este entorno y no se actualizó.",
"contact_deleted_successfully": "Contacto eliminado correctamente",
"contact_not_found": "No se ha encontrado dicho contacto",
"contacts_table_refresh": "Actualizar contactos",
"contacts_table_refresh_success": "Contactos actualizados correctamente",
"create_attribute": "Crear atributo",
"create_key": "Crear clave",
"create_new_attribute": "Crear atributo nuevo",
"create_new_attribute_description": "Crea un atributo nuevo para fines de segmentación.",
"custom_attributes": "Atributos personalizados",
"data_type": "Tipo de dato",
"data_type_cannot_be_changed": "El tipo de dato no se puede cambiar después de la creación",
"data_type_description": "Elige cómo debe almacenarse y filtrarse este atributo",
"date_value_required": "Se requiere un valor de fecha. Usa el botón de eliminar para quitar este atributo si no quieres establecer una fecha.",
"delete_attribute_confirmation": "{value, plural, one {Esto eliminará el atributo seleccionado. Se perderán todos los datos de contacto asociados con este atributo.} other {Esto eliminará los atributos seleccionados. Se perderán todos los datos de contacto asociados con estos atributos.}}",
"delete_contact_confirmation": "Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con este contacto. Cualquier segmentación y personalización basada en los datos de este contacto se perderá.",
"delete_contact_confirmation_with_quotas": "{value, plural, one {Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con este contacto. Cualquier segmentación y personalización basada en los datos de este contacto se perderá. Si este contacto tiene respuestas que cuentan para las cuotas de encuesta, los recuentos de cuota se reducirán pero los límites de cuota permanecerán sin cambios.} other {Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con estos contactos. Cualquier segmentación y personalización basada en los datos de estos contactos se perderá. Si estos contactos tienen respuestas que cuentan para las cuotas de encuesta, los recuentos de cuota se reducirán pero los límites de cuota permanecerán sin cambios.}}",
"displays": "Visualizaciones",
"edit_attribute": "Editar atributo",
"edit_attribute_description": "Actualiza la etiqueta y la descripción de este atributo.",
"edit_attribute_values": "Editar atributos",
"edit_attribute_values_description": "Cambia los valores de atributos específicos para este contacto.",
"edit_attributes": "Editar atributos",
"edit_attributes_success": "Atributos del contacto actualizados correctamente",
"generate_personal_link": "Generar enlace personal",
"generate_personal_link_description": "Selecciona una encuesta publicada para generar un enlace personalizado para este contacto.",
"invalid_csv_column_names": "Nombre(s) de columna CSV no válido(s): {columns}. Los nombres de columna que se convertirán en nuevos atributos solo deben contener letras minúsculas, números y guiones bajos, y deben comenzar con una letra.",
"invalid_date_format": "Formato de fecha no válido. Por favor, usa una fecha válida.",
"invalid_number_format": "Formato de número no válido. Por favor, introduce un número válido.",
"no_activity_yet": "Aún no hay actividad",
"no_published_link_surveys_available": "No hay encuestas de enlace publicadas disponibles. Por favor, publica primero una encuesta de enlace.",
"no_published_surveys": "No hay encuestas publicadas",
"no_responses_found": "No se encontraron respuestas",
"not_provided": "No proporcionado",
"number_value_required": "Se requiere un valor numérico. Usa el botón de eliminar para quitar este atributo.",
"personal_link_generated": "Enlace personal generado correctamente",
"personal_link_generated_but_clipboard_failed": "Enlace personal generado pero falló al copiar al portapapeles: {url}",
"personal_survey_link": "Enlace personal de encuesta",
@@ -691,24 +659,13 @@
"search_contact": "Buscar contacto",
"select_a_survey": "Selecciona una encuesta",
"select_attribute": "Seleccionar atributo",
"select_attribute_key": "Seleccionar clave de atributo",
"survey_viewed": "Encuesta vista",
"survey_viewed_at": "Vista el",
"system_attributes": "Atributos del sistema",
"unlock_contacts_description": "Gestiona contactos y envía encuestas dirigidas",
"unlock_contacts_title": "Desbloquea contactos con un plan superior",
"upload_contacts_error_attribute_type_mismatch": "El atributo \"{key}\" está tipado como \"{dataType}\" pero el CSV contiene valores no válidos: {values}",
"upload_contacts_error_duplicate_mappings": "Se encontraron mapeos duplicados para los siguientes atributos: {attributes}",
"upload_contacts_error_file_too_large": "El tamaño del archivo supera el límite máximo de 800 KB",
"upload_contacts_error_generic": "Se produjo un error al cargar los contactos. Por favor, inténtalo de nuevo más tarde.",
"upload_contacts_error_invalid_file_type": "Por favor, carga un archivo CSV",
"upload_contacts_error_no_valid_contacts": "El archivo CSV cargado no contiene ningún contacto válido, por favor consulta el archivo CSV de ejemplo para ver el formato correcto.",
"upload_contacts_modal_attribute_header": "Atributo de Formbricks",
"upload_contacts_modal_attributes_description": "Asigna las columnas de tu CSV a los atributos en Formbricks.",
"upload_contacts_modal_attributes_new": "Nuevo atributo",
"upload_contacts_modal_attributes_search_or_add": "Buscar o añadir atributo",
"upload_contacts_modal_attributes_should_be_mapped_to": "debe asignarse a",
"upload_contacts_modal_attributes_title": "Atributos",
"upload_contacts_modal_csv_column_header": "Columna CSV",
"upload_contacts_modal_description": "Sube un CSV para importar rápidamente contactos con atributos",
"upload_contacts_modal_download_example_csv": "Descargar CSV de ejemplo",
"upload_contacts_modal_duplicates_description": "¿Cómo deberíamos manejar si un contacto ya existe en tus contactos?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Vincular Google Sheet",
"link_new_sheet": "Vincular nueva hoja",
"no_integrations_yet": "Tus integraciones de Google Sheet aparecerán aquí tan pronto como las añadas. ⏲️",
"reconnect_button": "Reconectar",
"reconnect_button_description": "Tu conexión con Google Sheets ha caducado. Reconecta para continuar sincronizando respuestas. Tus enlaces de hojas de cálculo y datos existentes se conservarán.",
"reconnect_button_tooltip": "Reconecta la integración para actualizar tu acceso. Tus enlaces de hojas de cálculo y datos existentes se conservarán.",
"spreadsheet_permission_error": "No tienes permiso para acceder a esta hoja de cálculo. Asegúrate de que la hoja de cálculo esté compartida con tu cuenta de Google y de que tengas acceso de escritura a la hoja de cálculo.",
"spreadsheet_url": "URL de la hoja de cálculo",
"token_expired_error": "El token de actualización de Google Sheets ha caducado o ha sido revocado. Reconecta la integración."
"spreadsheet_url": "URL de la hoja de cálculo"
},
"include_created_at": "Incluir fecha de creación",
"include_hidden_fields": "Incluir campos ocultos",
@@ -894,40 +846,6 @@
"no_attributes_yet": "¡Aún no hay atributos!",
"no_filters_yet": "¡Aún no hay filtros!",
"no_segments_yet": "Actualmente no tienes segmentos guardados.",
"operator_contains": "contiene",
"operator_does_not_contain": "no contiene",
"operator_ends_with": "termina con",
"operator_is_after": "es después de",
"operator_is_before": "es antes de",
"operator_is_between": "está entre",
"operator_is_newer_than": "es más reciente que",
"operator_is_not_set": "no está establecido",
"operator_is_older_than": "es más antiguo que",
"operator_is_same_day": "es el mismo día",
"operator_is_set": "está establecido",
"operator_starts_with": "comienza con",
"operator_title_contains": "Contiene",
"operator_title_does_not_contain": "No contiene",
"operator_title_ends_with": "Termina con",
"operator_title_equals": "Es igual a",
"operator_title_greater_equal": "Mayor o igual que",
"operator_title_greater_than": "Mayor que",
"operator_title_is_after": "Es después de",
"operator_title_is_before": "Es antes de",
"operator_title_is_between": "Está entre",
"operator_title_is_newer_than": "Es más reciente que",
"operator_title_is_not_set": "No está establecido",
"operator_title_is_older_than": "Es más antiguo que",
"operator_title_is_same_day": "Es el mismo día",
"operator_title_is_set": "Está establecido",
"operator_title_less_equal": "Menor o igual que",
"operator_title_less_than": "Menor que",
"operator_title_not_equals": "No es igual a",
"operator_title_starts_with": "Comienza con",
"operator_title_user_is_in": "El usuario está en",
"operator_title_user_is_not_in": "El usuario no está en",
"operator_user_is_in": "El usuario está en",
"operator_user_is_not_in": "El usuario no está en",
"person_and_attributes": "Persona y atributos",
"phone": "Teléfono",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Por favor, elimina el segmento de estas encuestas para poder borrarlo.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "La segmentación de usuarios actualmente solo está disponible cuando",
"value_cannot_be_empty": "El valor no puede estar vacío.",
"value_must_be_a_number": "El valor debe ser un número.",
"value_must_be_positive": "El valor debe ser un número positivo.",
"view_filters": "Ver filtros",
"where": "Donde",
"with_the_formbricks_sdk": "con el SDK de Formbricks"
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Hola {userName}",
"email_customization_preview_email_text": "Este es un correo electrónico de vista previa para mostrarte qué logotipo se mostrará en los correos electrónicos.",
"error_deleting_organization_please_try_again": "Error al eliminar la organización. Por favor, inténtalo de nuevo.",
"from_your_organization": "{memberName} de tu organización",
"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}",
@@ -1250,7 +1167,6 @@
"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": "Solo se aplica a encuestas dentro del producto.",
"add_logic": "Añadir lógica",
"add_none_of_the_above": "Añadir \"Ninguna de las anteriores\"",
"add_option": "Añadir opción",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "Seguimiento actualizado y se guardará cuando guardes la encuesta.",
"follow_ups_new": "Nuevo seguimiento",
"follow_ups_upgrade_button_text": "Actualiza para habilitar seguimientos",
"form_styling": "Estilo del formulario",
"formbricks_sdk_is_not_connected": "El SDK de Formbricks no está conectado",
"four_points": "4 puntos",
"heading": "Encabezado",
@@ -1621,7 +1538,7 @@
"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.",
"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",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
"survey_display_settings": "Ajustes de visualización de la encuesta",
"survey_placement": "Ubicación de la encuesta",
"survey_styling": "Estilo del formulario",
"survey_trigger": "Activador de la encuesta",
"switch_multi_language_on_to_get_started": "Activa el modo multiidioma para comenzar 👉",
"target_block_not_found": "Bloque objetivo no encontrado",
@@ -1758,6 +1674,7 @@
"welcome_message": "Mensaje de bienvenida",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sin un filtro, todos tus usuarios pueden ser encuestados.",
"you_have_not_created_a_segment_yet": "Aún no has creado un segmento",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Necesitas tener dos o más idiomas configurados en tu proyecto para trabajar con traducciones.",
"your_description_here_recall_information_with": "Tu descripción aquí. Recupera información con @",
"your_question_here_recall_information_with": "Tu pregunta aquí. Recupera información con @",
"your_web_app": "Tu aplicación web",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Respuestas filtradas (Excel)",
"generating_qr_code": "Generando código QR",
"impressions": "Impresiones",
"impressions_identified_only": "Solo se muestran impresiones de contactos identificados",
"impressions_tooltip": "Número de veces que se ha visto la encuesta.",
"in_app": {
"connection_description": "La encuesta se mostrará a los usuarios de tu sitio web que cumplan con los criterios enumerados a continuación",
@@ -2008,7 +1924,6 @@
"last_quarter": "Último trimestre",
"last_year": "Último año",
"limit": "Límite",
"no_identified_impressions": "No hay impresiones de contactos identificados",
"no_responses_found": "No se han encontrado respuestas",
"other_values_found": "Otros valores encontrados",
"overall": "General",
@@ -2164,7 +2079,7 @@
"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 descripción más ligero o más grueso.",
"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",
@@ -2173,12 +2088,12 @@
"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 mínima",
"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 mínima de la entrada.",
"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.",
@@ -2187,8 +2102,6 @@
"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": "Color del borde",
"advanced_styling_field_option_border_description": "Delimita las opciones de radio y casillas de verificació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",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "Mostrar firma 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo actualizado correctamente",
"suggest_colors": "Sugerir colores",
"suggested_colors_applied_please_save": "Colores sugeridos generados correctamente. Pulsa \"Guardar\" para conservar los cambios.",
"theme": "Tema",
"theme_settings_description": "Crea un tema de estilo para todas las encuestas. Puedes activar el estilo personalizado para cada encuesta."
},
@@ -3008,9 +2920,6 @@
"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_question_open_text_headline": "¿Hay algo más que te gustaría compartir?",
"preview_survey_question_open_text_placeholder": "Escribe tu respuesta aquí...",
"preview_survey_question_open_text_subheader": "Tus comentarios nos ayudan a mejorar.",
"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",
+9 -100
View File
@@ -188,10 +188,8 @@
"customer_success": "Succès Client",
"dark_overlay": "Foncée",
"date": "Date",
"days": "jours",
"default": "Par défaut",
"delete": "Supprimer",
"delete_what": "Supprimer {deleteWhat}",
"description": "Description",
"dev_env": "Environnement de développement",
"development": "Développement",
@@ -207,8 +205,6 @@
"download": "Télécharger",
"draft": "Brouillon",
"duplicate": "Dupliquer",
"duplicate_copy": "(copie)",
"duplicate_copy_number": "(copie {copyNumber})",
"e_commerce": "E-commerce",
"edit": "Modifier",
"email": "Email",
@@ -221,16 +217,13 @@
"error": "Erreur",
"error_component_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.",
"error_component_title": "Erreur de chargement des ressources",
"error_loading_data": "Erreur lors du chargement des données",
"error_rate_limit_description": "Nombre maximal de demandes atteint. Veuillez réessayer plus tard.",
"error_rate_limit_title": "Limite de Taux Dépassée",
"expand_rows": "Développer les lignes",
"failed_to_copy_to_clipboard": "Échec de la copie dans le presse-papiers",
"failed_to_load_organizations": "Échec du chargement des organisations",
"failed_to_load_workspaces": "Échec du chargement des projets",
"filter": "Filtre",
"finish": "Terminer",
"first_name": "Prénom",
"follow_these": "Suivez ceci",
"formbricks_version": "Version de Formbricks",
"full_name": "Nom complet",
@@ -243,7 +236,6 @@
"hidden_field": "Champ caché",
"hidden_fields": "Champs cachés",
"hide_column": "Cacher la colonne",
"id": "ID",
"image": "Image",
"images": "Images",
"import": "Importer",
@@ -261,7 +253,6 @@
"key": "Clé",
"label": "Étiquette",
"language": "Langue",
"last_name": "Nom de famille",
"learn_more": "En savoir plus",
"license_expired": "License Expired",
"light_overlay": "Claire",
@@ -284,11 +275,9 @@
"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é!",
"months": "mois",
"move_down": "Déplacer vers le bas",
"move_up": "Déplacer vers le haut",
"multiple_languages": "Plusieurs langues",
"my_product": "mon produit",
"name": "Nom",
"new": "Nouveau",
"new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !",
@@ -404,7 +393,6 @@
"status": "Statut",
"step_by_step_manual": "Manuel étape par étape",
"storage_not_configured": "Stockage de fichiers non configuré, les téléchargements risquent d'échouer",
"string": "Texte",
"styling": "Style",
"submit": "Soumettre",
"summary": "Résumé",
@@ -437,7 +425,6 @@
"top_right": "En haut à droite",
"try_again": "Réessayer",
"type": "Type",
"unknown_survey": "Enquête inconnue",
"unlock_more_workspaces_with_a_higher_plan": "Débloquez plus de projets avec un forfait supérieur.",
"update": "Mise à jour",
"updated": "Mise à jour",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Connexion de sites Web et d'applications",
"website_app_survey": "Sondage sur le site Web et l'application",
"website_survey": "Sondage de site web",
"weeks": "semaines",
"welcome_card": "Carte de bienvenue",
"workspace_configuration": "Configuration du projet",
"workspace_created_successfully": "Projet créé avec succès",
@@ -472,7 +458,6 @@
"workspace_not_found": "Projet introuvable",
"workspace_permission_not_found": "Permission du projet introuvable",
"workspaces": "Projets",
"years": "années",
"you": "Vous",
"you_are_downgraded_to_the_community_edition": "Vous êtes rétrogradé à l'édition communautaire.",
"you_are_not_authorized_to_perform_this_action": "Vous n'êtes pas autorisé à effectuer cette action.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Attribut mis à jour avec succès",
"attribute_value": "Valeur",
"attribute_value_placeholder": "Valeur d'attribut",
"attributes_msg_attribute_limit_exceeded": "Impossible de créer {count, plural, one {# nouvel attribut} other {# nouveaux attributs}} car cela dépasserait la limite maximale de {limit} classes d'attributs. Les attributs existants ont été mis à jour avec succès.",
"attributes_msg_attribute_type_validation_error": "{error} (l'attribut « {key} » a le type de données: {dataType})",
"attributes_msg_email_already_exists": "L'adresse e-mail existe déjà pour cet environnement et n'a pas été mise à jour.",
"attributes_msg_email_or_userid_required": "L'adresse e-mail ou l'identifiant utilisateur est requis. Les valeurs existantes ont été conservées.",
"attributes_msg_new_attribute_created": "Nouvel attribut « {key} » créé avec le type « {dataType} »",
"attributes_msg_userid_already_exists": "L'identifiant utilisateur existe déjà pour cet environnement et n'a pas été mis à jour.",
"contact_deleted_successfully": "Contact supprimé avec succès",
"contact_not_found": "Aucun contact trouvé",
"contacts_table_refresh": "Actualiser les contacts",
"contacts_table_refresh_success": "Contacts rafraîchis avec succès",
"create_attribute": "Créer un attribut",
"create_key": "Créer une clé",
"create_new_attribute": "Créer un nouvel attribut",
"create_new_attribute_description": "Créez un nouvel attribut à des fins de segmentation.",
"custom_attributes": "Attributs personnalisés",
"data_type": "Type de données",
"data_type_cannot_be_changed": "Le type de données ne peut pas être modifié après la création",
"data_type_description": "Choisis comment cet attribut doit être stocké et filtré",
"date_value_required": "Une valeur de date est requise. Utilise le bouton supprimer pour retirer cet attribut si tu ne veux pas définir de date.",
"delete_attribute_confirmation": "{value, plural, one {Cela supprimera l'attribut sélectionné. Toutes les données de contact associées à cet attribut seront perdues.} other {Cela supprimera les attributs sélectionnés. Toutes les données de contact associées à ces attributs seront perdues.}}",
"delete_contact_confirmation": "Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus.",
"delete_contact_confirmation_with_quotas": "{value, plural, other {Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus. Si ce contact a des réponses qui comptent dans les quotas de l'enquête, les comptes de quotas seront réduits mais les limites de quota resteront inchangées.}}",
"displays": "Affichages",
"edit_attribute": "Modifier l'attribut",
"edit_attribute_description": "Mettez à jour l'étiquette et la description de cet attribut.",
"edit_attribute_values": "Modifier les attributs",
"edit_attribute_values_description": "Modifiez les valeurs d'attributs spécifiques pour ce contact.",
"edit_attributes": "Modifier les attributs",
"edit_attributes_success": "Attributs du contact mis à jour avec succès",
"generate_personal_link": "Générer un lien personnel",
"generate_personal_link_description": "Sélectionnez une enquête publiée pour générer un lien personnalisé pour ce contact.",
"invalid_csv_column_names": "Nom(s) de colonne CSV invalide(s): {columns}. Les noms de colonnes qui deviendront de nouveaux attributs ne doivent contenir que des lettres minuscules, des chiffres et des underscores, et doivent commencer par une lettre.",
"invalid_date_format": "Format de date invalide. Merci d'utiliser une date valide.",
"invalid_number_format": "Format de nombre invalide. Veuillez saisir un nombre valide.",
"no_activity_yet": "Aucune activité pour le moment",
"no_published_link_surveys_available": "Aucune enquête par lien publiée n'est disponible. Veuillez d'abord publier une enquête par lien.",
"no_published_surveys": "Aucune enquête publiée",
"no_responses_found": "Aucune réponse trouvée",
"not_provided": "Non fourni",
"number_value_required": "La valeur numérique est requise. Utilisez le bouton supprimer pour retirer cet attribut.",
"personal_link_generated": "Lien personnel généré avec succès",
"personal_link_generated_but_clipboard_failed": "Lien personnel généré mais échec de la copie dans le presse-papiers: {url}",
"personal_survey_link": "Lien vers le sondage personnel",
@@ -691,24 +659,13 @@
"search_contact": "Rechercher un contact",
"select_a_survey": "Sélectionner une enquête",
"select_attribute": "Sélectionner un attribut",
"select_attribute_key": "Sélectionner une clé d'attribut",
"survey_viewed": "Enquête consultée",
"survey_viewed_at": "Consultée le",
"system_attributes": "Attributs système",
"unlock_contacts_description": "Gérer les contacts et envoyer des enquêtes ciblées",
"unlock_contacts_title": "Débloquez des contacts avec un plan supérieur.",
"upload_contacts_error_attribute_type_mismatch": "L'attribut « {key} » est de type « {dataType} » mais le CSV contient des valeurs invalides: {values}",
"upload_contacts_error_duplicate_mappings": "Mappages en double trouvés pour les attributs suivants: {attributes}",
"upload_contacts_error_file_too_large": "La taille du fichier dépasse la limite maximale de 800 Ko",
"upload_contacts_error_generic": "Une erreur s'est produite lors de l'importation des contacts. Veuillez réessayer plus tard.",
"upload_contacts_error_invalid_file_type": "Veuillez importer un fichier CSV",
"upload_contacts_error_no_valid_contacts": "Le fichier CSV importé ne contient aucun contact valide, veuillez consulter l'exemple de fichier CSV pour le format correct.",
"upload_contacts_modal_attribute_header": "Attribut Formbricks",
"upload_contacts_modal_attributes_description": "Mappez les colonnes de votre CSV aux attributs dans Formbricks.",
"upload_contacts_modal_attributes_new": "Nouvel attribut",
"upload_contacts_modal_attributes_search_or_add": "Rechercher ou ajouter un attribut",
"upload_contacts_modal_attributes_should_be_mapped_to": "devrait être mappé à",
"upload_contacts_modal_attributes_title": "Attributs",
"upload_contacts_modal_csv_column_header": "Colonne CSV",
"upload_contacts_modal_description": "Téléchargez un fichier CSV pour importer rapidement des contacts avec des attributs.",
"upload_contacts_modal_download_example_csv": "Télécharger un exemple de CSV",
"upload_contacts_modal_duplicates_description": "Que faire si un contact existe déjà ?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Lien Google Sheet",
"link_new_sheet": "Lier une nouvelle feuille",
"no_integrations_yet": "Vos intégrations Google Sheets apparaîtront ici dès que vous les ajouterez. ⏲️",
"reconnect_button": "Reconnecter",
"reconnect_button_description": "Votre connexion Google Sheets a expiré. Veuillez vous reconnecter pour continuer à synchroniser les réponses. Vos liens de feuilles de calcul et données existants seront préservés.",
"reconnect_button_tooltip": "Reconnectez l'intégration pour actualiser votre accès. Vos liens de feuilles de calcul et données existants seront préservés.",
"spreadsheet_permission_error": "Vous n'avez pas la permission d'accéder à cette feuille de calcul. Veuillez vous assurer que la feuille de calcul est partagée avec votre compte Google et que vous disposez d'un accès en écriture.",
"spreadsheet_url": "URL de la feuille de calcul",
"token_expired_error": "Le jeton d'actualisation Google Sheets a expiré ou a été révoqué. Veuillez reconnecter l'intégration."
"spreadsheet_url": "URL de la feuille de calcul"
},
"include_created_at": "Inclure la date de création",
"include_hidden_fields": "Inclure les champs cachés",
@@ -894,40 +846,6 @@
"no_attributes_yet": "Aucun attribut pour le moment !",
"no_filters_yet": "Il n'y a pas encore de filtres !",
"no_segments_yet": "Aucun segment n'est actuellement enregistré.",
"operator_contains": "contient",
"operator_does_not_contain": "ne contient pas",
"operator_ends_with": "se termine par",
"operator_is_after": "est après",
"operator_is_before": "est avant",
"operator_is_between": "est entre",
"operator_is_newer_than": "est plus récent que",
"operator_is_not_set": "n'est pas défini",
"operator_is_older_than": "est plus ancien que",
"operator_is_same_day": "est le même jour",
"operator_is_set": "est défini",
"operator_starts_with": "commence par",
"operator_title_contains": "Contient",
"operator_title_does_not_contain": "Ne contient pas",
"operator_title_ends_with": "Se termine par",
"operator_title_equals": "Égal",
"operator_title_greater_equal": "Supérieur ou égal à",
"operator_title_greater_than": "Supérieur à",
"operator_title_is_after": "Est après",
"operator_title_is_before": "Est avant",
"operator_title_is_between": "Est entre",
"operator_title_is_newer_than": "Est plus récent que",
"operator_title_is_not_set": "N'est pas défini",
"operator_title_is_older_than": "Est plus ancien que",
"operator_title_is_same_day": "Est le même jour",
"operator_title_is_set": "Est défini",
"operator_title_less_equal": "Inférieur ou égal à",
"operator_title_less_than": "Inférieur à",
"operator_title_not_equals": "N'est pas égal à",
"operator_title_starts_with": "Commence par",
"operator_title_user_is_in": "L'utilisateur est dans",
"operator_title_user_is_not_in": "L'utilisateur n'est pas dans",
"operator_user_is_in": "L'utilisateur est dans",
"operator_user_is_not_in": "L'utilisateur n'est pas dans",
"person_and_attributes": "Personne et attributs",
"phone": "Téléphone",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Veuillez supprimer le segment de ces enquêtes afin de le supprimer.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "La ciblage des utilisateurs est actuellement disponible uniquement lorsque",
"value_cannot_be_empty": "La valeur ne peut pas être vide.",
"value_must_be_a_number": "La valeur doit être un nombre.",
"value_must_be_positive": "La valeur doit être un nombre positif.",
"view_filters": "Filtres de vue",
"where": "Où",
"with_the_formbricks_sdk": "avec le SDK Formbricks"
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Salut {userName}",
"email_customization_preview_email_text": "Cette est une prévisualisation d'e-mail pour vous montrer quel logo sera rendu dans les e-mails.",
"error_deleting_organization_please_try_again": "Erreur lors de la suppression de l'organisation. Veuillez réessayer.",
"from_your_organization": "{memberName} de votre organisation",
"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}",
@@ -1250,7 +1167,6 @@
"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": "S'applique uniquement aux sondages intégrés au produit.",
"add_logic": "Ajouter de la logique",
"add_none_of_the_above": "Ajouter \"Aucun des éléments ci-dessus\"",
"add_option": "Ajouter une option",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "\"Suivi mis à jour et sera enregistré une fois que vous sauvegarderez le sondage.\"",
"follow_ups_new": "Nouveau suivi",
"follow_ups_upgrade_button_text": "Passez à la version supérieure pour activer les relances",
"form_styling": "Style de formulaire",
"formbricks_sdk_is_not_connected": "Le SDK Formbricks n'est pas connecté",
"four_points": "4 points",
"heading": "En-tête",
@@ -1621,7 +1538,7 @@
"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.",
"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",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
"survey_display_settings": "Paramètres d'affichage de l'enquête",
"survey_placement": "Placement de l'enquête",
"survey_styling": "Style de formulaire",
"survey_trigger": "Déclencheur d'enquête",
"switch_multi_language_on_to_get_started": "Activez le mode multilingue pour commencer 👉",
"target_block_not_found": "Bloc cible non trouvé",
@@ -1758,6 +1674,7 @@
"welcome_message": "Message de bienvenue",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sans filtre, tous vos utilisateurs peuvent être sondés.",
"you_have_not_created_a_segment_yet": "Tu n'as pas encore créé de segment.",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Vous devez avoir deux langues ou plus configurées dans votre espace de travail pour travailler avec les traductions.",
"your_description_here_recall_information_with": "Votre description ici. Rappelez-vous des informations avec @",
"your_question_here_recall_information_with": "Votre question ici. Rappelez-vous des informations avec @",
"your_web_app": "Votre application web",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Réponses filtrées (Excel)",
"generating_qr_code": "Génération du code QR",
"impressions": "Impressions",
"impressions_identified_only": "Affichage uniquement des impressions des contacts identifiés",
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
"in_app": {
"connection_description": "Le sondage sera affiché aux utilisateurs de votre site web, qui correspondent aux critères listés ci-dessous",
@@ -2008,7 +1924,6 @@
"last_quarter": "dernier trimestre",
"last_year": "l'année dernière",
"limit": "Limite",
"no_identified_impressions": "Aucune impression des contacts identifiés",
"no_responses_found": "Aucune réponse trouvée",
"other_values_found": "D'autres valeurs trouvées",
"overall": "Globalement",
@@ -2173,12 +2088,12 @@
"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 minimale",
"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 min. du champ de saisie.",
"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.",
@@ -2187,8 +2102,6 @@
"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": "Couleur de bordure",
"advanced_styling_field_option_border_description": "Contours des options de boutons radio et de cases à cocher.",
"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",
@@ -2243,7 +2156,6 @@
"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",
"suggested_colors_applied_please_save": "Couleurs suggérées générées avec succès. Appuyez sur « Enregistrer » pour conserver les modifications.",
"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."
},
@@ -3008,9 +2920,6 @@
"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_question_open_text_headline": "Autre chose que vous aimeriez partager?",
"preview_survey_question_open_text_placeholder": "Entrez votre réponse ici...",
"preview_survey_question_open_text_subheader": "Vos commentaires nous aident à nous améliorer.",
"preview_survey_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",
+69 -160
View File
@@ -188,10 +188,8 @@
"customer_success": "Ügyfélsiker",
"dark_overlay": "Sötét rávetítés",
"date": "Dátum",
"days": "napok",
"default": "Alapértelmezett",
"delete": "Törlés",
"delete_what": "{deleteWhat} törlése",
"description": "Leírás",
"dev_env": "Fejlesztői környezet",
"development": "Fejlesztés",
@@ -207,8 +205,6 @@
"download": "Letöltés",
"draft": "Piszkozat",
"duplicate": "Kettőzés",
"duplicate_copy": "(másolat)",
"duplicate_copy_number": "({copyNumber}. másolat)",
"e_commerce": "E-kereskedelem",
"edit": "Szerkesztés",
"email": "E-mail",
@@ -221,16 +217,13 @@
"error": "Hiba",
"error_component_description": "Ez az erőforrás nem létezik, vagy nem rendelkezik a hozzáféréshez szükséges jogosultságokkal.",
"error_component_title": "Hiba az erőforrások betöltésekor",
"error_loading_data": "Hiba az adatok betöltése során",
"error_rate_limit_description": "A kérések legnagyobb száma elérve. Próbálja meg később újra.",
"error_rate_limit_title": "A sebességkorlát elérve",
"expand_rows": "Sorok kinyitása",
"failed_to_copy_to_clipboard": "Nem sikerült másolni a vágólapra",
"failed_to_load_organizations": "Nem sikerült betölteni a szervezeteket",
"failed_to_load_workspaces": "Nem sikerült a munkaterületek betöltése",
"filter": "Szűrő",
"finish": "Befejezés",
"first_name": "Keresztnév",
"follow_these": "Ezek követése",
"formbricks_version": "Formbricks verziója",
"full_name": "Teljes név",
@@ -243,7 +236,6 @@
"hidden_field": "Rejtett mező",
"hidden_fields": "Rejtett mezők",
"hide_column": "Oszlop elrejtése",
"id": "ID",
"image": "Kép",
"images": "Képek",
"import": "Importálás",
@@ -261,7 +253,6 @@
"key": "Kulcs",
"label": "Címke",
"language": "Nyelv",
"last_name": "Vezetéknév",
"learn_more": "Tudjon meg többet",
"license_expired": "A licenc lejárt",
"light_overlay": "Világos rávetítés",
@@ -284,11 +275,9 @@
"mobile_overlay_app_works_best_on_desktop": "A Formbricks nagyobb képernyőn működik a legjobban. A kérdőívek kezeléséhez vagy összeállításához váltson másik eszközre.",
"mobile_overlay_surveys_look_good": "Ne aggódjon a kérdőívei minden eszközön és képernyőméretnél remekül néznek ki!",
"mobile_overlay_title": "Hoppá, apró képernyő észlelve!",
"months": "hónapok",
"move_down": "Mozgatás le",
"move_up": "Mozgatás fel",
"multiple_languages": "Több nyelv",
"my_product": "saját termék",
"name": "Név",
"new": "Új",
"new_version_available": "A Formbricks {version} megérkezett. Frissítsen most!",
@@ -296,7 +285,7 @@
"no_background_image_found": "Nem található háttérkép.",
"no_code": "Kód nélkül",
"no_files_uploaded": "Nem lettek fájlok feltöltve",
"no_overlay": "Nincs rávetítés",
"no_overlay": "Nincs átfedés",
"no_quotas_found": "Nem találhatók kvóták",
"no_result_found": "Nem található eredmény",
"no_results": "Nincs találat",
@@ -323,7 +312,7 @@
"organization_teams_not_found": "A szervezeti csapatok nem találhatók",
"other": "Egyéb",
"others": "Egyebek",
"overlay_color": "Rávetítés színe",
"overlay_color": "Átfedés színe",
"overview": "Áttekintés",
"password": "Jelszó",
"paused": "Szüneteltetve",
@@ -363,7 +352,7 @@
"request_trial_license": "Próbalicenc kérése",
"reset_to_default": "Visszaállítás az alapértelmezettre",
"response": "Válasz",
"response_id": "Válaszazonosító",
"response_id": "Válasz azonosító",
"responses": "Válaszok",
"restart": "Újraindítás",
"role": "Szerep",
@@ -404,7 +393,6 @@
"status": "Állapot",
"step_by_step_manual": "Lépésenkénti kézikönyv",
"storage_not_configured": "A fájltároló nincs beállítva, a feltöltések valószínűleg sikertelenek lesznek",
"string": "Szöveg",
"styling": "Stíluskészítés",
"submit": "Elküldés",
"summary": "Összegzés",
@@ -437,7 +425,6 @@
"top_right": "Jobbra fent",
"try_again": "Próbálja újra",
"type": "Típus",
"unknown_survey": "Ismeretlen kérdőív",
"unlock_more_workspaces_with_a_higher_plan": "Több munkaterület feloldása egy magasabb csomaggal.",
"update": "Frissítés",
"updated": "Frissítve",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Webhely és alkalmazáskapcsolódás",
"website_app_survey": "Webhely és alkalmazás-kérdőív",
"website_survey": "Webhely kérdőív",
"weeks": "hetek",
"welcome_card": "Üdvözlő kártya",
"workspace_configuration": "Munkaterület beállítása",
"workspace_created_successfully": "A munkaterület sikeresen létrehozva",
@@ -472,7 +458,6 @@
"workspace_not_found": "A munkaterület nem található",
"workspace_permission_not_found": "A munkaterület-jogosultság nem található",
"workspaces": "Munkaterületek",
"years": "évek",
"you": "Ön",
"you_are_downgraded_to_the_community_edition": "Visszaváltott a közösségi kiadásra.",
"you_are_not_authorized_to_perform_this_action": "Nincs felhatalmazva ennek a műveletnek a végrehajtásához.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Az attribútum sikeresen frissítve",
"attribute_value": "Érték",
"attribute_value_placeholder": "Attribútum értéke",
"attributes_msg_attribute_limit_exceeded": "Nem sikerült létrehozni {count} új attribútumot, mivel az meghaladná a maximális {limit} attribútumosztály-korlátot. A meglévő attribútumok sikeresen frissítve lettek.",
"attributes_msg_attribute_type_validation_error": "{error} (a(z) '{key}' attribútum adattípusa: {dataType})",
"attributes_msg_email_already_exists": "Az e-mail cím már létezik ebben a környezetben, és nem lett frissítve.",
"attributes_msg_email_or_userid_required": "E-mail cím vagy felhasználói azonosító megadása kötelező. A meglévő értékek megmaradtak.",
"attributes_msg_new_attribute_created": "Új '{key}' attribútum létrehozva '{dataType}' típussal",
"attributes_msg_userid_already_exists": "A felhasználói azonosító már létezik ebben a környezetben, és nem lett frissítve.",
"contact_deleted_successfully": "A partner sikeresen törölve",
"contact_not_found": "Nem található ilyen partner",
"contacts_table_refresh": "Partnerek frissítése",
"contacts_table_refresh_success": "A partnerek sikeresen frissítve",
"create_attribute": "Attribútum létrehozása",
"create_key": "Kulcs létrehozása",
"create_new_attribute": "Új attribútum létrehozása",
"create_new_attribute_description": "Új attribútum létrehozása szakaszolási célokhoz.",
"custom_attributes": "Egyéni attribútumok",
"data_type": "Adattípus",
"data_type_cannot_be_changed": "Az adattípus létrehozás után nem módosítható",
"data_type_description": "Válaszd ki, hogyan legyen tárolva és szűrve ez az attribútum",
"date_value_required": "Dátum érték megadása kötelező. Használd a törlés gombot az attribútum eltávolításához, ha nem szeretnél dátumot megadni.",
"delete_attribute_confirmation": "{value, plural, one {Ez törölni fogja a kiválasztott attribútumot. Az ehhez az attribútumhoz hozzárendelt összes partneradat el fog veszni.} other {Ez törölni fogja a kiválasztott attribútumokat. Az ezekhez az attribútumokhoz hozzárendelt összes partneradat el fog veszni.}}",
"delete_contact_confirmation": "Ez törölni fogja az ehhez a partnerhez tartozó összes kérdőívválaszt és partnerattribútumot. A partner adatain alapuló bármilyen célzás és személyre szabás el fog veszni.",
"delete_contact_confirmation_with_quotas": "{value, plural, one {Ez törölni fogja az ehhez a partnerhez tartozó összes kérdőívválaszt és partnerattribútumot. A partner adatain alapuló bármilyen célzás és személyre szabás el fog veszni. Ha ez a partner olyan válaszokkal rendelkezik, amelyek a kérdőívkvótákba beletartoznak, akkor a kvóta számlálója csökkentve lesz, de a kvóta korlátai változatlanok maradnak.} other {Ez törölni fogja az ezekhez a partnerekhez tartozó összes kérdőívválaszt és partnerattribútumot. A partnerek adatain alapuló bármilyen célzás és személyre szabás el fog veszni. Ha ezek a partnerek olyan válaszokkal rendelkeznek, amelyek a kérdőívkvótákba beletartoznak, akkor a kvóta számlálója csökkentve lesz, de a kvóta korlátai változatlanok maradnak.}}",
"displays": "Megjelenítések",
"edit_attribute": "Attribútum szerkesztése",
"edit_attribute_description": "Az attribútum címkéjének és leírásának frissítése.",
"edit_attribute_values": "Attribútumok szerkesztése",
"edit_attribute_values_description": "Bizonyos attribútumok értékének megváltoztatása ennél a partnernél.",
"edit_attributes": "Attribútumok szerkesztése",
"edit_attributes_success": "A partner attribútumai sikeresen frissítve",
"generate_personal_link": "Személyes hivatkozás előállítása",
"generate_personal_link_description": "Válasszon egy közzétett kérdőívet, hogy személyre szabott hivatkozást állítson elő ehhez a partnerhez.",
"invalid_csv_column_names": "Érvénytelen CSV oszlopnév(nevek): {columns}. Az új attribútumokká váló oszlopnevek csak kisbetűket, számokat és aláhúzásjeleket tartalmazhatnak, és betűvel kell kezdődniük.",
"invalid_date_format": "Érvénytelen dátumformátum. Kérlek, adj meg egy érvényes dátumot.",
"invalid_number_format": "Érvénytelen számformátum. Kérlek, adj meg egy érvényes számot.",
"no_activity_yet": "Még nincs aktivitás",
"no_published_link_surveys_available": "Nem érhetők el közzétett hivatkozás-kérdőívek. Először tegyen közzé egy hivatkozás-kérdőívet.",
"no_published_surveys": "Nincsenek közzétett kérdőívek",
"no_responses_found": "Nem találhatók válaszok",
"not_provided": "Nincs megadva",
"number_value_required": "Szám érték megadása kötelező. Használd a törlés gombot az attribútum eltávolításához.",
"personal_link_generated": "A személyes hivatkozás sikeresen előállítva",
"personal_link_generated_but_clipboard_failed": "A személyes hivatkozás előállítva, de nem sikerült a vágólapra másolni: {url}",
"personal_survey_link": "Személyes kérdőív-hivatkozás",
@@ -691,24 +659,13 @@
"search_contact": "Partner keresése",
"select_a_survey": "Kérdőív kiválasztása",
"select_attribute": "Attribútum kiválasztása",
"select_attribute_key": "Attribútum kulcs kiválasztása",
"survey_viewed": "Kérdőív megtekintve",
"survey_viewed_at": "Megtekintve",
"system_attributes": "Rendszer attribútumok",
"unlock_contacts_description": "Partnerek kezelése és célzott kérdőívek kiküldése",
"unlock_contacts_title": "Partnerek feloldása egy magasabb csomaggal",
"upload_contacts_error_attribute_type_mismatch": "A(z) \"{key}\" attribútum típusa \"{dataType}\", de a CSV érvénytelen értékeket tartalmaz: {values}",
"upload_contacts_error_duplicate_mappings": "Duplikált leképezések találhatók a következő attribútumokhoz: {attributes}",
"upload_contacts_error_file_too_large": "A fájl mérete meghaladja a maximális 800KB-os limitet",
"upload_contacts_error_generic": "Hiba történt a kapcsolatok feltöltése során. Kérjük, próbáld újra később.",
"upload_contacts_error_invalid_file_type": "Kérjük, tölts fel egy CSV fájlt",
"upload_contacts_error_no_valid_contacts": "A feltöltött CSV fájl nem tartalmaz érvényes kapcsolatokat, kérjük, nézd meg a minta CSV fájlt a helyes formátumhoz.",
"upload_contacts_modal_attribute_header": "Formbricks attribútum",
"upload_contacts_modal_attributes_description": "A CSV-ben lévő oszlopok leképezése a Formbricksben lévő attribútumokra.",
"upload_contacts_modal_attributes_new": "Új attribútum",
"upload_contacts_modal_attributes_search_or_add": "Attribútum keresése vagy hozzáadása",
"upload_contacts_modal_attributes_should_be_mapped_to": "le kell képezni erre:",
"upload_contacts_modal_attributes_title": "Attribútumok",
"upload_contacts_modal_csv_column_header": "CSV oszlop",
"upload_contacts_modal_description": "CSV feltöltése a partnerek attribútumokkal együtt történő gyors importálásához",
"upload_contacts_modal_download_example_csv": "Példa CSV letöltése",
"upload_contacts_modal_duplicates_description": "Hogyan kell kezelnünk, ha egy partner már szerepel a partnerek között?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Google Táblázatok összekapcsolása",
"link_new_sheet": "Új táblázat összekapcsolása",
"no_integrations_yet": "A Google Táblázatok integrációi itt fognak megjelenni, amint hozzáadja azokat. ⏲️",
"reconnect_button": "Újrakapcsolódás",
"reconnect_button_description": "A Google Táblázatok kapcsolata lejárt. Kérjük, csatlakozzon újra a válaszok szinkronizálásának folytatásához. A meglévő táblázathivatkozások és adatok megmaradnak.",
"reconnect_button_tooltip": "Csatlakoztassa újra az integrációt a hozzáférés frissítéséhez. A meglévő táblázathivatkozások és adatok megmaradnak.",
"spreadsheet_permission_error": "Nincs jogosultsága a táblázat eléréséhez. Kérjük, győződjön meg arról, hogy a táblázat meg van osztva a Google-fiókjával, és írási jogosultsággal rendelkezik a táblázathoz.",
"spreadsheet_url": "Táblázat URL-e",
"token_expired_error": "A Google Táblázatok frissítési tokenje lejárt vagy visszavonásra került. Kérjük, csatlakoztassa újra az integrációt."
"spreadsheet_url": "Táblázat URL-e"
},
"include_created_at": "Létrehozva felvétele",
"include_hidden_fields": "Rejtett mezők felvétele",
@@ -894,40 +846,6 @@
"no_attributes_yet": "Még nincsenek attribútumok!",
"no_filters_yet": "Még nincsenek szűrők!",
"no_segments_yet": "Jelenleg nincsenek mentett szakaszai.",
"operator_contains": "tartalmazza",
"operator_does_not_contain": "nem tartalmazza",
"operator_ends_with": "ezzel végződik",
"operator_is_after": "ez után",
"operator_is_before": "ez előtt",
"operator_is_between": "között",
"operator_is_newer_than": "újabb mint",
"operator_is_not_set": "nincs beállítva",
"operator_is_older_than": "régebbi mint",
"operator_is_same_day": "ugyanazon a napon",
"operator_is_set": "beállítva",
"operator_starts_with": "ezzel kezdődik",
"operator_title_contains": "Tartalmazza",
"operator_title_does_not_contain": "Nem tartalmazza",
"operator_title_ends_with": "Ezzel végződik",
"operator_title_equals": "Egyenlő",
"operator_title_greater_equal": "Nagyobb vagy egyenlő",
"operator_title_greater_than": "Nagyobb mint",
"operator_title_is_after": "Ez után",
"operator_title_is_before": "Ez előtt",
"operator_title_is_between": "Között",
"operator_title_is_newer_than": "Újabb mint",
"operator_title_is_not_set": "Nincs beállítva",
"operator_title_is_older_than": "Régebbi mint",
"operator_title_is_same_day": "Ugyanazon a napon",
"operator_title_is_set": "Beállítva",
"operator_title_less_equal": "Kisebb vagy egyenlő",
"operator_title_less_than": "Kisebb mint",
"operator_title_not_equals": "Nem egyenlő",
"operator_title_starts_with": "Ezzel kezdődik",
"operator_title_user_is_in": "A felhasználó benne van",
"operator_title_user_is_not_in": "A felhasználó nincs benne",
"operator_user_is_in": "A felhasználó benne van",
"operator_user_is_not_in": "A felhasználó nincs benne",
"person_and_attributes": "Személy és attribútumok",
"phone": "Telefon",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Távolítsa el a szakaszt ezekből a kérdőívekből, hogy törölhesse azt.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "A felhasználók megcélzása jelenleg csak akkor érhető el, ha",
"value_cannot_be_empty": "Az érték nem lehet üres.",
"value_must_be_a_number": "Az értékének számnak kell lennie.",
"value_must_be_positive": "Az értéknek pozitív számnak kell lennie.",
"view_filters": "Szűrők megtekintése",
"where": "Ahol",
"with_the_formbricks_sdk": "a Formbricks SDK-val"
@@ -1039,25 +956,25 @@
"enterprise_features": "Vállalati funkciók",
"get_an_enterprise_license_to_get_access_to_all_features": "Vállalati licenc megszerzése az összes funkcióhoz való hozzáféréshez.",
"keep_full_control_over_your_data_privacy_and_security": "Az adatvédelem és biztonság fölötti rendelkezés teljes kézben tartása.",
"license_invalid_description": "Az ENTERPRISE_LICENSE_KEY környezeti változóban lévő licenckulcs nem érvényes. Ellenőrizze, hogy nem gépelte-e el, vagy kérjen új kulcsot.",
"license_status": "Licencállapot",
"license_invalid_description": "Az ENTERPRISE_LICENSE_KEY környezeti változóban található licenckulcs nem érvényes. Kérjük, ellenőrizd az elgépeléseket, vagy kérj új kulcsot.",
"license_status": "Licenc állapota",
"license_status_active": "Aktív",
"license_status_description": "A vállalati licenc állapota.",
"license_status_description": "A vállalati licenced állapota.",
"license_status_expired": "Lejárt",
"license_status_invalid": "Érvénytelen licenc",
"license_status_unreachable": "Nem érhető el",
"license_unreachable_grace_period": "A licenckiszolgálót nem lehet elérni. A vállalati funkciók egy 3 napos türelmi időszak alatt aktívak maradnak, egészen eddig: {gracePeriodEnd}.",
"license_status_unreachable": "Nem elérhető",
"license_unreachable_grace_period": "A licenckiszolgáló nem érhető el. A vállalati funkciók a 3 napos türelmi időszak alatt aktívak maradnak, amely {gracePeriodEnd} időpontban jár le.",
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Nincs szükség telefonálásra, nincs feltételekhez kötöttség: kérjen 30 napos ingyenes próbalicencet az összes funkció kipróbálásához az alábbi űrlap kitöltésével:",
"no_credit_card_no_sales_call_just_test_it": "Nem kell hitelkártya. Nincsenek értékesítési hívások. Egyszerűen csak próbálja ki :)",
"on_request": "Kérésre",
"organization_roles": "Szervezeti szerepek (adminisztrátor, szerkesztő, fejlesztő stb.)",
"questions_please_reach_out_to": "Kérdése van? Írjon nekünk erre az e-mail-címre:",
"recheck_license": "Licenc újraellenőrzése",
"recheck_license_failed": "A licencellenőrzés nem sikerült. Lehet, hogy a licenckiszolgáló nem érhető el.",
"recheck_license_invalid": "A licenckulcs érvénytelen. Ellenőrizze az ENTERPRISE_LICENSE_KEY értékét.",
"recheck_license_success": "A licencellenőrzés sikeres",
"recheck_license_unreachable": "A licenckiszolgáló nem érhető el. Próbálja meg később újra.",
"rechecking": "Újraellenőrzés",
"recheck_license_failed": "A licenc ellenőrzése sikertelen. Előfordulhat, hogy a licenckiszolgáló nem elérhető.",
"recheck_license_invalid": "A licenckulcs érvénytelen. Kérjük, ellenőrizd az ENTERPRISE_LICENSE_KEY értékét.",
"recheck_license_success": "A licenc ellenőrzése sikeres",
"recheck_license_unreachable": "A licenckiszolgáló nem elérhető. Kérjük, próbáld újra később.",
"rechecking": "Újraellenőrzés...",
"request_30_day_trial_license": "30 napos ingyenes licenc kérése",
"saml_sso": "SAML SSO",
"service_level_agreement": "Szolgáltatási megállapodás",
@@ -1085,10 +1002,10 @@
"email_customization_preview_email_heading": "Helló {userName}",
"email_customization_preview_email_text": "Ez egy e-mail előnézet, amely azt mutatja meg, hogy melyik logó fog megjelenni az e-mailekben.",
"error_deleting_organization_please_try_again": "Hiba a szervezet törlésekor. Próbálja meg újra.",
"from_your_organization": "{memberName} a szervezetből",
"from_your_organization": "a szervezetétől",
"invitation_sent_once_more": "A meghívó még egyszer elküldve.",
"invite_deleted_successfully": "A meghívó sikeresen törölve",
"invite_expires_on": "A meghívó lejár ekkor: {date}",
"invite_expires_on": "A meghívó lejár: {date}",
"invites_failed": "A meghívás sikertelen",
"leave_organization": "Szervezet elhagyása",
"leave_organization_description": "Elhagyja ezt a szervezetet, és elveszíti az összes kérdőívhez és válaszhoz való hozzáférését. Csak akkor tud ismét csatlakozni, ha újra meghívják.",
@@ -1250,7 +1167,6 @@
"add_fallback_placeholder": "Helykitöltő hozzáadása annak megjelenítéshez, hogy nincs visszahívandó érték.",
"add_hidden_field_id": "Rejtett mezőazonosító hozzáadása",
"add_highlight_border": "Kiemelési szegély hozzáadása",
"add_highlight_border_description": "Csak a terméken belüli felmérésekre vonatkozik.",
"add_logic": "Logika hozzáadása",
"add_none_of_the_above": "„A fentiek közül egyik sem” hozzáadása",
"add_option": "Lehetőség hozzáadása",
@@ -1289,7 +1205,7 @@
"block_duplicated": "A blokk kettőzve.",
"bold": "Félkövér",
"brand_color": "Márkajel színe",
"brand_color_description": "Gombokra, hivatkozásokra és kiemelésekre alkalmazva.",
"brand_color_description": "Gomboknál, hivatkozásoknál és kiemeléseknél alkalmazva.",
"brightness": "Fényerő",
"bulk_edit": "Tömeges szerkesztés",
"bulk_edit_description": "Az összes lenti lehetőség szerkesztése, soronként egy. Az üres sorok kihagyásra kerülnek, az ismétlődések pedig el lesznek távolítva.",
@@ -1309,7 +1225,7 @@
"card_background_color": "Kártya hátterének színe",
"card_background_color_description": "Kitölti a kérdőívkártya területét.",
"card_border_color": "Kártya szegélyének színe",
"card_border_color_description": "Körberajzolja a kérdőívkártyát.",
"card_border_color_description": "Körvonalazza a kérdőívkártyát.",
"card_styling": "Kártya stílusának beállítása",
"casual": "Alkalmi",
"caution_edit_duplicate": "Kettőzés és szerkesztés",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "A követés frissítve, és akkor lesz elmentve, ha elmenti a kérdőívet.",
"follow_ups_new": "Új követés",
"follow_ups_upgrade_button_text": "Magasabb csomagra váltás a követések engedélyezéséhez",
"form_styling": "Űrlap stílusának beállítása",
"formbricks_sdk_is_not_connected": "A Formbricks SDK nincs csatlakoztatva",
"four_points": "4 pont",
"heading": "Címsor",
@@ -1474,9 +1391,9 @@
"initial_value": "Kezdeti érték",
"inner_text": "Belső szöveg",
"input_border_color": "Beviteli mező szegélyének színe",
"input_border_color_description": "Körberajzolja a szöveges beviteli mezőket és a szövegdobozokat.",
"input_border_color_description": "Körvonalazza a szövegbeviteli mezőket és szövegterületeket.",
"input_color": "Beviteli mező színe",
"input_color_description": "Kitölti a szöveges beviteli mezők belsejét.",
"input_color_description": "Kitölti a szövegbeviteli mezők belsejét.",
"insert_link": "Hivatkozás beszúrása",
"invalid_targeting": "Érvénytelen célzás: ellenőrizze a közönség szűrőit",
"invalid_video_url_warning": "Adjon meg egy érvényes YouTube, Vimeo vagy Loom URL-t. Jelenleg nem támogatunk más videomegosztó szolgáltatókat.",
@@ -1621,7 +1538,7 @@
"response_limits_redirections_and_more": "Válaszkorlátok, átirányítások és egyebek.",
"response_options": "Válasz beállításai",
"roundness": "Kerekesség",
"roundness_description": "Szabályozza a sarkok lekerekítését.",
"roundness_description": "Szabályozza a kártya sarkai lekerekítését.",
"row_used_in_logic_error": "Ez a sor használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
"rows": "Sorok",
"save_and_close": "Mentés és bezárás",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Ez a szabad és nyílt forráskódú kérdőív le lett zárva",
"survey_display_settings": "Kérdőív megjelenítésének beállításai",
"survey_placement": "Kérdőív elhelyezése",
"survey_styling": "Űrlap stílusának beállítása",
"survey_trigger": "Kérdőív aktiválója",
"switch_multi_language_on_to_get_started": "Kapcsolja be a többnyelvűséget a kezdéshez 👉",
"target_block_not_found": "A célblokk nem található",
@@ -1758,6 +1674,7 @@
"welcome_message": "Üdvözlő üzenet",
"without_a_filter_all_of_your_users_can_be_surveyed": "Szűrő nélkül az összes felhasználója megkérdezhető.",
"you_have_not_created_a_segment_yet": "Még nem hozott létre szakaszt",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Be kell állítania kettő vagy több nyelvet a munkaterületen a fordításokkal való munkához.",
"your_description_here_recall_information_with": "Ide jön a leírás. Információk visszahívása a @ karakterrel.",
"your_question_here_recall_information_with": "Ide jön a kérdés. Információk visszahívása a @ karakterrel.",
"your_web_app": "Saját webalkalmazás",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Szűrt válaszok (Excel)",
"generating_qr_code": "QR-kód előállítása",
"impressions": "Benyomások",
"impressions_identified_only": "Csak az azonosított kapcsolatok megjelenítései láthatók",
"impressions_tooltip": "A kérdőív megtekintési alkalmainak száma.",
"in_app": {
"connection_description": "A kérdőív a webhelye azon felhasználóinak lesz megjelenítve, akik megfelelnek az alább felsorolt feltételeknek",
@@ -2008,7 +1924,6 @@
"last_quarter": "Elmúlt negyedév",
"last_year": "Elmúlt év",
"limit": "Korlát",
"no_identified_impressions": "Nincsenek megjelenítések azonosított kapcsolatoktól",
"no_responses_found": "Nem találhatók válaszok",
"other_values_found": "Más értékek találhatók",
"overall": "Összesen",
@@ -2148,71 +2063,69 @@
"look": {
"add_background_color": "Háttérszín hozzáadása",
"add_background_color_description": "Hátérszín hozzáadása a logó tárolódobozához.",
"advanced_styling_field_border_radius": "Szegély sugara",
"advanced_styling_field_border_radius": "Szegély lekerekítése",
"advanced_styling_field_button_bg": "Gomb háttere",
"advanced_styling_field_button_bg_description": "Kitölti a „Következő” és az „Elküldés gombokat.",
"advanced_styling_field_button_bg_description": "Kitölti a következő / küldés gombot.",
"advanced_styling_field_button_border_radius_description": "Lekerekíti a gomb sarkait.",
"advanced_styling_field_button_font_size_description": "Átméretezi a gomb címkéjének szövegét.",
"advanced_styling_field_button_font_size_description": "Méretezi a gomb feliratának szövegét.",
"advanced_styling_field_button_font_weight_description": "Vékonyabbá vagy vastagabbá teszi a gomb szövegét.",
"advanced_styling_field_button_height_description": "A gomb magasságát vezérli.",
"advanced_styling_field_button_padding_x_description": "Térközt ad hozzá balra és jobbra.",
"advanced_styling_field_button_height_description": "Szabályozza a gomb magasságát.",
"advanced_styling_field_button_padding_x_description": "Térközt ad hozzá bal és jobb oldalon.",
"advanced_styling_field_button_padding_y_description": "Térközt ad hozzá fent és lent.",
"advanced_styling_field_button_text": "Gomb szövege",
"advanced_styling_field_button_text_description": "Kiszínezi a gombokon belüli címkét.",
"advanced_styling_field_button_text_description": "Színezi a gombokon belüli feliratot.",
"advanced_styling_field_description_color": "Leírás színe",
"advanced_styling_field_description_color_description": "Kiszínezi az egyes címsorok alatti szöveget.",
"advanced_styling_field_description_color_description": "Színezi az egyes címsorok alatti szöveget.",
"advanced_styling_field_description_size": "Leírás betűmérete",
"advanced_styling_field_description_size_description": "Átméretezi a leírás szövegét.",
"advanced_styling_field_description_size_description": "Méretezi a leírás szövegét.",
"advanced_styling_field_description_weight": "Leírás betűvastagsága",
"advanced_styling_field_description_weight_description": "Vékonyabbá vagy vastagabbá teszi a leírás szövegét.",
"advanced_styling_field_font_size": "Betűméret",
"advanced_styling_field_font_weight": "Betűvastagság",
"advanced_styling_field_headline_color": "Címsor színe",
"advanced_styling_field_headline_color_description": "Kiszínezi a fő kérdés szövegét.",
"advanced_styling_field_headline_color_description": "Színezi a fő kérdés szövegét.",
"advanced_styling_field_headline_size": "Címsor betűmérete",
"advanced_styling_field_headline_size_description": "Átméretezi a címsor szövegét.",
"advanced_styling_field_headline_size_description": "Méretezi a címsor szövegét.",
"advanced_styling_field_headline_weight": "Címsor betűvastagsága",
"advanced_styling_field_headline_weight_description": "Vékonyabbá vagy vastagabbá teszi a címsor szövegét.",
"advanced_styling_field_height": "Minimális magasság",
"advanced_styling_field_height": "Magasság",
"advanced_styling_field_indicator_bg": "Jelző háttere",
"advanced_styling_field_indicator_bg_description": "Kiszínezi a sáv kitöltött részét.",
"advanced_styling_field_indicator_bg_description": "Színezi a sáv kitöltött részét.",
"advanced_styling_field_input_border_radius_description": "Lekerekíti a beviteli mező sarkait.",
"advanced_styling_field_input_font_size_description": "Átméretezi a beviteli mezőkbe beírt szöveget.",
"advanced_styling_field_input_height_description": "Szabályozza a beviteli mező minimális magasságát.",
"advanced_styling_field_input_padding_x_description": "Térközt ad hozzá balra és jobbra.",
"advanced_styling_field_input_padding_y_description": "Térközt ad hozzá fent és lent.",
"advanced_styling_field_input_placeholder_opacity_description": "Elhalványítja a helykitöltő súgószöveget.",
"advanced_styling_field_input_shadow_description": "Vetett árnyékot ad hozzá a beviteli mezők köré.",
"advanced_styling_field_input_font_size_description": "Méretezi a beviteli mezőkbe gépelt szöveget.",
"advanced_styling_field_input_height_description": "Szabályozza a beviteli mező magasságát.",
"advanced_styling_field_input_padding_x_description": "Térközt ad hozzá bal és jobb oldalon.",
"advanced_styling_field_input_padding_y_description": "Térközt ad hozzá felül és alul.",
"advanced_styling_field_input_placeholder_opacity_description": "Halványítja a helyőrző szöveget.",
"advanced_styling_field_input_shadow_description": "Árnyékot ad a beviteli mezők köré.",
"advanced_styling_field_input_text": "Beviteli mező szövege",
"advanced_styling_field_input_text_description": "Kiszínezi a beviteli mezőkbe beírt szöveget.",
"advanced_styling_field_input_text_description": "Színezi a beviteli mezőkbe írt szöveget.",
"advanced_styling_field_option_bg": "Háttér",
"advanced_styling_field_option_bg_description": "Kitölti a választási lehetőség elemeit.",
"advanced_styling_field_option_border": "Szegély színe",
"advanced_styling_field_option_border_description": "A rádiógomb és jelölőnégyzet opciók körvonalát határozza meg.",
"advanced_styling_field_option_border_radius_description": "Lekerekíti a választási lehetőség sarkait.",
"advanced_styling_field_option_font_size_description": "Átméretezi a választási lehetőség címkéjének szövegét.",
"advanced_styling_field_option_bg_description": "Kitölti az opciós elemeket.",
"advanced_styling_field_option_border_radius_description": "Lekerekíti az opciók sarkait.",
"advanced_styling_field_option_font_size_description": "Méretezi az opciók címkeszövegét.",
"advanced_styling_field_option_label": "Címke színe",
"advanced_styling_field_option_label_description": "Kiszínezi a választási lehetőség címkéjének szövegét.",
"advanced_styling_field_option_label_description": "Színezi az opciók címkeszövegét.",
"advanced_styling_field_option_padding_x_description": "Térközt ad hozzá balra és jobbra.",
"advanced_styling_field_option_padding_y_description": "Térközt ad hozzá fent és lent.",
"advanced_styling_field_padding_x": "X kitöltés",
"advanced_styling_field_padding_y": "Y kitöltés",
"advanced_styling_field_placeholder_opacity": "Helykitöltő átlátszatlansága",
"advanced_styling_field_option_padding_y_description": "Térközt ad hozzá felül és alul.",
"advanced_styling_field_padding_x": "Belső margó X",
"advanced_styling_field_padding_y": "Belső margó Y",
"advanced_styling_field_placeholder_opacity": "Helyőrző átlátszósága",
"advanced_styling_field_shadow": "Árnyék",
"advanced_styling_field_track_bg": "Követés háttere",
"advanced_styling_field_track_bg_description": "Kiszínezi a sáv kitöltetlen részét.",
"advanced_styling_field_track_height": "Követés magassága",
"advanced_styling_field_track_height_description": "A folyamatjelző vastagságát vezérli.",
"advanced_styling_field_upper_label_color": "Címsor címkéjének színe",
"advanced_styling_field_upper_label_color_description": "Kiszínezi a beviteli mezők fölötti kis címkéket.",
"advanced_styling_field_upper_label_size": "Címsor címkéjének betűmérete",
"advanced_styling_field_upper_label_size_description": "Átméretezi a beviteli mezők fölötti kis címkéket.",
"advanced_styling_field_upper_label_weight": "Címsor címkéjének betűvastagsága",
"advanced_styling_field_track_bg": "Sáv háttere",
"advanced_styling_field_track_bg_description": "Színezi a sáv kitöltetlen részét.",
"advanced_styling_field_track_height": "Sáv magassága",
"advanced_styling_field_track_height_description": "Szabályozza a folyamatjelző sáv vastagságát.",
"advanced_styling_field_upper_label_color": "Felső címke színe",
"advanced_styling_field_upper_label_color_description": "Színezi a beviteli mezők feletti kis címkét.",
"advanced_styling_field_upper_label_size": "Felső címke betűmérete",
"advanced_styling_field_upper_label_size_description": "Méretezi a beviteli mezők feletti kis címkét.",
"advanced_styling_field_upper_label_weight": "Felső címke betűvastagsága",
"advanced_styling_field_upper_label_weight_description": "Vékonyabbá vagy vastagabbá teszi a címkét.",
"advanced_styling_section_buttons": "Gombok",
"advanced_styling_section_headlines": "Címsorok és leírások",
"advanced_styling_section_inputs": "Beviteli mezők",
"advanced_styling_section_options": "Lehetőségek (rádiógomb vagy jelölőnégyzet)",
"advanced_styling_section_options": "Opciók (rádiógomb/jelölőnégyzet)",
"app_survey_placement": "Alkalmazás-kérdőív elhelyezése",
"app_survey_placement_settings_description": "Annak megváltoztatása, hogy a kérdőívek hol jelennek meg a webalkalmazásban vagy a webhelyen.",
"email_customization": "E-mail személyre szabás",
@@ -2226,8 +2139,8 @@
"formbricks_branding_settings_description": "Nagyra értékeljük a támogatását, de megértjük, ha kikapcsolja.",
"formbricks_branding_shown": "A Formbricks márkajel megjelenik.",
"generate_theme_btn": "Előállítás",
"generate_theme_confirmation": "Szeretne hozzáillő színtémát létrehozni a márkajel színei alapján? Ez felülírja a jelenlegi színbeállításokat.",
"generate_theme_header": "Előállítja a színtémát?",
"generate_theme_confirmation": "Szeretnél egy illeszkedő színtémát generálni a márka színed alapján? Ez felülírja a jelenlegi színbeállításaidat.",
"generate_theme_header": "Színtéma előállítása?",
"logo_removed_successfully": "A logó sikeresen eltávolítva",
"logo_settings_description": "Vállalati logo feltöltése a kérdőívek és hivatkozások előnézeteinek márkaépítéséhez.",
"logo_updated_successfully": "A logó sikeresen frissítve",
@@ -2242,8 +2155,7 @@
"show_formbricks_branding_in": "Formbricks márkajel megjelenítése a(z) {type} kérdőívekben",
"show_powered_by_formbricks": "Az „A gépházban: Formbricks” aláírás megjelenítése",
"styling_updated_successfully": "A stílus sikeresen frissítve",
"suggest_colors": "Színek ajánlása",
"suggested_colors_applied_please_save": "A javasolt színek sikeresen generálva. Nyomd meg a \"Mentés\" gombot a változtatások véglegesítéséhez.",
"suggest_colors": "Színek javaslása",
"theme": "Téma",
"theme_settings_description": "Stílustéma létrehozása az összes kérdőívhez. Egyéni stílust engedélyezhet minden egyes kérdőívhez."
},
@@ -2461,7 +2373,7 @@
"career_development_survey_question_6_choice_2": "Igazgató",
"career_development_survey_question_6_choice_3": "Vezető igazgató",
"career_development_survey_question_6_choice_4": "Alelnök",
"career_development_survey_question_6_choice_5": "Ügyvezető",
"career_development_survey_question_6_choice_5": "Igazgató",
"career_development_survey_question_6_choice_6": "Egyéb",
"career_development_survey_question_6_headline": "Az alábbiak közül melyik írja le legjobban a jelenlegi munkája szintjét?",
"career_development_survey_question_6_subheader": "Válassza ki a következő lehetőségek egyikét:",
@@ -2974,7 +2886,7 @@
"onboarding_segmentation": "Beléptetés szakaszolása",
"onboarding_segmentation_description": "További információk azzal kapcsolatban, hogy kik regisztráltak a termékére és miért.",
"onboarding_segmentation_question_1_choice_1": "Alapító",
"onboarding_segmentation_question_1_choice_2": "Ügyvezető",
"onboarding_segmentation_question_1_choice_2": "Igazgató",
"onboarding_segmentation_question_1_choice_3": "Termékmenedzser",
"onboarding_segmentation_question_1_choice_4": "Terméktulajdonos",
"onboarding_segmentation_question_1_choice_5": "Szoftvermérnök",
@@ -3007,10 +2919,7 @@
"preview_survey_question_2_choice_1_label": "Igen, folyamatosan tájékoztassanak.",
"preview_survey_question_2_choice_2_label": "Nem, köszönöm!",
"preview_survey_question_2_headline": "Szeretne naprakész maradni?",
"preview_survey_question_2_subheader": "Ez egy példa a leírásra.",
"preview_survey_question_open_text_headline": "Bármi egyéb, amit meg szeretne osztani?",
"preview_survey_question_open_text_placeholder": "Írja be ide a válaszát…",
"preview_survey_question_open_text_subheader": "A visszajelzése segít nekünk a fejlődésben.",
"preview_survey_question_2_subheader": "Ez egy példa leírás.",
"preview_survey_welcome_card_headline": "Üdvözöljük!",
"prioritize_features_description": "A felhasználóknak leginkább és legkevésbé szükséges funkciók azonosítása.",
"prioritize_features_name": "Funkciók rangsorolása",
@@ -3045,7 +2954,7 @@
"product_market_fit_superhuman_question_2_headline": "Mennyire lenne csalódott, ha többé nem használhatná a(z) $[projectName] projektet?",
"product_market_fit_superhuman_question_2_subheader": "Válassza ki a következő lehetőségek egyikét:",
"product_market_fit_superhuman_question_3_choice_1": "Alapító",
"product_market_fit_superhuman_question_3_choice_2": "Ügyvezető",
"product_market_fit_superhuman_question_3_choice_2": "Igazgató",
"product_market_fit_superhuman_question_3_choice_3": "Termékmenedzser",
"product_market_fit_superhuman_question_3_choice_4": "Terméktulajdonos",
"product_market_fit_superhuman_question_3_choice_5": "Szoftvermérnök",
+9 -100
View File
@@ -188,10 +188,8 @@
"customer_success": "カスタマーサクセス",
"dark_overlay": "暗いオーバーレイ",
"date": "日付",
"days": "日",
"default": "デフォルト",
"delete": "削除",
"delete_what": "{deleteWhat}を削除",
"description": "説明",
"dev_env": "開発環境",
"development": "開発",
@@ -207,8 +205,6 @@
"download": "ダウンロード",
"draft": "下書き",
"duplicate": "複製",
"duplicate_copy": "(コピー)",
"duplicate_copy_number": "(コピー {copyNumber})",
"e_commerce": "Eコマース",
"edit": "編集",
"email": "メールアドレス",
@@ -221,16 +217,13 @@
"error": "エラー",
"error_component_description": "この リソース は 存在 しない か、アクセス する ための 必要な 権限 が ありません。",
"error_component_title": "リソース の 読み込み エラー",
"error_loading_data": "データの読み込みエラー",
"error_rate_limit_description": "リクエストの最大数に達しました。後でもう一度試してください。",
"error_rate_limit_title": "レート制限を超えました",
"expand_rows": "行を展開",
"failed_to_copy_to_clipboard": "クリップボードへのコピーに失敗しました",
"failed_to_load_organizations": "組織の読み込みに失敗しました",
"failed_to_load_workspaces": "ワークスペースの読み込みに失敗しました",
"filter": "フィルター",
"finish": "完了",
"first_name": "名",
"follow_these": "こちらの手順に従って",
"formbricks_version": "Formbricksバージョン",
"full_name": "氏名",
@@ -243,7 +236,6 @@
"hidden_field": "非表示フィールド",
"hidden_fields": "非表示フィールド",
"hide_column": "列を非表示",
"id": "ID",
"image": "画像",
"images": "画像",
"import": "インポート",
@@ -261,7 +253,6 @@
"key": "キー",
"label": "ラベル",
"language": "言語",
"last_name": "姓",
"learn_more": "詳細を見る",
"license_expired": "License Expired",
"light_overlay": "明るいオーバーレイ",
@@ -284,11 +275,9 @@
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
"mobile_overlay_title": "おっと、 小さな 画面 が 検出されました!",
"months": "ヶ月",
"move_down": "下に移動",
"move_up": "上に移動",
"multiple_languages": "多言語",
"my_product": "マイプロダクト",
"name": "名前",
"new": "新規",
"new_version_available": "Formbricks {version} が利用可能です。今すぐアップグレード!",
@@ -404,7 +393,6 @@
"status": "ステータス",
"step_by_step_manual": "ステップバイステップマニュアル",
"storage_not_configured": "ファイルストレージが設定されていないため、アップロードは失敗する可能性があります",
"string": "テキスト",
"styling": "スタイル",
"submit": "送信",
"summary": "概要",
@@ -437,7 +425,6 @@
"top_right": "右上",
"try_again": "もう一度お試しください",
"type": "種類",
"unknown_survey": "不明なフォーム",
"unlock_more_workspaces_with_a_higher_plan": "上位プランでより多くのワークスペースを利用できます。",
"update": "更新",
"updated": "更新済み",
@@ -461,7 +448,6 @@
"website_and_app_connection": "ウェブサイト&アプリ接続",
"website_app_survey": "ウェブサイト&アプリフォーム",
"website_survey": "ウェブサイトフォーム",
"weeks": "週間",
"welcome_card": "ウェルカムカード",
"workspace_configuration": "ワークスペース設定",
"workspace_created_successfully": "ワークスペースが正常に作成されました",
@@ -472,7 +458,6 @@
"workspace_not_found": "ワークスペースが見つかりません",
"workspace_permission_not_found": "ワークスペースの権限が見つかりません",
"workspaces": "ワークスペース",
"years": "年",
"you": "あなた",
"you_are_downgraded_to_the_community_edition": "コミュニティ版にダウングレードされました。",
"you_are_not_authorized_to_perform_this_action": "このアクションを実行する権限がありません。",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "属性を更新しました",
"attribute_value": "値",
"attribute_value_placeholder": "属性値",
"attributes_msg_attribute_limit_exceeded": "最大制限の{limit}個の属性クラスを超えるため、{count}個の新しい属性を作成できませんでした。既存の属性は正常に更新されました。",
"attributes_msg_attribute_type_validation_error": "{error}(属性'{key}'のデータ型: {dataType}",
"attributes_msg_email_already_exists": "このメールアドレスはこの環境に既に存在するため、更新されませんでした。",
"attributes_msg_email_or_userid_required": "メールアドレスまたはユーザーIDのいずれかが必要です。既存の値は保持されました。",
"attributes_msg_new_attribute_created": "新しい属性'{key}'をタイプ'{dataType}'で作成しました",
"attributes_msg_userid_already_exists": "このユーザーIDはこの環境に既に存在するため、更新されませんでした。",
"contact_deleted_successfully": "連絡先を正常に削除しました",
"contact_not_found": "そのような連絡先は見つかりません",
"contacts_table_refresh": "連絡先を更新",
"contacts_table_refresh_success": "連絡先を正常に更新しました",
"create_attribute": "属性を作成",
"create_key": "キーを作成",
"create_new_attribute": "新しい属性を作成",
"create_new_attribute_description": "セグメンテーション用の新しい属性を作成します。",
"custom_attributes": "カスタム属性",
"data_type": "データ型",
"data_type_cannot_be_changed": "データ型は作成後に変更できません",
"data_type_description": "この属性の保存方法とフィルタリング方法を選択してください",
"date_value_required": "日付の値が必要です。日付を設定したくない場合は、削除ボタンを使用してこの属性を削除してください。",
"delete_attribute_confirmation": "{value, plural, one {選択した属性を削除します。この属性に関連付けられたすべてのコンタクトデータは失われます。} other {選択した属性を削除します。これらの属性に関連付けられたすべてのコンタクトデータは失われます。}}",
"delete_contact_confirmation": "これにより、この連絡先に関連付けられているすべてのフォーム回答と連絡先属性が削除されます。この連絡先のデータに基づいたターゲティングとパーソナライゼーションはすべて失われます。",
"delete_contact_confirmation_with_quotas": "{value, plural, one {これにより この連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。この連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。この連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。} other {これにより これらの連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。これらの連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。これらの連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。}}",
"displays": "表示回数",
"edit_attribute": "属性を編集",
"edit_attribute_description": "この属性のラベルと説明を更新します。",
"edit_attribute_values": "属性を編集",
"edit_attribute_values_description": "この連絡先の特定の属性の値を変更します。",
"edit_attributes": "属性を編集",
"edit_attributes_success": "連絡先属性が正常に更新されました",
"generate_personal_link": "個人リンクを生成",
"generate_personal_link_description": "公開されたフォームを選択して、この連絡先用のパーソナライズされたリンクを生成します。",
"invalid_csv_column_names": "無効なCSV列名: {columns}。新しい属性となる列名は、小文字、数字、アンダースコアのみを含み、文字で始まる必要があります。",
"invalid_date_format": "無効な日付形式です。有効な日付を使用してください。",
"invalid_number_format": "無効な数値形式です。有効な数値を入力してください。",
"no_activity_yet": "まだアクティビティがありません",
"no_published_link_surveys_available": "公開されたリンクフォームはありません。まずリンクフォームを公開してください。",
"no_published_surveys": "公開されたフォームはありません",
"no_responses_found": "回答が見つかりません",
"not_provided": "提供されていません",
"number_value_required": "数値が必要です。この属性を削除するには削除ボタンを使用してください。",
"personal_link_generated": "個人リンクが正常に生成されました",
"personal_link_generated_but_clipboard_failed": "個人用リンクは生成されましたが、クリップボードへのコピーに失敗しました: {url}",
"personal_survey_link": "個人調査リンク",
@@ -691,24 +659,13 @@
"search_contact": "連絡先を検索",
"select_a_survey": "フォームを選択",
"select_attribute": "属性を選択",
"select_attribute_key": "属性キーを選択",
"survey_viewed": "フォームを閲覧",
"survey_viewed_at": "閲覧日時",
"system_attributes": "システム属性",
"unlock_contacts_description": "連絡先を管理し、特定のフォームを送信します",
"unlock_contacts_title": "上位プランで連絡先をアンロック",
"upload_contacts_error_attribute_type_mismatch": "属性「{key}」は「{dataType}」として型付けされていますが、CSVに無効な値が含まれています:{values}",
"upload_contacts_error_duplicate_mappings": "次の属性に重複したマッピングが見つかりました:{attributes}",
"upload_contacts_error_file_too_large": "ファイルサイズが最大制限の800KBを超えています",
"upload_contacts_error_generic": "連絡先のアップロード中にエラーが発生しました。後でもう一度お試しください。",
"upload_contacts_error_invalid_file_type": "CSVファイルをアップロードしてください",
"upload_contacts_error_no_valid_contacts": "アップロードされたCSVファイルには有効な連絡先が含まれていません。正しい形式についてはサンプルCSVファイルをご確認ください。",
"upload_contacts_modal_attribute_header": "Formbricks属性",
"upload_contacts_modal_attributes_description": "CSVの列をFormbricksの属性にマッピングします。",
"upload_contacts_modal_attributes_new": "新しい属性",
"upload_contacts_modal_attributes_search_or_add": "属性を検索または追加",
"upload_contacts_modal_attributes_should_be_mapped_to": "は以下にマッピングする必要があります",
"upload_contacts_modal_attributes_title": "属性",
"upload_contacts_modal_csv_column_header": "CSV列",
"upload_contacts_modal_description": "CSVをアップロードして、属性を持つ連絡先をすばやくインポート",
"upload_contacts_modal_download_example_csv": "CSVの例をダウンロード",
"upload_contacts_modal_duplicates_description": "連絡先がすでに存在する場合、どのように処理しますか?",
@@ -765,12 +722,7 @@
"link_google_sheet": "スプレッドシートをリンク",
"link_new_sheet": "新しいシートをリンク",
"no_integrations_yet": "Google スプレッドシート連携は、追加するとここに表示されます。⏲️",
"reconnect_button": "再接続",
"reconnect_button_description": "Google Sheetsの接続が期限切れになりました。回答の同期を続けるには再接続してください。既存のスプレッドシートリンクとデータは保持されます。",
"reconnect_button_tooltip": "統合を再接続してアクセスを更新します。既存のスプレッドシートリンクとデータは保持されます。",
"spreadsheet_permission_error": "このスプレッドシートにアクセスする権限がありません。スプレッドシートがGoogleアカウントと共有されており、書き込みアクセス権があることを確認してください。",
"spreadsheet_url": "スプレッドシートURL",
"token_expired_error": "Google Sheetsのリフレッシュトークンが期限切れになったか、取り消されました。統合を再接続してください。"
"spreadsheet_url": "スプレッドシートURL"
},
"include_created_at": "作成日時を含める",
"include_hidden_fields": "非表示フィールドを含める",
@@ -894,40 +846,6 @@
"no_attributes_yet": "属性がまだありません!",
"no_filters_yet": "フィルターはまだありません!",
"no_segments_yet": "保存されたセグメントはまだありません。",
"operator_contains": "を含む",
"operator_does_not_contain": "を含まない",
"operator_ends_with": "で終わる",
"operator_is_after": "より後",
"operator_is_before": "より前",
"operator_is_between": "の間である",
"operator_is_newer_than": "より新しい",
"operator_is_not_set": "設定されていない",
"operator_is_older_than": "より古い",
"operator_is_same_day": "同じ日である",
"operator_is_set": "設定されている",
"operator_starts_with": "で始まる",
"operator_title_contains": "を含む",
"operator_title_does_not_contain": "を含まない",
"operator_title_ends_with": "で終わる",
"operator_title_equals": "と等しい",
"operator_title_greater_equal": "以上",
"operator_title_greater_than": "より大きい",
"operator_title_is_after": "より後",
"operator_title_is_before": "より前",
"operator_title_is_between": "の間である",
"operator_title_is_newer_than": "より新しい",
"operator_title_is_not_set": "設定されていない",
"operator_title_is_older_than": "より古い",
"operator_title_is_same_day": "同じ日である",
"operator_title_is_set": "設定されている",
"operator_title_less_equal": "以下",
"operator_title_less_than": "より小さい",
"operator_title_not_equals": "等しくない",
"operator_title_starts_with": "で始まる",
"operator_title_user_is_in": "ユーザーが含まれる",
"operator_title_user_is_not_in": "ユーザーが含まれない",
"operator_user_is_in": "ユーザーが含まれる",
"operator_user_is_not_in": "ユーザーが含まれない",
"person_and_attributes": "人物と属性",
"phone": "電話",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "このセグメントを削除するには、まず以下のフォームから外してください。",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "ユーザーターゲティングは現在、利用条件を満たす場合のみ利用可能です",
"value_cannot_be_empty": "値は空にできません。",
"value_must_be_a_number": "値は数値である必要があります。",
"value_must_be_positive": "値は正の数である必要があります。",
"view_filters": "フィルターを表示",
"where": "条件",
"with_the_formbricks_sdk": "Formbricks SDK を利用して"
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "こんにちは、{userName}さん",
"email_customization_preview_email_text": "これは、メールに表示されるロゴを確認するためのプレビューメールです。",
"error_deleting_organization_please_try_again": "組織の削除中にエラーが発生しました。もう一度お試しください。",
"from_your_organization": "組織から{memberName}を削除",
"from_your_organization": "あなたの組織から",
"invitation_sent_once_more": "招待状を再度送信しました。",
"invite_deleted_successfully": "招待を正常に削除しました",
"invite_expires_on": "招待は{date}に期限切れ",
@@ -1250,7 +1167,6 @@
"add_fallback_placeholder": "質問がスキップされた場合に表示するプレースホルダーを追加:",
"add_hidden_field_id": "非表示フィールドIDを追加",
"add_highlight_border": "ハイライトボーダーを追加",
"add_highlight_border_description": "プロダクト内サーベイにのみ適用されます。",
"add_logic": "ロジックを追加",
"add_none_of_the_above": "\"いずれも該当しません\" を追加",
"add_option": "オプションを追加",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "フォローアップ が 更新され、 アンケートを 保存すると保存されます。",
"follow_ups_new": "新しいフォローアップ",
"follow_ups_upgrade_button_text": "フォローアップを有効にするためにアップグレード",
"form_styling": "フォームのスタイル",
"formbricks_sdk_is_not_connected": "Formbricks SDKが接続されていません",
"four_points": "4点",
"heading": "見出し",
@@ -1621,7 +1538,7 @@
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
"response_options": "回答オプション",
"roundness": "丸み",
"roundness_description": "角の丸みを調整します。",
"roundness_description": "カードの角の丸みを調整します。",
"row_used_in_logic_error": "この行は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
"rows": "行",
"save_and_close": "保存して閉じる",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
"survey_display_settings": "フォーム表示設定",
"survey_placement": "フォームの配置",
"survey_styling": "フォームのスタイル",
"survey_trigger": "フォームのトリガー",
"switch_multi_language_on_to_get_started": "多言語機能をオンにして開始 👉",
"target_block_not_found": "対象ブロックが見つかりません",
@@ -1758,6 +1674,7 @@
"welcome_message": "ウェルカムメッセージ",
"without_a_filter_all_of_your_users_can_be_surveyed": "フィルターがなければ、すべてのユーザーがフォームに回答できます。",
"you_have_not_created_a_segment_yet": "まだセグメントを作成していません",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "翻訳を使用するには、ワークスペースに2つ以上の言語を設定する必要があります。",
"your_description_here_recall_information_with": "ここにあなたの説明。@ で情報を呼び出す",
"your_question_here_recall_information_with": "ここにあなたの質問。@ で情報を呼び出す",
"your_web_app": "あなたのウェブアプリ",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "フィルター済み回答 (Excel)",
"generating_qr_code": "QRコードを生成中",
"impressions": "表示回数",
"impressions_identified_only": "識別済みコンタクトからのインプレッションのみを表示しています",
"impressions_tooltip": "フォームが表示された回数。",
"in_app": {
"connection_description": "このフォームは、以下の条件に一致するあなたのウェブサイトのユーザーに表示されます",
@@ -2008,7 +1924,6 @@
"last_quarter": "前四半期",
"last_year": "昨年",
"limit": "制限",
"no_identified_impressions": "識別済みコンタクトからのインプレッションはありません",
"no_responses_found": "回答が見つかりません",
"other_values_found": "他の値が見つかりました",
"overall": "全体",
@@ -2173,12 +2088,12 @@
"advanced_styling_field_headline_size_description": "見出しテキストのサイズを調整します。",
"advanced_styling_field_headline_weight": "見出しのフォントの太さ",
"advanced_styling_field_headline_weight_description": "見出しテキストを細くまたは太くします。",
"advanced_styling_field_height": "最小の高さ",
"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_height_description": "入力フィールドの高さを調整します。",
"advanced_styling_field_input_padding_x_description": "左右にスペースを追加します。",
"advanced_styling_field_input_padding_y_description": "上下にスペースを追加します。",
"advanced_styling_field_input_placeholder_opacity_description": "プレースホルダーのヒントテキストを薄くします。",
@@ -2187,8 +2102,6 @@
"advanced_styling_field_input_text_description": "入力フィールドに入力されたテキストの色を設定します。",
"advanced_styling_field_option_bg": "背景",
"advanced_styling_field_option_bg_description": "オプション項目を塗りつぶします。",
"advanced_styling_field_option_border": "枠線の色",
"advanced_styling_field_option_border_description": "ラジオボタンとチェックボックスの選択肢の輪郭を設定します。",
"advanced_styling_field_option_border_radius_description": "オプションの角を丸くします。",
"advanced_styling_field_option_font_size_description": "オプションラベルのテキストサイズを調整します。",
"advanced_styling_field_option_label": "ラベルの色",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "「Powered by Formbricks」署名を表示",
"styling_updated_successfully": "スタイルを正常に更新しました",
"suggest_colors": "カラーを提案",
"suggested_colors_applied_please_save": "推奨カラーが正常に生成されました。変更を保存するには「保存」を押してください。",
"theme": "テーマ",
"theme_settings_description": "すべてのアンケート用のスタイルテーマを作成します。各アンケートでカスタムスタイルを有効にできます。"
},
@@ -3008,9 +2920,6 @@
"preview_survey_question_2_choice_2_label": "いいえ、結構です!",
"preview_survey_question_2_headline": "最新情報を知りたいですか?",
"preview_survey_question_2_subheader": "これは説明の例です。",
"preview_survey_question_open_text_headline": "他に共有したいことはありますか?",
"preview_survey_question_open_text_placeholder": "ここに回答を入力してください...",
"preview_survey_question_open_text_subheader": "あなたのフィードバックは、私たちの改善に役立ちます。",
"preview_survey_welcome_card_headline": "ようこそ!",
"prioritize_features_description": "ユーザーが最も必要とする機能と最も必要としない機能を特定する。",
"prioritize_features_name": "機能の優先順位付け",
+9 -100
View File
@@ -188,10 +188,8 @@
"customer_success": "Klant succes",
"dark_overlay": "Donkere overlay",
"date": "Datum",
"days": "dagen",
"default": "Standaard",
"delete": "Verwijderen",
"delete_what": "Verwijder {deleteWhat}",
"description": "Beschrijving",
"dev_env": "Ontwikkelomgeving",
"development": "Ontwikkeling",
@@ -207,8 +205,6 @@
"download": "Downloaden",
"draft": "Voorlopige versie",
"duplicate": "Duplicaat",
"duplicate_copy": "(kopie)",
"duplicate_copy_number": "(kopie {copyNumber})",
"e_commerce": "E-commerce",
"edit": "Bewerking",
"email": "E-mail",
@@ -221,16 +217,13 @@
"error": "Fout",
"error_component_description": "Deze bron bestaat niet of u beschikt niet over de benodigde toegangsrechten.",
"error_component_title": "Fout bij het laden van bronnen",
"error_loading_data": "Fout bij het laden van gegevens",
"error_rate_limit_description": "Maximaal aantal verzoeken bereikt. Probeer het later opnieuw.",
"error_rate_limit_title": "Tarieflimiet overschreden",
"expand_rows": "Vouw rijen uit",
"failed_to_copy_to_clipboard": "Kopiëren naar klembord mislukt",
"failed_to_load_organizations": "Laden van organisaties mislukt",
"failed_to_load_workspaces": "Laden van werkruimtes mislukt",
"filter": "Filter",
"finish": "Finish",
"first_name": "Voornaam",
"follow_these": "Volg deze",
"formbricks_version": "Formbricks-versie",
"full_name": "Volledige naam",
@@ -243,7 +236,6 @@
"hidden_field": "Verborgen veld",
"hidden_fields": "Verborgen velden",
"hide_column": "Kolom verbergen",
"id": "ID",
"image": "Afbeelding",
"images": "Afbeeldingen",
"import": "Importeren",
@@ -261,7 +253,6 @@
"key": "Sleutel",
"label": "Label",
"language": "Taal",
"last_name": "Achternaam",
"learn_more": "Meer informatie",
"license_expired": "License Expired",
"light_overlay": "Lichte overlay",
@@ -284,11 +275,9 @@
"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!",
"months": "maanden",
"move_down": "Ga naar beneden",
"move_up": "Ga omhoog",
"multiple_languages": "Meerdere talen",
"my_product": "mijn product",
"name": "Naam",
"new": "Nieuw",
"new_version_available": "Formbricks {version} is hier. Upgrade nu!",
@@ -404,7 +393,6 @@
"status": "Status",
"step_by_step_manual": "Stap voor stap handleiding",
"storage_not_configured": "Bestandsopslag is niet ingesteld, uploads zullen waarschijnlijk mislukken",
"string": "Tekst",
"styling": "Styling",
"submit": "Indienen",
"summary": "Samenvatting",
@@ -437,7 +425,6 @@
"top_right": "Rechtsboven",
"try_again": "Probeer het opnieuw",
"type": "Type",
"unknown_survey": "Onbekende enquête",
"unlock_more_workspaces_with_a_higher_plan": "Ontgrendel meer werkruimtes met een hoger abonnement.",
"update": "Update",
"updated": "Bijgewerkt",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Website- en app-verbinding",
"website_app_survey": "Website- en app-enquête",
"website_survey": "Website-enquête",
"weeks": "weken",
"welcome_card": "Welkomstkaart",
"workspace_configuration": "Werkruimte-configuratie",
"workspace_created_successfully": "Project succesvol aangemaakt",
@@ -472,7 +458,6 @@
"workspace_not_found": "Werkruimte niet gevonden",
"workspace_permission_not_found": "Werkruimte-machtiging niet gevonden",
"workspaces": "Werkruimtes",
"years": "jaren",
"you": "Jij",
"you_are_downgraded_to_the_community_edition": "Je bent gedowngraded naar de Community-editie.",
"you_are_not_authorized_to_perform_this_action": "U bent niet geautoriseerd om deze actie uit te voeren.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Attribuut succesvol bijgewerkt",
"attribute_value": "Waarde",
"attribute_value_placeholder": "Attribuutwaarde",
"attributes_msg_attribute_limit_exceeded": "Kon {count} nieuwe attribu(u)t(en) niet aanmaken omdat dit de maximale limiet van {limit} attribuutklassen zou overschrijden. Bestaande attributen zijn succesvol bijgewerkt.",
"attributes_msg_attribute_type_validation_error": "{error} (attribuut '{key}' heeft dataType: {dataType})",
"attributes_msg_email_already_exists": "Het e-mailadres bestaat al voor deze omgeving en is niet bijgewerkt.",
"attributes_msg_email_or_userid_required": "E-mailadres of userId is vereist. De bestaande waarden zijn behouden.",
"attributes_msg_new_attribute_created": "Nieuw attribuut '{key}' aangemaakt met type '{dataType}'",
"attributes_msg_userid_already_exists": "De userId bestaat al voor deze omgeving en is niet bijgewerkt.",
"contact_deleted_successfully": "Contact succesvol verwijderd",
"contact_not_found": "Er is geen dergelijk contact gevonden",
"contacts_table_refresh": "Vernieuw contacten",
"contacts_table_refresh_success": "Contacten zijn vernieuwd",
"create_attribute": "Attribuut aanmaken",
"create_key": "Sleutel aanmaken",
"create_new_attribute": "Nieuw attribuut aanmaken",
"create_new_attribute_description": "Maak een nieuw attribuut aan voor segmentatiedoeleinden.",
"custom_attributes": "Aangepaste kenmerken",
"data_type": "Gegevenstype",
"data_type_cannot_be_changed": "Gegevenstype kan niet worden gewijzigd na aanmaak",
"data_type_description": "Kies hoe dit attribuut moet worden opgeslagen en gefilterd",
"date_value_required": "Datumwaarde is vereist. Gebruik de verwijderknop om dit attribuut te verwijderen als je geen datum wilt instellen.",
"delete_attribute_confirmation": "{value, plural, one {Dit verwijdert het geselecteerde attribuut. Alle contactgegevens die aan dit attribuut zijn gekoppeld, gaan verloren.} other {Dit verwijdert de geselecteerde attributen. Alle contactgegevens die aan deze attributen zijn gekoppeld, gaan verloren.}}",
"delete_contact_confirmation": "Hierdoor worden alle enquêtereacties en contactkenmerken verwijderd die aan dit contact zijn gekoppeld. Elke targeting en personalisatie op basis van de gegevens van dit contact gaat verloren.",
"delete_contact_confirmation_with_quotas": "{value, plural, one {Dit verwijdert alle enquêteresultaten en contactattributen die aan dit contact zijn gekoppeld. Alle targeting en personalisatie op basis van de gegevens van dit contact gaan verloren. Als dit contact reacties heeft die meetellen voor enquêtekvota, worden de quotawaarden verlaagd maar blijven de limieten ongewijzigd.} other {Dit verwijdert alle enquêteresultaten en contactattributen die aan deze contacten zijn gekoppeld. Alle targeting en personalisatie op basis van de gegevens van deze contacten gaan verloren. Als deze contacten reacties hebben die meetellen voor enquêtekvota, worden de quotawaarden verlaagd maar blijven de limieten ongewijzigd.}}",
"displays": "Weergaven",
"edit_attribute": "Attribuut bewerken",
"edit_attribute_description": "Werk het label en de beschrijving voor dit attribuut bij.",
"edit_attribute_values": "Attributen bewerken",
"edit_attribute_values_description": "Wijzig de waarden voor specifieke attributen voor dit contact.",
"edit_attributes": "Attributen bewerken",
"edit_attributes_success": "Contactattributen succesvol bijgewerkt",
"generate_personal_link": "Persoonlijke link genereren",
"generate_personal_link_description": "Selecteer een gepubliceerde enquête om een gepersonaliseerde link voor dit contact te genereren.",
"invalid_csv_column_names": "Ongeldige CSV-kolomna(a)m(en): {columns}. Kolomnamen die nieuwe kenmerken worden, mogen alleen kleine letters, cijfers en underscores bevatten en moeten beginnen met een letter.",
"invalid_date_format": "Ongeldig datumformaat. Gebruik een geldige datum.",
"invalid_number_format": "Ongeldig getalformaat. Voer een geldig getal in.",
"no_activity_yet": "Nog geen activiteit",
"no_published_link_surveys_available": "Geen gepubliceerde link-enquêtes beschikbaar. Publiceer eerst een link-enquête.",
"no_published_surveys": "Geen gepubliceerde enquêtes",
"no_responses_found": "Geen reacties gevonden",
"not_provided": "Niet voorzien",
"number_value_required": "Getalwaarde is verplicht. Gebruik de verwijderknop om dit attribuut te verwijderen.",
"personal_link_generated": "Persoonlijke link succesvol gegenereerd",
"personal_link_generated_but_clipboard_failed": "Persoonlijke link gegenereerd maar kopiëren naar klembord mislukt: {url}",
"personal_survey_link": "Persoonlijke enquêtelink",
@@ -691,24 +659,13 @@
"search_contact": "Zoek contactpersoon",
"select_a_survey": "Selecteer een enquête",
"select_attribute": "Selecteer Kenmerk",
"select_attribute_key": "Selecteer kenmerksleutel",
"survey_viewed": "Enquête bekeken",
"survey_viewed_at": "Bekeken op",
"system_attributes": "Systeemkenmerken",
"unlock_contacts_description": "Beheer contacten en verstuur gerichte enquêtes",
"unlock_contacts_title": "Ontgrendel contacten met een hoger abonnement",
"upload_contacts_error_attribute_type_mismatch": "Attribuut \"{key}\" is getypeerd als \"{dataType}\" maar CSV bevat ongeldige waarden: {values}",
"upload_contacts_error_duplicate_mappings": "Dubbele koppelingen gevonden voor de volgende attributen: {attributes}",
"upload_contacts_error_file_too_large": "Bestandsgrootte overschrijdt de maximale limiet van 800KB",
"upload_contacts_error_generic": "Er is een fout opgetreden bij het uploaden van de contacten. Probeer het later opnieuw.",
"upload_contacts_error_invalid_file_type": "Upload een CSV-bestand",
"upload_contacts_error_no_valid_contacts": "Het geüploade CSV-bestand bevat geen geldige contacten, zie het voorbeeld CSV-bestand voor het juiste formaat.",
"upload_contacts_modal_attribute_header": "Formbricks attribuut",
"upload_contacts_modal_attributes_description": "Wijs de kolommen in uw CSV toe aan de attributen in Formbricks.",
"upload_contacts_modal_attributes_new": "Nieuw attribuut",
"upload_contacts_modal_attributes_search_or_add": "Kenmerk zoeken of toevoegen",
"upload_contacts_modal_attributes_should_be_mapped_to": "in kaart moeten worden gebracht",
"upload_contacts_modal_attributes_title": "Kenmerken",
"upload_contacts_modal_csv_column_header": "CSV kolom",
"upload_contacts_modal_description": "Upload een CSV om snel contacten met attributen te importeren",
"upload_contacts_modal_download_example_csv": "Voorbeeld-CSV downloaden",
"upload_contacts_modal_duplicates_description": "Hoe moeten we omgaan als er al een contact bestaat in uw contacten?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Link Google Spreadsheet",
"link_new_sheet": "Nieuw blad koppelen",
"no_integrations_yet": "Uw Google Spreadsheet-integraties verschijnen hier zodra u ze toevoegt. ⏲️",
"reconnect_button": "Maak opnieuw verbinding",
"reconnect_button_description": "Je Google Sheets-verbinding is verlopen. Maak opnieuw verbinding om door te gaan met het synchroniseren van antwoorden. Je bestaande spreadsheetlinks en gegevens blijven behouden.",
"reconnect_button_tooltip": "Maak opnieuw verbinding met de integratie om je toegang te vernieuwen. Je bestaande spreadsheetlinks en gegevens blijven behouden.",
"spreadsheet_permission_error": "Je hebt geen toestemming om deze spreadsheet te openen. Zorg ervoor dat de spreadsheet is gedeeld met je Google-account en dat je schrijftoegang hebt tot de spreadsheet.",
"spreadsheet_url": "Spreadsheet-URL",
"token_expired_error": "Het vernieuwingstoken van Google Sheets is verlopen of ingetrokken. Maak opnieuw verbinding met de integratie."
"spreadsheet_url": "Spreadsheet-URL"
},
"include_created_at": "Inclusief gemaakt op",
"include_hidden_fields": "Inclusief verborgen velden",
@@ -894,40 +846,6 @@
"no_attributes_yet": "Nog geen attributen!",
"no_filters_yet": "Er zijn nog geen filters!",
"no_segments_yet": "Je hebt momenteel geen opgeslagen segmenten.",
"operator_contains": "bevat",
"operator_does_not_contain": "bevat niet",
"operator_ends_with": "eindigt met",
"operator_is_after": "is na",
"operator_is_before": "is eerder",
"operator_is_between": "is tussen",
"operator_is_newer_than": "is nieuwer dan",
"operator_is_not_set": "is niet ingesteld",
"operator_is_older_than": "is ouder dan",
"operator_is_same_day": "is dezelfde dag",
"operator_is_set": "is ingesteld",
"operator_starts_with": "begint met",
"operator_title_contains": "Bevat",
"operator_title_does_not_contain": "Bevat niet",
"operator_title_ends_with": "Eindigt met",
"operator_title_equals": "Gelijk aan",
"operator_title_greater_equal": "Groter dan of gelijk aan",
"operator_title_greater_than": "Groter dan",
"operator_title_is_after": "Is na",
"operator_title_is_before": "Is eerder",
"operator_title_is_between": "Is tussen",
"operator_title_is_newer_than": "Is nieuwer dan",
"operator_title_is_not_set": "Is niet ingesteld",
"operator_title_is_older_than": "Is ouder dan",
"operator_title_is_same_day": "Is dezelfde dag",
"operator_title_is_set": "Is ingesteld",
"operator_title_less_equal": "Kleiner dan of gelijk aan",
"operator_title_less_than": "Kleiner dan",
"operator_title_not_equals": "Is niet gelijk aan",
"operator_title_starts_with": "Begint met",
"operator_title_user_is_in": "Gebruiker zit in",
"operator_title_user_is_not_in": "Gebruiker zit niet in",
"operator_user_is_in": "Gebruiker zit in",
"operator_user_is_not_in": "Gebruiker zit niet in",
"person_and_attributes": "Persoon & attributen",
"phone": "Telefoon",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Verwijder het segment uit deze enquêtes om het te kunnen verwijderen.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "Gebruikerstargeting is momenteel alleen beschikbaar wanneer",
"value_cannot_be_empty": "Waarde kan niet leeg zijn.",
"value_must_be_a_number": "Waarde moet een getal zijn.",
"value_must_be_positive": "Waarde moet een positief getal zijn.",
"view_filters": "Bekijk filters",
"where": "Waar",
"with_the_formbricks_sdk": "met de Formbricks SDK"
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Hé {userName}",
"email_customization_preview_email_text": "Dit is een e-mailvoorbeeld om u te laten zien welk logo in de e-mails wordt weergegeven.",
"error_deleting_organization_please_try_again": "Fout bij verwijderen van organisatie. Probeer het opnieuw.",
"from_your_organization": "{memberName} uit je organisatie",
"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}",
@@ -1250,7 +1167,6 @@
"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": "Geldt alleen voor in-product enquêtes.",
"add_logic": "Voeg logica toe",
"add_none_of_the_above": "Voeg 'Geen van bovenstaande' toe",
"add_option": "Optie toevoegen",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "Follow-up bijgewerkt en wordt opgeslagen zodra u de enquête opslaat.",
"follow_ups_new": "Nieuw vervolg",
"follow_ups_upgrade_button_text": "Upgrade om follow-ups mogelijk te maken",
"form_styling": "Vorm styling",
"formbricks_sdk_is_not_connected": "Formbricks SDK is niet verbonden",
"four_points": "4 punten",
"heading": "Rubriek",
@@ -1621,7 +1538,7 @@
"response_limits_redirections_and_more": "Reactielimieten, omleidingen en meer.",
"response_options": "Reactieopties",
"roundness": "Rondheid",
"roundness_description": "Bepaalt hoe afgerond de hoeken zijn.",
"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",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
"survey_display_settings": "Enquêteweergave-instellingen",
"survey_placement": "Enquête plaatsing",
"survey_styling": "Vorm styling",
"survey_trigger": "Enquêtetrigger",
"switch_multi_language_on_to_get_started": "Schakel meertaligheid in om te beginnen 👉",
"target_block_not_found": "Doelblok niet gevonden",
@@ -1758,6 +1674,7 @@
"welcome_message": "Welkomstbericht",
"without_a_filter_all_of_your_users_can_be_surveyed": "Zonder filter kunnen al uw gebruikers worden bevraagd.",
"you_have_not_created_a_segment_yet": "U heeft nog geen segment aangemaakt",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Je moet twee of meer talen hebben ingesteld in je werkruimte om met vertalingen te kunnen werken.",
"your_description_here_recall_information_with": "Uw beschrijving hier. Roep informatie op met @",
"your_question_here_recall_information_with": "Uw vraag hier. Roep informatie op met @",
"your_web_app": "Uw web-app",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Gefilterde reacties (Excel)",
"generating_qr_code": "QR-code genereren",
"impressions": "Indrukken",
"impressions_identified_only": "Alleen weergaven van geïdentificeerde contacten worden getoond",
"impressions_tooltip": "Aantal keren dat de enquête is bekeken.",
"in_app": {
"connection_description": "De enquête wordt getoond aan gebruikers van uw website die voldoen aan de onderstaande criteria",
@@ -2008,7 +1924,6 @@
"last_quarter": "Laatste kwartaal",
"last_year": "Vorig jaar",
"limit": "Beperken",
"no_identified_impressions": "Geen weergaven van geïdentificeerde contacten",
"no_responses_found": "Geen reacties gevonden",
"other_values_found": "Andere waarden gevonden",
"overall": "Algemeen",
@@ -2173,12 +2088,12 @@
"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": "Minimale hoogte",
"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 min. hoogte van het invoerveld.",
"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.",
@@ -2187,8 +2102,6 @@
"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": "Randkleur",
"advanced_styling_field_option_border_description": "Omlijnt radio- en checkboxopties.",
"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",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "Toon 'Powered by Formbricks' handtekening",
"styling_updated_successfully": "Styling succesvol bijgewerkt",
"suggest_colors": "Kleuren voorstellen",
"suggested_colors_applied_please_save": "Voorgestelde kleuren succesvol gegenereerd. Druk op \"Opslaan\" om de wijzigingen te behouden.",
"theme": "Thema",
"theme_settings_description": "Maak een stijlthema voor alle enquêtes. Je kunt aangepaste styling inschakelen voor elke enquête."
},
@@ -3008,9 +2920,6 @@
"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_question_open_text_headline": "Wil je nog iets delen?",
"preview_survey_question_open_text_placeholder": "Typ hier je antwoord...",
"preview_survey_question_open_text_subheader": "Je feedback helpt ons verbeteren.",
"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",
+9 -100
View File
@@ -188,10 +188,8 @@
"customer_success": "Sucesso do Cliente",
"dark_overlay": "sobreposição escura",
"date": "Encontro",
"days": "dias",
"default": "Padrão",
"delete": "Apagar",
"delete_what": "Excluir {deleteWhat}",
"description": "Descrição",
"dev_env": "Ambiente de Desenvolvimento",
"development": "Desenvolvimento",
@@ -207,8 +205,6 @@
"download": "baixar",
"draft": "Rascunho",
"duplicate": "Duplicar",
"duplicate_copy": "(cópia)",
"duplicate_copy_number": "(cópia {copyNumber})",
"e_commerce": "comércio eletrônico",
"edit": "Editar",
"email": "Email",
@@ -221,16 +217,13 @@
"error": "Erro",
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
"error_component_title": "Erro ao carregar recursos",
"error_loading_data": "Erro ao carregar dados",
"error_rate_limit_description": "Número máximo de requisições atingido. Por favor, tente novamente mais tarde.",
"error_rate_limit_title": "Limite de Taxa Excedido",
"expand_rows": "Expandir linhas",
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
"failed_to_load_organizations": "Falha ao carregar organizações",
"failed_to_load_workspaces": "Falha ao carregar projetos",
"filter": "Filtro",
"finish": "Terminar",
"first_name": "Primeiro nome",
"follow_these": "Siga esses",
"formbricks_version": "Versão do Formbricks",
"full_name": "Nome completo",
@@ -243,7 +236,6 @@
"hidden_field": "Campo oculto",
"hidden_fields": "Campos ocultos",
"hide_column": "Ocultar coluna",
"id": "ID",
"image": "imagem",
"images": "Imagens",
"import": "importar",
@@ -261,7 +253,6 @@
"key": "Chave",
"label": "Etiqueta",
"language": "Língua",
"last_name": "Sobrenome",
"learn_more": "Saiba mais",
"license_expired": "License Expired",
"light_overlay": "sobreposição leve",
@@ -284,11 +275,9 @@
"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!",
"months": "meses",
"move_down": "Descer",
"move_up": "Subir",
"multiple_languages": "Vários idiomas",
"my_product": "meu produto",
"name": "Nome",
"new": "Novo",
"new_version_available": "Formbricks {version} chegou. Atualize agora!",
@@ -404,7 +393,6 @@
"status": "status",
"step_by_step_manual": "Manual passo a passo",
"storage_not_configured": "Armazenamento de arquivos não configurado, uploads provavelmente falharão",
"string": "Texto",
"styling": "Estilização",
"submit": "Enviar",
"summary": "Resumo",
@@ -437,7 +425,6 @@
"top_right": "Canto Superior Direito",
"try_again": "Tenta de novo",
"type": "Tipo",
"unknown_survey": "Pesquisa desconhecida",
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
"update": "atualizar",
"updated": "atualizado",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Conexão de Site e App",
"website_app_survey": "Pesquisa de Site e App",
"website_survey": "Pesquisa de Site",
"weeks": "semanas",
"welcome_card": "Cartão de boas-vindas",
"workspace_configuration": "Configuração do projeto",
"workspace_created_successfully": "Projeto criado com sucesso",
@@ -472,7 +458,6 @@
"workspace_not_found": "Projeto não encontrado",
"workspace_permission_not_found": "Permissão do projeto não encontrada",
"workspaces": "Projetos",
"years": "anos",
"you": "Você",
"you_are_downgraded_to_the_community_edition": "Você foi rebaixado para a Edição Comunitária.",
"you_are_not_authorized_to_perform_this_action": "Você não tem autorização para realizar essa ação.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Atributo atualizado com sucesso",
"attribute_value": "Valor",
"attribute_value_placeholder": "Valor do atributo",
"attributes_msg_attribute_limit_exceeded": "Não foi possível criar {count} novo(s) atributo(s), pois excederia o limite máximo de {limit} classes de atributos. Os atributos existentes foram atualizados com sucesso.",
"attributes_msg_attribute_type_validation_error": "{error} (atributo '{key}' tem dataType: {dataType})",
"attributes_msg_email_already_exists": "O e-mail já existe para este ambiente e não foi atualizado.",
"attributes_msg_email_or_userid_required": "E-mail ou userId é obrigatório. Os valores existentes foram preservados.",
"attributes_msg_new_attribute_created": "Novo atributo '{key}' criado com tipo '{dataType}'",
"attributes_msg_userid_already_exists": "O userId já existe para este ambiente e não foi atualizado.",
"contact_deleted_successfully": "Contato excluído com sucesso",
"contact_not_found": "Nenhum contato encontrado",
"contacts_table_refresh": "Atualizar contatos",
"contacts_table_refresh_success": "Contatos atualizados com sucesso",
"create_attribute": "Criar atributo",
"create_key": "Criar chave",
"create_new_attribute": "Criar novo atributo",
"create_new_attribute_description": "Crie um novo atributo para fins de segmentação.",
"custom_attributes": "Atributos personalizados",
"data_type": "Tipo de dados",
"data_type_cannot_be_changed": "O tipo de dados não pode ser alterado após a criação",
"data_type_description": "Escolha como este atributo deve ser armazenado e filtrado",
"date_value_required": "O valor da data é obrigatório. Use o botão excluir para remover este atributo se você não quiser definir uma data.",
"delete_attribute_confirmation": "{value, plural, one {Isso excluirá o atributo selecionado. Todos os dados de contato associados a este atributo serão perdidos.} other {Isso excluirá os atributos selecionados. Todos os dados de contato associados a estes atributos serão perdidos.}}",
"delete_contact_confirmation": "Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos. Se este contato tiver respostas que contam para cotas da pesquisa, as contagens das cotas serão reduzidas, mas os limites das cotas permanecerão inalterados.}}",
"displays": "Exibições",
"edit_attribute": "Editar atributo",
"edit_attribute_description": "Atualize a etiqueta e a descrição deste atributo.",
"edit_attribute_values": "Editar atributos",
"edit_attribute_values_description": "Altere os valores de atributos específicos para este contato.",
"edit_attributes": "Editar atributos",
"edit_attributes_success": "Atributos do contato atualizados com sucesso",
"generate_personal_link": "Gerar link pessoal",
"generate_personal_link_description": "Selecione uma pesquisa publicada para gerar um link personalizado para este contato.",
"invalid_csv_column_names": "Nome(s) de coluna CSV inválido(s): {columns}. Os nomes de colunas que se tornarão novos atributos devem conter apenas letras minúsculas, números e sublinhados, e devem começar com uma letra.",
"invalid_date_format": "Formato de data inválido. Por favor, use uma data válida.",
"invalid_number_format": "Formato de número inválido. Por favor, insira um número válido.",
"no_activity_yet": "Nenhuma atividade ainda",
"no_published_link_surveys_available": "Não há pesquisas de link publicadas disponíveis. Por favor, publique uma pesquisa de link primeiro.",
"no_published_surveys": "Sem pesquisas publicadas",
"no_responses_found": "Nenhuma resposta encontrada",
"not_provided": "Não fornecido",
"number_value_required": "O valor numérico é obrigatório. Use o botão excluir para remover este atributo.",
"personal_link_generated": "Link pessoal gerado com sucesso",
"personal_link_generated_but_clipboard_failed": "Link pessoal gerado, mas falha ao copiar para a área de transferência: {url}",
"personal_survey_link": "Link da pesquisa pessoal",
@@ -691,24 +659,13 @@
"search_contact": "Buscar contato",
"select_a_survey": "Selecione uma pesquisa",
"select_attribute": "Selecionar Atributo",
"select_attribute_key": "Selecionar chave de atributo",
"survey_viewed": "Pesquisa visualizada",
"survey_viewed_at": "Visualizada em",
"system_attributes": "Atributos do sistema",
"unlock_contacts_description": "Gerencie contatos e envie pesquisas direcionadas",
"unlock_contacts_title": "Desbloqueie contatos com um plano superior",
"upload_contacts_error_attribute_type_mismatch": "O atributo \"{key}\" está tipado como \"{dataType}\", mas o CSV contém valores inválidos: {values}",
"upload_contacts_error_duplicate_mappings": "Mapeamentos duplicados encontrados para os seguintes atributos: {attributes}",
"upload_contacts_error_file_too_large": "O tamanho do arquivo excede o limite máximo de 800KB",
"upload_contacts_error_generic": "Ocorreu um erro ao fazer upload dos contatos. Por favor, tente novamente mais tarde.",
"upload_contacts_error_invalid_file_type": "Por favor, faça upload de um arquivo CSV",
"upload_contacts_error_no_valid_contacts": "O arquivo CSV enviado não contém nenhum contato válido, por favor veja o arquivo CSV de exemplo para o formato correto.",
"upload_contacts_modal_attribute_header": "Atributo do Formbricks",
"upload_contacts_modal_attributes_description": "Mapeie as colunas do seu CSV para os atributos no Formbricks.",
"upload_contacts_modal_attributes_new": "Novo atributo",
"upload_contacts_modal_attributes_search_or_add": "Buscar ou adicionar atributo",
"upload_contacts_modal_attributes_should_be_mapped_to": "deve ser mapeado para",
"upload_contacts_modal_attributes_title": "Atributos",
"upload_contacts_modal_csv_column_header": "Coluna CSV",
"upload_contacts_modal_description": "Faça upload de um CSV para importar contatos com atributos rapidamente",
"upload_contacts_modal_download_example_csv": "Baixar exemplo de CSV",
"upload_contacts_modal_duplicates_description": "O que devemos fazer se um contato já existir nos seus contatos?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Link da Planilha do Google",
"link_new_sheet": "Vincular nova planilha",
"no_integrations_yet": "Suas integrações do Google Sheets vão aparecer aqui assim que você adicioná-las. ⏲️",
"reconnect_button": "Reconectar",
"reconnect_button_description": "Sua conexão com o Google Sheets expirou. Reconecte para continuar sincronizando respostas. Seus links de planilhas e dados existentes serão preservados.",
"reconnect_button_tooltip": "Reconecte a integração para atualizar seu acesso. Seus links de planilhas e dados existentes serão preservados.",
"spreadsheet_permission_error": "Você não tem permissão para acessar esta planilha. Certifique-se de que a planilha está compartilhada com sua conta do Google e que você tem acesso de escrita à planilha.",
"spreadsheet_url": "URL da planilha",
"token_expired_error": "O token de atualização do Google Sheets expirou ou foi revogado. Reconecte a integração."
"spreadsheet_url": "URL da planilha"
},
"include_created_at": "Incluir Data de Criação",
"include_hidden_fields": "Incluir Campos Ocultos",
@@ -894,40 +846,6 @@
"no_attributes_yet": "Ainda não tem atributos!",
"no_filters_yet": "Ainda não tem filtros!",
"no_segments_yet": "Você não tem segmentos salvos no momento.",
"operator_contains": "contém",
"operator_does_not_contain": "não contém",
"operator_ends_with": "termina com",
"operator_is_after": "é depois",
"operator_is_before": "é antes",
"operator_is_between": "está entre",
"operator_is_newer_than": "é mais recente que",
"operator_is_not_set": "não está definido",
"operator_is_older_than": "é mais antigo que",
"operator_is_same_day": "é no mesmo dia",
"operator_is_set": "está definido",
"operator_starts_with": "começa com",
"operator_title_contains": "Contém",
"operator_title_does_not_contain": "Não contém",
"operator_title_ends_with": "Termina com",
"operator_title_equals": "Igual",
"operator_title_greater_equal": "Maior ou igual a",
"operator_title_greater_than": "Maior que",
"operator_title_is_after": "É depois",
"operator_title_is_before": "É antes",
"operator_title_is_between": "Está entre",
"operator_title_is_newer_than": "É mais recente que",
"operator_title_is_not_set": "Não está definido",
"operator_title_is_older_than": "É mais antigo que",
"operator_title_is_same_day": "É no mesmo dia",
"operator_title_is_set": "Está definido",
"operator_title_less_equal": "Menor ou igual a",
"operator_title_less_than": "Menor que",
"operator_title_not_equals": "Diferente de",
"operator_title_starts_with": "Começa com",
"operator_title_user_is_in": "Usuário está em",
"operator_title_user_is_not_in": "Usuário não está em",
"operator_user_is_in": "Usuário está em",
"operator_user_is_not_in": "Usuário não está em",
"person_and_attributes": "Pessoa & Atributos",
"phone": "Celular",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Por favor, remova o segmento dessas pesquisas para deletá-lo.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "A segmentação de usuários está disponível apenas quando",
"value_cannot_be_empty": "O valor não pode estar vazio.",
"value_must_be_a_number": "O valor deve ser um número.",
"value_must_be_positive": "O valor deve ser um número positivo.",
"view_filters": "Ver filtros",
"where": "Onde",
"with_the_formbricks_sdk": "com o SDK do Formbricks."
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Oi {userName}",
"email_customization_preview_email_text": "Esta é uma pré-visualização de e-mail para mostrar qual logo será renderizado nos e-mails.",
"error_deleting_organization_please_try_again": "Erro ao deletar a organização. Por favor, tente novamente.",
"from_your_organization": "{memberName} da sua organização",
"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}",
@@ -1250,7 +1167,6 @@
"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": "Aplica-se apenas a pesquisas no produto.",
"add_logic": "Adicionar lógica",
"add_none_of_the_above": "Adicionar \"Nenhuma das opções acima\"",
"add_option": "Adicionar opção",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "Acompanhamento atualizado e será salvo assim que você salvar a pesquisa.",
"follow_ups_new": "Novo acompanhamento",
"follow_ups_upgrade_button_text": "Atualize para habilitar os Acompanhamentos",
"form_styling": "Estilização de Formulários",
"formbricks_sdk_is_not_connected": "O SDK do Formbricks não está conectado",
"four_points": "4 pontos",
"heading": "Título",
@@ -1621,7 +1538,7 @@
"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.",
"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",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
"survey_display_settings": "Configurações de Exibição da Pesquisa",
"survey_placement": "Posicionamento da Pesquisa",
"survey_styling": "Estilização de Formulários",
"survey_trigger": "Gatilho de Pesquisa",
"switch_multi_language_on_to_get_started": "Ative o modo multilíngue para começar 👉",
"target_block_not_found": "Bloco de destino não encontrado",
@@ -1758,6 +1674,7 @@
"welcome_message": "Mensagem de boas-vindas",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus usuários podem ser pesquisados.",
"you_have_not_created_a_segment_yet": "Você ainda não criou um segmento.",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Você precisa ter dois ou mais idiomas configurados em seu espaço de trabalho para trabalhar com traduções.",
"your_description_here_recall_information_with": "Sua descrição aqui. Lembre-se de informações com @",
"your_question_here_recall_information_with": "Sua pergunta aqui. Lembre-se de informações com @",
"your_web_app": "Sua aplicação web",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Respostas filtradas (Excel)",
"generating_qr_code": "Gerando código QR",
"impressions": "Impressões",
"impressions_identified_only": "Mostrando apenas impressões de contatos identificados",
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
"in_app": {
"connection_description": "A pesquisa será exibida para usuários do seu site, que atendam aos critérios listados abaixo",
@@ -2008,7 +1924,6 @@
"last_quarter": "Último trimestre",
"last_year": "Último ano",
"limit": "Limite",
"no_identified_impressions": "Nenhuma impressão de contatos identificados",
"no_responses_found": "Nenhuma resposta encontrada",
"other_values_found": "Outros valores encontrados",
"overall": "No geral",
@@ -2173,12 +2088,12 @@
"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 mínima",
"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 mínima da entrada.",
"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.",
@@ -2187,8 +2102,6 @@
"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": "Cor da borda",
"advanced_styling_field_option_border_description": "Contorna as opções de botões de rádio e caixas de seleçã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",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo atualizado com sucesso",
"suggest_colors": "Sugerir cores",
"suggested_colors_applied_please_save": "Cores sugeridas geradas com sucesso. Pressione \"Salvar\" para manter as alterações.",
"theme": "Tema",
"theme_settings_description": "Crie um tema de estilo para todas as pesquisas. Você pode ativar estilo personalizado para cada pesquisa."
},
@@ -3008,9 +2920,6 @@
"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_question_open_text_headline": "Tem mais alguma coisa que você gostaria de compartilhar?",
"preview_survey_question_open_text_placeholder": "Digite sua resposta aqui...",
"preview_survey_question_open_text_subheader": "Seu feedback nos ajuda a melhorar.",
"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",
+9 -100
View File
@@ -188,10 +188,8 @@
"customer_success": "Sucesso do Cliente",
"dark_overlay": "Sobreposição escura",
"date": "Data",
"days": "dias",
"default": "Padrão",
"delete": "Eliminar",
"delete_what": "Eliminar {deleteWhat}",
"description": "Descrição",
"dev_env": "Ambiente de Desenvolvimento",
"development": "Desenvolvimento",
@@ -207,8 +205,6 @@
"download": "Transferir",
"draft": "Rascunho",
"duplicate": "Duplicar",
"duplicate_copy": "(cópia)",
"duplicate_copy_number": "(cópia {copyNumber})",
"e_commerce": "Comércio Eletrónico",
"edit": "Editar",
"email": "Email",
@@ -221,16 +217,13 @@
"error": "Erro",
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
"error_component_title": "Erro ao carregar recursos",
"error_loading_data": "Erro ao carregar dados",
"error_rate_limit_description": "Número máximo de pedidos alcançado. Por favor, tente novamente mais tarde.",
"error_rate_limit_title": "Limite de Taxa Excedido",
"expand_rows": "Expandir linhas",
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
"failed_to_load_organizations": "Falha ao carregar organizações",
"failed_to_load_workspaces": "Falha ao carregar projetos",
"filter": "Filtro",
"finish": "Concluir",
"first_name": "Primeiro nome",
"follow_these": "Siga estes",
"formbricks_version": "Versão do Formbricks",
"full_name": "Nome completo",
@@ -243,7 +236,6 @@
"hidden_field": "Campo oculto",
"hidden_fields": "Campos ocultos",
"hide_column": "Ocultar coluna",
"id": "ID",
"image": "Imagem",
"images": "Imagens",
"import": "Importar",
@@ -261,7 +253,6 @@
"key": "Chave",
"label": "Etiqueta",
"language": "Idioma",
"last_name": "Apelido",
"learn_more": "Saiba mais",
"license_expired": "License Expired",
"light_overlay": "Sobreposição leve",
@@ -284,11 +275,9 @@
"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!",
"months": "meses",
"move_down": "Mover para baixo",
"move_up": "Mover para cima",
"multiple_languages": "Várias línguas",
"my_product": "o meu produto",
"name": "Nome",
"new": "Novo",
"new_version_available": "Formbricks {version} está aqui. Atualize agora!",
@@ -404,7 +393,6 @@
"status": "Estado",
"step_by_step_manual": "Manual passo a passo",
"storage_not_configured": "Armazenamento de ficheiros não configurado, uploads provavelmente falharão",
"string": "Texto",
"styling": "Estilo",
"submit": "Submeter",
"summary": "Resumo",
@@ -437,7 +425,6 @@
"top_right": "Superior Direito",
"try_again": "Tente novamente",
"type": "Tipo",
"unknown_survey": "Inquérito desconhecido",
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
"update": "Atualizar",
"updated": "Atualizado",
@@ -461,7 +448,6 @@
"website_and_app_connection": "Ligação de Website e Aplicação",
"website_app_survey": "Inquérito do Website e da Aplicação",
"website_survey": "Inquérito do Website",
"weeks": "semanas",
"welcome_card": "Cartão de boas-vindas",
"workspace_configuration": "Configuração do projeto",
"workspace_created_successfully": "Projeto criado com sucesso",
@@ -472,7 +458,6 @@
"workspace_not_found": "Projeto não encontrado",
"workspace_permission_not_found": "Permissão do projeto não encontrada",
"workspaces": "Projetos",
"years": "anos",
"you": "Você",
"you_are_downgraded_to_the_community_edition": "Foi rebaixado para a Edição Comunitária.",
"you_are_not_authorized_to_perform_this_action": "Não está autorizado a realizar esta ação.",
@@ -644,45 +629,28 @@
"attribute_updated_successfully": "Atributo atualizado com sucesso",
"attribute_value": "Valor",
"attribute_value_placeholder": "Valor do atributo",
"attributes_msg_attribute_limit_exceeded": "Não foi possível criar {count} novo(s) atributo(s), pois excederia o limite máximo de {limit} classes de atributos. Os atributos existentes foram atualizados com sucesso.",
"attributes_msg_attribute_type_validation_error": "{error} (o atributo '{key}' tem dataType: {dataType})",
"attributes_msg_email_already_exists": "O email já existe para este ambiente e não foi atualizado.",
"attributes_msg_email_or_userid_required": "É necessário email ou userId. Os valores existentes foram preservados.",
"attributes_msg_new_attribute_created": "Criado novo atributo '{key}' com tipo '{dataType}'",
"attributes_msg_userid_already_exists": "O userId já existe para este ambiente e não foi atualizado.",
"contact_deleted_successfully": "Contacto eliminado com sucesso",
"contact_not_found": "Nenhum contacto encontrado",
"contacts_table_refresh": "Atualizar contactos",
"contacts_table_refresh_success": "Contactos atualizados com sucesso",
"create_attribute": "Criar atributo",
"create_key": "Criar chave",
"create_new_attribute": "Criar novo atributo",
"create_new_attribute_description": "Crie um novo atributo para fins de segmentação.",
"custom_attributes": "Atributos personalizados",
"data_type": "Tipo de dados",
"data_type_cannot_be_changed": "O tipo de dados não pode ser alterado após a criação",
"data_type_description": "Escolhe como este atributo deve ser armazenado e filtrado",
"date_value_required": "O valor da data é obrigatório. Usa o botão eliminar para remover este atributo se não quiseres definir uma data.",
"delete_attribute_confirmation": "{value, plural, one {Isto irá eliminar o atributo selecionado. Todos os dados de contacto associados a este atributo serão perdidos.} other {Isto irá eliminar os atributos selecionados. Todos os dados de contacto associados a estes atributos serão perdidos.}}",
"delete_contact_confirmation": "Isto irá eliminar todas as respostas das pesquisas e os atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isto irá eliminar todas as respostas das pesquisas e os atributos de contacto associados a este contacto. Qualquer segmentação e personalização baseados nos dados deste contacto serão perdidos. Se este contacto tiver respostas que contribuam para as quotas das pesquisas, as contagens de quotas serão reduzidas, mas os limites das quotas permanecerão inalterados.}}",
"displays": "Visualizações",
"edit_attribute": "Editar atributo",
"edit_attribute_description": "Atualize a etiqueta e a descrição deste atributo.",
"edit_attribute_values": "Editar atributos",
"edit_attribute_values_description": "Altere os valores de atributos específicos para este contacto.",
"edit_attributes": "Editar atributos",
"edit_attributes_success": "Atributos do contacto atualizados com sucesso",
"generate_personal_link": "Gerar Link Pessoal",
"generate_personal_link_description": "Selecione um inquérito publicado para gerar um link personalizado para este contacto.",
"invalid_csv_column_names": "Nome(s) de coluna CSV inválido(s): {columns}. Os nomes de colunas que se tornarão novos atributos devem conter apenas letras minúsculas, números e underscores, e devem começar com uma letra.",
"invalid_date_format": "Formato de data inválido. Por favor, usa uma data válida.",
"invalid_number_format": "Formato de número inválido. Por favor, introduz um número válido.",
"no_activity_yet": "Ainda sem atividade",
"no_published_link_surveys_available": "Não existem inquéritos de link publicados disponíveis. Por favor, publique primeiro um inquérito de link.",
"no_published_surveys": "Sem inquéritos publicados",
"no_responses_found": "Nenhuma resposta encontrada",
"not_provided": "Não fornecido",
"number_value_required": "O valor numérico é obrigatório. Usa o botão eliminar para remover este atributo.",
"personal_link_generated": "Link pessoal gerado com sucesso",
"personal_link_generated_but_clipboard_failed": "Link pessoal gerado mas falha ao copiar para a área de transferência: {url}",
"personal_survey_link": "Link do inquérito pessoal",
@@ -691,24 +659,13 @@
"search_contact": "Procurar contacto",
"select_a_survey": "Selecione um inquérito",
"select_attribute": "Selecionar Atributo",
"select_attribute_key": "Selecionar chave de atributo",
"survey_viewed": "Inquérito visualizado",
"survey_viewed_at": "Visualizado em",
"system_attributes": "Atributos do sistema",
"unlock_contacts_description": "Gerir contactos e enviar inquéritos direcionados",
"unlock_contacts_title": "Desbloqueie os contactos com um plano superior",
"upload_contacts_error_attribute_type_mismatch": "O atributo \"{key}\" está definido como \"{dataType}\", mas o CSV contém valores inválidos: {values}",
"upload_contacts_error_duplicate_mappings": "Foram encontrados mapeamentos duplicados para os seguintes atributos: {attributes}",
"upload_contacts_error_file_too_large": "O tamanho do ficheiro excede o limite máximo de 800KB",
"upload_contacts_error_generic": "Ocorreu um erro ao carregar os contactos. Por favor, tenta novamente mais tarde.",
"upload_contacts_error_invalid_file_type": "Por favor, carrega um ficheiro CSV",
"upload_contacts_error_no_valid_contacts": "O ficheiro CSV carregado não contém contactos válidos, por favor consulta o ficheiro CSV de exemplo para o formato correto.",
"upload_contacts_modal_attribute_header": "Atributo Formbricks",
"upload_contacts_modal_attributes_description": "Mapeie as colunas no seu CSV para os atributos no Formbricks.",
"upload_contacts_modal_attributes_new": "Novo atributo",
"upload_contacts_modal_attributes_search_or_add": "Pesquisar ou adicionar atributo",
"upload_contacts_modal_attributes_should_be_mapped_to": "deve ser mapeado para",
"upload_contacts_modal_attributes_title": "Atributos",
"upload_contacts_modal_csv_column_header": "Coluna CSV",
"upload_contacts_modal_description": "Carregue um ficheiro CSV para importar rapidamente contactos com atributos",
"upload_contacts_modal_download_example_csv": "Descarregar exemplo de CSV",
"upload_contacts_modal_duplicates_description": "Como devemos proceder se um contacto já existir nos seus contactos?",
@@ -765,12 +722,7 @@
"link_google_sheet": "Ligar Folha do Google",
"link_new_sheet": "Ligar nova Folha",
"no_integrations_yet": "As suas integrações com o Google Sheets aparecerão aqui assim que as adicionar. ⏲️",
"reconnect_button": "Reconectar",
"reconnect_button_description": "A tua ligação ao Google Sheets expirou. Por favor, reconecta para continuar a sincronizar respostas. As tuas ligações de folhas de cálculo e dados existentes serão preservados.",
"reconnect_button_tooltip": "Reconecta a integração para atualizar o teu acesso. As tuas ligações de folhas de cálculo e dados existentes serão preservados.",
"spreadsheet_permission_error": "Não tens permissão para aceder a esta folha de cálculo. Por favor, certifica-te de que a folha de cálculo está partilhada com a tua conta Google e que tens acesso de escrita à folha de cálculo.",
"spreadsheet_url": "URL da folha de cálculo",
"token_expired_error": "O token de atualização do Google Sheets expirou ou foi revogado. Por favor, reconecta a integração."
"spreadsheet_url": "URL da folha de cálculo"
},
"include_created_at": "Incluir Criado Em",
"include_hidden_fields": "Incluir Campos Ocultos",
@@ -894,40 +846,6 @@
"no_attributes_yet": "Ainda não há atributos!",
"no_filters_yet": "Ainda não há filtros!",
"no_segments_yet": "Atualmente, não tem segmentos guardados.",
"operator_contains": "contém",
"operator_does_not_contain": "não contém",
"operator_ends_with": "termina com",
"operator_is_after": "é depois",
"operator_is_before": "é antes",
"operator_is_between": "está entre",
"operator_is_newer_than": "é mais recente que",
"operator_is_not_set": "não está definido",
"operator_is_older_than": "é mais antigo que",
"operator_is_same_day": "é no mesmo dia",
"operator_is_set": "está definido",
"operator_starts_with": "começa com",
"operator_title_contains": "Contém",
"operator_title_does_not_contain": "Não contém",
"operator_title_ends_with": "Termina com",
"operator_title_equals": "Igual",
"operator_title_greater_equal": "Maior ou igual a",
"operator_title_greater_than": "Maior que",
"operator_title_is_after": "É depois",
"operator_title_is_before": "É antes",
"operator_title_is_between": "Está entre",
"operator_title_is_newer_than": "É mais recente que",
"operator_title_is_not_set": "Não está definido",
"operator_title_is_older_than": "É mais antigo que",
"operator_title_is_same_day": "É no mesmo dia",
"operator_title_is_set": "Está definido",
"operator_title_less_equal": "Menor ou igual a",
"operator_title_less_than": "Menor que",
"operator_title_not_equals": "Diferente de",
"operator_title_starts_with": "Começa com",
"operator_title_user_is_in": "O utilizador está em",
"operator_title_user_is_not_in": "O utilizador não está em",
"operator_user_is_in": "O utilizador está em",
"operator_user_is_not_in": "O utilizador não está em",
"person_and_attributes": "Pessoa e Atributos",
"phone": "Telefone",
"please_remove_the_segment_from_these_surveys_in_order_to_delete_it": "Por favor, remova o segmento destes questionários para o eliminar.",
@@ -952,7 +870,6 @@
"user_targeting_is_currently_only_available_when": "A segmentação de utilizadores está atualmente disponível apenas quando",
"value_cannot_be_empty": "O valor não pode estar vazio.",
"value_must_be_a_number": "O valor deve ser um número.",
"value_must_be_positive": "O valor deve ser um número positivo.",
"view_filters": "Ver filtros",
"where": "Onde",
"with_the_formbricks_sdk": "com o SDK Formbricks"
@@ -1085,7 +1002,7 @@
"email_customization_preview_email_heading": "Olá {userName}",
"email_customization_preview_email_text": "Esta é uma pré-visualização de email para mostrar qual logotipo será exibido nos emails.",
"error_deleting_organization_please_try_again": "Erro ao eliminar a organização. Por favor, tente novamente.",
"from_your_organization": "{memberName} da sua organização",
"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}",
@@ -1250,7 +1167,6 @@
"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": "Aplica-se apenas a inquéritos no produto.",
"add_logic": "Adicionar lógica",
"add_none_of_the_above": "Adicionar \"Nenhuma das Opções Acima\"",
"add_option": "Adicionar opção",
@@ -1449,6 +1365,7 @@
"follow_ups_modal_updated_successfull_toast": "Seguimento atualizado e será guardado assim que guardar o questionário.",
"follow_ups_new": "Novo acompanhamento",
"follow_ups_upgrade_button_text": "Atualize para ativar os acompanhamentos",
"form_styling": "Estilo do formulário",
"formbricks_sdk_is_not_connected": "O SDK do Formbricks não está conectado",
"four_points": "4 pontos",
"heading": "Cabeçalho",
@@ -1621,7 +1538,7 @@
"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.",
"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",
@@ -1667,7 +1584,6 @@
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
"survey_display_settings": "Configurações de Exibição do Inquérito",
"survey_placement": "Colocação do Inquérito",
"survey_styling": "Estilo do formulário",
"survey_trigger": "Desencadeador de Inquérito",
"switch_multi_language_on_to_get_started": "Ative o modo multilingue para começar 👉",
"target_block_not_found": "Bloco de destino não encontrado",
@@ -1758,6 +1674,7 @@
"welcome_message": "Mensagem de boas-vindas",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus utilizadores podem ser pesquisados.",
"you_have_not_created_a_segment_yet": "Ainda não criou um segmento",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Precisa de ter dois ou mais idiomas configurados no seu espaço de trabalho para trabalhar com traduções.",
"your_description_here_recall_information_with": "A sua descrição aqui. Recorde a informação com @",
"your_question_here_recall_information_with": "A sua pergunta aqui. Recorde a informação com @",
"your_web_app": "A sua aplicação web",
@@ -1965,7 +1882,6 @@
"filtered_responses_excel": "Respostas filtradas (Excel)",
"generating_qr_code": "A gerar código QR",
"impressions": "Impressões",
"impressions_identified_only": "A mostrar apenas impressões de contactos identificados",
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
"in_app": {
"connection_description": "O questionário será exibido aos utilizadores do seu website que correspondam aos critérios listados abaixo",
@@ -2008,7 +1924,6 @@
"last_quarter": "Último trimestre",
"last_year": "Ano passado",
"limit": "Limite",
"no_identified_impressions": "Sem impressões de contactos identificados",
"no_responses_found": "Nenhuma resposta encontrada",
"other_values_found": "Outros valores encontrados",
"overall": "Geral",
@@ -2173,12 +2088,12 @@
"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 mínima",
"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 mínima da entrada.",
"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.",
@@ -2187,8 +2102,6 @@
"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": "Cor do contorno",
"advanced_styling_field_option_border_description": "Contorna as opções de botões de rádio e caixas de seleçã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",
@@ -2243,7 +2156,6 @@
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo atualizado com sucesso",
"suggest_colors": "Sugerir cores",
"suggested_colors_applied_please_save": "Cores sugeridas geradas com sucesso. Pressiona \"Guardar\" para manter as alterações.",
"theme": "Tema",
"theme_settings_description": "Crie um tema de estilo para todos os inquéritos. Pode ativar estilos personalizados para cada inquérito."
},
@@ -3008,9 +2920,6 @@
"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_question_open_text_headline": "Mais alguma coisa que gostaria de partilhar?",
"preview_survey_question_open_text_placeholder": "Escreva a sua resposta aqui...",
"preview_survey_question_open_text_subheader": "O seu feedback ajuda-nos a melhorar.",
"preview_survey_welcome_card_headline": "Bem-vindo!",
"prioritize_features_description": "Identifique as funcionalidades que os seus utilizadores precisam mais e menos.",
"prioritize_features_name": "Priorizar Funcionalidades",

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