chore: merge epic/v5 into feedback records gateway

This commit is contained in:
Bhagya Amarasinghe
2026-04-30 01:22:24 +05:30
198 changed files with 8954 additions and 4098 deletions
+7 -7
View File
@@ -167,16 +167,16 @@ AZUREAD_TENANT_ID=
# Configure Formbricks AI at the instance level
# Set the provider used for AI features on this instance.
# Accepted values for AI_PROVIDER: aws, gcp, azure
# Accepted values for AI_PROVIDER: aws, google, azure
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching credentials below.
# AI_PROVIDER=gcp
# AI_PROVIDER=google
# AI_MODEL=gemini-2.5-flash
# Google Vertex AI credentials
# AI_GCP_PROJECT=
# AI_GCP_LOCATION=
# AI_GCP_CREDENTIALS_JSON=
# AI_GCP_APPLICATION_CREDENTIALS=
# Google Cloud credentials for Gemini models
# AI_GOOGLE_CLOUD_PROJECT=
# AI_GOOGLE_CLOUD_LOCATION=
# AI_GOOGLE_CLOUD_CREDENTIALS_JSON=
# AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS=
# Amazon Bedrock credentials
# AI_AWS_REGION=
@@ -60,8 +60,8 @@ const mockTemplate: TXMTemplate = {
],
styling: {
brandColor: { light: "#0000FF" },
questionColor: { light: "#00FF00" },
inputColor: { light: "#FF0000" },
elementHeadlineColor: { light: "#00FF00" },
inputBgColor: { light: "#FF0000" },
},
};
@@ -0,0 +1 @@
export { WorkspaceFeedbackSourcesPage as default } from "@/modules/workspaces/settings/sources/page";
@@ -23,9 +23,13 @@ import { createWorkspace } from "@/modules/workspaces/settings/lib/workspace";
import { getOrganizationsByUserId } from "./lib/organization";
import { getWorkspacesByUserId } from "./lib/workspace";
const ZCreateWorkspaceInput = ZWorkspaceUpdateInput.extend({
feedbackRecordDirectoryId: ZId.optional(),
});
const ZCreateWorkspaceAction = z.object({
organizationId: ZId,
data: ZWorkspaceUpdateInput,
data: ZCreateWorkspaceInput,
});
export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCreateWorkspaceAction).action(
@@ -40,7 +44,7 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
access: [
{
data: parsedInput.data,
schema: ZWorkspaceUpdateInput,
schema: ZCreateWorkspaceInput,
type: "organization",
roles: ["owner", "manager"],
},
@@ -117,7 +121,7 @@ const ZGetWorkspacesForSwitcherAction = z.object({
});
/**
* Fetches projects list for switcher dropdown.
* Fetches workspaces list for switcher dropdown.
* Called on-demand when user opens the workspace switcher.
*/
export const getWorkspacesForSwitcherAction = authenticatedActionClient
@@ -10,12 +10,12 @@ import {
Loader2,
LogOutIcon,
MessageCircle,
MessageSquareTextIcon,
PanelLeftCloseIcon,
PanelLeftOpenIcon,
PlusIcon,
RocketIcon,
SettingsIcon,
Shapes,
UserCircleIcon,
UserIcon,
} from "lucide-react";
@@ -146,58 +146,77 @@ export const MainNavigation = ({
}
}, [pathname]);
const mainNavigation = useMemo(
const mainNavigationSections = useMemo(
() => [
{
name: t("common.surveys"),
href: `/workspaces/${workspace.id}/surveys`,
icon: MessageCircle,
isActive: pathname?.includes("/surveys"),
isHidden: false,
disabled: isMembershipPending || isBilling,
},
{
href: `/workspaces/${workspace.id}/contacts`,
name: t("common.contacts"),
icon: UserIcon,
isActive:
pathname?.includes("/contacts") ||
pathname?.includes("/segments") ||
pathname?.includes("/attributes"),
disabled: isMembershipPending || isBilling,
},
{
name: t("common.analysis"),
href: `/workspaces/${workspace.id}/dashboards`,
icon: BarChart3Icon,
isActive: pathname?.includes("/dashboards") || pathname?.includes("/charts"),
isHidden: false,
disabled: isMembershipPending || isBilling,
id: "ask",
name: t("common.ask"),
items: [
{
name: t("common.surveys"),
href: `/workspaces/${workspace.id}/surveys`,
icon: MessageCircle,
isActive: pathname?.includes("/surveys"),
isHidden: false,
disabled: isMembershipPending || isBilling,
},
{
href: `/workspaces/${workspace.id}/contacts`,
name: t("common.contacts"),
icon: UserIcon,
isActive:
pathname?.includes("/contacts") ||
pathname?.includes("/segments") ||
pathname?.includes("/attributes"),
disabled: isMembershipPending || isBilling,
},
],
},
{
id: "unify-feedback",
name: t("workspace.unify.unify_feedback"),
href: `/workspaces/${workspace.id}/unify/sources`,
icon: Shapes,
isActive: pathname?.includes("/unify"),
},
{
name: t("common.configuration"),
href: `/workspaces/${workspace.id}/general`,
icon: Cog,
isActive:
pathname?.includes("/general") ||
pathname?.includes("/look") ||
pathname?.includes("/app-connection") ||
pathname?.includes("/integrations") ||
pathname?.includes("/teams") ||
pathname?.includes("/languages") ||
pathname?.includes("/tags"),
disabled: isMembershipPending || isBilling,
items: [
{
name: t("workspace.unify.feedback_records"),
href: `/workspaces/${workspace.id}/unify/feedback-records`,
icon: MessageSquareTextIcon,
isActive: pathname?.includes("/unify/feedback-records"),
isHidden: false,
disabled: isMembershipPending || isBilling,
},
{
name: t("common.dashboards"),
href: `/workspaces/${workspace.id}/dashboards`,
icon: BarChart3Icon,
isActive: pathname?.includes("/dashboards") || pathname?.includes("/charts"),
isHidden: false,
disabled: isMembershipPending || isBilling,
},
],
},
],
[t, workspace.id, pathname, isMembershipPending, isBilling]
);
const configurationNavigationItem = useMemo(
() => ({
name: t("common.configuration"),
href: `/workspaces/${workspace.id}/general`,
icon: Cog,
isActive:
pathname?.includes("/general") ||
pathname?.includes("/look") ||
pathname?.includes("/app-connection") ||
pathname?.includes("/feedback-sources") ||
pathname?.includes("/integrations") ||
pathname?.includes("/teams") ||
pathname?.includes("/languages") ||
pathname?.includes("/tags"),
disabled: isMembershipPending || isBilling,
}),
[t, workspace.id, pathname, isMembershipPending, isBilling]
);
const dropdownNavigation = [
{
label: t("common.account"),
@@ -256,6 +275,11 @@ export const MainNavigation = ({
label: t("common.website_and_app_connection"),
href: `/workspaces/${workspace.id}/app-connection`,
},
{
id: "feedback-sources",
label: t("workspace.unify.feedback_sources"),
href: `/workspaces/${workspace.id}/feedback-sources`,
},
{
id: "integrations",
label: t("common.integrations"),
@@ -552,23 +576,52 @@ export const MainNavigation = ({
</div>
{/* Main Nav Switch */}
<ul>
{mainNavigation.map(
(item) =>
!item.isHidden && (
<NavigationLink
key={item.name}
href={item.href}
isActive={item.isActive}
isCollapsed={isCollapsed}
isTextVisible={isTextVisible}
disabled={item.disabled}
disabledMessage={item.disabled ? disabledNavigationMessage : undefined}
linkText={item.name}>
<item.icon strokeWidth={1.5} />
</NavigationLink>
)
)}
<ul className="space-y-2">
{mainNavigationSections.map((section) => (
<li key={section.id}>
{!isCollapsed && !isTextVisible && (
<p className="px-4 pb-1 pt-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
{section.name}
</p>
)}
<ul>
{section.items.map(
(item) =>
!item.isHidden && (
<NavigationLink
key={item.name}
href={item.href}
isActive={item.isActive}
isCollapsed={isCollapsed}
isTextVisible={isTextVisible}
disabled={item.disabled}
disabledMessage={item.disabled ? disabledNavigationMessage : undefined}
linkText={item.name}>
<item.icon strokeWidth={1.5} />
</NavigationLink>
)
)}
</ul>
</li>
))}
<li className={cn("mt-2 border-t border-slate-100 pt-2", isCollapsed && "border-t-0 pt-0")}>
<ul>
<NavigationLink
href={configurationNavigationItem.href}
isActive={configurationNavigationItem.isActive}
isCollapsed={isCollapsed}
isTextVisible={isTextVisible}
disabled={configurationNavigationItem.disabled}
disabledMessage={
configurationNavigationItem.disabled ? disabledNavigationMessage : undefined
}
linkText={configurationNavigationItem.name}>
<configurationNavigationItem.icon strokeWidth={1.5} />
</NavigationLink>
</ul>
</li>
</ul>
</div>
@@ -118,6 +118,11 @@ export const WorkspaceBreadcrumb = ({
label: t("common.website_and_app_connection"),
href: `${workspaceBasePath}/app-connection`,
},
{
id: "feedback-sources",
label: t("workspace.unify.feedback_sources"),
href: `${workspaceBasePath}/feedback-sources`,
},
{
id: "integrations",
label: t("common.integrations"),
@@ -8,7 +8,7 @@ import type { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/type
import { Badge } from "@/modules/ui/components/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
type TPublicLicenseFeatureKey = Exclude<keyof TEnterpriseLicenseFeatures, "isMultiOrgEnabled" | "ai">;
type TPublicLicenseFeatureKey = Exclude<keyof TEnterpriseLicenseFeatures, "isMultiOrgEnabled">;
type TFeatureDefinition = {
key: TPublicLicenseFeatureKey;
@@ -61,6 +61,16 @@ const getFeatureDefinitions = (t: TFunction): TFeatureDefinition[] => {
labelKey: t("workspace.settings.enterprise.license_feature_spam_protection"),
docsUrl: "https://formbricks.com/docs/xm-and-surveys/surveys/general-features/spam-protection",
},
{
key: "aiSmartTools",
labelKey: t("workspace.settings.general.ai_smart_tools_enabled"),
docsUrl: "https://formbricks.com/docs/self-hosting/configuration/ai",
},
{
key: "aiDataAnalysis",
labelKey: t("workspace.settings.general.ai_data_analysis_enabled"),
docsUrl: "https://formbricks.com/docs/self-hosting/configuration/ai",
},
{
key: "auditLogs",
labelKey: t("workspace.settings.enterprise.license_feature_audit_logs"),
@@ -6,24 +6,32 @@ import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
import { updateOrganizationAISettingsAction } from "@/app/(app)/workspaces/[workspaceId]/settings/(organization)/general/actions";
import { getDisplayedOrganizationAISettingValue, getOrganizationAIEnablementState } from "@/lib/ai/utils";
import { getAccessFlags } from "@/lib/membership/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { type ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
interface AISettingsToggleProps {
organization: TOrganization;
membershipRole?: TOrganizationRole;
isInstanceAIConfigured: boolean;
hasAIPermission: boolean;
isFormbricksCloud: boolean;
}
export const AISettingsToggle = ({
organization,
membershipRole,
isInstanceAIConfigured,
hasAIPermission,
isFormbricksCloud,
}: Readonly<AISettingsToggleProps>) => {
const { workspace } = useWorkspace();
const workspaceBasePath = `/workspaces/${workspace?.id}`;
const [loadingField, setLoadingField] = useState<string | null>(null);
const { t } = useTranslation();
const router = useRouter();
@@ -78,6 +86,32 @@ export const AISettingsToggle = ({
}
};
const upgradeButtons: [ModalButton, ModalButton] = [
{
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: isFormbricksCloud
? `${workspaceBasePath}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: isFormbricksCloud
? `${workspaceBasePath}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
];
if (!hasAIPermission) {
return (
<UpgradePrompt
title={t("workspace.settings.general.unlock_ai_features_with_a_higher_plan")}
description={t("workspace.settings.general.unlock_ai_features_description")}
buttons={upgradeButtons}
feature="ai_features"
/>
);
}
return (
<div className="space-y-4">
{showInstanceConfigWarning && (
@@ -3,7 +3,12 @@ import { isInstanceAIConfigured } from "@/lib/ai/service";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
import {
getIsAIDataAnalysisEnabled,
getIsAISmartToolsEnabled,
getIsMultiOrgEnabled,
getWhiteLabelPermission,
} from "@/modules/ee/license-check/lib/utils";
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { IdBadge } from "@/modules/ui/components/id-badge";
@@ -27,8 +32,14 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
const user = session?.user?.id ? await getUser(session.user.id) : null;
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.id);
const [isMultiOrgEnabled, hasWhiteLabelPermission, hasAISmartToolsPermission, hasAIDataAnalysisPermission] =
await Promise.all([
getIsMultiOrgEnabled(),
getWhiteLabelPermission(organization.id),
getIsAISmartToolsEnabled(organization.id),
getIsAIDataAnalysisEnabled(organization.id),
]);
const hasAIPermission = hasAISmartToolsPermission || hasAIDataAnalysisPermission;
const isDeleteDisabled = !isOwner || !isMultiOrgEnabled;
const currentUserRole = currentUserMembership?.role;
@@ -64,6 +75,8 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
organization={organization}
membershipRole={currentUserMembership?.role}
isInstanceAIConfigured={isInstanceAIConfigured()}
hasAIPermission={hasAIPermission}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
/>
</SettingsCard>
<EmailCustomizationSettings
@@ -21,6 +21,7 @@ export const SettingsCard = ({
beta,
className,
buttonInfo,
cta,
}: {
title: string;
description: string;
@@ -30,6 +31,7 @@ export const SettingsCard = ({
beta?: boolean;
className?: string;
buttonInfo?: ButtonInfo;
cta?: React.ReactNode;
}) => {
const { t } = useTranslation();
return (
@@ -52,11 +54,12 @@ export const SettingsCard = ({
{description}
</Small>
</div>
{buttonInfo && (
<Button type="button" onClick={buttonInfo?.onClick} variant={buttonInfo?.variant ?? "default"}>
{buttonInfo?.text}
</Button>
)}
{cta ??
(buttonInfo && (
<Button type="button" onClick={buttonInfo?.onClick} variant={buttonInfo?.variant ?? "default"}>
{buttonInfo?.text}
</Button>
))}
</div>
<div className={cn(noPadding ? "" : "px-4 pt-4")}>{children}</div>
</div>
@@ -1,6 +1,7 @@
"use client";
import { useTranslation } from "react-i18next";
import { Badge } from "@/modules/ui/components/badge";
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
interface UnifyConfigNavigationProps {
@@ -17,15 +18,24 @@ export const UnifyConfigNavigation = ({
const { t } = useTranslation();
const baseHref = `/workspaces/${workspaceId}/unify`;
const activeId = activeIdProp ?? "sources";
const activeId = activeIdProp ?? "feedback-records";
const navigation = [
{ id: "sources", label: t("workspace.unify.sources"), href: `${baseHref}/sources` },
{
id: "feedback-records",
label: t("workspace.unify.feedback_records"),
href: `${baseHref}/feedback-records`,
},
{
id: "topics-subtopics",
label: (
<span className="inline-flex items-center gap-2">
{t("workspace.unify.topics_and_subtopics")}
<Badge text={t("common.soon")} type="gray" size="tiny" />
</span>
),
disabled: true,
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
@@ -0,0 +1,172 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { createFeedbackRecord, retrieveFeedbackRecord, updateFeedbackRecord } from "@/modules/hub/service";
import type { FeedbackRecordCreateParams, FeedbackRecordUpdateParams } from "@/modules/hub/types";
import {
TCreateFeedbackRecordAction,
TRetrieveFeedbackRecordAction,
TUpdateFeedbackRecordAction,
ZCreateFeedbackRecordAction,
ZRetrieveFeedbackRecordAction,
ZUpdateFeedbackRecordAction,
} from "./types";
const ensureAccess = async (
userId: string,
workspaceId: string,
minPermission: "read" | "readWrite"
): Promise<void> => {
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
await checkAuthorizationUpdated({
userId,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
minPermission,
workspaceId,
},
],
});
};
const getWorkspaceDirectoryIds = async (workspaceId: string): Promise<Set<string>> => {
const directories = await getFeedbackRecordDirectoriesByWorkspaceId(workspaceId);
return new Set(directories.map((directory) => directory.id));
};
const assertRecordBelongsToWorkspace = (directoryIds: Set<string>, tenantId: string): void => {
if (!directoryIds.has(tenantId)) {
// Throw a generic error indistinguishable from "not found" to prevent IDOR
throw new Error("Feedback record not found");
}
};
export const retrieveFeedbackRecordAction = authenticatedActionClient
.inputSchema(ZRetrieveFeedbackRecordAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: TRetrieveFeedbackRecordAction;
}) => {
const [, workspaceDirectoryIds] = await Promise.all([
ensureAccess(ctx.user.id, parsedInput.workspaceId, "read"),
getWorkspaceDirectoryIds(parsedInput.workspaceId),
]);
const recordResult = await retrieveFeedbackRecord(parsedInput.recordId);
if (!recordResult.data || recordResult.error) {
throw new Error("Feedback record not found");
}
assertRecordBelongsToWorkspace(workspaceDirectoryIds, recordResult.data.tenant_id);
return recordResult.data;
}
);
export const createFeedbackRecordAction = authenticatedActionClient
.inputSchema(ZCreateFeedbackRecordAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: TCreateFeedbackRecordAction;
}) => {
await ensureAccess(ctx.user.id, parsedInput.workspaceId, "readWrite");
const workspaceDirectoryIds = await getWorkspaceDirectoryIds(parsedInput.workspaceId);
assertRecordBelongsToWorkspace(workspaceDirectoryIds, parsedInput.recordInput.tenant_id);
const { recordInput } = parsedInput;
const createParams: FeedbackRecordCreateParams = {
submission_id: recordInput.submission_id,
tenant_id: recordInput.tenant_id,
source_type: recordInput.source_type,
field_id: recordInput.field_id,
field_type: recordInput.field_type,
collected_at: recordInput.collected_at,
source_id: recordInput.source_id,
source_name: recordInput.source_name,
field_label: recordInput.field_label,
field_group_id: recordInput.field_group_id,
field_group_label: recordInput.field_group_label,
value_text: recordInput.value_text,
value_number: recordInput.value_number,
value_boolean: recordInput.value_boolean,
value_date: recordInput.value_date,
metadata: recordInput.metadata,
language: recordInput.language,
user_identifier: recordInput.user_identifier,
};
const createResult = await createFeedbackRecord(createParams);
if (!createResult.data || createResult.error) {
throw new Error(createResult.error?.message || "Failed to create feedback record");
}
return createResult.data;
}
);
export const updateFeedbackRecordAction = authenticatedActionClient
.inputSchema(ZUpdateFeedbackRecordAction)
.action(
async ({
ctx,
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: TUpdateFeedbackRecordAction;
}) => {
const [, workspaceDirectoryIds] = await Promise.all([
ensureAccess(ctx.user.id, parsedInput.workspaceId, "readWrite"),
getWorkspaceDirectoryIds(parsedInput.workspaceId),
]);
const currentRecordResult = await retrieveFeedbackRecord(parsedInput.recordId);
if (!currentRecordResult.data || currentRecordResult.error) {
throw new Error("Feedback record not found");
}
assertRecordBelongsToWorkspace(workspaceDirectoryIds, currentRecordResult.data.tenant_id);
const { updateInput } = parsedInput;
const updateParams: FeedbackRecordUpdateParams = {
...(updateInput.value_text !== undefined && { value_text: updateInput.value_text ?? undefined }),
...(updateInput.value_number !== undefined && {
value_number: updateInput.value_number ?? undefined,
}),
...(updateInput.value_boolean !== undefined && {
value_boolean: updateInput.value_boolean ?? undefined,
}),
...(updateInput.value_date !== undefined && { value_date: updateInput.value_date ?? undefined }),
...(updateInput.language !== undefined && { language: updateInput.language ?? undefined }),
...(updateInput.metadata !== undefined && { metadata: updateInput.metadata }),
...(updateInput.user_identifier !== undefined && {
user_identifier: updateInput.user_identifier ?? undefined,
}),
};
const updateResult = await updateFeedbackRecord(parsedInput.recordId, updateParams);
if (!updateResult.data || updateResult.error) {
throw new Error(updateResult.error?.message || "Failed to update feedback record");
}
return updateResult.data;
}
);
@@ -0,0 +1,816 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
import { Button } from "@/modules/ui/components/button";
import {
FormControl,
FormError,
FormField,
FormItem,
FormLabel,
FormProvider,
} from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/modules/ui/components/sheet";
import { Switch } from "@/modules/ui/components/switch";
import {
createFeedbackRecordAction,
retrieveFeedbackRecordAction,
updateFeedbackRecordAction,
} from "../actions";
import {
FIELD_TYPE_OPTIONS,
SOURCE_TYPE_CUSTOM_VALUE,
SOURCE_TYPE_PRESET_OPTIONS,
type TFeedbackRecordFormValues,
ZFeedbackRecordFormValues,
} from "../lib/types";
import {
formatSourceType,
getCreateDefaults,
getReadOnlyMetadataEntries,
getValueFieldByType,
isPresetSourceType,
mapRecordToValues,
parseNumberValue,
toISOOrUndefined,
} from "../lib/utils";
import { type TFeedbackRecordUpdateInput } from "../types";
type FeedbackRecordDrawerMode = "create" | "edit";
interface FeedbackRecordFormDrawerProps {
mode: FeedbackRecordDrawerMode;
open: boolean;
onOpenChange: (open: boolean) => void;
workspaceId: string;
directories: { id: string; name: string }[];
canWrite: boolean;
recordId?: string;
onSuccess: () => Promise<void> | void;
}
export const FeedbackRecordFormDrawer = ({
mode,
open,
onOpenChange,
workspaceId,
directories,
canWrite,
recordId,
onSuccess,
}: Readonly<FeedbackRecordFormDrawerProps>) => {
const { t } = useTranslation();
const [record, setRecord] = useState<FeedbackRecordData | null>(null);
const [isLoadingRecord, setIsLoadingRecord] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
const defaultValues = useMemo(() => getCreateDefaults(directories), [directories]);
const form = useForm<TFeedbackRecordFormValues>({
resolver: zodResolver(ZFeedbackRecordFormValues),
defaultValues,
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "metadataEntries",
});
const fieldType = form.watch("field_type");
const selectedValueField = getValueFieldByType(fieldType);
const isEditMode = mode === "edit";
const isReadOnly = isEditMode && !canWrite;
const [sourceTypeMode, setSourceTypeMode] = useState<string>("survey");
const [customSourceType, setCustomSourceType] = useState("");
const readOnlyMetadataEntries = useMemo(() => (record ? getReadOnlyMetadataEntries(record) : []), [record]);
const resetForCreate = useCallback(() => {
const nextDefaults = getCreateDefaults(directories);
form.reset(nextDefaults);
setRecord(null);
setSourceTypeMode(nextDefaults.source_type);
setCustomSourceType("");
}, [directories, form]);
useEffect(() => {
if (!open) return;
if (mode === "create") {
resetForCreate();
return;
}
if (!recordId) return;
const loadRecord = async () => {
setIsLoadingRecord(true);
const result = await retrieveFeedbackRecordAction({ workspaceId, recordId });
if (!result?.data) {
toast.error(getFormattedErrorMessage(result) || t("workspace.unify.failed_to_load_feedback_records"));
setIsLoadingRecord(false);
onOpenChange(false);
return;
}
setRecord(result.data);
form.reset(mapRecordToValues(result.data));
const isPreset = isPresetSourceType(result.data.source_type);
setSourceTypeMode(isPreset ? result.data.source_type : SOURCE_TYPE_CUSTOM_VALUE);
setCustomSourceType(isPreset ? "" : result.data.source_type);
setIsLoadingRecord(false);
};
void loadRecord();
}, [form, mode, onOpenChange, open, recordId, resetForCreate, t, workspaceId]);
const requestClose = useCallback(() => {
if (form.formState.isDirty && !isSubmitting) {
setIsDiscardDialogOpen(true);
return;
}
onOpenChange(false);
}, [form.formState.isDirty, isSubmitting, onOpenChange]);
const handleDrawerOpenChange = useCallback(
(nextOpen: boolean) => {
if (nextOpen) {
onOpenChange(true);
return;
}
requestClose();
},
[onOpenChange, requestClose]
);
const handleDiscardChanges = () => {
setIsDiscardDialogOpen(false);
onOpenChange(false);
};
const setStrictValueValidationError = (message: string) => {
form.setError(selectedValueField, { type: "manual", message });
};
const isCreateValueFieldValid = (values: TFeedbackRecordFormValues): boolean => {
if (selectedValueField === "value_text") return Boolean(values.value_text?.trim());
if (selectedValueField === "value_number") return parseNumberValue(values.value_number ?? "") != null;
if (selectedValueField === "value_boolean") return values.value_boolean !== undefined;
if (selectedValueField === "value_date") return Boolean(toISOOrUndefined(values.value_date));
return true;
};
const buildMetadataMap = (values: TFeedbackRecordFormValues): Record<string, string> =>
Object.fromEntries(
values.metadataEntries
.map((entry) => ({ key: entry.key.trim(), value: entry.value }))
.filter((entry) => entry.key.length > 0)
.map((entry) => [entry.key, entry.value])
);
const buildCreateValueFields = (values: TFeedbackRecordFormValues) => ({
value_text: selectedValueField === "value_text" ? (values.value_text ?? "") : null,
value_number:
selectedValueField === "value_number"
? (parseNumberValue(values.value_number ?? "") ?? undefined)
: undefined,
value_boolean: selectedValueField === "value_boolean" ? values.value_boolean : undefined,
value_date: selectedValueField === "value_date" ? toISOOrUndefined(values.value_date) : undefined,
});
const getUpdateValueField = (
values: TFeedbackRecordFormValues
): Pick<TFeedbackRecordUpdateInput, "value_text" | "value_number" | "value_boolean" | "value_date"> => {
if (selectedValueField === "value_text") return { value_text: values.value_text?.trim() ?? "" };
if (selectedValueField === "value_number") {
return { value_number: parseNumberValue(values.value_number ?? "") };
}
if (selectedValueField === "value_boolean") return { value_boolean: values.value_boolean ?? null };
if (selectedValueField === "value_date") {
return { value_date: toISOOrUndefined(values.value_date) ?? null };
}
return {};
};
const submitCreate = async (
values: TFeedbackRecordFormValues,
metadata: Record<string, string>
): Promise<boolean> => {
const sourceTypeValue =
sourceTypeMode === SOURCE_TYPE_CUSTOM_VALUE ? customSourceType.trim() : values.source_type;
const result = await createFeedbackRecordAction({
workspaceId,
recordInput: {
submission_id: values.submission_id.trim(),
tenant_id: values.tenant_id,
source_type: sourceTypeValue,
source_id: values.source_id?.trim() ? values.source_id.trim() : null,
source_name: values.source_name?.trim() ? values.source_name.trim() : null,
field_id: values.field_id.trim(),
field_label: values.field_label?.trim() ? values.field_label.trim() : null,
field_type: values.field_type,
field_group_id: values.field_group_id?.trim() || undefined,
field_group_label: values.field_group_label?.trim() ? values.field_group_label.trim() : null,
collected_at: toISOOrUndefined(values.collected_at),
...buildCreateValueFields(values),
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
language: values.language?.trim() || undefined,
user_identifier: values.user_identifier?.trim() || undefined,
},
});
if (!result?.data) {
toast.error(getFormattedErrorMessage(result));
return false;
}
return true;
};
const submitUpdate = async (
values: TFeedbackRecordFormValues,
metadata: Record<string, string>
): Promise<boolean> => {
if (!recordId) return false;
const preservedMetadata = Object.fromEntries(
Object.entries(record?.metadata ?? {}).filter(([, value]) => typeof value !== "string")
);
const result = await updateFeedbackRecordAction({
workspaceId,
recordId,
updateInput: {
language: values.language?.trim() || null,
user_identifier: values.user_identifier?.trim() || null,
metadata: { ...preservedMetadata, ...metadata },
...getUpdateValueField(values),
},
});
if (!result?.data) {
toast.error(getFormattedErrorMessage(result));
return false;
}
return true;
};
const handleSubmit = form.handleSubmit(async (values) => {
form.clearErrors();
if (mode === "create" && !isCreateValueFieldValid(values)) {
setStrictValueValidationError(t("workspace.unify.feedback_record_value_required"));
return;
}
const metadata = buildMetadataMap(values);
setIsSubmitting(true);
try {
const ok =
mode === "create" ? await submitCreate(values, metadata) : await submitUpdate(values, metadata);
if (!ok) return;
toast.success(
mode === "create"
? t("workspace.unify.feedback_record_created_successfully")
: t("workspace.unify.feedback_record_updated_successfully")
);
await onSuccess();
onOpenChange(false);
} finally {
setIsSubmitting(false);
}
});
const drawerTitle =
mode === "create"
? t("workspace.unify.add_feedback_record")
: t("workspace.unify.feedback_record_details");
const drawerDescription =
mode === "create"
? t("workspace.unify.add_feedback_record_description")
: t("workspace.unify.feedback_record_details_description");
const valueBooleanStatus = form.watch("value_boolean");
let valueBooleanLabel = t("common.not_set");
if (valueBooleanStatus === true) {
valueBooleanLabel = t("common.yes");
} else if (valueBooleanStatus === false) {
valueBooleanLabel = t("common.no");
}
return (
<>
<Sheet open={open} onOpenChange={handleDrawerOpenChange}>
<SheetContent className="w-full overflow-y-auto bg-white px-5 sm:max-w-2xl">
<SheetHeader>
<SheetTitle>{drawerTitle}</SheetTitle>
<SheetDescription>{drawerDescription}</SheetDescription>
</SheetHeader>
{isLoadingRecord ? (
<div className="py-8 text-sm text-slate-500">{t("common.loading")}</div>
) : (
<FormProvider {...form}>
<form className="space-y-4 py-4" onSubmit={handleSubmit}>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("common.id")}</FormLabel>
<FormControl>
<Input {...field} disabled placeholder={t("workspace.unify.auto_generated")} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="tenant_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.feedback_record_directory")}</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange} disabled>
<SelectTrigger>
<SelectValue
placeholder={t("workspace.unify.select_feedback_record_directory")}
/>
</SelectTrigger>
<SelectContent>
{directories.map((directory) => (
<SelectItem key={directory.id} value={directory.id}>
{directory.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormError />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="submission_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.submission_id")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
<FormError />
</FormItem>
)}
/>
<FormField
control={form.control}
name="collected_at"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.collected_at")}</FormLabel>
<FormControl>
<Input {...field} type="datetime-local" disabled={isEditMode || !canWrite} />
</FormControl>
<FormError />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="created_at"
render={({ field }) => (
<FormItem>
<FormLabel>{t("common.created_at")}</FormLabel>
<FormControl>
<Input {...field} disabled placeholder={t("workspace.unify.auto_generated")} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="updated_at"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.updated_at")}</FormLabel>
<FormControl>
<Input {...field} disabled placeholder={t("workspace.unify.auto_generated")} />
</FormControl>
</FormItem>
)}
/>
</div>
{isEditMode ? (
<FormField
control={form.control}
name="source_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.source_type")}</FormLabel>
<FormControl>
<Input {...field} value={formatSourceType(field.value, t)} disabled />
</FormControl>
</FormItem>
)}
/>
) : (
<div className="space-y-2">
<FormLabel>{t("workspace.unify.source_type")}</FormLabel>
<Select
value={sourceTypeMode}
onValueChange={(value) => {
setSourceTypeMode(value);
if (value !== SOURCE_TYPE_CUSTOM_VALUE) {
form.setValue("source_type", value, { shouldDirty: true });
}
}}
disabled={!canWrite}>
<SelectTrigger>
<SelectValue placeholder={t("workspace.unify.select_feedback_record_source_type")} />
</SelectTrigger>
<SelectContent>
{SOURCE_TYPE_PRESET_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{formatSourceType(option, t)}
</SelectItem>
))}
<SelectItem value={SOURCE_TYPE_CUSTOM_VALUE}>
{t("workspace.unify.custom_source_type")}
</SelectItem>
</SelectContent>
</Select>
{sourceTypeMode === SOURCE_TYPE_CUSTOM_VALUE && (
<Input
value={customSourceType}
onChange={(event) => {
setCustomSourceType(event.target.value);
form.setValue("source_type", event.target.value, { shouldDirty: true });
}}
placeholder={t("workspace.unify.custom_source_type_placeholder")}
disabled={!canWrite}
/>
)}
<FormError>{form.formState.errors.source_type?.message}</FormError>
</div>
)}
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="source_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.source_id")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="source_name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.source_name")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="field_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.field_id")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
<FormError />
</FormItem>
)}
/>
<FormField
control={form.control}
name="field_label"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.field_label")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="field_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.field_type")}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) =>
field.onChange(value as TFeedbackRecordFormValues["field_type"])
}
disabled={isEditMode || !canWrite}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FIELD_TYPE_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="field_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.field_group_id")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="field_group_label"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.field_group_label")}</FormLabel>
<FormControl>
<Input {...field} disabled={isEditMode || !canWrite} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="value_text"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.value_text")}</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
disabled={selectedValueField !== "value_text" || isReadOnly || !canWrite}
/>
</FormControl>
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="value_number"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.value_number")}</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
type="number"
step="any"
disabled={selectedValueField !== "value_number" || isReadOnly || !canWrite}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="value_date"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.value_date")}</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
type="datetime-local"
disabled={selectedValueField !== "value_date" || isReadOnly || !canWrite}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="value_boolean"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.value_boolean")}</FormLabel>
<FormControl>
<div className="flex items-center gap-3 rounded-md border border-slate-200 px-3 py-2">
<Switch
checked={field.value ?? false}
onCheckedChange={(checked) => field.onChange(checked)}
disabled={selectedValueField !== "value_boolean" || isReadOnly || !canWrite}
/>
<span className="text-sm text-slate-600">{valueBooleanLabel}</span>
</div>
</FormControl>
</FormItem>
)}
/>
<FormError>{form.formState.errors[selectedValueField]?.message}</FormError>
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>{t("common.language")}</FormLabel>
<FormControl>
<Input {...field} disabled={!canWrite || isReadOnly} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_identifier"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.user_identifier")}</FormLabel>
<FormControl>
<Input {...field} disabled={!canWrite || isReadOnly} />
</FormControl>
</FormItem>
)}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel>{t("workspace.unify.metadata")}</FormLabel>
{canWrite && !isReadOnly && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ key: "", value: "" })}>
<PlusIcon className="h-4 w-4" />
{t("common.add")}
</Button>
)}
</div>
<div className="space-y-2">
{fields.map((field, index) => (
<div key={field.id} className="grid grid-cols-[1fr_1fr_auto] gap-2">
<FormField
control={form.control}
name={`metadataEntries.${index}.key`}
render={({ field: entryField }) => (
<FormItem>
<FormControl>
<Input
{...entryField}
placeholder={t("workspace.unify.metadata_key")}
disabled={isReadOnly || !canWrite}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={`metadataEntries.${index}.value`}
render={({ field: entryField }) => (
<FormItem>
<FormControl>
<Input
{...entryField}
placeholder={t("workspace.unify.metadata_value")}
disabled={isReadOnly || !canWrite}
/>
</FormControl>
</FormItem>
)}
/>
{canWrite && !isReadOnly && (
<Button type="button" variant="outline" onClick={() => remove(index)}>
{t("common.delete")}
</Button>
)}
</div>
))}
</div>
{readOnlyMetadataEntries.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-slate-500">
{t("workspace.unify.metadata_read_only_entries")}
</p>
{readOnlyMetadataEntries.map((entry) => (
<div
key={entry.key}
className="grid grid-cols-2 gap-2 rounded-md bg-slate-50 p-2 text-xs">
<span className="font-medium text-slate-700">{entry.key}</span>
<span className="truncate text-slate-600" title={entry.value}>
{entry.value}
</span>
</div>
))}
</div>
)}
</div>
</form>
</FormProvider>
)}
<SheetFooter className="mt-2">
<Button variant="outline" onClick={requestClose} disabled={isSubmitting}>
{t("common.cancel")}
</Button>
{canWrite && (
<Button onClick={handleSubmit} loading={isSubmitting} disabled={isLoadingRecord}>
{mode === "create" ? t("workspace.unify.add_feedback_record") : t("common.save")}
</Button>
)}
</SheetFooter>
</SheetContent>
</Sheet>
<AlertDialog
open={isDiscardDialogOpen}
setOpen={setIsDiscardDialogOpen}
headerText={t("workspace.unify.discard_feedback_record_changes_title")}
mainText={t("workspace.unify.discard_feedback_record_changes_description")}
confirmBtnLabel={t("common.discard")}
declineBtnLabel={t("common.cancel")}
declineBtnVariant="outline"
onDecline={() => setIsDiscardDialogOpen(false)}
onConfirm={handleDiscardChanges}
/>
</>
);
};
@@ -4,38 +4,38 @@ import { useTranslation } from "react-i18next";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UnifyConfigNavigation } from "../components/UnifyConfigNavigation";
import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation";
import { FeedbackRecordsTable } from "./feedback-records-table";
interface FeedbackRecordsPageClientProps {
workspaceId: string;
directories: { id: string; name: string }[];
initialFrdId: string | null;
initialRecords: FeedbackRecordData[];
initialNextCursor?: string;
frdMap: Record<string, string>;
csvSources: { id: string; name: string }[];
canWrite: boolean;
}
export function FeedbackRecordsPageClient({
workspaceId,
directories,
initialFrdId,
initialRecords,
initialNextCursor,
}: FeedbackRecordsPageClientProps) {
frdMap,
csvSources,
canWrite,
}: Readonly<FeedbackRecordsPageClientProps>) {
const { t } = useTranslation();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("workspace.unify.unify_feedback")}>
<PageHeader pageTitle={t("workspace.unify.feedback_records")}>
<UnifyConfigNavigation workspaceId={workspaceId} activeId="feedback-records" />
</PageHeader>
<FeedbackRecordsTable
workspaceId={workspaceId}
directories={directories}
initialFrdId={initialFrdId}
initialRecords={initialRecords}
initialNextCursor={initialNextCursor}
frdMap={frdMap}
csvSources={csvSources}
canWrite={canWrite}
/>
</PageContentWrapper>
);
@@ -0,0 +1,373 @@
"use client";
import { TFunction } from "i18next";
import {
CalendarIcon,
ChevronDownIcon,
HashIcon,
MessageSquareTextIcon,
PlusIcon,
RefreshCwIcon,
ToggleLeftIcon,
TypeIcon,
} from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { listFeedbackRecordsAction } from "@/lib/connector/actions";
import { formatDateForDisplay, formatDateTimeForDisplay } from "@/lib/utils/datetime";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { CsvImportModal } from "../../sources/components/csv-import-modal";
import { formatSourceType } from "../lib/utils";
import { FeedbackRecordFormDrawer } from "./feedback-record-form-drawer";
const RECORDS_PER_PAGE = 50;
const FIELD_TYPE_ICONS: Record<string, React.ReactNode> = {
text: <TypeIcon className="h-3.5 w-3.5" />,
categorical: <HashIcon className="h-3.5 w-3.5" />,
nps: <HashIcon className="h-3.5 w-3.5" />,
csat: <HashIcon className="h-3.5 w-3.5" />,
ces: <HashIcon className="h-3.5 w-3.5" />,
rating: <HashIcon className="h-3.5 w-3.5" />,
number: <HashIcon className="h-3.5 w-3.5" />,
boolean: <ToggleLeftIcon className="h-3.5 w-3.5" />,
date: <CalendarIcon className="h-3.5 w-3.5" />,
};
const formatValue = (record: FeedbackRecordData, t: TFunction, locale: string): string => {
if (record.value_text != null) return record.value_text;
if (record.value_number != null) return String(record.value_number);
if (record.value_boolean != null) return record.value_boolean ? t("common.yes") : t("common.no");
if (record.value_date != null) return formatDateForDisplay(new Date(record.value_date), locale);
return "—";
};
function truncate(str: string, maxLen: number): string {
if (str.length <= maxLen) return str;
return str.slice(0, maxLen) + "…";
}
interface FeedbackRecordsTableProps {
workspaceId: string;
initialRecords: FeedbackRecordData[];
frdMap: Record<string, string>;
csvSources: { id: string; name: string }[];
canWrite: boolean;
}
export const FeedbackRecordsTable = ({
workspaceId,
initialRecords,
frdMap,
csvSources,
canWrite,
}: Readonly<FeedbackRecordsTableProps>) => {
const { t, i18n } = useTranslation();
const [records, setRecords] = useState<FeedbackRecordData[]>(initialRecords);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [drawerMode, setDrawerMode] = useState<"create" | "edit">("edit");
const [drawerRecordId, setDrawerRecordId] = useState<string | undefined>();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [csvImportSource, setCsvImportSource] = useState<{ id: string; name: string } | null>(null);
const directories = useMemo(
() =>
Object.entries(frdMap)
.map(([id, name]) => ({ id, name }))
.sort((a, b) => a.name.localeCompare(b.name)),
[frdMap]
);
const handleRefresh = async () => {
if (isRefreshing) return;
setIsRefreshing(true);
setError(null);
const toastId = toast.loading(t("workspace.unify.refreshing_feedback_records"));
const directoryIds = Object.keys(frdMap);
const results = await Promise.all(
directoryIds.map((frdId) =>
listFeedbackRecordsAction({
workspaceId,
frdId,
limit: RECORDS_PER_PAGE,
})
)
);
if (results.some((result) => !result?.data)) {
const firstErrorResult = results.find((result) => !result?.data);
const errorMessage = firstErrorResult ? getFormattedErrorMessage(firstErrorResult) : undefined;
toast.error(errorMessage ?? t("workspace.unify.failed_to_load_feedback_records"), {
id: toastId,
});
setIsRefreshing(false);
return;
}
const successfulRecords = results.flatMap((result) => result?.data?.data ?? []);
const mergedRecords = successfulRecords
.toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1))
.slice(0, RECORDS_PER_PAGE);
setRecords(mergedRecords);
setIsRefreshing(false);
toast.success(t("workspace.unify.feedback_records_refreshed"), { id: toastId });
};
if (error) {
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex h-48 flex-col items-center justify-center gap-3 px-4 text-center">
<MessageSquareTextIcon className="h-8 w-8 text-slate-400" />
<p className="text-sm text-slate-500">{error}</p>
<Button variant="secondary" size="sm" onClick={handleRefresh}>
{t("common.retry")}
</Button>
</div>
</div>
);
}
const isEmpty = records.length === 0 && !isRefreshing;
const openEditDrawer = (recordId: string) => {
setDrawerMode("edit");
setDrawerRecordId(recordId);
setIsDrawerOpen(true);
};
const openCreateDrawer = () => {
setDrawerMode("create");
setDrawerRecordId(undefined);
setIsDrawerOpen(true);
};
const hasCsvSources = csvSources.length > 0;
return (
<>
<div className="space-y-3">
<div className="flex items-center justify-between">
{isEmpty ? (
<span />
) : (
<p className="text-sm text-slate-500">
{t("workspace.unify.showing_count_loaded", {
count: records.length,
})}
</p>
)}
<div className="flex items-center gap-2">
{canWrite &&
(hasCsvSources ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="secondary">
<PlusIcon className="h-4 w-4" />
{t("workspace.unify.add_feedback_record")}
<ChevronDownIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={openCreateDrawer}>
{t("workspace.unify.add_feedback_record")}
</DropdownMenuItem>
<DropdownMenuSeparator />
{csvSources.map((source) => (
<DropdownMenuItem
key={source.id}
onClick={() => {
setCsvImportSource(source);
}}>
{t("workspace.unify.import_via_source_name", { sourceName: source.name })}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button size="sm" variant="secondary" onClick={openCreateDrawer}>
<PlusIcon className="h-4 w-4" />
{t("workspace.unify.add_feedback_record")}
</Button>
))}
<Button size="sm" asChild>
<Link href={`/workspaces/${workspaceId}/feedback-sources`}>
{t("workspace.unify.manage_feedback_sources")}
</Link>
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={t("workspace.unify.refresh_feedback_records")}>
<RefreshCwIcon className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
</div>
</div>
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="overflow-x-auto">
<table className="w-full min-w-[900px]">
<thead>
<tr className="border-b border-slate-200 text-left text-sm text-slate-900 [&>th]:font-semibold">
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.collected_at")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_name")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_label")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.value")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.user_identifier")}</th>
</tr>
</thead>
{isEmpty ? (
<tbody>
<tr>
<td colSpan={7}>
<div className="flex h-32 items-center justify-center">
<p className="text-sm text-slate-500">{t("workspace.unify.no_feedback_records")}</p>
</div>
</td>
</tr>
</tbody>
) : (
<tbody className="divide-y divide-slate-100">
{records.map((record) => (
<FeedbackRecordRow
key={record.id}
record={record}
workspaceId={workspaceId}
locale={i18n.resolvedLanguage ?? i18n.language ?? "en-US"}
t={t}
onClick={() => openEditDrawer(record.id)}
/>
))}
</tbody>
)}
</table>
</div>
</div>
</div>
<FeedbackRecordFormDrawer
mode={drawerMode}
open={isDrawerOpen}
onOpenChange={setIsDrawerOpen}
workspaceId={workspaceId}
directories={directories}
canWrite={canWrite}
recordId={drawerMode === "edit" ? drawerRecordId : undefined}
onSuccess={handleRefresh}
/>
{csvImportSource && (
<CsvImportModal
open={csvImportSource !== null}
onOpenChange={(open) => {
if (!open) {
setCsvImportSource(null);
}
}}
connectorId={csvImportSource.id}
workspaceId={workspaceId}
/>
)}
</>
);
};
const FeedbackRecordRow = ({
record,
workspaceId,
locale,
t,
onClick,
}: {
record: FeedbackRecordData;
workspaceId: string;
locale: string;
t: TFunction;
onClick: () => void;
}) => {
const value = formatValue(record, t, locale);
const isLongValue = value.length > 60;
const isFormbricksSurveySource =
(record.source_type === "formbricks" || record.source_type === "formbricks_survey") && !!record.source_id;
const surveySummaryHref = `/workspaces/${workspaceId}/surveys/${record.source_id}/summary`;
return (
<tr
className="cursor-pointer text-sm text-slate-700 transition-colors focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400"
tabIndex={0}
role="button"
aria-label={record.field_label ?? record.field_id}
onClick={onClick}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onClick();
}
}}>
<td className="whitespace-nowrap px-4 py-3 text-slate-500">
{formatDateTimeForDisplay(new Date(record.collected_at), locale)}
</td>
<td className="whitespace-nowrap px-4 py-3">
<Badge text={formatSourceType(record.source_type, t)} type="gray" size="tiny" />
</td>
<td className="max-w-[150px] truncate px-4 py-3" title={record.source_name ?? undefined}>
{isFormbricksSurveySource ? (
<Link
href={surveySummaryHref}
className="text-slate-700 underline underline-offset-2 hover:text-slate-900"
onClick={(event) => event.stopPropagation()}>
{record.source_name ?? "—"}
</Link>
) : (
<span>{record.source_name ?? "—"}</span>
)}
</td>
<td className="max-w-[200px] truncate px-4 py-3" title={record.field_label ?? undefined}>
{record.field_label ?? record.field_id}
</td>
<td className="whitespace-nowrap px-4 py-3">
<span className="inline-flex items-center gap-1 text-slate-600">
{FIELD_TYPE_ICONS[record.field_type] ?? <HashIcon className="h-3.5 w-3.5" />}
{record.field_type}
</span>
</td>
<td className="max-w-[250px] px-4 py-3">
{isLongValue ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default truncate">{truncate(value, 60)}</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm whitespace-pre-wrap">
{value}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span>{value}</span>
)}
</td>
<td className="max-w-[120px] truncate px-4 py-3 text-slate-500" title={record.user_identifier}>
{record.user_identifier ?? "—"}
</td>
</tr>
);
};
@@ -1,303 +0,0 @@
"use client";
import { TFunction } from "i18next";
import {
CalendarIcon,
HashIcon,
MessageSquareTextIcon,
RefreshCwIcon,
ToggleLeftIcon,
TypeIcon,
} from "lucide-react";
import { useCallback, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { listFeedbackRecordsAction } from "@/lib/connector/actions";
import { formatDateForDisplay, formatDateTimeForDisplay } from "@/lib/utils/datetime";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
const RECORDS_PER_PAGE = 50;
const FIELD_TYPE_ICONS: Record<string, React.ReactNode> = {
text: <TypeIcon className="h-3.5 w-3.5" />,
categorical: <HashIcon className="h-3.5 w-3.5" />,
nps: <HashIcon className="h-3.5 w-3.5" />,
csat: <HashIcon className="h-3.5 w-3.5" />,
ces: <HashIcon className="h-3.5 w-3.5" />,
rating: <HashIcon className="h-3.5 w-3.5" />,
number: <HashIcon className="h-3.5 w-3.5" />,
boolean: <ToggleLeftIcon className="h-3.5 w-3.5" />,
date: <CalendarIcon className="h-3.5 w-3.5" />,
};
const formatValue = (record: FeedbackRecordData, t: TFunction, locale: string): string => {
if (record.value_text != null) return record.value_text;
if (record.value_number != null) return String(record.value_number);
if (record.value_boolean != null) return record.value_boolean ? t("common.yes") : t("common.no");
if (record.value_date != null) return formatDateForDisplay(new Date(record.value_date), locale);
return "—";
};
function truncate(str: string, maxLen: number): string {
if (str.length <= maxLen) return str;
return str.slice(0, maxLen) + "…";
}
interface FeedbackRecordsTableProps {
workspaceId: string;
directories: { id: string; name: string }[];
initialFrdId: string | null;
initialRecords: FeedbackRecordData[];
initialNextCursor?: string;
}
export const FeedbackRecordsTable = ({
workspaceId,
directories,
initialFrdId,
initialRecords,
initialNextCursor,
}: FeedbackRecordsTableProps) => {
const { t, i18n } = useTranslation();
const locale = i18n.resolvedLanguage ?? i18n.language ?? "en-US";
const [selectedFrdId, setSelectedFrdId] = useState<string | null>(initialFrdId);
const [records, setRecords] = useState<FeedbackRecordData[]>(initialRecords);
const [nextCursor, setNextCursor] = useState<string | undefined>(initialNextCursor);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchRecords = useCallback(
async (frdId: string, cursor: string | undefined, append: boolean): Promise<string | null> => {
const setLoading = append ? setIsLoadingMore : setIsRefreshing;
setLoading(true);
setError(null);
const result = await listFeedbackRecordsAction({
workspaceId,
frdId,
limit: RECORDS_PER_PAGE,
cursor,
});
if (!result?.data) {
const message =
getFormattedErrorMessage(result) ?? t("workspace.unify.failed_to_load_feedback_records");
setError(message);
setLoading(false);
return message;
}
const response = result.data;
setRecords((prev) => (append ? [...prev, ...response.data] : response.data));
setNextCursor(response.next_cursor);
setLoading(false);
return null;
},
[workspaceId, t]
);
const handleFrdChange = (frdId: string) => {
setSelectedFrdId(frdId);
setRecords([]);
setNextCursor(undefined);
fetchRecords(frdId, undefined, false);
};
const handleLoadMore = () => {
if (!selectedFrdId) return;
fetchRecords(selectedFrdId, nextCursor, true);
};
const handleRefresh = async () => {
if (!selectedFrdId || isRefreshing) return;
const toastId = toast.loading(t("workspace.unify.refreshing_feedback_records"));
const errorMessage = await fetchRecords(selectedFrdId, undefined, false);
if (errorMessage) {
toast.error(errorMessage, { id: toastId });
return;
}
toast.success(t("workspace.unify.feedback_records_refreshed"), { id: toastId });
};
const hasMore = !!nextCursor;
const isEmpty = records.length === 0 && !isRefreshing;
const currentFrdName = directories.find((d) => d.id === selectedFrdId)?.name ?? "—";
if (directories.length === 0) {
return (
<div className="rounded-xl border border-dashed border-slate-200 bg-slate-50 p-8 text-center">
<MessageSquareTextIcon className="mx-auto h-8 w-8 text-slate-400" />
<p className="mt-2 text-sm text-slate-500">
{t("workspace.unify.no_feedback_record_directory_available")}
</p>
</div>
);
}
if (error) {
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="flex h-48 flex-col items-center justify-center gap-3 px-4 text-center">
<MessageSquareTextIcon className="h-8 w-8 text-slate-400" />
<p className="text-sm text-slate-500">{error}</p>
<Button variant="secondary" size="sm" onClick={handleRefresh}>
{t("common.retry")}
</Button>
</div>
</div>
);
}
return (
<div className="space-y-3">
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-col gap-1">
<Label>{t("workspace.unify.feedback_record_directory")}</Label>
{directories.length === 1 ? (
<p className="text-sm font-medium text-slate-900">{currentFrdName}</p>
) : (
<Select value={selectedFrdId ?? ""} onValueChange={handleFrdChange}>
<SelectTrigger className="min-w-[220px]">
<SelectValue placeholder={t("workspace.unify.select_feedback_record_directory")} />
</SelectTrigger>
<SelectContent>
{directories.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div className="flex items-center gap-3">
{!isEmpty && (
<p className="text-sm text-slate-500">
{t("workspace.unify.showing_count_loaded", { count: records.length })}
</p>
)}
<Button
variant="secondary"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={t("workspace.unify.refresh_feedback_records")}>
<RefreshCwIcon className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
</div>
</div>
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="overflow-x-auto">
<table className="w-full min-w-[900px]">
<thead>
<tr className="border-b border-slate-200 text-left text-sm text-slate-900 [&>th]:font-semibold">
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.collected_at")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.source_name")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_label")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.field_type")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.value")}</th>
<th className="whitespace-nowrap px-4 py-3">{t("workspace.unify.user_identifier")}</th>
</tr>
</thead>
{isEmpty ? (
<tbody>
<tr>
<td colSpan={7}>
<div className="flex h-32 items-center justify-center">
<p className="text-sm text-slate-500">{t("workspace.unify.no_feedback_records")}</p>
</div>
</td>
</tr>
</tbody>
) : (
<tbody className="divide-y divide-slate-100">
{records.map((record) => (
<FeedbackRecordRow key={record.id} record={record} locale={locale} t={t} />
))}
</tbody>
)}
</table>
</div>
</div>
{hasMore && (
<div className="flex justify-center">
<Button variant="secondary" size="sm" onClick={handleLoadMore} loading={isLoadingMore}>
{t("common.load_more")}
</Button>
</div>
)}
</div>
);
};
const FeedbackRecordRow = ({
record,
locale,
t,
}: {
record: FeedbackRecordData;
locale: string;
t: TFunction;
}) => {
const value = formatValue(record, t, locale);
const isLongValue = value.length > 60;
return (
<tr className="text-sm text-slate-700 transition-colors hover:bg-slate-50">
<td className="whitespace-nowrap px-4 py-3 text-slate-500">
{formatDateTimeForDisplay(new Date(record.collected_at), locale)}
</td>
<td className="whitespace-nowrap px-4 py-3">
<Badge text={record.source_type} type="gray" size="tiny" />
</td>
<td className="max-w-[150px] truncate px-4 py-3" title={record.source_name ?? undefined}>
{record.source_name ?? "—"}
</td>
<td className="max-w-[200px] truncate px-4 py-3" title={record.field_label ?? undefined}>
{record.field_label ?? record.field_id}
</td>
<td className="whitespace-nowrap px-4 py-3">
<span className="inline-flex items-center gap-1 text-slate-600">
{FIELD_TYPE_ICONS[record.field_type] ?? <HashIcon className="h-3.5 w-3.5" />}
{record.field_type}
</span>
</td>
<td className="max-w-[250px] px-4 py-3">
{isLongValue ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default truncate">{truncate(value, 60)}</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm whitespace-pre-wrap">
{value}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span>{value}</span>
)}
</td>
<td className="max-w-[120px] truncate px-4 py-3 text-slate-500" title={record.user_identifier}>
{record.user_identifier ?? "—"}
</td>
</tr>
);
};
@@ -0,0 +1,57 @@
import { z } from "zod";
export const FIELD_TYPE_OPTIONS = [
"text",
"categorical",
"nps",
"csat",
"ces",
"rating",
"number",
"boolean",
"date",
] as const;
export const SOURCE_TYPE_PRESET_OPTIONS = [
"survey",
"review",
"feedback_form",
"support",
"social",
"interview",
"usability_test",
"nps_campaign",
] as const;
export const SOURCE_TYPE_CUSTOM_VALUE = "__custom__";
const ZMetadataEntry = z.object({
key: z.string().trim(),
value: z.string(),
});
export const ZFeedbackRecordFormValues = z.object({
id: z.string().optional(),
tenant_id: z.string().min(1),
submission_id: z.string().min(1),
collected_at: z.string().min(1),
created_at: z.string().optional(),
updated_at: z.string().optional(),
source_type: z.string().min(1),
source_id: z.string().optional(),
source_name: z.string().optional(),
field_id: z.string().min(1),
field_label: z.string().optional(),
field_type: z.enum(FIELD_TYPE_OPTIONS),
field_group_id: z.string().optional(),
field_group_label: z.string().optional(),
value_text: z.string().optional(),
value_number: z.string().optional(),
value_boolean: z.boolean().optional(),
value_date: z.string().optional(),
language: z.string().optional(),
user_identifier: z.string().optional(),
metadataEntries: z.array(ZMetadataEntry),
});
export type TFeedbackRecordFormValues = z.infer<typeof ZFeedbackRecordFormValues>;
@@ -0,0 +1,169 @@
import { describe, expect, test, vi } from "vitest";
import type { FeedbackRecordData } from "@/modules/hub/types";
import {
formatSourceType,
getCreateDefaults,
getReadOnlyMetadataEntries,
getValueFieldByType,
isPresetSourceType,
mapRecordToValues,
parseNumberValue,
toISOOrUndefined,
toLocalDateTimeInput,
} from "./utils";
vi.mock("uuid", () => ({ v7: () => "mock-uuid-v7" }));
const makeRecord = (overrides: Partial<FeedbackRecordData> = {}): FeedbackRecordData => ({
id: "rec-1",
tenant_id: "tenant-1",
submission_id: "sub-1",
collected_at: "2026-03-15T12:00:00.000Z",
created_at: "2026-03-15T12:00:00.000Z",
updated_at: "2026-03-15T12:00:00.000Z",
source_type: "survey",
field_id: "f1",
field_type: "text",
...overrides,
});
describe("getValueFieldByType", () => {
test.each([
["boolean", "value_boolean"],
["date", "value_date"],
["nps", "value_number"],
["csat", "value_number"],
["ces", "value_number"],
["rating", "value_number"],
["number", "value_number"],
["text", "value_text"],
["categorical", "value_text"],
] as const)("returns %s → %s", (input, expected) => {
expect(getValueFieldByType(input)).toBe(expected);
});
});
describe("toLocalDateTimeInput", () => {
test("formats valid ISO date", () => {
const result = toLocalDateTimeInput("2026-03-15T14:30:00.000Z");
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
});
test("returns empty string for invalid date", () => {
expect(toLocalDateTimeInput("not-a-date")).toBe("");
});
});
describe("toISOOrUndefined", () => {
test("returns ISO string for valid input", () => {
expect(toISOOrUndefined("2026-03-15T14:30")).toMatch(/2026-03-15/);
});
test("returns undefined for empty string", () => {
expect(toISOOrUndefined("")).toBeUndefined();
});
test("returns undefined for undefined", () => {
expect(toISOOrUndefined(undefined)).toBeUndefined();
});
test("returns undefined for invalid date", () => {
expect(toISOOrUndefined("not-a-date")).toBeUndefined();
});
});
describe("getCreateDefaults", () => {
test("uses first directory as tenant_id", () => {
const dirs = [{ id: "dir-1", name: "Dir 1" }];
const result = getCreateDefaults(dirs);
expect(result.tenant_id).toBe("dir-1");
expect(result.submission_id).toBe("mock-uuid-v7");
expect(result.field_type).toBe("text");
expect(result.metadataEntries).toEqual([]);
});
test("handles empty directories", () => {
const result = getCreateDefaults([]);
expect(result.tenant_id).toBe("");
});
});
describe("mapRecordToValues", () => {
test("maps a full record", () => {
const record = makeRecord({
value_text: "hello",
value_number: 42,
source_id: "s1",
source_name: "Survey",
metadata: { tag: "vip", nested: { a: 1 } },
});
const result = mapRecordToValues(record);
expect(result.id).toBe("rec-1");
expect(result.value_text).toBe("hello");
expect(result.value_number).toBe("42");
expect(result.source_id).toBe("s1");
expect(result.metadataEntries).toEqual([{ key: "tag", value: "vip" }]);
});
test("handles nullish optional fields", () => {
const record = makeRecord({ value_number: undefined, source_id: undefined });
const result = mapRecordToValues(record);
expect(result.value_number).toBe("");
expect(result.source_id).toBe("");
});
});
describe("getReadOnlyMetadataEntries", () => {
test("returns only non-string metadata values", () => {
const record = makeRecord({ metadata: { tag: "vip", count: 5, nested: { a: 1 } } });
const result = getReadOnlyMetadataEntries(record);
expect(result).toEqual([
{ key: "count", value: "5" },
{ key: "nested", value: '{"a":1}' },
]);
});
test("returns empty array when no metadata", () => {
expect(getReadOnlyMetadataEntries(makeRecord())).toEqual([]);
});
});
describe("parseNumberValue", () => {
test.each([
["42", 42],
["3.14", 3.14],
["-1", -1],
["", null],
[" ", null],
["abc", null],
["Infinity", null],
])("parseNumberValue(%s) → %s", (input, expected) => {
expect(parseNumberValue(input)).toBe(expected);
});
});
describe("isPresetSourceType", () => {
test("returns true for preset values", () => {
expect(isPresetSourceType("survey")).toBe(true);
expect(isPresetSourceType("nps_campaign")).toBe(true);
});
test("returns false for custom values", () => {
expect(isPresetSourceType("custom_type")).toBe(false);
expect(isPresetSourceType("")).toBe(false);
});
});
describe("formatSourceType", () => {
const t = ((key: string) => key) as any;
test("maps known source types", () => {
expect(formatSourceType("formbricks", t)).toBe("workspace.unify.formbricks_surveys");
expect(formatSourceType("formbricks_survey", t)).toBe("workspace.unify.formbricks_surveys");
expect(formatSourceType("csv", t)).toBe("workspace.unify.csv_import");
});
test("returns raw value for unknown types", () => {
expect(formatSourceType("custom", t)).toBe("custom");
});
});
@@ -0,0 +1,159 @@
import { TFunction } from "i18next";
import { v7 as uuidv7 } from "uuid";
import type { FeedbackRecordData } from "@/modules/hub/types";
import { SOURCE_TYPE_PRESET_OPTIONS, type TFeedbackRecordFormValues } from "./types";
export const getValueFieldByType = (
fieldType: TFeedbackRecordFormValues["field_type"]
): "value_text" | "value_number" | "value_boolean" | "value_date" => {
switch (fieldType) {
case "boolean":
return "value_boolean";
case "date":
return "value_date";
case "nps":
case "csat":
case "ces":
case "rating":
case "number":
return "value_number";
default:
return "value_text";
}
};
export const toLocalDateTimeInput = (isoDate: string): string => {
const date = new Date(isoDate);
if (!Number.isFinite(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}`;
};
export const toISOOrUndefined = (dateTimeValue: string | undefined): string | undefined => {
if (!dateTimeValue) {
return undefined;
}
const parsed = new Date(dateTimeValue);
if (!Number.isFinite(parsed.getTime())) {
return undefined;
}
return parsed.toISOString();
};
export const getCreateDefaults = (directories: { id: string; name: string }[]): TFeedbackRecordFormValues => {
const now = new Date();
const defaultDirectoryId = directories[0]?.id ?? "";
return {
id: "",
tenant_id: defaultDirectoryId,
submission_id: uuidv7(),
collected_at: toLocalDateTimeInput(now.toISOString()),
created_at: "",
updated_at: "",
source_type: "survey",
source_id: "",
source_name: "",
field_id: "",
field_label: "",
field_type: "text",
field_group_id: "",
field_group_label: "",
value_text: "",
value_number: "",
value_boolean: undefined,
value_date: "",
language: "",
user_identifier: "",
metadataEntries: [],
};
};
export const mapRecordToValues = (record: FeedbackRecordData): TFeedbackRecordFormValues => {
const metadataEntries = Object.entries(record.metadata ?? {})
.filter(([, value]) => typeof value === "string")
.map(([key, value]) => ({
key,
value: value as string,
}));
return {
id: record.id,
tenant_id: record.tenant_id,
submission_id: record.submission_id,
collected_at: toLocalDateTimeInput(record.collected_at),
created_at: record.created_at ? toLocalDateTimeInput(record.created_at) : "",
updated_at: record.updated_at ? toLocalDateTimeInput(record.updated_at) : "",
source_type: record.source_type,
source_id: record.source_id ?? "",
source_name: record.source_name ?? "",
field_id: record.field_id,
field_label: record.field_label ?? "",
field_type: record.field_type,
field_group_id: record.field_group_id ?? "",
field_group_label: record.field_group_label ?? "",
value_text: record.value_text ?? "",
value_number: record.value_number == null ? "" : String(record.value_number),
value_boolean: record.value_boolean,
value_date: record.value_date ? toLocalDateTimeInput(record.value_date) : "",
language: record.language ?? "",
user_identifier: record.user_identifier ?? "",
metadataEntries,
};
};
export const getReadOnlyMetadataEntries = (record: FeedbackRecordData): { key: string; value: string }[] => {
return Object.entries(record.metadata ?? {})
.filter(([, value]) => typeof value !== "string")
.map(([key, value]) => ({
key,
value: JSON.stringify(value),
}));
};
export const parseNumberValue = (value: string): number | null => {
if (value.trim() === "") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
export const isPresetSourceType = (value: string): value is (typeof SOURCE_TYPE_PRESET_OPTIONS)[number] =>
(SOURCE_TYPE_PRESET_OPTIONS as readonly string[]).includes(value);
export const formatSourceType = (sourceType: string, t: TFunction): string => {
switch (sourceType) {
case "formbricks":
case "formbricks_survey":
return t("workspace.unify.formbricks_surveys");
case "csv":
return t("workspace.unify.csv_import");
case "survey":
return t("workspace.unify.source_type_label_survey");
case "review":
return t("workspace.unify.source_type_label_review");
case "feedback_form":
return t("workspace.unify.source_type_label_feedback_form");
case "support":
return t("workspace.unify.source_type_label_support");
case "social":
return t("workspace.unify.source_type_label_social");
case "interview":
return t("workspace.unify.source_type_label_interview");
case "usability_test":
return t("workspace.unify.source_type_label_usability_test");
case "nps_campaign":
return t("workspace.unify.source_type_label_nps_campaign");
default:
return sourceType;
}
};
@@ -1,16 +1,16 @@
import { notFound } from "next/navigation";
import { getConnectorsWithMappings } from "@/lib/connector/service";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { FeedbackRecordListResponse } from "@/modules/hub";
import { listFeedbackRecords } from "@/modules/hub/service";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { FeedbackRecordsPageClient } from "./feedback-records-page-client";
import { FeedbackRecordsPageClient } from "./components/feedback-records-page-client";
const INITIAL_PAGE_SIZE = 10;
const INITIAL_PAGE_SIZE = 50;
export default async function UnifyFeedbackRecordsPage(props: {
readonly params: Promise<{ workspaceId: string }>;
}) {
export default async function UnifyFeedbackRecordsPage(
props: Readonly<{ params: Promise<{ workspaceId: string }> }>
) {
const t = await getTranslate();
const params = await props.params;
@@ -22,31 +22,40 @@ export default async function UnifyFeedbackRecordsPage(props: {
}
const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess;
const canWrite = isOwner || isManager || hasReadWriteAccess || hasManageAccess;
if (!hasAccess) {
return notFound();
}
const frds = await getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId);
const [frds, connectors] = await Promise.all([
getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId),
getConnectorsWithMappings(params.workspaceId),
]);
// Preload first FRD's records server-side for fast initial render
const initialFrdId = frds[0]?.id;
let initialRecords: FeedbackRecordListResponse | null = null;
const results = await Promise.all(
frds.map((frd) => listFeedbackRecords({ tenant_id: frd.id, limit: INITIAL_PAGE_SIZE }))
);
if (initialFrdId) {
const result = await listFeedbackRecords({ tenant_id: initialFrdId, limit: INITIAL_PAGE_SIZE });
// Don't crash if Hub is down — show empty state
if (!result.error) {
initialRecords = result.data;
}
}
// Don't crash if Hub is unreachable — show empty state
const successfulResults = results.filter((r) => !r.error);
const merged = successfulResults
.flatMap((r) => r.data?.data ?? [])
.toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1))
.slice(0, INITIAL_PAGE_SIZE);
const frdMap = Object.fromEntries(frds.map((f) => [f.id, f.name]));
const csvSources = connectors
.filter((connector) => connector.type === "csv")
.map((connector) => ({ id: connector.id, name: connector.name }));
return (
<FeedbackRecordsPageClient
workspaceId={params.workspaceId}
directories={frds}
initialFrdId={initialFrdId ?? null}
initialRecords={initialRecords?.data ?? []}
initialNextCursor={initialRecords?.next_cursor}
initialRecords={merged}
frdMap={frdMap}
csvSources={csvSources}
canWrite={canWrite}
/>
);
}
@@ -0,0 +1,80 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
export const ZFeedbackRecordId = z.uuid();
export const ZFeedbackRecordFieldType = z.enum([
"text",
"categorical",
"nps",
"csat",
"ces",
"rating",
"number",
"boolean",
"date",
]);
export const ZFeedbackRecordMetadata = z.record(z.string(), z.unknown());
export const ZFeedbackRecordCreateInput = z.object({
submission_id: z.string().min(1),
tenant_id: ZId,
source_type: z.string().min(1),
field_id: z.string().min(1),
field_type: ZFeedbackRecordFieldType,
collected_at: z.iso.datetime().optional(),
source_id: z.string().optional().nullable(),
source_name: z.string().optional().nullable(),
field_label: z.string().optional().nullable(),
field_group_id: z.string().optional(),
field_group_label: z.string().optional().nullable(),
value_text: z.string().optional().nullable(),
value_number: z.number().optional(),
value_boolean: z.boolean().optional(),
value_date: z.iso.datetime().optional(),
metadata: ZFeedbackRecordMetadata.optional(),
language: z.string().optional(),
user_identifier: z.string().optional(),
});
export type TFeedbackRecordCreateInput = z.infer<typeof ZFeedbackRecordCreateInput>;
export const ZFeedbackRecordUpdateInput = z
.object({
value_text: z.string().optional().nullable(),
value_number: z.number().optional().nullable(),
value_boolean: z.boolean().optional().nullable(),
value_date: z.iso.datetime().optional().nullable(),
language: z.string().optional().nullable(),
metadata: ZFeedbackRecordMetadata.optional(),
user_identifier: z.string().optional().nullable(),
})
.refine(
(value) => Object.values(value).some((entry) => entry !== undefined),
"At least one field must be provided for update"
);
export type TFeedbackRecordUpdateInput = z.infer<typeof ZFeedbackRecordUpdateInput>;
export const ZRetrieveFeedbackRecordAction = z.object({
workspaceId: ZId,
recordId: ZFeedbackRecordId,
});
export type TRetrieveFeedbackRecordAction = z.infer<typeof ZRetrieveFeedbackRecordAction>;
export const ZCreateFeedbackRecordAction = z.object({
workspaceId: ZId,
recordInput: ZFeedbackRecordCreateInput,
});
export type TCreateFeedbackRecordAction = z.infer<typeof ZCreateFeedbackRecordAction>;
export const ZUpdateFeedbackRecordAction = z.object({
workspaceId: ZId,
recordId: ZFeedbackRecordId,
updateInput: ZFeedbackRecordUpdateInput,
});
export type TUpdateFeedbackRecordAction = z.infer<typeof ZUpdateFeedbackRecordAction>;
@@ -2,5 +2,5 @@ import { redirect } from "next/navigation";
export default async function UnifyPage(props: { params: Promise<{ workspaceId: string }> }) {
const params = await props.params;
redirect(`/workspaces/${params.workspaceId}/unify/sources`);
redirect(`/workspaces/${params.workspaceId}/unify/feedback-records`);
}
@@ -0,0 +1,26 @@
"use client";
import { FileSpreadsheetIcon, FormIcon } from "lucide-react";
import { TConnectorType } from "@formbricks/types/connector";
export const getConnectorIcon = (type: TConnectorType, className: string) => {
switch (type) {
case "formbricks_survey":
return <FormIcon className={className} />;
case "csv":
return <FileSpreadsheetIcon className={className} />;
default:
return <FormIcon className={className} />;
}
};
export const getConnectorTypeLabelKey = (type: TConnectorType): string => {
switch (type) {
case "formbricks_survey":
return "workspace.unify.formbricks_surveys";
case "csv":
return "workspace.unify.csv_import";
default:
return type;
}
};
@@ -2,6 +2,7 @@
import {
CopyIcon,
EyeIcon,
FileSpreadsheetIcon,
MoreVertical,
PauseIcon,
@@ -9,6 +10,7 @@ import {
SquarePenIcon,
TrashIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TConnectorWithMappings } from "@formbricks/types/connector";
@@ -39,12 +41,15 @@ export function ConnectorRowDropdown({
onToggleStatus,
onDelete,
}: ConnectorRowDropdownProps) {
const router = useRouter();
const { t } = useTranslation();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const isActive = connector.status === "active";
const linkedSurveyId =
connector.type === "formbricks_survey" ? connector.formbricksMappings[0]?.surveyId : undefined;
const handleDelete = async () => {
setIsDeleting(true);
@@ -89,6 +94,25 @@ export function ConnectorRowDropdown({
</>
)}
{linkedSurveyId && (
<>
<DropdownMenuItem>
<button
type="button"
className="flex w-full items-center"
onClick={(e) => {
e.preventDefault();
setIsDropDownOpen(false);
router.push(`/workspaces/${connector.workspaceId}/surveys/${linkedSurveyId}/summary`);
}}>
<EyeIcon className="mr-2 h-4 w-4" />
{`${t("common.view")} ${t("common.survey")}`}
</button>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem>
<button
type="button"
@@ -1,56 +1,82 @@
"use client";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { TConnectorType } from "@formbricks/types/connector";
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
import { Badge } from "@/modules/ui/components/badge";
import { getConnectorOptions } from "../utils";
import { TConnectorOptionId, getConnectorOptions } from "../utils";
interface ConnectorTypeSelectorProps {
selectedType: TConnectorType | null;
onSelectType: (type: TConnectorType) => void;
selectedType: TConnectorOptionId | null;
onSelectType: (type: TConnectorOptionId) => void;
}
export function ConnectorTypeSelector({ selectedType, onSelectType }: ConnectorTypeSelectorProps) {
const getOptionClassName = (
selectedType: TConnectorOptionId | null,
optionId: TConnectorOptionId,
disabled: boolean
): string => {
if (selectedType === optionId) {
return "border-brand-dark bg-slate-50";
}
if (disabled) {
return "cursor-not-allowed border-slate-200 bg-slate-50 opacity-60";
}
return "border-slate-200 hover:border-slate-300 hover:bg-slate-50";
};
export function ConnectorTypeSelector({ selectedType, onSelectType }: Readonly<ConnectorTypeSelectorProps>) {
const { t } = useTranslation();
const connectorOptions = getConnectorOptions(t);
return (
<div className="space-y-3">
<p className="text-sm text-slate-600">{t("workspace.unify.select_source_type_prompt")}</p>
<div className="space-y-2">
{connectorOptions.map((option) => (
<button
key={option.id}
type="button"
disabled={option.disabled}
onClick={() => onSelectType(option.id as TConnectorType)}
className={`flex w-full items-center justify-between rounded-lg border p-4 text-left transition-colors ${
selectedType === option.id
? "border-brand-dark bg-slate-50"
: option.disabled
? "cursor-not-allowed border-slate-200 bg-slate-50 opacity-60"
: "border-slate-200 hover:border-slate-300 hover:bg-slate-50"
}`}>
onClick={() => onSelectType(option.id)}
className={`flex w-full items-center justify-between rounded-lg border p-3.5 text-left text-sm transition-colors ${getOptionClassName(
selectedType,
option.id,
option.disabled
)}`}>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900">{option.name}</span>
<span className="font-medium leading-5 text-slate-900">{option.name}</span>
{option.badge && <Badge text={option.badge.text} type={option.badge.type} size="tiny" />}
</div>
<p className="mt-1 text-sm text-slate-500">{option.description}</p>
<p className="mt-0.5 text-xs text-slate-500">{option.description}</p>
</div>
<div
className={`ml-4 h-5 w-5 rounded-full border-2 ${
className={`ml-3 h-4 w-4 rounded-full border-2 ${
selectedType === option.id ? "border-brand-dark bg-brand-dark" : "border-slate-300"
}`}>
{selectedType === option.id && (
<div className="flex h-full w-full items-center justify-center">
<div className="h-2 w-2 rounded-full bg-white" />
<div className="h-1.5 w-1.5 rounded-full bg-white" />
</div>
)}
</div>
</button>
))}
</div>
<Alert variant="outbound" size="small">
<AlertTitle>{t("workspace.unify.missing_feedback_source_title")}</AlertTitle>
<AlertButton asChild>
<Link
href="https://app.formbricks.com/s/cmob8tub9s2ndu5010ei4it0g"
target="_blank"
rel="noopener noreferrer"
className="text-slate-900 hover:underline">
{t("workspace.unify.request_feedback_source")}
</Link>
</AlertButton>
</Alert>
</div>
);
}
@@ -1,10 +1,12 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TConnectorType, TConnectorWithMappings, THubTargetField } from "@formbricks/types/connector";
import { SettingsCard } from "@/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard";
import {
createConnectorWithMappingsAction,
deleteConnectorAction,
@@ -12,9 +14,10 @@ import {
updateConnectorWithMappingsAction,
} from "@/lib/connector/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertButton, AlertDescription } from "@/modules/ui/components/alert";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation";
import { WorkspaceConfigNavigation } from "@/modules/workspaces/settings/components/workspace-config-navigation";
import { TFieldMapping, TUnifySurvey } from "../types";
import { ConnectorsTable } from "./connectors-table";
import { CreateConnectorModal } from "./create-connector-modal";
@@ -33,12 +36,21 @@ export function ConnectorsSection({
initialConnectors,
initialSurveys,
directories,
}: ConnectorsSectionProps) {
}: Readonly<ConnectorsSectionProps>) {
const { t } = useTranslation();
const router = useRouter();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingConnector, setEditingConnector] = useState<TConnectorWithMappings | null>(null);
const [csvImportConnector, setCsvImportConnector] = useState<TConnectorWithMappings | null>(null);
const directoryNames = directories.map((directory) => directory.name).join(", ");
const feedbackDirectoryAccessText =
directories.length === 1
? t("workspace.unify.feedback_sources_directory_access_single", {
directoryNames,
})
: t("workspace.unify.feedback_sources_directory_access_multiple", {
directoryNames,
});
const handleCreateConnector = async (data: {
name: string;
@@ -55,9 +67,9 @@ export function ConnectorsSection({
feedbackRecordDirectoryId: data.feedbackRecordDirectoryId,
},
formbricksMappings:
data.type === "formbricks" && data.surveyMappings?.length ? data.surveyMappings : undefined,
data.type === "formbricks_survey" && data.surveyMappings?.length ? data.surveyMappings : undefined,
fieldMappings:
data.type !== "formbricks" && data.fieldMappings?.length
data.type !== "formbricks_survey" && data.fieldMappings?.length
? data.fieldMappings.map((m) => ({
sourceFieldId: m.sourceFieldId || "",
targetFieldId: m.targetFieldId as THubTargetField,
@@ -154,22 +166,18 @@ export function ConnectorsSection({
return (
<PageContentWrapper>
<PageHeader
pageTitle={t("workspace.unify.unify_feedback")}
cta={
<CreateConnectorModal
open={isCreateModalOpen}
onOpenChange={setIsCreateModalOpen}
onCreateConnector={handleCreateConnector}
surveys={initialSurveys}
workspaceId={workspaceId}
directories={directories}
/>
}>
<UnifyConfigNavigation workspaceId={workspaceId} activeId="sources" />
<PageHeader pageTitle={t("common.workspace_configuration")}>
<WorkspaceConfigNavigation activeId="feedback-sources" />
</PageHeader>
<div className="space-y-6">
<SettingsCard
title={t("workspace.unify.feedback_sources")}
description={t("workspace.unify.feedback_sources_settings_description")}
buttonInfo={{
text: t("workspace.unify.add_source"),
onClick: () => setIsCreateModalOpen(true),
variant: "default",
}}>
<ConnectorsTable
connectors={initialConnectors}
onConnectorClick={setEditingConnector}
@@ -179,7 +187,27 @@ export function ConnectorsSection({
onDelete={handleDeleteConnector}
isLoading={false}
/>
</div>
{directories.length > 0 && (
<Alert size="small" className="mt-4">
<AlertDescription>{feedbackDirectoryAccessText}</AlertDescription>
<AlertButton asChild>
<Link href={`/workspaces/${workspaceId}/settings/feedback-record-directories`}>
{t("workspace.unify.manage_directories")}
</Link>
</AlertButton>
</Alert>
)}
</SettingsCard>
<CreateConnectorModal
open={isCreateModalOpen}
onOpenChange={setIsCreateModalOpen}
onCreateConnector={handleCreateConnector}
surveys={initialSurveys}
workspaceId={workspaceId}
directories={directories}
showTrigger={false}
/>
<EditConnectorModal
connector={editingConnector}
@@ -187,7 +215,6 @@ export function ConnectorsSection({
onOpenChange={(open) => !open && setEditingConnector(null)}
onUpdateConnector={handleUpdateConnector}
surveys={initialSurveys}
directories={directories}
onOpenCsvImport={() => {
if (editingConnector) {
setCsvImportConnector(editingConnector);
@@ -1,9 +1,9 @@
"use client";
import { FileSpreadsheetIcon, FormIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TConnectorStatus, TConnectorType, TConnectorWithMappings } from "@formbricks/types/connector";
import { Badge } from "@/modules/ui/components/badge";
import { getConnectorIcon, getConnectorTypeLabelKey } from "./connector-display";
import { ConnectorRowDropdown } from "./connector-row-dropdown";
const RELATIVE_TIME_DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [
@@ -39,17 +39,6 @@ interface ConnectorsTableDataRowProps {
onDelete: () => Promise<void>;
}
function getConnectorIcon(type: TConnectorType) {
switch (type) {
case "formbricks":
return <FormIcon className="h-4 w-4 text-slate-500" />;
case "csv":
return <FileSpreadsheetIcon className="h-4 w-4 text-slate-500" />;
default:
return <FormIcon className="h-4 w-4 text-slate-500" />;
}
}
const STATUS_BADGE_TYPE: Record<TConnectorStatus, "success" | "warning" | "error"> = {
active: "success",
paused: "warning",
@@ -63,28 +52,28 @@ export function ConnectorsTableDataRow({
onDuplicate,
onToggleStatus,
onDelete,
}: ConnectorsTableDataRowProps) {
}: Readonly<ConnectorsTableDataRowProps>) {
const { t, i18n } = useTranslation();
const getStatusLabel = (s: TConnectorStatus) => {
switch (s) {
case "active":
return t("workspace.unify.status_active");
case "paused":
return t("workspace.unify.status_paused");
case "error":
return t("workspace.unify.status_error");
const handleRowClick = () => {
if (connector.type === "csv" && onCsvImport) {
onCsvImport();
return;
}
onEdit();
};
const getConnectorTypeLabel = (connectorType: TConnectorType) => {
switch (connectorType) {
case "formbricks":
return t("workspace.unify.formbricks_surveys");
case "csv":
return t("workspace.unify.csv_import");
default:
return connectorType;
const getStatusLabel = (s: TConnectorStatus, connectorType: TConnectorType) => {
switch (s) {
case "active":
if (connectorType === "csv") {
return t("workspace.unify.status_ready");
}
return t("workspace.unify.status_live_sync");
case "paused":
return t("common.disabled");
case "error":
return t("workspace.unify.status_error");
}
};
@@ -93,28 +82,27 @@ export function ConnectorsTableDataRow({
role="button"
tabIndex={0}
className="grid h-12 min-h-12 cursor-pointer grid-cols-12 content-center p-2 text-left transition-colors ease-in-out hover:bg-slate-50"
onClick={onEdit}
onClick={handleRowClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onEdit();
handleRowClick();
}
}}>
<div className="col-span-1 flex items-center gap-2 pl-4" title={getConnectorTypeLabel(connector.type)}>
{getConnectorIcon(connector.type)}
<div
className="col-span-1 flex items-center gap-2 pl-4"
title={t(getConnectorTypeLabelKey(connector.type))}>
{getConnectorIcon(connector.type, "h-4 w-4 text-slate-500")}
</div>
<div className="col-span-3 flex items-center">
<div className="col-span-5 flex items-center">
<span className="truncate text-sm font-medium text-slate-900">{connector.name}</span>
</div>
<div className="col-span-1 hidden items-center justify-center sm:flex">
<Badge
text={getStatusLabel(connector.status)}
text={getStatusLabel(connector.status, connector.type)}
type={STATUS_BADGE_TYPE[connector.status]}
size="tiny"
/>
</div>
<div className="col-span-2 hidden items-center justify-center text-sm text-slate-500 sm:flex">
{getRelativeTime(connector.createdAt, i18n.language)}
</div>
<div className="col-span-2 hidden items-center justify-center text-sm text-slate-500 sm:flex">
{getRelativeTime(connector.updatedAt, i18n.language)}
</div>
@@ -23,16 +23,15 @@ export function ConnectorsTable({
onToggleStatus,
onDelete,
isLoading = false,
}: ConnectorsTableProps) {
}: Readonly<ConnectorsTableProps>) {
const { t } = useTranslation();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-12 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="col-span-1 pl-6">{t("common.type")}</div>
<div className="col-span-3">{t("common.name")}</div>
<div className="col-span-5">{t("common.name")}</div>
<div className="col-span-1 hidden text-center sm:block">{t("common.status")}</div>
<div className="col-span-2 hidden text-center sm:block">{t("common.created")}</div>
<div className="col-span-2 hidden text-center sm:block">{t("workspace.unify.updated_at")}</div>
<div className="col-span-2 hidden text-center sm:block">{t("workspace.unify.created_by")}</div>
<div className="col-span-1" />
@@ -1,7 +1,10 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2Icon, PlusIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TConnectorType, UNSUPPORTED_CONNECTOR_ELEMENT_TYPES } from "@formbricks/types/connector";
@@ -21,6 +24,14 @@ import {
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import {
FormControl,
FormError,
FormField,
FormItem,
FormLabel,
FormProvider,
} from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import {
@@ -30,21 +41,32 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { Switch } from "@/modules/ui/components/switch";
import {
FEEDBACK_RECORD_FIELDS,
TCreateConnectorStep,
TFieldMapping,
TFormbricksConnectorForm,
TSourceField,
TUnifySurvey,
ZFormbricksConnectorForm,
} from "../types";
import { TEnumValidationError, parseCSVColumnsToFields, validateEnumMappings } from "../utils";
import {
TConnectorOptionId,
TEnumValidationError,
areAllRequiredFieldsMapped,
isConnectorNameValid,
parseCSVColumnsToFields,
toggleQuestionId,
validateEnumMappings,
} from "../utils";
import { ConnectorTypeSelector } from "./connector-type-selector";
import { CsvConnectorUI } from "./csv-connector-ui";
import { FormbricksSurveySelector } from "./formbricks-survey-selector";
import { FormbricksQuestionList } from "./formbricks-question-list";
interface CreateConnectorModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
showTrigger?: boolean;
onCreateConnector: (data: {
name: string;
type: TConnectorType;
@@ -59,139 +81,96 @@ interface CreateConnectorModalProps {
const getDialogTitle = (
step: TCreateConnectorStep,
type: TConnectorType | null,
type: TConnectorOptionId | null,
t: (key: string) => string
): string => {
if (step === "selectType") return t("workspace.unify.add_feedback_source");
if (type === "formbricks") return t("workspace.unify.select_survey_and_questions");
if (type === "formbricks_survey") return t("workspace.unify.select_survey_and_questions");
if (type === "csv") return t("workspace.unify.import_csv_data");
return t("workspace.unify.configure_mapping");
};
const getDialogDescription = (
step: TCreateConnectorStep,
type: TConnectorType | null,
type: TConnectorOptionId | null,
t: (key: string) => string
): string => {
if (step === "selectType") return t("workspace.unify.select_source_type_description");
if (type === "formbricks") return t("workspace.unify.select_survey_questions_description");
if (type === "formbricks_survey") return t("workspace.unify.select_survey_questions_description");
if (type === "csv") return t("workspace.unify.upload_csv_data_description");
return t("workspace.unify.configure_mapping");
};
const getNextStepButtonLabel = (type: TConnectorType | null, t: (key: string) => string): string => {
if (type === "formbricks") return t("workspace.unify.select_questions");
const getNextStepButtonLabel = (type: TConnectorOptionId | null, t: (key: string) => string): string => {
if (type === "formbricks_survey") return t("workspace.unify.select_questions");
if (type === "csv") return t("workspace.unify.configure_import");
if (type === "api_ingestion") return t("workspace.unify.api_ingestion_manage_api_keys");
if (type === "feedback_record_mcp") return t("common.learn_more");
return t("workspace.unify.create_mapping");
};
const getCreateDisabled = (
type: TConnectorType | null,
isFormbricksValid: boolean,
isCsvValid: boolean,
allRequiredMapped: boolean
): boolean => {
if (type === "formbricks") return !isFormbricksValid;
if (type === "csv") return !isCsvValid || !allRequiredMapped;
return !allRequiredMapped;
};
interface AggregateImportSectionProps {
surveyEntries: {
surveyId: string;
surveyName: string;
responseCount: number;
elementCount: number;
importHistorical: boolean;
}[];
onImportHistoricalChange: (surveyId: string, checked: boolean) => void;
t: (key: string, options?: Record<string, unknown>) => string;
}
const AggregateImportSection = ({
surveyEntries,
onImportHistoricalChange,
t,
}: AggregateImportSectionProps) => {
const totalRecords = surveyEntries.reduce((sum, e) => sum + e.responseCount * e.elementCount, 0);
const checkedCount = surveyEntries.filter((e) => e.importHistorical).length;
const checkedTotal = surveyEntries
.filter((e) => e.importHistorical)
.reduce((sum, e) => sum + e.responseCount * e.elementCount, 0);
return (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4">
<div className="space-y-2">
{surveyEntries.map((entry) => (
<label key={entry.surveyId} className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={entry.importHistorical}
onChange={(e) => onImportHistoricalChange(entry.surveyId, e.target.checked)}
className="h-4 w-4 rounded border-amber-300 text-amber-600 focus:ring-amber-500"
/>
<span className="text-xs text-amber-800">
{t("workspace.unify.survey_import_line", {
surveyName: entry.surveyName,
responseCount: entry.responseCount,
questionCount: entry.elementCount,
total: entry.responseCount * entry.elementCount,
})}
</span>
</label>
))}
</div>
{surveyEntries.length > 1 && (
<p className="mt-3 border-t border-amber-200 pt-2 text-xs font-medium text-amber-900">
{t("workspace.unify.total_feedback_records", {
checked: checkedTotal,
total: totalRecords,
surveyCount: checkedCount,
})}
</p>
)}
</div>
);
};
const getSelectableQuestionIds = (survey: TUnifySurvey): string[] =>
survey.elements
.filter((element) => !(UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(element.type))
.map((element) => element.id);
export const CreateConnectorModal = ({
open,
onOpenChange,
showTrigger = true,
onCreateConnector,
surveys,
workspaceId,
directories,
}: CreateConnectorModalProps) => {
const { t } = useTranslation();
const router = useRouter();
const defaultConnectorName = useMemo<Record<TConnectorType, string>>(
() => ({
formbricks_survey: t("workspace.unify.default_connector_name_formbricks"),
csv: t("workspace.unify.default_connector_name_csv"),
}),
[t]
);
const formbricksForm = useForm<TFormbricksConnectorForm>({
resolver: zodResolver(ZFormbricksConnectorForm),
defaultValues: {
sourceName: defaultConnectorName.formbricks_survey,
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
},
mode: "onChange",
});
const defaultConnectorName: Record<TConnectorType, string> = {
formbricks: t("workspace.unify.default_connector_name_formbricks"),
csv: t("workspace.unify.default_connector_name_csv"),
};
const [currentStep, setCurrentStep] = useState<TCreateConnectorStep>("selectType");
const [selectedType, setSelectedType] = useState<TConnectorType | null>(null);
const [connectorName, setConnectorName] = useState("");
const [selectedType, setSelectedType] = useState<TConnectorOptionId | null>(null);
const [mappings, setMappings] = useState<TFieldMapping[]>([]);
const [sourceFields, setSourceFields] = useState<TSourceField[]>([]);
const [selectedSurveyId, setSelectedSurveyId] = useState<string | null>(null);
const [elementIdsBySurvey, setElementIdsBySurvey] = useState<Record<string, string[]>>({});
const [csvParsedData, setCsvParsedData] = useState<Record<string, string>[]>([]);
const [enumValidationErrors, setEnumValidationErrors] = useState<TEnumValidationError[]>([]);
const selectedElementIds = selectedSurveyId ? (elementIdsBySurvey[selectedSurveyId] ?? []) : [];
const [csvConnectorName, setCsvConnectorName] = useState("");
const [responseCountBySurvey, setResponseCountBySurvey] = useState<Record<string, number | null>>({});
const [importHistoricalBySurvey, setImportHistoricalBySurvey] = useState<Record<string, boolean>>({});
const [isImporting, setIsImporting] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [selectedDirectoryId, setSelectedDirectoryId] = useState<string | null>(
directories.length === 1 ? directories[0].id : null
const [selectedDirectoryId, setSelectedDirectoryId] = useState<string | null>(directories[0]?.id ?? null);
const formbricksValues = formbricksForm.watch();
const selectedSurveyId = formbricksValues.surveyId;
const selectedQuestionIds = formbricksValues.selectedQuestionIds ?? [];
const selectedSurvey = useMemo(
() => surveys.find((survey) => survey.id === selectedSurveyId) ?? null,
[surveys, selectedSurveyId]
);
const selectedSurveyResponseCount =
selectedSurveyId && responseCountBySurvey[selectedSurveyId] !== undefined
? responseCountBySurvey[selectedSurveyId]
: null;
const fetchResponseCount = useCallback(
async (surveyId: string) => {
if (responseCountBySurvey[surveyId] !== undefined) return;
@@ -204,30 +183,50 @@ export const CreateConnectorModal = ({
setResponseCountBySurvey((prev) => ({ ...prev, [surveyId]: null }));
}
},
[workspaceId, responseCountBySurvey]
[responseCountBySurvey, workspaceId]
);
useEffect(() => {
if (selectedSurveyId && selectedType === "formbricks") {
if (selectedSurveyId && currentStep === "mapping" && selectedType === "formbricks_survey") {
fetchResponseCount(selectedSurveyId);
}
}, [selectedSurveyId, selectedType, fetchResponseCount]);
}, [currentStep, fetchResponseCount, selectedSurveyId, selectedType]);
useEffect(() => {
if (currentStep !== "mapping" || selectedType !== "formbricks_survey" || !selectedSurveyId) {
return;
}
const survey = surveys.find((item) => item.id === selectedSurveyId);
const supportedElementIds = survey ? getSelectableQuestionIds(survey) : [];
formbricksForm.setValue("selectedQuestionIds", supportedElementIds, {
shouldDirty: true,
shouldValidate: true,
});
formbricksForm.setValue("importHistorical", true, {
shouldDirty: true,
});
}, [currentStep, formbricksForm, selectedSurveyId, selectedType, surveys]);
const resetForm = () => {
setCurrentStep("selectType");
setSelectedType(null);
setConnectorName("");
formbricksForm.reset({
sourceName: defaultConnectorName.formbricks_survey,
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
});
setMappings([]);
setSourceFields([]);
setCsvParsedData([]);
setEnumValidationErrors([]);
setSelectedSurveyId(null);
setElementIdsBySurvey({});
setResponseCountBySurvey({});
setImportHistoricalBySurvey({});
setCsvConnectorName("");
setIsImporting(false);
setIsCreating(false);
setSelectedDirectoryId(directories.length === 1 ? directories[0].id : null);
setSelectedDirectoryId(directories[0]?.id ?? null);
};
const handleOpenChange = (newOpen: boolean) => {
@@ -239,50 +238,31 @@ export const CreateConnectorModal = ({
const handleNextStep = () => {
if (currentStep !== "selectType" || !selectedType) return;
const selectedSurvey = surveys.find((s) => s.id === selectedSurveyId);
setConnectorName(
selectedType === "formbricks" && selectedSurvey
? `${selectedSurvey.name} ${t("workspace.unify.connection")}`
: defaultConnectorName[selectedType]
);
setCurrentStep("mapping");
};
const handleSurveySelect = (surveyId: string | null) => {
setSelectedSurveyId(surveyId);
};
const handleElementToggle = (elementId: string) => {
if (!selectedSurveyId) return;
setElementIdsBySurvey((prev) => {
const current = prev[selectedSurveyId] ?? [];
return {
...prev,
[selectedSurveyId]: current.includes(elementId)
? current.filter((id) => id !== elementId)
: [...current, elementId],
};
});
};
const handleSelectAllElements = (surveyId: string) => {
const survey = surveys.find((s) => s.id === surveyId);
if (survey) {
setElementIdsBySurvey((prev) => ({
...prev,
[surveyId]: survey.elements
.filter((e) => !(UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(e.type))
.map((e) => e.id),
}));
if (selectedType === "api_ingestion") {
handleOpenChange(false);
router.push(`/workspaces/${workspaceId}/settings/api-keys`);
return;
}
};
const handleDeselectAllElements = () => {
if (!selectedSurveyId) return;
setElementIdsBySurvey((prev) => ({
...prev,
[selectedSurveyId]: [],
}));
if (selectedType === "feedback_record_mcp") {
window.open("https://formbricks.com/docs", "_blank", "noopener,noreferrer");
return;
}
if (selectedType === "formbricks_survey") {
formbricksForm.reset({
sourceName: defaultConnectorName.formbricks_survey,
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
});
}
if (selectedType === "csv") {
setCsvConnectorName(defaultConnectorName.csv);
}
setCurrentStep("mapping");
};
const handleBack = () => {
@@ -290,52 +270,31 @@ export const CreateConnectorModal = ({
setCurrentStep("selectType");
setMappings([]);
setSourceFields([]);
setEnumValidationErrors([]);
}
};
const getSurveyMappings = () =>
Object.entries(elementIdsBySurvey)
.filter(([, ids]) => ids.length > 0)
.map(([surveyId, elementIds]) => ({ surveyId, elementIds }));
const handleHistoricalImports = async (connectorId: string) => {
const surveysToImport = Object.entries(importHistoricalBySurvey)
.filter(([surveyId, checked]) => checked && (elementIdsBySurvey[surveyId]?.length ?? 0) > 0)
.map(([surveyId]) => surveyId);
if (surveysToImport.length === 0) return;
const handleHistoricalImport = async (connectorId: string, surveyId: string) => {
const responseCount = responseCountBySurvey[surveyId] ?? 0;
if (responseCount <= 0) return;
setIsImporting(true);
let totalSuccesses = 0;
let totalFailures = 0;
let totalSkipped = 0;
for (const surveyId of surveysToImport) {
const importResult = await importHistoricalResponsesAction({
connectorId,
workspaceId,
surveyId,
});
if (importResult?.data) {
totalSuccesses += importResult.data.successes;
totalFailures += importResult.data.failures;
totalSkipped += importResult.data.skipped;
} else {
toast.error(getFormattedErrorMessage(importResult));
}
}
const importResult = await importHistoricalResponsesAction({
connectorId,
workspaceId,
surveyId,
});
setIsImporting(false);
if (totalSuccesses > 0 || totalFailures > 0) {
if (importResult?.data) {
toast.success(
t("workspace.unify.historical_import_complete", {
successes: totalSuccesses,
failures: totalFailures,
skipped: totalSkipped,
successes: importResult.data.successes,
failures: importResult.data.failures,
skipped: importResult.data.skipped,
})
);
} else {
toast.error(getFormattedErrorMessage(importResult));
}
};
@@ -361,10 +320,37 @@ export const CreateConnectorModal = ({
}
};
const handleCreate = async () => {
if (!selectedType || !connectorName.trim() || !selectedDirectoryId) return;
const handleFormbricksQuestionToggle = (questionId: string) => {
const nextSelection = toggleQuestionId(formbricksForm.getValues("selectedQuestionIds"), questionId);
formbricksForm.setValue("selectedQuestionIds", nextSelection, {
shouldDirty: true,
shouldValidate: true,
});
};
if (selectedType === "csv" && csvParsedData.length > 0) {
const handleCreateFormbricksConnector = async (values: TFormbricksConnectorForm) => {
if (!selectedDirectoryId) return;
setIsCreating(true);
const connectorId = await onCreateConnector({
name: values.sourceName.trim(),
type: "formbricks_survey",
feedbackRecordDirectoryId: selectedDirectoryId,
surveyMappings: [{ surveyId: values.surveyId, elementIds: values.selectedQuestionIds }],
});
if (connectorId && values.importHistorical) {
await handleHistoricalImport(connectorId, values.surveyId);
}
setIsCreating(false);
resetForm();
onOpenChange(false);
};
const handleCreateCsvConnector = async () => {
if (!selectedDirectoryId || !isConnectorNameValid(csvConnectorName)) return;
if (csvParsedData.length > 0) {
const errors = validateEnumMappings(mappings, csvParsedData);
if (errors.length > 0) {
setEnumValidationErrors(errors);
@@ -375,21 +361,14 @@ export const CreateConnectorModal = ({
setIsCreating(true);
const surveyMappings = getSurveyMappings();
const connectorId = await onCreateConnector({
name: connectorName.trim(),
type: selectedType,
name: csvConnectorName.trim(),
type: "csv",
feedbackRecordDirectoryId: selectedDirectoryId,
surveyMappings: selectedType === "formbricks" && surveyMappings.length > 0 ? surveyMappings : undefined,
fieldMappings: selectedType !== "formbricks" && mappings.length > 0 ? mappings : undefined,
fieldMappings: mappings.length > 0 ? mappings : undefined,
});
if (connectorId && selectedType === "formbricks") {
await handleHistoricalImports(connectorId);
}
if (connectorId && selectedType === "csv" && csvParsedData.length > 0) {
if (connectorId && csvParsedData.length > 0) {
await handleCsvImport(connectorId);
}
@@ -398,14 +377,8 @@ export const CreateConnectorModal = ({
onOpenChange(false);
};
const requiredFields = FEEDBACK_RECORD_FIELDS.filter((f) => f.required);
const allRequiredMapped = requiredFields.every((field) =>
mappings.some((m) => m.targetFieldId === field.id && (m.sourceFieldId || m.staticValue))
);
const hasAnyElementSelections = Object.values(elementIdsBySurvey).some((ids) => ids.length > 0);
const isFormbricksValid = selectedType === "formbricks" && hasAnyElementSelections;
const isCsvValid = selectedType === "csv" && sourceFields.length > 0;
const areCsvRequiredFieldsMapped = areAllRequiredFieldsMapped(mappings);
const handleLoadSourceFields = () => {
if (selectedType === "csv") {
@@ -416,10 +389,12 @@ export const CreateConnectorModal = ({
return (
<>
<Button onClick={() => onOpenChange(true)} size="sm">
{t("workspace.unify.add_source")}
<PlusIcon className="ml-2 h-4 w-4" />
</Button>
{showTrigger && (
<Button onClick={() => onOpenChange(true)} size="sm">
{t("workspace.unify.add_source")}
<PlusIcon className="ml-2 h-4 w-4" />
</Button>
)}
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-3xl">
@@ -444,63 +419,97 @@ export const CreateConnectorModal = ({
<ConnectorTypeSelector selectedType={selectedType} onSelectType={setSelectedType} />
)}
{currentStep === "mapping" && selectedType === "formbricks" && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="connectorName">{t("workspace.unify.source_name")}</Label>
<Input
id="connectorName"
value={connectorName}
onChange={(e) => setConnectorName(e.target.value)}
placeholder={t("workspace.unify.enter_name_for_source")}
{currentStep === "mapping" && selectedType === "formbricks_survey" && (
<FormProvider {...formbricksForm}>
<form className="space-y-4">
<FormField
control={formbricksForm.control}
name="sourceName"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.source_name")}</FormLabel>
<FormControl>
<Input
value={field.value}
onChange={field.onChange}
placeholder={t("workspace.unify.enter_name_for_source")}
/>
</FormControl>
<FormError />
</FormItem>
)}
/>
</div>
<FrdPicker
directories={directories}
selectedDirectoryId={selectedDirectoryId}
onChange={setSelectedDirectoryId}
workspaceId={workspaceId}
t={t}
/>
{directories.length === 0 && (
<NoFeedbackRecordDirectoryAlert workspaceId={workspaceId} t={t} />
)}
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<FormbricksSurveySelector
surveys={surveys}
selectedSurveyId={selectedSurveyId}
selectedElementIds={selectedElementIds}
onSurveySelect={handleSurveySelect}
onElementToggle={handleElementToggle}
onSelectAllElements={handleSelectAllElements}
onDeselectAllElements={handleDeselectAllElements}
<FormField
control={formbricksForm.control}
name="surveyId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.select_survey")}</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder={t("workspace.unify.select_survey")} />
</SelectTrigger>
<SelectContent>
{surveys.map((survey) => (
<SelectItem key={survey.id} value={survey.id}>
{survey.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormError />
</FormItem>
)}
/>
</div>
{(() => {
const entries = Object.entries(elementIdsBySurvey)
.filter(([, ids]) => ids.length > 0)
.map(([surveyId, ids]) => ({
surveyId,
surveyName: surveys.find((s) => s.id === surveyId)?.name ?? surveyId,
responseCount: responseCountBySurvey[surveyId] ?? 0,
elementCount: ids.length,
importHistorical: importHistoricalBySurvey[surveyId] ?? false,
}))
.filter((e) => e.responseCount > 0);
<FormField
control={formbricksForm.control}
name="selectedQuestionIds"
render={() => (
<FormItem>
<FormLabel>{t("workspace.unify.select_questions")}</FormLabel>
<FormControl>
<div>
<FormbricksQuestionList
survey={selectedSurvey}
selectedQuestionIds={selectedQuestionIds}
onQuestionToggle={handleFormbricksQuestionToggle}
/>
</div>
</FormControl>
<FormError />
</FormItem>
)}
/>
if (entries.length === 0) return null;
return (
<AggregateImportSection
surveyEntries={entries}
onImportHistoricalChange={(surveyId, checked) => {
setImportHistoricalBySurvey((prev) => ({ ...prev, [surveyId]: checked }));
}}
t={t}
{selectedSurveyResponseCount !== null && selectedSurveyResponseCount > 0 && (
<FormField
control={formbricksForm.control}
name="importHistorical"
render={({ field }) => (
<FormItem className="rounded-md border border-slate-200 p-3">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<FormLabel>{t("workspace.unify.import_historical_responses")}</FormLabel>
<p className="text-sm text-slate-500">
{t("workspace.unify.import_historical_responses_description")}
</p>
</div>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</div>
</FormItem>
)}
/>
);
})()}
</div>
)}
</form>
</FormProvider>
)}
{currentStep === "mapping" && selectedType === "csv" && (
@@ -509,21 +518,17 @@ export const CreateConnectorModal = ({
<Label htmlFor="connectorName">{t("workspace.unify.source_name")}</Label>
<Input
id="connectorName"
value={connectorName}
onChange={(e) => setConnectorName(e.target.value)}
value={csvConnectorName}
onChange={(event) => setCsvConnectorName(event.target.value)}
placeholder={t("workspace.unify.enter_name_for_source")}
/>
</div>
<FrdPicker
directories={directories}
selectedDirectoryId={selectedDirectoryId}
onChange={setSelectedDirectoryId}
workspaceId={workspaceId}
t={t}
/>
{directories.length === 0 && (
<NoFeedbackRecordDirectoryAlert workspaceId={workspaceId} t={t} />
)}
<div className="max-h-[55vh] overflow-y-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
<div className="max-h-[55vh] overflow-y-auto rounded-lg border border-slate-200 p-4">
<CsvConnectorUI
sourceFields={sourceFields}
mappings={mappings}
@@ -582,13 +587,20 @@ export const CreateConnectorModal = ({
</Button>
) : (
<Button
onClick={handleCreate}
onClick={
selectedType === "formbricks_survey"
? () => void formbricksForm.handleSubmit(handleCreateFormbricksConnector)()
: handleCreateCsvConnector
}
disabled={
isCreating ||
isImporting ||
!connectorName.trim() ||
!selectedDirectoryId ||
getCreateDisabled(selectedType, !!isFormbricksValid, isCsvValid, allRequiredMapped)
(selectedType === "formbricks_survey"
? !isConnectorNameValid(formbricksValues.sourceName ?? "") ||
!formbricksValues.surveyId ||
!formbricksValues.selectedQuestionIds?.length
: !isConnectorNameValid(csvConnectorName) || !isCsvValid || !areCsvRequiredFieldsMapped)
}>
{isCreating && <Loader2Icon className="mr-2 h-4 w-4 animate-spin" />}
{t("workspace.unify.setup_connection")}
@@ -601,52 +613,22 @@ export const CreateConnectorModal = ({
);
};
interface FrdPickerProps {
directories: { id: string; name: string }[];
selectedDirectoryId: string | null;
onChange: (id: string) => void;
interface NoFeedbackRecordDirectoryAlertProps {
workspaceId: string;
t: (key: string) => string;
}
const FrdPicker = ({ directories, selectedDirectoryId, onChange, workspaceId, t }: FrdPickerProps) => {
if (directories.length === 0) {
return (
<Alert variant="error" size="small">
<div>
<p>{t("workspace.unify.no_feedback_record_directory_available")}</p>
<a
className="mt-1 inline-block font-medium underline"
href={`/workspaces/${workspaceId}/settings/feedback-record-directories`}>
{t("workspace.unify.go_to_feedback_record_directories")}
</a>
</div>
</Alert>
);
}
if (directories.length === 1) {
return (
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
{t("workspace.unify.records_will_go_to")}{" "}
<span className="font-medium text-slate-900">{directories[0].name}</span>
</div>
);
}
const NoFeedbackRecordDirectoryAlert = ({ workspaceId, t }: NoFeedbackRecordDirectoryAlertProps) => {
return (
<div className="space-y-2">
<Label htmlFor="feedbackRecordDirectory">{t("workspace.unify.feedback_record_directory")}</Label>
<Select value={selectedDirectoryId ?? ""} onValueChange={onChange}>
<SelectTrigger id="feedbackRecordDirectory">
<SelectValue placeholder={t("workspace.unify.select_feedback_record_directory")} />
</SelectTrigger>
<SelectContent>
{directories.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Alert variant="error" size="small">
<div>
<p>{t("workspace.unify.no_feedback_record_directory_available")}</p>
<a
className="mt-1 inline-block font-medium underline"
href={`/workspaces/${workspaceId}/settings/feedback-record-directories`}>
{t("workspace.unify.go_to_feedback_record_directories")}
</a>
</div>
</Alert>
);
};
@@ -1,9 +1,10 @@
"use client";
import { FileSpreadsheetIcon, GlobeIcon } from "lucide-react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { TConnectorType, TConnectorWithMappings } from "@formbricks/types/connector";
import { TConnectorWithMappings } from "@formbricks/types/connector";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
@@ -13,17 +14,39 @@ import {
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import {
FormControl,
FormError,
FormField,
FormItem,
FormLabel,
FormProvider,
} from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import {
FEEDBACK_RECORD_FIELDS,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import {
SAMPLE_CSV_COLUMNS,
TFieldMapping,
TFormbricksConnectorForm,
TSourceField,
TUnifySurvey,
ZFormbricksConnectorForm,
} from "../types";
import { parseCSVColumnsToFields } from "../utils";
import { FormbricksSurveySelector } from "./formbricks-survey-selector";
import {
areAllRequiredFieldsMapped,
isConnectorNameValid,
parseCSVColumnsToFields,
toggleQuestionId,
} from "../utils";
import { getConnectorIcon, getConnectorTypeLabelKey } from "./connector-display";
import { FormbricksQuestionList } from "./formbricks-question-list";
import { MappingUI } from "./mapping-ui";
interface EditConnectorModalProps {
@@ -38,78 +61,61 @@ interface EditConnectorModalProps {
fieldMappings?: TFieldMapping[];
}) => Promise<void>;
surveys: TUnifySurvey[];
directories: { id: string; name: string }[];
onOpenCsvImport?: () => void;
}
const getConnectorIcon = (type: TConnectorType) => {
switch (type) {
case "formbricks":
return <GlobeIcon className="h-5 w-5 text-slate-500" />;
case "csv":
return <FileSpreadsheetIcon className="h-5 w-5 text-slate-500" />;
default:
return <GlobeIcon className="h-5 w-5 text-slate-500" />;
}
};
const getConnectorTypeLabelKey = (type: TConnectorType): string => {
switch (type) {
case "formbricks":
return "workspace.unify.formbricks_surveys";
case "csv":
return "workspace.unify.csv_import";
default:
return type;
}
};
const groupMappingsBySurvey = (
mappings: { surveyId: string; elementId: string }[]
): Record<string, string[]> => {
const grouped: Record<string, string[]> = {};
for (const m of mappings) {
if (!grouped[m.surveyId]) grouped[m.surveyId] = [];
grouped[m.surveyId].push(m.elementId);
}
return grouped;
};
export const EditConnectorModal = ({
connector,
open,
onOpenChange,
onUpdateConnector,
surveys,
directories,
onOpenCsvImport,
}: EditConnectorModalProps) => {
const { t } = useTranslation();
const [connectorName, setConnectorName] = useState("");
const [csvConnectorName, setCsvConnectorName] = useState("");
const [mappings, setMappings] = useState<TFieldMapping[]>([]);
const [sourceFields, setSourceFields] = useState<TSourceField[]>([]);
const [isUpdating, setIsUpdating] = useState(false);
const [selectedSurveyId, setSelectedSurveyId] = useState<string | null>(null);
const [elementIdsBySurvey, setElementIdsBySurvey] = useState<Record<string, string[]>>({});
const formbricksForm = useForm<TFormbricksConnectorForm>({
resolver: zodResolver(ZFormbricksConnectorForm),
defaultValues: {
sourceName: "",
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
},
mode: "onChange",
});
const selectedElementIds = selectedSurveyId ? (elementIdsBySurvey[selectedSurveyId] ?? []) : [];
const requiredFields = FEEDBACK_RECORD_FIELDS.filter((f) => f.required);
const allRequiredMapped = requiredFields.every((field) =>
mappings.some((m) => m.targetFieldId === field.id && (m.sourceFieldId || m.staticValue))
const formbricksValues = formbricksForm.watch();
const selectedSurveyId = formbricksValues.surveyId;
const selectedQuestionIds = formbricksValues.selectedQuestionIds ?? [];
const selectedSurvey = useMemo(
() => surveys.find((survey) => survey.id === selectedSurveyId) ?? null,
[surveys, selectedSurveyId]
);
useEffect(() => {
if (connector) {
setConnectorName(connector.name);
if (connector.type === "formbricks_survey") {
const mappedSurveyId = connector.formbricksMappings[0]?.surveyId ?? "";
const mappedQuestionIds = connector.formbricksMappings
.filter((mapping) => mapping.surveyId === mappedSurveyId)
.map((mapping) => mapping.elementId);
if (connector.type === "formbricks") {
const fbMappings = connector.formbricksMappings;
setSelectedSurveyId(fbMappings.length > 0 ? fbMappings[0].surveyId : null);
setElementIdsBySurvey(groupMappingsBySurvey(fbMappings));
formbricksForm.reset({
sourceName: connector.name,
surveyId: mappedSurveyId,
selectedQuestionIds: mappedQuestionIds,
importHistorical: true,
});
setCsvConnectorName("");
setSourceFields([]);
setMappings([]);
} else if (connector.type === "csv") {
setCsvConnectorName(connector.name);
const columnsFromMappings = [
...new Set(connector.fieldMappings.map((m) => m.sourceFieldId).filter(Boolean)),
];
@@ -125,23 +131,37 @@ export const EditConnectorModal = ({
staticValue: m.staticValue ?? undefined,
}))
);
setSelectedSurveyId(null);
setElementIdsBySurvey({});
formbricksForm.reset({
sourceName: "",
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
});
} else {
setCsvConnectorName("");
setSourceFields([]);
setMappings([]);
setSelectedSurveyId(null);
setElementIdsBySurvey({});
formbricksForm.reset({
sourceName: "",
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
});
}
}
}, [connector]);
}, [connector, formbricksForm]);
const resetForm = () => {
setConnectorName("");
setCsvConnectorName("");
setMappings([]);
setSourceFields([]);
setSelectedSurveyId(null);
setElementIdsBySurvey({});
formbricksForm.reset({
sourceName: "",
surveyId: "",
selectedQuestionIds: [],
importHistorical: true,
});
setIsUpdating(false);
};
const handleOpenChange = (newOpen: boolean) => {
@@ -151,76 +171,60 @@ export const EditConnectorModal = ({
onOpenChange(newOpen);
};
const handleSurveySelect = (surveyId: string | null) => {
setSelectedSurveyId(surveyId);
};
const handleElementToggle = (elementId: string) => {
if (!selectedSurveyId) return;
setElementIdsBySurvey((prev) => {
const current = prev[selectedSurveyId] ?? [];
return {
...prev,
[selectedSurveyId]: current.includes(elementId)
? current.filter((id) => id !== elementId)
: [...current, elementId],
};
});
};
const handleSelectAllElements = (surveyId: string) => {
const survey = surveys.find((s) => s.id === surveyId);
if (survey) {
setElementIdsBySurvey((prev) => ({
...prev,
[surveyId]: survey.elements.map((e) => e.id),
}));
}
};
const handleDeselectAllElements = () => {
if (!selectedSurveyId) return;
setElementIdsBySurvey((prev) => ({
...prev,
[selectedSurveyId]: [],
}));
};
const handleUpdate = async () => {
if (!connector || !connectorName.trim()) return;
const surveyMappings = Object.entries(elementIdsBySurvey)
.filter(([, ids]) => ids.length > 0)
.map(([surveyId, elementIds]) => ({ surveyId, elementIds }));
const handleUpdateFormbricksConnector = async (values: TFormbricksConnectorForm) => {
if (connector?.type !== "formbricks_survey") return;
setIsUpdating(true);
await onUpdateConnector({
connectorId: connector.id,
workspaceId: connector.workspaceId,
name: connectorName.trim(),
surveyMappings:
connector.type === "formbricks" && surveyMappings.length > 0 ? surveyMappings : undefined,
fieldMappings: connector.type !== "formbricks" && mappings.length > 0 ? mappings : undefined,
name: values.sourceName.trim(),
surveyMappings: [{ surveyId: values.surveyId, elementIds: values.selectedQuestionIds }],
fieldMappings: undefined,
});
setIsUpdating(false);
handleOpenChange(false);
};
const assignedDirectoryName =
directories.find((d) => d.id === connector?.feedbackRecordDirectoryId)?.name ??
connector?.feedbackRecordDirectoryId ??
"—";
const handleUpdateCsvConnector = async () => {
if (connector?.type !== "csv" || !isConnectorNameValid(csvConnectorName)) return;
setIsUpdating(true);
await onUpdateConnector({
connectorId: connector.id,
workspaceId: connector.workspaceId,
name: csvConnectorName.trim(),
surveyMappings: undefined,
fieldMappings: mappings.length > 0 ? mappings : undefined,
});
setIsUpdating(false);
handleOpenChange(false);
};
const saveChangesDisbaled = useMemo(() => {
const handleFormbricksQuestionToggle = (questionId: string) => {
const nextSelection = toggleQuestionId(formbricksForm.getValues("selectedQuestionIds"), questionId);
formbricksForm.setValue("selectedQuestionIds", nextSelection, {
shouldDirty: true,
shouldValidate: true,
});
};
const saveChangesDisabled = useMemo(() => {
if (!connector) return true;
if (!connectorName.trim()) return true;
if (isUpdating) return true;
if (connector.type === "formbricks") {
return !Object.values(elementIdsBySurvey).some((ids) => ids.length > 0);
if (connector.type === "formbricks_survey") {
return (
!isConnectorNameValid(formbricksValues.sourceName ?? "") ||
!formbricksValues.surveyId ||
!formbricksValues.selectedQuestionIds?.length
);
}
if (connector.type === "csv") {
return !allRequiredMapped;
return !isConnectorNameValid(csvConnectorName) || !areAllRequiredFieldsMapped(mappings);
}
}, [allRequiredMapped, connector, connectorName, elementIdsBySurvey]);
return true;
}, [connector, csvConnectorName, formbricksValues, isUpdating, mappings]);
if (!connector) return null;
@@ -233,53 +237,109 @@ export const EditConnectorModal = ({
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex items-center gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3">
{getConnectorIcon(connector.type)}
<div>
<p className="text-sm font-medium text-slate-900">
{t(getConnectorTypeLabelKey(connector.type))}
</p>
<p className="text-xs text-slate-500">{t("workspace.unify.source_type_cannot_be_changed")}</p>
</div>
</div>
{connector.type === "formbricks_survey" ? (
<FormProvider {...formbricksForm}>
<form className="space-y-4">
<FormField
control={formbricksForm.control}
name="sourceName"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.source_name")}</FormLabel>
<FormControl>
<Input
value={field.value}
onChange={field.onChange}
placeholder={t("workspace.unify.enter_name_for_source")}
/>
</FormControl>
<FormError />
</FormItem>
)}
/>
<div className="space-y-2">
<Label htmlFor="editConnectorName">{t("workspace.unify.source_name")}</Label>
<Input
id="editConnectorName"
value={connectorName}
onChange={(e) => setConnectorName(e.target.value)}
placeholder={t("workspace.unify.enter_name_for_source")}
/>
</div>
<FormField
control={formbricksForm.control}
name="surveyId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("workspace.unify.select_survey")}</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange} disabled>
<SelectTrigger>
<SelectValue placeholder={t("workspace.unify.select_survey")} />
</SelectTrigger>
<SelectContent>
{selectedSurvey && (
<SelectItem key={selectedSurvey.id} value={selectedSurvey.id}>
{selectedSurvey.name}
</SelectItem>
)}
{!selectedSurvey && field.value && (
<SelectItem value={field.value}>{field.value}</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>
<FormError />
</FormItem>
)}
/>
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
{t("workspace.unify.records_will_go_to")}{" "}
<span className="font-medium text-slate-900">{assignedDirectoryName}</span>
<p className="mt-1 text-xs text-slate-400">{t("workspace.unify.frd_cannot_be_changed")}</p>
</div>
{connector.type === "formbricks" ? (
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<FormbricksSurveySelector
surveys={surveys}
selectedSurveyId={selectedSurveyId}
selectedElementIds={selectedElementIds}
onSurveySelect={handleSurveySelect}
onElementToggle={handleElementToggle}
onSelectAllElements={handleSelectAllElements}
onDeselectAllElements={handleDeselectAllElements}
/>
</div>
<FormField
control={formbricksForm.control}
name="selectedQuestionIds"
render={() => (
<FormItem>
<FormLabel>{t("workspace.unify.select_questions")}</FormLabel>
<FormControl>
<div>
<FormbricksQuestionList
survey={selectedSurvey}
selectedQuestionIds={selectedQuestionIds}
onQuestionToggle={handleFormbricksQuestionToggle}
/>
</div>
</FormControl>
<FormError />
</FormItem>
)}
/>
</form>
</FormProvider>
) : (
<div className="max-h-[40vh] overflow-y-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
<MappingUI
sourceFields={sourceFields}
mappings={mappings}
onMappingsChange={setMappings}
connectorType={connector.type}
/>
</div>
<>
<div className="flex items-center gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3">
{getConnectorIcon(connector.type, "h-5 w-5 text-slate-500")}
<div>
<p className="text-sm font-medium text-slate-900">
{t(getConnectorTypeLabelKey(connector.type))}
</p>
<p className="text-xs text-slate-500">
{t("workspace.unify.source_type_cannot_be_changed")}
</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="editConnectorName">{t("workspace.unify.source_name")}</Label>
<Input
id="editConnectorName"
value={csvConnectorName}
onChange={(event) => setCsvConnectorName(event.target.value)}
placeholder={t("workspace.unify.enter_name_for_source")}
/>
</div>
<div className="max-h-[40vh] overflow-y-auto rounded-lg border border-slate-200 p-4">
<MappingUI
sourceFields={sourceFields}
mappings={mappings}
onMappingsChange={setMappings}
connectorType={connector.type}
/>
</div>
</>
)}
</div>
@@ -294,7 +354,13 @@ export const EditConnectorModal = ({
{t("workspace.unify.import_feedback")}
</Button>
)}
<Button onClick={handleUpdate} disabled={saveChangesDisbaled}>
<Button
onClick={
connector.type === "formbricks_survey"
? () => void formbricksForm.handleSubmit(handleUpdateFormbricksConnector)()
: handleUpdateCsvConnector
}
disabled={saveChangesDisabled}>
{t("workspace.unify.save_changes")}
</Button>
</DialogFooter>
@@ -0,0 +1,80 @@
"use client";
import { useTranslation } from "react-i18next";
import { UNSUPPORTED_CONNECTOR_ELEMENT_TYPES } from "@formbricks/types/connector";
import { getTSurveyElementTypeEnumName } from "@/modules/survey/lib/elements";
import { Checkbox } from "@/modules/ui/components/checkbox";
import { Label } from "@/modules/ui/components/label";
import { TUnifySurvey } from "../types";
interface FormbricksQuestionListProps {
survey: TUnifySurvey | null;
selectedQuestionIds: string[];
onQuestionToggle: (questionId: string) => void;
}
const isUnsupportedElementType = (type: string): boolean =>
(UNSUPPORTED_CONNECTOR_ELEMENT_TYPES as readonly string[]).includes(type);
export const FormbricksQuestionList = ({
survey,
selectedQuestionIds,
onQuestionToggle,
}: Readonly<FormbricksQuestionListProps>) => {
const { t } = useTranslation();
if (!survey) {
return (
<div className="rounded-md border border-dashed border-slate-300 p-3">
<p className="text-sm text-slate-500">{t("workspace.unify.select_a_survey_to_see_questions")}</p>
</div>
);
}
if (survey.elements.length === 0) {
return (
<div className="rounded-md border border-dashed border-slate-300 p-3">
<p className="text-sm text-slate-500">{t("workspace.unify.survey_has_no_questions")}</p>
</div>
);
}
return (
<div className="max-h-64 space-y-2 overflow-y-auto rounded-md border border-slate-200 p-3">
{survey.elements.map((element) => {
const unsupported = isUnsupportedElementType(element.type);
const isChecked = selectedQuestionIds.includes(element.id);
const elementTypeLabel = getTSurveyElementTypeEnumName(element.type, t) ?? element.type;
const inputId = `connector-question-${element.id}`;
return (
<div
key={element.id}
className={`flex items-start gap-3 rounded-md border border-slate-100 p-2 ${
unsupported ? "opacity-60" : ""
}`}>
<Checkbox
id={inputId}
checked={!unsupported && isChecked}
disabled={unsupported}
onCheckedChange={() => {
if (!unsupported) {
onQuestionToggle(element.id);
}
}}
/>
<div className="space-y-0.5">
<Label htmlFor={inputId} className={unsupported ? "cursor-not-allowed" : "cursor-pointer"}>
{element.headline}
</Label>
<p className="text-xs text-slate-500">{elementTypeLabel}</p>
{unsupported && (
<p className="text-xs text-slate-500">{t("workspace.unify.question_type_not_supported")}</p>
)}
</div>
</div>
);
})}
</div>
);
};
@@ -1,233 +0,0 @@
"use client";
import { CheckIcon, ChevronRightIcon, FileTextIcon, MessageSquareTextIcon, StarIcon } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import { UNSUPPORTED_CONNECTOR_ELEMENT_TYPES } from "@formbricks/types/connector";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
import { getTSurveyElementTypeEnumName } from "@/modules/survey/lib/elements";
import { Badge } from "@/modules/ui/components/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { TUnifySurvey } from "../types";
interface FormbricksSurveySelectorProps {
surveys: TUnifySurvey[];
selectedSurveyId: string | null;
selectedElementIds: string[];
onSurveySelect: (surveyId: string | null) => void;
onElementToggle: (elementId: string) => void;
onSelectAllElements: (surveyId: string) => void;
onDeselectAllElements: () => void;
}
const getElementIcon = (type: TSurveyElementTypeEnum) => {
switch (type) {
case "openText":
return <MessageSquareTextIcon className="h-4 w-4 text-slate-500" />;
case "rating":
case "nps":
return <StarIcon className="h-4 w-4 text-amber-500" />;
default:
return <FileTextIcon className="h-4 w-4 text-slate-500" />;
}
};
const isUnsupportedType = (type: TSurveyElementTypeEnum): boolean => {
return UNSUPPORTED_CONNECTOR_ELEMENT_TYPES.includes(type);
};
export const FormbricksSurveySelector = ({
surveys,
selectedSurveyId,
selectedElementIds,
onSurveySelect,
onElementToggle,
onSelectAllElements,
onDeselectAllElements,
}: FormbricksSurveySelectorProps) => {
const { t } = useTranslation();
const selectedSurvey = surveys.find((s) => s.id === selectedSurveyId);
const supportedElements = selectedSurvey?.elements.filter((e) => !isUnsupportedType(e.type)) ?? [];
const allSupportedSelected =
supportedElements.length > 0 && supportedElements.every((e) => selectedElementIds.includes(e.id));
const handleSurveyClick = (survey: TUnifySurvey) => {
if (selectedSurveyId !== survey.id) {
onSurveySelect(survey.id);
}
};
const handleSelectAllSupported = (surveyId: string) => {
onSelectAllElements(surveyId);
};
const getStatusBadge = (status: TUnifySurvey["status"]) => {
switch (status) {
case "active":
return <Badge text={t("workspace.unify.status_active")} type="success" size="tiny" />;
case "paused":
return <Badge text={t("workspace.unify.status_paused")} type="warning" size="tiny" />;
case "draft":
return <Badge text={t("workspace.unify.status_draft")} type="gray" size="tiny" />;
case "completed":
return <Badge text={t("workspace.unify.status_completed")} type="gray" size="tiny" />;
default:
return null;
}
};
const getSupportedElementCount = (survey: TUnifySurvey) =>
survey.elements.filter((e) => !isUnsupportedType(e.type)).length;
const getElementButtonClassName = (unsupported: boolean, isSelected: boolean): string => {
if (unsupported) return "cursor-not-allowed border-slate-100 bg-slate-50 opacity-50";
if (isSelected) return "border-green-300 bg-green-50";
return "border-slate-200 bg-white hover:border-slate-300";
};
const getCheckboxClassName = (unsupported: boolean, isSelected: boolean): string => {
if (unsupported) return "border border-slate-200 bg-slate-100";
if (isSelected) return "bg-green-500 text-white";
return "border border-slate-300 bg-white";
};
const renderElementPanel = () => {
if (!selectedSurvey) {
return (
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed border-slate-300 bg-slate-50">
<p className="text-sm text-slate-500">{t("workspace.unify.select_a_survey_to_see_questions")}</p>
</div>
);
}
if (selectedSurvey.elements.length === 0) {
return (
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed border-slate-300 bg-slate-50">
<p className="text-sm text-slate-500">{t("workspace.unify.survey_has_no_questions")}</p>
</div>
);
}
return (
<div className="space-y-2 overflow-y-auto pr-1">
<TooltipProvider delayDuration={200}>
{selectedSurvey.elements.map((element) => {
const isSelected = selectedElementIds.includes(element.id);
const unsupported = isUnsupportedType(element.type);
const button = (
<button
key={element.id}
type="button"
disabled={unsupported}
onClick={() => onElementToggle(element.id)}
className={`flex w-full items-center gap-3 rounded-lg border p-3 text-left transition-colors ${getElementButtonClassName(unsupported, isSelected)}`}>
<div
className={`flex h-5 w-5 items-center justify-center rounded ${getCheckboxClassName(unsupported, isSelected)}`}>
{isSelected && !unsupported && <CheckIcon className="h-3 w-3" />}
</div>
<div className="flex items-center gap-2">{getElementIcon(element.type)}</div>
<div className="flex-1">
<p className={`text-sm ${unsupported ? "text-slate-400" : "text-slate-900"}`}>
{element.headline}
</p>
<span className={`text-xs ${unsupported ? "text-slate-300" : "text-slate-500"}`}>
{getTSurveyElementTypeEnumName(element.type, t) ?? element.type}
</span>
</div>
</button>
);
if (unsupported) {
return (
<Tooltip key={element.id}>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>{t("workspace.unify.question_type_not_supported")}</TooltipContent>
</Tooltip>
);
}
return button;
})}
</TooltipProvider>
{selectedElementIds.length > 0 && (
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
<p className="text-xs text-blue-700">
<Trans
i18nKey={
selectedElementIds.length === 1
? "workspace.unify.question_selected"
: "workspace.unify.questions_selected"
}
values={{ count: selectedElementIds.length }}
components={{ strong: <strong /> }}
/>
</p>
</div>
)}
</div>
);
};
return (
<div className="grid h-[50vh] grid-cols-2 gap-6">
{/* Left: Survey List */}
<div className="flex flex-col gap-3 overflow-hidden">
<h4 className="shrink-0 text-sm font-medium text-slate-700">{t("workspace.unify.select_survey")}</h4>
<div className="space-y-2 overflow-y-auto pr-1">
{surveys.length === 0 ? (
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-slate-300 bg-slate-50">
<p className="text-sm text-slate-500">{t("workspace.unify.no_surveys_found")}</p>
</div>
) : (
surveys.map((survey) => {
const isSelected = selectedSurveyId === survey.id;
return (
<div key={survey.id}>
<button
type="button"
onClick={() => handleSurveyClick(survey)}
className={`flex w-full items-center gap-3 rounded-lg border bg-white p-3 text-left transition-colors ${
isSelected ? "border-brand-dark bg-slate-50" : "border-slate-200 hover:border-slate-300"
}`}>
<div className="min-w-0 flex-1 space-y-1">
<div>{getStatusBadge(survey.status)}</div>
<span className="block truncate text-sm font-medium text-slate-900">{survey.name}</span>
<p className="text-xs text-slate-500">
{t("workspace.unify.n_supported_questions", {
count: getSupportedElementCount(survey),
})}
</p>
</div>
{isSelected && <ChevronRightIcon className="h-5 w-5 shrink-0 text-brand-dark" />}
</button>
</div>
);
})
)}
</div>
</div>
{/* Right: Element Selection */}
<div className="flex flex-col gap-3 overflow-hidden">
<div className="flex shrink-0 items-center justify-between">
<h4 className="text-sm font-medium text-slate-700">{t("workspace.unify.select_questions")}</h4>
{selectedSurvey && supportedElements.length > 0 && (
<button
type="button"
onClick={() =>
allSupportedSelected ? onDeselectAllElements() : handleSelectAllSupported(selectedSurvey.id)
}
className="text-xs text-slate-500 hover:text-slate-700">
{allSupportedSelected ? t("workspace.unify.deselect_all") : t("workspace.unify.select_all")}
</button>
)}
</div>
{renderElementPanel()}
</div>
</div>
);
};
@@ -1,42 +1,6 @@
import { notFound } from "next/navigation";
import { getConnectorsWithMappings } from "@/lib/connector/service";
import { getSurveys } from "@/lib/survey/service";
import { getTranslate } from "@/lingodotdev/server";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
import { ConnectorsSection } from "./components/connectors-page-client";
import { transformToUnifySurvey } from "./lib";
import { redirect } from "next/navigation";
export default async function UnifySourcesPage(props: { params: Promise<{ workspaceId: string }> }) {
const t = await getTranslate();
const params = await props.params;
const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session } =
await getWorkspaceAuth(params.workspaceId);
if (!session) {
throw new Error(t("common.session_not_found"));
}
const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess;
if (!hasAccess) {
return notFound();
}
const [connectors, surveys, directories] = await Promise.all([
getConnectorsWithMappings(params.workspaceId),
getSurveys(params.workspaceId),
getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId),
]);
const unifySurveys = surveys.map(transformToUnifySurvey);
return (
<ConnectorsSection
workspaceId={params.workspaceId}
initialConnectors={connectors}
initialSurveys={unifySurveys}
directories={directories}
/>
);
redirect(`/workspaces/${params.workspaceId}/feedback-sources`);
}
@@ -80,6 +80,14 @@ export const FEEDBACK_RECORD_FIELDS: TTargetField[] = [
required: false,
description: "Tenant/organization identifier for multi-tenant deployments",
},
{
id: "submission_id",
name: "Submission ID",
type: "string",
required: false,
description:
"Optional. Map to a stable column (e.g. order_id, ticket_id) to enable idempotent re-imports. Auto-generated UUID per row if unmapped.",
},
{
id: "source_id",
name: "Source ID",
@@ -210,3 +218,12 @@ export const createFeedbackCSVDataSchema = (t: TFunction) =>
export type TFeedbackCSVData = z.infer<ReturnType<typeof createFeedbackCSVDataSchema>>;
export type TCreateConnectorStep = "selectType" | "mapping";
export const ZFormbricksConnectorForm = z.object({
sourceName: z.string().trim().min(1),
surveyId: z.string().min(1),
selectedQuestionIds: z.array(z.string()).min(1),
importHistorical: z.boolean(),
});
export type TFormbricksConnectorForm = z.infer<typeof ZFormbricksConnectorForm>;
@@ -1,15 +1,24 @@
import { describe, expect, test } from "vitest";
import { MAX_CSV_VALUES, TSourceField } from "./types";
import { getConnectorOptions, parseCSVColumnsToFields, validateCsvFile } from "./utils";
import { MAX_CSV_VALUES, TFieldMapping, TSourceField } from "./types";
import {
areAllRequiredFieldsMapped,
getConnectorOptions,
isConnectorNameValid,
parseCSVColumnsToFields,
toggleQuestionId,
validateCsvFile,
} from "./utils";
const mockT = (key: string) => key;
describe("getConnectorOptions", () => {
test("returns formbricks and csv options", () => {
test("returns formbricks, csv, api ingestion, and mcp options", () => {
const options = getConnectorOptions(mockT as never);
expect(options).toHaveLength(2);
expect(options[0].id).toBe("formbricks");
expect(options).toHaveLength(4);
expect(options[0].id).toBe("formbricks_survey");
expect(options[1].id).toBe("csv");
expect(options[2].id).toBe("api_ingestion");
expect(options[3].id).toBe("feedback_record_mcp");
});
test("both options are enabled by default", () => {
@@ -23,6 +32,10 @@ describe("getConnectorOptions", () => {
expect(options[0].description).toBe("workspace.unify.source_connect_formbricks_description");
expect(options[1].name).toBe("workspace.unify.csv_import");
expect(options[1].description).toBe("workspace.unify.source_connect_csv_description");
expect(options[2].name).toBe("workspace.unify.api_ingestion");
expect(options[2].description).toBe("workspace.unify.api_ingestion_settings_description");
expect(options[3].name).toBe("workspace.unify.feedback_record_mcp");
expect(options[3].description).toBe("workspace.unify.source_connect_feedback_record_mcp_description");
});
});
@@ -109,3 +122,111 @@ describe("validateCsvFile", () => {
expect(result).toEqual({ valid: false, error: "workspace.unify.csv_files_only" });
});
});
describe("isConnectorNameValid", () => {
test("returns true for non-empty name", () => {
expect(isConnectorNameValid("My Connector")).toBe(true);
});
test("returns false for empty string", () => {
expect(isConnectorNameValid("")).toBe(false);
});
test("returns false for whitespace-only string", () => {
expect(isConnectorNameValid(" ")).toBe(false);
expect(isConnectorNameValid("\t\n ")).toBe(false);
});
test("returns true for name with surrounding whitespace", () => {
expect(isConnectorNameValid(" name ")).toBe(true);
});
test("returns true for single character", () => {
expect(isConnectorNameValid("a")).toBe(true);
});
});
describe("areAllRequiredFieldsMapped", () => {
const requiredMappings: TFieldMapping[] = [
{ targetFieldId: "collected_at", sourceFieldId: "ts" },
{ targetFieldId: "source_type", staticValue: "csv" },
{ targetFieldId: "field_id", sourceFieldId: "qid" },
{ targetFieldId: "field_type", staticValue: "text" },
];
test("returns true when all required fields have a sourceFieldId or staticValue", () => {
expect(areAllRequiredFieldsMapped(requiredMappings)).toBe(true);
});
test("returns false when a required field is missing entirely", () => {
const missing = requiredMappings.slice(0, 3);
expect(areAllRequiredFieldsMapped(missing)).toBe(false);
});
test("returns false when a required mapping has neither sourceFieldId nor staticValue", () => {
const incomplete: TFieldMapping[] = [...requiredMappings.slice(0, 3), { targetFieldId: "field_type" }];
expect(areAllRequiredFieldsMapped(incomplete)).toBe(false);
});
test("ignores mappings for non-required target fields", () => {
const withOptionals: TFieldMapping[] = [
...requiredMappings,
{ targetFieldId: "tenant_id", sourceFieldId: "tenant" },
{ targetFieldId: "unknown_field", sourceFieldId: "anything" },
];
expect(areAllRequiredFieldsMapped(withOptionals)).toBe(true);
});
test("returns false for empty mappings array", () => {
expect(areAllRequiredFieldsMapped([])).toBe(false);
});
test("treats empty staticValue and missing sourceFieldId as unmapped", () => {
const incomplete: TFieldMapping[] = [
{ targetFieldId: "collected_at", sourceFieldId: "ts" },
{ targetFieldId: "source_type", sourceFieldId: "", staticValue: "" },
{ targetFieldId: "field_id", sourceFieldId: "qid" },
{ targetFieldId: "field_type", staticValue: "text" },
];
expect(areAllRequiredFieldsMapped(incomplete)).toBe(false);
});
test("counts required field as mapped when only staticValue is set", () => {
const onlyStatic: TFieldMapping[] = [
{ targetFieldId: "collected_at", staticValue: "2026-01-01" },
{ targetFieldId: "source_type", staticValue: "csv" },
{ targetFieldId: "field_id", staticValue: "id" },
{ targetFieldId: "field_type", staticValue: "text" },
];
expect(areAllRequiredFieldsMapped(onlyStatic)).toBe(true);
});
});
describe("toggleQuestionId", () => {
test("adds id when not present", () => {
expect(toggleQuestionId(["a", "b"], "c")).toEqual(["a", "b", "c"]);
});
test("removes id when present", () => {
expect(toggleQuestionId(["a", "b", "c"], "b")).toEqual(["a", "c"]);
});
test("adds to empty selection", () => {
expect(toggleQuestionId([], "x")).toEqual(["x"]);
});
test("returns empty when removing the only id", () => {
expect(toggleQuestionId(["only"], "only")).toEqual([]);
});
test("does not mutate the input array", () => {
const input = ["a", "b"];
const result = toggleQuestionId(input, "c");
expect(input).toEqual(["a", "b"]);
expect(result).not.toBe(input);
});
test("removes only the matching id when duplicates exist", () => {
expect(toggleQuestionId(["a", "b", "a"], "a")).toEqual(["b"]);
});
});
@@ -1,9 +1,11 @@
import { TFunction } from "i18next";
import { THubFieldType } from "@formbricks/types/connector";
import { TConnectorType, THubFieldType } from "@formbricks/types/connector";
import { FEEDBACK_RECORD_FIELDS, MAX_CSV_VALUES, TFieldMapping, TSourceField } from "./types";
export type TConnectorOptionId = TConnectorType | "api_ingestion" | "feedback_record_mcp";
export interface TConnectorOption {
id: string;
id: TConnectorOptionId;
name: string;
description: string;
disabled: boolean;
@@ -12,7 +14,7 @@ export interface TConnectorOption {
export const getConnectorOptions = (t: TFunction): TConnectorOption[] => [
{
id: "formbricks",
id: "formbricks_survey",
name: t("workspace.unify.formbricks_surveys"),
description: t("workspace.unify.source_connect_formbricks_description"),
disabled: false,
@@ -23,6 +25,18 @@ export const getConnectorOptions = (t: TFunction): TConnectorOption[] => [
description: t("workspace.unify.source_connect_csv_description"),
disabled: false,
},
{
id: "api_ingestion",
name: t("workspace.unify.api_ingestion"),
description: t("workspace.unify.api_ingestion_settings_description"),
disabled: false,
},
{
id: "feedback_record_mcp",
name: t("workspace.unify.feedback_record_mcp"),
description: t("workspace.unify.source_connect_feedback_record_mcp_description"),
disabled: false,
},
];
export const parseCSVColumnsToFields = (columns: string): TSourceField[] => {
@@ -76,6 +90,32 @@ export const validateEnumMappings = (
return errors;
};
export const isConnectorNameValid = (name: string): boolean => name.trim().length > 0;
export const areAllRequiredFieldsMapped = (mappings: TFieldMapping[]): boolean => {
const requiredFieldIds = new Set(
FEEDBACK_RECORD_FIELDS.filter((field) => field.required).map((field) => field.id)
);
for (const mapping of mappings) {
if (!requiredFieldIds.has(mapping.targetFieldId)) {
continue;
}
if (mapping.sourceFieldId || mapping.staticValue) {
requiredFieldIds.delete(mapping.targetFieldId);
}
}
return requiredFieldIds.size === 0;
};
export const toggleQuestionId = (currentSelection: string[], questionId: string): string[] => {
return currentSelection.includes(questionId)
? currentSelection.filter((id) => id !== questionId)
: [...currentSelection, questionId];
};
export const validateCsvFile = (
file: File,
t: TFunction
@@ -1,318 +0,0 @@
import { PipelineTriggers, Webhook } from "@prisma/client";
import { headers } from "next/headers";
import { v7 as uuidv7 } from "uuid";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { handleConnectorPipeline } from "@/lib/connector/pipeline-handler";
import { CRON_SECRET, POSTHOG_KEY } from "@/lib/constants";
import { generateStandardWebhookSignature } from "@/lib/crypto";
import { getIntegrations } from "@/lib/integration/service";
import { getOrganization } from "@/lib/organization/service";
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey, updateSurvey } from "@/lib/survey/service";
import { convertDatesInObject } from "@/lib/time";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
import { queueAuditEvent } from "@/modules/ee/audit-logs/lib/handler";
import { TAuditStatus, UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
import { recordResponseCreatedMeterEvent } from "@/modules/ee/billing/lib/metering";
import { sendResponseFinishedEmail } from "@/modules/email";
import { handleIntegrations } from "@/modules/response-pipeline/lib/handle-integrations";
import { captureSurveyResponsePostHogEvent } from "@/modules/response-pipeline/lib/posthog";
import { sendTelemetryEvents } from "@/modules/response-pipeline/lib/telemetry";
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";
export const POST = async (request: Request) => {
const requestHeaders = await headers();
// Check authentication
if (requestHeaders.get("x-api-key") !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}
const jsonInput = await request.json();
const convertedJsonInput = convertDatesInObject(
jsonInput,
new Set(["contactAttributes", "variables", "data", "meta"])
);
const inputValidation = ZPipelineInput.safeParse(convertedJsonInput);
if (!inputValidation.success) {
logger.error(
{ error: inputValidation.error, url: request.url },
"Error in POST /api/(internal)/pipeline"
);
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
const { workspaceId, surveyId, event, response } = inputValidation.data;
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
const organization = await getOrganization(organizationId);
if (!organization) {
throw new ResourceNotFoundError("Organization", "Organization not found");
}
// Fetch survey for webhook payload
const survey = await getSurvey(surveyId);
if (!survey) {
logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`);
return responses.notFoundResponse("Survey", surveyId, true);
}
if (survey.workspaceId !== workspaceId) {
logger.error(
{ url: request.url, surveyId, workspaceId, surveyWorkspaceId: survey.workspaceId },
`Survey ${surveyId} does not belong to workspace ${workspaceId}`
);
return responses.badRequestResponse("Survey not found in this workspace");
}
// Fetch webhooks
const getWebhooksForPipeline = async (workspaceId: string, event: PipelineTriggers, surveyId: string) => {
const webhooks = await prisma.webhook.findMany({
where: {
workspaceId,
triggers: { has: event },
OR: [{ surveyIds: { has: surveyId } }, { surveyIds: { isEmpty: true } }],
},
});
return webhooks;
};
const webhooks: Webhook[] = await getWebhooksForPipeline(workspaceId, event, surveyId);
// Prepare webhook and email promises
// Fetch with timeout of 5 seconds to prevent hanging
const fetchWithTimeout = (url: string, options: RequestInit, timeout: number = 5000): Promise<Response> => {
return Promise.race([
fetch(url, options),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
]);
};
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,
status: survey.status,
createdAt: survey.createdAt,
updatedAt: survey.updatedAt,
},
},
});
// Generate Standard Webhooks headers
const webhookMessageId = uuidv7();
const webhookTimestamp = Math.floor(Date.now() / 1000);
const requestHeaders: Record<string, string> = {
"content-type": "application/json",
"webhook-id": webhookMessageId,
"webhook-timestamp": webhookTimestamp.toString(),
};
// Add signature if webhook has a secret configured
if (webhook.secret) {
requestHeaders["webhook-signature"] = generateStandardWebhookSignature(
webhookMessageId,
webhookTimestamp,
body,
webhook.secret
);
}
return validateWebhookUrl(webhook.url)
.then(() =>
fetchWithTimeout(webhook.url, {
method: "POST",
headers: requestHeaders,
body,
})
)
.catch((error) => {
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
});
});
if (event === "responseFinished") {
// Handle connector pipeline for Hub integration (only on responseFinished to avoid duplicates)
// This sends response data to the Hub for configured connectors
try {
await handleConnectorPipeline(response, survey, workspaceId);
} catch (error) {
// Log but don't throw - connector failures shouldn't break the main pipeline
logger.error({ error, surveyId, responseId: response.id }, "Connector pipeline failed");
}
// Fetch integrations and responseCount in parallel
const [integrations, responseCount] = await Promise.all([
getIntegrations(workspaceId),
getResponseCountBySurveyId(surveyId),
]);
if (integrations.length > 0) {
await handleIntegrations(integrations, inputValidation.data, survey);
}
// Fetch users with notifications in a single query
const usersWithNotifications = await prisma.user.findMany({
where: {
memberships: {
some: {
organization: {
workspaces: {
some: {
id: workspaceId,
},
},
},
},
},
OR: [
{
memberships: {
every: {
role: {
in: ["owner", "manager"],
},
},
},
},
{
teamUsers: {
some: {
team: {
workspaceTeams: {
some: {
workspace: {
id: workspaceId,
},
},
},
},
},
},
},
],
notificationSettings: {
path: ["alert", surveyId],
equals: true,
},
},
select: { email: true, locale: true },
});
if (survey.followUps?.length > 0) {
// send follow up emails
const followUpsResult = await sendFollowUpsForResponse(response.id);
if (!followUpsResult.ok) {
const { error: followUpsError } = followUpsResult;
if (followUpsError.code !== FollowUpSendError.FOLLOW_UP_NOT_ALLOWED) {
logger.error({ error: followUpsError }, `Failed to send follow-up emails for survey ${surveyId}`);
}
}
}
const emailPromises = usersWithNotifications.map((user) =>
sendResponseFinishedEmail(user.email, user.locale, workspaceId, survey, response, responseCount).catch(
(error) => {
logger.error(
{ error, url: request.url, userEmail: user.email },
`Failed to send email to ${user.email}`
);
}
)
);
// Update survey status if necessary
if (survey.autoComplete && responseCount >= survey.autoComplete) {
let logStatus: TAuditStatus = "success";
try {
await updateSurvey({
...survey,
status: "completed",
});
} catch (error) {
logStatus = "failure";
logger.error(
{ error, url: request.url, surveyId },
`Failed to update survey ${surveyId} status to completed`
);
} finally {
await queueAuditEvent({
status: logStatus,
action: "updated",
targetType: "survey",
userId: UNKNOWN_DATA,
userType: "system",
targetId: survey.id,
organizationId: organization.id,
newObject: {
status: "completed",
},
});
}
}
// Await webhook and email promises with allSettled to prevent early rejection
const results = await Promise.allSettled([...webhookPromises, ...emailPromises]);
results.forEach((result) => {
if (result.status === "rejected") {
logger.error({ error: result.reason, url: request.url }, "Promise rejected");
}
});
} else {
// Await webhook promises if no emails are sent (with allSettled to prevent early rejection)
const results = await Promise.allSettled(webhookPromises);
results.forEach((result) => {
if (result.status === "rejected") {
logger.error({ error: result.reason, url: request.url }, "Promise rejected");
}
});
}
if (event === "responseCreated") {
recordResponseCreatedMeterEvent({
stripeCustomerId: organization.billing.stripeCustomerId,
responseId: response.id,
createdAt: response.createdAt,
}).catch((error) => {
logger.error({ error, responseId: response.id }, "Failed to record response meter event");
});
if (POSTHOG_KEY) {
const responseCount = await getResponseCountBySurveyId(surveyId);
captureSurveyResponsePostHogEvent({
organizationId: organization.id,
surveyId,
surveyType: survey.type,
workspaceId,
responseCount,
});
}
// Send telemetry events
await sendTelemetryEvents();
}
return Response.json({ data: {} });
};
@@ -1,12 +0,0 @@
import { z } from "zod";
import { ZWebhook } from "@formbricks/database/zod/webhooks";
import { ZResponse } from "@formbricks/types/responses";
export const ZPipelineInput = z.object({
event: ZWebhook.shape.triggers.element,
response: ZResponse,
workspaceId: z.string(),
surveyId: z.string(),
});
export type TPipelineInput = z.infer<typeof ZPipelineInput>;
@@ -254,7 +254,7 @@ export const putResponseHandler = async ({
const { quotaFull, ...responseData } = updatedResponse;
sendToPipeline({
await sendToPipeline({
event: "responseUpdated",
workspaceId: survey.workspaceId,
surveyId: survey.id,
@@ -262,7 +262,7 @@ export const putResponseHandler = async ({
});
if (updatedResponse.finished) {
sendToPipeline({
await sendToPipeline({
event: "responseFinished",
workspaceId: survey.workspaceId,
surveyId: survey.id,
@@ -186,7 +186,7 @@ export const POST = withV1ApiWrapper({
const { quotaFull, ...responseData } = response;
sendToPipeline({
await sendToPipeline({
event: "responseCreated",
workspaceId,
surveyId: responseData.surveyId,
@@ -194,7 +194,7 @@ export const POST = withV1ApiWrapper({
});
if (responseInput.finished) {
sendToPipeline({
await sendToPipeline({
event: "responseFinished",
workspaceId,
surveyId: responseData.surveyId,
@@ -169,7 +169,7 @@ export const PUT = withV1ApiWrapper({
auditLog.newObject = updated;
}
sendToPipeline({
await sendToPipeline({
event: "responseUpdated",
workspaceId: result.survey.workspaceId,
surveyId: result.survey.id,
@@ -177,7 +177,7 @@ export const PUT = withV1ApiWrapper({
});
if (updated.finished) {
sendToPipeline({
await sendToPipeline({
event: "responseFinished",
workspaceId: result.survey.workspaceId,
surveyId: result.survey.id,
@@ -165,7 +165,7 @@ export const POST = withV1ApiWrapper({
auditLog.newObject = response;
}
sendToPipeline({
await sendToPipeline({
event: "responseCreated",
workspaceId: surveyResult.survey.workspaceId,
surveyId: response.surveyId,
@@ -173,7 +173,7 @@ export const POST = withV1ApiWrapper({
});
if (response.finished) {
sendToPipeline({
await sendToPipeline({
event: "responseFinished",
workspaceId: surveyResult.survey.workspaceId,
surveyId: response.surveyId,
@@ -237,7 +237,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
}
const { quotaFull, ...responseData } = createdResponse;
sendToPipeline({
await sendToPipeline({
event: "responseCreated",
workspaceId,
surveyId: responseData.surveyId,
@@ -245,7 +245,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
});
if (responseData.finished) {
sendToPipeline({
await sendToPipeline({
event: "responseFinished",
workspaceId,
surveyId: responseData.surveyId,
+58 -87
View File
@@ -1,113 +1,84 @@
import { PipelineTriggers } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TResponsePipelineJobData, getBackgroundJobProducer } from "@formbricks/jobs";
import { logger } from "@formbricks/logger";
import { TResponse } from "@formbricks/types/responses";
import { TPipelineInput } from "@/app/lib/types/pipelines";
import { sendToPipeline } from "./pipelines";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getJobsQueueingConfig } from "@/lib/jobs/config";
// Mock the constants module
vi.mock("@/lib/constants", () => ({
CRON_SECRET: "mocked-cron-secret",
WEBAPP_URL: "https://test.formbricks.com",
const mockEnqueueResponsePipeline = vi.fn();
vi.mock("@formbricks/jobs", () => ({
getBackgroundJobProducer: vi.fn(() => ({
enqueueResponsePipeline: mockEnqueueResponsePipeline,
})),
}));
vi.mock("@/lib/jobs/config", () => ({
getJobsQueueingConfig: vi.fn(),
}));
// Mock the logger
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe("sendToPipeline", () => {
const testData: TResponsePipelineJobData = {
event: PipelineTriggers.responseCreated,
surveyId: "cm8ckvchx000008lb710n0gdn",
workspaceId: "cm8cmp9hp000008jf7l570ml2",
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
};
describe("pipelines", () => {
// Reset mocks before each test
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getJobsQueueingConfig).mockReturnValue({
enabled: true,
redisUrl: "redis://localhost:6379",
});
});
// Clean up after each test
afterEach(() => {
vi.clearAllMocks();
});
test("sendToPipeline should call fetch with correct parameters", async () => {
// Mock the fetch implementation to return a successful response
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
test("enqueues the pipeline job through the BullMQ producer", async () => {
mockEnqueueResponsePipeline.mockResolvedValue({
jobId: "job-1",
jobName: "response-pipeline.process",
queueName: "background-jobs",
});
// Create sample data for testing
const testData: TPipelineInput = {
event: PipelineTriggers.responseCreated,
surveyId: "cm8ckvchx000008lb710n0gdn",
workspaceId: "cm8cnq2hp000008jf7l570abc",
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
};
// Call the function with test data
await sendToPipeline(testData);
// Check that fetch was called with the correct arguments
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith("https://test.formbricks.com/api/pipeline", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "mocked-cron-secret",
},
body: JSON.stringify({
workspaceId: testData.workspaceId,
surveyId: testData.surveyId,
expect(getBackgroundJobProducer).toHaveBeenCalledTimes(1);
expect(mockEnqueueResponsePipeline).toHaveBeenCalledWith(testData);
});
test("logs enqueue failures and rethrows", async () => {
const testError = new Error("Redis unavailable");
mockEnqueueResponsePipeline.mockRejectedValue(testError);
await expect(sendToPipeline(testData)).rejects.toThrow(testError);
expect(logger.error).toHaveBeenCalledWith(
{
error: testError,
event: testData.event,
response: testData.response,
}),
surveyId: testData.surveyId,
workspaceId: testData.workspaceId,
},
"Error queueing pipeline event"
);
});
test("throws when BullMQ queueing is disabled", async () => {
vi.mocked(getJobsQueueingConfig).mockReturnValue({
enabled: false,
redisUrl: null,
});
});
test("sendToPipeline should handle fetch errors", async () => {
// Mock fetch to throw an error
const testError = new Error("Network error");
mockFetch.mockRejectedValueOnce(testError);
// Create sample data for testing
const testData: TPipelineInput = {
event: PipelineTriggers.responseCreated,
surveyId: "cm8ckvchx000008lb710n0gdn",
workspaceId: "cm8cnq2hp000008jf7l570abc",
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
};
// Call the function
await sendToPipeline(testData);
// Check that the error was logged using logger
expect(logger.error).toHaveBeenCalledWith(testError, "Error sending event to pipeline");
});
test("sendToPipeline should throw error if CRON_SECRET is not set", async () => {
// For this test, we need to mock CRON_SECRET as undefined
// Let's use a more compatible approach to reset the mocks
const originalModule = await import("@/lib/constants");
const mockConstants = { ...originalModule, CRON_SECRET: undefined };
vi.doMock("@/lib/constants", () => mockConstants);
// Re-import the module to get the new mocked values
const { sendToPipeline: sendToPipelineNoSecret } = await import("./pipelines");
// Create sample data for testing
const testData: TPipelineInput = {
event: PipelineTriggers.responseCreated,
surveyId: "cm8ckvchx000008lb710n0gdn",
workspaceId: "cm8cnq2hp000008jf7l570abc",
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
};
// Expect the function to throw an error
await expect(sendToPipelineNoSecret(testData)).rejects.toThrow("CRON_SECRET is not set");
await expect(sendToPipeline(testData)).rejects.toThrow(
"BullMQ response pipeline queueing is not enabled"
);
expect(getBackgroundJobProducer).not.toHaveBeenCalled();
});
});
+17 -21
View File
@@ -1,25 +1,21 @@
import { TResponsePipelineJobData, getBackgroundJobProducer } from "@formbricks/jobs";
import { logger } from "@formbricks/logger";
import { TPipelineInput } from "@/app/lib/types/pipelines";
import { CRON_SECRET, WEBAPP_URL } from "@/lib/constants";
import { getJobsQueueingConfig } from "@/lib/jobs/config";
export const sendToPipeline = async ({ event, surveyId, workspaceId, response }: TPipelineInput) => {
if (!CRON_SECRET) {
throw new Error("CRON_SECRET is not set");
export const sendToPipeline = async (job: TResponsePipelineJobData): Promise<void> => {
try {
const jobsQueueingConfig = getJobsQueueingConfig();
if (!jobsQueueingConfig.enabled) {
throw new Error("BullMQ response pipeline queueing is not enabled");
}
const producer = getBackgroundJobProducer();
await producer.enqueueResponsePipeline(job);
} catch (error) {
logger.error(
{ error, event: job.event, surveyId: job.surveyId, workspaceId: job.workspaceId },
"Error queueing pipeline event"
);
throw error;
}
return fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": CRON_SECRET,
},
body: JSON.stringify({
workspaceId,
surveyId,
event,
response,
}),
}).catch((error) => {
logger.error(error, "Error sending event to pipeline");
});
};
-9
View File
@@ -1,9 +0,0 @@
import { PipelineTriggers } from "@prisma/client";
import { TResponse } from "@formbricks/types/responses";
export interface TPipelineInput {
event: PipelineTriggers;
response: TResponse;
workspaceId: string;
surveyId: string;
}
+3
View File
@@ -21,6 +21,9 @@ export const PostHogIdentify = ({ posthogKey, userId, email, name }: PostHogIden
defaults: "2026-01-30",
capture_exceptions: true,
debug: process.env.NODE_ENV === "development",
session_recording: {
blockSelector: "#chatwoot_live_chat_widget",
},
});
}
+98 -30
View File
@@ -98,6 +98,7 @@ checksums:
common/activity: 1948763de8e531483a798b68195e297e
common/add: 87c4a663507f2bcbbf79934af8164e13
common/add_action: 66fefc4dd6a7b939c2224272cf0d2669
common/add_chart: 0c8539d3ccc83fce87bb1e0dc3e30005
common/add_charts: c377a42e165e8ab67bfbb8ad72026dd8
common/add_existing_chart_description: b1292a1d6df2e03ad7b399689312c37f
common/add_filter: ed5d8e9bfcb05cd1e10e4c403befbae6
@@ -120,6 +121,7 @@ checksums:
common/apply_filters: 6543c1e80038b3da0f4a42848d08d4d1
common/archived: cf5127ecfd7e43a35466a1ba5fe16450
common/are_you_sure: 6d5cd13628a7887711fd0c29f1123652
common/ask: 24150ae04c60dcd8688d93a8a3a2d238
common/attributes: 86d0ae6fea0fbb119722ed3841f8385a
common/back: f541015a827e37cb3b1234e56bc2aa3c
common/billing: b01dbdd049ebbd4a349fa64d6ce65a3b
@@ -132,7 +134,7 @@ checksums:
common/change_workspace: 489cbcf7eef9b9b960e426fbf4da318f
common/chart: 6f4d9c56e45ceb8fc22d2f74454cd813
common/charts: 1da4564d89264c89de4ed28d7451b43e
common/choice_n: ee41eb382bae7289a221d959f3046965
common/choice_n: a6965b8fb3e479e94175b3826839d9ae
common/choices: 8a7a77a71ec6eebc363c5dc0f8490a4d
common/choose_organization: a8f5db68012323bfbb1a0ad0fb194603
common/choose_workspace: f9ed22d76c69cc75aa56cf3da3fa6320
@@ -145,7 +147,7 @@ checksums:
common/close: 2c2e22f8424a1031de89063bd0022e16
common/code: 343bc5386149b97cece2b093c39034b2
common/collapse_rows: 24988527f9180f37aa55d2aa183ccb21
common/column_n: 550955aee6a92d8ccc96989300add693
common/column_n: b98315f0e504fad7e784d77f153a7d9d
common/completed: 0e4bbce9985f25eb673d9a054c8d5334
common/configuration: e3ab18ebb36c218cd4897c620f5809ac
common/confirm: 90930b51154032f119fa75c1bd422d8b
@@ -185,6 +187,7 @@ checksums:
common/delete_what: 718ddfcc1dec7f3e8b67856fba838267
common/description: e17686a22ffad04cc7bb70524ed4478b
common/disable: 81b754fd7962e0bd9b6ba87f3972e7fc
common/disabled: 0889a3dfd914a7ef638611796b17bf72
common/disallow: 01c8ed3ce545ed836d3ccffc562c8a0c
common/discard: de83a114a79d086e372c43dbfe9f47b4
common/dismissed: f0e21b3fe28726c577a7238a63cc29c2
@@ -216,7 +219,7 @@ checksums:
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4
common/failed_to_parse_csv: 7a3d675ecbb3d15884faf1006a5752d6
common/field_placeholder: ec26d96643d86da164162204ec6c650f
common/field_placeholder: 1fedb1aab1a4d42ad49ddece6d8df372
common/filter: 626325a05e4c8800f7ede7012b0cadaf
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/first_name: cf040a5d6a9fd696be400380cc99f54b
@@ -304,6 +307,7 @@ checksums:
common/not_authenticated: fed6c62208524ea6782b5f9c07a95a4f
common/not_authorized: 4be80383fe1a6f52c61138f1aa8d01d4
common/not_connected: 91ebf07fff6b2ead94d85bd17212e0ba
common/not_set: 380482630d60ee2d1531b31246caa467
common/note: e0337f202c911423275f834edeffc54b
common/notifications: c52df856139b50dbb1cae7bfb1cf73bb
common/number: 2789f8391f63e7200a5521078aab017d
@@ -363,13 +367,14 @@ checksums:
common/report_survey: 147dd05db52e35f5d1f837460fb720f5
common/request_trial_license: 560df1240ef621f7c60d3f7d65422ccd
common/reset_to_default: 68ee98b46677392f44b505b268053b26
common/resize: 20887e5af5294f08bc72cdedeee6e7a8
common/response: c7a9d88269d8ff117abcbc0d97f88b2c
common/response_id: 73375099cc976dc7203b8e27f5f709e0
common/responses: 14bb6c69f906d7bbd1359f7ef1bb3c28
common/restart: bab6232e89f24e3129f8e48268739d5b
common/retry: 6e44d18639560596569a1278f9c83676
common/role: 53743bbb6ca938f5b893552e839d067f
common/row_n: eb5bb04b244fadd7a6962aa58bf6bd17
common/row_n: f90f7018a69f2d7025ad99a90bd23dc9
common/saas: f01686245bcfb35a3590ab56db677bdb
common/sales: 38758eb50094cd8190a71fe67be4d647
common/save: f7a2929f33bc420195e59ac5a8bcd454
@@ -404,6 +409,7 @@ checksums:
common/some_files_failed_to_upload: a0e26efeb29ae905257ecf93b112dff0
common/something_went_wrong: a3cd2f01c073f1f5ff436d4b132d39cf
common/something_went_wrong_please_try_again: c62a7718d9a1e9c4ffb707807550f836
common/soon: b12e79beb0aef9414a445a1b95dd4322
common/sort_by: 8adf3dbc5668379558957662f0c43563
common/start_free_trial: e346e4ed7d138dcc873db187922369da
common/status: 4e1fcce15854d824919b4a582c697c90
@@ -1617,7 +1623,7 @@ checksums:
workspace/analysis/charts/chart_type_bar: c11d460595d3ddfe8efd67ac068574c5
workspace/analysis/charts/chart_type_big_number: 9d17fb96241507c955dca25e143ae67a
workspace/analysis/charts/chart_type_line: f42dd53238ed4d44def306a61d47d5c4
workspace/analysis/charts/chart_type_not_supported: 7ff0afc493b36f3f3c12c7c230df9757
workspace/analysis/charts/chart_type_not_supported: c25334de42fd6192ff8355158865a3e8
workspace/analysis/charts/chart_type_pie: 068a797404233ccf68d07ad63af7b50c
workspace/analysis/charts/chart_updated_successfully: a2c210523902c726aa1328bbeda0b357
workspace/analysis/charts/configure_description: 2939321f78e4ffbc57b4259ddaddb09d
@@ -1655,6 +1661,7 @@ checksums:
workspace/analysis/charts/failed_to_execute_query: d1153133aa4cd3d1cd02e39942413168
workspace/analysis/charts/failed_to_load_chart: abea098fbf8e728f95414d3ae8bb63a4
workspace/analysis/charts/failed_to_load_chart_data: ea980a6d12b1b1efed90d991dd0dd0fd
workspace/analysis/charts/failed_to_load_dashboards: 876c54d9cc69ceda6f808231e2557eb2
workspace/analysis/charts/failed_to_save_chart: e237cf1a56a8f9ee30067fdb0757f7c5
workspace/analysis/charts/field: cfd632297d7809a3539e90c9cd4728d9
workspace/analysis/charts/field_label_average_score: 5b5aa7322549521d1e813b1c8312d443
@@ -1705,7 +1712,7 @@ checksums:
workspace/analysis/charts/no_valid_data_to_display: d1ba2b0686520c0a2c62ee73daa1c9c9
workspace/analysis/charts/not_contains: 5894f5474271b8902d7892e43500d227
workspace/analysis/charts/not_equals: 427715f1ea349965c36f5c628784eb08
workspace/analysis/charts/open_chart: bc3bed1517ad63c1bcccfbbc430ab333
workspace/analysis/charts/open_chart: 729a54bbc4bcb3f431865af5e5a50dd4
workspace/analysis/charts/open_options: 2c6a35fec9b9d008e41728594bcd07d7
workspace/analysis/charts/or_filter_logic: 0208d355f231c386b19390f0bea41b95
workspace/analysis/charts/original: 7e55782bdf7cb49f5616b326c003c278
@@ -1716,8 +1723,10 @@ checksums:
workspace/analysis/charts/please_select_dashboard: 8f062db96f815ed8268584dd8d292fa6
workspace/analysis/charts/predefined_measures: 7651141f62c991954edcff70899b2a8b
workspace/analysis/charts/preset: a17bb0bf56f3326c9567be3ea896ee19
workspace/analysis/charts/preview_chart: 8db30f87ba44165401f340a1ee7f549b
workspace/analysis/charts/query_executed_successfully: 9d6f9dad526fcfe0161757c2d2fe2c69
workspace/analysis/charts/reset_to_ai_suggestion: 51ced8dd7c0eea8b7fc4e08b35cfbf30
workspace/analysis/charts/save_and_add_to_dashboard: a76ed91c62dae10c5f8a9d48cbacd566
workspace/analysis/charts/save_chart: 2e4505f7bf3d1c35b0b37b1e9d3dc566
workspace/analysis/charts/save_chart_dialog_title: 2e4505f7bf3d1c35b0b37b1e9d3dc566
workspace/analysis/charts/select_data_source: 983394bc0182b65ec68f713a46b97302
@@ -1726,11 +1735,12 @@ checksums:
workspace/analysis/charts/select_field: 45665a44f7d5707506364f17f28db3bf
workspace/analysis/charts/select_measures: c9f101aeb53bf0d4abdd652aaf60a1bf
workspace/analysis/charts/select_preset: e68bad9a209a6ca35c62184f1f1d829c
workspace/analysis/charts/showing_first_n_of: 4dec3215fd3150a16ad5c72f17ae02bc
workspace/analysis/charts/showing_first_n_of: e9c1e76a46d0635f775a5b86bddbe1c3
workspace/analysis/charts/start_date: 881de78c79b56f5ceb9b7103bf23cb2c
workspace/analysis/charts/time_dimension: 5c967f2a6a875b00825068df5cb2ef84
workspace/analysis/charts/time_dimension_title: 9353ce9a075a0cc8c3ba7dfa9ef19a8d
workspace/analysis/charts/time_dimension_toggle_description: 77251d8b3b564390bad8b76f56905190
workspace/analysis/charts/update_chart: 7d9223335d9f0c5938ec30356d7034a9
workspace/analysis/dashboards/add_count_charts: b4ee1f29efce0bb380a060e0bc5d64fa
workspace/analysis/dashboards/charts_add_failed: c4fda79ede798ab6747a989f083a0947
workspace/analysis/dashboards/charts_add_partial_failure: b1a9fc6fe18ab20fe16c16e91a05c195
@@ -1739,6 +1749,7 @@ checksums:
workspace/analysis/dashboards/create_dashboard: bedb308708fe9c576e161a2fa16d3439
workspace/analysis/dashboards/create_dashboard_description: d29f60615f6d8c96cc4265541e75ec26
workspace/analysis/dashboards/create_failed: 7b58f15568047a35220b3a47cc3b0f71
workspace/analysis/dashboards/create_new_chart: e03c0fdf4b861454c09707d66fb9bf4c
workspace/analysis/dashboards/create_success: 1fa4dea7702ba03a8a3533295276ff1b
workspace/analysis/dashboards/dashboard: c9380ea68c8c76ea451bd9613329a07c
workspace/analysis/dashboards/dashboard_delete_confirmation: 468a0fb0e24a985cc47a778b50b334ba
@@ -1753,11 +1764,13 @@ checksums:
workspace/analysis/dashboards/duplicate_failed: 6ebaf8ad373b156f88f1ed79a5efd441
workspace/analysis/dashboards/duplicate_success: 37cbb14143776d4c215432673e32ebd9
workspace/analysis/dashboards/failed_to_load_chart_data: ea980a6d12b1b1efed90d991dd0dd0fd
workspace/analysis/dashboards/no_charts_available_description: 796ed01bcb53f770e5f627002839dcb4
workspace/analysis/dashboards/no_charts_to_add_message: ad4cec703aa7d59c407bbb021dce4273
workspace/analysis/dashboards/no_dashboards_found: e049ec0356009c3a0aa2c729d916efc6
workspace/analysis/dashboards/no_data_message: 464d50cf30281a5b6af2726846eb14b4
workspace/analysis/dashboards/please_enter_name: b9211ed8a0882c0e0109beba48685d68
workspace/analysis/manage_feedback_sources: 6aa6a82334ab680b5aa187b7245e8ec8
workspace/analysis/no_feedback_records_message: 67d6ebb9c040304789017d795ca474fc
workspace/analysis/no_feedback_records_with_sources_message: 4b72636a55afb4dcf977161ad5a15467
workspace/analysis/setup_feedback_source: 7cc5855a2b0c762fe2ae13b4921f3e92
workspace/api_keys/add_api_key: 3c7633bae18a6e19af7a5af12f9bc3da
workspace/api_keys/api_key: ce825fec5b3e1f8e27c45b1a63619985
workspace/api_keys/api_key_copied_to_clipboard: daeeac786ba09ffa650e206609b88f9c
@@ -1786,6 +1799,9 @@ checksums:
workspace/app-connection/app_connection_description: dde226414bd2265cbd0daf6635efcfdd
workspace/app-connection/cache_update_delay_description: 3368e4a8090b7684117a16c94f0c409c
workspace/app-connection/cache_update_delay_title: 60e4a0fcfbd8850bddf29b5c3f59550c
workspace/app-connection/environment_id_legacy: d5c701874d34b4591e780755f7ac7a58
workspace/app-connection/environment_id_legacy_alert: 09ac96821ff99fad4590c661503fa0cd
workspace/app-connection/environment_id_legacy_alert_link: 25c529078a115d1ff044a321dd8ee01b
workspace/app-connection/formbricks_sdk_connected: 29e8a40ad6a7fdb5af5ee9451a70a9aa
workspace/app-connection/formbricks_sdk_not_connected: 557c534e665750978ba6edb0eacb428e
workspace/app-connection/formbricks_sdk_not_connected_description: 4ddbacae084238bd0cefeded0fe9dbb9
@@ -2288,7 +2304,7 @@ checksums:
workspace/settings/billing/most_popular: 03051978338d93d9abdd999bc06284f9
workspace/settings/billing/pending_change_removed: c80cc7f1f83f28db186e897fb18282a3
workspace/settings/billing/pending_plan_badge: 1283929a2810dcf6110765f387dc118e
workspace/settings/billing/pending_plan_change_description: a50400c802ab04c23019d8219c5e7e1c
workspace/settings/billing/pending_plan_change_description: 6923bada769d33cadcad557521362c1f
workspace/settings/billing/pending_plan_change_title: 730a8df084494ccf06c0a2f44c28f9fc
workspace/settings/billing/pending_plan_cta: 1283929a2810dcf6110765f387dc118e
workspace/settings/billing/per_month: 64e96490ee2d7811496cf04adae30aa4
@@ -2438,22 +2454,24 @@ checksums:
workspace/settings/feedback_record_directories/error_directory_name_duplicate: 349d650f562cff96b084787126323ca2
workspace/settings/feedback_record_directories/error_directory_name_required: 0f42d7292979006a1069063ab213b8e3
workspace/settings/feedback_record_directories/error_directory_workspaces_invalid_org: 477b5c1a466c4194668544ffd42ec9bf
workspace/settings/feedback_record_directories/error_workspace_already_assigned: 6f851ad28a4e91e48fe13da917ea1ae0
workspace/settings/feedback_record_directories/nav_label: cf9a57b3cbac0f04b98e06fb693e986e
workspace/settings/feedback_record_directories/no_access: cc3385cd01a11e3949003a2cc6fb5b31
workspace/settings/feedback_record_directories/no_connectors: b1becb4fe4e2ba7c5d277db149f092ff
workspace/settings/feedback_record_directories/pause_connectors_confirmation_description: a3c2c56daed9f2a9e6a853cb8b924bad
workspace/settings/feedback_record_directories/pause_connectors_confirmation_title: 09041363c55fb2686f8115df6fa2afc1
workspace/settings/feedback_record_directories/select_workspaces_placeholder: 7d8c8f5910b264525f73bd32107765db
workspace/settings/feedback_record_directories/show_archived: c4c1c3bbddc1bb1540c079b589a2d3de
workspace/settings/feedback_record_directories/title: e3d425c27f80162f29ce094e31a3fd8f
workspace/settings/feedback_record_directories/unarchive: 671fc7e9d7c8cb4d182a25a46551c168
workspace/settings/general/ai_data_analysis_disabled_for_organization: 2066fe71ecf8994ba738c79b63a1934b
workspace/settings/feedback_record_directories/unarchive_workspace_conflict: 82f4b8ebaf41589cfb96e6398dafcc76
workspace/settings/feedback_record_directories/workspace_access: 32407b39cf878fb579559c1ed3660892
workspace/settings/general/ai_data_analysis_enabled: 45fabb594da6851f73fef50ca40fe525
workspace/settings/general/ai_data_analysis_enabled_description: 46d4f0bdf4ebf89e78f79cc961a2de83
workspace/settings/general/ai_enabled: 3cb1fce89c525e754448d5bd143eb6b5
workspace/settings/general/ai_enabled_description: e8c3e9f362588898a6cea85e18c013a1
workspace/settings/general/ai_features_not_enabled_for_organization: e344473bd813fc43f69c51138f74bc8e
workspace/settings/general/ai_instance_not_configured: 37a80753eb22b5bfc985d0e1f2145e3f
workspace/settings/general/ai_settings_updated_successfully: 2a6f534dc3a246ced46becd8a4a9543d
workspace/settings/general/ai_smart_tools_disabled_for_organization: 13df84ae47d35dfa6e86ffa62f29c75d
workspace/settings/general/ai_smart_tools_enabled: 1dda984f5262c5f9120ee9a409236758
workspace/settings/general/ai_smart_tools_enabled_description: 1ceca6707746d3ab4a530712a06d91da
workspace/settings/general/bulk_invite_warning_description: e8737a2fbd5ff353db5580d17b4b5a37
@@ -2512,6 +2530,8 @@ checksums:
workspace/settings/general/share_invite_link: b40b7ffbcf02d7464be52fb562df5e3a
workspace/settings/general/share_this_link_to_let_your_organization_member_join_your_organization: 6eb43d5b1c855572b7ab35f527ba953c
workspace/settings/general/test_email_sent_successfully: aa68214f5e0707c9615e01343640ab32
workspace/settings/general/unlock_ai_features_description: c15c8c050a4a16d99dc595d9c6419bc4
workspace/settings/general/unlock_ai_features_with_a_higher_plan: e0140d3ffd07524fb8f1fec637c4149a
workspace/settings/notifications/auto_subscribe_to_new_surveys: 8102c9ce2fbcae53bd8d979c42932fa9
workspace/settings/notifications/email_alerts_surveys: 12be5a073d74453a531167debd947bd6
workspace/settings/notifications/every_response: 526988e9015f37bc2d32414d7dc05c7c
@@ -2639,6 +2659,18 @@ checksums:
workspace/surveys/edit/adjust_survey_closed_message: ae6f38c9daf08656362bd84459a312fa
workspace/surveys/edit/adjust_survey_closed_message_description: e906aebd9af6451a2a39c73287927299
workspace/surveys/edit/adjust_the_theme_in_the: bccdafda8af5871513266f668b55d690
workspace/surveys/edit/ai_data_analysis_disabled: 2066fe71ecf8994ba738c79b63a1934b
workspace/surveys/edit/ai_features_not_enabled: e344473bd813fc43f69c51138f74bc8e
workspace/surveys/edit/ai_instance_not_configured: 939ad7c3240fa8de98a325239f1b36bc
workspace/surveys/edit/ai_smart_tools_disabled: 13df84ae47d35dfa6e86ffa62f29c75d
workspace/surveys/edit/ai_translate: f25943cdeffe155ee524428f4daa5da2
workspace/surveys/edit/ai_translating: 098a2293b39f9f258d67f926cf03df37
workspace/surveys/edit/ai_translation_all_fields_populated: d78f6a663ea19ce77045970179bd200f
workspace/surveys/edit/ai_translation_complete: f443d0801404f728e68000b46ca67598
workspace/surveys/edit/ai_translation_failed: fd356a173d0abde7a0fc660394954cc7
workspace/surveys/edit/ai_translation_instance_not_configured: 6deeb8aeaff3982d07e1d5a045e06d2d
workspace/surveys/edit/ai_translation_not_available: 2f060bf93a558e6d12ec90988fdd162e
workspace/surveys/edit/ai_translation_not_enabled: 9066bc85f62ea0e96620c058a4004388
workspace/surveys/edit/all_are_true: 05d02c5afac857da530b73dcf18dd8e4
workspace/surveys/edit/all_other_answers_will_continue_to: 9a5d09eea42ff5fd1c18cc58a14dcabd
workspace/surveys/edit/allow_multi_select: 7b4b83f7a0205e2a0a8971671a69a174
@@ -3442,16 +3474,21 @@ checksums:
workspace/teams/permission: cc2ed7274bd8267f9e0a10b079584d8b
workspace/teams/team_name: d1a5f99dbf503ca53f06b3a98b511d02
workspace/teams/team_settings_description: 52f91883b9ceb6de83efbf8efd4f11c0
workspace/unify/add_feedback_record: 19cf2b1fef0ca1400f2400e7ee681ea0
workspace/unify/add_feedback_record_description: 94bca46246ba7353049b33742554b4c0
workspace/unify/add_feedback_source: d046fb437ac478ca30b7b59d6afa8e45
workspace/unify/add_source: 4cc055cbd6312cf0a5db1edf537ce65e
workspace/unify/allowed_values: 430e0721aa2c52745ef8f8b6918bb7d2
workspace/unify/api_ingestion: a14642d27bbb6843f9f4903b6555dfbb
workspace/unify/api_ingestion_manage_api_keys: 116786a004fb7b16ead8a5b7a6a2debe
workspace/unify/api_ingestion_settings_description: a2597917ca1c724607d1d32178d670b3
workspace/unify/auto_generated: 6e83e8febd63275692c444cb8074531d
workspace/unify/change_file: c5163ac18bf443370228a8ecbb0b07da
workspace/unify/click_load_sample_csv: 0ee0bf93f10f02863fc658b359706316
workspace/unify/click_to_upload: 74a7e7d79a88b6bbfd9f22084bffdb9b
workspace/unify/collected_at: b41902ddb4586ba4a4611d726b5014aa
workspace/unify/configure_import: 71d550661f7e9fe322b60e7e870aa2fd
workspace/unify/configure_mapping: c794411c50bc511f8fc332def0e4e2f9
workspace/unify/connection: 421e709602c92ffbe04a266f6a092089
workspace/unify/connector_created_successfully: ea927316021fb2a41cc69ca3ec89d0aa
workspace/unify/connector_deleted_successfully: ea3c9842c5b8f75b02ecb9c80c74d780
workspace/unify/connector_duplicated_successfully: eb21ce42cdbef5fa38244206bf65fe4e
@@ -3470,9 +3507,12 @@ checksums:
workspace/unify/csv_import_duplicate_warning: 56625e4613b93690e95661e5faaa4b27
workspace/unify/csv_inconsistent_columns: b308be183a41a581707eb5c4c0797ad6
workspace/unify/csv_max_records: 21ce7adae30821d40a553bcf37f39bbf
workspace/unify/custom_source_type: d931a8a74d3a5becd568e398107979da
workspace/unify/custom_source_type_placeholder: f139e3e5d70dbf426d7c6b5ab2b198cc
workspace/unify/default_connector_name_csv: ef4060fef24c4fec064987b9d2a9fa4b
workspace/unify/default_connector_name_formbricks: e7afdf7cc1cd7bcf75e7b5d64903a110
workspace/unify/deselect_all: facf8871b2e84a454c6bfe40c2821922
workspace/unify/discard_feedback_record_changes_description: 48ccde99858dcbeb4d679749d0f51941
workspace/unify/discard_feedback_record_changes_title: 52df2800f7b0e8a1d04c47113e019a3e
workspace/unify/drop_a_field_here: 884f3025e618e0a5dcbcb5567335d1bb
workspace/unify/drop_field_or: 5287a8af30f2961ce5a8f14f73ddc353
workspace/unify/edit_csv_mapping: 4f3bad444664d58ffe8ace3dc9e200f9
@@ -3482,47 +3522,64 @@ checksums:
workspace/unify/enum: 96fc644f35edd6b1c09d1d503f078acc
workspace/unify/failed_to_load_feedback_records: 57f6c8c5fa524d7c2d8777315e5036c8
workspace/unify/feedback_date: ddba5d3270d4a6394d29721025a04400
workspace/unify/feedback_record_created_successfully: 0ff30472085f1313a5ad53837c83e7c1
workspace/unify/feedback_record_details: 823f3353db049a9d263ef31405054cda
workspace/unify/feedback_record_details_description: 0b6f908154161241ce6bdeb4a2acaecd
workspace/unify/feedback_record_directory: 89a08a540d1c6eb9f0b1a4b8f56e8aca
workspace/unify/feedback_record_fields: 88c0f13afeb88fe751f85e79b0f73064
workspace/unify/feedback_record_mcp: cdddbef2944489820fd7f376a49c2803
workspace/unify/feedback_record_updated_successfully: cb40ef4b924e21fa627ebe6809d1d826
workspace/unify/feedback_record_value_required: b54d4d86f82071a93dc979e8eb359cf0
workspace/unify/feedback_records: e24cf48bb6985910f4ffe5e00512d388
workspace/unify/feedback_records_refreshed: 4b27a8e2a8dbe8afa945d9f874aa7ef1
workspace/unify/feedback_sources: e58ec9be19db8789e7096a756d24f2b2
workspace/unify/feedback_sources_directory_access_multiple: 11d613bc1e9825aa6faa3db17ae678eb
workspace/unify/feedback_sources_directory_access_single: c9da6b30d410a0ca6302a00a5747dc19
workspace/unify/feedback_sources_settings_description: 45f162f2f81cd195c23cb3ec490bb3df
workspace/unify/field_group_id: 17024bb46ff1e088afb6a279dc85aad4
workspace/unify/field_group_label: 3df09c3b6fd22310359cf955ecff5c8e
workspace/unify/field_id: 7791b5d581b7a525dcadf11ec73c6ab7
workspace/unify/field_label: 6384505ca0e40010c666b712511132a6
workspace/unify/field_type: 2581066dc304c853a4a817c20996fa08
workspace/unify/formbricks_surveys: eba2fce04ee68f02626e5509adf7d66a
workspace/unify/frd_cannot_be_changed: 265c12529f540d8309811f4e0090272f
workspace/unify/go_to_feedback_record_directories: 16b66b62f85e7be311778f39315d118a
workspace/unify/historical_import_complete: f46f98bf4db63bf2993bfb234dc95f62
workspace/unify/import_csv_data: f05e1d1ed88d528256efe5702df46646
workspace/unify/import_feedback: f05e1d1ed88d528256efe5702df46646
workspace/unify/import_historical_responses: d7941f65344b6bfba56a40cc53a063b4
workspace/unify/import_historical_responses_description: c860f7c6dbe8b74383ecf9cae9c219a0
workspace/unify/import_rows: d2963498a7d2766264c4d67db677e8ff
workspace/unify/import_via_source_name: eae32ae2fc87f925ca016fe8283bcbfd
workspace/unify/importing_data: a6d4478379a0faee05cd2c10ffe74984
workspace/unify/importing_historical_data: f5be578704ec26dc4ec573309e9fff20
workspace/unify/invalid_enum_values: e6ca8740dab72f64e8dc5780b5cffcc6
workspace/unify/invalid_values_found: 5011dc9c0294a222033f9910ea919b8a
workspace/unify/load_sample_csv: ad21fa63f4a3df96a5939c753be21f4e
workspace/unify/n_supported_questions: d75413d386441b5eb137a1ea191e4bd9
workspace/unify/manage_directories: 460e00e1cbf1f51de57a2548546e33d7
workspace/unify/manage_feedback_sources: 6aa6a82334ab680b5aa187b7245e8ec8
workspace/unify/metadata: 695d4f7da261ba76e3be4de495491028
workspace/unify/metadata_key: c478d228673f59fa556208ece60452f6
workspace/unify/metadata_read_only_entries: 1934fee46c0a117f4926b61cc3d2d602
workspace/unify/metadata_value: 8d69be1f5a20d9473a33c35670dff216
workspace/unify/missing_feedback_source_title: 9ab1b8d54b4da72dd00ce03fe3b698b5
workspace/unify/no_feedback_record_directory_available: b8126ef5d6276d9655a9b27ffcaca824
workspace/unify/no_feedback_records: 16a905c40f6d47a5e8f93b3d8c6f6693
workspace/unify/no_source_fields_loaded: a597b1d16262cbe897001046eb3ff640
workspace/unify/no_sources_connected: 0e8a5612530bfc82091091f40f95012f
workspace/unify/no_surveys_found: 649a2f29b4c34525778d9177605fb326
workspace/unify/optional: 396fb9a0472daf401c392bdc3e248943
workspace/unify/or_drag_and_drop: 6c7d6b05d39dcbfc710d35fcab25cb8c
workspace/unify/question_selected: b9ff13b6212874258da911867932dc7d
workspace/unify/question_type_not_supported: 8d9f7554e3b509dfd5307d8d1fef08d7
workspace/unify/questions_selected: 1f13d6fecafa2ce5ea9e6d07078a1d38
workspace/unify/records_will_go_to: 6a3f5a6580857a931bab389ad354831c
workspace/unify/refresh_feedback_records: c111751e02a7dee57390ed7fb79cfcc6
workspace/unify/refreshing_feedback_records: 2a03b44510ebe19eea6473639e9a7222
workspace/unify/request_feedback_source: 51045caa2c81dee971d23a1841d19a7e
workspace/unify/required: 04d7fb6f37ffe0a6ca97d49e2a8b6eb5
workspace/unify/save_changes: 53dd9f4f0a4accc822fa5c1f2f6d118a
workspace/unify/select_a_survey_to_see_questions: 792eba3d2f6d210231a2266401111a20
workspace/unify/select_a_value: 115002bf2d9eec536165a7b7efc62862
workspace/unify/select_all: eedc7cdb02de467c15dc418a066a77f2
workspace/unify/select_feedback_record_directory: 88afbf2c2a322249908ee5d00ec5f65d
workspace/unify/select_feedback_record_source_type: 10997fcbea2f93e756888cf7a7476fdf
workspace/unify/select_questions: 13c79b8c284423eb6140534bf2137e56
workspace/unify/select_source_type_description: fd7e3c49b81f8e89f294c8fd94efcdfc
workspace/unify/select_source_type_prompt: c3fce7d908ee62b9e1b7fab1b17606d7
workspace/unify/select_survey: bac52e59c7847417bef6fe7b7096b475
workspace/unify/select_survey_and_questions: 53914988a2f48caecea23f3b3b868b9f
workspace/unify/select_survey_questions_description: 3386ed56085eabebefa3cc453269fc5b
@@ -3532,20 +3589,27 @@ checksums:
workspace/unify/showing_rows: 83d3440314d1e6f2721e034369a3a131
workspace/unify/source: 45309626f464f4bda161ee783a4c8c80
workspace/unify/source_connect_csv_description: 2f9d1dd31668ac52578f16323157b746
workspace/unify/source_connect_feedback_record_mcp_description: a3f56e2a6e403f4021e83f1b1a466d95
workspace/unify/source_connect_formbricks_description: 77bda4e1d485d76770ba2221f1faf9ff
workspace/unify/source_fields: 1bae074990e64cbfd820a0b6462397be
workspace/unify/source_id: 134a9a7d473508c5623ac724a5ba4be9
workspace/unify/source_name: 157675beca12efcd8ec512c5256b1a61
workspace/unify/source_type: d1ff69af76c687eb189db72030717570
workspace/unify/source_type_cannot_be_changed: bb5232c6e92df7f88731310fabbb1eb1
workspace/unify/sources: ecbbe6e49baa335c5afd7b04b609d006
workspace/unify/status_active: 3de9afebcb9d4ce8ac42e14995f79ffd
workspace/unify/status_completed: 0e4bbce9985f25eb673d9a054c8d5334
workspace/unify/status_draft: e8a92958ad300aacfe46c2bf6644927e
workspace/unify/source_type_label_feedback_form: 65e0f65a81cca1c9034943ee6a95c3f4
workspace/unify/source_type_label_interview: 4c58354a7ef4293327d14e9e97d6f694
workspace/unify/source_type_label_nps_campaign: 9f4638404242468f67cdb1a1fe656383
workspace/unify/source_type_label_review: 299f75db25382980b2895622d7712927
workspace/unify/source_type_label_social: ff80c74b36f0511287404d286ec7976e
workspace/unify/source_type_label_support: 55aab5fd0f31a9cb055a2edeeedfaf63
workspace/unify/source_type_label_survey: b659d270a53dada994d926e0cc6e9a54
workspace/unify/source_type_label_usability_test: 33a7b1e9ee8b975008c48e0a524f0e57
workspace/unify/status_error: 3c95bcb32c2104b99a46f5b3dd015248
workspace/unify/status_paused: edb1f7b7219e1c9b7aa67159090d6991
workspace/unify/status_live_sync: 7e794257419414f57d34845ef38d0939
workspace/unify/status_ready: 437c0eea608e15ad5cdab94bde2f4b48
workspace/unify/submission_id: 02edf76883b47079dbe20f3f36b7c1a7
workspace/unify/survey_has_no_questions: c08514b6bce5eb464a4492239be5934d
workspace/unify/survey_import_line: 63fa0ea1d7daa3ba333436fbc65f8b19
workspace/unify/total_feedback_records: 8962087650b62e4a12b81e7d09317ffa
workspace/unify/topics_and_subtopics: 1148eca01a1993fadca932efcdea7641
workspace/unify/unify_feedback: cd68c8ce0445767e7dcfb4de789903d5
workspace/unify/update_mapping_description: 58d5966c0c9b406c037dff3aa8bcb396
workspace/unify/updated_at: 8fdb85248e591254973403755dcc3724
@@ -3553,6 +3617,10 @@ checksums:
workspace/unify/upload_csv_file: b77797b68cb46a614b3adaa4db24d4c2
workspace/unify/user_identifier: 61073457a5c3901084b557d065f876be
workspace/unify/value: 34b0eaa85808b15cbc4be94c64d0146b
workspace/unify/value_boolean: bbdcd3f46954b6304b9069e94e1371ab
workspace/unify/value_date: c8d705d1975affc01c002324725fec3f
workspace/unify/value_number: 1f14da79d14bd7b1c2324141f4470675
workspace/unify/value_text: e097a597cc507c716401ad18255de578
workspace/xm-templates/ces: e2ea309b2f7f13257967b966c2fda1e9
workspace/xm-templates/ces_description: c8d9794dd17d5ab85a979f1b3e1bc935
workspace/xm-templates/csat: fdfc1dc6214cce661dcdc32a71d80337
+1
View File
@@ -32,6 +32,7 @@ type TJobsRuntimeGlobal = typeof globalThis & {
const globalForJobsRuntime = globalThis as TJobsRuntimeGlobal;
const RESPONSE_PIPELINE_JOB_NAME = "response-pipeline.process";
const SURVEY_SCHEDULING_JOB_NAME = "survey-scheduling.reconcile";
const responsePipelineJobHandler: NonNullable<JobHandlerOverrides[string]> = async (data, context) => {
await processResponsePipelineJob(data as TResponsePipelineJobData, context);
};
+7 -15
View File
@@ -13,7 +13,6 @@ const mocks = vi.hoisted(() => ({
getOrganization: vi.fn(),
getIsAIDataAnalysisEnabled: vi.fn(),
getIsAISmartToolsEnabled: vi.fn(),
getTranslate: vi.fn(),
loggerError: vi.fn(),
}));
@@ -40,12 +39,12 @@ vi.mock("@formbricks/logger", () => ({
vi.mock("@/lib/env", () => ({
env: {
AI_PROVIDER: "gcp",
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GCP_PROJECT: "vertex-project",
AI_GCP_LOCATION: "us-central1",
AI_GCP_CREDENTIALS_JSON: undefined,
AI_GCP_APPLICATION_CREDENTIALS: "/tmp/vertex.json",
AI_GOOGLE_CLOUD_PROJECT: "google-cloud-project",
AI_GOOGLE_CLOUD_LOCATION: "us-central1",
AI_GOOGLE_CLOUD_CREDENTIALS_JSON: undefined,
AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS: "/tmp/google-cloud.json",
AI_AWS_REGION: "us-east-1",
AI_AWS_ACCESS_KEY_ID: "aws-access-key-id",
AI_AWS_SECRET_ACCESS_KEY: "aws-secret-access-key",
@@ -66,10 +65,6 @@ vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getIsAISmartToolsEnabled: mocks.getIsAISmartToolsEnabled,
}));
vi.mock("@/lingodotdev/server", () => ({
getTranslate: mocks.getTranslate,
}));
describe("AI organization service", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -82,9 +77,6 @@ describe("AI organization service", () => {
});
mocks.getIsAISmartToolsEnabled.mockResolvedValue(true);
mocks.getIsAIDataAnalysisEnabled.mockResolvedValue(true);
mocks.getTranslate.mockResolvedValue((key: string, values?: Record<string, string>) =>
values ? `${key}:${JSON.stringify(values)}` : key
);
});
test("returns the instance AI status and organization settings", async () => {
@@ -152,9 +144,9 @@ describe("AI organization service", () => {
prompt: "Translate this survey",
},
expect.objectContaining({
AI_PROVIDER: "gcp",
AI_PROVIDER: "google",
AI_MODEL: "gemini-2.5-flash",
AI_GCP_PROJECT: "vertex-project",
AI_GOOGLE_CLOUD_PROJECT: "google-cloud-project",
})
);
});
+13 -12
View File
@@ -4,9 +4,17 @@ import { logger } from "@formbricks/logger";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { env } from "@/lib/env";
import { getOrganization } from "@/lib/organization/service";
import { getTranslate } from "@/lingodotdev/server";
import { getIsAIDataAnalysisEnabled, getIsAISmartToolsEnabled } from "@/modules/ee/license-check/lib/utils";
export const AI_ERROR_CODES = {
FEATURES_NOT_ENABLED: "ai_features_not_enabled",
SMART_TOOLS_DISABLED: "ai_smart_tools_disabled",
DATA_ANALYSIS_DISABLED: "ai_data_analysis_disabled",
INSTANCE_NOT_CONFIGURED: "ai_instance_not_configured",
} as const;
export type TAIErrorCode = (typeof AI_ERROR_CODES)[keyof typeof AI_ERROR_CODES];
export interface TOrganizationAIConfig {
organizationId: string;
isAISmartToolsEnabled: boolean;
@@ -44,31 +52,24 @@ export const assertOrganizationAIConfigured = async (
organizationId: string,
capability: "smartTools" | "dataAnalysis"
): Promise<TOrganizationAIConfig> => {
const t = await getTranslate();
const aiConfig = await getOrganizationAIConfig(organizationId);
const isCapabilityEntitled =
capability === "smartTools" ? aiConfig.isAISmartToolsEntitled : aiConfig.isAIDataAnalysisEntitled;
if (!isCapabilityEntitled) {
throw new OperationNotAllowedError(
t("workspace.settings.general.ai_features_not_enabled_for_organization")
);
throw new OperationNotAllowedError(AI_ERROR_CODES.FEATURES_NOT_ENABLED);
}
if (capability === "smartTools" && !aiConfig.isAISmartToolsEnabled) {
throw new OperationNotAllowedError(
t("workspace.settings.general.ai_smart_tools_disabled_for_organization")
);
throw new OperationNotAllowedError(AI_ERROR_CODES.SMART_TOOLS_DISABLED);
}
if (capability === "dataAnalysis" && !aiConfig.isAIDataAnalysisEnabled) {
throw new OperationNotAllowedError(
t("workspace.settings.general.ai_data_analysis_disabled_for_organization")
);
throw new OperationNotAllowedError(AI_ERROR_CODES.DATA_ANALYSIS_DISABLED);
}
if (!aiConfig.isInstanceConfigured) {
throw new OperationNotAllowedError(t("workspace.settings.general.ai_instance_not_configured"));
throw new OperationNotAllowedError(AI_ERROR_CODES.INSTANCE_NOT_CONFIGURED);
}
return aiConfig;
+27 -18
View File
@@ -12,7 +12,7 @@ import {
ZConnectorUpdateInput,
getHubFieldTypeFromElementType,
} from "@formbricks/types/connector";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { AuthorizationError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { getElementsFromBlocks } from "@/lib/survey/utils";
@@ -86,20 +86,24 @@ const resolveSurveyMappings = async (
const elements = getElementsFromBlocks(survey.blocks);
const elementMap = new Map(elements.map((el) => [el.id, el]));
return elementIds
.filter((elementId) => {
if (elementMap.has(elementId)) return true;
return elementIds.flatMap((elementId) => {
const element = elementMap.get(elementId);
if (!element) {
logger.warn({ surveyId, elementId }, "Skipping unknown elementId when building connector mappings");
return false;
})
.map((elementId) => {
const element = elementMap.get(elementId)!;
return {
surveyId,
elementId,
hubFieldType: getHubFieldTypeFromElementType(element.type),
};
});
return [];
}
const hubFieldType = getHubFieldTypeFromElementType(element.type);
if (!hubFieldType) {
logger.warn(
{ surveyId, elementId, elementType: element.type },
"Skipping unmappable element type when building connector mappings"
);
return [];
}
return [{ surveyId, elementId, hubFieldType }];
});
};
const resolveFormbricksMappingsInput = async (
@@ -108,7 +112,12 @@ const resolveFormbricksMappingsInput = async (
const allMappings = await Promise.all(
entries.map(({ surveyId, elementIds }) => resolveSurveyMappings(surveyId, elementIds))
);
return { type: "formbricks", mappings: allMappings.flat() };
const flattenedMappings = allMappings.flat();
if (flattenedMappings.length === 0) {
throw new InvalidInputError("No supported survey questions selected for connector mapping");
}
return { type: "formbricks_survey", mappings: flattenedMappings };
};
const ZFormbricksSurveyMapping = z.object({
@@ -124,7 +133,7 @@ const ZCreateConnectorWithMappingsAction = z
fieldMappings: z.array(ZConnectorFieldMappingCreateInput).optional(),
})
.superRefine((data, ctx) => {
if (data.connectorInput.type === "formbricks") {
if (data.connectorInput.type === "formbricks_survey") {
if (!data.formbricksMappings?.length) {
ctx.addIssue({
code: "custom",
@@ -298,9 +307,9 @@ export const duplicateConnectorAction = authenticatedActionClient
let mappingsInput: TMappingsInput | undefined;
if (source.type === "formbricks" && source.formbricksMappings.length > 0) {
if (source.type === "formbricks_survey" && source.formbricksMappings.length > 0) {
mappingsInput = {
type: "formbricks",
type: "formbricks_survey",
mappings: source.formbricksMappings.map((m) => ({
surveyId: m.surveyId,
elementId: m.elementId,
+1 -1
View File
@@ -57,7 +57,7 @@ describe("importCsvData", () => {
});
test("throws InvalidInputError for non-csv connector", async () => {
const connector = makeConnector({ type: "formbricks" });
const connector = makeConnector({ type: "formbricks_survey" });
await expect(importCsvData(connector, [])).rejects.toThrow(InvalidInputError);
});
+86 -15
View File
@@ -3,6 +3,7 @@ import { TConnectorFieldMapping } from "@formbricks/types/connector";
import { transformCsvRowToFeedbackRecord, transformCsvRowsToFeedbackRecords } from "./csv-transform";
const NOW = new Date("2026-02-25T10:00:00.000Z");
const TENANT = "tenant-test";
const makeMapping = (
sourceFieldId: string,
@@ -34,7 +35,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
timestamp: "2026-01-15T10:00:00Z",
};
const result = transformCsvRowToFeedbackRecord(row, baseMappings);
const result = transformCsvRowToFeedbackRecord(row, baseMappings, TENANT);
expect(result).not.toBeNull();
expect(result!.source_type).toBe("survey");
@@ -42,13 +43,77 @@ describe("transformCsvRowToFeedbackRecord", () => {
expect(result!.field_type).toBe("text");
expect(result!.value_text).toBe("Great product!");
expect(result!.collected_at).toBe("2026-01-15T10:00:00.000Z");
expect(result!.tenant_id).toBe(TENANT);
});
test("returns null when required fields are missing", () => {
const row = { feedback_text: "Great product!" };
const mappings = [makeMapping("feedback_text", "value_text")];
const result = transformCsvRowToFeedbackRecord(row, mappings);
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result).toBeNull();
});
test("returns null when tenant_id is missing", () => {
const row = {
feedback_text: "Great product!",
question: "q1",
timestamp: "2026-01-15T10:00:00Z",
};
const result = transformCsvRowToFeedbackRecord(row, baseMappings);
expect(result).toBeNull();
});
test("auto-generates submission_id as a UUID when unmapped", () => {
const row = {
feedback_text: "Great product!",
question: "q1",
timestamp: "2026-01-15T10:00:00Z",
};
const a = transformCsvRowToFeedbackRecord(row, baseMappings, TENANT);
const b = transformCsvRowToFeedbackRecord(row, baseMappings, TENANT);
expect(a!.submission_id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
expect(b!.submission_id).not.toBe(a!.submission_id);
});
test("uses explicit submission_id mapping when provided", () => {
const mappings = [...baseMappings, makeMapping("order_id", "submission_id")];
const row = {
feedback_text: "x",
question: "q1",
timestamp: "2026-01-15",
order_id: "ORD-42",
};
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result!.submission_id).toBe("ORD-42");
});
test("returns null when submission_id mapped but cell is empty", () => {
const mappings = [...baseMappings, makeMapping("order_id", "submission_id")];
const row = {
feedback_text: "x",
question: "q1",
timestamp: "2026-01-15",
order_id: "",
};
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result).toBeNull();
});
test("returns null when submission_id mapped but column missing from row", () => {
const mappings = [...baseMappings, makeMapping("order_id", "submission_id")];
const row = {
feedback_text: "x",
question: "q1",
timestamp: "2026-01-15",
};
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result).toBeNull();
});
@@ -61,7 +126,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
rating: "4.5",
};
const result = transformCsvRowToFeedbackRecord(row, mappings);
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result!.value_number).toBe(4.5);
});
@@ -74,7 +139,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
rating: "not-a-number",
};
const result = transformCsvRowToFeedbackRecord(row, mappings);
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result!.value_number).toBeUndefined();
});
@@ -84,21 +149,24 @@ describe("transformCsvRowToFeedbackRecord", () => {
expect(
transformCsvRowToFeedbackRecord(
{ feedback_text: "x", question: "q1", timestamp: "2026-01-15", is_promoter: "true" },
mappings
mappings,
TENANT
)!.value_boolean
).toBe(true);
expect(
transformCsvRowToFeedbackRecord(
{ feedback_text: "x", question: "q1", timestamp: "2026-01-15", is_promoter: "0" },
mappings
mappings,
TENANT
)!.value_boolean
).toBe(false);
expect(
transformCsvRowToFeedbackRecord(
{ feedback_text: "x", question: "q1", timestamp: "2026-01-15", is_promoter: "yes" },
mappings
mappings,
TENANT
)!.value_boolean
).toBe(true);
});
@@ -114,7 +182,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
makeMapping("", "collected_at", "$now"),
];
const result = transformCsvRowToFeedbackRecord({ question: "q1" }, mappings);
const result = transformCsvRowToFeedbackRecord({ question: "q1" }, mappings, TENANT);
expect(result!.collected_at).toBe(NOW.toISOString());
vi.useRealTimers();
@@ -129,7 +197,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
];
const row = { question: "q1", type_column: "review", timestamp: "2026-01-15" };
const result = transformCsvRowToFeedbackRecord(row, mappings);
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result!.source_type).toBe("always_survey");
});
@@ -140,7 +208,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
timestamp: "2026-01-15T10:00:00Z",
};
const result = transformCsvRowToFeedbackRecord(row, baseMappings);
const result = transformCsvRowToFeedbackRecord(row, baseMappings, TENANT);
expect(result!.value_text).toBeUndefined();
});
@@ -153,7 +221,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
meta: '{"device":"mobile","version":"2.1"}',
};
const result = transformCsvRowToFeedbackRecord(row, mappings);
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result!.metadata).toEqual({ device: "mobile", version: "2.1" });
});
@@ -166,7 +234,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
meta: "just a string",
};
const result = transformCsvRowToFeedbackRecord(row, mappings);
const result = transformCsvRowToFeedbackRecord(row, mappings, TENANT);
expect(result!.metadata).toEqual({ raw: "just a string" });
});
@@ -177,7 +245,7 @@ describe("transformCsvRowToFeedbackRecord", () => {
timestamp: "not-a-date",
};
const result = transformCsvRowToFeedbackRecord(row, baseMappings);
const result = transformCsvRowToFeedbackRecord(row, baseMappings, TENANT);
expect(result!.collected_at).toBeUndefined();
});
});
@@ -198,16 +266,19 @@ describe("transformCsvRowsToFeedbackRecords", () => {
makeMapping("timestamp", "collected_at"),
];
const { records, skipped } = transformCsvRowsToFeedbackRecords(rows, mappings);
const { records, skipped } = transformCsvRowsToFeedbackRecords(rows, mappings, TENANT);
expect(records).toHaveLength(2);
expect(skipped).toBe(1);
expect(records[0].field_id).toBe("q1");
expect(records[1].field_id).toBe("q2");
expect(records[0].submission_id).toBeTruthy();
expect(records[1].submission_id).toBeTruthy();
expect(records[0].submission_id).not.toBe(records[1].submission_id);
});
test("returns empty records for empty input", () => {
const { records, skipped } = transformCsvRowsToFeedbackRecords([], baseMappings);
const { records, skipped } = transformCsvRowsToFeedbackRecords([], baseMappings, TENANT);
expect(records).toHaveLength(0);
expect(skipped).toBe(0);
});
+17 -2
View File
@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
import { TConnectorFieldMapping, THubTargetField } from "@formbricks/types/connector";
import { FeedbackRecordCreateParams } from "@/modules/hub";
@@ -50,8 +51,10 @@ const resolveValue = (
/**
* Transform a single CSV row into a FeedbackRecord using field mappings.
*
* Each mapping maps a CSV column (sourceFieldId) or a static value to a target field.
* Returns null if required fields (source_type, field_id, field_type) are missing after mapping.
* Returns null if any of source_type, field_id, field_type, tenant_id are missing,
* or if submission_id is mapped but resolves empty for this row (would break
* idempotency on re-import). Falls back to a random UUID for submission_id only
* when no mapping for it exists.
*/
export const transformCsvRowToFeedbackRecord = (
row: Record<string, string>,
@@ -83,6 +86,18 @@ export const transformCsvRowToFeedbackRecord = (
record.tenant_id = tenantId;
}
if (!record.tenant_id) {
return null;
}
if (!("submission_id" in record)) {
const submissionMapped = mappings.some((m) => m.targetFieldId === "submission_id");
if (submissionMapped) {
return null;
}
record.submission_id = randomUUID();
}
return record as unknown as FeedbackRecordCreateParams;
};
+1 -1
View File
@@ -30,7 +30,7 @@ const mockConnector: TConnectorWithMappings = {
createdAt: NOW,
updatedAt: NOW,
name: "Test Connector",
type: "formbricks",
type: "formbricks_survey",
status: "active",
workspaceId: ENV_ID,
lastSyncAt: null,
+1 -1
View File
@@ -37,7 +37,7 @@ export const importHistoricalResponses = async (
connector: TConnectorWithMappings,
survey: TSurvey
): Promise<TImportResult> => {
if (connector.type !== "formbricks") {
if (connector.type !== "formbricks_survey") {
throw new InvalidInputError("Historical import is only supported for Formbricks connectors");
}
@@ -53,7 +53,7 @@ function createConnector(
createdAt: new Date(),
updatedAt: new Date(),
name: "Test Connector",
type: "formbricks",
type: "formbricks_survey",
status: "active",
workspaceId: "env-1",
feedbackRecordDirectoryId: "frd-1",
@@ -79,7 +79,7 @@ const oneFeedbackRecord = [
{
field_id: "el-1",
field_type: "rating" as const,
source_type: "formbricks",
source_type: "formbricks_survey",
source_id: "survey-1",
source_name: "Test Survey",
field_label: "Question?",
+2 -2
View File
@@ -34,7 +34,7 @@ const logFailedRecords = (
const processConnector = async (
connector: TConnectorWithMappings,
response: TResponse,
survey: TSurvey,
survey: Pick<TSurvey, "id" | "name" | "blocks">,
workspaceId: string
): Promise<void> => {
const feedbackRecords = transformResponseToFeedbackRecords(
@@ -94,7 +94,7 @@ const processConnector = async (
*/
export const handleConnectorPipeline = async (
response: TResponse,
survey: TSurvey,
survey: Pick<TSurvey, "id" | "name" | "blocks">,
workspaceId: string
): Promise<void> => {
try {
+13 -8
View File
@@ -47,7 +47,7 @@ const mockConnector = {
createdAt: NOW,
updatedAt: NOW,
name: "Test Connector",
type: "formbricks" as const,
type: "formbricks_survey" as const,
status: "active" as const,
workspaceId: ENV_ID,
lastSyncAt: null,
@@ -144,7 +144,7 @@ describe("getConnectorsBySurveyId", () => {
expect(prisma.connector.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
type: "formbricks",
type: "formbricks_survey",
status: "active",
formbricksMappings: { some: { surveyId: SURVEY_ID } },
},
@@ -303,13 +303,18 @@ describe("createConnectorWithMappings", () => {
const result = await createConnectorWithMappings(ENV_ID, {
name: "New",
type: "formbricks",
type: "formbricks_survey",
feedbackRecordDirectoryId: FRD_ID,
});
expect(tx.connector.create).toHaveBeenCalledWith(
expect.objectContaining({
data: { name: "New", type: "formbricks", workspaceId: ENV_ID, feedbackRecordDirectoryId: FRD_ID },
data: {
name: "New",
type: "formbricks_survey",
workspaceId: ENV_ID,
feedbackRecordDirectoryId: FRD_ID,
},
})
);
expect(tx.connectorFormbricksMapping.create).not.toHaveBeenCalled();
@@ -325,9 +330,9 @@ describe("createConnectorWithMappings", () => {
await createConnectorWithMappings(
ENV_ID,
{ name: "FB", type: "formbricks", feedbackRecordDirectoryId: FRD_ID },
{ name: "FB", type: "formbricks_survey", feedbackRecordDirectoryId: FRD_ID },
{
type: "formbricks",
type: "formbricks_survey",
mappings: [
{ surveyId: SURVEY_ID, elementId: "el-1", hubFieldType: "text" },
{ surveyId: SURVEY_ID, elementId: "el-2", hubFieldType: "nps" },
@@ -392,7 +397,7 @@ describe("createConnectorWithMappings", () => {
await expect(
createConnectorWithMappings(ENV_ID, {
name: "Dup",
type: "formbricks",
type: "formbricks_survey",
feedbackRecordDirectoryId: FRD_ID,
})
).rejects.toThrow(InvalidInputError);
@@ -470,7 +475,7 @@ describe("updateConnectorWithMappings", () => {
ENV_ID,
{ name: "Updated" },
{
type: "formbricks",
type: "formbricks_survey",
mappings: [{ surveyId: SURVEY_ID, elementId: "el-new", hubFieldType: "nps" }],
}
);
+4 -4
View File
@@ -132,7 +132,7 @@ export const getConnectorsBySurveyId = reactCache(
try {
const connectors = await prisma.connector.findMany({
where: {
type: "formbricks",
type: "formbricks_survey",
status: "active",
formbricksMappings: {
some: {
@@ -213,7 +213,7 @@ export const deleteConnector = async (connectorId: string, workspaceId: string):
// -- Composite functions --
export type TFormbricksMappingsInput = {
type: "formbricks";
type: "formbricks_survey";
mappings: TConnectorFormbricksMappingCreateInput[];
};
@@ -243,7 +243,7 @@ export const createConnectorWithMappings = async (
},
});
if (mappingsInput?.type === "formbricks") {
if (mappingsInput?.type === "formbricks_survey") {
await Promise.all(
mappingsInput.mappings.map((mapping) =>
tx.connectorFormbricksMapping.create({
@@ -311,7 +311,7 @@ export const updateConnectorWithMappings = async (
},
});
if (mappingsInput?.type === "formbricks") {
if (mappingsInput?.type === "formbricks_survey") {
await tx.connectorFormbricksMapping.deleteMany({
where: { connectorId, workspaceId },
});
+19 -1
View File
@@ -123,7 +123,7 @@ describe("transformResponseToFeedbackRecords", () => {
const result = transformResponseToFeedbackRecords(mockResponse, mockSurvey, mappings);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
source_type: "formbricks",
source_type: "formbricks_survey",
field_id: "el-text",
field_type: "text",
field_label: "How can we improve?",
@@ -185,6 +185,24 @@ describe("transformResponseToFeedbackRecords", () => {
expect(result[0].collected_at).toBe(NOW.toISOString());
});
test("falls back to updatedAt when createdAt is missing", () => {
const updatedAt = new Date("2026-02-25T10:00:00.000Z");
const response = { ...mockResponse, createdAt: undefined, updatedAt } as unknown as TResponse;
const mappings = [createMapping({ elementId: "el-text", hubFieldType: "text" })];
const result = transformResponseToFeedbackRecords(response, mockSurvey, mappings);
expect(result[0].collected_at).toBe(updatedAt.toISOString());
});
test("parses string createdAt values for collected_at", () => {
const response = {
...mockResponse,
createdAt: "2026-02-26T10:00:00.000Z",
} as unknown as TResponse;
const mappings = [createMapping({ elementId: "el-text", hubFieldType: "text" })];
const result = transformResponseToFeedbackRecords(response, mockSurvey, mappings);
expect(result[0].collected_at).toBe("2026-02-26T10:00:00.000Z");
});
test("includes tenant_id when provided", () => {
const mappings = [createMapping({ elementId: "el-text", hubFieldType: "text" })];
const result = transformResponseToFeedbackRecords(mockResponse, mockSurvey, mappings, "tenant-abc");
+20 -4
View File
@@ -14,6 +14,23 @@ const getHeadlineFromElement = (element?: TSurveyElement): string => {
return getTextContent(raw) || "Untitled";
};
const toIsoTimestamp = (value: unknown): string | null => {
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value.toISOString();
}
if (typeof value === "string" || typeof value === "number") {
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
}
return null;
};
const getCollectedAt = (response: TResponse): string => {
return toIsoTimestamp(response.createdAt) ?? toIsoTimestamp(response.updatedAt) ?? new Date().toISOString();
};
function extractResponseValue(responseData: TResponseData, elementId: string): TResponseDataValue {
if (!responseData || typeof responseData !== "object") return undefined;
return responseData[elementId];
@@ -79,7 +96,7 @@ const convertValueToHubFields = (
*/
export function transformResponseToFeedbackRecords(
response: TResponse,
survey: TSurvey,
survey: Pick<TSurvey, "id" | "name" | "blocks">,
mappings: TConnectorFormbricksMapping[],
tenantId: string
): FeedbackRecordCreateParams[] {
@@ -99,9 +116,8 @@ export function transformResponseToFeedbackRecords(
const valueFields = convertValueToHubFields(value, mapping.hubFieldType);
const feedbackRecord = {
collected_at:
response.createdAt instanceof Date ? response.createdAt.toISOString() : String(response.createdAt),
source_type: "formbricks",
collected_at: getCollectedAt(response),
source_type: "formbricks_survey",
submission_id: response.id,
tenant_id: tenantId,
field_id: mapping.elementId,
+47 -28
View File
@@ -6,10 +6,10 @@ const ZActiveAIProvider = z.enum(AI_PROVIDERS);
const ZAIConfigurationEnv = z.object({
AI_PROVIDER: ZActiveAIProvider.optional(),
AI_MODEL: z.string().optional(),
AI_GCP_PROJECT: z.string().optional(),
AI_GCP_LOCATION: z.string().optional(),
AI_GCP_CREDENTIALS_JSON: z.string().optional(),
AI_GCP_APPLICATION_CREDENTIALS: z.string().optional(),
AI_GOOGLE_CLOUD_PROJECT: z.string().optional(),
AI_GOOGLE_CLOUD_LOCATION: z.string().optional(),
AI_GOOGLE_CLOUD_CREDENTIALS_JSON: z.string().optional(),
AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS: z.string().optional(),
AI_AWS_REGION: z.string().optional(),
AI_AWS_ACCESS_KEY_ID: z.string().optional(),
AI_AWS_SECRET_ACCESS_KEY: z.string().optional(),
@@ -20,6 +20,9 @@ const ZAIConfigurationEnv = z.object({
type TAIConfigurationEnv = z.infer<typeof ZAIConfigurationEnv>;
const isJsonObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const addEnvIssue = (ctx: z.RefinementCtx, path: keyof TAIConfigurationEnv, message: string): void => {
ctx.addIssue({
code: "custom",
@@ -48,28 +51,44 @@ const validateAwsAIConfiguration = (values: TAIConfigurationEnv, ctx: z.Refineme
}
};
const validateGcpAIConfiguration = (values: TAIConfigurationEnv, ctx: z.RefinementCtx): void => {
if (!values.AI_GCP_PROJECT) {
addEnvIssue(ctx, "AI_GCP_PROJECT", "AI_GCP_PROJECT is required when AI_PROVIDER=gcp");
}
if (!values.AI_GCP_LOCATION) {
addEnvIssue(ctx, "AI_GCP_LOCATION", "AI_GCP_LOCATION is required when AI_PROVIDER=gcp");
}
if (!values.AI_GCP_CREDENTIALS_JSON && !values.AI_GCP_APPLICATION_CREDENTIALS) {
const validateGoogleAIConfiguration = (values: TAIConfigurationEnv, ctx: z.RefinementCtx): void => {
if (!values.AI_GOOGLE_CLOUD_PROJECT) {
addEnvIssue(
ctx,
"AI_GCP_CREDENTIALS_JSON",
"AI_GCP_CREDENTIALS_JSON or AI_GCP_APPLICATION_CREDENTIALS is required when AI_PROVIDER=gcp"
"AI_GOOGLE_CLOUD_PROJECT",
"AI_GOOGLE_CLOUD_PROJECT is required when AI_PROVIDER=google"
);
}
if (values.AI_GCP_CREDENTIALS_JSON) {
if (!values.AI_GOOGLE_CLOUD_LOCATION) {
addEnvIssue(
ctx,
"AI_GOOGLE_CLOUD_LOCATION",
"AI_GOOGLE_CLOUD_LOCATION is required when AI_PROVIDER=google"
);
}
if (!values.AI_GOOGLE_CLOUD_CREDENTIALS_JSON && !values.AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS) {
addEnvIssue(
ctx,
"AI_GOOGLE_CLOUD_CREDENTIALS_JSON",
"AI_GOOGLE_CLOUD_CREDENTIALS_JSON or AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS is required when AI_PROVIDER=google"
);
}
if (values.AI_GOOGLE_CLOUD_CREDENTIALS_JSON) {
try {
JSON.parse(values.AI_GCP_CREDENTIALS_JSON);
const parsedCredentials = JSON.parse(values.AI_GOOGLE_CLOUD_CREDENTIALS_JSON) as unknown;
if (!isJsonObject(parsedCredentials)) {
throw new Error("AI_GOOGLE_CLOUD_CREDENTIALS_JSON must be a JSON object");
}
} catch {
addEnvIssue(ctx, "AI_GCP_CREDENTIALS_JSON", "AI_GCP_CREDENTIALS_JSON must be valid JSON");
addEnvIssue(
ctx,
"AI_GOOGLE_CLOUD_CREDENTIALS_JSON",
"AI_GOOGLE_CLOUD_CREDENTIALS_JSON must be a valid JSON object"
);
}
}
};
@@ -100,7 +119,7 @@ const validateActiveAIProviderConfiguration = (values: TAIConfigurationEnv, ctx:
(values: TAIConfigurationEnv, ctx: z.RefinementCtx) => void
> = {
aws: validateAwsAIConfiguration,
gcp: validateGcpAIConfiguration,
google: validateGoogleAIConfiguration,
azure: validateAzureAIConfiguration,
};
@@ -160,10 +179,10 @@ const parsedEnv = createEnv({
GITHUB_SECRET: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
AI_GCP_PROJECT: z.string().optional(),
AI_GCP_LOCATION: z.string().optional(),
AI_GCP_CREDENTIALS_JSON: z.string().optional(),
AI_GCP_APPLICATION_CREDENTIALS: z.string().optional(),
AI_GOOGLE_CLOUD_PROJECT: z.string().optional(),
AI_GOOGLE_CLOUD_LOCATION: z.string().optional(),
AI_GOOGLE_CLOUD_CREDENTIALS_JSON: z.string().optional(),
AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS: z.string().optional(),
GOOGLE_SHEETS_CLIENT_ID: z.string().optional(),
GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(),
GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(),
@@ -315,10 +334,10 @@ const parsedEnv = createEnv({
GITHUB_SECRET: process.env.GITHUB_SECRET,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
AI_GCP_PROJECT: process.env.AI_GCP_PROJECT,
AI_GCP_LOCATION: process.env.AI_GCP_LOCATION,
AI_GCP_CREDENTIALS_JSON: process.env.AI_GCP_CREDENTIALS_JSON,
AI_GCP_APPLICATION_CREDENTIALS: process.env.AI_GCP_APPLICATION_CREDENTIALS,
AI_GOOGLE_CLOUD_PROJECT: process.env.AI_GOOGLE_CLOUD_PROJECT,
AI_GOOGLE_CLOUD_LOCATION: process.env.AI_GOOGLE_CLOUD_LOCATION,
AI_GOOGLE_CLOUD_CREDENTIALS_JSON: process.env.AI_GOOGLE_CLOUD_CREDENTIALS_JSON,
AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS: process.env.AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS,
GOOGLE_SHEETS_CLIENT_ID: process.env.GOOGLE_SHEETS_CLIENT_ID,
GOOGLE_SHEETS_CLIENT_SECRET: process.env.GOOGLE_SHEETS_CLIENT_SECRET,
GOOGLE_SHEETS_REDIRECT_URL: process.env.GOOGLE_SHEETS_REDIRECT_URL,
+3 -1
View File
@@ -6,6 +6,8 @@ import { parseRecallInfo } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
export type TElementResponseMappingSurvey = Pick<TSurvey, "blocks" | "languages">;
// function to convert response value of type string | number | string[] or Record<string, string> to string | string[]
export const convertResponseValue = (
answer: TResponseDataValue,
@@ -34,7 +36,7 @@ export const convertResponseValue = (
};
export const getElementResponseMapping = (
survey: TSurvey,
survey: TElementResponseMappingSurvey,
response: TResponse
): { element: string; response: string | string[]; type: TSurveyElementTypeEnum }[] => {
const elementResponseMapping: {
+6 -47
View File
@@ -4,8 +4,8 @@ import { isLight, mixColor } from "@/lib/utils/colors";
export const COLOR_DEFAULTS = {
brandColor: "#64748b",
questionColor: "#2b2524",
inputColor: "#ffffff",
elementHeadlineColor: "#2b2524",
inputBgColor: "#ffffff",
inputBorderColor: "#cbd5e1",
cardBackgroundColor: "#ffffff",
cardBorderColor: "#f8fafc",
@@ -40,10 +40,8 @@ export const getSuggestedColors = (brandColor: string = DEFAULT_BRAND_COLOR) =>
return {
// General
"brandColor.light": brandColor,
"questionColor.light": questionColor,
// Headlines & Descriptions — use questionColor to match the legacy behaviour
// where all text elements derived their color from questionColor.
// Headlines & Descriptions
"elementHeadlineColor.light": questionColor,
"elementDescriptionColor.light": questionColor,
"elementUpperLabelColor.light": questionColor,
@@ -53,7 +51,7 @@ export const getSuggestedColors = (brandColor: string = DEFAULT_BRAND_COLOR) =>
"buttonTextColor.light": isLight(brandColor) ? "#0f172a" : "#ffffff",
// Inputs
"inputColor.light": inputBg,
"inputBgColor.light": inputBg,
"inputBorderColor.light": inputBorder,
"inputTextColor.light": questionColor,
@@ -94,8 +92,6 @@ const _colors = getSuggestedColors(DEFAULT_BRAND_COLOR);
export const STYLE_DEFAULTS: TWorkspaceStyling = {
allowStyleOverwrite: true,
brandColor: { light: _colors["brandColor.light"] },
questionColor: { light: _colors["questionColor.light"] },
inputColor: { light: _colors["inputColor.light"] },
inputBorderColor: { light: _colors["inputBorderColor.light"] },
cardBackgroundColor: { light: _colors["cardBackgroundColor.light"] },
cardBorderColor: { light: _colors["cardBorderColor.light"] },
@@ -117,6 +113,7 @@ export const STYLE_DEFAULTS: TWorkspaceStyling = {
elementUpperLabelFontWeight: 400,
// Inputs
inputBgColor: { light: _colors["inputBgColor.light"] },
inputTextColor: { light: _colors["inputTextColor.light"] },
inputBorderRadius: 8,
inputHeight: 20,
@@ -151,43 +148,6 @@ export const STYLE_DEFAULTS: TWorkspaceStyling = {
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 TWorkspaceStyling object from a single brand color.
*
@@ -203,13 +163,12 @@ export const buildStylingFromBrandColor = (brandColor: string = DEFAULT_BRAND_CO
return {
...STYLE_DEFAULTS,
brandColor: { light: colors["brandColor.light"] },
questionColor: { light: colors["questionColor.light"] },
elementHeadlineColor: { light: colors["elementHeadlineColor.light"] },
elementDescriptionColor: { light: colors["elementDescriptionColor.light"] },
elementUpperLabelColor: { light: colors["elementUpperLabelColor.light"] },
buttonBgColor: { light: colors["buttonBgColor.light"] },
buttonTextColor: { light: colors["buttonTextColor.light"] },
inputColor: { light: colors["inputColor.light"] },
inputBgColor: { light: colors["inputBgColor.light"] },
inputBorderColor: { light: colors["inputBorderColor.light"] },
inputTextColor: { light: colors["inputTextColor.light"] },
optionBgColor: { light: colors["optionBgColor.light"] },
+8 -8
View File
@@ -36,13 +36,13 @@ describe("Template Utilities", () => {
} as unknown as TSurveyElement;
const workspace = {
name: "TestProject",
name: "TestWorkspace",
} as unknown as TWorkspace;
const result = replaceElementPresetPlaceholders(element, workspace);
// The function directly replaces without calling getLocalizedValue in the test scenario
expect(result.headline?.default).toBe("How do you like TestProject?");
expect(result.headline?.default).toBe("How do you like TestWorkspace?");
});
test("replaces workspaceName placeholder in subheader", () => {
@@ -53,13 +53,13 @@ describe("Template Utilities", () => {
} as unknown as TSurveyElement;
const workspace = {
name: "TestProject",
name: "TestWorkspace",
} as unknown as TWorkspace;
const result = replaceElementPresetPlaceholders(element, workspace);
expect(result.headline?.default).toBe("Question");
expect(result.subheader?.default).toBe("Subheader for TestProject");
expect(result.subheader?.default).toBe("Subheader for TestWorkspace");
});
test("handles missing headline and subheader", () => {
@@ -68,7 +68,7 @@ describe("Template Utilities", () => {
} as unknown as TSurveyElement;
const workspace = {
name: "TestProject",
name: "TestWorkspace",
} as unknown as TWorkspace;
const result = replaceElementPresetPlaceholders(element, workspace);
@@ -106,14 +106,14 @@ describe("Template Utilities", () => {
} as unknown as TTemplate;
const workspace = {
name: "TestProject",
name: "TestWorkspace",
} as TWorkspace;
const result = replacePresetPlaceholders(mockTemplate, workspace);
expect(structuredClone).toHaveBeenCalledWith(mockTemplate.preset);
expect(result.preset.name).toBe("TestProject Feedback");
expect(result.preset.blocks[0].elements[0].headline?.default).toBe("How would you rate TestProject?");
expect(result.preset.name).toBe("TestWorkspace Feedback");
expect(result.preset.blocks[0].elements[0].headline?.default).toBe("How would you rate TestWorkspace?");
});
});
});
+1
View File
@@ -12,6 +12,7 @@ const selectWorkspace = {
id: true,
createdAt: true,
updatedAt: true,
legacyEnvironmentId: true,
name: true,
organizationId: true,
languages: true,
+112 -44
View File
@@ -125,6 +125,7 @@
"activity": "Aktivität",
"add": "Hinzufügen",
"add_action": "Aktion hinzufügen",
"add_chart": "Diagramm hinzufügen",
"add_charts": "Diagramme hinzufügen",
"add_existing_chart_description": "Suche und wähle Diagramme aus, um sie zu diesem Dashboard hinzuzufügen.",
"add_filter": "Filter hinzufügen",
@@ -147,6 +148,7 @@
"apply_filters": "Filter anwenden",
"archived": "Archiviert",
"are_you_sure": "Bist du sicher?",
"ask": "Ask",
"attributes": "Attribute",
"back": "Zurück",
"billing": "Abrechnung",
@@ -159,10 +161,10 @@
"change_workspace": "Workspace wechseln",
"chart": "Diagramm",
"charts": "Diagramme",
"choice_n": "Auswahl {{n}}",
"choice_n": "Auswahl {n}",
"choices": "Entscheidungen",
"choose_organization": "Organisation auswählen",
"choose_workspace": "Projekt auswählen",
"choose_workspace": "Workspace auswählen",
"clear_all": "Alles löschen",
"clear_filters": "Filter löschen",
"clear_selection": "Auswahl aufheben",
@@ -172,7 +174,7 @@
"close": "Schließen",
"code": "Code",
"collapse_rows": "Zeilen einklappen",
"column_n": "Spalte {{n}}",
"column_n": "Spalte {n}",
"completed": "Abgeschlossen",
"configuration": "Konfigurieren",
"confirm": "Bestätigen",
@@ -212,6 +214,7 @@
"delete_what": "{deleteWhat} löschen",
"description": "Beschreibung",
"disable": "Deaktivieren",
"disabled": "Deaktiviert",
"disallow": "Nicht erlauben",
"discard": "Verwerfen",
"dismissed": "Verworfen",
@@ -241,9 +244,9 @@
"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",
"failed_to_load_workspaces": "Workspaces konnten nicht geladen werden",
"failed_to_parse_csv": "CSV-Analyse fehlgeschlagen",
"field_placeholder": "{{field}}-Platzhalter",
"field_placeholder": "Platzhalter für {field}",
"filter": "Filter",
"finish": "Fertig",
"first_name": "Vorname",
@@ -331,6 +334,7 @@
"not_authenticated": "Du bist nicht authentifiziert, um diese Aktion durchzuführen.",
"not_authorized": "Nicht autorisiert",
"not_connected": "Nicht verbunden",
"not_set": "Nicht festgelegt",
"note": "Hinweis",
"notifications": "Benachrichtigungen",
"number": "Nummer",
@@ -390,13 +394,14 @@
"report_survey": "Umfrage melden",
"request_trial_license": "Testlizenz anfordern",
"reset_to_default": "Auf Standard zurücksetzen",
"resize": "Größe ändern",
"response": "Antwort",
"response_id": "Antwort-ID",
"responses": "Antworten",
"restart": "Neu starten",
"retry": "Erneut versuchen",
"role": "Rolle",
"row_n": "Zeile {{n}}",
"row_n": "Zeile {n}",
"saas": "SaaS",
"sales": "Vertrieb",
"save": "Speichern",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden",
"something_went_wrong": "Etwas ist schiefgelaufen",
"something_went_wrong_please_try_again": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
"soon": "Bald",
"sort_by": "Sortieren nach",
"start_free_trial": "Kostenlose Testversion starten",
"status": "Status",
@@ -501,13 +507,13 @@
"weeks": "Wochen",
"welcome_card": "Willkommenskarte",
"workspace": "Arbeitsbereich",
"workspace_configuration": "Projektkonfiguration",
"workspace_created_successfully": "Projekt erfolgreich erstellt",
"workspace_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
"workspace_id": "Projekt-ID",
"workspace_name": "Projektname",
"workspace_configuration": "Workspace-Konfiguration",
"workspace_created_successfully": "Workspace erfolgreich erstellt",
"workspace_creation_description": "Organisiere Umfragen in Workspaces für eine bessere Zugriffskontrolle.",
"workspace_id": "Workspace-ID",
"workspace_name": "Workspace-Name",
"workspace_name_placeholder": "z. B. Formbricks",
"workspaces": "Projekte",
"workspaces": "Workspaces",
"years": "Jahre",
"yes": "Ja",
"you_are_downgraded_to_the_community_edition": "Du wurdest auf die Community Edition herabgestuft.",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Balkendiagramm",
"chart_type_big_number": "Große Zahl",
"chart_type_line": "Liniendiagramm",
"chart_type_not_supported": "Diagrammtyp \"{{chartType}}\" wird noch nicht unterstützt",
"chart_type_not_supported": "Diagrammtyp \"{chartType}\" wird noch nicht unterstützt",
"chart_type_pie": "Kreisdiagramm",
"chart_updated_successfully": "Diagramm erfolgreich aktualisiert!",
"configure_description": "Ändere den Diagrammtyp und andere Einstellungen für diese Visualisierung.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Abfrage konnte nicht ausgeführt werden",
"failed_to_load_chart": "Diagramm konnte nicht geladen werden",
"failed_to_load_chart_data": "Diagrammdaten konnten nicht geladen werden",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Diagramm konnte nicht gespeichert werden",
"field": "Feld",
"field_label_average_score": "Durchschnittliche Bewertung",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Keine gültigen Daten zur Anzeige",
"not_contains": "enthält nicht",
"not_equals": "ist nicht gleich",
"open_chart": "Diagramm {{name}} öffnen",
"open_chart": "Diagramm {name} öffnen",
"open_options": "Diagrammoptionen öffnen",
"or_filter_logic": "ODER",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Bitte wähle ein Dashboard aus",
"predefined_measures": "Vordefinierte Kennzahlen",
"preset": "Vorlage",
"preview_chart": "Vorschaudiagramm",
"query_executed_successfully": "Abfrage erfolgreich ausgeführt",
"reset_to_ai_suggestion": "Auf KI-Vorschlag zurücksetzen",
"save_and_add_to_dashboard": "Speichern und zum Dashboard hinzufügen",
"save_chart": "Diagramm speichern",
"save_chart_dialog_title": "Diagramm speichern",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Feld auswählen",
"select_measures": "Metriken auswählen...",
"select_preset": "Vorlage auswählen",
"showing_first_n_of": "Zeige die ersten {{n}} von {{count}} Zeilen",
"showing_first_n_of": "Zeige die ersten {n} von {count} Zeilen",
"start_date": "Startdatum",
"time_dimension": "Zeitdimension",
"time_dimension_title": "Zeitbasierte Gruppierung hinzufügen",
"time_dimension_toggle_description": "Beobachte Trends im Zeitverlauf."
"time_dimension_toggle_description": "Beobachte Trends im Zeitverlauf.",
"update_chart": "Diagramm aktualisieren"
},
"dashboards": {
"add_count_charts": "{count} Diagramm(e) hinzufügen",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Dashboard erstellen",
"create_dashboard_description": "Gib einen Namen für dein neues Dashboard ein.",
"create_failed": "Dashboard konnte nicht erstellt werden",
"create_new_chart": "Neues Diagramm erstellen",
"create_success": "Dashboard erfolgreich erstellt!",
"dashboard": "Dashboard",
"dashboard_delete_confirmation": "Bist du sicher, dass du dieses Dashboard löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Dashboard konnte nicht dupliziert werden",
"duplicate_success": "Dashboard erfolgreich dupliziert!",
"failed_to_load_chart_data": "Diagrammdaten konnten nicht geladen werden",
"no_charts_available_description": "Es gibt keine Diagramme, die zu diesem Dashboard hinzugefügt werden können. Entweder existieren noch keine Diagramme oder alle vorhandenen Diagramme wurden bereits hinzugefügt. Gehe zur Diagramm-Seite, um neue Diagramme zu erstellen.",
"no_charts_to_add_message": "Keine Diagramme zum Hinzufügen zu diesem Dashboard vorhanden.",
"no_dashboards_found": "Keine Dashboards gefunden.",
"no_data_message": "Keine Daten. Es gibt derzeit keine Informationen zum Anzeigen. Füge Diagramme hinzu, um dein Dashboard zu erstellen.",
"please_enter_name": "Bitte gib einen Dashboard-Namen ein"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Sie haben keine Feedback-Datensätze, über die Sie berichten können. Richten Sie Feedbackquellen ein, um Daten in das System einzuspeisen.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Richten Sie Feedbackquellen ein"
},
"api_keys": {
"add_api_key": "API-Key hinzufügen",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Verbinde deine App oder Website mit Formbricks.",
"cache_update_delay_description": "Wenn du Aktualisierungen an Umfragen, Kontakten, Aktionen oder anderen Daten vornimmst, kann es bis zu 1 Minute dauern, bis diese Änderungen in deiner lokalen App mit dem Formbricks SDK sichtbar werden.",
"cache_update_delay_title": "Änderungen werden nach ~1 Minute durch Caching übernommen",
"environment_id_legacy": "Umgebungs-ID (veraltet)",
"environment_id_legacy_alert": "Deine bestehende SDK-Konfiguration verwendet möglicherweise noch eine veraltete Umgebungs-ID.",
"environment_id_legacy_alert_link": "Erfahre, warum und wie du migrieren kannst.",
"formbricks_sdk_connected": "Formbricks SDK ist verbunden",
"formbricks_sdk_not_connected": "Formbricks SDK ist noch nicht verbunden.",
"formbricks_sdk_not_connected_description": "Füge das Formbricks SDK zu deiner Website oder App hinzu, um es mit Formbricks zu verbinden",
@@ -2389,7 +2405,7 @@
"most_popular": "Am beliebtesten",
"pending_change_removed": "Geplante Planänderung entfernt.",
"pending_plan_badge": "Geplant",
"pending_plan_change_description": "Dein Plan wechselt am {{date}} zu {{plan}}.",
"pending_plan_change_description": "Dein Plan wechselt am {date} zu {plan}.",
"pending_plan_change_title": "Geplante Planänderung",
"pending_plan_cta": "Geplant",
"per_month": "pro Monat",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Ein Feedback-Datensatz-Verzeichnis mit diesem Namen existiert bereits.",
"error_directory_name_required": "Verzeichnisname ist erforderlich.",
"error_directory_workspaces_invalid_org": "Einige der angegebenen Workspaces gehören nicht zu dieser Organisation.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Feedback-Verzeichnisse",
"no_access": "Du hast keine Berechtigung, Feedback-Datensatz-Verzeichnisse zu verwalten.",
"no_connectors": "Noch keine Connectoren mit diesem Verzeichnis verknüpft.",
"pause_connectors_confirmation_description": "Wenn du diese Connectoren pausierst, werden keine neuen Datensätze mehr hinzugefügt.",
"pause_connectors_confirmation_title": "Verknüpfte Connectoren pausieren?",
"select_workspaces_placeholder": "Workspaces auswählen...",
"show_archived": "Archivierte anzeigen",
"title": "Feedback-Datensatz-Verzeichnisse",
"unarchive": "Aus Archiv wiederherstellen"
"unarchive": "Aus Archiv wiederherstellen",
"unarchive_workspace_conflict": "Dieses Verzeichnis kann nicht wiederhergestellt werden, weil ein oder mehrere zugewiesene Workspaces archiviert sind.",
"workspace_access": "Workspace-Zugriff"
},
"general": {
"ai_data_analysis_disabled_for_organization": "KI-Datenanalyse ist für diese Organisation deaktiviert.",
"ai_data_analysis_enabled": "Datenanreicherung & -analyse (KI)",
"ai_data_analysis_enabled_description": "KI nutzen, um mehr aus deinen Daten herauszuholen richte Dashboards, Diagramme, Berichte und mehr ein. Greift auf deine Erfahrungsdaten zu.",
"ai_enabled": "Formbricks KI",
"ai_enabled_description": "Verwalte KI-gestützte Funktionen für diese Organisation.",
"ai_features_not_enabled_for_organization": "KI-Funktionen sind für diese Organisation nicht aktiviert.",
"ai_instance_not_configured": "KI wird auf Instanzebene über Umgebungsvariablen konfiguriert. Bitte deine:n Administrator:in, AI_PROVIDER, AI_MODEL und die passenden Provider-Zugangsdaten zu setzen, bevor du KI-Funktionen aktivierst.",
"ai_settings_updated_successfully": "KI-Einstellungen erfolgreich aktualisiert",
"ai_smart_tools_disabled_for_organization": "KI-Smart-Tools sind für diese Organisation deaktiviert.",
"ai_smart_tools_enabled": "Smarte Funktionen (KI)",
"ai_smart_tools_enabled_description": "KI, die dir hilft, in kürzerer Zeit mehr zu erreichen. Greift niemals auf mit Formbricks gesammelte Daten zu. Wird nur verwendet, um z. B. Umfragen in andere Sprachen zu übersetzen.",
"bulk_invite_warning_description": "Im kostenlosen Tarif erhalten alle Organisationsmitglieder automatisch die Rolle „Inhaber:in“.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Hier anmelden.",
"share_invite_link": "Einladungslink teilen",
"share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit deine Organisationsmitglieder deiner Organisation beitreten können:",
"test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet"
"test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet",
"unlock_ai_features_description": "KI-gestützte Übersetzungen, smarte Tools und Datenanalyse sind in höheren Plänen verfügbar. Upgrade jetzt und bring deine Umfragen mit KI auf das nächste Level.",
"unlock_ai_features_with_a_higher_plan": "Schalte KI-Funktionen mit einem höheren Plan frei"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Automatisch bei neuen Umfragen anmelden",
@@ -2757,7 +2777,19 @@
"address_line_2": "Adresszeile 2",
"adjust_survey_closed_message": "„Umfrage geschlossen“-Nachricht anpassen",
"adjust_survey_closed_message_description": "Ändere die Nachricht, die Besucher sehen, wenn die Umfrage geschlossen ist.",
"adjust_the_theme_in_the": "Passe das Theme im",
"adjust_the_theme_in_the": "Passe das Thema an in den",
"ai_data_analysis_disabled": "KI-Datenanalyse ist für diese Organisation deaktiviert.",
"ai_features_not_enabled": "KI-Funktionen sind für diese Organisation nicht aktiviert.",
"ai_instance_not_configured": "KI ist nicht konfiguriert. Kontaktiere deinen Administrator.",
"ai_smart_tools_disabled": "KI-Smart-Tools sind für diese Organisation deaktiviert.",
"ai_translate": "Mit KI übersetzen",
"ai_translating": "Übersetze mit KI... Bitte lasse dieses Fenster geöffnet.",
"ai_translation_all_fields_populated": "Alle Felder sind bereits übersetzt",
"ai_translation_complete": "KI-Übersetzung abgeschlossen",
"ai_translation_failed": "Übersetzung fehlgeschlagen",
"ai_translation_instance_not_configured": "KI ist auf dieser Instanz nicht konfiguriert. Kontaktiere deinen Administrator.",
"ai_translation_not_available": "KI-Übersetzung ist in deinem aktuellen Plan nicht verfügbar. Upgraden, um diese Funktion freizuschalten.",
"ai_translation_not_enabled": "KI-Smart-Tools sind für diese Organisation deaktiviert. Aktiviere sie in den Organisationseinstellungen.",
"all_are_true": "alle sind wahr",
"all_other_answers_will_continue_to": "Alle anderen Antworten werden weiterhin",
"allow_multi_select": "Mehrfachauswahl erlauben",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Sieh nach, welche Teams auf diesen Workspace zugreifen können."
},
"unify": {
"add_feedback_record": "Feedback-Datensatz hinzufügen",
"add_feedback_record_description": "Erstellen Sie manuell einen Feedback-Datensatz.",
"add_feedback_source": "Feedback-Quelle hinzufügen",
"add_source": "Quelle hinzufügen",
"allowed_values": "Zulässige Werte: {values}",
"api_ingestion": "API-Erfassung",
"api_ingestion_manage_api_keys": "API-Schlüssel verwalten",
"api_ingestion_settings_description": "Sende Feedback-Datensätze über die Management-API.",
"auto_generated": "Automatisch generiert",
"change_file": "Datei ändern",
"click_load_sample_csv": "Klick auf 'Beispiel-CSV laden', um Spalten zu sehen",
"click_to_upload": "Zum Hochladen klicken",
"collected_at": "Erfasst am",
"configure_import": "Import konfigurieren",
"configure_mapping": "Mapping konfigurieren",
"connection": "Verbindung",
"connector_created_successfully": "Connector erfolgreich erstellt",
"connector_deleted_successfully": "Connector erfolgreich gelöscht",
"connector_duplicated_successfully": "Connector erfolgreich dupliziert",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Wenn Du die Daten zweimal importierst, entstehen doppelte Einträge.",
"csv_inconsistent_columns": "Zeile {row} hat inkonsistente Spalten. Alle Zeilen müssen die gleichen Überschriften haben.",
"csv_max_records": "Maximal {max} Einträge erlaubt.",
"custom_source_type": "Benutzerdefinierter Quelltyp",
"custom_source_type_placeholder": "Geben Sie den benutzerdefinierten Quelltyp ein",
"default_connector_name_csv": "CSV-Import",
"default_connector_name_formbricks": "Formbricks-Umfrage-Verbindung",
"deselect_all": "Alle abwählen",
"discard_feedback_record_changes_description": "Ihre Änderungen gehen verloren, wenn Sie diese Schublade schließen.",
"discard_feedback_record_changes_title": "Nicht gespeicherte Änderungen verwerfen?",
"drop_a_field_here": "Ziehe ein Feld hierher",
"drop_field_or": "Feld ablegen oder",
"edit_csv_mapping": "CSV-Zuordnung bearbeiten",
@@ -3642,47 +3682,64 @@
"enum": "Aufzählung",
"failed_to_load_feedback_records": "Feedback-Einträge konnten nicht geladen werden",
"feedback_date": "Aktuelles Datum",
"feedback_record_created_successfully": "Feedback-Datensatz erfolgreich erstellt",
"feedback_record_details": "Details zum Feedback-Datensatz",
"feedback_record_details_description": "Überprüfen und aktualisieren Sie die Felder des Feedback-Datensatzes.",
"feedback_record_directory": "Feedback-Datensatz-Verzeichnis",
"feedback_record_fields": "Feedback-Eintragsfelder",
"feedback_record_mcp": "Feedback-Datensatz MCP",
"feedback_record_updated_successfully": "Feedback-Datensatz erfolgreich aktualisiert",
"feedback_record_value_required": "Für den ausgewählten Feldtyp ist ein Wert erforderlich",
"feedback_records": "Feedback-Einträge",
"feedback_records_refreshed": "Feedback-Einträge aktualisiert",
"feedback_sources": "Feedback-Quellen",
"feedback_sources_directory_access_multiple": "Neue Datensätze aus diesen Quellen werden gespeichert in: {directoryNames}",
"feedback_sources_directory_access_single": "Neue Datensätze aus dieser Quelle werden gespeichert in: {directoryNames}",
"feedback_sources_settings_description": "Verbinde und verwalte alle Feedback-Quellen für diesen Workspace.",
"field_group_id": "Feldgruppen-ID",
"field_group_label": "Feldgruppenbezeichnung",
"field_id": "Feld-ID",
"field_label": "Feldbezeichnung",
"field_type": "Feldtyp",
"formbricks_surveys": "Formbricks-Umfragen",
"frd_cannot_be_changed": "Das Feedback-Verzeichnis kann nach der Erstellung nicht mehr geändert werden.",
"go_to_feedback_record_directories": "Zu den Verzeichnis-Einstellungen",
"historical_import_complete": "Import abgeschlossen: {successes} erfolgreich, {failures} fehlgeschlagen, {skipped} übersprungen (keine Daten)",
"import_csv_data": "Feedback importieren",
"import_feedback": "Feedback importieren",
"import_historical_responses": "Bisherige Antworten importieren",
"import_historical_responses_description": "Importiere jetzt vorhandene Antworten aus dieser Umfrage.",
"import_rows": "{count} Zeilen importieren",
"import_via_source_name": "Import über „{sourceName}“",
"importing_data": "Daten werden importiert...",
"importing_historical_data": "Historische Daten werden importiert...",
"invalid_enum_values": "Ungültige Werte in der Spalte, die {field} zugeordnet ist",
"invalid_values_found": "Gefunden: {values} (Zeilen: {rows}) {extra}",
"load_sample_csv": "Beispiel-CSV laden",
"n_supported_questions": "{count} unterstützte Fragen",
"manage_directories": "Verzeichnisse verwalten",
"manage_feedback_sources": "Feedbackquellen verwalten",
"metadata": "Metadaten",
"metadata_key": "Metadatenschlüssel",
"metadata_read_only_entries": "Schreibgeschützte Metadatenwerte (keine Zeichenfolge)",
"metadata_value": "Metadatenwert",
"missing_feedback_source_title": "Feedback-Quelle fehlt?",
"no_feedback_record_directory_available": "Diesem Workspace ist kein Feedback-Datensatz-Verzeichnis zugewiesen. Erstelle oder weise zuerst eines zu.",
"no_feedback_records": "Noch keine Feedback-Einträge vorhanden. Einträge erscheinen hier, sobald deine Konnektoren Daten senden.",
"no_source_fields_loaded": "Noch keine Quellfelder geladen",
"no_sources_connected": "Noch keine Quellen verbunden. Füge eine Quelle hinzu, um loszulegen.",
"no_surveys_found": "Keine Umfragen in dieser Umgebung gefunden",
"optional": "Optional",
"or_drag_and_drop": "oder per Drag & Drop",
"question_selected": "<strong>{count}</strong> Frage ausgewählt. Jede Antwort auf diese Frage wird einen neuen Feedback-Eintrag erstellen.",
"question_type_not_supported": "Dieser Fragetyp wird nicht unterstützt",
"questions_selected": "<strong>{count}</strong> Fragen ausgewählt. Jede Antwort auf diese Fragen wird einen neuen Feedback-Eintrag erstellen.",
"records_will_go_to": "Datensätze gehen an",
"refresh_feedback_records": "Feedback-Einträge aktualisieren",
"refreshing_feedback_records": "Feedback-Einträge werden aktualisiert...",
"request_feedback_source": "Quellen-Integration anfragen",
"required": "Erforderlich",
"save_changes": "Änderungen speichern",
"select_a_survey_to_see_questions": "Wähle eine Umfrage aus, um ihre Fragen zu sehen",
"select_a_value": "Wähle einen Wert aus...",
"select_all": "Alle auswählen",
"select_feedback_record_directory": "Verzeichnis auswählen",
"select_feedback_record_source_type": "Wählen Sie den Quelltyp aus",
"select_questions": "Fragen auswählen",
"select_source_type_description": "Wähle die Art der Feedback-Quelle aus, die Du verbinden möchtest.",
"select_source_type_prompt": "Wähle die Art der Feedback-Quelle aus, die Du verbinden möchtest:",
"select_survey": "Umfrage auswählen",
"select_survey_and_questions": "Umfrage & Fragen auswählen",
"select_survey_questions_description": "Wähle aus, welche Umfragefragen FeedbackRecords erstellen sollen.",
@@ -3692,27 +3749,38 @@
"showing_rows": "3 von {count} Zeilen werden angezeigt",
"source": "Quelle",
"source_connect_csv_description": "Feedback aus CSV-Dateien importieren",
"source_connect_feedback_record_mcp_description": "Sende Feedback-Datensätze über die MCP-Integration.",
"source_connect_formbricks_description": "Feedback aus Deinen Formbricks-Umfragen verbinden",
"source_fields": "Quellfelder",
"source_id": "Quell-ID",
"source_name": "Quellenname",
"source_type": "Quellentyp",
"source_type_cannot_be_changed": "Quellentyp kann nicht geändert werden",
"sources": "Quellen",
"status_active": "In Bearbeitung",
"status_completed": "Abgeschlossen",
"status_draft": "Entwurf",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Fehler",
"status_paused": "Pausiert",
"status_live_sync": "Live-Synchronisierung",
"status_ready": "Bereit",
"submission_id": "Einreichungs-ID",
"survey_has_no_questions": "Diese Umfrage hat keine Fragen",
"survey_import_line": "{surveyName}: {responseCount} Antworten × {questionCount} Fragen = {total} Feedback-Datensätze",
"total_feedback_records": "Gesamt: {checked} von {total} Feedback-Datensätzen ausgewählt über {surveyCount} Umfragen",
"topics_and_subtopics": "Themen & Unterthemen",
"unify_feedback": "Feedback vereinheitlichen",
"update_mapping_description": "Aktualisiere die Zuordnungskonfiguration für diese Quelle.",
"updated_at": "Aktualisiert am",
"upload_csv_data_description": "Lade eine CSV-Datei hoch, um Feedback-Daten zu importieren.",
"upload_csv_file": "CSV-Datei hochladen",
"user_identifier": "Benutzer",
"value": "Wert"
"value": "Wert",
"value_boolean": "Wert (Boolescher Wert)",
"value_date": "Wert (Datum)",
"value_number": "Wert (Anzahl)",
"value_text": "Wert (Text)"
},
"xm-templates": {
"ces": "CES",
+103 -35
View File
@@ -125,6 +125,7 @@
"activity": "Activity",
"add": "Add",
"add_action": "Add action",
"add_chart": "Add chart",
"add_charts": "Add charts",
"add_existing_chart_description": "Search and select charts to add to this dashboard.",
"add_filter": "Add filter",
@@ -147,6 +148,7 @@
"apply_filters": "Apply filters",
"archived": "Archived",
"are_you_sure": "Are you sure?",
"ask": "Ask",
"attributes": "Attributes",
"back": "Back",
"billing": "Billing",
@@ -159,7 +161,7 @@
"change_workspace": "Change workspace",
"chart": "Chart",
"charts": "Charts",
"choice_n": "Choice {{n}}",
"choice_n": "Choice {n}",
"choices": "Choices",
"choose_organization": "Choose organization",
"choose_workspace": "Choose workspace",
@@ -172,7 +174,7 @@
"close": "Close",
"code": "Code",
"collapse_rows": "Collapse rows",
"column_n": "Column {{n}}",
"column_n": "Column {n}",
"completed": "Completed",
"configuration": "Configure",
"confirm": "Confirm",
@@ -212,6 +214,7 @@
"delete_what": "Delete {deleteWhat}",
"description": "Description",
"disable": "Disable",
"disabled": "Disabled",
"disallow": "Do not allow",
"discard": "Discard",
"dismissed": "Dismissed",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Failed to load organizations",
"failed_to_load_workspaces": "Failed to load workspaces",
"failed_to_parse_csv": "Failed to parse CSV",
"field_placeholder": "{{field}} Placeholder",
"field_placeholder": "{field} Placeholder",
"filter": "Filter",
"finish": "Finish",
"first_name": "First Name",
@@ -331,6 +334,7 @@
"not_authenticated": "You are not authenticated to perform this action.",
"not_authorized": "Not authorized",
"not_connected": "Not Connected",
"not_set": "Not set",
"note": "Note",
"notifications": "Notifications",
"number": "Number",
@@ -390,13 +394,14 @@
"report_survey": "Report Survey",
"request_trial_license": "Request trial license",
"reset_to_default": "Reset to default",
"resize": "Resize",
"response": "Response",
"response_id": "Response ID",
"responses": "Responses",
"restart": "Restart",
"retry": "Retry",
"role": "Role",
"row_n": "Row {{n}}",
"row_n": "Row {n}",
"saas": "SaaS",
"sales": "Sales",
"save": "Save",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Some files failed to upload",
"something_went_wrong": "Something went wrong",
"something_went_wrong_please_try_again": "Something went wrong. Please try again.",
"soon": "Soon",
"sort_by": "Sort by",
"start_free_trial": "Start free trial",
"status": "Status",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Bar Chart",
"chart_type_big_number": "Big Number",
"chart_type_line": "Line Chart",
"chart_type_not_supported": "Chart type \"{{chartType}}\" not yet supported",
"chart_type_not_supported": "Chart type \"{chartType}\" not yet supported",
"chart_type_pie": "Pie Chart",
"chart_updated_successfully": "Chart updated successfully!",
"configure_description": "Modify the chart type and other settings for this visualization.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Failed to execute query",
"failed_to_load_chart": "Failed to load chart",
"failed_to_load_chart_data": "Failed to load chart data",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Failed to save chart",
"field": "Field",
"field_label_average_score": "Average Score",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "No valid data to display",
"not_contains": "not contains",
"not_equals": "not equals",
"open_chart": "Open chart {{name}}",
"open_chart": "Open chart {name}",
"open_options": "Open chart options",
"or_filter_logic": "OR",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Please select a dashboard",
"predefined_measures": "Predefined Measures",
"preset": "Preset",
"preview_chart": "Preview chart",
"query_executed_successfully": "Query executed successfully",
"reset_to_ai_suggestion": "Reset to AI suggestion",
"save_and_add_to_dashboard": "Save & add to dashboard",
"save_chart": "Save Chart",
"save_chart_dialog_title": "Save Chart",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Select field",
"select_measures": "Select measures...",
"select_preset": "Select preset",
"showing_first_n_of": "Showing first {{n}} of {{count}} rows",
"showing_first_n_of": "Showing first {n} of {count} rows",
"start_date": "Start date",
"time_dimension": "Time Dimension",
"time_dimension_title": "Add time-based grouping",
"time_dimension_toggle_description": "Monitor trends over time."
"time_dimension_toggle_description": "Monitor trends over time.",
"update_chart": "Update chart"
},
"dashboards": {
"add_count_charts": "Add {count} chart(s)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Create dashboard",
"create_dashboard_description": "Enter a name for your new dashboard.",
"create_failed": "Failed to create dashboard",
"create_new_chart": "Create new chart",
"create_success": "Dashboard created successfully!",
"dashboard": "Dashboard",
"dashboard_delete_confirmation": "Are you sure you want to delete this dashboard? This action cannot be undone.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Failed to duplicate dashboard",
"duplicate_success": "Dashboard duplicated successfully!",
"failed_to_load_chart_data": "Failed to load chart data",
"no_charts_available_description": "There are no charts that can be added to this dashboard. Either no charts exist yet, or all existing charts have already been added. Go to the Charts page to create new charts.",
"no_charts_to_add_message": "No charts to add to this dashboard.",
"no_dashboards_found": "No dashboards found.",
"no_data_message": "No Data. There is currently no information to display. Add charts to build your dashboard.",
"please_enter_name": "Please enter a dashboard name"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "You don't have Feedback Records to report on. Setup Feedback Sources to feed data into the system.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Setup feedback sources"
},
"api_keys": {
"add_api_key": "Add API Key",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Connect your app or website to Formbricks.",
"cache_update_delay_description": "When you make updates to surveys, contacts, actions, or other data, it can take up to 1 minute for those changes to appear in your local app running the Formbricks SDK.",
"cache_update_delay_title": "Changes will be reflected after ~1 minute due to caching",
"environment_id_legacy": "Environment ID (legacy)",
"environment_id_legacy_alert": "Your existing SDK setup may still use a legacy Environment ID.",
"environment_id_legacy_alert_link": "Learn why and how to migrate.",
"formbricks_sdk_connected": "Formbricks SDK is connected",
"formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.",
"formbricks_sdk_not_connected_description": "Add the Formbricks SDK to your website or app to connect it with Formbricks",
@@ -2389,7 +2405,7 @@
"most_popular": "Most popular",
"pending_change_removed": "Scheduled plan change removed.",
"pending_plan_badge": "Scheduled",
"pending_plan_change_description": "Your plan will switch to {{plan}} on {{date}}.",
"pending_plan_change_description": "Your plan will switch to {plan} on {date}.",
"pending_plan_change_title": "Scheduled plan change",
"pending_plan_cta": "Scheduled",
"per_month": "per month",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "A feedback record directory with this name already exists.",
"error_directory_name_required": "Directory name is required.",
"error_directory_workspaces_invalid_org": "Some specified workspaces do not belong to this organization.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Feedback Directories",
"no_access": "You do not have permission to manage feedback record directories.",
"no_connectors": "No connectors linked to this directory yet.",
"pause_connectors_confirmation_description": "Pausing these connectors will stop new records from being added.",
"pause_connectors_confirmation_title": "Pause linked connectors?",
"select_workspaces_placeholder": "Select workspaces...",
"show_archived": "Show archived",
"title": "Feedback Record Directories",
"unarchive": "Unarchive"
"unarchive": "Unarchive",
"unarchive_workspace_conflict": "Cannot unarchive this directory because one or more assigned workspaces are archived.",
"workspace_access": "Workspace access"
},
"general": {
"ai_data_analysis_disabled_for_organization": "AI data analysis is disabled for this organization.",
"ai_data_analysis_enabled": "Data enrichment & analysis (AI)",
"ai_data_analysis_enabled_description": "AI to get more out of your data, setup dashboards, charts, reports and more. Touches your experience data.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "Manage AI-powered features for this organization.",
"ai_features_not_enabled_for_organization": "AI features are not enabled for this organization.",
"ai_instance_not_configured": "AI is configured at the instance level via environment variables. Ask your administrator to set AI_PROVIDER, AI_MODEL, and the matching provider credentials before enabling AI features.",
"ai_settings_updated_successfully": "AI settings updated successfully",
"ai_smart_tools_disabled_for_organization": "AI smart tools are disabled for this organization.",
"ai_smart_tools_enabled": "Smart functionality (AI)",
"ai_smart_tools_enabled_description": "AI to help you achieve more in less time. Never touches data collected with Formbricks. Only used to e.g. translate surveys to other languages.",
"bulk_invite_warning_description": "On the free plan, all organization members are always assigned the “Owner” role.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Sign up here.",
"share_invite_link": "Share Invite Link",
"share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:",
"test_email_sent_successfully": "Test email sent successfully"
"test_email_sent_successfully": "Test email sent successfully",
"unlock_ai_features_description": "AI-powered translations, smart tools, and data analysis are available on higher plans. Upgrade to supercharge your surveys with AI.",
"unlock_ai_features_with_a_higher_plan": "Unlock AI features with a higher plan"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Auto-subscribe to new surveys",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Adjust “Survey Closed” message",
"adjust_survey_closed_message_description": "Change the message visitors see when the survey is closed.",
"adjust_the_theme_in_the": "Adjust the theme in the",
"ai_data_analysis_disabled": "AI data analysis is disabled for this organization.",
"ai_features_not_enabled": "AI features are not enabled for this organization.",
"ai_instance_not_configured": "AI is not configured. Contact your administrator.",
"ai_smart_tools_disabled": "AI smart tools are disabled for this organization.",
"ai_translate": "Translate with AI",
"ai_translating": "Translating with AI... Please keep this modal open.",
"ai_translation_all_fields_populated": "All fields are already translated",
"ai_translation_complete": "AI translation complete",
"ai_translation_failed": "Translation failed",
"ai_translation_instance_not_configured": "AI is not configured on this instance. Contact your administrator.",
"ai_translation_not_available": "AI translation is not available on your current plan. Upgrade to unlock this feature.",
"ai_translation_not_enabled": "AI smart tools are disabled for this organization. Enable them in organization settings.",
"all_are_true": "all are true",
"all_other_answers_will_continue_to": "All other answers will continue to",
"allow_multi_select": "Allow multi-select",
@@ -3602,16 +3634,21 @@
"team_settings_description": "See which teams can access this workspace."
},
"unify": {
"add_feedback_record": "Add feedback record",
"add_feedback_record_description": "Create a feedback record manually.",
"add_feedback_source": "Add Feedback Source",
"add_source": "Add source",
"allowed_values": "Allowed values: {values}",
"api_ingestion": "API ingestion",
"api_ingestion_manage_api_keys": "Manage API keys",
"api_ingestion_settings_description": "Send feedback records using the Management API.",
"auto_generated": "Auto-generated",
"change_file": "Change file",
"click_load_sample_csv": "Click 'Load sample CSV' to see columns",
"click_to_upload": "Click to upload",
"collected_at": "Collected At",
"configure_import": "Configure import",
"configure_mapping": "Configure Mapping",
"connection": "Connection",
"connector_created_successfully": "Connector created successfully",
"connector_deleted_successfully": "Connector deleted successfully",
"connector_duplicated_successfully": "Connector duplicated successfully",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Importing data twice will create duplicate records.",
"csv_inconsistent_columns": "Row {row} has inconsistent columns. All rows must have the same headers.",
"csv_max_records": "Maximum {max} records allowed.",
"custom_source_type": "Custom source type",
"custom_source_type_placeholder": "Enter custom source type",
"default_connector_name_csv": "CSV Import",
"default_connector_name_formbricks": "Formbricks Survey Connection",
"deselect_all": "Deselect all",
"discard_feedback_record_changes_description": "Your changes will be lost if you close this drawer.",
"discard_feedback_record_changes_title": "Discard unsaved changes?",
"drop_a_field_here": "Drop a field here",
"drop_field_or": "Drop field or",
"edit_csv_mapping": "Edit CSV mapping",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Failed to load feedback records",
"feedback_date": "Current date",
"feedback_record_created_successfully": "Feedback record created successfully",
"feedback_record_details": "Feedback record details",
"feedback_record_details_description": "Review and update feedback record fields.",
"feedback_record_directory": "Feedback Record Directory",
"feedback_record_fields": "Feedback Record Fields",
"feedback_record_mcp": "Feedback Record MCP",
"feedback_record_updated_successfully": "Feedback record updated successfully",
"feedback_record_value_required": "A value is required for the selected field type",
"feedback_records": "Feedback Records",
"feedback_records_refreshed": "Feedback records refreshed",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
"field_group_id": "Field Group ID",
"field_group_label": "Field Group Label",
"field_id": "Field ID",
"field_label": "Field Label",
"field_type": "Field Type",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedback directory cannot be changed after creation.",
"go_to_feedback_record_directories": "Go to directories settings",
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
"import_csv_data": "Import feedback",
"import_feedback": "Import feedback",
"import_historical_responses": "Import historical responses",
"import_historical_responses_description": "Import existing responses from this survey now.",
"import_rows": "Import {count} rows",
"import_via_source_name": "Import via \"{sourceName}\"",
"importing_data": "Importing data...",
"importing_historical_data": "Importing historical data...",
"invalid_enum_values": "Invalid values in column mapped to {field}",
"invalid_values_found": "Found: {values} (rows: {rows}) {extra}",
"load_sample_csv": "Load sample CSV",
"n_supported_questions": "{count} supported questions",
"manage_directories": "Manage directories",
"manage_feedback_sources": "Manage feedback sources",
"metadata": "Metadata",
"metadata_key": "Metadata key",
"metadata_read_only_entries": "Read-only metadata values (non-string)",
"metadata_value": "Metadata value",
"missing_feedback_source_title": "Missing feedback source?",
"no_feedback_record_directory_available": "No feedback record directory assigned to this workspace. Create or assign one first.",
"no_feedback_records": "No feedback records yet. Records will appear here once your connectors start sending data.",
"no_source_fields_loaded": "No source fields loaded yet",
"no_sources_connected": "No sources connected yet. Add a source to get started.",
"no_surveys_found": "No surveys found in this environment",
"optional": "Optional",
"or_drag_and_drop": "or drag and drop",
"question_selected": "<strong>{count}</strong> question selected. Each response to these questions will create a new Feedback Record.",
"question_type_not_supported": "This question type is not supported",
"questions_selected": "<strong>{count}</strong> questions selected. Each response to these questions will create a new Feedback Record.",
"records_will_go_to": "Records will go to",
"refresh_feedback_records": "Refresh feedback records",
"refreshing_feedback_records": "Refreshing feedback records...",
"request_feedback_source": "Request source integration",
"required": "Required",
"save_changes": "Save changes",
"select_a_survey_to_see_questions": "Select a survey to see its questions",
"select_a_value": "Select a value...",
"select_all": "Select all",
"select_feedback_record_directory": "Select a directory",
"select_feedback_record_source_type": "Select source type",
"select_questions": "Select questions",
"select_source_type_description": "Select the type of feedback source you want to connect.",
"select_source_type_prompt": "Select the type of feedback source you want to connect:",
"select_survey": "Select Survey",
"select_survey_and_questions": "Select Survey & Questions",
"select_survey_questions_description": "Choose which survey questions should create FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Showing 3 of {count} rows",
"source": "source",
"source_connect_csv_description": "Import feedback from CSV files",
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
"source_connect_formbricks_description": "Connect feedback from your Formbricks surveys",
"source_fields": "Source Fields",
"source_id": "Source ID",
"source_name": "Source Name",
"source_type": "Source Type",
"source_type_cannot_be_changed": "Source type cannot be changed",
"sources": "Sources",
"status_active": "In Progress",
"status_completed": "Completed",
"status_draft": "Draft",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Error",
"status_paused": "Paused",
"status_live_sync": "Live sync",
"status_ready": "Ready",
"submission_id": "Submission ID",
"survey_has_no_questions": "This survey has no questions",
"survey_import_line": "{surveyName}: {responseCount} responses × {questionCount} questions = {total} Feedback Records",
"total_feedback_records": "Total: {checked} of {total} Feedback Records selected across {surveyCount} surveys",
"topics_and_subtopics": "Topics & Subtopics",
"unify_feedback": "Unify Feedback",
"update_mapping_description": "Update the mapping configuration for this source.",
"updated_at": "Updated at",
"upload_csv_data_description": "Upload a CSV file to import feedback data.",
"upload_csv_file": "Upload CSV File",
"user_identifier": "User",
"value": "Value"
"value": "Value",
"value_boolean": "Value (Boolean)",
"value_date": "Value (Date)",
"value_number": "Value (Number)",
"value_text": "Value (Text)"
},
"xm-templates": {
"ces": "CES",
+139 -71
View File
@@ -125,14 +125,15 @@
"activity": "Actividad",
"add": "Añadir",
"add_action": "Añadir acción",
"add_chart": "Agregar gráfico",
"add_charts": "Añadir gráficos",
"add_existing_chart_description": "Busca y selecciona gráficos para añadir a este panel.",
"add_filter": "Añadir filtro",
"add_logo": "Añadir logotipo",
"add_member": "Añadir miembro",
"add_new_workspace": "Añadir proyecto nuevo",
"add_new_workspace": "Añadir nuevo espacio de trabajo",
"add_to_team": "Añadir al equipo",
"add_workspace": "Añadir proyecto",
"add_workspace": "Añadir espacio de trabajo",
"all": "Todos",
"all_questions": "Todas las preguntas",
"allow": "Permitir",
@@ -147,6 +148,7 @@
"apply_filters": "Aplicar filtros",
"archived": "Archivado",
"are_you_sure": "¿Estás seguro?",
"ask": "Ask",
"attributes": "Atributos",
"back": "Atrás",
"billing": "Facturación",
@@ -159,10 +161,10 @@
"change_workspace": "Cambiar espacio de trabajo",
"chart": "Gráfico",
"charts": "Gráficos",
"choice_n": "Opción {{n}}",
"choice_n": "Opción {n}",
"choices": "Opciones",
"choose_organization": "Elegir organización",
"choose_workspace": "Elegir proyecto",
"choose_workspace": "Elegir espacio de trabajo",
"clear_all": "Borrar todo",
"clear_filters": "Borrar filtros",
"clear_selection": "Borrar selección",
@@ -172,7 +174,7 @@
"close": "Cerrar",
"code": "Código",
"collapse_rows": "Contraer filas",
"column_n": "Columna {{n}}",
"column_n": "Columna {n}",
"completed": "Completado",
"configuration": "Configurar",
"confirm": "Confirmar",
@@ -197,7 +199,7 @@
"create_new_organization": "Crear organización nueva",
"create_segment": "Crear segmento",
"create_survey": "Crear encuesta",
"create_workspace": "Crear proyecto",
"create_workspace": "Crear espacio de trabajo",
"created": "Creado",
"created_at": "Creado el",
"created_by": "Creado por",
@@ -212,6 +214,7 @@
"delete_what": "Eliminar {deleteWhat}",
"description": "Descripción",
"disable": "Desactivar",
"disabled": "Desactivado",
"disallow": "No permitir",
"discard": "Descartar",
"dismissed": "Descartado",
@@ -241,9 +244,9 @@
"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",
"failed_to_load_workspaces": "Error al cargar los espacios de trabajo",
"failed_to_parse_csv": "Error al analizar el CSV",
"field_placeholder": "Marcador de posición de {{field}}",
"field_placeholder": "Marcador de posición de {field}",
"filter": "Filtro",
"finish": "Finalizar",
"first_name": "Nombre",
@@ -331,6 +334,7 @@
"not_authenticated": "No estás autenticado para realizar esta acción.",
"not_authorized": "No autorizado",
"not_connected": "No conectado",
"not_set": "No establecido",
"note": "Nota",
"notifications": "Notificaciones",
"number": "Número",
@@ -390,13 +394,14 @@
"report_survey": "Reportar encuesta",
"request_trial_license": "Solicitar licencia de prueba",
"reset_to_default": "Restablecer a valores predeterminados",
"resize": "Cambiar tamaño",
"response": "Respuesta",
"response_id": "ID de respuesta",
"responses": "Respuestas",
"restart": "Reiniciar",
"retry": "Reintentar",
"role": "Rol",
"row_n": "Fila {{n}}",
"row_n": "Fila {n}",
"saas": "SaaS",
"sales": "Ventas",
"save": "Guardar",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Algunos archivos no se han podido subir",
"something_went_wrong": "Algo ha salido mal",
"something_went_wrong_please_try_again": "Algo ha salido mal. Por favor, inténtalo de nuevo.",
"soon": "Próximamente",
"sort_by": "Ordenar por",
"start_free_trial": "Iniciar prueba gratuita",
"status": "Estado",
@@ -474,7 +480,7 @@
"type": "Tipo",
"unify": "Unificar",
"unknown_survey": "Encuesta desconocida",
"unlock_more_workspaces_with_a_higher_plan": "Desbloquea más proyectos con un plan superior.",
"unlock_more_workspaces_with_a_higher_plan": "Desbloquea más espacios de trabajo con un plan superior.",
"update": "Actualizar",
"updated": "Actualizado",
"updated_at": "Actualizado el",
@@ -501,13 +507,13 @@
"weeks": "semanas",
"welcome_card": "Tarjeta de bienvenida",
"workspace": "Espacio de trabajo",
"workspace_configuration": "Configuración del proyecto",
"workspace_created_successfully": "Proyecto creado correctamente",
"workspace_creation_description": "Organiza las encuestas en proyectos para un mejor control de acceso.",
"workspace_id": "ID del proyecto",
"workspace_name": "Nombre del proyecto",
"workspace_configuration": "Configuración del espacio de trabajo",
"workspace_created_successfully": "Espacio de trabajo creado correctamente",
"workspace_creation_description": "Organiza las encuestas en espacios de trabajo para un mejor control de acceso.",
"workspace_id": "ID del espacio de trabajo",
"workspace_name": "Nombre del espacio de trabajo",
"workspace_name_placeholder": "p. ej. Formbricks",
"workspaces": "Proyectos",
"workspaces": "Espacios de trabajo",
"years": "años",
"yes": "Sí",
"you_are_downgraded_to_the_community_edition": "Has sido degradado a la edición Community.",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Gráfico de barras",
"chart_type_big_number": "Número grande",
"chart_type_line": "Gráfico de líneas",
"chart_type_not_supported": "El tipo de gráfico \"{{chartType}}\" aún no está soportado",
"chart_type_not_supported": "El tipo de gráfico \"{chartType}\" aún no está soportado",
"chart_type_pie": "Gráfico circular",
"chart_updated_successfully": "¡Gráfico actualizado correctamente!",
"configure_description": "Modifica el tipo de gráfico y otros ajustes para esta visualización.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Error al ejecutar la consulta",
"failed_to_load_chart": "Error al cargar el gráfico",
"failed_to_load_chart_data": "Error al cargar los datos del gráfico",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Error al guardar el gráfico",
"field": "Campo",
"field_label_average_score": "Puntuación media",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "No hay datos válidos para mostrar",
"not_contains": "no contiene",
"not_equals": "no es igual a",
"open_chart": "Abrir gráfico {{name}}",
"open_chart": "Abrir gráfico {name}",
"open_options": "Abrir opciones del gráfico",
"or_filter_logic": "O",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Selecciona un panel de control",
"predefined_measures": "Medidas predefinidas",
"preset": "Preajuste",
"preview_chart": "Vista previa del gráfico",
"query_executed_successfully": "Consulta ejecutada correctamente",
"reset_to_ai_suggestion": "Restablecer a sugerencia de IA",
"save_and_add_to_dashboard": "Guardar y agregar al panel",
"save_chart": "Guardar gráfico",
"save_chart_dialog_title": "Guardar gráfico",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Seleccionar campo",
"select_measures": "Seleccionar medidas...",
"select_preset": "Seleccionar preajuste",
"showing_first_n_of": "Mostrando las primeras {{n}} de {{count}} filas",
"showing_first_n_of": "Mostrando las primeras {n} de {count} filas",
"start_date": "Fecha de inicio",
"time_dimension": "Dimensión temporal",
"time_dimension_title": "Añadir agrupación temporal",
"time_dimension_toggle_description": "Supervisa las tendencias a lo largo del tiempo."
"time_dimension_toggle_description": "Supervisa las tendencias a lo largo del tiempo.",
"update_chart": "Cuadro de actualización"
},
"dashboards": {
"add_count_charts": "Añadir {count} gráfico(s)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Crear panel",
"create_dashboard_description": "Introduce un nombre para tu panel de control nuevo.",
"create_failed": "Error al crear el panel de control",
"create_new_chart": "Crear nuevo gráfico",
"create_success": "Panel de control creado correctamente",
"dashboard": "Panel",
"dashboard_delete_confirmation": "¿Estás seguro de que quieres eliminar este panel? Esta acción no se puede deshacer.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Error al duplicar el panel de control",
"duplicate_success": "Panel de control duplicado correctamente",
"failed_to_load_chart_data": "Error al cargar los datos del gráfico",
"no_charts_available_description": "No hay gráficos que se puedan añadir a este panel. O bien no existen gráficos todavía, o todos los gráficos existentes ya se han añadido. Ve a la página de Gráficos para crear nuevos gráficos.",
"no_charts_to_add_message": "No hay gráficos para añadir a este panel.",
"no_dashboards_found": "No se han encontrado paneles de control.",
"no_data_message": "Sin datos. Actualmente no hay información que mostrar. Añade gráficos para crear tu panel.",
"please_enter_name": "Por favor, introduce un nombre para el panel de control"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "No tienes registros de comentarios sobre los que informar. Configure fuentes de comentarios para introducir datos en el sistema.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Configurar fuentes de comentarios"
},
"api_keys": {
"add_api_key": "Añadir clave API",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Conecta tu aplicación o sitio web a Formbricks.",
"cache_update_delay_description": "Cuando realizas actualizaciones en encuestas, contactos, acciones u otros datos, puede tardar hasta 1 minuto en que esos cambios aparezcan en tu aplicación local que ejecuta el SDK de Formbricks.",
"cache_update_delay_title": "Los cambios se reflejarán después de ~1 minuto debido al almacenamiento en caché",
"environment_id_legacy": "ID de entorno (heredado)",
"environment_id_legacy_alert": "Tu configuración actual del SDK puede seguir utilizando un ID de entorno heredado.",
"environment_id_legacy_alert_link": "Descubre por qué y cómo migrar.",
"formbricks_sdk_connected": "El SDK de Formbricks está conectado",
"formbricks_sdk_not_connected": "El SDK de Formbricks aún no está conectado.",
"formbricks_sdk_not_connected_description": "Añade el SDK de Formbricks a tu sitio web o aplicación para conectarlo con Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Logo de Formbricks",
"general": {
"cannot_delete_only_workspace": "Este es tu único proyecto, no se puede eliminar. Crea primero un proyecto nuevo.",
"cannot_delete_only_workspace": "Este es tu único espacio de trabajo, no se puede eliminar. Crea primero un nuevo espacio de trabajo.",
"custom_scripts": "Scripts personalizados",
"custom_scripts_card_description": "Añade scripts de seguimiento y píxeles a todas las encuestas con enlace en este espacio de trabajo.",
"custom_scripts_description": "Los scripts se inyectarán en el <head> de todas las páginas de encuestas con enlace.",
@@ -1989,20 +2005,20 @@
"custom_scripts_placeholder": "<!-- Pega tus scripts de seguimiento aquí -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personalizados actualizados correctamente",
"custom_scripts_warning": "Los scripts se ejecutan con acceso completo al navegador. Solo añade scripts de fuentes confiables.",
"delete_workspace": "Eliminar proyecto",
"delete_workspace": "Eliminar espacio de trabajo",
"delete_workspace_confirmation": "¿Estás seguro de que quieres eliminar {workspaceName}? Esta acción no se puede deshacer.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {workspaceName} incluyendo todas las encuestas, respuestas, personas, acciones y atributos.",
"delete_workspace_settings_description": "Eliminar proyecto con todas las encuestas, respuestas, personas, acciones y atributos. Esto no se puede deshacer.",
"error_saving_workspace_information": "Error al guardar la información del proyecto",
"only_owners_or_managers_can_delete_workspaces": "Solo los propietarios o administradores pueden eliminar proyectos",
"delete_workspace_settings_description": "Elimina el espacio de trabajo con todas las encuestas, respuestas, personas, acciones y atributos. Esta acción no se puede deshacer.",
"error_saving_workspace_information": "Error al guardar la información del espacio de trabajo",
"only_owners_or_managers_can_delete_workspaces": "Solo los propietarios o administradores pueden eliminar espacios de trabajo",
"recontact_waiting_time": "Periodo de espera (entre encuestas)",
"recontact_waiting_time_settings_description": "Controla con qué frecuencia se puede encuestar a los usuarios en todas las encuestas de sitio web y aplicación de este espacio de trabajo.",
"this_action_cannot_be_undone": "Esta acción no se puede deshacer.",
"wait_x_days_before_showing_next_survey": "Esperar X días antes de mostrar la siguiente encuesta:",
"waiting_period_updated_successfully": "Periodo de espera actualizado correctamente",
"whats_your_workspace_called": "¿Cómo se llama tu proyecto?",
"workspace_deleted_successfully": "Proyecto eliminado correctamente",
"workspace_name_settings_description": "Cambia el nombre de tu proyecto.",
"whats_your_workspace_called": "¿Cómo se llama tu espacio de trabajo?",
"workspace_deleted_successfully": "Espacio de trabajo eliminado correctamente",
"workspace_name_settings_description": "Cambia el nombre de tu espacio de trabajo.",
"workspace_name_updated_successfully": "Nombre del espacio de trabajo actualizado correctamente"
},
"integrations": {
@@ -2389,7 +2405,7 @@
"most_popular": "Más popular",
"pending_change_removed": "Cambio de plan programado eliminado.",
"pending_plan_badge": "Programado",
"pending_plan_change_description": "Tu plan cambiará a {{plan}} el {{date}}.",
"pending_plan_change_description": "Tu plan cambiará a {plan} el {date}.",
"pending_plan_change_title": "Cambio de plan programado",
"pending_plan_cta": "Programado",
"per_month": "por mes",
@@ -2442,7 +2458,7 @@
"trial_payment_method_added_description": "¡Todo listo! Tu plan Pro continuará automáticamente cuando termine el periodo de prueba.",
"trial_title": "¡Consigue Formbricks Pro gratis!",
"unlimited_responses": "Respuestas ilimitadas",
"unlimited_workspaces": "Proyectos ilimitados",
"unlimited_workspaces": "Espacios de trabajo ilimitados",
"upgrade": "Actualizar",
"upgrade_now": "Actualizar ahora",
"usage_cycle": "Usage cycle",
@@ -2465,7 +2481,7 @@
"pretty_url": "URL bonita",
"survey_name": "Nombre de la encuesta",
"title": "URL bonitas",
"workspace": "Proyecto"
"workspace": "Espacio de trabajo"
},
"enterprise": {
"audit_logs": "Registros de auditoría",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Ya existe un directorio de registros de comentarios con este nombre.",
"error_directory_name_required": "El nombre del directorio es obligatorio.",
"error_directory_workspaces_invalid_org": "Algunos de los espacios de trabajo especificados no pertenecen a esta organización.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Directorios de Feedback",
"no_access": "No tienes permiso para gestionar los directorios de registros de feedback.",
"no_connectors": "Aún no hay conectores vinculados a este directorio.",
"pause_connectors_confirmation_description": "Si pausas estos conectores, no se añadirán nuevos registros.",
"pause_connectors_confirmation_title": "¿Pausar conectores vinculados?",
"select_workspaces_placeholder": "Selecciona espacios de trabajo...",
"show_archived": "Mostrar archivados",
"title": "Directorios de Registros de Feedback",
"unarchive": "Desarchivar"
"unarchive": "Desarchivar",
"unarchive_workspace_conflict": "No se puede desarchivar este directorio porque uno o más espacios de trabajo asignados están archivados.",
"workspace_access": "Acceso al espacio de trabajo"
},
"general": {
"ai_data_analysis_disabled_for_organization": "El análisis y enriquecimiento de datos con IA está deshabilitado para esta organización.",
"ai_data_analysis_enabled": "Enriquecimiento y análisis de datos (IA)",
"ai_data_analysis_enabled_description": "IA para sacar más partido a tus datos, configurar paneles, gráficos, informes y más. Accede a los datos de experiencia.",
"ai_enabled": "IA de Formbricks",
"ai_enabled_description": "Gestiona las funciones impulsadas por IA para esta organización.",
"ai_features_not_enabled_for_organization": "Las funciones de IA no están habilitadas para esta organización.",
"ai_instance_not_configured": "La IA se configura a nivel de instancia mediante variables de entorno. Pide a tu administrador que configure AI_PROVIDER, las credenciales de ese proveedor y la lista de modelos correspondiente antes de habilitar las funciones de IA.",
"ai_settings_updated_successfully": "Configuración de IA actualizada correctamente",
"ai_smart_tools_disabled_for_organization": "Las herramientas inteligentes de IA están deshabilitadas para esta organización.",
"ai_smart_tools_enabled": "Funcionalidad inteligente (IA)",
"ai_smart_tools_enabled_description": "IA para ayudarte a conseguir más en menos tiempo. Nunca accede a los datos recopilados con Formbricks. Solo se usa para, por ejemplo, traducir encuestas a otros idiomas.",
"bulk_invite_warning_description": "En el plan gratuito, a todos los miembros de la organización se les asigna siempre el rol de \"Propietario\".",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "No puedes abandonar esta organización ya que es tu única organización. Crea una nueva organización primero.",
"copy_invite_link_to_clipboard": "Copiar enlace de invitación al portapapeles",
"create_new_organization": "Crear nueva organización",
"create_new_organization_description": "Crea una nueva organización para gestionar un conjunto diferente de proyectos.",
"create_new_organization_description": "Crea una nueva organización para gestionar un conjunto diferente de espacios de trabajo.",
"customize_email_with_a_higher_plan": "Personaliza el correo electrónico con un plan superior",
"delete_member_confirmation": "Los miembros eliminados perderán acceso a todos los proyectos y encuestas de tu organización.",
"delete_member_confirmation": "Los miembros eliminados perderán el acceso a todos los espacios de trabajo y encuestas de tu organización.",
"delete_organization": "Eliminar organización",
"delete_organization_description": "Eliminar organización con todos sus proyectos incluyendo todas las encuestas, respuestas, personas, acciones y atributos",
"delete_organization_description": "Elimina la organización con todos sus espacios de trabajo, incluyendo todas las encuestas, respuestas, personas, acciones y atributos",
"delete_organization_warning": "Antes de proceder con la eliminación de esta organización, ten en cuenta las siguientes consecuencias:",
"delete_organization_warning_1": "Eliminación permanente de todos los proyectos vinculados a esta organización.",
"delete_organization_warning_1": "Eliminación permanente de todos los espacios de trabajo vinculados a esta organización.",
"delete_organization_warning_2": "Esta acción no se puede deshacer. Si desaparece, desaparece para siempre.",
"delete_organization_warning_3": "Por favor, introduce {organizationName} en el siguiente campo para confirmar la eliminación definitiva de esta organización:",
"eliminate_branding_with_whitelabel": "Elimina la marca Formbricks y habilita opciones adicionales de personalización de marca blanca.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Regístrate aquí.",
"share_invite_link": "Compartir enlace de invitación",
"share_this_link_to_let_your_organization_member_join_your_organization": "Comparte este enlace para permitir que los miembros de tu organización se unan a tu organización:",
"test_email_sent_successfully": "Correo electrónico de prueba enviado correctamente"
"test_email_sent_successfully": "Correo electrónico de prueba enviado correctamente",
"unlock_ai_features_description": "Las traducciones impulsadas por IA, herramientas inteligentes y análisis de datos están disponibles en planes superiores. Mejora tu plan para potenciar tus encuestas con IA.",
"unlock_ai_features_with_a_higher_plan": "Desbloquea funciones de IA con un plan superior"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Suscripción automática a nuevas encuestas",
@@ -2669,10 +2689,10 @@
},
"teams": {
"add_members_description": "Añade miembros al equipo y determina su rol.",
"add_workspaces_description": "Controla a qué proyectos pueden acceder los miembros del equipo.",
"add_workspaces_description": "Controla a qué espacios de trabajo pueden acceder los miembros del equipo.",
"all_members_added": "Todos los miembros añadidos a este equipo.",
"all_workspaces_added": "Todos los proyectos añadidos a este equipo.",
"are_you_sure_you_want_to_delete_this_team": "¿Estás seguro de que quieres eliminar este equipo? Esto también elimina el acceso a todos los proyectos y encuestas asociados con este equipo.",
"all_workspaces_added": "Todos los espacios de trabajo añadidos a este equipo.",
"are_you_sure_you_want_to_delete_this_team": "¿Estás seguro de que quieres eliminar este equipo? Esto también elimina el acceso a todos los espacios de trabajo y encuestas asociadas con este equipo.",
"billing_role_description": "Solo tienen acceso a la información de facturación.",
"bulk_invite": "Invitación masiva",
"contributor": "Colaborador",
@@ -2688,14 +2708,14 @@
"manage": "Gestionar",
"manage_team": "Gestionar equipo",
"manage_team_disabled": "Solo los propietarios de la organización, gestores y administradores de equipo pueden gestionar equipos.",
"manager_role_description": "Los gestores pueden acceder a todos los proyectos y añadir y eliminar miembros.",
"manager_role_description": "Los gestores pueden acceder a todos los espacios de trabajo y añadir o eliminar miembros.",
"member": "Miembro",
"member_role_description": "Los miembros pueden trabajar en proyectos seleccionados.",
"member_role_info_message": "Para dar a los nuevos miembros acceso a un proyecto, por favor añádelos a un equipo a continuación. Con los equipos puedes gestionar quién tiene acceso a qué proyecto.",
"member_role_description": "Los miembros pueden trabajar en los espacios de trabajo seleccionados.",
"member_role_info_message": "Para dar acceso a nuevos miembros a un espacio de trabajo, añádelos a un equipo a continuación. Con los equipos puedes gestionar quién tiene acceso a qué espacio de trabajo.",
"organization_role": "Rol en la organización",
"owner_role_description": "Los propietarios tienen control total sobre la organización.",
"please_fill_all_member_fields": "Por favor, rellena todos los campos para añadir un nuevo miembro.",
"please_fill_all_workspace_fields": "Por favor, rellena todos los campos para añadir un proyecto nuevo.",
"please_fill_all_workspace_fields": "Por favor, rellena todos los campos para añadir un nuevo espacio de trabajo.",
"read": "Lectura",
"read_write": "Lectura y escritura",
"team_admin": "Administrador de equipo",
@@ -2708,8 +2728,8 @@
"team_settings_description": "Gestiona miembros del equipo, derechos de acceso y más.",
"team_updated_successfully": "Equipo actualizado correctamente",
"teams": "Equipos",
"teams_description": "Asigna miembros a equipos y da acceso a los equipos a proyectos.",
"unlock_teams_description": "Gestiona qué miembros de la organización tienen acceso a proyectos y encuestas específicos.",
"teams_description": "Asigna miembros a equipos y otorga a los equipos acceso a los espacios de trabajo.",
"unlock_teams_description": "Gestiona qué miembros de la organización tienen acceso a espacios de trabajo y encuestas específicos.",
"unlock_teams_title": "Desbloquea Equipos con un plan superior.",
"upgrade_plan_notice_message": "Desbloquea Roles de Organización con un plan superior.",
"you_are_a_member": "Eres miembro"
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Ajustar mensaje 'Encuesta cerrada'",
"adjust_survey_closed_message_description": "Cambiar el mensaje que ven los visitantes cuando la encuesta está cerrada.",
"adjust_the_theme_in_the": "Ajustar el tema en el",
"ai_data_analysis_disabled": "El análisis de datos con IA está deshabilitado para esta organización.",
"ai_features_not_enabled": "Las funciones de IA no están habilitadas para esta organización.",
"ai_instance_not_configured": "La IA no está configurada. Contacta con tu administrador.",
"ai_smart_tools_disabled": "Las herramientas inteligentes de IA están deshabilitadas para esta organización.",
"ai_translate": "Traducir con IA",
"ai_translating": "Traduciendo con IA... Por favor, mantén este modal abierto.",
"ai_translation_all_fields_populated": "Todos los campos ya están traducidos",
"ai_translation_complete": "Traducción con IA completada",
"ai_translation_failed": "La traducción ha fallado",
"ai_translation_instance_not_configured": "La IA no está configurada en esta instancia. Contacta con tu administrador.",
"ai_translation_not_available": "La traducción por IA no está disponible en tu plan actual. Mejora tu plan para desbloquear esta función.",
"ai_translation_not_enabled": "Las herramientas inteligentes de IA están desactivadas para esta organización. Actívalas en la configuración de la organización.",
"all_are_true": "todas son verdaderas",
"all_other_answers_will_continue_to": "Todas las demás respuestas continuarán",
"allow_multi_select": "Permitir selección múltiple",
@@ -3043,7 +3075,7 @@
"options_used_in_logic_bulk_error": "Las siguientes opciones se utilizan en la lógica: {questionIndexes}. Por favor, elimínalas de la lógica primero.",
"override_theme_with_individual_styles_for_this_survey": "Anular el tema con estilos individuales para esta encuesta.",
"overwrite_global_waiting_time": "Establecer periodo de espera personalizado",
"overwrite_global_waiting_time_description": "Anular la configuración del proyecto solo para esta encuesta.",
"overwrite_global_waiting_time_description": "Anula la configuración del espacio de trabajo solo para esta encuesta.",
"overwrite_placement": "Sobrescribir ubicación",
"overwrite_survey_logo": "Establecer logotipo personalizado para la encuesta",
"overwrite_the_global_placement_of_the_survey": "Sobrescribir la ubicación global de la encuesta",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Consulta qué equipos pueden acceder a este espacio de trabajo."
},
"unify": {
"add_feedback_record": "Agregar registro de comentarios",
"add_feedback_record_description": "Cree un registro de comentarios manualmente.",
"add_feedback_source": "Añadir fuente de feedback",
"add_source": "Añadir fuente",
"allowed_values": "Valores permitidos: {values}",
"api_ingestion": "Ingesta de API",
"api_ingestion_manage_api_keys": "Gestionar claves de API",
"api_ingestion_settings_description": "Envía registros de feedback mediante la API de gestión.",
"auto_generated": "Generado automáticamente",
"change_file": "Cambiar archivo",
"click_load_sample_csv": "Haz clic en 'Cargar CSV de muestra' para ver las columnas",
"click_to_upload": "Haz clic para subir",
"collected_at": "Recopilado el",
"configure_import": "Configurar importación",
"configure_mapping": "Configurar asignación",
"connection": "Conexión",
"connector_created_successfully": "Conector creado correctamente",
"connector_deleted_successfully": "Conector eliminado correctamente",
"connector_duplicated_successfully": "Conector duplicado correctamente",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Importar datos dos veces creará registros duplicados.",
"csv_inconsistent_columns": "La fila {row} tiene columnas inconsistentes. Todas las filas deben tener los mismos encabezados.",
"csv_max_records": "Máximo de {max} registros permitidos.",
"custom_source_type": "Tipo de fuente personalizado",
"custom_source_type_placeholder": "Ingrese el tipo de fuente personalizado",
"default_connector_name_csv": "Importación CSV",
"default_connector_name_formbricks": "Conexión de encuesta de Formbricks",
"deselect_all": "Deseleccionar todo",
"discard_feedback_record_changes_description": "Sus cambios se perderán si cierra este cajón.",
"discard_feedback_record_changes_title": "¿Descartar los cambios no guardados?",
"drop_a_field_here": "Suelta un campo aquí",
"drop_field_or": "Suelta el campo o",
"edit_csv_mapping": "Editar mapeo de CSV",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Error al cargar los registros de comentarios",
"feedback_date": "Fecha actual",
"feedback_record_created_successfully": "Registro de comentarios creado correctamente",
"feedback_record_details": "Detalles del registro de comentarios",
"feedback_record_details_description": "Revise y actualice los campos del registro de comentarios.",
"feedback_record_directory": "Directorio de Registros de Comentarios",
"feedback_record_fields": "Campos de registro de comentarios",
"feedback_record_mcp": "MCP de registros de feedback",
"feedback_record_updated_successfully": "Registro de comentarios actualizado correctamente",
"feedback_record_value_required": "Se requiere un valor para el tipo de campo seleccionado",
"feedback_records": "Registros de comentarios",
"feedback_records_refreshed": "Registros de comentarios actualizados",
"feedback_sources": "Fuentes de feedback",
"feedback_sources_directory_access_multiple": "Los nuevos registros de estas fuentes se almacenarán en: {directoryNames}",
"feedback_sources_directory_access_single": "Los nuevos registros de esta fuente se almacenarán en: {directoryNames}",
"feedback_sources_settings_description": "Conecta y gestiona todas las fuentes de feedback para este espacio de trabajo.",
"field_group_id": "ID de grupo de campos",
"field_group_label": "Etiqueta de grupo de campos",
"field_id": "ID de campo",
"field_label": "Etiqueta de campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "El directorio de comentarios no se puede cambiar después de su creación.",
"go_to_feedback_record_directories": "Ir a la configuración de directorios",
"historical_import_complete": "Importación completada: {successes} correctas, {failures} fallidas, {skipped} omitidas (sin datos)",
"import_csv_data": "Importar comentarios",
"import_feedback": "Importar comentarios",
"import_historical_responses": "Importar respuestas históricas",
"import_historical_responses_description": "Importa las respuestas existentes de esta encuesta ahora.",
"import_rows": "Importar {count} filas",
"import_via_source_name": "Importar mediante \"{sourceName}\"",
"importing_data": "Importando datos...",
"importing_historical_data": "Importando datos históricos...",
"invalid_enum_values": "Valores no válidos en la columna asignada a {field}",
"invalid_values_found": "Encontrados: {values} (filas: {rows}) {extra}",
"load_sample_csv": "Cargar CSV de muestra",
"n_supported_questions": "{count} preguntas compatibles",
"manage_directories": "Gestionar directorios",
"manage_feedback_sources": "Administrar fuentes de comentarios",
"metadata": "Metadatos",
"metadata_key": "Clave de metadatos",
"metadata_read_only_entries": "Valores de metadatos de solo lectura (no cadenas)",
"metadata_value": "Valor de metadatos",
"missing_feedback_source_title": "¿Falta alguna fuente de feedback?",
"no_feedback_record_directory_available": "No hay ningún directorio de registros de comentarios asignado a este espacio de trabajo. Crea o asigna uno primero.",
"no_feedback_records": "Aún no hay registros de comentarios. Los registros aparecerán aquí una vez que tus conectores empiecen a enviar datos.",
"no_source_fields_loaded": "Aún no se han cargado campos de origen",
"no_sources_connected": "Aún no hay fuentes conectadas. Añade una fuente para empezar.",
"no_surveys_found": "No se encontraron encuestas en este entorno",
"optional": "Opcional",
"or_drag_and_drop": "o arrastra y suelta",
"question_selected": "<strong>{count}</strong> pregunta seleccionada. Cada respuesta a esta pregunta creará un registro de feedback nuevo.",
"question_type_not_supported": "Este tipo de pregunta no es compatible",
"questions_selected": "<strong>{count}</strong> preguntas seleccionadas. Cada respuesta a estas preguntas creará un registro de feedback nuevo.",
"records_will_go_to": "Los registros se enviarán a",
"refresh_feedback_records": "Actualizar los registros de comentarios",
"refreshing_feedback_records": "Actualizando registros de comentarios...",
"request_feedback_source": "Solicitar integración de fuente",
"required": "Obligatorio",
"save_changes": "Guardar cambios",
"select_a_survey_to_see_questions": "Selecciona una encuesta para ver sus preguntas",
"select_a_value": "Selecciona un valor...",
"select_all": "Seleccionar todo",
"select_feedback_record_directory": "Selecciona un directorio",
"select_feedback_record_source_type": "Seleccionar tipo de fuente",
"select_questions": "Seleccionar preguntas",
"select_source_type_description": "Selecciona el tipo de fuente de feedback que quieres conectar.",
"select_source_type_prompt": "Selecciona el tipo de fuente de feedback que quieres conectar:",
"select_survey": "Seleccionar encuesta",
"select_survey_and_questions": "Seleccionar encuesta y preguntas",
"select_survey_questions_description": "Elige qué preguntas de la encuesta deben crear FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Mostrando 3 de {count} filas",
"source": "origen",
"source_connect_csv_description": "Importar feedback desde archivos CSV",
"source_connect_feedback_record_mcp_description": "Envía registros de feedback a través de la integración MCP.",
"source_connect_formbricks_description": "Conectar feedback de tus encuestas de Formbricks",
"source_fields": "Campos de origen",
"source_id": "ID de fuente",
"source_name": "Nombre de origen",
"source_type": "Tipo de fuente",
"source_type_cannot_be_changed": "El tipo de origen no se puede cambiar",
"sources": "Orígenes",
"status_active": "En progreso",
"status_completed": "Completado",
"status_draft": "Borrador",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Error",
"status_paused": "Pausado",
"status_live_sync": "Sincronización en vivo",
"status_ready": "Listo",
"submission_id": "ID de envío",
"survey_has_no_questions": "Esta encuesta no tiene preguntas",
"survey_import_line": "{surveyName}: {responseCount} respuestas × {questionCount} preguntas = {total} registros de feedback",
"total_feedback_records": "Total: {checked} de {total} registros de feedback seleccionados en {surveyCount} encuestas",
"topics_and_subtopics": "Temas y subtemas",
"unify_feedback": "Unificar feedback",
"update_mapping_description": "Actualiza la configuración de mapeo para esta fuente.",
"updated_at": "Actualizado el",
"upload_csv_data_description": "Sube un archivo CSV para importar datos de comentarios.",
"upload_csv_file": "Subir archivo CSV",
"user_identifier": "Usuario",
"value": "Valor"
"value": "Valor",
"value_boolean": "Valor (booleano)",
"value_date": "Valor (Fecha)",
"value_number": "Valor (Número)",
"value_text": "Valor (Texto)"
},
"xm-templates": {
"ces": "CES",
+132 -64
View File
@@ -125,14 +125,15 @@
"activity": "Activité",
"add": "Ajouter",
"add_action": "Ajouter une action",
"add_chart": "Ajouter un graphique",
"add_charts": "Ajouter des graphiques",
"add_existing_chart_description": "Recherchez et sélectionnez des graphiques à ajouter à ce tableau de bord.",
"add_filter": "Ajouter un filtre",
"add_logo": "Ajouter un logo",
"add_member": "Ajouter un membre",
"add_new_workspace": "Ajouter un nouveau projet",
"add_new_workspace": "Ajouter un nouvel espace de travail",
"add_to_team": "Ajouter à l'équipe",
"add_workspace": "Ajouter un projet",
"add_workspace": "Ajouter un espace de travail",
"all": "Tout",
"all_questions": " toutes les questions",
"allow": "Autoriser",
@@ -147,6 +148,7 @@
"apply_filters": "Appliquer des filtres",
"archived": "Archivé",
"are_you_sure": "Es-tu sûr ?",
"ask": "Ask",
"attributes": "Attributs",
"back": "Retour",
"billing": "Facturation",
@@ -159,10 +161,10 @@
"change_workspace": "Changer d'espace de travail",
"chart": "Graphique",
"charts": "Graphiques",
"choice_n": "Choix {{n}}",
"choice_n": "Choix {n}",
"choices": "Choix",
"choose_organization": "Choisir l'organisation",
"choose_workspace": "Choisir un projet",
"choose_workspace": "Choisir un espace de travail",
"clear_all": "Tout effacer",
"clear_filters": "Effacer les filtres",
"clear_selection": "Effacer la sélection",
@@ -172,7 +174,7 @@
"close": "Fermer",
"code": "Code",
"collapse_rows": "Réduire les lignes",
"column_n": "Colonne {{n}}",
"column_n": "Colonne {n}",
"completed": "Terminé",
"configuration": "Configurer",
"confirm": "Confirmer",
@@ -197,7 +199,7 @@
"create_new_organization": "Créer une nouvelle organisation",
"create_segment": "Créer un segment",
"create_survey": "Créer un sondage",
"create_workspace": "Créer un projet",
"create_workspace": "Créer un espace de travail",
"created": "Créé",
"created_at": "Créé le",
"created_by": "Créé par",
@@ -212,6 +214,7 @@
"delete_what": "Supprimer {deleteWhat}",
"description": "Description",
"disable": "Désactiver",
"disabled": "Désactivé",
"disallow": "Ne pas autoriser",
"discard": "Annuler",
"dismissed": "Rejeté",
@@ -241,9 +244,9 @@
"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",
"failed_to_load_workspaces": "Échec du chargement des espaces de travail",
"failed_to_parse_csv": "Échec de l'analyse du CSV",
"field_placeholder": "Espace réservé {{field}}",
"field_placeholder": "Espace réservé pour {field}",
"filter": "Filtre",
"finish": "Terminer",
"first_name": "Prénom",
@@ -331,6 +334,7 @@
"not_authenticated": "Vous n'êtes pas authentifié pour effectuer cette action.",
"not_authorized": "Non autorisé",
"not_connected": "Non connecté",
"not_set": "Non défini",
"note": "Remarque",
"notifications": "Notifications",
"number": "Numéro",
@@ -390,13 +394,14 @@
"report_survey": "Rapport d'enquête",
"request_trial_license": "Demander une licence d'essai",
"reset_to_default": "Réinitialiser par défaut",
"resize": "Redimensionner",
"response": "Réponse",
"response_id": "ID de réponse",
"responses": "Réponses",
"restart": "Recommencer",
"retry": "Réessayer",
"role": "Rôle",
"row_n": "Ligne {{n}}",
"row_n": "Ligne {n}",
"saas": "SaaS",
"sales": "Ventes",
"save": "Enregistrer",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Certains fichiers n'ont pas pu être téléchargés",
"something_went_wrong": "Quelque chose s'est mal passé.",
"something_went_wrong_please_try_again": "Une erreur s'est produite. Veuillez réessayer.",
"soon": "Bientôt",
"sort_by": "Trier par",
"start_free_trial": "Commencer l'essai gratuit",
"status": "Statut",
@@ -474,7 +480,7 @@
"type": "Type",
"unify": "Unifier",
"unknown_survey": "Enquête inconnue",
"unlock_more_workspaces_with_a_higher_plan": "Débloquez plus de projets avec un forfait supérieur.",
"unlock_more_workspaces_with_a_higher_plan": "Débloque plus d'espaces de travail avec un forfait supérieur.",
"update": "Mise à jour",
"updated": "Mise à jour",
"updated_at": "Mis à jour à",
@@ -501,13 +507,13 @@
"weeks": "semaines",
"welcome_card": "Carte de bienvenue",
"workspace": "Espace de travail",
"workspace_configuration": "Configuration du projet",
"workspace_created_successfully": "Projet créé avec succès",
"workspace_creation_description": "Organisez les enquêtes dans des projets pour un meilleur contrôle d'accès.",
"workspace_id": "ID du projet",
"workspace_name": "Nom du projet",
"workspace_configuration": "Configuration de l'espace de travail",
"workspace_created_successfully": "Espace de travail créé avec succès",
"workspace_creation_description": "Organise tes enquêtes dans des espaces de travail pour un meilleur contrôle d'accès.",
"workspace_id": "ID de l'espace de travail",
"workspace_name": "Nom de l'espace de travail",
"workspace_name_placeholder": "par ex. Formbricks",
"workspaces": "Projets",
"workspaces": "Espaces de travail",
"years": "années",
"yes": "Oui",
"you_are_downgraded_to_the_community_edition": "Vous êtes rétrogradé à l'édition communautaire.",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Graphique à barres",
"chart_type_big_number": "Grand nombre",
"chart_type_line": "Graphique linéaire",
"chart_type_not_supported": "Le type de graphique « {{chartType}} » n'est pas encore pris en charge",
"chart_type_not_supported": "Le type de graphique \"{chartType}\" n'est pas encore pris en charge",
"chart_type_pie": "Graphique circulaire",
"chart_updated_successfully": "Graphique mis à jour avec succès!",
"configure_description": "Modifiez le type de graphique et d'autres paramètres pour cette visualisation.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Échec de l'exécution de la requête",
"failed_to_load_chart": "Échec du chargement du graphique",
"failed_to_load_chart_data": "Échec du chargement des données du graphique",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Échec de l'enregistrement du graphique",
"field": "Champ",
"field_label_average_score": "Score moyen",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Aucune donnée valide à afficher",
"not_contains": "ne contient pas",
"not_equals": "n'est pas égal à",
"open_chart": "Ouvrir le graphique {{name}}",
"open_chart": "Ouvrir le graphique {name}",
"open_options": "Ouvrir les options du graphique",
"or_filter_logic": "OU",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Veuillez sélectionner un tableau de bord",
"predefined_measures": "Mesures prédéfinies",
"preset": "Préréglage",
"preview_chart": "Aperçu du graphique",
"query_executed_successfully": "Requête exécutée avec succès",
"reset_to_ai_suggestion": "Réinitialiser à la suggestion IA",
"save_and_add_to_dashboard": "Enregistrer et ajouter au tableau de bord",
"save_chart": "Enregistrer le graphique",
"save_chart_dialog_title": "Enregistrer le graphique",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Sélectionner un champ",
"select_measures": "Sélectionner les mesures...",
"select_preset": "Sélectionner un préréglage",
"showing_first_n_of": "Affichage des {{n}} premières lignes sur {{count}}",
"showing_first_n_of": "Affichage des {n} premières lignes sur {count}",
"start_date": "Date de début",
"time_dimension": "Dimension temporelle",
"time_dimension_title": "Ajouter un groupement temporel",
"time_dimension_toggle_description": "Surveille les tendances dans le temps."
"time_dimension_toggle_description": "Surveille les tendances dans le temps.",
"update_chart": "Mettre à jour le graphique"
},
"dashboards": {
"add_count_charts": "Ajouter {count} graphique(s)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Créer un tableau de bord",
"create_dashboard_description": "Saisissez un nom pour votre nouveau tableau de bord.",
"create_failed": "Échec de la création du tableau de bord",
"create_new_chart": "Créer un nouveau graphique",
"create_success": "Tableau de bord créé avec succès!",
"dashboard": "Tableau de bord",
"dashboard_delete_confirmation": "Es-tu sûr(e) de vouloir supprimer ce tableau de bord ? Cette action est irréversible.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Échec de la duplication du tableau de bord",
"duplicate_success": "Tableau de bord dupliqué avec succès!",
"failed_to_load_chart_data": "Échec du chargement des données du graphique",
"no_charts_available_description": "Il n'y a aucun graphique pouvant être ajouté à ce tableau de bord. Soit aucun graphique n'existe encore, soit tous les graphiques existants ont déjà été ajoutés. Rendez-vous sur la page Graphiques pour créer de nouveaux graphiques.",
"no_charts_to_add_message": "Aucun graphique à ajouter à ce tableau de bord.",
"no_dashboards_found": "Aucun tableau de bord trouvé.",
"no_data_message": "Aucune donnée. Il n'y a actuellement aucune information à afficher. Ajoute des graphiques pour construire ton tableau de bord.",
"please_enter_name": "Veuillez saisir un nom de tableau de bord"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Vous n'avez pas d'enregistrements de commentaires sur lesquels créer des rapports. Configurez des sources de commentaires pour introduire des données dans le système.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Configurer les sources de commentaires"
},
"api_keys": {
"add_api_key": "Ajouter une clé API",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Connectez votre application ou votre site web à Formbricks.",
"cache_update_delay_description": "Lorsque vous effectuez des mises à jour de sondages, de contacts, d'actions ou d'autres données, il peut falloir jusqu'à 1 minute pour que ces modifications apparaissent dans votre application locale exécutant le SDK Formbricks.",
"cache_update_delay_title": "Les modifications seront reflétées après environ 1 minute en raison de la mise en cache",
"environment_id_legacy": "ID d'environnement (ancien)",
"environment_id_legacy_alert": "Votre configuration SDK existante peut encore utiliser un ID d'environnement ancien.",
"environment_id_legacy_alert_link": "Découvrez pourquoi et comment migrer.",
"formbricks_sdk_connected": "Le SDK Formbricks est connecté",
"formbricks_sdk_not_connected": "Le SDK Formbricks n'est pas encore connecté.",
"formbricks_sdk_not_connected_description": "Ajoutez le SDK Formbricks à votre site web ou à votre application pour le connecter à Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Logo Formbricks",
"general": {
"cannot_delete_only_workspace": "Il s'agit de votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.",
"cannot_delete_only_workspace": "C'est ton seul espace de travail, il ne peut pas être supprimé. Crée d'abord un nouvel espace de travail.",
"custom_scripts": "Scripts personnalisés",
"custom_scripts_card_description": "Ajouter des scripts de suivi et des pixels à toutes les enquêtes par lien dans cet espace de travail.",
"custom_scripts_description": "Les scripts seront injectés dans le <head> de toutes les pages d'enquête par lien.",
@@ -1989,21 +2005,21 @@
"custom_scripts_placeholder": "<!-- Collez vos scripts de suivi ici -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personnalisés mis à jour avec succès",
"custom_scripts_warning": "Les scripts s'exécutent avec un accès complet au navigateur. Ajoutez uniquement des scripts provenant de sources fiables.",
"delete_workspace": "Supprimer le projet",
"delete_workspace": "Supprimer l'espace de travail",
"delete_workspace_confirmation": "Es-tu sûr de vouloir supprimer {workspaceName} ? Cette action est irréversible.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Supprimer {workspaceName} y compris tous les sondages, réponses, personnes, actions et attributs.",
"delete_workspace_settings_description": "Supprimer le projet avec toutes les enquêtes, réponses, personnes, actions et attributs. Cette opération est irréversible.",
"error_saving_workspace_information": "Erreur lors de l'enregistrement des informations du projet",
"only_owners_or_managers_can_delete_workspaces": "Seuls les propriétaires ou les gestionnaires peuvent supprimer des projets",
"delete_workspace_settings_description": "Supprimer l'espace de travail avec toutes les enquêtes, réponses, personnes, actions et attributs. Cette action est irréversible.",
"error_saving_workspace_information": "Erreur lors de l'enregistrement des informations de l'espace de travail",
"only_owners_or_managers_can_delete_workspaces": "Seuls les propriétaires ou les gestionnaires peuvent supprimer des espaces de travail",
"recontact_waiting_time": "Période de refroidissement (entre les sondages)",
"recontact_waiting_time_settings_description": "Contrôlez la fréquence à laquelle les utilisateurs peuvent être interrogés dans tous les sondages de site web et d'application de cet espace de travail.",
"this_action_cannot_be_undone": "Cette action ne peut pas être annulée.",
"wait_x_days_before_showing_next_survey": "Attendre X jours avant d'afficher la prochaine enquête:",
"waiting_period_updated_successfully": "Période d'attente mise à jour avec succès",
"whats_your_workspace_called": "Comment s'appelle votre projet?",
"workspace_deleted_successfully": "Projet supprimé avec succès",
"workspace_name_settings_description": "Modifiez le nom de votre projet.",
"workspace_name_updated_successfully": "Nom du projet mis à jour avec succès"
"whats_your_workspace_called": "Comment s'appelle ton espace de travail ?",
"workspace_deleted_successfully": "Espace de travail supprimé avec succès",
"workspace_name_settings_description": "Modifie le nom de ton espace de travail.",
"workspace_name_updated_successfully": "Nom de l'espace de travail mis à jour avec succès"
},
"integrations": {
"activepieces_integration_description": "Connectez instantanément Formbricks à des applications populaires pour automatiser des tâches sans effectuer de codage.",
@@ -2161,7 +2177,7 @@
"alias_tooltip": "L'alias est un nom alternatif pour identifier la langue dans les enquêtes par lien et le SDK (facultatif)",
"cannot_remove_language_warning": "Vous ne pouvez pas supprimer cette langue car elle est encore utilisée dans ces enquêtes:",
"conflict_between_identifier_and_alias": "Il y a un conflit entre l'identifiant d'une langue ajoutée et l'un de vos alias. Les alias et les identifiants ne peuvent pas être identiques.",
"conflict_between_selected_alias_and_another_language": "Il y a un conflit entre l'alias sélectionné et une autre langue qui possède cet identifiant. Veuillez plutôt ajouter la langue avec cet identifiant à votre projet pour éviter les incohérences.",
"conflict_between_selected_alias_and_another_language": "Il y a un conflit entre l'alias sélectionné et une autre langue qui possède cet identifiant. Ajoute plutôt la langue avec cet identifiant à ton espace de travail pour éviter les incohérences.",
"delete_language_confirmation": "Êtes-vous sûr de vouloir supprimer cette langue? Cette action ne peut pas être annulée.",
"duplicate_language_or_language_id": "Langue ou identifiant de langue en double",
"edit_languages": "Modifier les langues",
@@ -2389,7 +2405,7 @@
"most_popular": "Le plus populaire",
"pending_change_removed": "Changement de formule programmé supprimé.",
"pending_plan_badge": "Programmé",
"pending_plan_change_description": "Ta formule passera à {{plan}} le {{date}}.",
"pending_plan_change_description": "Votre forfait passera à {plan} le {date}.",
"pending_plan_change_title": "Changement de formule programmé",
"pending_plan_cta": "Programmé",
"per_month": "par mois",
@@ -2442,7 +2458,7 @@
"trial_payment_method_added_description": "Tout est prêt ! Votre abonnement Pro se poursuivra automatiquement après la fin de la période d'essai.",
"trial_title": "Obtenez Formbricks Pro gratuitement !",
"unlimited_responses": "Réponses illimitées",
"unlimited_workspaces": "Projets illimités",
"unlimited_workspaces": "Espaces de travail illimités",
"upgrade": "Mise à niveau",
"upgrade_now": "Passer à la formule supérieure maintenant",
"usage_cycle": "Usage cycle",
@@ -2465,7 +2481,7 @@
"pretty_url": "URL personnalisée",
"survey_name": "Nom de l'enquête",
"title": "URL personnalisées",
"workspace": "Projet"
"workspace": "Espace de travail"
},
"enterprise": {
"audit_logs": "Journaux d'audit",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Un répertoire d'enregistrement de feedback avec ce nom existe déjà.",
"error_directory_name_required": "Le nom du répertoire est requis.",
"error_directory_workspaces_invalid_org": "Certains espaces de travail spécifiés n'appartiennent pas à cette organisation.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Répertoires de feedback",
"no_access": "Tu n'as pas la permission de gérer les répertoires de feedback.",
"no_connectors": "Aucun connecteur lié à ce répertoire pour le moment.",
"pause_connectors_confirmation_description": "Si vous mettez ces connecteurs en pause, aucun nouvel enregistrement ne sera ajouté.",
"pause_connectors_confirmation_title": "Mettre en pause les connecteurs liés ?",
"select_workspaces_placeholder": "Sélectionner des espaces de travail...",
"show_archived": "Afficher les éléments archivés",
"title": "Répertoires d'enregistrement des retours",
"unarchive": "Désarchiver"
"unarchive": "Désarchiver",
"unarchive_workspace_conflict": "Impossible de désarchiver ce répertoire, car un ou plusieurs espaces de travail attribués sont archivés.",
"workspace_access": "Accès à lespace de travail"
},
"general": {
"ai_data_analysis_disabled_for_organization": "L'analyse et l'enrichissement des données par IA sont désactivés pour cette organisation.",
"ai_data_analysis_enabled": "Enrichissement et analyse des données (IA)",
"ai_data_analysis_enabled_description": "L'IA pour tirer le meilleur parti de vos données, configurer des tableaux de bord, des graphiques, des rapports et plus encore. Accède à vos données d'expérience.",
"ai_enabled": "IA Formbricks",
"ai_enabled_description": "Gérer les fonctionnalités alimentées par l'IA pour cette organisation.",
"ai_features_not_enabled_for_organization": "Les fonctionnalités d'IA ne sont pas activées pour cette organisation.",
"ai_instance_not_configured": "L'IA est configurée au niveau de l'instance via des variables d'environnement. Demandez à votre administrateur de définir AI_PROVIDER, les identifiants du fournisseur et la liste de modèles correspondante avant d'activer les fonctionnalités d'IA.",
"ai_settings_updated_successfully": "Paramètres IA mis à jour avec succès",
"ai_smart_tools_disabled_for_organization": "Les outils intelligents d'IA sont désactivés pour cette organisation.",
"ai_smart_tools_enabled": "Fonctionnalités intelligentes (IA)",
"ai_smart_tools_enabled_description": "L'IA pour vous aider à accomplir plus en moins de temps. N'accède jamais aux données collectées avec Formbricks. Utilisée uniquement pour, par exemple, traduire les sondages dans d'autres langues.",
"bulk_invite_warning_description": "Dans le plan gratuit, tous les membres de l'organisation se voient toujours attribuer le rôle \"Owner\".",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "Vous ne pouvez pas quitter cette organisation car c'est votre seule organisation. Créez d'abord une nouvelle organisation.",
"copy_invite_link_to_clipboard": "Copier le lien d'invitation dans le presse-papiers",
"create_new_organization": "Créer une nouvelle organisation",
"create_new_organization_description": "Créez une nouvelle organisation pour gérer un ensemble différent de projets.",
"create_new_organization_description": "Crée une nouvelle organisation pour gérer un ensemble différent d'espaces de travail.",
"customize_email_with_a_higher_plan": "Personnalisez vos e-mails en passant à un forfait supérieur",
"delete_member_confirmation": "Les membres supprimés perdront l'accès à tous les projets et enquêtes de votre organisation.",
"delete_member_confirmation": "Les membres supprimés perdront l'accès à tous les espaces de travail et enquêtes de votre organisation.",
"delete_organization": "Supprimer l'organisation",
"delete_organization_description": "Supprimer l'organisation avec tous ses projets, y compris toutes les enquêtes, réponses, personnes, actions et attributs",
"delete_organization_description": "Supprimer l'organisation avec tous ses espaces de travail, incluant toutes les enquêtes, réponses, personnes, actions et attributs",
"delete_organization_warning": "Avant de procéder à la suppression de cette organisation, veuillez prendre connaissance des conséquences suivantes :",
"delete_organization_warning_1": "Suppression définitive de tous les projets liés à cette organisation.",
"delete_organization_warning_1": "Suppression définitive de tous les espaces de travail liés à cette organisation.",
"delete_organization_warning_2": "Cette action ne peut pas être annulée. Si c'est parti, c'est parti.",
"delete_organization_warning_3": "Veuillez entrer {organizationName} dans le champ suivant pour confirmer la suppression définitive de cette organisation :",
"eliminate_branding_with_whitelabel": "Le logo Formbricks n'apparaîtra plus et d'autres options de personnalisation s'offriront à vous.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Inscrivez-vous ici.",
"share_invite_link": "Partager le lien d'invitation",
"share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :",
"test_email_sent_successfully": "E-mail de test envoyé avec succès"
"test_email_sent_successfully": "E-mail de test envoyé avec succès",
"unlock_ai_features_description": "Les traductions automatiques, les outils intelligents et l'analyse de données sont disponibles avec les forfaits supérieurs. Passez à un forfait supérieur pour booster vos enquêtes avec l'IA.",
"unlock_ai_features_with_a_higher_plan": "Débloquez les fonctionnalités IA avec un forfait supérieur"
},
"notifications": {
"auto_subscribe_to_new_surveys": "S'abonner automatiquement aux nouvelles enquêtes",
@@ -2669,7 +2689,7 @@
},
"teams": {
"add_members_description": "Ajoutez des membres à l'équipe et déterminez leur rôle.",
"add_workspaces_description": "Contrôlez les projets auxquels les membres de l'équipe peuvent accéder.",
"add_workspaces_description": "Contrôlez les espaces de travail auxquels les membres de l'équipe peuvent accéder.",
"all_members_added": "Tous les membres ajoutés à cette équipe.",
"all_workspaces_added": "Tous les espaces de travail ont été ajoutés à cette équipe.",
"are_you_sure_you_want_to_delete_this_team": "Êtes-vous sûr de vouloir supprimer cette équipe? Cela supprime également l'accès à tous les espaces de travail et enquêtes associés à cette équipe.",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Ajuster le message \"Sondage fermé\"",
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
"adjust_the_theme_in_the": "Ajustez le thème dans le",
"ai_data_analysis_disabled": "L'analyse de données par IA est désactivée pour cette organisation.",
"ai_features_not_enabled": "Les fonctionnalités IA ne sont pas activées pour cette organisation.",
"ai_instance_not_configured": "L'IA n'est pas configurée. Contacte ton administrateur.",
"ai_smart_tools_disabled": "Les outils intelligents IA sont désactivés pour cette organisation.",
"ai_translate": "Traduire avec l'IA",
"ai_translating": "Traduction en cours avec l'IA... Garde cette fenêtre ouverte.",
"ai_translation_all_fields_populated": "Tous les champs sont déjà traduits",
"ai_translation_complete": "Traduction IA terminée",
"ai_translation_failed": "La traduction a échoué",
"ai_translation_instance_not_configured": "L'IA n'est pas configurée sur cette instance. Contacte ton administrateur.",
"ai_translation_not_available": "La traduction IA n'est pas disponible avec votre forfait actuel. Passez à un forfait supérieur pour débloquer cette fonctionnalité.",
"ai_translation_not_enabled": "Les outils intelligents IA sont désactivés pour cette organisation. Active-les dans les paramètres de l'organisation.",
"all_are_true": "toutes sont vraies",
"all_other_answers_will_continue_to": "Toutes les autres réponses continueront à",
"allow_multi_select": "Autoriser la sélection multiple",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Voir quelles équipes peuvent accéder à cet espace de travail."
},
"unify": {
"add_feedback_record": "Ajouter un enregistrement de commentaires",
"add_feedback_record_description": "Créez manuellement un enregistrement de commentaires.",
"add_feedback_source": "Ajouter une source de feedback",
"add_source": "Ajouter une source",
"allowed_values": "Valeurs autorisées: {values}",
"api_ingestion": "Ingestion par API",
"api_ingestion_manage_api_keys": "Gérer les clés API",
"api_ingestion_settings_description": "Envoyer des enregistrements de feedback via l'API de gestion.",
"auto_generated": "Généré automatiquement",
"change_file": "Changer de fichier",
"click_load_sample_csv": "Clique sur « Charger un exemple CSV » pour voir les colonnes",
"click_to_upload": "Clique pour charger",
"collected_at": "Collecté le",
"configure_import": "Configurer l'importation",
"configure_mapping": "Configurer le mappage",
"connection": "Connexion",
"connector_created_successfully": "Connecteur créé avec succès",
"connector_deleted_successfully": "Connecteur supprimé avec succès",
"connector_duplicated_successfully": "Connecteur dupliqué avec succès",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Importer les données deux fois créera des enregistrements en double.",
"csv_inconsistent_columns": "La ligne {row} a des colonnes incohérentes. Toutes les lignes doivent avoir les mêmes en-têtes.",
"csv_max_records": "Maximum {max} enregistrements autorisés.",
"custom_source_type": "Type de source personnalisé",
"custom_source_type_placeholder": "Entrez le type de source personnalisé",
"default_connector_name_csv": "Importation CSV",
"default_connector_name_formbricks": "Connexion de sondage Formbricks",
"deselect_all": "Tout désélectionner",
"discard_feedback_record_changes_description": "Vos modifications seront perdues si vous fermez ce tiroir.",
"discard_feedback_record_changes_title": "Supprimer les modifications non enregistrées ?",
"drop_a_field_here": "Déposez un champ ici",
"drop_field_or": "Déposez un champ ou",
"edit_csv_mapping": "Modifier le mappage CSV",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Échec du chargement des enregistrements de feedback",
"feedback_date": "Date actuelle",
"feedback_record_created_successfully": "Enregistrement de commentaires créé avec succès",
"feedback_record_details": "Détails de l'enregistrement des commentaires",
"feedback_record_details_description": "Examiner et mettre à jour les champs denregistrement des commentaires.",
"feedback_record_directory": "Répertoire d'enregistrements de retour d'expérience",
"feedback_record_fields": "Champs d'enregistrement de feedback",
"feedback_record_mcp": "MCP d'enregistrement de feedback",
"feedback_record_updated_successfully": "L'enregistrement des commentaires a été mis à jour avec succès",
"feedback_record_value_required": "Une valeur est requise pour le type de champ sélectionné",
"feedback_records": "Enregistrements de feedback",
"feedback_records_refreshed": "Enregistrements de feedback actualisés",
"feedback_sources": "Sources de feedback",
"feedback_sources_directory_access_multiple": "Les nouveaux enregistrements de ces sources seront stockés dans : {directoryNames}",
"feedback_sources_directory_access_single": "Les nouveaux enregistrements de cette source seront stockés dans : {directoryNames}",
"feedback_sources_settings_description": "Connecte et gère toutes les sources de feedback pour cet espace de travail.",
"field_group_id": "ID de groupe de champs",
"field_group_label": "Libellé du groupe de champs",
"field_id": "Identifiant du champ",
"field_label": "Libellé du champ",
"field_type": "Type de champ",
"formbricks_surveys": "Sondages Formbricks",
"frd_cannot_be_changed": "Le répertoire de retours d'expérience ne peut pas être modifié après sa création.",
"go_to_feedback_record_directories": "Accéder aux paramètres des répertoires",
"historical_import_complete": "Importation terminée: {successes} réussies, {failures} échouées, {skipped} ignorées (aucune donnée)",
"import_csv_data": "Importer les retours",
"import_feedback": "Importer les retours",
"import_historical_responses": "Importer les réponses historiques",
"import_historical_responses_description": "Importe les réponses existantes de cette enquête maintenant.",
"import_rows": "Importer {count} lignes",
"import_via_source_name": "Importer via \"{sourceName}\"",
"importing_data": "Importation des données...",
"importing_historical_data": "Importation des données historiques...",
"invalid_enum_values": "Valeurs non valides dans la colonne mappée à {field}",
"invalid_values_found": "Trouvées: {values} (lignes: {rows}) {extra}",
"load_sample_csv": "Charger un exemple de CSV",
"n_supported_questions": "{count} questions prises en charge",
"manage_directories": "Gérer les répertoires",
"manage_feedback_sources": "Gérer les sources de commentaires",
"metadata": "Métadonnées",
"metadata_key": "Clé de métadonnées",
"metadata_read_only_entries": "Valeurs de métadonnées en lecture seule (non-chaîne)",
"metadata_value": "Valeur des métadonnées",
"missing_feedback_source_title": "Il manque une source de feedback ?",
"no_feedback_record_directory_available": "Aucun répertoire d'enregistrements de retour d'expérience n'est assigné à cet espace de travail. Créez-en un ou assignez-en un d'abord.",
"no_feedback_records": "Aucun enregistrement de feedback pour le moment. Les enregistrements apparaîtront ici une fois que vos connecteurs commenceront à envoyer des données.",
"no_source_fields_loaded": "Aucun champ source chargé pour le moment",
"no_sources_connected": "Aucune source connectée pour le moment. Ajoutez une source pour commencer.",
"no_surveys_found": "Aucune enquête trouvée dans cet environnement",
"optional": "Facultatif",
"or_drag_and_drop": "ou glisser-déposer",
"question_selected": "<strong>{count}</strong> question sélectionnée. Chaque réponse à cette question créera un nouvel enregistrement de feedback.",
"question_type_not_supported": "Ce type de question n'est pas pris en charge",
"questions_selected": "<strong>{count}</strong> questions sélectionnées. Chaque réponse à ces questions créera un nouvel enregistrement de feedback.",
"records_will_go_to": "Les enregistrements seront envoyés vers",
"refresh_feedback_records": "Actualiser les enregistrements de retours",
"refreshing_feedback_records": "Actualisation des enregistrements de feedback...",
"request_feedback_source": "Demander une intégration de source",
"required": "Requis",
"save_changes": "Enregistrer les modifications",
"select_a_survey_to_see_questions": "Sélectionnez une enquête pour voir ses questions",
"select_a_value": "Sélectionnez une valeur...",
"select_all": "Sélectionner tout",
"select_feedback_record_directory": "Sélectionner un répertoire",
"select_feedback_record_source_type": "Sélectionnez le type de source",
"select_questions": "Sélectionner les questions",
"select_source_type_description": "Sélectionnez le type de source de feedback que vous souhaitez connecter.",
"select_source_type_prompt": "Sélectionnez le type de source de feedback que vous souhaitez connecter:",
"select_survey": "Sélectionner l'enquête",
"select_survey_and_questions": "Sélectionner l'enquête et les questions",
"select_survey_questions_description": "Choisissez quelles questions d'enquête doivent créer des FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Affichage de 3 sur {count} lignes",
"source": "source",
"source_connect_csv_description": "Importer des feedbacks depuis des fichiers CSV",
"source_connect_feedback_record_mcp_description": "Envoyer des enregistrements de feedback via l'intégration MCP.",
"source_connect_formbricks_description": "Connecter les feedbacks de vos enquêtes Formbricks",
"source_fields": "Champs source",
"source_id": "Identifiant de la source",
"source_name": "Nom de la source",
"source_type": "Type de source",
"source_type_cannot_be_changed": "Le type de source ne peut pas être modifié",
"sources": "Sources",
"status_active": "En cours",
"status_completed": "Terminé",
"status_draft": "Brouillon",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Erreur",
"status_paused": "En pause",
"status_live_sync": "Synchronisation en direct",
"status_ready": "Prêt",
"submission_id": "ID de soumission",
"survey_has_no_questions": "Ce sondage n'a pas de questions",
"survey_import_line": "{surveyName}: {responseCount} réponses × {questionCount} questions = {total} enregistrements de feedback",
"total_feedback_records": "Total: {checked} sur {total} enregistrements de feedback sélectionnés parmi {surveyCount} sondages",
"topics_and_subtopics": "Thèmes et sous-thèmes",
"unify_feedback": "Unifier les retours",
"update_mapping_description": "Mettre à jour la configuration de mappage pour cette source.",
"updated_at": "Mis à jour à",
"upload_csv_data_description": "Téléchargez un fichier CSV pour importer des données de feedback.",
"upload_csv_file": "Télécharger un fichier CSV",
"user_identifier": "Utilisateur",
"value": "Valeur"
"value": "Valeur",
"value_boolean": "Valeur (booléenne)",
"value_date": "Valeur (Date)",
"value_number": "Valeur (Nombre)",
"value_text": "Valeur (texte)"
},
"xm-templates": {
"ces": "CES",
+103 -35
View File
@@ -125,6 +125,7 @@
"activity": "Tevékenység",
"add": "Hozzáadás",
"add_action": "Művelet hozzáadása",
"add_chart": "Diagram hozzáadása",
"add_charts": "Diagramok hozzáadása",
"add_existing_chart_description": "Keressen és válasszon diagramokat a műszerfalhoz való hozzáadáshoz.",
"add_filter": "Szűrő hozzáadása",
@@ -147,6 +148,7 @@
"apply_filters": "Szűrők alkalmazása",
"archived": "Archivált",
"are_you_sure": "Biztos benne?",
"ask": "Ask",
"attributes": "Attribútumok",
"back": "Vissza",
"billing": "Számlázás",
@@ -159,7 +161,7 @@
"change_workspace": "Munkaterület módosítása",
"chart": "Diagram",
"charts": "Diagramok",
"choice_n": "{{n}}. választás",
"choice_n": "{n}. választás",
"choices": "Választási lehetőségek",
"choose_organization": "Szervezet kiválasztása",
"choose_workspace": "Munkaterület kiválasztása",
@@ -172,7 +174,7 @@
"close": "Bezárás",
"code": "Kód",
"collapse_rows": "Sorok összecsukása",
"column_n": "{{n}}. oszlop",
"column_n": "{n}. oszlop",
"completed": "Befejezve",
"configuration": "Konfigurálás",
"confirm": "Megerősítés",
@@ -212,6 +214,7 @@
"delete_what": "{deleteWhat} törlése",
"description": "Leírás",
"disable": "Letiltás",
"disabled": "Letiltva",
"disallow": "Ne engedélyezze",
"discard": "Elvetés",
"dismissed": "Eltüntetve",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Nem sikerült betölteni a szervezeteket",
"failed_to_load_workspaces": "Nem sikerült a munkaterületek betöltése",
"failed_to_parse_csv": "A CSV elemzése sikertelen",
"field_placeholder": "{{field}} helyőrző",
"field_placeholder": "{field} helykitöltő",
"filter": "Szűrő",
"finish": "Befejezés",
"first_name": "Keresztnév",
@@ -331,6 +334,7 @@
"not_authenticated": "Nincs jogosultsága ennek a műveletnek a végrehajtásához.",
"not_authorized": "Nincs felhatalmazva",
"not_connected": "Nincs kapcsolódva",
"not_set": "Nincs beállítva",
"note": "Jegyzet",
"notifications": "Értesítések",
"number": "Szám",
@@ -390,13 +394,14 @@
"report_survey": "Kérdőív jelentése",
"request_trial_license": "Próbaidőszaki licenc kérése",
"reset_to_default": "Visszaállítás az alapértelmezettre",
"resize": "Átméretezés",
"response": "Válasz",
"response_id": "Válaszazonosító",
"responses": "Válaszok",
"restart": "Újraindítás",
"retry": "Újra",
"role": "Szerep",
"row_n": "{{n}}. sor",
"row_n": "{n}. sor",
"saas": "SaaS",
"sales": "Értékesítés",
"save": "Mentés",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Néhány fájlt nem sikerült feltölteni",
"something_went_wrong": "Valami probléma történt",
"something_went_wrong_please_try_again": "Valami probléma történt. Próbálja meg újra.",
"soon": "Hamarosan",
"sort_by": "Rendezési sorrend",
"start_free_trial": "Ingyenes próbaidőszak indítása",
"status": "Állapot",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Oszlopdiagram",
"chart_type_big_number": "Nagy szám",
"chart_type_line": "Vonaldiagram",
"chart_type_not_supported": "A(z) \"{{chartType}}\" diagramtípus még nem támogatott",
"chart_type_not_supported": "A(z) \"{chartType}\" diagramtípus még nem támogatott",
"chart_type_pie": "Kördiagram",
"chart_updated_successfully": "A diagram sikeresen frissítve!",
"configure_description": "Módosítsd a diagram típusát és egyéb beállításait ehhez a vizualizációhoz.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "A lekérdezés végrehajtása sikertelen",
"failed_to_load_chart": "A diagram betöltése sikertelen",
"failed_to_load_chart_data": "A diagram adatainak betöltése sikertelen",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "A diagram mentése sikertelen",
"field": "Mező",
"field_label_average_score": "Átlagos pontszám",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Nincsenek megjeleníthető érvényes adatok",
"not_contains": "nem tartalmazza",
"not_equals": "nem egyenlő",
"open_chart": "{{name}} diagram megnyitása",
"open_chart": "{name} diagram megnyitása",
"open_options": "Diagram beállításainak megnyitása",
"or_filter_logic": "VAGY",
"original": "Eredeti",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Kérjük, válassz egy vezérlőpultot",
"predefined_measures": "Előre definiált mérőszámok",
"preset": "Előbeállítás",
"preview_chart": "Előnézet diagram",
"query_executed_successfully": "Lekérdezés sikeresen végrehajtva",
"reset_to_ai_suggestion": "Visszaállítás AI javaslatra",
"save_and_add_to_dashboard": "Mentés és hozzáadása az irányítópulthoz",
"save_chart": "Diagram mentése",
"save_chart_dialog_title": "Diagram mentése",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Mező kiválasztása",
"select_measures": "Mérőszámok kiválasztása...",
"select_preset": "Előbeállítás kiválasztása",
"showing_first_n_of": "Az első {{n}} sor megjelenítése {{count}} sorból",
"showing_first_n_of": "Az első {n} sor megjelenítése a(z) {count} sorból",
"start_date": "Kezdési dátum",
"time_dimension": "Időbeli dimenzió",
"time_dimension_title": "Időalapú csoportosítás hozzáadása",
"time_dimension_toggle_description": "Trendek figyelemmel kísérése az idő függvényében."
"time_dimension_toggle_description": "Trendek figyelemmel kísérése az idő függvényében.",
"update_chart": "Frissítse a diagramot"
},
"dashboards": {
"add_count_charts": "{count} diagram hozzáadása",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Műszerfal létrehozása",
"create_dashboard_description": "Adjon nevet az új vezérlőpultnak.",
"create_failed": "A vezérlőpult létrehozása sikertelen",
"create_new_chart": "Új diagram létrehozása",
"create_success": "A vezérlőpult sikeresen létrehozva!",
"dashboard": "Műszerfal",
"dashboard_delete_confirmation": "Biztos benne, hogy törölni kívánja ezt a műszerfalat? Ez a művelet nem vonható vissza.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "A vezérlőpult másolása sikertelen",
"duplicate_success": "A vezérlőpult sikeresen lemásolva!",
"failed_to_load_chart_data": "A diagram adatainak betöltése sikertelen",
"no_charts_available_description": "Nincsenek diagramok, amelyek hozzáadhatók ehhez az irányítópulthoz. Vagy még nem léteznek diagramok, vagy az összes meglévő diagram már hozzá lett adva. Látogassa meg a Diagramok oldalt új diagramok létrehozásához.",
"no_charts_to_add_message": "Nincsenek hozzáadható diagramok ehhez az irányítópulthoz.",
"no_dashboards_found": "Nem található vezérlőpult.",
"no_data_message": "Nincsenek adatok. Jelenleg nincsenek megjeleníthető információk. Adjon hozzá diagramokat az irányítópult felépítéséhez.",
"please_enter_name": "Kérjük, adjon nevet a vezérlőpultnak"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Nincsenek visszajelzési rekordjai, amelyekről jelentést tehetne. Állítsa be a visszacsatolási forrásokat, hogy adatokat tápláljon be a rendszerbe.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Visszajelzési források beállítása"
},
"api_keys": {
"add_api_key": "API-kulcs hozzáadása",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Alkalmazás vagy webhely csatlakoztatása a Formbrickshez.",
"cache_update_delay_description": "Ha frissítéseket hajt végre a kérdőíveken, partnereken, műveleteken vagy egyéb adatokon, akkor akár 1 percet is igénybe vehet, mire azok a változtatások megjelennek a Formbricks SDK-t futtató helyi alkalmazásban.",
"cache_update_delay_title": "A változtatások körülbelül 1 perc múlva jelennek meg a gyorsítótárazás miatt",
"environment_id_legacy": "Környezet azonosító (elavult)",
"environment_id_legacy_alert": "Az Ön meglévő SDK beállítása esetleg még egy elavult környezet azonosítót használ.",
"environment_id_legacy_alert_link": "Ismerje meg, miért és hogyan kell migrálnia.",
"formbricks_sdk_connected": "A Formbricks SDK csatlakoztatva van",
"formbricks_sdk_not_connected": "A Formbricks SDK még nincs csatlakoztatva.",
"formbricks_sdk_not_connected_description": "Adja hozzá a Formbricks SDK-t a webhelyéhez vagy az alkalmazásához, hogy összekapcsolja azt a Formbricks platformmal",
@@ -2389,7 +2405,7 @@
"most_popular": "Legnépszerűbb",
"pending_change_removed": "Az ütemezett csomagváltoztatás eltávolítva.",
"pending_plan_badge": "Ütemezett",
"pending_plan_change_description": "A csomagja {{plan}} csomagra fog váltani ekkor: {{date}}.",
"pending_plan_change_description": "Az Ön csomagja {plan} csomagra vált {date} dátummal.",
"pending_plan_change_title": "Ütemezett csomagváltoztatás",
"pending_plan_cta": "Ütemezett",
"per_month": "havonta",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Ezzel a névvel már létezik visszajelzési rekord könyvtár.",
"error_directory_name_required": "A könyvtár neve kötelező megadni.",
"error_directory_workspaces_invalid_org": "Egyes megadott munkaterületek nem ehhez a szervezethez tartoznak.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Visszajelzési könyvtárak",
"no_access": "Nem rendelkezik jogosultsággal a visszajelzési nyilvántartási könyvtárak kezeléséhez.",
"no_connectors": "Még nincsenek csatlakozók társítva ehhez a könyvtárhoz.",
"pause_connectors_confirmation_description": "Ha szünetelteti ezeket a csatlakozókat, nem kerülnek be új rekordok.",
"pause_connectors_confirmation_title": "Szünetelteti a kapcsolódó csatlakozókat?",
"select_workspaces_placeholder": "Munkaterületek kiválasztása...",
"show_archived": "Archivált elemek megjelenítése",
"title": "Visszajelzési Nyilvántartási Könyvtárak",
"unarchive": "Archiválás visszavonása"
"unarchive": "Archiválás visszavonása",
"unarchive_workspace_conflict": "A könyvtár nem állítható vissza, mert egy vagy több hozzárendelt munkaterület archiválva van.",
"workspace_access": "Munkaterület-hozzáférés"
},
"general": {
"ai_data_analysis_disabled_for_organization": "Az MI-alapú adatelemzés és adatgazdagítás ki van kapcsolva ennél a szervezetnél.",
"ai_data_analysis_enabled": "Adatgazdagítás és elemzés (AI)",
"ai_data_analysis_enabled_description": "AI segítségével többet hozhat ki az adataiból, irányítópultokat, diagramokat, jelentéseket és egyebeket állíthat be. Hozzáfér az élményekhez kapcsolódó adatokhoz.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "AI-alapú funkciók kezelése ehhez a szervezethez.",
"ai_features_not_enabled_for_organization": "Az MI-funkciók nincsenek engedélyezve ennél a szervezetnél.",
"ai_instance_not_configured": "Az MI példányszinten, környezeti változókkal van konfigurálva. Kérd meg a rendszergazdát, hogy állítsa be az AI_PROVIDER értékét, a szolgáltató hitelesítő adatait és a megfelelő modelllistát, mielőtt engedélyezné az MI-funkciókat.",
"ai_settings_updated_successfully": "AI beállítások sikeresen frissítve",
"ai_smart_tools_disabled_for_organization": "Az MI intelligens funkciói ki vannak kapcsolva ennél a szervezetnél.",
"ai_smart_tools_enabled": "Intelligens funkciók (AI)",
"ai_smart_tools_enabled_description": "AI segítségével kevesebb idő alatt többet érhet el. Soha nem fér hozzá a Formbricks által gyűjtött adatokhoz. Csak például felmérések más nyelvekre történő fordításához használatos.",
"bulk_invite_warning_description": "Az ingyenes csomagban az összes szervezeti tag mindig a „Tulajdonos” szerephez van hozzárendelve.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Regisztráljon itt.",
"share_invite_link": "Meghívási hivatkozás megosztása",
"share_this_link_to_let_your_organization_member_join_your_organization": "Ossza meg ezt a hivatkozást, hogy szervezetének tagja csatlakozhasson a szervezetéhez:",
"test_email_sent_successfully": "A teszt e-mail sikeresen elküldve"
"test_email_sent_successfully": "A teszt e-mail sikeresen elküldve",
"unlock_ai_features_description": "A mesterséges intelligencia által támogatott fordítások, intelligens eszközök és adatelemzés a magasabb csomagokban érhetők el. Frissítsen, hogy mesterséges intelligenciával turbózza fel kérdőíveit.",
"unlock_ai_features_with_a_higher_plan": "Oldja fel a mesterséges intelligencia funkcióit egy magasabb csomaggal"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Automatikus feliratkozás az új kérdőívekre",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "A „Kérdőív lezárva” üzenet módosítása",
"adjust_survey_closed_message_description": "Annak az üzenetnek a megváltoztatása, amelyet a látogatók akkor látnak, amikor a kérdőív lezárul.",
"adjust_the_theme_in_the": "A téma beállítása ebben:",
"ai_data_analysis_disabled": "Az AI adatelemzés le van tiltva ezen szervezet számára.",
"ai_features_not_enabled": "Az AI funkciók nincsenek engedélyezve ezen szervezet számára.",
"ai_instance_not_configured": "Az AI nincs konfigurálva. Kérjük, forduljon a rendszergazdájához.",
"ai_smart_tools_disabled": "Az AI intelligens eszközök le vannak tiltva ezen szervezet számára.",
"ai_translate": "Fordítás mesterséges intelligenciával",
"ai_translating": "AI fordítás folyamatban... Kérjük, tartsa nyitva ezt az ablakot.",
"ai_translation_all_fields_populated": "Minden mező már le van fordítva",
"ai_translation_complete": "A mesterséges intelligencia által végzett fordítás befejeződött",
"ai_translation_failed": "A fordítás sikertelen volt",
"ai_translation_instance_not_configured": "Az AI nincs konfigurálva ezen az instance-on. Vegye fel a kapcsolatot az adminisztrátorral.",
"ai_translation_not_available": "A mesterséges intelligencia által támogatott fordítás nem érhető el az Ön jelenlegi csomagjában. Frissítsen ennek a funkciónak a feloldásához.",
"ai_translation_not_enabled": "Az AI intelligens eszközök le vannak tiltva ennél a szervezetnél. Engedélyezze őket a szervezeti beállításokban.",
"all_are_true": "az összes igaz",
"all_other_answers_will_continue_to": "Az összes többi válasz továbbra is",
"allow_multi_select": "Több választás engedélyezése",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Annak megtekintése, hogy mely csapatok férhetnek hozzá ehhez a munkaterülethez."
},
"unify": {
"add_feedback_record": "Visszajelzés hozzáadása",
"add_feedback_record_description": "Készítsen visszajelzési rekordot manuálisan.",
"add_feedback_source": "Visszajelzési forrás hozzáadása",
"add_source": "Forrás hozzáadása",
"allowed_values": "Engedélyezett értékek: {values}",
"api_ingestion": "API betöltés",
"api_ingestion_manage_api_keys": "API kulcsok kezelése",
"api_ingestion_settings_description": "Visszajelzési rekordok küldése a Management API használatával.",
"auto_generated": "Automatikusan generált",
"change_file": "Fájl módosítása",
"click_load_sample_csv": "Kattintson a 'Minta CSV betöltése' gombra az oszlopok megtekintéséhez",
"click_to_upload": "Kattintson a feltöltéshez",
"collected_at": "Gyűjtve",
"configure_import": "Importálás konfigurálása",
"configure_mapping": "Leképezés konfigurálása",
"connection": "Kapcsolat",
"connector_created_successfully": "Csatlakozó sikeresen létrehozva",
"connector_deleted_successfully": "Csatlakozó sikeresen törölve",
"connector_duplicated_successfully": "Csatlakozó sikeresen duplikálva",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Az adatok kétszeri importálása duplikált rekordokat hoz létre.",
"csv_inconsistent_columns": "A(z) {row}. sor inkonzisztens oszlopokat tartalmaz. Minden sornak ugyanazokkal a fejlécekkel kell rendelkeznie.",
"csv_max_records": "Maximum {max} rekord engedélyezett.",
"custom_source_type": "Egyéni forrástípus",
"custom_source_type_placeholder": "Adja meg az egyéni forrástípust",
"default_connector_name_csv": "CSV importálás",
"default_connector_name_formbricks": "Formbricks kérdőív kapcsolat",
"deselect_all": "Összes kijelölés törlése",
"discard_feedback_record_changes_description": "A módosítások elvesznek, ha bezárja ezt a fiókot.",
"discard_feedback_record_changes_title": "Elveti a nem mentett módosításokat?",
"drop_a_field_here": "Húzz ide egy mezőt",
"drop_field_or": "Húzz ide egy mezőt vagy",
"edit_csv_mapping": "CSV leképezés szerkesztése",
@@ -3642,47 +3682,64 @@
"enum": "felsorolás",
"failed_to_load_feedback_records": "Nem sikerült betölteni a visszajelzési rekordokat",
"feedback_date": "Aktuális dátum",
"feedback_record_created_successfully": "A visszajelzési rekord sikeresen létrehozva",
"feedback_record_details": "A visszajelzési rekord részletei",
"feedback_record_details_description": "Tekintse át és frissítse a visszajelzési rekordmezőket.",
"feedback_record_directory": "Visszajelzési Rekord Könyvtár",
"feedback_record_fields": "Visszajelzési rekord mezők",
"feedback_record_mcp": "Visszajelzési rekord MCP",
"feedback_record_updated_successfully": "A visszajelzési rekord sikeresen frissítve",
"feedback_record_value_required": "A kiválasztott mezőtípushoz értéket kell megadni",
"feedback_records": "Visszajelzési rekordok",
"feedback_records_refreshed": "Visszajelzési rekordok frissítve",
"feedback_sources": "Visszajelzési források",
"feedback_sources_directory_access_multiple": "Az ezekből a forrásokból származó új rekordok a következő helyen lesznek tárolva: {directoryNames}",
"feedback_sources_directory_access_single": "Az ebből a forrásból származó új rekordok a következő helyen lesznek tárolva: {directoryNames}",
"feedback_sources_settings_description": "Összes visszajelzési forrás csatlakoztatása és kezelése ezen munkaterület számára.",
"field_group_id": "Mezőcsoport azonosítója",
"field_group_label": "Mezőcsoport címke",
"field_id": "Mezőazonosító",
"field_label": "Mező címke",
"field_type": "Mező típus",
"formbricks_surveys": "Formbricks kérdőívek",
"frd_cannot_be_changed": "A visszajelzési könyvtár a létrehozás után nem módosítható.",
"go_to_feedback_record_directories": "Ugrás a könyvtárbeállításokhoz",
"historical_import_complete": "Importálás befejezve: {successes} sikeres, {failures} sikertelen, {skipped} kihagyva (nincs adat)",
"import_csv_data": "Visszajelzés importálása",
"import_feedback": "Visszajelzés importálása",
"import_historical_responses": "Korábbi válaszok importálása",
"import_historical_responses_description": "Meglévő válaszok importálása ebből a felmérésből most.",
"import_rows": "{count} sor importálása",
"import_via_source_name": "Importálás a következőn keresztül: \"{sourceName}\"",
"importing_data": "Adatok importálása...",
"importing_historical_data": "Történeti adatok importálása...",
"invalid_enum_values": "Érvénytelen értékek a(z) {field} mezőhöz rendelt oszlopban",
"invalid_values_found": "Talált értékek: {values} (sorok: {rows}) {extra}",
"load_sample_csv": "Minta CSV betöltése",
"n_supported_questions": "{count} támogatott kérdés",
"manage_directories": "Könyvtárak kezelése",
"manage_feedback_sources": "Visszajelzési források kezelése",
"metadata": "Metaadatok",
"metadata_key": "Metaadatkulcs",
"metadata_read_only_entries": "Csak olvasható metaadatértékek (nem karakterlánc)",
"metadata_value": "A metaadat értéke",
"missing_feedback_source_title": "Hiányzik egy visszajelzési forrás?",
"no_feedback_record_directory_available": "Ehhez a munkaterülethez nem tartozik visszajelzési rekord könyvtár. Először hozzon létre vagy rendeljen hozzá egyet.",
"no_feedback_records": "Még nincsenek visszajelzési rekordok. A rekordok itt fognak megjelenni, amint a csatlakozók elkezdik küldeni az adatokat.",
"no_source_fields_loaded": "Még nincsenek forrás mezők betöltve",
"no_sources_connected": "Még nincsenek források csatlakoztatva. Adj hozzá egy forrást a kezdéshez.",
"no_surveys_found": "Nem találhatók kérdőívek ebben a környezetben",
"optional": "Elhagyható",
"or_drag_and_drop": "vagy húzd ide",
"question_selected": "<strong>{count}</strong> kérdés kiválasztva. Minden válasz ezekre a kérdésekre új visszajelzési rekordot hoz létre.",
"question_type_not_supported": "Ez a kérdéstípus nem támogatott",
"questions_selected": "<strong>{count}</strong> kérdés kiválasztva. Minden válasz ezekre a kérdésekre új visszajelzési rekordot hoz létre.",
"records_will_go_to": "A rekordok ide kerülnek",
"refresh_feedback_records": "Visszajelzési rekordok frissítése",
"refreshing_feedback_records": "Visszajelzési rekordok frissítése...",
"request_feedback_source": "Forrásintegráció kérése",
"required": "Kötelező",
"save_changes": "Változtatások mentése",
"select_a_survey_to_see_questions": "Válassz egy kérdőívet a kérdések megtekintéséhez",
"select_a_value": "Válassz egy értéket...",
"select_all": "Összes kiválasztása",
"select_feedback_record_directory": "Válasszon egy könyvtárat",
"select_feedback_record_source_type": "Válassza ki a forrás típusát",
"select_questions": "Kérdések kiválasztása",
"select_source_type_description": "Válassza ki a csatlakoztatni kívánt visszajelzési forrás típusát.",
"select_source_type_prompt": "Válassza ki a csatlakoztatni kívánt visszajelzési forrás típusát:",
"select_survey": "Kérdőív kiválasztása",
"select_survey_and_questions": "Kérdőív és kérdések kiválasztása",
"select_survey_questions_description": "Válassza ki, mely kérdőívkérdések hozzanak létre visszajelzési rekordokat.",
@@ -3692,27 +3749,38 @@
"showing_rows": "3 megjelenítve {count} sorból",
"source": "forrás",
"source_connect_csv_description": "Visszajelzések importálása CSV fájlokból",
"source_connect_feedback_record_mcp_description": "Visszajelzési rekordok küldése az MCP integráción keresztül.",
"source_connect_formbricks_description": "Visszajelzések csatlakoztatása a Formbricks kérdőívekből",
"source_fields": "Forrásmezők",
"source_id": "Forrásazonosító",
"source_name": "Forrásnév",
"source_type": "Forrás típus",
"source_type_cannot_be_changed": "A forrástípus nem módosítható",
"sources": "Források",
"status_active": "Folyamatban",
"status_completed": "Befejezve",
"status_draft": "Piszkozat",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Hiba",
"status_paused": "Szüneteltetve",
"status_live_sync": "Élő szinkronizálás",
"status_ready": "Kész",
"submission_id": "Beküldés azonosítója",
"survey_has_no_questions": "Ez a felmérés nem tartalmaz kérdéseket",
"survey_import_line": "{surveyName}: {responseCount} válasz × {questionCount} kérdés = {total} visszajelzési rekord",
"total_feedback_records": "Összesen: {checked} / {total} visszajelzési rekord kiválasztva {surveyCount} felmérésből",
"topics_and_subtopics": "Témák és altémák",
"unify_feedback": "Visszajelzések egyesítése",
"update_mapping_description": "Frissítse a leképezési konfigurációt ehhez a forráshoz.",
"updated_at": "Frissítve",
"upload_csv_data_description": "Tölts fel egy CSV fájlt a visszajelzési adatok importálásához.",
"upload_csv_file": "CSV fájl feltöltése",
"user_identifier": "Felhasználó",
"value": "Érték"
"value": "Érték",
"value_boolean": "Érték (logikai)",
"value_date": "Érték (dátum)",
"value_number": "Érték (szám)",
"value_text": "Érték (szöveg)"
},
"xm-templates": {
"ces": "CES",
+103 -35
View File
@@ -125,6 +125,7 @@
"activity": "アクティビティ",
"add": "追加",
"add_action": "アクションを追加",
"add_chart": "チャートを追加",
"add_charts": "グラフを追加",
"add_existing_chart_description": "このダッシュボードに追加するグラフを検索して選択してください。",
"add_filter": "フィルターを追加",
@@ -147,6 +148,7 @@
"apply_filters": "フィルターを適用",
"archived": "アーカイブ済み",
"are_you_sure": "よろしいですか?",
"ask": "Ask",
"attributes": "属性",
"back": "戻る",
"billing": "請求",
@@ -159,7 +161,7 @@
"change_workspace": "ワークスペースを変更",
"chart": "チャート",
"charts": "チャート",
"choice_n": "選択肢 {{n}}",
"choice_n": "選択肢 {n}",
"choices": "選択肢",
"choose_organization": "組織を選択",
"choose_workspace": "ワークスペースを選択",
@@ -172,7 +174,7 @@
"close": "閉じる",
"code": "コード",
"collapse_rows": "行を非表示",
"column_n": "列 {{n}}",
"column_n": "列 {n}",
"completed": "完了",
"configuration": "設定",
"confirm": "確認",
@@ -212,6 +214,7 @@
"delete_what": "{deleteWhat}を削除",
"description": "説明",
"disable": "無効にする",
"disabled": "無効",
"disallow": "許可しない",
"discard": "破棄",
"dismissed": "非表示",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "組織の読み込みに失敗しました",
"failed_to_load_workspaces": "ワークスペースの読み込みに失敗しました",
"failed_to_parse_csv": "CSVの解析に失敗しました",
"field_placeholder": "{{field}} プレースホルダー",
"field_placeholder": "{field} プレースホルダー",
"filter": "フィルター",
"finish": "完了",
"first_name": "名",
@@ -331,6 +334,7 @@
"not_authenticated": "このアクションを実行するための認証がされていません。",
"not_authorized": "権限がありません",
"not_connected": "未接続",
"not_set": "未設定",
"note": "メモ",
"notifications": "通知",
"number": "数値",
@@ -390,13 +394,14 @@
"report_survey": "フォームを報告",
"request_trial_license": "トライアルライセンスをリクエスト",
"reset_to_default": "デフォルトにリセット",
"resize": "サイズ変更",
"response": "回答",
"response_id": "回答ID",
"responses": "回答",
"restart": "再開",
"retry": "再試行",
"role": "役割",
"row_n": "行 {{n}}",
"row_n": "行 {n}",
"saas": "SaaS",
"sales": "セールス",
"save": "保存",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "一部のファイルのアップロードに失敗しました",
"something_went_wrong": "問題が発生しました",
"something_went_wrong_please_try_again": "問題が発生しました。もう一度お試しください。",
"soon": "近日公開",
"sort_by": "並び替え",
"start_free_trial": "無料トライアルを開始",
"status": "ステータス",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "棒グラフ",
"chart_type_big_number": "大きな数値",
"chart_type_line": "折れ線グラフ",
"chart_type_not_supported": "チャートタイプ「{{chartType}}」はまだサポートされていません",
"chart_type_not_supported": "チャートタイプ「{chartType}」はまだサポートされていません",
"chart_type_pie": "円グラフ",
"chart_updated_successfully": "チャートを更新しました!",
"configure_description": "このビジュアライゼーションのチャートタイプやその他の設定を変更します。",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "クエリの実行に失敗しました",
"failed_to_load_chart": "チャートの読み込みに失敗しました",
"failed_to_load_chart_data": "チャートデータの読み込みに失敗しました",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "チャートの保存に失敗しました",
"field": "フィールド",
"field_label_average_score": "平均スコア",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "表示する有効なデータがありません",
"not_contains": "を含まない",
"not_equals": "と等しくない",
"open_chart": "チャート{{name}}を開く",
"open_chart": "チャート {name} を開く",
"open_options": "チャートオプションを開く",
"or_filter_logic": "OR",
"original": "オリジナル",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "ダッシュボードを選択してください",
"predefined_measures": "事前定義されたメジャー",
"preset": "プリセット",
"preview_chart": "グラフのプレビュー",
"query_executed_successfully": "クエリが正常に実行されました",
"reset_to_ai_suggestion": "AIの提案にリセット",
"save_and_add_to_dashboard": "保存してダッシュボードに追加",
"save_chart": "チャートを保存",
"save_chart_dialog_title": "チャートを保存",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "フィールドを選択",
"select_measures": "メジャーを選択...",
"select_preset": "プリセットを選択",
"showing_first_n_of": "{{count}}行中、最初の{{n}}行を表示",
"showing_first_n_of": "{count} 行中、最初の {n} 行を表示しています",
"start_date": "開始日",
"time_dimension": "時間ディメンション",
"time_dimension_title": "時間ベースのグループ化を追加",
"time_dimension_toggle_description": "時間の経過に伴うトレンドを監視します。"
"time_dimension_toggle_description": "時間の経過に伴うトレンドを監視します。",
"update_chart": "チャートを更新"
},
"dashboards": {
"add_count_charts": "{count}個のグラフを追加",
@@ -1806,6 +1816,7 @@
"create_dashboard": "ダッシュボードを作成",
"create_dashboard_description": "新しいダッシュボードの名前を入力してください。",
"create_failed": "ダッシュボードの作成に失敗しました",
"create_new_chart": "新しいチャートを作成する",
"create_success": "ダッシュボードを正常に作成しました!",
"dashboard": "ダッシュボード",
"dashboard_delete_confirmation": "このダッシュボードを削除してもよろしいですか?この操作は元に戻せません。",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "ダッシュボードの複製に失敗しました",
"duplicate_success": "ダッシュボードを正常に複製しました!",
"failed_to_load_chart_data": "チャートデータの読み込みに失敗しました",
"no_charts_available_description": "このダッシュボードに追加できるチャートがありません。チャートがまだ存在しないか、既存のチャートがすべて追加済みです。新しいチャートを作成するには、チャートページに移動してください。",
"no_charts_to_add_message": "このダッシュボードに追加するチャートがありません。",
"no_dashboards_found": "ダッシュボードが見つかりません。",
"no_data_message": "データがありません。現在表示する情報がありません。ダッシュボードを構築するにはチャートを追加してください。",
"please_enter_name": "ダッシュボード名を入力してください"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "レポートするフィードバック レコードがありません。データをシステムにフィードするためのフィードバック ソースをセットアップします。",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "フィードバックソースのセットアップ"
},
"api_keys": {
"add_api_key": "APIキーを追加",
@@ -1858,6 +1871,9 @@
"app_connection_description": "アプリやウェブサイトをFormbricksに接続します。",
"cache_update_delay_description": "アンケート、連絡先、アクション、その他のデータを更新した場合、Formbricks SDKを実行しているローカルアプリに変更が反映されるまで最大1分かかることがあります。",
"cache_update_delay_title": "キャッシュにより変更は約1分後に反映されます",
"environment_id_legacy": "環境ID(レガシー)",
"environment_id_legacy_alert": "既存のSDKセットアップでは、レガシーの環境IDが使用されている可能性があります。",
"environment_id_legacy_alert_link": "理由と移行方法について詳しく見る。",
"formbricks_sdk_connected": "Formbricks SDKは接続されています",
"formbricks_sdk_not_connected": "Formbricks SDKはまだ接続されていません。",
"formbricks_sdk_not_connected_description": "ウェブサイトやアプリにFormbricks SDKを追加してFormbricksと接続してください",
@@ -2389,7 +2405,7 @@
"most_popular": "人気",
"pending_change_removed": "予定されていたプラン変更を取り消しました。",
"pending_plan_badge": "変更予定",
"pending_plan_change_description": "{{date}}に{{plan}}へ切り替わります。",
"pending_plan_change_description": "プランは {date}{plan}切り替わります。",
"pending_plan_change_title": "プラン変更の予定",
"pending_plan_cta": "変更予定",
"per_month": "/月",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "この名前のフィードバック記録ディレクトリは既に存在します。",
"error_directory_name_required": "ディレクトリ名は必須です。",
"error_directory_workspaces_invalid_org": "指定されたワークスペースの一部がこの組織に属していません。",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "フィードバックディレクトリ",
"no_access": "フィードバック記録ディレクトリを管理する権限がありません。",
"no_connectors": "このディレクトリにリンクされているコネクタはまだありません。",
"pause_connectors_confirmation_description": "これらのコネクタを一時停止すると、新しいレコードは追加されません。",
"pause_connectors_confirmation_title": "関連するコネクタを一時停止しますか?",
"select_workspaces_placeholder": "ワークスペースを選択...",
"show_archived": "アーカイブ済みを表示",
"title": "フィードバック記録ディレクトリ",
"unarchive": "アーカイブ解除"
"unarchive": "アーカイブ解除",
"unarchive_workspace_conflict": "割り当てられているワークスペースの1つ以上がアーカイブされているため、このディレクトリをアーカイブ解除できません。",
"workspace_access": "ワークスペースアクセス"
},
"general": {
"ai_data_analysis_disabled_for_organization": "この組織では AI によるデータ分析と拡張が無効になっています。",
"ai_data_analysis_enabled": "データエンリッチメントと分析(AI)",
"ai_data_analysis_enabled_description": "AIを活用してデータから最大限の価値を引き出し、ダッシュボード、チャート、レポートなどを設定できます。エクスペリエンスデータに触れます。",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "この組織のAI機能を管理します。",
"ai_features_not_enabled_for_organization": "この組織では AI 機能が有効になっていません。",
"ai_instance_not_configured": "AI は環境変数を使ってインスタンスレベルで設定されます。AI 機能を有効にする前に、管理者に AI_PROVIDER、このプロバイダーの認証情報、および対応するモデル一覧を設定してもらってください。",
"ai_settings_updated_successfully": "AI設定が正常に更新されました",
"ai_smart_tools_disabled_for_organization": "この組織では AI スマートツールが無効になっています。",
"ai_smart_tools_enabled": "スマート機能(AI",
"ai_smart_tools_enabled_description": "AIを活用して、より短時間でより多くのことを達成できます。Formbricksで収集されたデータには一切触れません。アンケートを他の言語に翻訳するなどの用途にのみ使用されます。",
"bulk_invite_warning_description": "無料プランでは、すべての組織メンバーに常に「オーナー」ロールが割り当てられます。",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "こちらからサインアップしてください。",
"share_invite_link": "招待リンクを共有",
"share_this_link_to_let_your_organization_member_join_your_organization": "このリンクを共有して、組織メンバーを招待できます:",
"test_email_sent_successfully": "テストメールを正常に送信しました"
"test_email_sent_successfully": "テストメールを正常に送信しました",
"unlock_ai_features_description": "AI翻訳、スマートツール、データ分析は上位プランでご利用いただけます。アップグレードしてAIでアンケートを強化しましょう。",
"unlock_ai_features_with_a_higher_plan": "上位プランでAI機能を利用する"
},
"notifications": {
"auto_subscribe_to_new_surveys": "新しいフォームに自動購読",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "「フォームはクローズしました」メッセージを調整",
"adjust_survey_closed_message_description": "フォームがクローズしたときに訪問者が見るメッセージを変更します。",
"adjust_the_theme_in_the": "テーマを",
"ai_data_analysis_disabled": "この組織ではAIデータ分析が無効になっています。",
"ai_features_not_enabled": "この組織ではAI機能が有効になっていません。",
"ai_instance_not_configured": "AIが設定されていません。管理者にお問い合わせください。",
"ai_smart_tools_disabled": "この組織ではAIスマートツールが無効になっています。",
"ai_translate": "AIで翻訳",
"ai_translating": "AIで翻訳中... このモーダルを開いたままにしてください。",
"ai_translation_all_fields_populated": "すべてのフィールドは既に翻訳されています",
"ai_translation_complete": "AI翻訳が完了しました",
"ai_translation_failed": "翻訳に失敗しました",
"ai_translation_instance_not_configured": "このインスタンスではAIが設定されていません。管理者に連絡してください。",
"ai_translation_not_available": "現在のプランではAI翻訳は利用できません。この機能を利用するにはアップグレードしてください。",
"ai_translation_not_enabled": "この組織ではAIスマートツールが無効になっています。組織設定で有効にしてください。",
"all_are_true": "すべてが真である",
"all_other_answers_will_continue_to": "他のすべての回答は引き続き",
"allow_multi_select": "複数選択を許可",
@@ -3602,16 +3634,21 @@
"team_settings_description": "このワークスペースにアクセスできるチームを確認します。"
},
"unify": {
"add_feedback_record": "フィードバックレコードを追加する",
"add_feedback_record_description": "フィードバック記録を手動で作成します。",
"add_feedback_source": "フィードバックソースを追加",
"add_source": "ソースを追加",
"allowed_values": "許可される値: {values}",
"api_ingestion": "API取り込み",
"api_ingestion_manage_api_keys": "APIキーを管理",
"api_ingestion_settings_description": "管理APIを使用してフィードバックレコードを送信します。",
"auto_generated": "自動生成",
"change_file": "ファイルを変更",
"click_load_sample_csv": "「サンプルCSVを読み込む」をクリックして列を表示",
"click_to_upload": "クリックしてアップロード",
"collected_at": "収集日時",
"configure_import": "インポートを設定",
"configure_mapping": "マッピングを設定",
"connection": "接続",
"connector_created_successfully": "コネクタが正常に作成されました",
"connector_deleted_successfully": "コネクタが正常に削除されました",
"connector_duplicated_successfully": "コネクタが正常に複製されました",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "データを2回インポートすると、重複したレコードが作成されます。",
"csv_inconsistent_columns": "行 {row} の列が一致しません。すべての行は同じヘッダーを持つ必要があります。",
"csv_max_records": "最大 {max} 件のレコードまで許可されています。",
"custom_source_type": "カスタムソースタイプ",
"custom_source_type_placeholder": "カスタムソースタイプを入力してください",
"default_connector_name_csv": "CSVインポート",
"default_connector_name_formbricks": "Formbricks フォーム接続",
"deselect_all": "すべて選択解除",
"discard_feedback_record_changes_description": "このドロワーを閉じると、変更内容は失われます。",
"discard_feedback_record_changes_title": "保存されていない変更を破棄しますか?",
"drop_a_field_here": "ここにフィールドをドロップ",
"drop_field_or": "フィールドをドロップまたは",
"edit_csv_mapping": "CSVマッピングを編集",
@@ -3642,47 +3682,64 @@
"enum": "列挙型",
"failed_to_load_feedback_records": "フィードバックレコードの読み込みに失敗しました",
"feedback_date": "現在の日付",
"feedback_record_created_successfully": "フィードバックレコードが正常に作成されました",
"feedback_record_details": "フィードバック記録の詳細",
"feedback_record_details_description": "フィードバック レコード フィールドを確認して更新します。",
"feedback_record_directory": "フィードバックレコードディレクトリ",
"feedback_record_fields": "フィードバックレコードフィールド",
"feedback_record_mcp": "フィードバックレコードMCP",
"feedback_record_updated_successfully": "フィードバックレコードが正常に更新されました",
"feedback_record_value_required": "選択したフィールド タイプには値が必要です",
"feedback_records": "フィードバックレコード",
"feedback_records_refreshed": "フィードバックレコードを更新しました",
"feedback_sources": "フィードバックソース",
"feedback_sources_directory_access_multiple": "これらのソースからの新しいレコードは次の場所に保存されます:{directoryNames}",
"feedback_sources_directory_access_single": "このソースからの新しいレコードは次の場所に保存されます:{directoryNames}",
"feedback_sources_settings_description": "このワークスペースのすべてのフィードバックソースを接続・管理します。",
"field_group_id": "フィールドグループID",
"field_group_label": "フィールドグループラベル",
"field_id": "フィールドID",
"field_label": "フィールドラベル",
"field_type": "フィールドタイプ",
"formbricks_surveys": "Formbricks フォーム",
"frd_cannot_be_changed": "フィードバックディレクトリは作成後に変更できません。",
"go_to_feedback_record_directories": "ディレクトリ設定へ移動",
"historical_import_complete": "インポート完了: {successes}件成功、{failures}件失敗、{skipped}件スキップ(データなし)",
"import_csv_data": "フィードバックをインポート",
"import_feedback": "フィードバックをインポート",
"import_historical_responses": "過去の回答をインポート",
"import_historical_responses_description": "このアンケートから既存の回答を今すぐインポートします。",
"import_rows": "{count}行をインポート",
"import_via_source_name": "「{sourceName}」経由でインポート",
"importing_data": "データをインポート中...",
"importing_historical_data": "過去のデータをインポート中...",
"invalid_enum_values": "{field}にマッピングされた列に無効な値があります",
"invalid_values_found": "検出された値: {values}(行: {rows}{extra}",
"load_sample_csv": "サンプルCSVを読み込む",
"n_supported_questions": "{count} 件のサポートされている質問",
"manage_directories": "ディレクトリを管理",
"manage_feedback_sources": "フィードバックソースを管理する",
"metadata": "メタデータ",
"metadata_key": "メタデータキー",
"metadata_read_only_entries": "読み取り専用メタデータ値 (非文字列)",
"metadata_value": "メタデータ値",
"missing_feedback_source_title": "フィードバックソースが見つかりませんか?",
"no_feedback_record_directory_available": "このワークスペースにフィードバックレコードディレクトリが割り当てられていません。まず作成または割り当てを行ってください。",
"no_feedback_records": "フィードバックレコードはまだありません。コネクタがデータの送信を開始すると、ここにレコードが表示されます。",
"no_source_fields_loaded": "ソースフィールドがまだ読み込まれていません",
"no_sources_connected": "ソースがまだ接続されていません。開始するにはソースを追加してください。",
"no_surveys_found": "この環境にフォームが見つかりません",
"optional": "任意",
"or_drag_and_drop": "またはドラッグ&ドロップ",
"question_selected": "<strong>{count}</strong>件の質問が選択されています。これらの質問への各回答は、新しいフィードバックレコードを作成します。",
"question_type_not_supported": "この質問タイプはサポートされていません",
"questions_selected": "<strong>{count}</strong>件の質問が選択されています。これらの質問への各回答は、新しいフィードバックレコードを作成します。",
"records_will_go_to": "レコードの保存先",
"refresh_feedback_records": "フィードバック記録を更新",
"refreshing_feedback_records": "フィードバックレコードを更新中...",
"request_feedback_source": "ソース統合をリクエスト",
"required": "必須",
"save_changes": "変更を保存",
"select_a_survey_to_see_questions": "フォームを選択して質問を表示",
"select_a_value": "値を選択...",
"select_all": "すべて選択",
"select_feedback_record_directory": "ディレクトリを選択",
"select_feedback_record_source_type": "ソースタイプを選択してください",
"select_questions": "質問を選択",
"select_source_type_description": "接続するフィードバックソースの種類を選択してください。",
"select_source_type_prompt": "接続するフィードバックソースの種類を選択してください:",
"select_survey": "フォームを選択",
"select_survey_and_questions": "フォームと質問を選択",
"select_survey_questions_description": "フィードバックレコードを作成するフォームの質問を選択してください。",
@@ -3692,27 +3749,38 @@
"showing_rows": "{count}行中3行を表示",
"source": "ソース",
"source_connect_csv_description": "CSVファイルからフィードバックをインポート",
"source_connect_feedback_record_mcp_description": "MCP統合を通じてフィードバックレコードを送信します。",
"source_connect_formbricks_description": "Formbricksフォームからフィードバックを接続",
"source_fields": "ソースフィールド",
"source_id": "ソースID",
"source_name": "ソース名",
"source_type": "ソースタイプ",
"source_type_cannot_be_changed": "ソースタイプは変更できません",
"sources": "ソース",
"status_active": "進行中",
"status_completed": "完了",
"status_draft": "下書き",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "エラー",
"status_paused": "一時停止",
"status_live_sync": "リアルタイム同期",
"status_ready": "準備完了",
"submission_id": "提出ID",
"survey_has_no_questions": "このアンケートには質問がありません",
"survey_import_line": "{surveyName}: {responseCount}件の回答 × {questionCount}件の質問 = {total}件のフィードバックレコード",
"total_feedback_records": "合計: {surveyCount}件のアンケート全体で{total}件中{checked}件のフィードバックレコードが選択されています",
"topics_and_subtopics": "トピックとサブトピック",
"unify_feedback": "フィードバックを統合",
"update_mapping_description": "このソースのマッピング設定を更新します。",
"updated_at": "更新日時",
"upload_csv_data_description": "CSVファイルをアップロードして、フィードバックデータをインポートします。",
"upload_csv_file": "CSVファイルをアップロード",
"user_identifier": "ユーザー",
"value": "値"
"value": "値",
"value_boolean": "値 (ブール値)",
"value_date": "値 (日付)",
"value_number": "値(数値)",
"value_text": "値 (テキスト)"
},
"xm-templates": {
"ces": "CES",
+123 -55
View File
@@ -125,6 +125,7 @@
"activity": "Activiteit",
"add": "Toevoegen",
"add_action": "Actie toevoegen",
"add_chart": "Diagram toevoegen",
"add_charts": "Grafieken toevoegen",
"add_existing_chart_description": "Zoek en selecteer grafieken om toe te voegen aan dit dashboard.",
"add_filter": "Filter toevoegen",
@@ -147,6 +148,7 @@
"apply_filters": "Pas filters toe",
"archived": "Gearchiveerd",
"are_you_sure": "Weet je het zeker?",
"ask": "Ask",
"attributes": "Kenmerken",
"back": "Rug",
"billing": "Facturering",
@@ -159,7 +161,7 @@
"change_workspace": "Werkruimte wijzigen",
"chart": "Grafiek",
"charts": "Grafieken",
"choice_n": "Keuze {{n}}",
"choice_n": "Keuze {n}",
"choices": "Keuzes",
"choose_organization": "Kies organisatie",
"choose_workspace": "Kies werkruimte",
@@ -172,7 +174,7 @@
"close": "Dichtbij",
"code": "Code",
"collapse_rows": "Rijen samenvouwen",
"column_n": "Kolom {{n}}",
"column_n": "Kolom {n}",
"completed": "Voltooid",
"configuration": "Configureren",
"confirm": "Bevestigen",
@@ -212,6 +214,7 @@
"delete_what": "Verwijder {deleteWhat}",
"description": "Beschrijving",
"disable": "Uitzetten",
"disabled": "Uitgeschakeld",
"disallow": "Niet toestaan",
"discard": "Weggooien",
"dismissed": "Afgewezen",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Laden van organisaties mislukt",
"failed_to_load_workspaces": "Laden van werkruimtes mislukt",
"failed_to_parse_csv": "Kan CSV niet verwerken",
"field_placeholder": "Tijdelijke aanduiding voor {{field}}",
"field_placeholder": "{field} Tijdelijke aanduiding",
"filter": "Filter",
"finish": "Finish",
"first_name": "Voornaam",
@@ -331,6 +334,7 @@
"not_authenticated": "U bent niet geverifieerd om deze actie uit te voeren.",
"not_authorized": "Niet geautoriseerd",
"not_connected": "Niet verbonden",
"not_set": "Niet ingesteld",
"note": "Opmerking",
"notifications": "Meldingen",
"number": "Nummer",
@@ -390,13 +394,14 @@
"report_survey": "Enquête melden",
"request_trial_license": "Proeflicentie aanvragen",
"reset_to_default": "Resetten naar standaard",
"resize": "Formaat wijzigen",
"response": "Antwoord",
"response_id": "Antwoord-ID",
"responses": "Reacties",
"restart": "Opnieuw opstarten",
"retry": "Opnieuw proberen",
"role": "Rol",
"row_n": "Rij {{n}}",
"row_n": "Rij {n}",
"saas": "SaaS",
"sales": "Verkoop",
"save": "Redden",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Sommige bestanden konden niet worden geüpload",
"something_went_wrong": "Er is iets misgegaan",
"something_went_wrong_please_try_again": "Er is iets misgegaan. Probeer het opnieuw.",
"soon": "Binnenkort",
"sort_by": "Sorteer op",
"start_free_trial": "Start gratis proefperiode",
"status": "Status",
@@ -502,7 +508,7 @@
"welcome_card": "Welkomstkaart",
"workspace": "Werkruimte",
"workspace_configuration": "Werkruimte-configuratie",
"workspace_created_successfully": "Project succesvol aangemaakt",
"workspace_created_successfully": "Werkruimte succesvol aangemaakt",
"workspace_creation_description": "Organiseer enquêtes in werkruimtes voor beter toegangsbeheer.",
"workspace_id": "Werkruimte-ID",
"workspace_name": "Werkruimtenaam",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Staafdiagram",
"chart_type_big_number": "Groot getal",
"chart_type_line": "Lijndiagram",
"chart_type_not_supported": "Grafiektype \"{{chartType}}\" wordt nog niet ondersteund",
"chart_type_not_supported": "Grafiektype \"{chartType}\" wordt nog niet ondersteund",
"chart_type_pie": "Cirkeldiagram",
"chart_updated_successfully": "Grafiek succesvol bijgewerkt!",
"configure_description": "Pas het diagramtype en andere instellingen voor deze visualisatie aan.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Query uitvoeren mislukt",
"failed_to_load_chart": "Diagram laden mislukt",
"failed_to_load_chart_data": "Diagramdata laden mislukt",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Opslaan van diagram mislukt",
"field": "Veld",
"field_label_average_score": "Gemiddelde score",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Geen geldige gegevens om weer te geven",
"not_contains": "bevat niet",
"not_equals": "is niet gelijk aan",
"open_chart": "Open diagram {{name}}",
"open_chart": "Open grafiek {name}",
"open_options": "Open diagramopties",
"or_filter_logic": "OF",
"original": "Origineel",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Selecteer een dashboard",
"predefined_measures": "Vooraf gedefinieerde metingen",
"preset": "Voorinstelling",
"preview_chart": "Voorbeeldgrafiek",
"query_executed_successfully": "Query succesvol uitgevoerd",
"reset_to_ai_suggestion": "Herstel naar AI-suggestie",
"save_and_add_to_dashboard": "Opslaan en toevoegen aan dashboard",
"save_chart": "Diagram opslaan",
"save_chart_dialog_title": "Diagram opslaan",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Selecteer veld",
"select_measures": "Selecteer metingen...",
"select_preset": "Selecteer voorinstelling",
"showing_first_n_of": "Eerste {{n}} van {{count}} rijen worden getoond",
"showing_first_n_of": "Toont eerste {n} van {count} rijen",
"start_date": "Startdatum",
"time_dimension": "Tijdsdimensie",
"time_dimension_title": "Tijdgebaseerde groepering toevoegen",
"time_dimension_toggle_description": "Volg trends over tijd."
"time_dimension_toggle_description": "Volg trends over tijd.",
"update_chart": "Diagram bijwerken"
},
"dashboards": {
"add_count_charts": "{count} grafiek(en) toevoegen",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Dashboard maken",
"create_dashboard_description": "Voer een naam in voor je nieuwe dashboard.",
"create_failed": "Dashboard creëren mislukt",
"create_new_chart": "Maak een nieuw diagram",
"create_success": "Dashboard succesvol aangemaakt!",
"dashboard": "Dashboard",
"dashboard_delete_confirmation": "Weet je zeker dat je dit dashboard wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Dashboard dupliceren mislukt",
"duplicate_success": "Dashboard succesvol gedupliceerd!",
"failed_to_load_chart_data": "Grafiekgegevens laden mislukt",
"no_charts_available_description": "Er zijn geen grafieken die aan dit dashboard kunnen worden toegevoegd. Er bestaan nog geen grafieken, of alle bestaande grafieken zijn al toegevoegd. Ga naar de pagina Grafieken om nieuwe grafieken te maken.",
"no_charts_to_add_message": "Geen grafieken om toe te voegen aan dit dashboard.",
"no_dashboards_found": "Geen dashboards gevonden.",
"no_data_message": "Geen gegevens. Er is momenteel geen informatie om weer te geven. Voeg grafieken toe om je dashboard op te bouwen.",
"please_enter_name": "Voer een dashboardnaam in"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "U heeft geen feedbackrecords om over te rapporteren. Stel feedbackbronnen in om gegevens in het systeem in te voeren.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Feedbackbronnen instellen"
},
"api_keys": {
"add_api_key": "API-sleutel toevoegen",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Verbind uw app of website met Formbricks.",
"cache_update_delay_description": "Wanneer u wijzigingen aanbrengt in enquêtes, contacten, acties of andere gegevens, kan het tot 1 minuut duren voordat deze wijzigingen verschijnen in uw lokale app die de Formbricks SDK gebruikt.",
"cache_update_delay_title": "Wijzigingen worden na ~1 minuut weergegeven vanwege caching",
"environment_id_legacy": "Omgeving-ID (verouderd)",
"environment_id_legacy_alert": "Je bestaande SDK-configuratie gebruikt mogelijk nog een verouderde Omgeving-ID.",
"environment_id_legacy_alert_link": "Lees waarom en hoe je kunt migreren.",
"formbricks_sdk_connected": "Formbricks SDK is verbonden",
"formbricks_sdk_not_connected": "Formbricks SDK is nog niet verbonden.",
"formbricks_sdk_not_connected_description": "Voeg de Formbricks SDK toe aan uw website of app om deze te verbinden met Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Formbricks-logo",
"general": {
"cannot_delete_only_workspace": "Dit is uw enige project, het kan niet worden verwijderd. Maak eerst een nieuw project aan.",
"cannot_delete_only_workspace": "Dit is je enige werkruimte en kan niet worden verwijderd. Maak eerst een nieuwe werkruimte aan.",
"custom_scripts": "Aangepaste scripts",
"custom_scripts_card_description": "Voeg trackingscripts en pixels toe aan alle linkenquêtes in deze werkruimte.",
"custom_scripts_description": "Scripts worden geïnjecteerd in de <head> van alle linkenquêtepagina's.",
@@ -1989,20 +2005,20 @@
"custom_scripts_placeholder": "<!-- Plak hier je trackingscripts -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Aangepaste scripts succesvol bijgewerkt",
"custom_scripts_warning": "Scripts worden uitgevoerd met volledige browsertoegang. Voeg alleen scripts toe van vertrouwde bronnen.",
"delete_workspace": "Project verwijderen",
"delete_workspace": "Werkruimte verwijderen",
"delete_workspace_confirmation": "Weet je zeker dat je {workspaceName} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Verwijder {workspaceName} inclusief alle enquêtes, antwoorden, mensen, acties en attributen.",
"delete_workspace_settings_description": "Verwijder project met alle enquêtes, reacties, mensen, acties en attributen. Dit kan niet ongedaan worden gemaakt.",
"error_saving_workspace_information": "Fout bij opslaan van projectinformatie",
"only_owners_or_managers_can_delete_workspaces": "Alleen eigenaren of beheerders kunnen projecten verwijderen",
"delete_workspace_settings_description": "Verwijder werkruimte met alle enquêtes, reacties, personen, acties en attributen. Dit kan niet ongedaan worden gemaakt.",
"error_saving_workspace_information": "Fout bij het opslaan van werkruimte-informatie",
"only_owners_or_managers_can_delete_workspaces": "Alleen eigenaren of managers kunnen werkruimtes verwijderen",
"recontact_waiting_time": "Afkoelperiode (voor alle enquêtes)",
"recontact_waiting_time_settings_description": "Bepaal hoe vaak gebruikers kunnen worden bevraagd voor alle website- en app-enquêtes in deze workspace.",
"this_action_cannot_be_undone": "Deze actie kan niet ongedaan worden gemaakt.",
"wait_x_days_before_showing_next_survey": "Wacht X dagen voordat de volgende enquête wordt getoond:",
"waiting_period_updated_successfully": "Wachtperiode succesvol bijgewerkt",
"whats_your_workspace_called": "Hoe heet uw project?",
"workspace_deleted_successfully": "Project succesvol verwijderd",
"workspace_name_settings_description": "Wijzig de naam van uw project.",
"whats_your_workspace_called": "Hoe heet je werkruimte?",
"workspace_deleted_successfully": "Werkruimte succesvol verwijderd",
"workspace_name_settings_description": "Wijzig de naam van je werkruimte.",
"workspace_name_updated_successfully": "Werkruimtenaam succesvol bijgewerkt"
},
"integrations": {
@@ -2389,7 +2405,7 @@
"most_popular": "Meest populair",
"pending_change_removed": "Geplande abonnementswijziging verwijderd.",
"pending_plan_badge": "Gepland",
"pending_plan_change_description": "Je abonnement wordt op {{date}} omgezet naar {{plan}}.",
"pending_plan_change_description": "Je abonnement wordt op {date} omgezet naar {plan}.",
"pending_plan_change_title": "Geplande abonnementswijziging",
"pending_plan_cta": "Gepland",
"per_month": "per maand",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Er bestaat al een feedback-recordmap met deze naam.",
"error_directory_name_required": "Mapnaam is verplicht.",
"error_directory_workspaces_invalid_org": "Sommige opgegeven werkruimtes behoren niet tot deze organisatie.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Feedbackmappen",
"no_access": "Je hebt geen toestemming om feedbackregistratiemappen te beheren.",
"no_connectors": "Nog geen connectoren gekoppeld aan deze map.",
"pause_connectors_confirmation_description": "Als je deze connectoren pauzeert, worden er geen nieuwe records meer toegevoegd.",
"pause_connectors_confirmation_title": "Gekoppelde connectoren pauzeren?",
"select_workspaces_placeholder": "Selecteer werkruimtes...",
"show_archived": "Gearchiveerde weergeven",
"title": "Feedbackregistratiemappen",
"unarchive": "Dearchiveren"
"unarchive": "Dearchiveren",
"unarchive_workspace_conflict": "Deze map kan niet worden gedearchiveerd omdat een of meer toegewezen workspaces zijn gearchiveerd.",
"workspace_access": "Workspace-toegang"
},
"general": {
"ai_data_analysis_disabled_for_organization": "AI-gegevensverrijking en -analyse is uitgeschakeld voor deze organisatie.",
"ai_data_analysis_enabled": "Dataverrijking & analyse (AI)",
"ai_data_analysis_enabled_description": "AI om meer uit je data te halen, dashboards op te zetten, grafieken, rapporten en meer. Raakt je ervaringsdata aan.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "Beheer AI-functies voor deze organisatie.",
"ai_features_not_enabled_for_organization": "AI-functies zijn niet ingeschakeld voor deze organisatie.",
"ai_instance_not_configured": "AI wordt op instantieniveau geconfigureerd via omgevingsvariabelen. Vraag je beheerder om AI_PROVIDER, de inloggegevens voor die provider en de bijbehorende modellenlijst in te stellen voordat AI-functies worden ingeschakeld.",
"ai_settings_updated_successfully": "AI-instellingen succesvol bijgewerkt",
"ai_smart_tools_disabled_for_organization": "AI-slimme functies zijn uitgeschakeld voor deze organisatie.",
"ai_smart_tools_enabled": "Slimme functionaliteit (AI)",
"ai_smart_tools_enabled_description": "AI om je te helpen meer te bereiken in minder tijd. Raakt nooit data aan die met Formbricks is verzameld. Wordt alleen gebruikt om bijvoorbeeld enquêtes naar andere talen te vertalen.",
"bulk_invite_warning_description": "Bij het gratis abonnement krijgen alle organisatieleden altijd de rol 'Eigenaar' toegewezen.",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "U kunt deze organisatie niet verlaten, aangezien dit uw enige organisatie is. Maak eerst een nieuwe organisatie aan.",
"copy_invite_link_to_clipboard": "Kopieer de uitnodigingslink naar het klembord",
"create_new_organization": "Creëer een nieuwe organisatie",
"create_new_organization_description": "Creëer een nieuwe organisatie om een andere reeks projecten af te handelen.",
"create_new_organization_description": "Maak een nieuwe organisatie aan om een andere set werkruimtes te beheren.",
"customize_email_with_a_higher_plan": "Pas e-mail aan met een hoger abonnement",
"delete_member_confirmation": "Verwijderde leden verliezen de toegang tot alle projecten en enquêtes van uw organisatie.",
"delete_member_confirmation": "Verwijderde leden verliezen toegang tot alle werkruimtes en enquêtes van je organisatie.",
"delete_organization": "Organisatie verwijderen",
"delete_organization_description": "Verwijder de organisatie met al haar projecten, inclusief alle enquêtes, reacties, mensen, acties en attributen",
"delete_organization_description": "Verwijder organisatie met al haar werkruimtes inclusief alle enquêtes, reacties, personen, acties en attributen",
"delete_organization_warning": "Voordat u doorgaat met het verwijderen van deze organisatie, moet u rekening houden met de volgende gevolgen:",
"delete_organization_warning_1": "Permanente verwijdering van alle projecten die aan deze organisatie zijn gekoppeld.",
"delete_organization_warning_1": "Permanente verwijdering van alle werkruimtes die aan deze organisatie zijn gekoppeld.",
"delete_organization_warning_2": "Deze actie kan niet ongedaan worden gemaakt. Als het weg is, is het weg.",
"delete_organization_warning_3": "Voer {organizationName} in het volgende veld in om de definitieve verwijdering van deze organisatie te bevestigen:",
"eliminate_branding_with_whitelabel": "Elimineer de Formbricks-branding en maak extra white-label aanpassingsopties mogelijk.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Meld je hier aan.",
"share_invite_link": "Deel de uitnodigingslink",
"share_this_link_to_let_your_organization_member_join_your_organization": "Deel deze link om uw organisatielid lid te laten worden van uw organisatie:",
"test_email_sent_successfully": "Test-e-mail succesvol verzonden"
"test_email_sent_successfully": "Test-e-mail succesvol verzonden",
"unlock_ai_features_description": "AI-vertalingen, slimme tools en data-analyse zijn beschikbaar op hogere abonnementen. Upgrade om je enquêtes te versterken met AI.",
"unlock_ai_features_with_a_higher_plan": "Ontgrendel AI-functies met een hoger abonnement"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Automatisch abonneren op nieuwe enquêtes",
@@ -2672,7 +2692,7 @@
"add_workspaces_description": "Bepaal tot welke werkruimtes de teamleden toegang hebben.",
"all_members_added": "Alle leden zijn aan dit team toegevoegd.",
"all_workspaces_added": "Alle werkruimtes toegevoegd aan dit team.",
"are_you_sure_you_want_to_delete_this_team": "Weet u zeker dat u dit team wilt verwijderen? Hiermee wordt ook de toegang verwijderd tot alle projecten en enquêtes die aan dit team zijn gekoppeld.",
"are_you_sure_you_want_to_delete_this_team": "Weet je zeker dat je dit team wilt verwijderen? Dit verwijdert ook de toegang tot alle werkruimtes en enquêtes die bij dit team horen.",
"billing_role_description": "U heeft alleen toegang tot factuurgegevens.",
"bulk_invite": "Bulk-uitnodiging",
"contributor": "Bijdrager",
@@ -2688,10 +2708,10 @@
"manage": "Beheren",
"manage_team": "Beheer team",
"manage_team_disabled": "Alleen organisatie-eigenaren, managers en teambeheerders kunnen teams beheren.",
"manager_role_description": "Managers hebben toegang tot alle projecten en kunnen leden toevoegen en verwijderen.",
"manager_role_description": "Managers hebben toegang tot alle werkruimtes en kunnen leden toevoegen en verwijderen.",
"member": "Lid",
"member_role_description": "Leden kunnen in geselecteerde projecten werken.",
"member_role_info_message": "Om nieuwe leden toegang te geven tot een project, voegt u ze hieronder toe aan een team. Met Teams kun je beheren wie toegang heeft tot welk project.",
"member_role_description": "Leden kunnen werken in geselecteerde werkruimtes.",
"member_role_info_message": "Om nieuwe leden toegang te geven tot een werkruimte, voeg ze toe aan een Team hieronder. Met Teams kun je beheren wie toegang heeft tot welke werkruimte.",
"organization_role": "Organisatierol",
"owner_role_description": "Eigenaars hebben volledige controle over de organisatie.",
"please_fill_all_member_fields": "Vul alle velden in om een nieuw lid toe te voegen.",
@@ -2708,8 +2728,8 @@
"team_settings_description": "Beheer teamleden, toegangsrechten en meer.",
"team_updated_successfully": "Team succesvol bijgewerkt",
"teams": "Teams",
"teams_description": "Wijs leden toe aan teams en geef teams toegang tot projecten.",
"unlock_teams_description": "Beheer welke organisatieleden toegang hebben tot specifieke projecten en enquêtes.",
"teams_description": "Wijs leden toe aan teams en geef teams toegang tot werkruimtes.",
"unlock_teams_description": "Beheer welke organisatieleden toegang hebben tot specifieke werkruimtes en enquêtes.",
"unlock_teams_title": "Ontgrendel teams met een hoger plan.",
"upgrade_plan_notice_message": "Ontgrendel organisatierollen met een hoger plan.",
"you_are_a_member": "Je bent lid"
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Pas het bericht 'Enquête gesloten' aan",
"adjust_survey_closed_message_description": "Wijzig het bericht dat bezoekers zien wanneer de enquête wordt gesloten.",
"adjust_the_theme_in_the": "Pas het thema aan in de",
"ai_data_analysis_disabled": "AI-gegevensanalyse is uitgeschakeld voor deze organisatie.",
"ai_features_not_enabled": "AI-functies zijn niet ingeschakeld voor deze organisatie.",
"ai_instance_not_configured": "AI is niet geconfigureerd. Neem contact op met je beheerder.",
"ai_smart_tools_disabled": "AI slimme tools zijn uitgeschakeld voor deze organisatie.",
"ai_translate": "Vertalen met AI",
"ai_translating": "Vertalen met AI... Laat dit venster open.",
"ai_translation_all_fields_populated": "Alle velden zijn al vertaald",
"ai_translation_complete": "AI-vertaling voltooid",
"ai_translation_failed": "Vertaling mislukt",
"ai_translation_instance_not_configured": "AI is niet geconfigureerd op deze instantie. Neem contact op met je beheerder.",
"ai_translation_not_available": "AI-vertaling is niet beschikbaar op je huidige abonnement. Upgrade om deze functie te ontgrendelen.",
"ai_translation_not_enabled": "AI smart tools zijn uitgeschakeld voor deze organisatie. Schakel ze in via de organisatie-instellingen.",
"all_are_true": "alle zijn waar",
"all_other_answers_will_continue_to": "Alle andere antwoorden blijven hetzelfde",
"allow_multi_select": "Multi-select toestaan",
@@ -3043,7 +3075,7 @@
"options_used_in_logic_bulk_error": "De volgende opties worden gebruikt in logica: {questionIndexes}. Verwijder ze eerst uit de logica.",
"override_theme_with_individual_styles_for_this_survey": "Overschrijf het thema met individuele stijlen voor deze enquête.",
"overwrite_global_waiting_time": "Aangepaste afkoelperiode instellen",
"overwrite_global_waiting_time_description": "Overschrijf de projectconfiguratie alleen voor deze enquête.",
"overwrite_global_waiting_time_description": "Overschrijf de werkruimte-instellingen alleen voor deze enquête.",
"overwrite_placement": "Plaatsing overschrijven",
"overwrite_survey_logo": "Stel aangepast enquêtelogo in",
"overwrite_the_global_placement_of_the_survey": "Overschrijf de globale plaatsing van de enquête",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Bekijk welke teams toegang hebben tot deze workspace."
},
"unify": {
"add_feedback_record": "Feedbackrecord toevoegen",
"add_feedback_record_description": "Maak handmatig een feedbackrecord.",
"add_feedback_source": "Feedbackbron toevoegen",
"add_source": "Bron toevoegen",
"allowed_values": "Toegestane waarden: {values}",
"api_ingestion": "API-inname",
"api_ingestion_manage_api_keys": "API-sleutels beheren",
"api_ingestion_settings_description": "Verstuur feedbackrecords via de Management API.",
"auto_generated": "Automatisch gegenereerd",
"change_file": "Bestand wijzigen",
"click_load_sample_csv": "Klik op 'Voorbeeld CSV laden' om kolommen te zien",
"click_to_upload": "Klik om te uploaden",
"collected_at": "Verzameld op",
"configure_import": "Import configureren",
"configure_mapping": "Koppeling configureren",
"connection": "Verbinding",
"connector_created_successfully": "Connector succesvol aangemaakt",
"connector_deleted_successfully": "Connector succesvol verwijderd",
"connector_duplicated_successfully": "Connector succesvol gedupliceerd",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Gegevens twee keer importeren zal dubbele records aanmaken.",
"csv_inconsistent_columns": "Rij {row} heeft inconsistente kolommen. Alle rijen moeten dezelfde headers hebben.",
"csv_max_records": "Maximaal {max} records toegestaan.",
"custom_source_type": "Aangepast brontype",
"custom_source_type_placeholder": "Voer een aangepast brontype in",
"default_connector_name_csv": "CSV import",
"default_connector_name_formbricks": "Formbricks Survey verbinding",
"deselect_all": "Alles deselecteren",
"discard_feedback_record_changes_description": "Als u deze lade sluit, gaan uw wijzigingen verloren.",
"discard_feedback_record_changes_title": "Niet-opgeslagen wijzigingen verwijderen?",
"drop_a_field_here": "Zet hier een veld neer",
"drop_field_or": "Zet veld neer of",
"edit_csv_mapping": "CSV-mapping bewerken",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Kan feedbackrecords niet laden",
"feedback_date": "Huidige datum",
"feedback_record_created_successfully": "Feedbackrecord is succesvol aangemaakt",
"feedback_record_details": "Details van feedbackrecord",
"feedback_record_details_description": "Controleer en update de feedbackrecordvelden.",
"feedback_record_directory": "Feedbackrecordmap",
"feedback_record_fields": "Feedbackrecordvelden",
"feedback_record_mcp": "Feedbackrecord MCP",
"feedback_record_updated_successfully": "Feedbackrecord is succesvol bijgewerkt",
"feedback_record_value_required": "Er is een waarde vereist voor het geselecteerde veldtype",
"feedback_records": "Feedbackrecords",
"feedback_records_refreshed": "Feedbackrecords vernieuwd",
"feedback_sources": "Feedbackbronnen",
"feedback_sources_directory_access_multiple": "Nieuwe records van deze bronnen worden opgeslagen in: {directoryNames}",
"feedback_sources_directory_access_single": "Nieuwe records van deze bron worden opgeslagen in: {directoryNames}",
"feedback_sources_settings_description": "Verbind en beheer alle feedbackbronnen voor deze werkruimte.",
"field_group_id": "Veldgroep-ID",
"field_group_label": "Veldgroeplabel",
"field_id": "Veld-ID",
"field_label": "Veldlabel",
"field_type": "Veldtype",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedbackmap kan niet worden gewijzigd na aanmaak.",
"go_to_feedback_record_directories": "Ga naar map-instellingen",
"historical_import_complete": "Import voltooid: {successes} geslaagd, {failures} mislukt, {skipped} overgeslagen (geen data)",
"import_csv_data": "Feedback importeren",
"import_feedback": "Feedback importeren",
"import_historical_responses": "Historische reacties importeren",
"import_historical_responses_description": "Importeer bestaande reacties van deze enquête nu.",
"import_rows": "{count, plural, one {Importeer 1 rij} other {Importeer # rijen}}",
"import_via_source_name": "Importeren via \"{sourceName}\"",
"importing_data": "Gegevens importeren...",
"importing_historical_data": "Historische gegevens importeren...",
"invalid_enum_values": "Ongeldige waarden in kolom gekoppeld aan {field}",
"invalid_values_found": "Gevonden: {values} (rijen: {rows}) {extra}",
"load_sample_csv": "Voorbeeld-CSV laden",
"n_supported_questions": "{count} ondersteunde vragen",
"manage_directories": "Mappen beheren",
"manage_feedback_sources": "Beheer feedbackbronnen",
"metadata": "Metagegevens",
"metadata_key": "Metagegevenssleutel",
"metadata_read_only_entries": "Alleen-lezen metadatawaarden (niet-tekenreeks)",
"metadata_value": "Metagegevenswaarde",
"missing_feedback_source_title": "Mis je een feedbackbron?",
"no_feedback_record_directory_available": "Geen feedbackrecordmap toegewezen aan deze workspace. Maak er eerst een aan of wijs er een toe.",
"no_feedback_records": "Nog geen feedbackrecords. Records verschijnen hier zodra je connectoren gegevens beginnen te verzenden.",
"no_source_fields_loaded": "Nog geen bronvelden geladen",
"no_sources_connected": "Nog geen bronnen verbonden. Voeg een bron toe om te beginnen.",
"no_surveys_found": "Geen enquêtes gevonden in deze omgeving",
"optional": "Optioneel",
"or_drag_and_drop": "of sleep en zet neer",
"question_selected": "<strong>{count}</strong> vraag geselecteerd. Elk antwoord op deze vraag zal een nieuw feedbackrecord aanmaken.",
"question_type_not_supported": "Dit vraagtype wordt niet ondersteund",
"questions_selected": "<strong>{count}</strong> vragen geselecteerd. Elk antwoord op deze vragen zal een nieuw feedbackrecord aanmaken.",
"records_will_go_to": "Records gaan naar",
"refresh_feedback_records": "Feedbackrecords verversen",
"refreshing_feedback_records": "Feedbackrecords vernieuwen...",
"request_feedback_source": "Bronintegratie aanvragen",
"required": "Vereist",
"save_changes": "Wijzigingen opslaan",
"select_a_survey_to_see_questions": "Selecteer een enquête om de vragen te zien",
"select_a_value": "Selecteer een waarde...",
"select_all": "Selecteer alles",
"select_feedback_record_directory": "Selecteer een map",
"select_feedback_record_source_type": "Selecteer brontype",
"select_questions": "Selecteer vragen",
"select_source_type_description": "Selecteer het type feedbackbron dat je wilt verbinden.",
"select_source_type_prompt": "Selecteer het type feedbackbron dat je wilt verbinden:",
"select_survey": "Selecteer enquête",
"select_survey_and_questions": "Selecteer enquête & vragen",
"select_survey_questions_description": "Kies welke enquêtevragen FeedbackRecords moeten aanmaken.",
@@ -3692,27 +3749,38 @@
"showing_rows": "3 van {count} rijen weergegeven",
"source": "bron",
"source_connect_csv_description": "Importeer feedback uit CSV-bestanden",
"source_connect_feedback_record_mcp_description": "Verstuur feedbackrecords via de MCP-integratie.",
"source_connect_formbricks_description": "Verbind feedback van je Formbricks-enquêtes",
"source_fields": "Bronvelden",
"source_id": "Bron-ID",
"source_name": "Bronnaam",
"source_type": "Brontype",
"source_type_cannot_be_changed": "Brontype kan niet worden gewijzigd",
"sources": "Bronnen",
"status_active": "In uitvoering",
"status_completed": "Voltooid",
"status_draft": "Voorlopige versie",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Fout",
"status_paused": "Gepauzeerd",
"status_live_sync": "Live synchronisatie",
"status_ready": "Klaar",
"submission_id": "Inzendings-ID",
"survey_has_no_questions": "Deze enquête heeft geen vragen",
"survey_import_line": "{surveyName}: {responseCount} antwoorden × {questionCount} vragen = {total} feedbackrecords",
"total_feedback_records": "Totaal: {checked} van {total} feedbackrecords geselecteerd over {surveyCount} enquêtes",
"topics_and_subtopics": "Onderwerpen en subonderwerpen",
"unify_feedback": "Feedback verenigen",
"update_mapping_description": "Werk de mappingconfiguratie voor deze bron bij.",
"updated_at": "Bijgewerkt op",
"upload_csv_data_description": "Upload een CSV-bestand om feedbackgegevens te importeren.",
"upload_csv_file": "CSV-bestand uploaden",
"user_identifier": "Gebruiker",
"value": "Waarde"
"value": "Waarde",
"value_boolean": "Waarde (Booleaans)",
"value_date": "Waarde (datum)",
"value_number": "Waarde (getal)",
"value_text": "Waarde (tekst)"
},
"xm-templates": {
"ces": "CES",
+132 -64
View File
@@ -125,14 +125,15 @@
"activity": "Atividade",
"add": "Adicionar",
"add_action": "Adicionar ação",
"add_chart": "Adicionar gráfico",
"add_charts": "Adicionar gráficos",
"add_existing_chart_description": "Pesquise e selecione gráficos para adicionar a este painel.",
"add_filter": "Adicionar filtro",
"add_logo": "Adicionar logo",
"add_member": "Adicionar membro",
"add_new_workspace": "Adicionar novo projeto",
"add_new_workspace": "Adicionar novo workspace",
"add_to_team": "Adicionar à equipe",
"add_workspace": "Adicionar projeto",
"add_workspace": "Adicionar workspace",
"all": "Todos",
"all_questions": "Todas as perguntas",
"allow": "permitir",
@@ -147,6 +148,7 @@
"apply_filters": "Aplicar filtros",
"archived": "Arquivado",
"are_you_sure": "Certeza?",
"ask": "Ask",
"attributes": "atributos",
"back": "Voltar",
"billing": "Faturamento",
@@ -159,10 +161,10 @@
"change_workspace": "Alterar espaço de trabalho",
"chart": "Gráfico",
"charts": "Gráficos",
"choice_n": "Escolha {{n}}",
"choice_n": "Escolha {n}",
"choices": "Escolhas",
"choose_organization": "Escolher organização",
"choose_workspace": "Escolher projeto",
"choose_workspace": "Escolher workspace",
"clear_all": "Limpar tudo",
"clear_filters": "Limpar filtros",
"clear_selection": "Limpar seleção",
@@ -172,7 +174,7 @@
"close": "Fechar",
"code": "Código",
"collapse_rows": "Recolher linhas",
"column_n": "Coluna {{n}}",
"column_n": "Coluna {n}",
"completed": "Concluído",
"configuration": "Configurar",
"confirm": "Confirmar",
@@ -197,7 +199,7 @@
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar pesquisa",
"create_workspace": "Criar projeto",
"create_workspace": "Criar workspace",
"created": "Criado",
"created_at": "Data de criação",
"created_by": "Criado por",
@@ -212,6 +214,7 @@
"delete_what": "Excluir {deleteWhat}",
"description": "Descrição",
"disable": "desativar",
"disabled": "Desativado",
"disallow": "Não permita",
"discard": "Descartar",
"dismissed": "Dispensado",
@@ -241,9 +244,9 @@
"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",
"failed_to_load_workspaces": "Falha ao carregar workspaces",
"failed_to_parse_csv": "Falha ao analisar CSV",
"field_placeholder": "Espaço reservado de {{field}}",
"field_placeholder": "Espaço reservado de {field}",
"filter": "Filtro",
"finish": "Terminar",
"first_name": "Primeiro nome",
@@ -331,6 +334,7 @@
"not_authenticated": "Você não está autenticado para realizar essa ação.",
"not_authorized": "Não autorizado",
"not_connected": "Desconectado",
"not_set": "Não definido",
"note": "Nota",
"notifications": "Notificações",
"number": "Número",
@@ -390,13 +394,14 @@
"report_survey": "Relatório de Pesquisa",
"request_trial_license": "Pedir licença de teste",
"reset_to_default": "Restaurar para o padrão",
"resize": "Redimensionar",
"response": "Resposta",
"response_id": "ID da resposta",
"responses": "Respostas",
"restart": "Reiniciar",
"retry": "Tentar novamente",
"role": "Rolê",
"row_n": "Linha {{n}}",
"row_n": "Linha {n}",
"saas": "SaaS",
"sales": "vendas",
"save": "Salvar",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Alguns arquivos falharam ao enviar",
"something_went_wrong": "Algo deu errado",
"something_went_wrong_please_try_again": "Algo deu errado. Tente novamente.",
"soon": "Em breve",
"sort_by": "Ordenar por",
"start_free_trial": "Iniciar teste gratuito",
"status": "status",
@@ -474,7 +480,7 @@
"type": "Tipo",
"unify": "Unificar",
"unknown_survey": "Pesquisa desconhecida",
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais workspaces com um plano superior.",
"update": "atualizar",
"updated": "atualizado",
"updated_at": "Atualizado em",
@@ -501,13 +507,13 @@
"weeks": "semanas",
"welcome_card": "Cartão de boas-vindas",
"workspace": "Espaço de trabalho",
"workspace_configuration": "Configuração do projeto",
"workspace_created_successfully": "Projeto criado com sucesso",
"workspace_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
"workspace_id": "ID do projeto",
"workspace_name": "Nome do projeto",
"workspace_configuration": "Configuração do Workspace",
"workspace_created_successfully": "Workspace criado com sucesso",
"workspace_creation_description": "Organize pesquisas em workspaces para melhor controle de acesso.",
"workspace_id": "ID do Workspace",
"workspace_name": "Nome do Workspace",
"workspace_name_placeholder": "ex: Formbricks",
"workspaces": "Projetos",
"workspaces": "Workspaces",
"years": "anos",
"yes": "Sim",
"you_are_downgraded_to_the_community_edition": "Você foi rebaixado para a Edição Comunitária.",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Gráfico de barras",
"chart_type_big_number": "Número grande",
"chart_type_line": "Gráfico de linhas",
"chart_type_not_supported": "Tipo de gráfico \"{{chartType}}\" ainda não suportado",
"chart_type_not_supported": "Tipo de gráfico \"{chartType}\" ainda não é suportado",
"chart_type_pie": "Gráfico de pizza",
"chart_updated_successfully": "Gráfico atualizado com sucesso!",
"configure_description": "Modifique o tipo de gráfico e outras configurações para esta visualização.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Falha ao executar consulta",
"failed_to_load_chart": "Falha ao carregar gráfico",
"failed_to_load_chart_data": "Falha ao carregar dados do gráfico",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Falha ao salvar gráfico",
"field": "Campo",
"field_label_average_score": "Pontuação média",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Nenhum dado válido para exibir",
"not_contains": "não contém",
"not_equals": "diferente de",
"open_chart": "Abrir gráfico {{name}}",
"open_chart": "Abrir gráfico {name}",
"open_options": "Abrir opções do gráfico",
"or_filter_logic": "OU",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Por favor, selecione um painel",
"predefined_measures": "Medidas predefinidas",
"preset": "Predefinição",
"preview_chart": "Visualizar gráfico",
"query_executed_successfully": "Consulta executada com sucesso",
"reset_to_ai_suggestion": "Redefinir para sugestão da IA",
"save_and_add_to_dashboard": "Salvar e adicionar ao painel",
"save_chart": "Salvar gráfico",
"save_chart_dialog_title": "Salvar gráfico",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Selecionar campo",
"select_measures": "Selecionar medidas...",
"select_preset": "Selecionar predefinição",
"showing_first_n_of": "Mostrando os primeiros {{n}} de {{count}} registros",
"showing_first_n_of": "Mostrando as primeiras {n} de {count} linhas",
"start_date": "Data inicial",
"time_dimension": "Dimensão temporal",
"time_dimension_title": "Adicionar agrupamento por tempo",
"time_dimension_toggle_description": "Monitore tendências ao longo do tempo."
"time_dimension_toggle_description": "Monitore tendências ao longo do tempo.",
"update_chart": "Atualizar gráfico"
},
"dashboards": {
"add_count_charts": "Adicionar {count} gráfico(s)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Criar painel",
"create_dashboard_description": "Digite um nome para o seu novo painel.",
"create_failed": "Falha ao criar painel",
"create_new_chart": "Criar novo gráfico",
"create_success": "Painel criado com sucesso!",
"dashboard": "Painel",
"dashboard_delete_confirmation": "Tem certeza de que deseja excluir este painel? Esta ação não pode ser desfeita.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Falha ao duplicar painel",
"duplicate_success": "Painel duplicado com sucesso!",
"failed_to_load_chart_data": "Falha ao carregar os dados do gráfico",
"no_charts_available_description": "Não há gráficos que possam ser adicionados a este painel. Ou nenhum gráfico existe ainda, ou todos os gráficos existentes já foram adicionados. Vá para a página de Gráficos para criar novos gráficos.",
"no_charts_to_add_message": "Nenhum gráfico para adicionar a este painel.",
"no_dashboards_found": "Nenhum painel encontrado.",
"no_data_message": "Sem Dados. Não há informações para exibir no momento. Adicione gráficos para construir seu painel.",
"please_enter_name": "Por favor, digite um nome para o painel"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Você não tem registros de feedback para relatar. Configure fontes de feedback para alimentar dados no sistema.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Configurar fontes de feedback"
},
"api_keys": {
"add_api_key": "Adicionar chave de API",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Conecte seu app ou site ao Formbricks.",
"cache_update_delay_description": "Quando você faz atualizações em pesquisas, contatos, ações ou outros dados, pode levar até 1 minuto para que essas alterações apareçam no seu app local executando o SDK do Formbricks.",
"cache_update_delay_title": "As alterações serão refletidas após ~1 minuto devido ao cache",
"environment_id_legacy": "ID do Ambiente (legado)",
"environment_id_legacy_alert": "Sua configuração de SDK existente pode ainda estar usando um ID de Ambiente legado.",
"environment_id_legacy_alert_link": "Saiba por que e como migrar.",
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.",
"formbricks_sdk_not_connected_description": "Adicione o SDK do Formbricks ao seu site ou app para conectá-lo ao Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Logo da Formbricks",
"general": {
"cannot_delete_only_workspace": "Este é seu único projeto, ele não pode ser excluído. Crie um novo projeto primeiro.",
"cannot_delete_only_workspace": "Este é o seu único workspace, não pode ser excluído. Crie um novo workspace primeiro.",
"custom_scripts": "Scripts personalizados",
"custom_scripts_card_description": "Adicione scripts de rastreamento e pixels a todas as pesquisas de link neste workspace.",
"custom_scripts_description": "Os scripts serão injetados no <head> de todas as páginas de pesquisa de link.",
@@ -1989,21 +2005,21 @@
"custom_scripts_placeholder": "<!-- Cole seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personalizados atualizados com sucesso",
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes confiáveis.",
"delete_workspace": "Excluir projeto",
"delete_workspace": "Excluir Workspace",
"delete_workspace_confirmation": "Tem certeza que deseja excluir {workspaceName}? Esta ação não pode ser desfeita.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Excluir {workspaceName} incluindo todas as pesquisas, respostas, pessoas, ações e atributos.",
"delete_workspace_settings_description": "Excluir projeto com todas as pesquisas, respostas, pessoas, ações e atributos. Isso não pode ser desfeito.",
"error_saving_workspace_information": "Erro ao salvar informações do projeto",
"only_owners_or_managers_can_delete_workspaces": "Apenas proprietários ou gerentes podem excluir projetos",
"delete_workspace_settings_description": "Excluir workspace com todas as pesquisas, respostas, pessoas, ações e atributos. Isso não pode ser desfeito.",
"error_saving_workspace_information": "Erro ao salvar informações do workspace",
"only_owners_or_managers_can_delete_workspaces": "Apenas proprietários ou gerentes podem excluir workspaces",
"recontact_waiting_time": "Período de espera (entre pesquisas)",
"recontact_waiting_time_settings_description": "Controle com que frequência os usuários podem ser pesquisados em todas as pesquisas de website e app neste workspace.",
"this_action_cannot_be_undone": "Essa ação não pode ser desfeita.",
"wait_x_days_before_showing_next_survey": "Aguardar X dias antes de mostrar a próxima pesquisa:",
"waiting_period_updated_successfully": "Período de espera atualizado com sucesso",
"whats_your_workspace_called": "Como se chama seu projeto?",
"workspace_deleted_successfully": "Projeto excluído com sucesso",
"workspace_name_settings_description": "Altere o nome do seu projeto.",
"workspace_name_updated_successfully": "Nome do projeto atualizado com sucesso"
"whats_your_workspace_called": "Como seu workspace se chama?",
"workspace_deleted_successfully": "Workspace excluído com sucesso",
"workspace_name_settings_description": "Altere o nome do seu workspace.",
"workspace_name_updated_successfully": "Nome do workspace atualizado com sucesso"
},
"integrations": {
"activepieces_integration_description": "Conecte o Formbricks instantaneamente com aplicativos populares para automatizar tarefas sem codificação.",
@@ -2161,7 +2177,7 @@
"alias_tooltip": "O alias é um nome alternativo para identificar o idioma em pesquisas de link e no SDK (opcional)",
"cannot_remove_language_warning": "Você não pode remover este idioma, pois ele ainda está sendo usado nestas pesquisas:",
"conflict_between_identifier_and_alias": "Há um conflito entre o identificador de um idioma adicionado e um dos seus aliases. Aliases e identificadores não podem ser idênticos.",
"conflict_between_selected_alias_and_another_language": "Há um conflito entre o alias selecionado e outro idioma que possui este identificador. Adicione o idioma com este identificador ao seu projeto para evitar inconsistências.",
"conflict_between_selected_alias_and_another_language": "Há um conflito entre o alias selecionado e outro idioma que possui este identificador. Por favor, adicione o idioma com este identificador ao seu workspace para evitar inconsistências.",
"delete_language_confirmation": "Tem certeza de que deseja excluir este idioma? Essa ação não pode ser desfeita.",
"duplicate_language_or_language_id": "Idioma ou ID de idioma duplicado",
"edit_languages": "Editar idiomas",
@@ -2389,7 +2405,7 @@
"most_popular": "Mais popular",
"pending_change_removed": "Mudança de plano agendada removida.",
"pending_plan_badge": "Agendado",
"pending_plan_change_description": "Seu plano mudará para {{plan}} em {{date}}.",
"pending_plan_change_description": "Seu plano mudará para {plan} em {date}.",
"pending_plan_change_title": "Mudança de plano agendada",
"pending_plan_cta": "Agendado",
"per_month": "por mês",
@@ -2442,7 +2458,7 @@
"trial_payment_method_added_description": "Tudo pronto! Seu plano Pro continuará automaticamente após o término do período de teste.",
"trial_title": "Ganhe o Formbricks Pro gratuitamente!",
"unlimited_responses": "Respostas Ilimitadas",
"unlimited_workspaces": "Projetos ilimitados",
"unlimited_workspaces": "Workspaces Ilimitados",
"upgrade": "Atualizar",
"upgrade_now": "Fazer upgrade agora",
"usage_cycle": "Usage cycle",
@@ -2465,7 +2481,7 @@
"pretty_url": "URL amigável",
"survey_name": "Nome da Pesquisa",
"title": "URLs amigáveis",
"workspace": "Projeto"
"workspace": "Workspace"
},
"enterprise": {
"audit_logs": "Registros de Auditoria",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Já existe um diretório de registros de feedback com este nome.",
"error_directory_name_required": "O nome do diretório é obrigatório.",
"error_directory_workspaces_invalid_org": "Alguns espaços de trabalho especificados não pertencem a esta organização.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Diretórios de Feedback",
"no_access": "Você não tem permissão para gerenciar diretórios de registros de feedback.",
"no_connectors": "Nenhum conector vinculado a este diretório ainda.",
"pause_connectors_confirmation_description": "Ao pausar esses conectores, novos registros deixarão de ser adicionados.",
"pause_connectors_confirmation_title": "Pausar conectores vinculados?",
"select_workspaces_placeholder": "Selecionar espaços de trabalho...",
"show_archived": "Mostrar arquivados",
"title": "Diretórios de Registros de Feedback",
"unarchive": "Desarquivar"
"unarchive": "Desarquivar",
"unarchive_workspace_conflict": "Não é possível desarquivar este diretório porque um ou mais workspaces atribuídos estão arquivados.",
"workspace_access": "Acesso ao workspace"
},
"general": {
"ai_data_analysis_disabled_for_organization": "A análise e o enriquecimento de dados com IA estão desativados para esta organização.",
"ai_data_analysis_enabled": "Enriquecimento e análise de dados (IA)",
"ai_data_analysis_enabled_description": "IA para extrair mais dos seus dados, configurar dashboards, gráficos, relatórios e muito mais. Acessa os dados da sua experiência.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "Gerencie recursos com IA para esta organização.",
"ai_features_not_enabled_for_organization": "Os recursos de IA não estão habilitados para esta organização.",
"ai_instance_not_configured": "A IA é configurada no nível da instância por meio de variáveis de ambiente. Peça ao seu administrador para definir AI_PROVIDER, as credenciais desse provedor e a lista de modelos correspondente antes de habilitar os recursos de IA.",
"ai_settings_updated_successfully": "Configurações de IA atualizadas com sucesso",
"ai_smart_tools_disabled_for_organization": "As funcionalidades inteligentes de IA estão desativadas para esta organização.",
"ai_smart_tools_enabled": "Funcionalidades inteligentes (IA)",
"ai_smart_tools_enabled_description": "IA para ajudar você a conquistar mais em menos tempo. Nunca acessa dados coletados com o Formbricks. Usado apenas para, por exemplo, traduzir pesquisas para outros idiomas.",
"bulk_invite_warning_description": "Por favor, note que no Plano Gratuito, todos os membros da organização são automaticamente atribuídos ao papel de 'Owner', independentemente do papel especificado no arquivo CSV.",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "Você não pode sair dessa organização porque é a sua única. Crie uma nova organização primeiro.",
"copy_invite_link_to_clipboard": "Copiar link do convite para a área de transferência",
"create_new_organization": "Criar nova organização",
"create_new_organization_description": "Crie uma nova organização para gerenciar um conjunto diferente de projetos.",
"create_new_organization_description": "Crie uma nova organização para gerenciar um conjunto diferente de workspaces.",
"customize_email_with_a_higher_plan": "Personalize o email com um plano superior",
"delete_member_confirmation": "Membros excluídos perderão acesso a todos os projetos e pesquisas da sua organização.",
"delete_member_confirmation": "Membros excluídos perderão acesso a todos os workspaces e pesquisas da sua organização.",
"delete_organization": "Excluir Organização",
"delete_organization_description": "Excluir organização com todos os seus projetos, incluindo todas as pesquisas, respostas, pessoas, ações e atributos",
"delete_organization_description": "Excluir organização com todos os seus workspaces, incluindo todas as pesquisas, respostas, pessoas, ações e atributos",
"delete_organization_warning": "Antes de continuar com a exclusão desta organização, esteja ciente das seguintes consequências:",
"delete_organization_warning_1": "Remoção permanente de todos os projetos vinculados a esta organização.",
"delete_organization_warning_1": "Remoção permanente de todos os workspaces vinculados a esta organização.",
"delete_organization_warning_2": "Essa ação não pode ser desfeita. Se foi, foi.",
"delete_organization_warning_3": "Por favor, insira {organizationName} no campo abaixo para confirmar a exclusão definitiva desta organização:",
"eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Cadastre-se aqui.",
"share_invite_link": "Compartilhar Link de Convite",
"share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:",
"test_email_sent_successfully": "E-mail de teste enviado com sucesso"
"test_email_sent_successfully": "E-mail de teste enviado com sucesso",
"unlock_ai_features_description": "Traduções com inteligência artificial, ferramentas inteligentes e análise de dados estão disponíveis em planos superiores. Faça upgrade para turbinar suas pesquisas com IA.",
"unlock_ai_features_with_a_higher_plan": "Libere recursos de IA com um plano superior"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Inscrever-se automaticamente em novas pesquisas",
@@ -2669,7 +2689,7 @@
},
"teams": {
"add_members_description": "Adicione membros à equipe e determine sua função.",
"add_workspaces_description": "Controle quais projetos os membros da equipe podem acessar.",
"add_workspaces_description": "Controle quais workspaces os membros da equipe podem acessar.",
"all_members_added": "Todos os membros adicionados a esta equipe.",
"all_workspaces_added": "Todos os espaços de trabalho adicionados a esta equipe.",
"are_you_sure_you_want_to_delete_this_team": "Tem certeza de que deseja excluir esta equipe? Isso também remove o acesso a todos os espaços de trabalho e pesquisas associados a esta equipe.",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Ajustar mensagem 'Pesquisa Encerrada''",
"adjust_survey_closed_message_description": "Mude a mensagem que os visitantes veem quando a pesquisa está fechada.",
"adjust_the_theme_in_the": "Ajuste o tema no",
"ai_data_analysis_disabled": "A análise de dados por IA está desabilitada para esta organização.",
"ai_features_not_enabled": "Os recursos de IA não estão habilitados para esta organização.",
"ai_instance_not_configured": "A IA não está configurada. Entre em contato com seu administrador.",
"ai_smart_tools_disabled": "As ferramentas inteligentes de IA estão desabilitadas para esta organização.",
"ai_translate": "Traduzir com IA",
"ai_translating": "Traduzindo com IA... Por favor, mantenha este modal aberto.",
"ai_translation_all_fields_populated": "Todos os campos já estão traduzidos",
"ai_translation_complete": "Tradução com IA concluída",
"ai_translation_failed": "Falha na tradução",
"ai_translation_instance_not_configured": "A IA não está configurada nesta instância. Entre em contato com seu administrador.",
"ai_translation_not_available": "A tradução com IA não está disponível no seu plano atual. Faça upgrade para desbloquear este recurso.",
"ai_translation_not_enabled": "As ferramentas inteligentes de IA estão desativadas para esta organização. Ative-as nas configurações da organização.",
"all_are_true": "todas são verdadeiras",
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
"allow_multi_select": "Permitir seleção múltipla",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Veja quais equipes podem acessar este workspace."
},
"unify": {
"add_feedback_record": "Adicionar registro de feedback",
"add_feedback_record_description": "Crie um registro de feedback manualmente.",
"add_feedback_source": "Adicionar fonte de feedback",
"add_source": "Adicionar fonte",
"allowed_values": "Valores permitidos: {values}",
"api_ingestion": "Ingestão de API",
"api_ingestion_manage_api_keys": "Gerenciar chaves de API",
"api_ingestion_settings_description": "Envie registros de feedback usando a API de Gerenciamento.",
"auto_generated": "Gerado automaticamente",
"change_file": "Alterar arquivo",
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
"click_to_upload": "Clique para fazer upload",
"collected_at": "Coletado em",
"configure_import": "Configurar importação",
"configure_mapping": "Configurar mapeamento",
"connection": "Conexão",
"connector_created_successfully": "Conector criado com sucesso",
"connector_deleted_successfully": "Conector excluído com sucesso",
"connector_duplicated_successfully": "Conector duplicado com sucesso",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Importar dados duas vezes criará registros duplicados.",
"csv_inconsistent_columns": "A linha {row} possui colunas inconsistentes. Todas as linhas devem ter os mesmos cabeçalhos.",
"csv_max_records": "Máximo de {max} registros permitidos.",
"custom_source_type": "Tipo de origem personalizado",
"custom_source_type_placeholder": "Insira o tipo de fonte personalizado",
"default_connector_name_csv": "Importação CSV",
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
"deselect_all": "Desmarcar tudo",
"discard_feedback_record_changes_description": "Suas alterações serão perdidas se você fechar esta gaveta.",
"discard_feedback_record_changes_title": "Descartar alterações não salvas?",
"drop_a_field_here": "Solte um campo aqui",
"drop_field_or": "Solte o campo ou",
"edit_csv_mapping": "Editar mapeamento CSV",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Falha ao carregar registros de feedback",
"feedback_date": "Data atual",
"feedback_record_created_successfully": "Registro de feedback criado com sucesso",
"feedback_record_details": "Detalhes do registro de feedback",
"feedback_record_details_description": "Revise e atualize os campos de registro de feedback.",
"feedback_record_directory": "Diretório de Registros de Feedback",
"feedback_record_fields": "Campos do registro de feedback",
"feedback_record_mcp": "Registro de Feedback MCP",
"feedback_record_updated_successfully": "Registro de feedback atualizado com sucesso",
"feedback_record_value_required": "Um valor é obrigatório para o tipo de campo selecionado",
"feedback_records": "Registros de feedback",
"feedback_records_refreshed": "Registros de feedback atualizados",
"feedback_sources": "Fontes de Feedback",
"feedback_sources_directory_access_multiple": "Novos registros dessas fontes serão armazenados em: {directoryNames}",
"feedback_sources_directory_access_single": "Novos registros desta fonte serão armazenados em: {directoryNames}",
"feedback_sources_settings_description": "Conecte e gerencie todas as fontes de feedback para este workspace.",
"field_group_id": "ID do grupo de campos",
"field_group_label": "Etiqueta do grupo de campos",
"field_id": "ID do campo",
"field_label": "Rótulo do campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Pesquisas Formbricks",
"frd_cannot_be_changed": "O diretório de feedback não pode ser alterado após a criação.",
"go_to_feedback_record_directories": "Ir para configurações de diretórios",
"historical_import_complete": "Importação concluída: {successes} bem-sucedidas, {failures} falharam, {skipped} ignoradas (sem dados)",
"import_csv_data": "Importar feedback",
"import_feedback": "Importar feedback",
"import_historical_responses": "Importar respostas históricas",
"import_historical_responses_description": "Importe respostas existentes desta pesquisa agora.",
"import_rows": "Importar {count} linhas",
"import_via_source_name": "Importar via \"{sourceName}\"",
"importing_data": "Importando dados...",
"importing_historical_data": "Importando dados históricos...",
"invalid_enum_values": "Valores inválidos na coluna mapeada para {field}",
"invalid_values_found": "Encontrados: {values} (linhas: {rows}) {extra}",
"load_sample_csv": "Carregar CSV de exemplo",
"n_supported_questions": "{count} perguntas suportadas",
"manage_directories": "Gerenciar diretórios",
"manage_feedback_sources": "Gerenciar fontes de feedback",
"metadata": "Metadados",
"metadata_key": "Chave de metadados",
"metadata_read_only_entries": "Valores de metadados somente leitura (sem string)",
"metadata_value": "Valor dos metadados",
"missing_feedback_source_title": "Faltando alguma fonte de feedback?",
"no_feedback_record_directory_available": "Nenhum diretório de registros de feedback atribuído a este workspace. Crie ou atribua um primeiro.",
"no_feedback_records": "Nenhum registro de feedback ainda. Os registros aparecerão aqui assim que seus conectores começarem a enviar dados.",
"no_source_fields_loaded": "Nenhum campo de origem carregado ainda",
"no_sources_connected": "Nenhuma origem conectada ainda. Adicione uma origem para começar.",
"no_surveys_found": "Nenhuma pesquisa encontrada neste ambiente",
"optional": "Opcional",
"or_drag_and_drop": "ou arraste e solte",
"question_selected": "<strong>{count}</strong> pergunta selecionada. Cada resposta a esta pergunta criará um novo registro de feedback.",
"question_type_not_supported": "Este tipo de pergunta não é suportado",
"questions_selected": "<strong>{count}</strong> perguntas selecionadas. Cada resposta a estas perguntas criará um novo registro de feedback.",
"records_will_go_to": "Os registros serão enviados para",
"refresh_feedback_records": "Atualizar registros de feedback",
"refreshing_feedback_records": "Atualizando registros de feedback...",
"request_feedback_source": "Solicitar integração de fonte",
"required": "Obrigatório",
"save_changes": "Salvar alterações",
"select_a_survey_to_see_questions": "Selecione uma pesquisa para ver suas perguntas",
"select_a_value": "Selecione um valor...",
"select_all": "Selecionar tudo",
"select_feedback_record_directory": "Selecione um diretório",
"select_feedback_record_source_type": "Selecione o tipo de fonte",
"select_questions": "Selecionar perguntas",
"select_source_type_description": "Selecione o tipo de fonte de feedback que você deseja conectar.",
"select_source_type_prompt": "Selecione o tipo de fonte de feedback que você deseja conectar:",
"select_survey": "Selecionar pesquisa",
"select_survey_and_questions": "Selecionar pesquisa e perguntas",
"select_survey_questions_description": "Escolha quais perguntas da pesquisa devem criar FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Mostrando 3 de {count} linhas",
"source": "fonte",
"source_connect_csv_description": "Importar feedback de arquivos CSV",
"source_connect_feedback_record_mcp_description": "Envie registros de feedback através da integração MCP.",
"source_connect_formbricks_description": "Conectar feedback das suas pesquisas Formbricks",
"source_fields": "Campos de origem",
"source_id": "ID da fonte",
"source_name": "Nome da origem",
"source_type": "Tipo de fonte",
"source_type_cannot_be_changed": "O tipo de origem não pode ser alterado",
"sources": "Origens",
"status_active": "Em andamento",
"status_completed": "Concluído",
"status_draft": "Rascunho",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Erro",
"status_paused": "Pausado",
"status_live_sync": "Sincronização ao vivo",
"status_ready": "Pronto",
"submission_id": "ID de envio",
"survey_has_no_questions": "Esta pesquisa não possui perguntas",
"survey_import_line": "{surveyName}: {responseCount} respostas × {questionCount} perguntas = {total} registros de feedback",
"total_feedback_records": "Total: {checked} de {total} registros de feedback selecionados em {surveyCount} pesquisas",
"topics_and_subtopics": "Tópicos e subtópicos",
"unify_feedback": "Unificar feedback",
"update_mapping_description": "Atualize a configuração de mapeamento para esta fonte.",
"updated_at": "Atualizado em",
"upload_csv_data_description": "Faça upload de um arquivo CSV para importar dados de feedback.",
"upload_csv_file": "Fazer upload de arquivo CSV",
"user_identifier": "Usuário",
"value": "Valor"
"value": "Valor",
"value_boolean": "Valor (Booleano)",
"value_date": "Valor (Data)",
"value_number": "Valor (Número)",
"value_text": "Valor (Texto)"
},
"xm-templates": {
"ces": "CES",
+132 -64
View File
@@ -125,14 +125,15 @@
"activity": "Atividade",
"add": "Adicionar",
"add_action": "Adicionar ação",
"add_chart": "Adicionar gráfico",
"add_charts": "Adicionar gráficos",
"add_existing_chart_description": "Pesquisa e seleciona gráficos para adicionar a este painel.",
"add_filter": "Adicionar filtro",
"add_logo": "Adicionar logótipo",
"add_member": "Adicionar membro",
"add_new_workspace": "Adicionar novo projeto",
"add_new_workspace": "Adicionar novo espaço de trabalho",
"add_to_team": "Adicionar à equipa",
"add_workspace": "Adicionar projeto",
"add_workspace": "Adicionar espaço de trabalho",
"all": "Todos",
"all_questions": "Todas as perguntas",
"allow": "Permitir",
@@ -147,6 +148,7 @@
"apply_filters": "Aplicar filtros",
"archived": "Arquivado",
"are_you_sure": "Tem a certeza?",
"ask": "Ask",
"attributes": "Atributos",
"back": "Voltar",
"billing": "Faturação",
@@ -159,10 +161,10 @@
"change_workspace": "Alterar espaço de trabalho",
"chart": "Gráfico",
"charts": "Gráficos",
"choice_n": "Escolha {{n}}",
"choice_n": "Escolha {n}",
"choices": "Escolhas",
"choose_organization": "Escolher organização",
"choose_workspace": "Escolher projeto",
"choose_workspace": "Escolher espaço de trabalho",
"clear_all": "Limpar tudo",
"clear_filters": "Limpar filtros",
"clear_selection": "Limpar seleção",
@@ -172,7 +174,7 @@
"close": "Fechar",
"code": "Código",
"collapse_rows": "Recolher linhas",
"column_n": "Coluna {{n}}",
"column_n": "Coluna {n}",
"completed": "Concluído",
"configuration": "Configurar",
"confirm": "Confirmar",
@@ -197,7 +199,7 @@
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar inquérito",
"create_workspace": "Criar projeto",
"create_workspace": "Criar espaço de trabalho",
"created": "Criado",
"created_at": "Criado em",
"created_by": "Criado por",
@@ -212,6 +214,7 @@
"delete_what": "Eliminar {deleteWhat}",
"description": "Descrição",
"disable": "Desativar",
"disabled": "Desativado",
"disallow": "Não permitir",
"discard": "Descartar",
"dismissed": "Dispensado",
@@ -241,9 +244,9 @@
"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",
"failed_to_load_workspaces": "Falha ao carregar espaços de trabalho",
"failed_to_parse_csv": "Falha ao analisar o CSV",
"field_placeholder": "Espaço reservado de {{field}}",
"field_placeholder": "Espaço reservado de {field}",
"filter": "Filtro",
"finish": "Concluir",
"first_name": "Primeiro nome",
@@ -331,6 +334,7 @@
"not_authenticated": "Não está autenticado para realizar esta ação.",
"not_authorized": "Não autorizado",
"not_connected": "Não Conectado",
"not_set": "Não definido",
"note": "Nota",
"notifications": "Notificações",
"number": "Número",
@@ -390,13 +394,14 @@
"report_survey": "Relatório de Inquérito",
"request_trial_license": "Solicitar licença de teste",
"reset_to_default": "Repor para o padrão",
"resize": "Redimensionar",
"response": "Resposta",
"response_id": "ID de resposta",
"responses": "Respostas",
"restart": "Reiniciar",
"retry": "Tentar novamente",
"role": "Função",
"row_n": "Linha {{n}}",
"row_n": "Linha {n}",
"saas": "SaaS",
"sales": "Vendas",
"save": "Guardar",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar",
"something_went_wrong": "Algo correu mal",
"something_went_wrong_please_try_again": "Algo correu mal. Por favor, tente novamente.",
"soon": "Em breve",
"sort_by": "Ordem",
"start_free_trial": "Iniciar teste gratuito",
"status": "Estado",
@@ -474,7 +480,7 @@
"type": "Tipo",
"unify": "Unificar",
"unknown_survey": "Inquérito desconhecido",
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueia mais espaços de trabalho com um plano superior.",
"update": "Atualizar",
"updated": "Atualizado",
"updated_at": "Atualizado em",
@@ -501,13 +507,13 @@
"weeks": "semanas",
"welcome_card": "Cartão de boas-vindas",
"workspace": "Espaço de trabalho",
"workspace_configuration": "Configuração do projeto",
"workspace_created_successfully": "Projeto criado com sucesso",
"workspace_creation_description": "Organize inquéritos em projetos para melhor controlo de acesso.",
"workspace_id": "ID do projeto",
"workspace_name": "Nome do projeto",
"workspace_configuration": "Configuração do Espaço de Trabalho",
"workspace_created_successfully": "Espaço de trabalho criado com sucesso",
"workspace_creation_description": "Organiza inquéritos em espaços de trabalho para um melhor controlo de acesso.",
"workspace_id": "ID do Espaço de Trabalho",
"workspace_name": "Nome do Espaço de Trabalho",
"workspace_name_placeholder": "ex. Formbricks",
"workspaces": "Projetos",
"workspaces": "Espaços de Trabalho",
"years": "anos",
"yes": "Sim",
"you_are_downgraded_to_the_community_edition": "Foi rebaixado para a Edição Comunitária.",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Gráfico de barras",
"chart_type_big_number": "Número grande",
"chart_type_line": "Gráfico de linhas",
"chart_type_not_supported": "Tipo de gráfico \"{{chartType}}\" ainda não suportado",
"chart_type_not_supported": "O tipo de gráfico \"{chartType}\" ainda não é suportado",
"chart_type_pie": "Gráfico circular",
"chart_updated_successfully": "Gráfico atualizado com sucesso!",
"configure_description": "Modifique o tipo de gráfico e outras definições para esta visualização.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Falha ao executar consulta",
"failed_to_load_chart": "Falha ao carregar gráfico",
"failed_to_load_chart_data": "Falha ao carregar dados do gráfico",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Falha ao guardar gráfico",
"field": "Campo",
"field_label_average_score": "Pontuação média",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Nenhum dado válido para exibir",
"not_contains": "não contém",
"not_equals": "não é igual a",
"open_chart": "Abrir gráfico {{name}}",
"open_chart": "Abrir gráfico {name}",
"open_options": "Abrir opções do gráfico",
"or_filter_logic": "OU",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Por favor, seleciona um painel",
"predefined_measures": "Medidas predefinidas",
"preset": "Predefinição",
"preview_chart": "Visualizar gráfico",
"query_executed_successfully": "Consulta executada com sucesso",
"reset_to_ai_suggestion": "Repor sugestão da IA",
"save_and_add_to_dashboard": "Salvar e adicionar ao painel",
"save_chart": "Guardar gráfico",
"save_chart_dialog_title": "Guardar gráfico",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Selecionar campo",
"select_measures": "Selecionar medidas...",
"select_preset": "Selecionar predefinição",
"showing_first_n_of": "A mostrar as primeiras {{n}} de {{count}} linhas",
"showing_first_n_of": "A mostrar as primeiras {n} de {count} linhas",
"start_date": "Data de início",
"time_dimension": "Dimensão temporal",
"time_dimension_title": "Adicionar agrupamento temporal",
"time_dimension_toggle_description": "Monitoriza tendências ao longo do tempo."
"time_dimension_toggle_description": "Monitoriza tendências ao longo do tempo.",
"update_chart": "Atualizar gráfico"
},
"dashboards": {
"add_count_charts": "Adicionar {count} gráfico(s)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Criar painel",
"create_dashboard_description": "Introduza um nome para o seu novo painel.",
"create_failed": "Falha ao criar painel",
"create_new_chart": "Criar novo gráfico",
"create_success": "Painel criado com sucesso!",
"dashboard": "Painel",
"dashboard_delete_confirmation": "Tens a certeza de que queres eliminar este painel? Esta ação não pode ser revertida.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Falha ao duplicar painel",
"duplicate_success": "Painel duplicado com sucesso!",
"failed_to_load_chart_data": "Falha ao carregar os dados do gráfico",
"no_charts_available_description": "Não há gráficos que possam ser adicionados a este painel. Ou ainda não existem gráficos, ou todos os gráficos existentes já foram adicionados. Vai à página de Gráficos para criar novos gráficos.",
"no_charts_to_add_message": "Não há gráficos para adicionar a este painel.",
"no_dashboards_found": "Nenhum painel encontrado.",
"no_data_message": "Sem Dados. Atualmente não há informação para apresentar. Adiciona gráficos para construir o teu painel.",
"please_enter_name": "Por favor, introduza um nome para o painel"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Você não tem registros de feedback para relatar. Configure fontes de feedback para alimentar dados no sistema.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Configurar fontes de feedback"
},
"api_keys": {
"add_api_key": "Adicionar chave API",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Ligue a sua aplicação ou website ao Formbricks.",
"cache_update_delay_description": "Quando faz atualizações a inquéritos, contactos, ações ou outros dados, pode demorar até 1 minuto para que essas alterações apareçam na sua aplicação local que executa o SDK do Formbricks.",
"cache_update_delay_title": "As alterações serão refletidas após ~1 minuto devido ao caching",
"environment_id_legacy": "ID de Ambiente (legado)",
"environment_id_legacy_alert": "A tua configuração SDK existente pode ainda utilizar um ID de Ambiente legado.",
"environment_id_legacy_alert_link": "Descobre porquê e como migrar.",
"formbricks_sdk_connected": "O SDK do Formbricks está conectado",
"formbricks_sdk_not_connected": "O SDK do Formbricks ainda não está conectado.",
"formbricks_sdk_not_connected_description": "Adicione o SDK do Formbricks ao seu website ou aplicação para o conectar ao Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Logotipo do Formbricks",
"general": {
"cannot_delete_only_workspace": "Este é o seu único projeto, não pode ser eliminado. Crie primeiro um novo projeto.",
"cannot_delete_only_workspace": "Este é o teu único espaço de trabalho e não pode ser eliminado. Cria primeiro um novo espaço de trabalho.",
"custom_scripts": "Scripts personalizados",
"custom_scripts_card_description": "Adicionar scripts de rastreamento e pixels a todos os inquéritos de link nesta área de trabalho.",
"custom_scripts_description": "Os scripts serão injetados no <head> de todas as páginas de inquéritos de link.",
@@ -1989,21 +2005,21 @@
"custom_scripts_placeholder": "<!-- Cole os seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personalizados atualizados com sucesso",
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes fidedignas.",
"delete_workspace": "Eliminar projeto",
"delete_workspace": "Eliminar Espaço de Trabalho",
"delete_workspace_confirmation": "Tens a certeza de que queres eliminar {workspaceName}? Esta ação não pode ser revertida.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {workspaceName} incluindo todos os inquéritos, respostas, pessoas, ações e atributos.",
"delete_workspace_settings_description": "Eliminar projeto com todos os inquéritos, respostas, pessoas, ações e atributos. Isto não pode ser desfeito.",
"error_saving_workspace_information": "Erro ao guardar informações do projeto",
"only_owners_or_managers_can_delete_workspaces": "Apenas proprietários ou gestores podem eliminar projetos",
"delete_workspace_settings_description": "Elimina o espaço de trabalho com todos os inquéritos, respostas, pessoas, ações e atributos. Esta ação não pode ser revertida.",
"error_saving_workspace_information": "Erro ao guardar informações do espaço de trabalho",
"only_owners_or_managers_can_delete_workspaces": "Apenas proprietários ou gestores podem eliminar espaços de trabalho",
"recontact_waiting_time": "Período de espera (entre inquéritos)",
"recontact_waiting_time_settings_description": "Controle com que frequência os utilizadores podem ser inquiridos em todos os inquéritos de website e aplicação neste espaço de trabalho.",
"this_action_cannot_be_undone": "Esta ação não pode ser desfeita.",
"wait_x_days_before_showing_next_survey": "Aguardar X dias antes de mostrar o próximo inquérito:",
"waiting_period_updated_successfully": "Período de espera atualizado com sucesso",
"whats_your_workspace_called": "Como se chama o seu projeto?",
"workspace_deleted_successfully": "Projeto eliminado com sucesso",
"workspace_name_settings_description": "Altere o nome do seu projeto.",
"workspace_name_updated_successfully": "Nome do projeto atualizado com sucesso"
"whats_your_workspace_called": "Como se chama o teu espaço de trabalho?",
"workspace_deleted_successfully": "Espaço de trabalho eliminado com sucesso",
"workspace_name_settings_description": "Altera o nome do teu espaço de trabalho.",
"workspace_name_updated_successfully": "Nome do espaço de trabalho atualizado com sucesso"
},
"integrations": {
"activepieces_integration_description": "Conecte instantaneamente o Formbricks com apps populares para automatizar tarefas sem codificação.",
@@ -2161,7 +2177,7 @@
"alias_tooltip": "O alias é um nome alternativo para identificar o idioma em inquéritos de link e no SDK (opcional)",
"cannot_remove_language_warning": "Não pode remover este idioma porque ainda está a ser utilizado nestes inquéritos:",
"conflict_between_identifier_and_alias": "Existe um conflito entre o identificador de um idioma adicionado e um dos seus aliases. Aliases e identificadores não podem ser idênticos.",
"conflict_between_selected_alias_and_another_language": "Existe um conflito entre o alias selecionado e outro idioma que tem este identificador. Por favor, adicione o idioma com este identificador ao seu projeto para evitar inconsistências.",
"conflict_between_selected_alias_and_another_language": "Existe um conflito entre o alias selecionado e outro idioma que possui este identificador. Por favor, adiciona o idioma com este identificador ao teu espaço de trabalho para evitar inconsistências.",
"delete_language_confirmation": "Tem a certeza de que pretende eliminar este idioma? Esta ação não pode ser desfeita.",
"duplicate_language_or_language_id": "Idioma ou ID de idioma duplicado",
"edit_languages": "Editar idiomas",
@@ -2389,7 +2405,7 @@
"most_popular": "Mais popular",
"pending_change_removed": "Alteração de plano agendada removida.",
"pending_plan_badge": "Agendado",
"pending_plan_change_description": "O teu plano mudará para {{plan}} em {{date}}.",
"pending_plan_change_description": "O teu plano mudará para {plan} em {date}.",
"pending_plan_change_title": "Alteração de plano agendada",
"pending_plan_cta": "Agendado",
"per_month": "por mês",
@@ -2442,7 +2458,7 @@
"trial_payment_method_added_description": "Está tudo pronto! O teu plano Pro continuará automaticamente após o fim do período experimental.",
"trial_title": "Obtém o Formbricks Pro gratuitamente!",
"unlimited_responses": "Respostas Ilimitadas",
"unlimited_workspaces": "Projetos ilimitados",
"unlimited_workspaces": "Espaços de Trabalho Ilimitados",
"upgrade": "Atualizar",
"upgrade_now": "Fazer upgrade agora",
"usage_cycle": "Usage cycle",
@@ -2465,7 +2481,7 @@
"pretty_url": "URL amigável",
"survey_name": "Nome do inquérito",
"title": "URLs amigáveis",
"workspace": "Projeto"
"workspace": "Espaço de Trabalho"
},
"enterprise": {
"audit_logs": "Registos de Auditoria",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Já existe um diretório de registos de feedback com este nome.",
"error_directory_name_required": "O nome do diretório é obrigatório.",
"error_directory_workspaces_invalid_org": "Algumas áreas de trabalho especificadas não pertencem a esta organização.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Diretórios de Feedback",
"no_access": "Não tens permissão para gerir diretórios de registos de feedback.",
"no_connectors": "Ainda não há conectores associados a este diretório.",
"pause_connectors_confirmation_description": "Ao pausar estes conectores, deixam de ser adicionados novos registos.",
"pause_connectors_confirmation_title": "Pausar conectores associados?",
"select_workspaces_placeholder": "Selecionar espaços de trabalho...",
"show_archived": "Mostrar arquivados",
"title": "Diretórios de Registos de Feedback",
"unarchive": "Desarquivar"
"unarchive": "Desarquivar",
"unarchive_workspace_conflict": "Não é possível desarquivar este diretório porque um ou mais workspaces atribuídos estão arquivados.",
"workspace_access": "Acesso ao workspace"
},
"general": {
"ai_data_analysis_disabled_for_organization": "A análise e o enriquecimento de dados com IA estão desativados para esta organização.",
"ai_data_analysis_enabled": "Enriquecimento e análise de dados (IA)",
"ai_data_analysis_enabled_description": "IA para tirar mais partido dos teus dados, configurar dashboards, gráficos, relatórios e muito mais. Acede aos dados da tua experiência.",
"ai_enabled": "IA da Formbricks",
"ai_enabled_description": "Gerir funcionalidades com IA para esta organização.",
"ai_features_not_enabled_for_organization": "As funcionalidades de IA não estão ativadas para esta organização.",
"ai_instance_not_configured": "A IA é configurada ao nível da instância através de variáveis de ambiente. Peça ao seu administrador para definir AI_PROVIDER, as credenciais desse fornecedor e a lista de modelos correspondente antes de ativar as funcionalidades de IA.",
"ai_settings_updated_successfully": "Definições de IA atualizadas com sucesso",
"ai_smart_tools_disabled_for_organization": "As funcionalidades inteligentes de IA estão desativadas para esta organização.",
"ai_smart_tools_enabled": "Funcionalidade inteligente (IA)",
"ai_smart_tools_enabled_description": "IA para te ajudar a alcançar mais em menos tempo. Nunca acede aos dados recolhidos com o Formbricks. Apenas usado para, por exemplo, traduzir inquéritos para outros idiomas.",
"bulk_invite_warning_description": "No plano gratuito, todos os membros da organização são sempre atribuídos ao papel de \"Proprietário\".",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "Não pode sair desta organização, pois é a sua única organização. Crie uma nova organização primeiro.",
"copy_invite_link_to_clipboard": "Copiar link de convite para a área de transferência",
"create_new_organization": "Criar nova organização",
"create_new_organization_description": "Crie uma nova organização para gerir um conjunto diferente de projetos.",
"create_new_organization_description": "Cria uma nova organização para gerir um conjunto diferente de espaços de trabalho.",
"customize_email_with_a_higher_plan": "Personalize o e-mail com um plano superior",
"delete_member_confirmation": "Os membros eliminados perderão o acesso a todos os projetos e inquéritos da sua organização.",
"delete_member_confirmation": "Os membros eliminados perderão o acesso a todos os espaços de trabalho e inquéritos da tua organização.",
"delete_organization": "Eliminar Organização",
"delete_organization_description": "Eliminar organização com todos os seus projetos, incluindo todos os inquéritos, respostas, pessoas, ações e atributos",
"delete_organization_description": "Eliminar organização com todos os seus espaços de trabalho, incluindo todos os inquéritos, respostas, pessoas, ações e atributos",
"delete_organization_warning": "Antes de prosseguir com a eliminação desta organização, esteja ciente das seguintes consequências:",
"delete_organization_warning_1": "Remoção permanente de todos os projetos associados a esta organização.",
"delete_organization_warning_1": "Remoção permanente de todos os espaços de trabalho associados a esta organização.",
"delete_organization_warning_2": "Esta ação não pode ser desfeita. Se for eliminada, está eliminada.",
"delete_organization_warning_3": "Por favor, insira {organizationName} no campo seguinte para confirmar a eliminação definitiva desta organização:",
"eliminate_branding_with_whitelabel": "Elimine a marca Formbricks e ative opções adicionais de personalização de marca branca.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Inscreva-se aqui.",
"share_invite_link": "Partilhar Link de Convite",
"share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:",
"test_email_sent_successfully": "Email de teste enviado com sucesso"
"test_email_sent_successfully": "Email de teste enviado com sucesso",
"unlock_ai_features_description": "Traduções com IA, ferramentas inteligentes e análise de dados estão disponíveis em planos superiores. Faz upgrade para potenciar os teus inquéritos com IA.",
"unlock_ai_features_with_a_higher_plan": "Desbloqueia funcionalidades de IA com um plano superior"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Subscrever automaticamente a novos inquéritos",
@@ -2669,7 +2689,7 @@
},
"teams": {
"add_members_description": "Adicionar membros à equipa e determinar o seu papel.",
"add_workspaces_description": "Controle a quais projetos os membros da equipa podem aceder.",
"add_workspaces_description": "Controla a quais espaços de trabalho os membros da equipa podem aceder.",
"all_members_added": "Todos os membros adicionados a esta equipa.",
"all_workspaces_added": "Todos os espaços de trabalho adicionados a esta equipa.",
"are_you_sure_you_want_to_delete_this_team": "Tem a certeza de que pretende eliminar esta equipa? Isto também remove o acesso a todos os espaços de trabalho e inquéritos associados a esta equipa.",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Ajustar mensagem de 'Inquérito Fechado'",
"adjust_survey_closed_message_description": "Alterar a mensagem que os visitantes veem quando o inquérito está fechado.",
"adjust_the_theme_in_the": "Ajustar o tema no",
"ai_data_analysis_disabled": "A análise de dados por IA está desativada para esta organização.",
"ai_features_not_enabled": "As funcionalidades de IA não estão ativadas para esta organização.",
"ai_instance_not_configured": "A IA não está configurada. Contacta o teu administrador.",
"ai_smart_tools_disabled": "As ferramentas inteligentes de IA estão desativadas para esta organização.",
"ai_translate": "Traduzir com IA",
"ai_translating": "A traduzir com IA... Mantém esta janela aberta, por favor.",
"ai_translation_all_fields_populated": "Todos os campos já estão traduzidos",
"ai_translation_complete": "Tradução com IA concluída",
"ai_translation_failed": "A tradução falhou",
"ai_translation_instance_not_configured": "A IA não está configurada nesta instância. Contacta o teu administrador.",
"ai_translation_not_available": "A tradução com IA não está disponível no teu plano atual. Faz upgrade para desbloquear esta funcionalidade.",
"ai_translation_not_enabled": "As ferramentas inteligentes de IA estão desativadas para esta organização. Ativa-as nas definições da organização.",
"all_are_true": "todas são verdadeiras",
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
"allow_multi_select": "Permitir seleção múltipla",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Veja quais as equipas que podem aceder a este espaço de trabalho."
},
"unify": {
"add_feedback_record": "Adicionar registro de feedback",
"add_feedback_record_description": "Crie um registro de feedback manualmente.",
"add_feedback_source": "Adicionar fonte de feedback",
"add_source": "Adicionar fonte",
"allowed_values": "Valores permitidos: {values}",
"api_ingestion": "Ingestão de API",
"api_ingestion_manage_api_keys": "Gerir chaves de API",
"api_ingestion_settings_description": "Envia registos de feedback através da API de gestão.",
"auto_generated": "Gerado automaticamente",
"change_file": "Alterar ficheiro",
"click_load_sample_csv": "Clique em 'Carregar CSV de exemplo' para ver as colunas",
"click_to_upload": "Clique para carregar",
"collected_at": "Recolhido em",
"configure_import": "Configurar importação",
"configure_mapping": "Configurar mapeamento",
"connection": "Conexão",
"connector_created_successfully": "Conector criado com sucesso",
"connector_deleted_successfully": "Conector eliminado com sucesso",
"connector_duplicated_successfully": "Conector duplicado com sucesso",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Importar dados duas vezes irá criar registos duplicados.",
"csv_inconsistent_columns": "A linha {row} tem colunas inconsistentes. Todas as linhas devem ter os mesmos cabeçalhos.",
"csv_max_records": "Máximo de {max} registos permitidos.",
"custom_source_type": "Tipo de origem personalizado",
"custom_source_type_placeholder": "Insira o tipo de fonte personalizado",
"default_connector_name_csv": "Importação CSV",
"default_connector_name_formbricks": "Conexão de pesquisa Formbricks",
"deselect_all": "Desselecionar tudo",
"discard_feedback_record_changes_description": "Suas alterações serão perdidas se você fechar esta gaveta.",
"discard_feedback_record_changes_title": "Descartar alterações não salvas?",
"drop_a_field_here": "Solte um campo aqui",
"drop_field_or": "Solte o campo ou",
"edit_csv_mapping": "Editar mapeamento CSV",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Falha ao carregar registos de feedback",
"feedback_date": "Data atual",
"feedback_record_created_successfully": "Registro de feedback criado com sucesso",
"feedback_record_details": "Detalhes do registro de feedback",
"feedback_record_details_description": "Revise e atualize os campos de registro de feedback.",
"feedback_record_directory": "Diretório de Registos de Feedback",
"feedback_record_fields": "Campos de registo de feedback",
"feedback_record_mcp": "MCP de Registo de Feedback",
"feedback_record_updated_successfully": "Registro de feedback atualizado com sucesso",
"feedback_record_value_required": "Um valor é obrigatório para o tipo de campo selecionado",
"feedback_records": "Registos de feedback",
"feedback_records_refreshed": "Registos de feedback atualizados",
"feedback_sources": "Fontes de Feedback",
"feedback_sources_directory_access_multiple": "Novos registos destas fontes serão armazenados em: {directoryNames}",
"feedback_sources_directory_access_single": "Novos registos desta fonte serão armazenados em: {directoryNames}",
"feedback_sources_settings_description": "Liga e gere todas as fontes de feedback para este espaço de trabalho.",
"field_group_id": "ID do grupo de campos",
"field_group_label": "Etiqueta do grupo de campos",
"field_id": "ID do campo",
"field_label": "Etiqueta do campo",
"field_type": "Tipo de campo",
"formbricks_surveys": "Pesquisas Formbricks",
"frd_cannot_be_changed": "O diretório de feedback não pode ser alterado após a criação.",
"go_to_feedback_record_directories": "Ir para definições de diretórios",
"historical_import_complete": "Importação concluída: {successes} com sucesso, {failures} falharam, {skipped} ignorados (sem dados)",
"import_csv_data": "Importar feedback",
"import_feedback": "Importar feedback",
"import_historical_responses": "Importar respostas históricas",
"import_historical_responses_description": "Importa agora as respostas existentes deste inquérito.",
"import_rows": "Importar {count} linhas",
"import_via_source_name": "Importar via \"{sourceName}\"",
"importing_data": "A importar dados...",
"importing_historical_data": "A importar dados históricos...",
"invalid_enum_values": "Valores inválidos na coluna mapeada para {field}",
"invalid_values_found": "Encontrados: {values} (linhas: {rows}) {extra}",
"load_sample_csv": "Carregar CSV de exemplo",
"n_supported_questions": "{count} perguntas suportadas",
"manage_directories": "Gerir diretórios",
"manage_feedback_sources": "Gerenciar fontes de feedback",
"metadata": "Metadados",
"metadata_key": "Chave de metadados",
"metadata_read_only_entries": "Valores de metadados somente leitura (sem string)",
"metadata_value": "Valor dos metadados",
"missing_feedback_source_title": "Falta alguma fonte de feedback?",
"no_feedback_record_directory_available": "Não há nenhum diretório de registos de feedback atribuído a este espaço de trabalho. Cria ou atribui um primeiro.",
"no_feedback_records": "Ainda não há registos de feedback. Os registos aparecerão aqui assim que os teus conectores começarem a enviar dados.",
"no_source_fields_loaded": "Ainda não foram carregados campos de origem",
"no_sources_connected": "Ainda não há origens ligadas. Adicione uma origem para começar.",
"no_surveys_found": "Nenhum inquérito encontrado neste ambiente",
"optional": "Opcional",
"or_drag_and_drop": "ou arraste e largue",
"question_selected": "<strong>{count}</strong> pergunta selecionada. Cada resposta a esta pergunta criará um novo registo de feedback.",
"question_type_not_supported": "Este tipo de pergunta não é suportado",
"questions_selected": "<strong>{count}</strong> perguntas selecionadas. Cada resposta a estas perguntas criará um novo registo de feedback.",
"records_will_go_to": "Os registos irão para",
"refresh_feedback_records": "Atualizar registos de feedback",
"refreshing_feedback_records": "A atualizar registos de feedback...",
"request_feedback_source": "Solicitar integração de fonte",
"required": "Obrigatório",
"save_changes": "Guardar alterações",
"select_a_survey_to_see_questions": "Selecione um inquérito para ver as suas perguntas",
"select_a_value": "Selecione um valor...",
"select_all": "Selecionar tudo",
"select_feedback_record_directory": "Selecionar um diretório",
"select_feedback_record_source_type": "Selecione o tipo de fonte",
"select_questions": "Selecionar perguntas",
"select_source_type_description": "Selecione o tipo de fonte de feedback que pretende conectar.",
"select_source_type_prompt": "Selecione o tipo de fonte de feedback que pretende conectar:",
"select_survey": "Selecionar inquérito",
"select_survey_and_questions": "Selecionar inquérito e perguntas",
"select_survey_questions_description": "Escolha quais perguntas do inquérito devem criar FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "A mostrar 3 de {count} linhas",
"source": "fonte",
"source_connect_csv_description": "Importar feedback de ficheiros CSV",
"source_connect_feedback_record_mcp_description": "Envia registos de feedback através da integração MCP.",
"source_connect_formbricks_description": "Conectar feedback dos seus inquéritos Formbricks",
"source_fields": "Campos da fonte",
"source_id": "ID da fonte",
"source_name": "Nome da fonte",
"source_type": "Tipo de fonte",
"source_type_cannot_be_changed": "O tipo de fonte não pode ser alterado",
"sources": "Fontes",
"status_active": "Em progresso",
"status_completed": "Concluído",
"status_draft": "Rascunho",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Erro",
"status_paused": "Em pausa",
"status_live_sync": "Sincronização em direto",
"status_ready": "Pronto",
"submission_id": "ID de envio",
"survey_has_no_questions": "Este inquérito não tem perguntas",
"survey_import_line": "{surveyName}: {responseCount} respostas × {questionCount} perguntas = {total} registos de feedback",
"total_feedback_records": "Total: {checked} de {total} registos de feedback selecionados em {surveyCount} inquéritos",
"topics_and_subtopics": "Tópicos e subtópicos",
"unify_feedback": "Unificar feedback",
"update_mapping_description": "Atualiza a configuração de mapeamento para esta origem.",
"updated_at": "Atualizado em",
"upload_csv_data_description": "Carrega um ficheiro CSV para importar dados de feedback.",
"upload_csv_file": "Carregar ficheiro CSV",
"user_identifier": "Utilizador",
"value": "Valor"
"value": "Valor",
"value_boolean": "Valor (Booleano)",
"value_date": "Valor (Data)",
"value_number": "Valor (Número)",
"value_text": "Valor (Texto)"
},
"xm-templates": {
"ces": "CES",
+113 -45
View File
@@ -125,6 +125,7 @@
"activity": "Activitate",
"add": "Adaugă",
"add_action": "Adăugați acțiune",
"add_chart": "Adăugați diagramă",
"add_charts": "Adaugă grafice",
"add_existing_chart_description": "Caută și selectează grafice pentru a le adăuga la acest panou de control.",
"add_filter": "Adăugați filtru",
@@ -147,6 +148,7 @@
"apply_filters": "Aplică filtre",
"archived": "Arhivat",
"are_you_sure": "Ești sigur?",
"ask": "Ask",
"attributes": "Atribute",
"back": "Înapoi",
"billing": "Facturare",
@@ -159,7 +161,7 @@
"change_workspace": "Schimbă spațiul de lucru",
"chart": "Grafic",
"charts": "Grafice",
"choice_n": "Opțiunea {{n}}",
"choice_n": "Opțiunea {n}",
"choices": "Alegeri",
"choose_organization": "Alege organizația",
"choose_workspace": "Alege workspace",
@@ -172,7 +174,7 @@
"close": "Închide",
"code": "Cod",
"collapse_rows": "Restrânge rânduri",
"column_n": "Coloana {{n}}",
"column_n": "Coloana {n}",
"completed": "Completat",
"configuration": "Configurează",
"confirm": "Confirmare",
@@ -212,6 +214,7 @@
"delete_what": "Șterge {deleteWhat}",
"description": "Descriere",
"disable": "Dezactivează",
"disabled": "Dezactivat",
"disallow": "Nu permite",
"discard": "Renunță",
"dismissed": "Respins",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Nu s-a reușit încărcarea organizațiilor",
"failed_to_load_workspaces": "Nu s-au putut încărca workspaces",
"failed_to_parse_csv": "Nu s-a putut procesa fișierul CSV",
"field_placeholder": "Substituent {{field}}",
"field_placeholder": "Substituent {field}",
"filter": "Filtru",
"finish": "Finalizează",
"first_name": "Prenume",
@@ -331,6 +334,7 @@
"not_authenticated": "Nu sunteți autentificat pentru a efectua această acțiune.",
"not_authorized": "Neautorizat",
"not_connected": "Neconectat",
"not_set": "Nu setat",
"note": "Notă",
"notifications": "Notificări",
"number": "Număr",
@@ -390,13 +394,14 @@
"report_survey": "Raportează chestionarul",
"request_trial_license": "Solicitați o licență de încercare",
"reset_to_default": "Revino la implicit",
"resize": "Redimensionați",
"response": "Răspuns",
"response_id": "ID răspuns",
"responses": "Răspunsuri",
"restart": "Repornește",
"retry": "Reîncearcă",
"role": "Rolul",
"row_n": "Rândul {{n}}",
"row_n": "Rândul {n}",
"saas": "SaaS",
"sales": "Vânzări",
"save": "Salvează",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Unele fișiere nu au reușit să se încarce",
"something_went_wrong": "Ceva nu a mers bine",
"something_went_wrong_please_try_again": "Ceva nu a mers bine. Vă rugăm să încercați din nou.",
"soon": "În curând",
"sort_by": "Sortare după",
"start_free_trial": "Începe perioada de probă gratuită",
"status": "Stare",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Grafic de tip bară",
"chart_type_big_number": "Număr mare",
"chart_type_line": "Grafic de tip linie",
"chart_type_not_supported": "Tipul de grafic \"{{chartType}}\" nu este încă suportat",
"chart_type_not_supported": "Tipul de diagramă \"{chartType}\" nu este încă acceptat",
"chart_type_pie": "Grafic de tip plăcintă",
"chart_updated_successfully": "Graficul a fost actualizat cu succes!",
"configure_description": "Modifică tipul graficului și alte setări pentru această vizualizare.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Nu s-a putut executa interogarea",
"failed_to_load_chart": "Nu s-a putut încărca graficul",
"failed_to_load_chart_data": "Nu s-au putut încărca datele graficului",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Nu s-a putut salva graficul",
"field": "Câmp",
"field_label_average_score": "Scor mediu",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Nu există date valide de afișat",
"not_contains": "nu conține",
"not_equals": "nu este egal",
"open_chart": "Deschide graficul {{name}}",
"open_chart": "Deschide diagrama {name}",
"open_options": "Deschide opțiunile graficului",
"or_filter_logic": "SAU",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Te rugăm să selectezi un tablou de bord",
"predefined_measures": "Măsurători predefinite",
"preset": "Presetare",
"preview_chart": "Previzualizare diagramă",
"query_executed_successfully": "Interogarea a fost executată cu succes",
"reset_to_ai_suggestion": "Resetează la sugestia AI",
"save_and_add_to_dashboard": "Salvați și adăugați în tabloul de bord",
"save_chart": "Salvează graficul",
"save_chart_dialog_title": "Salvează graficul",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Selectează câmpul",
"select_measures": "Selectează măsuri...",
"select_preset": "Selectează presetarea",
"showing_first_n_of": "Se afișează primele {{n}} din {{count}} rânduri",
"showing_first_n_of": "Se afișează primele {n} din {count} rânduri",
"start_date": "Data de început",
"time_dimension": "Dimensiune temporală",
"time_dimension_title": "Adaugă grupare pe bază de timp",
"time_dimension_toggle_description": "Monitorizează tendințele în timp."
"time_dimension_toggle_description": "Monitorizează tendințele în timp.",
"update_chart": "Actualizați diagrama"
},
"dashboards": {
"add_count_charts": "Adaugă {count} grafic(e)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Creează panou de control",
"create_dashboard_description": "Introdu un nume pentru noul tău tablou de bord.",
"create_failed": "Crearea tabloului de bord a eșuat",
"create_new_chart": "Creați o nouă diagramă",
"create_success": "Tablou de bord creat cu succes!",
"dashboard": "Panou de control",
"dashboard_delete_confirmation": "Ești sigur că vrei să ștergi acest panou de control? Această acțiune nu poate fi anulată.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Duplicarea tabloului de bord a eșuat",
"duplicate_success": "Tablou de bord duplicat cu succes!",
"failed_to_load_chart_data": "Încărcarea datelor graficului a eșuat",
"no_charts_available_description": "Nu există grafice care pot fi adăugate la acest tablou de bord. Fie nu există încă grafice, fie toate graficele existente au fost deja adăugate. Mergi la pagina Grafice pentru a crea grafice noi.",
"no_charts_to_add_message": "Nu există grafice de adăugat la acest tablou de bord.",
"no_dashboards_found": "Nu s-a găsit niciun tablou de bord.",
"no_data_message": "Fără date. În prezent nu există informații de afișat. Adaugă grafice pentru a-ți construi tabloul de bord.",
"please_enter_name": "Te rugăm să introduci un nume pentru tablou de bord"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Nu aveți înregistrări de feedback despre care să raportați. Configurați sursele de feedback pentru a introduce date în sistem.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Configurați sursele de feedback"
},
"api_keys": {
"add_api_key": "Adaugă cheie API",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Conectează-ți aplicația sau site-ul la Formbricks.",
"cache_update_delay_description": "Când faci actualizări la sondaje, contacte, acțiuni sau alte date, poate dura până la 1 minut până când aceste modificări apar în aplicația ta locală care rulează SDK-ul Formbricks.",
"cache_update_delay_title": "Modificările vor fi vizibile după aproximativ 1 minut din cauza cache-ului",
"environment_id_legacy": "ID mediu (legacy)",
"environment_id_legacy_alert": "Configurația SDK existentă poate folosi încă un ID de mediu legacy.",
"environment_id_legacy_alert_link": "Află de ce și cum să migrezi.",
"formbricks_sdk_connected": "SDK Formbricks este conectat",
"formbricks_sdk_not_connected": "SDK Formbricks nu este încă conectat.",
"formbricks_sdk_not_connected_description": "Adaugă SDK-ul Formbricks pe site-ul sau în aplicația ta pentru a-l conecta cu Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Logo Formbricks",
"general": {
"cannot_delete_only_workspace": "Acesta este singurul tău proiect, nu poate fi șters. Creează mai întâi un proiect nou.",
"cannot_delete_only_workspace": "Acesta este singurul tău spațiu de lucru și nu poate fi șters. Creează mai întâi un spațiu de lucru nou.",
"custom_scripts": "Scripturi personalizate",
"custom_scripts_card_description": "Adaugă scripturi de tracking și pixeli tuturor sondajelor cu link din acest spațiu de lucru.",
"custom_scripts_description": "Scripturile vor fi injectate în <head> pe toate paginile sondajelor cu link.",
@@ -1989,21 +2005,21 @@
"custom_scripts_placeholder": "<!-- Lipește aici scripturile tale de tracking -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripturile personalizate au fost actualizate cu succes",
"custom_scripts_warning": "Scripturile se execută cu acces complet la browser. Adaugă doar scripturi din surse de încredere.",
"delete_workspace": "Șterge proiectul",
"delete_workspace": "Șterge Spațiul de Lucru",
"delete_workspace_confirmation": "Ești sigur că vrei să ștergi {workspaceName}? Această acțiune nu poate fi anulată.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Șterge {workspaceName} incluzând toate sondajele, răspunsurile, persoanele, acțiunile și atributele.",
"delete_workspace_settings_description": "Șterge proiectul cu toate sondajele, răspunsurile, persoanele, acțiunile și atributele. Aceasta nu poate fi anulată.",
"error_saving_workspace_information": "Eroare la salvarea informațiilor despre proiect",
"only_owners_or_managers_can_delete_workspaces": "Doar proprietarii sau managerii pot șterge proiecte",
"delete_workspace_settings_description": "Șterge spațiul de lucru împreună cu toate sondajele, răspunsurile, persoanele, acțiunile și atributele. Această acțiune nu poate fi anulată.",
"error_saving_workspace_information": "Eroare la salvarea informațiilor spațiului de lucru",
"only_owners_or_managers_can_delete_workspaces": "Doar proprietarii sau managerii pot șterge spațiile de lucru",
"recontact_waiting_time": "Perioadă de răcire (între sondaje)",
"recontact_waiting_time_settings_description": "Controlează cât de des pot fi chestionați utilizatorii în toate sondajele Website & App din acest workspace.",
"this_action_cannot_be_undone": "Această acțiune nu poate fi anulată.",
"wait_x_days_before_showing_next_survey": "Așteaptă X zile înainte de a afișa următorul sondaj:",
"waiting_period_updated_successfully": "Perioada de așteptare a fost actualizată cu succes",
"whats_your_workspace_called": "Cum se numește proiectul tău?",
"workspace_deleted_successfully": "Proiectul a fost șters cu succes",
"workspace_name_settings_description": "Schimbă numele proiectului tău.",
"workspace_name_updated_successfully": "Numele proiectului a fost actualizat cu succes"
"whats_your_workspace_called": "Cum se numește spațiul tău de lucru?",
"workspace_deleted_successfully": "Spațiul de lucru a fost șters cu succes",
"workspace_name_settings_description": "Schimbă numele spațiului tău de lucru.",
"workspace_name_updated_successfully": "Numele spațiului de lucru a fost actualizat cu succes"
},
"integrations": {
"activepieces_integration_description": "Conectați instantaneu Formbricks cu aplicații populare pentru a automatiza sarcini fără codare.",
@@ -2161,7 +2177,7 @@
"alias_tooltip": "Aliasul este un nume alternativ pentru a identifica limba în sondajele pe link și în SDK (opțional)",
"cannot_remove_language_warning": "Nu poți elimina această limbă deoarece este încă folosită în următoarele sondaje:",
"conflict_between_identifier_and_alias": "Există un conflict între identificatorul unei limbi adăugate și unul dintre aliasurile tale. Aliasurile și identificatorii nu pot fi identici.",
"conflict_between_selected_alias_and_another_language": "Există un conflict între aliasul selectat și o altă limbă care are acest identificator. Adaugă limba cu acest identificator în proiectul tău pentru a evita inconsistențele.",
"conflict_between_selected_alias_and_another_language": "Există un conflict între aliasul selectat și o altă limbă care are acest identificator. Te rugăm să adaugi în schimb limba cu acest identificator în spațiul tău de lucru pentru a evita inconsistențele.",
"delete_language_confirmation": "Sigur vrei să ștergi această limbă? Această acțiune nu poate fi anulată.",
"duplicate_language_or_language_id": "Limbă sau ID de limbă duplicat",
"edit_languages": "Editați limbile",
@@ -2389,7 +2405,7 @@
"most_popular": "Cel mai popular",
"pending_change_removed": "Schimbarea de plan programată a fost anulată.",
"pending_plan_badge": "Programat",
"pending_plan_change_description": "Planul tău va trece la {{plan}} pe {{date}}.",
"pending_plan_change_description": "Abonamentul tău va trece la {plan} pe {date}.",
"pending_plan_change_title": "Schimbare de plan programată",
"pending_plan_cta": "Programat",
"per_month": "pe lună",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Există deja un director de înregistrări feedback cu acest nume.",
"error_directory_name_required": "Numele directorului este obligatoriu.",
"error_directory_workspaces_invalid_org": "Unele spații de lucru specificate nu aparțin acestei organizații.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Directoare de feedback",
"no_access": "Nu ai permisiunea de a gestiona directoarele de înregistrări de feedback.",
"no_connectors": "Niciun conector asociat acestui director încă.",
"pause_connectors_confirmation_description": "Dacă pui pe pauză acești conectori, nu vor mai fi adăugate înregistrări noi.",
"pause_connectors_confirmation_title": "Pauzezi conectorii asociați?",
"select_workspaces_placeholder": "Selectează spații de lucru...",
"show_archived": "Afișează arhivate",
"title": "Directoare de Înregistrări Feedback",
"unarchive": "Dezarhivează"
"unarchive": "Dezarhivează",
"unarchive_workspace_conflict": "Acest director nu poate fi dezarhivat deoarece unul sau mai multe spații de lucru alocate sunt arhivate.",
"workspace_access": "Acces la spațiul de lucru"
},
"general": {
"ai_data_analysis_disabled_for_organization": "Analiza și îmbogățirea datelor cu AI sunt dezactivate pentru această organizație.",
"ai_data_analysis_enabled": "Îmbogățire și analiză de date (AI)",
"ai_data_analysis_enabled_description": "AI pentru a obține mai mult din datele tale, configurare dashboard-uri, grafice, rapoarte și multe altele. Accesează datele tale de experiență.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "Gestionează funcționalitățile bazate pe AI pentru această organizație.",
"ai_features_not_enabled_for_organization": "Funcționalitățile AI nu sunt activate pentru această organizație.",
"ai_instance_not_configured": "AI este configurată la nivel de instanță prin variabile de mediu. Cere administratorului să configureze AI_PROVIDER, credențialele acelui furnizor și lista de modele corespunzătoare înainte de a activa funcționalitățile AI.",
"ai_settings_updated_successfully": "Setările AI au fost actualizate cu succes",
"ai_smart_tools_disabled_for_organization": "Instrumentele inteligente AI sunt dezactivate pentru această organizație.",
"ai_smart_tools_enabled": "Funcționalitate inteligentă (AI)",
"ai_smart_tools_enabled_description": "AI care te ajută să faci mai mult în mai puțin timp. Nu accesează niciodată datele colectate cu Formbricks. Folosit doar, de exemplu, pentru a traduce chestionare în alte limbi.",
"bulk_invite_warning_description": "În planul gratuit, toți membrii organizației sunt întotdeauna alocați rolului „Proprietar”.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Înscrie-te aici.",
"share_invite_link": "Distribuie link-ul de invitație",
"share_this_link_to_let_your_organization_member_join_your_organization": "Distribuie acest link pentru a permite membrului organizației să se alăture organizației tale:",
"test_email_sent_successfully": "Email de test trimis cu succes"
"test_email_sent_successfully": "Email de test trimis cu succes",
"unlock_ai_features_description": "Traducerile alimentate de AI, instrumentele inteligente și analiza datelor sunt disponibile pe planuri superioare. Treci la un abonament superior pentru a-ți potenția chestionarele cu AI.",
"unlock_ai_features_with_a_higher_plan": "Deblochează funcțiile AI cu un abonament superior"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Auto-abonare la sondaje noi",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Ajustați mesajul 'Sondaj Închis'",
"adjust_survey_closed_message_description": "Schimbați mesajul pe care îl văd vizitatorii atunci când sondajul este închis.",
"adjust_the_theme_in_the": "Ajustați tema în",
"ai_data_analysis_disabled": "Analiza de date AI este dezactivată pentru această organizație.",
"ai_features_not_enabled": "Funcțiile AI nu sunt activate pentru această organizație.",
"ai_instance_not_configured": "AI nu este configurat. Contactează administratorul.",
"ai_smart_tools_disabled": "Instrumentele inteligente AI sunt dezactivate pentru această organizație.",
"ai_translate": "Traduce cu AI",
"ai_translating": "Se traduce cu AI... Te rugăm să ții această fereastră deschisă.",
"ai_translation_all_fields_populated": "Toate câmpurile sunt deja traduse",
"ai_translation_complete": "Traducerea AI finalizată",
"ai_translation_failed": "Traducerea a eșuat",
"ai_translation_instance_not_configured": "AI nu este configurat pe această instanță. Contactează administratorul.",
"ai_translation_not_available": "Traducerea AI nu este disponibilă pe abonamentul tău actual. Treci la un abonament superior pentru a debloca această funcție.",
"ai_translation_not_enabled": "Instrumentele inteligente AI sunt dezactivate pentru această organizație. Activează-le în setările organizației.",
"all_are_true": "toate sunt adevărate",
"all_other_answers_will_continue_to": "Toate celelalte răspunsuri vor continua să",
"allow_multi_select": "Permite selectare multiplă",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Vedeți ce echipe pot accesa acest spațiu de lucru."
},
"unify": {
"add_feedback_record": "Adăugați înregistrarea de feedback",
"add_feedback_record_description": "Creați manual o înregistrare de feedback.",
"add_feedback_source": "Adaugă sursă de feedback",
"add_source": "Adaugă sursă",
"allowed_values": "Valori permise: {values}",
"api_ingestion": "Ingestie API",
"api_ingestion_manage_api_keys": "Gestionează cheile API",
"api_ingestion_settings_description": "Trimite înregistrări de feedback folosind API-ul de management.",
"auto_generated": "Generat automat",
"change_file": "Schimbă fișierul",
"click_load_sample_csv": "Apasă pe „Încarcă CSV de exemplu” pentru a vedea coloanele",
"click_to_upload": "Apasă pentru a încărca",
"collected_at": "Colectat la",
"configure_import": "Configurează importul",
"configure_mapping": "Configurează maparea",
"connection": "Conexiune",
"connector_created_successfully": "Conector creat cu succes",
"connector_deleted_successfully": "Conector șters cu succes",
"connector_duplicated_successfully": "Conector duplicat cu succes",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Importarea datelor de două ori va crea înregistrări duplicate.",
"csv_inconsistent_columns": "Rândul {row} are coloane inconsistente. Toate rândurile trebuie să aibă aceleași antete.",
"csv_max_records": "Sunt permise maximum {max} înregistrări.",
"custom_source_type": "Tip sursă personalizat",
"custom_source_type_placeholder": "Introduceți tipul de sursă personalizat",
"default_connector_name_csv": "Import CSV",
"default_connector_name_formbricks": "Conexiune chestionar Formbricks",
"deselect_all": "Deselectează tot",
"discard_feedback_record_changes_description": "Modificările dvs. se vor pierde dacă închideți acest sertar.",
"discard_feedback_record_changes_title": "Renunțați la modificările nesalvate?",
"drop_a_field_here": "Trage un câmp aici",
"drop_field_or": "Trage câmpul sau",
"edit_csv_mapping": "Editează maparea CSV",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Nu s-au putut încărca înregistrările de feedback",
"feedback_date": "Data curentă",
"feedback_record_created_successfully": "Înregistrare de feedback creată cu succes",
"feedback_record_details": "Detaliile înregistrării feedback-ului",
"feedback_record_details_description": "Examinați și actualizați câmpurile pentru înregistrarea de feedback.",
"feedback_record_directory": "Director de înregistrări feedback",
"feedback_record_fields": "Câmpuri înregistrare feedback",
"feedback_record_mcp": "MCP Înregistrări Feedback",
"feedback_record_updated_successfully": "Înregistrarea feedback-ului a fost actualizată cu succes",
"feedback_record_value_required": "Este necesară o valoare pentru tipul de câmp selectat",
"feedback_records": "Înregistrări de feedback",
"feedback_records_refreshed": "Înregistrările de feedback au fost actualizate",
"feedback_sources": "Surse de feedback",
"feedback_sources_directory_access_multiple": "Înregistrările noi din aceste surse vor fi stocate în: {directoryNames}",
"feedback_sources_directory_access_single": "Înregistrările noi din această sursă vor fi stocate în: {directoryNames}",
"feedback_sources_settings_description": "Conectează și gestionează toate sursele de feedback pentru acest spațiu de lucru.",
"field_group_id": "ID grup de câmpuri",
"field_group_label": "Eticheta grupului de câmpuri",
"field_id": "ID-ul câmpului",
"field_label": "Etichetă câmp",
"field_type": "Tip câmp",
"formbricks_surveys": "Chestionare Formbricks",
"frd_cannot_be_changed": "Directorul de feedback nu poate fi modificat după creare.",
"go_to_feedback_record_directories": "Mergi la setările directoarelor",
"historical_import_complete": "Import finalizat: {successes} reușite, {failures} eșuate, {skipped} omise (fără date)",
"import_csv_data": "Importă feedback",
"import_feedback": "Importă feedback",
"import_historical_responses": "Importă răspunsuri istorice",
"import_historical_responses_description": "Importă acum răspunsurile existente din acest sondaj.",
"import_rows": "Importă {count, plural, one {# rând} few {# rânduri} other {# de rânduri}}",
"import_via_source_name": "Import prin „{sourceName}”",
"importing_data": "Se importă datele...",
"importing_historical_data": "Se importă datele istorice...",
"invalid_enum_values": "Valori invalide în coloana mapată la {field}",
"invalid_values_found": "Găsite: {values} (rânduri: {rows}) {extra}",
"load_sample_csv": "Încarcă un CSV de exemplu",
"n_supported_questions": "{count} întrebări acceptate",
"manage_directories": "Gestionează directoarele",
"manage_feedback_sources": "Gestionați sursele de feedback",
"metadata": "Metadate",
"metadata_key": "Cheia de metadate",
"metadata_read_only_entries": "Valori de metadate numai pentru citire (fără șir)",
"metadata_value": "Valoarea metadatelor",
"missing_feedback_source_title": "Lipsește o sursă de feedback?",
"no_feedback_record_directory_available": "Niciun director de înregistrări feedback atribuit acestui spațiu de lucru. Creează sau atribuie unul mai întâi.",
"no_feedback_records": "Nu există încă înregistrări de feedback. Înregistrările vor apărea aici după ce conectorii tăi vor începe să trimită date.",
"no_source_fields_loaded": "Nu au fost încă încărcate câmpuri sursă",
"no_sources_connected": "Nicio sursă conectată încă. Adaugă o sursă pentru a începe.",
"no_surveys_found": "Nu s-au găsit sondaje în acest mediu",
"optional": "Opțional",
"or_drag_and_drop": "sau trage și lasă aici",
"question_selected": "<strong>{count}</strong> întrebare selectată. Fiecare răspuns la aceste întrebări va crea un nou Feedback Record.",
"question_type_not_supported": "Acest tip de întrebare nu este suportat",
"questions_selected": "<strong>{count}</strong> întrebări selectate. Fiecare răspuns la aceste întrebări va crea un nou Feedback Record.",
"records_will_go_to": "Înregistrările vor ajunge în",
"refresh_feedback_records": "Reîmprospătează înregistrările de feedback",
"refreshing_feedback_records": "Se actualizează înregistrările de feedback...",
"request_feedback_source": "Solicită integrarea sursei",
"required": "Obligatoriu",
"save_changes": "Salvează modificările",
"select_a_survey_to_see_questions": "Selectează un chestionar pentru a vedea întrebările",
"select_a_value": "Selectează o valoare...",
"select_all": "Selectează tot",
"select_feedback_record_directory": "Selectează un director",
"select_feedback_record_source_type": "Selectați tipul sursei",
"select_questions": "Selectează întrebări",
"select_source_type_description": "Selectează tipul sursei de feedback pe care vrei să o conectezi.",
"select_source_type_prompt": "Selectează tipul sursei de feedback pe care vrei să o conectezi:",
"select_survey": "Selectează chestionar",
"select_survey_and_questions": "Selectează chestionar și întrebări",
"select_survey_questions_description": "Alege ce întrebări din chestionar vor crea FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Se afișează 3 din {count} rânduri",
"source": "sursă",
"source_connect_csv_description": "Importă feedback din fișiere CSV",
"source_connect_feedback_record_mcp_description": "Trimite înregistrări de feedback prin integrarea MCP.",
"source_connect_formbricks_description": "Conectează feedback din sondajele Formbricks",
"source_fields": "Câmpuri sursă",
"source_id": "ID sursă",
"source_name": "Nume sursă",
"source_type": "Tip sursă",
"source_type_cannot_be_changed": "Tipul sursei nu poate fi schimbat",
"sources": "Surse",
"status_active": "În progres",
"status_completed": "Finalizat",
"status_draft": "Schiță",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Eroare",
"status_paused": "Pauzat",
"status_live_sync": "Sincronizare în timp real",
"status_ready": "Gata",
"submission_id": "ID-ul trimiterii",
"survey_has_no_questions": "Acest sondaj nu are întrebări",
"survey_import_line": "{surveyName}: {responseCount} răspunsuri × {questionCount} întrebări = {total} Feedback Records",
"total_feedback_records": "Total: {checked} din {total} Feedback Records selectate în {surveyCount} sondaje",
"topics_and_subtopics": "Subiecte și subiecte secundare",
"unify_feedback": "Unify Feedback",
"update_mapping_description": "Actualizează configurația de mapare pentru această sursă.",
"updated_at": "Actualizat la",
"upload_csv_data_description": "Încarcă un fișier CSV pentru a importa date de feedback.",
"upload_csv_file": "Încarcă fișier CSV",
"user_identifier": "Utilizator",
"value": "Valoare"
"value": "Valoare",
"value_boolean": "Valoare (booleană)",
"value_date": "Valoare (data)",
"value_number": "Valoare (număr)",
"value_text": "Valoare (Text)"
},
"xm-templates": {
"ces": "CES",
+123 -55
View File
@@ -125,6 +125,7 @@
"activity": "Активность",
"add": "Добавить",
"add_action": "Добавить действие",
"add_chart": "Добавить диаграмму",
"add_charts": "Добавить графики",
"add_existing_chart_description": "Найдите и выберите графики для добавления на этот дашборд.",
"add_filter": "Добавить фильтр",
@@ -147,6 +148,7 @@
"apply_filters": "Применить фильтры",
"archived": "Архивный",
"are_you_sure": "Вы уверены?",
"ask": "Ask",
"attributes": "Атрибуты",
"back": "Назад",
"billing": "Оплата",
@@ -159,7 +161,7 @@
"change_workspace": "Сменить рабочее пространство",
"chart": "График",
"charts": "Графики",
"choice_n": "Вариант {{n}}",
"choice_n": "Вариант {n}",
"choices": "Варианты",
"choose_organization": "Выберите организацию",
"choose_workspace": "Выбрать рабочее пространство",
@@ -172,7 +174,7 @@
"close": "Закрыть",
"code": "Код",
"collapse_rows": "Свернуть строки",
"column_n": "Колонка {{n}}",
"column_n": "Столбец {n}",
"completed": "Завершено",
"configuration": "Настроить",
"confirm": "Подтвердить",
@@ -212,6 +214,7 @@
"delete_what": "Удалить {deleteWhat}",
"description": "Описание",
"disable": "Отключить",
"disabled": "Отключено",
"disallow": "Не разрешать",
"discard": "Отменить",
"dismissed": "Отклонено",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Не удалось загрузить организации",
"failed_to_load_workspaces": "Не удалось загрузить рабочие пространства",
"failed_to_parse_csv": "Не удалось обработать CSV",
"field_placeholder": "Заполнитель {{field}}",
"field_placeholder": "Заполнитель поля {field}",
"filter": "Фильтр",
"finish": "Завершить",
"first_name": "Имя",
@@ -331,6 +334,7 @@
"not_authenticated": "У вас нет прав для выполнения этого действия.",
"not_authorized": "Нет доступа",
"not_connected": "Нет подключения",
"not_set": "Не установлено",
"note": "Примечание",
"notifications": "Уведомления",
"number": "Номер",
@@ -390,13 +394,14 @@
"report_survey": "Пожаловаться на опрос",
"request_trial_license": "Запросить пробную лицензию",
"reset_to_default": "Сбросить по умолчанию",
"resize": "Изменить размер",
"response": "Ответ",
"response_id": "ID ответа",
"responses": "Ответы",
"restart": "Перезапустить",
"retry": "Повторить",
"role": "Роль",
"row_n": "Строка {{n}}",
"row_n": "Строка {n}",
"saas": "SaaS",
"sales": "Продажи",
"save": "Сохранить",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Не удалось загрузить некоторые файлы",
"something_went_wrong": "Что-то пошло не так",
"something_went_wrong_please_try_again": "Что-то пошло не так. Пожалуйста, попробуйте ещё раз.",
"soon": "Скоро",
"sort_by": "Сортировать по",
"start_free_trial": "Начать бесплатный пробный период",
"status": "Статус",
@@ -502,7 +508,7 @@
"welcome_card": "Приветственная карточка",
"workspace": "Рабочее пространство",
"workspace_configuration": "Настройка рабочего пространства",
"workspace_created_successfully": "Рабочий проект успешно создан",
"workspace_created_successfully": "Рабочее пространство успешно создано",
"workspace_creation_description": "Организуйте опросы в рабочих пространствах для лучшего контроля доступа.",
"workspace_id": "ID рабочего пространства",
"workspace_name": "Название рабочего пространства",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Столбчатая диаграмма",
"chart_type_big_number": "Большое число",
"chart_type_line": "Линейный график",
"chart_type_not_supported": "Тип графика «{{chartType} пока не поддерживается",
"chart_type_not_supported": "Тип диаграммы \"{chartType}\" пока не поддерживается",
"chart_type_pie": "Круговая диаграмма",
"chart_updated_successfully": "График успешно обновлён!",
"configure_description": "Измени тип графика и другие настройки этой визуализации.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Не удалось выполнить запрос",
"failed_to_load_chart": "Не удалось загрузить график",
"failed_to_load_chart_data": "Не удалось загрузить данные графика",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Не удалось сохранить график",
"field": "Поле",
"field_label_average_score": "Средний балл",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Нет корректных данных для отображения",
"not_contains": "не содержит",
"not_equals": "не равно",
"open_chart": "Открыть график {{name}}",
"open_chart": "Открыть диаграмму {name}",
"open_options": "Открыть настройки графика",
"or_filter_logic": "ИЛИ",
"original": "Оригинал",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Пожалуйста, выбери панель управления",
"predefined_measures": "Предустановленные показатели",
"preset": "Пресет",
"preview_chart": "Предварительный просмотр диаграммы",
"query_executed_successfully": "Запрос успешно выполнен",
"reset_to_ai_suggestion": "Сбросить к предложению ИИ",
"save_and_add_to_dashboard": "Сохранить и добавить на панель управления",
"save_chart": "Сохранить график",
"save_chart_dialog_title": "Сохранить график",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Выберите поле",
"select_measures": "Выберите показатели...",
"select_preset": "Выберите пресет",
"showing_first_n_of": "Показаны первые {{n}} из {{count}} строк",
"showing_first_n_of": "Показаны первые {n} из {count} строк",
"start_date": "Дата начала",
"time_dimension": "Временное измерение",
"time_dimension_title": "Добавить группировку по времени",
"time_dimension_toggle_description": "Отслеживайте тренды с течением времени."
"time_dimension_toggle_description": "Отслеживайте тренды с течением времени.",
"update_chart": "Обновить диаграмму"
},
"dashboards": {
"add_count_charts": "Добавить {count} график(ов)",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Создать дашборд",
"create_dashboard_description": "Введите название для новой панели управления.",
"create_failed": "Не удалось создать панель управления",
"create_new_chart": "Создать новую диаграмму",
"create_success": "Панель управления успешно создана!",
"dashboard": "Дашборд",
"dashboard_delete_confirmation": "Вы уверены, что хотите удалить этот дашборд? Это действие нельзя отменить.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Не удалось дублировать панель управления",
"duplicate_success": "Панель управления успешно продублирована!",
"failed_to_load_chart_data": "Не удалось загрузить данные графика",
"no_charts_available_description": "Нет графиков, которые можно добавить к этому дашборду. Либо графики ещё не созданы, либо все существующие графики уже добавлены. Перейдите на страницу «Графики», чтобы создать новые графики.",
"no_charts_to_add_message": "Нет графиков для добавления к этому дашборду.",
"no_dashboards_found": "Панели управления не найдены.",
"no_data_message": "Нет данных. В настоящее время нет информации для отображения. Добавьте графики, чтобы построить свой дашборд.",
"please_enter_name": "Пожалуйста, введите название панели управления"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "У вас нет записей обратной связи, о которых можно сообщить. Настройте источники обратной связи для подачи данных в систему.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Настройка источников обратной связи"
},
"api_keys": {
"add_api_key": "Добавить API-ключ",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Подключите ваше приложение или сайт к Formbricks.",
"cache_update_delay_description": "Когда вы обновляете опросы, контакты, действия или другие данные, изменения могут появиться в вашем локальном приложении с Formbricks SDK с задержкой до 1 минуты.",
"cache_update_delay_title": "Изменения отразятся примерно через 1 минуту из-за кэширования",
"environment_id_legacy": "ID среды (устаревший)",
"environment_id_legacy_alert": "Твоя текущая настройка SDK может всё ещё использовать устаревший ID среды.",
"environment_id_legacy_alert_link": "Узнай, почему и как выполнить миграцию.",
"formbricks_sdk_connected": "Formbricks SDK подключён",
"formbricks_sdk_not_connected": "Formbricks SDK ещё не подключён.",
"formbricks_sdk_not_connected_description": "Добавьте SDK Formbricks на ваш сайт или в приложение, чтобы подключить его к Formbricks",
@@ -1981,7 +1997,7 @@
},
"formbricks_logo": "Логотип Formbricks",
"general": {
"cannot_delete_only_workspace": "Это ваш единственный рабочий проект, его нельзя удалить. Сначала создайте новый проект.",
"cannot_delete_only_workspace": "Это ваше единственное рабочее пространство, его нельзя удалить. Сначала создайте новое рабочее пространство.",
"custom_scripts": "Пользовательские скрипты",
"custom_scripts_card_description": "Добавьте трекинговые скрипты и пиксели ко всем опросам по ссылке в этом рабочем пространстве.",
"custom_scripts_description": "Скрипты будут внедряться в <head> всех страниц опросов по ссылке.",
@@ -1989,20 +2005,20 @@
"custom_scripts_placeholder": "<!-- Вставьте сюда ваши трекинговые скрипты -->\n<script>\n // Google Tag Manager, Analytics и др.\n</script>",
"custom_scripts_updated_successfully": "Пользовательские скрипты успешно обновлены",
"custom_scripts_warning": "Скрипты выполняются с полным доступом к браузеру. Добавляйте только скрипты из доверенных источников.",
"delete_workspace": "Удалить рабочий проект",
"delete_workspace": "Удалить рабочее пространство",
"delete_workspace_confirmation": "Вы уверены, что хотите удалить {workspaceName}? Это действие нельзя отменить.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Удалить {workspaceName}, включая все опросы, ответы, людей, действия и атрибуты.",
"delete_workspace_settings_description": "Удалить рабочий проект со всеми опросами, ответами, пользователями, действиями и атрибутами. Это действие необратимо.",
"error_saving_workspace_information": "Ошибка при сохранении информации о рабочем проекте",
"only_owners_or_managers_can_delete_workspaces": "Только владельцы или менеджеры могут удалять рабочие проекты",
"delete_workspace_settings_description": "Удалить рабочее пространство со всеми опросами, ответами, людьми, действиями и атрибутами. Это действие нельзя отменить.",
"error_saving_workspace_information": "Ошибка при сохранении информации о рабочем пространстве",
"only_owners_or_managers_can_delete_workspaces": "Только владельцы или менеджеры могут удалять рабочие пространства",
"recontact_waiting_time": "Период ожидания (между опросами)",
"recontact_waiting_time_settings_description": "Управляйте частотой, с которой пользователи могут проходить опросы на сайте и в приложении в этом рабочем пространстве.",
"this_action_cannot_be_undone": "Это действие нельзя отменить.",
"wait_x_days_before_showing_next_survey": "Ждать X дней перед показом следующего опроса:",
"waiting_period_updated_successfully": "Период ожидания успешно обновлён",
"whats_your_workspace_called": "Как называется ваш рабочий проект?",
"workspace_deleted_successfully": "Рабочий проект успешно удалён",
"workspace_name_settings_description": "Измените название вашего рабочего проекта.",
"whats_your_workspace_called": "Как называется твоё рабочее пространство?",
"workspace_deleted_successfully": "Рабочее пространство успешно удалено",
"workspace_name_settings_description": "Измените название вашего рабочего пространства.",
"workspace_name_updated_successfully": "Название рабочей области успешно обновлено"
},
"integrations": {
@@ -2389,7 +2405,7 @@
"most_popular": "Самый популярный",
"pending_change_removed": "Запланированное изменение тарифа отменено.",
"pending_plan_badge": "Запланирован",
"pending_plan_change_description": "Ваш тариф изменится на {{plan}} {{date}}.",
"pending_plan_change_description": "Твой тариф сменится на {plan} {date}.",
"pending_plan_change_title": "Запланированное изменение тарифа",
"pending_plan_cta": "Запланирован",
"per_month": "в месяц",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Директория с записями обратной связи с таким именем уже существует.",
"error_directory_name_required": "Необходимо указать имя директории.",
"error_directory_workspaces_invalid_org": "Некоторые указанные рабочие пространства не принадлежат этой организации.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Каталоги отзывов",
"no_access": "У тебя нет прав для управления каталогами записей отзывов.",
"no_connectors": "К этому каталогу пока не привязано ни одного коннектора.",
"pause_connectors_confirmation_description": "Если приостановить эти коннекторы, новые записи больше не будут добавляться.",
"pause_connectors_confirmation_title": "Приостановить связанные коннекторы?",
"select_workspaces_placeholder": "Выберите рабочие области...",
"show_archived": "Показать архивные",
"title": "Директории записей обратной связи",
"unarchive": "Разархивировать"
"unarchive": "Разархивировать",
"unarchive_workspace_conflict": "Невозможно разархивировать этот каталог, потому что один или несколько назначенных рабочих пространств архивированы.",
"workspace_access": "Доступ к рабочему пространству"
},
"general": {
"ai_data_analysis_disabled_for_organization": "ИИ-анализ и обогащение данных отключены для этой организации.",
"ai_data_analysis_enabled": "Обогащение и анализ данных (ИИ)",
"ai_data_analysis_enabled_description": "ИИ для получения большего от твоих данных: настройка дашбордов, графиков, отчетов и не только. Работает с твоими данными об опыте.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "Управляй функциями на базе ИИ для этой организации.",
"ai_features_not_enabled_for_organization": "Функции ИИ не включены для этой организации.",
"ai_instance_not_configured": "ИИ настраивается на уровне инстанса через переменные окружения. Попросите администратора настроить AI_PROVIDER, учетные данные этого провайдера и соответствующий список моделей перед включением функций ИИ.",
"ai_settings_updated_successfully": "Настройки AI успешно обновлены",
"ai_smart_tools_disabled_for_organization": "Интеллектуальные функции ИИ отключены для этой организации.",
"ai_smart_tools_enabled": "Умные функции (ИИ)",
"ai_smart_tools_enabled_description": "ИИ помогает тебе делать больше за меньшее время. Никогда не использует данные, собранные с помощью Formbricks. Применяется, например, для перевода опросов на другие языки.",
"bulk_invite_warning_description": "В бесплатном тарифе всем участникам организации всегда назначается роль \"Владелец\".",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "Вы не можете покинуть эту организацию, так как она у вас единственная. Сначала создайте новую организацию.",
"copy_invite_link_to_clipboard": "Скопировать ссылку-приглашение в буфер обмена",
"create_new_organization": "Создать новую организацию",
"create_new_organization_description": "Создайте новую организацию для управления отдельным набором проектов.",
"create_new_organization_description": "Создайте новую организацию для управления другим набором рабочих пространств.",
"customize_email_with_a_higher_plan": "Настройте электронную почту с помощью более высокого тарифа",
"delete_member_confirmation": "Удалённые участники потеряют доступ ко всем проектам и опросам вашей организации.",
"delete_member_confirmation": "Удалённые участники потеряют доступ ко всем рабочим пространствам и опросам вашей организации.",
"delete_organization": "Удалить организацию",
"delete_organization_description": "Удалите организацию со всеми её проектами, включая все опросы, ответы, людей, действия и атрибуты",
"delete_organization_description": "Удалить организацию со всеми её рабочими пространствами, включая все опросы, ответы, людей, действия и атрибуты",
"delete_organization_warning": "Прежде чем продолжить удаление этой организации, обратите внимание на следующие последствия:",
"delete_organization_warning_1": "Безвозвратное удаление всех проектов, связанных с этой организацией.",
"delete_organization_warning_1": "Безвозвратное удаление всех рабочих пространств, связанных с этой организацией.",
"delete_organization_warning_2": "Это действие нельзя отменить. Если удалено — то удалено навсегда.",
"delete_organization_warning_3": "Пожалуйста, введите {organizationName} в поле ниже для подтверждения окончательного удаления этой организации:",
"eliminate_branding_with_whitelabel": "Уберите брендинг Formbricks и получите дополнительные возможности для white-label кастомизации.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Зарегистрируйтесь здесь.",
"share_invite_link": "Поделиться ссылкой-приглашением",
"share_this_link_to_let_your_organization_member_join_your_organization": "Поделитесь этой ссылкой, чтобы участник вашей организации мог присоединиться к ней:",
"test_email_sent_successfully": "Тестовое письмо успешно отправлено"
"test_email_sent_successfully": "Тестовое письмо успешно отправлено",
"unlock_ai_features_description": "Переводы с помощью ИИ, умные инструменты и анализ данных доступны на более высоких тарифах. Переходи на продвинутый план, чтобы усилить свои опросы возможностями ИИ.",
"unlock_ai_features_with_a_higher_plan": "Открой возможности ИИ с более высоким тарифом"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Автоматически подписываться на новые опросы",
@@ -2672,7 +2692,7 @@
"add_workspaces_description": "Управляйте доступом участников команды к рабочим пространствам.",
"all_members_added": "Все участники добавлены в эту команду.",
"all_workspaces_added": "Все рабочие пространства добавлены в эту команду.",
"are_you_sure_you_want_to_delete_this_team": "Вы уверены, что хотите удалить эту команду? Это также удалит доступ ко всем проектам и опросам, связанным с этой командой.",
"are_you_sure_you_want_to_delete_this_team": "Вы уверены, что хотите удалить эту команду? Это также удалит доступ ко всем рабочим пространствам и опросам, связанным с этой командой.",
"billing_role_description": "Доступ только к платёжной информации.",
"bulk_invite": "Массовое приглашение",
"contributor": "Участник",
@@ -2688,10 +2708,10 @@
"manage": "Управлять",
"manage_team": "Управлять командой",
"manage_team_disabled": "Только владельцы организации, менеджеры и администраторы команд могут управлять командами.",
"manager_role_description": "Менеджеры имеют доступ ко всем проектам, а также могут добавлять и удалять участников.",
"manager_role_description": "Менеджеры могут получать доступ ко всем рабочим пространствам, а также добавлять и удалять участников.",
"member": "Участник",
"member_role_description": "Участники могут работать в выбранных проектах.",
"member_role_info_message": "Чтобы предоставить новым участникам доступ к проекту, добавьте их в команду ниже. С помощью команд вы можете управлять доступом к проектам.",
"member_role_description": "Участники могут работать в выбранных рабочих пространствах.",
"member_role_info_message": "Чтобы предоставить новым участникам доступ к рабочему пространству, добавьте их в команду ниже. С помощью команд ты можешь управлять тем, кто имеет доступ к какому рабочему пространству.",
"organization_role": "Роль в организации",
"owner_role_description": "Владельцы имеют полный контроль над организацией.",
"please_fill_all_member_fields": "Пожалуйста, заполните все поля для добавления нового участника.",
@@ -2708,8 +2728,8 @@
"team_settings_description": "Управляйте участниками команды, правами доступа и другими параметрами.",
"team_updated_successfully": "Команда успешно обновлена",
"teams": "Команды",
"teams_description": "Распределяйте участников по командам и предоставляйте командам доступ к проектам.",
"unlock_teams_description": "Управляйте доступом участников организации к отдельным проектам и опросам.",
"teams_description": "Распределите участников по командам и предоставьте командам доступ к рабочим пространствам.",
"unlock_teams_description": "Управляйте тем, какие участники организации имеют доступ к конкретным рабочим пространствам и опросам.",
"unlock_teams_title": "Откройте доступ к командам с более высоким тарифом.",
"upgrade_plan_notice_message": "Откройте роли организации с более высоким тарифом.",
"you_are_a_member": "Вы участник"
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Изменить сообщение «Опрос закрыт»",
"adjust_survey_closed_message_description": "Измените сообщение, которое видят посетители, когда опрос закрыт.",
"adjust_the_theme_in_the": "Настройте тему в",
"ai_data_analysis_disabled": "Анализ данных с помощью ИИ отключён для этой организации.",
"ai_features_not_enabled": "Функции ИИ не включены для этой организации.",
"ai_instance_not_configured": "ИИ не настроен. Свяжись с администратором.",
"ai_smart_tools_disabled": "Умные инструменты ИИ отключены для этой организации.",
"ai_translate": "Перевести с помощью ИИ",
"ai_translating": "Перевод с помощью ИИ... Пожалуйста, не закрывай это окно.",
"ai_translation_all_fields_populated": "Все поля уже переведены",
"ai_translation_complete": "Перевод с помощью ИИ завершён",
"ai_translation_failed": "Перевод не удался",
"ai_translation_instance_not_configured": "ИИ не настроен на этом экземпляре. Свяжись с администратором.",
"ai_translation_not_available": "Перевод с помощью ИИ недоступен на твоем текущем тарифе. Переходи на более высокий план, чтобы разблокировать эту функцию.",
"ai_translation_not_enabled": "Интеллектуальные инструменты ИИ отключены для этой организации. Включи их в настройках организации.",
"all_are_true": "все условия выполняются",
"all_other_answers_will_continue_to": "Все остальные ответы будут продолжать",
"allow_multi_select": "Разрешить множественный выбор",
@@ -3043,7 +3075,7 @@
"options_used_in_logic_bulk_error": "Следующие варианты используются в логике: {questionIndexes}. Пожалуйста, сначала удалите их из логики.",
"override_theme_with_individual_styles_for_this_survey": "Переопределить тему индивидуальными стилями для этого опроса.",
"overwrite_global_waiting_time": "Установить свой период ожидания",
"overwrite_global_waiting_time_description": "Переопределить настройки проекта только для этого опроса.",
"overwrite_global_waiting_time_description": "Переопределить настройки рабочего пространства только для этого опроса.",
"overwrite_placement": "Переопределить размещение",
"overwrite_survey_logo": "Установить индивидуальный логотип опроса",
"overwrite_the_global_placement_of_the_survey": "Переопределить глобальное размещение опроса",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Посмотрите, какие команды имеют доступ к этому рабочему пространству."
},
"unify": {
"add_feedback_record": "Добавить запись отзыва",
"add_feedback_record_description": "Создайте запись обратной связи вручную.",
"add_feedback_source": "Добавить источник отзывов",
"add_source": "Добавить источник",
"allowed_values": "Допустимые значения: {values}",
"api_ingestion": "Импорт через API",
"api_ingestion_manage_api_keys": "Управление API-ключами",
"api_ingestion_settings_description": "Отправляйте записи обратной связи через Management API.",
"auto_generated": "Автоматически генерируется",
"change_file": "Изменить файл",
"click_load_sample_csv": "Нажмите «Загрузить пример CSV», чтобы увидеть столбцы",
"click_to_upload": "Кликните для загрузки",
"collected_at": "Собрано",
"configure_import": "Настроить импорт",
"configure_mapping": "Настроить сопоставление",
"connection": "Подключение",
"connector_created_successfully": "Коннектор успешно создан",
"connector_deleted_successfully": "Коннектор успешно удалён",
"connector_duplicated_successfully": "Коннектор успешно дублирован",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Импорт уже загруженных данных может создать дубликаты записей.",
"csv_inconsistent_columns": "В строке {row} несоответствие столбцов. Во всех строках должны быть одинаковые заголовки.",
"csv_max_records": "Допустимо не более {max} записей.",
"custom_source_type": "Пользовательский тип источника",
"custom_source_type_placeholder": "Введите собственный тип источника",
"default_connector_name_csv": "Импорт CSV",
"default_connector_name_formbricks": "Подключение опроса Formbricks",
"deselect_all": "Снять выделение со всех",
"discard_feedback_record_changes_description": "Ваши изменения будут потеряны, если вы закроете этот ящик.",
"discard_feedback_record_changes_title": "Отменить несохраненные изменения?",
"drop_a_field_here": "Перетащи сюда поле",
"drop_field_or": "Перетащи поле или",
"edit_csv_mapping": "Редактировать сопоставление CSV",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Не удалось загрузить отзывы",
"feedback_date": "Текущая дата",
"feedback_record_created_successfully": "Запись отзыва успешно создана",
"feedback_record_details": "Детали записи обратной связи",
"feedback_record_details_description": "Просмотрите и обновите поля записи отзыва.",
"feedback_record_directory": "Каталог записей обратной связи",
"feedback_record_fields": "Поля записи отзыва",
"feedback_record_mcp": "MCP для записей обратной связи",
"feedback_record_updated_successfully": "Запись отзыва успешно обновлена.",
"feedback_record_value_required": "Требуется значение для выбранного типа поля.",
"feedback_records": "Записи отзывов",
"feedback_records_refreshed": "Записи отзывов обновлены",
"feedback_sources": "Источники обратной связи",
"feedback_sources_directory_access_multiple": "Новые записи из этих источников будут сохранены в: {directoryNames}",
"feedback_sources_directory_access_single": "Новые записи из этого источника будут сохранены в: {directoryNames}",
"feedback_sources_settings_description": "Подключайте источники обратной связи и управляйте ими для этого рабочего пространства.",
"field_group_id": "Идентификатор группы полей",
"field_group_label": "Метка группы полей",
"field_id": "Идентификатор поля",
"field_label": "Метка поля",
"field_type": "Тип поля",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Каталог обратной связи нельзя изменить после создания.",
"go_to_feedback_record_directories": "Перейти к настройкам каталогов",
"historical_import_complete": "Импорт завершён: {successes} успешно, {failures} с ошибками, {skipped} пропущено (нет данных)",
"import_csv_data": "Импортировать отзывы",
"import_feedback": "Импортировать отзывы",
"import_historical_responses": "Импортировать предыдущие ответы",
"import_historical_responses_description": "Импортируйте существующие ответы из этого опроса прямо сейчас.",
"import_rows": "Импортировать {count, plural, one {# строку} few {# строки} many {# строк} other {# строки}}",
"import_via_source_name": "Импорт через «{sourceName}»",
"importing_data": "Импорт данных...",
"importing_historical_data": "Импорт исторических данных...",
"invalid_enum_values": "Недопустимые значения в столбце, сопоставленном с {field}",
"invalid_values_found": "Найдено: {values} (строки: {rows}) {extra}",
"load_sample_csv": "Загрузить пример CSV",
"n_supported_questions": "Поддерживается {count} вопрос(ов)",
"manage_directories": "Управление директориями",
"manage_feedback_sources": "Управление источниками обратной связи",
"metadata": "Метаданные",
"metadata_key": "Ключ метаданных",
"metadata_read_only_entries": "Значения метаданных только для чтения (нестроковые)",
"metadata_value": "Значение метаданных",
"missing_feedback_source_title": "Не нашли нужный источник обратной связи?",
"no_feedback_record_directory_available": "К этому рабочему пространству не назначен каталог записей обратной связи. Сначала создайте или назначьте каталог.",
"no_feedback_records": "Пока нет записей отзывов. Они появятся здесь, когда коннекторы начнут отправлять данные.",
"no_source_fields_loaded": "Поля источника ещё не загружены",
"no_sources_connected": "Нет подключённых источников. Добавьте источник, чтобы начать.",
"no_surveys_found": "В этой среде не найдено опросов",
"optional": "Необязательно",
"or_drag_and_drop": "или перетащите файл",
"question_selected": "<strong>{count}</strong> выбранный вопрос. Каждый ответ на эти вопросы создаст новую запись обратной связи.",
"question_type_not_supported": "Этот тип вопроса не поддерживается",
"questions_selected": "<strong>{count}</strong> выбранных вопроса. Каждый ответ на эти вопросы создаст новую запись обратной связи.",
"records_will_go_to": "Записи будут отправлены в",
"refresh_feedback_records": "Обновить записи отзывов",
"refreshing_feedback_records": "Обновляем записи отзывов...",
"request_feedback_source": "Запросить интеграцию источника",
"required": "Обязательно",
"save_changes": "Сохранить изменения",
"select_a_survey_to_see_questions": "Выберите опрос, чтобы увидеть его вопросы",
"select_a_value": "Выберите значение...",
"select_all": "Выбрать все",
"select_feedback_record_directory": "Выберите каталог",
"select_feedback_record_source_type": "Выберите тип источника",
"select_questions": "Выберите вопросы",
"select_source_type_description": "Выберите тип источника отзывов, который хотите подключить.",
"select_source_type_prompt": "Выберите тип источника отзывов, который хотите подключить:",
"select_survey": "Выбрать опрос",
"select_survey_and_questions": "Выбрать опрос и вопросы",
"select_survey_questions_description": "Выберите, какие вопросы опроса должны создавать FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Показано 3 из {count} строк",
"source": "источник",
"source_connect_csv_description": "Импортировать отзывы из CSV-файлов",
"source_connect_feedback_record_mcp_description": "Отправляйте записи обратной связи через интеграцию MCP.",
"source_connect_formbricks_description": "Подключить отзывы из ваших опросов Formbricks",
"source_fields": "Поля источника",
"source_id": "Идентификатор источника",
"source_name": "Имя источника",
"source_type": "Тип источника",
"source_type_cannot_be_changed": "Тип источника нельзя изменить",
"sources": "Источники",
"status_active": "В процессе",
"status_completed": "Завершён",
"status_draft": "Черновик",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Ошибка",
"status_paused": "Приостановлен",
"status_live_sync": "Синхронизация в реальном времени",
"status_ready": "Готово",
"submission_id": "Идентификатор отправки",
"survey_has_no_questions": "В этом опросе нет вопросов",
"survey_import_line": "{surveyName}: {responseCount} ответов × {questionCount} вопросов = {total} записей обратной связи",
"total_feedback_records": "Всего: выбрано {checked} из {total} записей обратной связи в {surveyCount} опросах",
"topics_and_subtopics": "Темы и подтемы",
"unify_feedback": "Обратная связь Unify",
"update_mapping_description": "Обнови настройки сопоставления для этого источника.",
"updated_at": "Обновлено",
"upload_csv_data_description": "Загрузи CSV-файл, чтобы импортировать данные отзывов.",
"upload_csv_file": "Загрузить CSV-файл",
"user_identifier": "Пользователь",
"value": "Значение"
"value": "Значение",
"value_boolean": "Значение (логическое)",
"value_date": "Значение (Дата)",
"value_number": "Значение (число)",
"value_text": "Значение (текст)"
},
"xm-templates": {
"ces": "CES",
+114 -46
View File
@@ -125,6 +125,7 @@
"activity": "Aktivitet",
"add": "Lägg till",
"add_action": "Lägg till åtgärd",
"add_chart": "Lägg till diagram",
"add_charts": "Lägg till diagram",
"add_existing_chart_description": "Sök och välj diagram att lägga till i den här instrumentpanelen.",
"add_filter": "Lägg till filter",
@@ -147,6 +148,7 @@
"apply_filters": "Tillämpa filter",
"archived": "Arkiverad",
"are_you_sure": "Är du säker?",
"ask": "Ask",
"attributes": "Attribut",
"back": "Tillbaka",
"billing": "Fakturering",
@@ -159,7 +161,7 @@
"change_workspace": "Byt arbetsyta",
"chart": "Diagram",
"charts": "Diagram",
"choice_n": "Val {{n}}",
"choice_n": "Val {n}",
"choices": "Val",
"choose_organization": "Välj organisation",
"choose_workspace": "Välj arbetsyta",
@@ -172,7 +174,7 @@
"close": "Stäng",
"code": "Kod",
"collapse_rows": "Dölj rader",
"column_n": "Kolumn {{n}}",
"column_n": "Kolumn {n}",
"completed": "Slutförd",
"configuration": "Konfigurera",
"confirm": "Bekräfta",
@@ -212,6 +214,7 @@
"delete_what": "Ta bort {deleteWhat}",
"description": "Beskrivning",
"disable": "Inaktivera",
"disabled": "Inaktiverad",
"disallow": "Tillåt inte",
"discard": "Förkasta",
"dismissed": "Avvisad",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Misslyckades att ladda organisationer",
"failed_to_load_workspaces": "Det gick inte att ladda arbetsytor",
"failed_to_parse_csv": "Det gick inte att tolka CSV-filen",
"field_placeholder": "Platshållare för {{field}}",
"field_placeholder": "Platshållare för {field}",
"filter": "Filter",
"finish": "Slutför",
"first_name": "Förnamn",
@@ -331,6 +334,7 @@
"not_authenticated": "Du är inte autentiserad för att utföra denna åtgärd.",
"not_authorized": "Ej behörig",
"not_connected": "Ej ansluten",
"not_set": "Inte inställt",
"note": "Anteckning",
"notifications": "Aviseringar",
"number": "Nummer",
@@ -390,13 +394,14 @@
"report_survey": "Rapportera enkät",
"request_trial_license": "Begär provlicens",
"reset_to_default": "Återställ till standard",
"resize": "Ändra storlek",
"response": "Svar",
"response_id": "Svar-ID",
"responses": "Svar",
"restart": "Starta om",
"retry": "Försök igen",
"role": "Roll",
"row_n": "Rad {{n}}",
"row_n": "Rad {n}",
"saas": "SaaS",
"sales": "Försäljning",
"save": "Spara",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Några filer misslyckades att laddas upp",
"something_went_wrong": "Något gick fel",
"something_went_wrong_please_try_again": "Något gick fel. Försök igen.",
"soon": "Snart",
"sort_by": "Sortera efter",
"start_free_trial": "Starta gratis provperiod",
"status": "Status",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Stapeldiagram",
"chart_type_big_number": "Stort tal",
"chart_type_line": "Linjediagram",
"chart_type_not_supported": "Diagramtypen \"{{chartType}}\" stöds inte än",
"chart_type_not_supported": "Diagramtypen \"{chartType}\" stöds inte ännu",
"chart_type_pie": "Cirkeldiagram",
"chart_updated_successfully": "Diagram uppdaterat!",
"configure_description": "Ändra diagramtyp och andra inställningar för den här visualiseringen.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Det gick inte att köra frågan",
"failed_to_load_chart": "Det gick inte att ladda diagrammet",
"failed_to_load_chart_data": "Det gick inte att ladda diagramdata",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Det gick inte att spara diagrammet",
"field": "Fält",
"field_label_average_score": "Genomsnittligt betyg",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Ingen giltig data att visa",
"not_contains": "innehåller inte",
"not_equals": "är inte lika med",
"open_chart": "Öppna diagram {{name}}",
"open_chart": "Öppna diagram {name}",
"open_options": "Öppna diagramalternativ",
"or_filter_logic": "ELLER",
"original": "Original",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Välj en instrumentpanel",
"predefined_measures": "Fördefinierade mått",
"preset": "Förinställning",
"preview_chart": "Förhandsgranska diagram",
"query_executed_successfully": "Frågan kördes utan problem",
"reset_to_ai_suggestion": "Återställ till AI-förslag",
"save_and_add_to_dashboard": "Spara och lägg till i instrumentpanelen",
"save_chart": "Spara diagram",
"save_chart_dialog_title": "Spara diagram",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "Välj fält",
"select_measures": "Välj mått...",
"select_preset": "Välj förinställning",
"showing_first_n_of": "Visar de första {{n}} av {{count}} raderna",
"showing_first_n_of": "Visar de första {n} av {count} raderna",
"start_date": "Startdatum",
"time_dimension": "Tidsdimension",
"time_dimension_title": "Lägg till tidsbaserad gruppering",
"time_dimension_toggle_description": "Övervaka trender över tid."
"time_dimension_toggle_description": "Övervaka trender över tid.",
"update_chart": "Uppdatera diagram"
},
"dashboards": {
"add_count_charts": "Lägg till {count} diagram",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Skapa instrumentpanel",
"create_dashboard_description": "Ange ett namn för din nya instrumentpanel.",
"create_failed": "Det gick inte att skapa instrumentpanelen",
"create_new_chart": "Skapa nytt diagram",
"create_success": "Instrumentpanelen har skapats!",
"dashboard": "Instrumentpanel",
"dashboard_delete_confirmation": "Är du säker på att du vill ta bort den här instrumentpanelen? Den här åtgärden kan inte ångras.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Det gick inte att duplicera instrumentpanelen",
"duplicate_success": "Instrumentpanelen har duplicerats!",
"failed_to_load_chart_data": "Det gick inte att ladda diagramdata",
"no_charts_available_description": "Det finns inga diagram som kan läggas till på den här instrumentpanelen. Antingen finns inga diagram än, eller så har alla befintliga diagram redan lagts till. Gå till sidan Diagram för att skapa nya diagram.",
"no_charts_to_add_message": "Inga diagram att lägga till på den här instrumentpanelen.",
"no_dashboards_found": "Inga instrumentpaneler hittades.",
"no_data_message": "Ingen data. Det finns för närvarande ingen information att visa. Lägg till diagram för att bygga din instrumentpanel.",
"please_enter_name": "Ange ett namn på instrumentpanelen"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Du har inga feedbackposter att rapportera om. Ställ in återkopplingskällor för att mata in data i systemet.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Ställ in feedbackkällor"
},
"api_keys": {
"add_api_key": "Lägg till API-nyckel",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Anslut din app eller webbplats till Formbricks.",
"cache_update_delay_description": "När du gör uppdateringar av undersökningar, kontakter, åtgärder eller annan data kan det ta upp till 1 minut innan ändringarna syns i din lokala app som kör Formbricks SDK.",
"cache_update_delay_title": "Ändringar syns efter cirka 1 minut på grund av cachelagring",
"environment_id_legacy": "Miljö-ID (äldre version)",
"environment_id_legacy_alert": "Din befintliga SDK-konfiguration kan fortfarande använda ett äldre miljö-ID.",
"environment_id_legacy_alert_link": "Läs mer om varför och hur du migrerar.",
"formbricks_sdk_connected": "Formbricks SDK är anslutet",
"formbricks_sdk_not_connected": "Formbricks SDK är ännu inte anslutet.",
"formbricks_sdk_not_connected_description": "Lägg till Formbricks SDK på din webbplats eller i din app för att ansluta den till Formbricks",
@@ -2389,7 +2405,7 @@
"most_popular": "Mest populär",
"pending_change_removed": "Schemalagd abonnemangsändring har tagits bort.",
"pending_plan_badge": "Schemalagd",
"pending_plan_change_description": "Ditt abonnemang kommer att ändras till {{plan}} den {{date}}.",
"pending_plan_change_description": "Din plan kommer att byta till {plan} den {date}.",
"pending_plan_change_title": "Schemalagd abonnemangsändring",
"pending_plan_cta": "Schemalagd",
"per_month": "per månad",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "En katalog för återkopplingsregister med detta namn finns redan.",
"error_directory_name_required": "Katalognamn krävs.",
"error_directory_workspaces_invalid_org": "Vissa angivna arbetsytor tillhör inte denna organisation.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Feedbackkataloger",
"no_access": "Du har inte behörighet att hantera kataloger för feedbackposter.",
"no_connectors": "Inga kopplingar länkade till den här katalogen ännu.",
"pause_connectors_confirmation_description": "Om du pausar dessa kopplingar kommer inga nya poster att läggas till.",
"pause_connectors_confirmation_title": "Pausa länkade kopplingar?",
"select_workspaces_placeholder": "Välj arbetsytor...",
"show_archived": "Visa arkiverade",
"title": "Feedbackkataloger",
"unarchive": "Avarkivera"
"unarchive": "Avarkivera",
"unarchive_workspace_conflict": "Den här katalogen kan inte avarkiveras eftersom en eller flera tilldelade arbetsytor är arkiverade.",
"workspace_access": "Arbetsyteåtkomst"
},
"general": {
"ai_data_analysis_disabled_for_organization": "AI-baserad dataanalys och databerikning är inaktiverad för den här organisationen.",
"ai_data_analysis_enabled": "Dataförbättring & analys (AI)",
"ai_data_analysis_enabled_description": "AI för att få ut mer av din data, skapa dashboards, diagram, rapporter och mer. Använder din upplevelsedata.",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "Hantera AI-drivna funktioner för den här organisationen.",
"ai_features_not_enabled_for_organization": "AI-funktioner är inte aktiverade för den här organisationen.",
"ai_instance_not_configured": "AI konfigureras på instansnivå via miljövariabler. Be din administratör att ange AI_PROVIDER, autentiseringsuppgifterna för den leverantören och den tillhörande modellistan innan AI-funktioner aktiveras.",
"ai_settings_updated_successfully": "AI-inställningarna har uppdaterats",
"ai_smart_tools_disabled_for_organization": "AI-smartverktyg är inaktiverade för den här organisationen.",
"ai_smart_tools_enabled": "Smarta funktioner (AI)",
"ai_smart_tools_enabled_description": "AI som hjälper dig att göra mer på kortare tid. Rör aldrig data som samlats in med Formbricks. Används bara till t.ex. att översätta enkäter till andra språk.",
"bulk_invite_warning_description": "På gratisplanen tilldelas alla organisationsmedlemmar alltid rollen \"Ägare\".",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "Du kan inte lämna denna organisation eftersom det är din enda organisation. Skapa en ny organisation först.",
"copy_invite_link_to_clipboard": "Kopiera inbjudningslänk till urklipp",
"create_new_organization": "Skapa ny organisation",
"create_new_organization_description": "Skapa en ny organisation för att hantera en annan uppsättning projekt.",
"create_new_organization_description": "Skapa en ny organisation för att hantera en annan uppsättning arbetsytor.",
"customize_email_with_a_higher_plan": "Anpassa e-post med en högre plan",
"delete_member_confirmation": "Borttagna medlemmar förlorar åtkomst till alla projekt och enkäter i din organisation.",
"delete_member_confirmation": "Borttagna medlemmar förlorar åtkomst till alla arbetsytor och undersökningar i din organisation.",
"delete_organization": "Ta bort organisation",
"delete_organization_description": "Ta bort organisation med alla dess projekt inklusive alla enkäter, svar, personer, åtgärder och attribut",
"delete_organization_description": "Ta bort organisation med alla dess arbetsytor inklusive alla undersökningar, svar, personer, åtgärder och attribut",
"delete_organization_warning": "Innan du fortsätter med att ta bort denna organisation, var medveten om följande konsekvenser:",
"delete_organization_warning_1": "Permanent borttagning av alla projekt kopplade till denna organisation.",
"delete_organization_warning_1": "Permanent borttagning av alla arbetsytor kopplade till denna organisation.",
"delete_organization_warning_2": "Denna åtgärd kan inte ångras. När det är borta, är det borta.",
"delete_organization_warning_3": "Vänligen ange {organizationName} i följande fält för att bekräfta den definitiva borttagningen av denna organisation:",
"eliminate_branding_with_whitelabel": "Eliminera Formbricks-varumärke och aktivera ytterligare white-label-anpassningsalternativ.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Registrera dig här.",
"share_invite_link": "Dela inbjudningslänk",
"share_this_link_to_let_your_organization_member_join_your_organization": "Dela denna länk för att låta din organisationsmedlem gå med i din organisation:",
"test_email_sent_successfully": "Test-e-post skickat"
"test_email_sent_successfully": "Test-e-post skickat",
"unlock_ai_features_description": "AI-drivna översättningar, smarta verktyg och dataanalys finns tillgängliga på högre planer. Uppgradera för att ladda dina undersökningar med AI-kraft.",
"unlock_ai_features_with_a_higher_plan": "Lås upp AI-funktioner med en högre plan"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Prenumerera automatiskt på nya enkäter",
@@ -2672,7 +2692,7 @@
"add_workspaces_description": "Styr vilka arbetsytor teammedlemmarna kan komma åt.",
"all_members_added": "Alla medlemmar tillagda i detta team.",
"all_workspaces_added": "Alla arbetsytor har lagts till i detta team.",
"are_you_sure_you_want_to_delete_this_team": "Är du säker på att du vill ta bort detta team? Detta tar även bort åtkomsten till alla projekt och enkäter kopplade till detta team.",
"are_you_sure_you_want_to_delete_this_team": "Är du säker på att du vill ta bort det här teamet? Detta tar även bort åtkomsten till alla arbetsytor och undersökningar som är kopplade till teamet.",
"billing_role_description": "Har endast åtkomst till faktureringsinformation.",
"bulk_invite": "Massinbjudning",
"contributor": "Bidragsgivare",
@@ -2688,10 +2708,10 @@
"manage": "Hantera",
"manage_team": "Hantera team",
"manage_team_disabled": "Endast organisationsägare, administratörer och teamadministratörer kan hantera team.",
"manager_role_description": "Administratörer kan komma åt alla projekt och lägga till och ta bort medlemmar.",
"manager_role_description": "Administratörer kan komma åt alla arbetsytor och lägga till samt ta bort medlemmar.",
"member": "Medlem",
"member_role_description": "Medlemmar kan arbeta i valda projekt.",
"member_role_info_message": "För att ge nya medlemmar åtkomst till ett projekt, vänligen lägg till dem i ett team nedan. Med team kan du hantera vem som har åtkomst till vilket projekt.",
"member_role_description": "Medlemmar kan arbeta i utvalda arbetsytor.",
"member_role_info_message": "För att ge nya medlemmar åtkomst till en arbetsyta, lägg till dem i ett team nedan. Med team kan du hantera vem som har åtkomst till vilken arbetsyta.",
"organization_role": "Organisationsroll",
"owner_role_description": "Ägare har full kontroll över organisationen.",
"please_fill_all_member_fields": "Vänligen fyll i alla fält för att lägga till en ny medlem.",
@@ -2708,8 +2728,8 @@
"team_settings_description": "Hantera teammedlemmar, åtkomsträttigheter och mer.",
"team_updated_successfully": "Team uppdaterat",
"teams": "Team",
"teams_description": "Tilldela medlemmar till team och ge team åtkomst till projekt.",
"unlock_teams_description": "Hantera vilka organisationsmedlemmar som har åtkomst till specifika projekt och enkäter.",
"teams_description": "Tilldela medlemmar till team och ge team åtkomst till arbetsytor.",
"unlock_teams_description": "Hantera vilka organisationsmedlemmar som har åtkomst till specifika arbetsytor och undersökningar.",
"unlock_teams_title": "Lås upp team med en högre plan.",
"upgrade_plan_notice_message": "Lås upp organisationsroller med en högre plan.",
"you_are_a_member": "Du är medlem"
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "Justera meddelande för 'Enkät stängd'",
"adjust_survey_closed_message_description": "Ändra meddelandet besökare ser när enkäten är stängd.",
"adjust_the_theme_in_the": "Justera temat i",
"ai_data_analysis_disabled": "AI-dataanalys är inaktiverad för den här organisationen.",
"ai_features_not_enabled": "AI-funktioner är inte aktiverade för den här organisationen.",
"ai_instance_not_configured": "AI är inte konfigurerad. Kontakta din administratör.",
"ai_smart_tools_disabled": "AI smarta verktyg är inaktiverade för den här organisationen.",
"ai_translate": "Översätt med AI",
"ai_translating": "Översätter med AI... Vänligen håll denna dialogruta öppen.",
"ai_translation_all_fields_populated": "Alla fält är redan översatta",
"ai_translation_complete": "AI-översättning klar",
"ai_translation_failed": "Översättningen misslyckades",
"ai_translation_instance_not_configured": "AI är inte konfigurerad på den här instansen. Kontakta din administratör.",
"ai_translation_not_available": "AI-översättning är inte tillgänglig på din nuvarande plan. Uppgradera för att låsa upp denna funktion.",
"ai_translation_not_enabled": "AI-smarta verktyg är inaktiverade för den här organisationen. Aktivera dem i organisationsinställningarna.",
"all_are_true": "alla är sanna",
"all_other_answers_will_continue_to": "Alla andra svar fortsätter till",
"allow_multi_select": "Tillåt flerval",
@@ -3043,7 +3075,7 @@
"options_used_in_logic_bulk_error": "Följande alternativ används i logiken: {questionIndexes}. Vänligen ta bort dem från logiken först.",
"override_theme_with_individual_styles_for_this_survey": "Åsidosätt temat med individuella stilar för denna enkät.",
"overwrite_global_waiting_time": "Ange anpassad väntetid",
"overwrite_global_waiting_time_description": "Åsidosätt projektkonfigurationen endast för denna enkät.",
"overwrite_global_waiting_time_description": "Åsidosätt arbetsytans konfiguration för enbart denna undersökning.",
"overwrite_placement": "Åsidosätt placering",
"overwrite_survey_logo": "Ange anpassad logotyp för undersökningen",
"overwrite_the_global_placement_of_the_survey": "Åsidosätt den globala placeringen av enkäten",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Se vilka team som har tillgång till denna arbetsyta."
},
"unify": {
"add_feedback_record": "Lägg till feedbackpost",
"add_feedback_record_description": "Skapa en feedbackpost manuellt.",
"add_feedback_source": "Lägg till feedbackkälla",
"add_source": "Lägg till källa",
"allowed_values": "Tillåtna värden: {values}",
"api_ingestion": "API ingestion",
"api_ingestion_manage_api_keys": "Manage API keys",
"api_ingestion_settings_description": "Send feedback records using the Management API.",
"auto_generated": "Automatiskt genererad",
"change_file": "Byt fil",
"click_load_sample_csv": "Klicka på 'Ladda exempel-CSV' för att se kolumner",
"click_to_upload": "Klicka för att ladda upp",
"collected_at": "Insamlad",
"configure_import": "Konfigurera import",
"configure_mapping": "Konfigurera mappning",
"connection": "Anslutning",
"connector_created_successfully": "Kopplingen skapades",
"connector_deleted_successfully": "Kopplingen togs bort",
"connector_duplicated_successfully": "Kopplingen har duplicerats",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Om du importerar data två gånger kommer det att skapa dubbletter.",
"csv_inconsistent_columns": "Rad {row} har inkonsekventa kolumner. Alla rader måste ha samma rubriker.",
"csv_max_records": "Maximalt {max} poster tillåtna.",
"custom_source_type": "Anpassad källtyp",
"custom_source_type_placeholder": "Ange anpassad källtyp",
"default_connector_name_csv": "CSV-import",
"default_connector_name_formbricks": "Formbricks Survey-anslutning",
"deselect_all": "Avmarkera alla",
"discard_feedback_record_changes_description": "Dina ändringar kommer att gå förlorade om du stänger den här lådan.",
"discard_feedback_record_changes_title": "Vill du ignorera osparade ändringar?",
"drop_a_field_here": "Släpp ett fält här",
"drop_field_or": "Släpp fält eller",
"edit_csv_mapping": "Redigera CSV-mappning",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Det gick inte att ladda feedbackposter",
"feedback_date": "Aktuellt datum",
"feedback_record_created_successfully": "Feedbackposten har skapats",
"feedback_record_details": "Feedbackpostdetaljer",
"feedback_record_details_description": "Granska och uppdatera fält för feedbackposter.",
"feedback_record_directory": "Katalog för feedbackposter",
"feedback_record_fields": "Fält för feedbackpost",
"feedback_record_mcp": "Feedback Record MCP",
"feedback_record_updated_successfully": "Feedbackposten har uppdaterats",
"feedback_record_value_required": "Ett värde krävs för den valda fälttypen",
"feedback_records": "Feedbackposter",
"feedback_records_refreshed": "Feedbackposter har uppdaterats",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
"field_group_id": "Fältgrupp-ID",
"field_group_label": "Fältgruppsetikett",
"field_id": "Fält-ID",
"field_label": "Fältetikett",
"field_type": "Fälttyp",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedbackkatalog kan inte ändras efter att den skapats.",
"go_to_feedback_record_directories": "Gå till kataloginställningar",
"historical_import_complete": "Importen klar: {successes} lyckades, {failures} misslyckades, {skipped} hoppades över (ingen data)",
"import_csv_data": "Importera feedback",
"import_feedback": "Importera feedback",
"import_historical_responses": "Import historical responses",
"import_historical_responses_description": "Import existing responses from this survey now.",
"import_rows": "Importera {count} rader",
"import_via_source_name": "Importera via \"{sourceName}\"",
"importing_data": "Importerar data...",
"importing_historical_data": "Importerar historisk data...",
"invalid_enum_values": "Ogiltiga värden i kolumnen som är kopplad till {field}",
"invalid_values_found": "Hittade: {values} (rader: {rows}) {extra}",
"load_sample_csv": "Ladda exempel-CSV",
"n_supported_questions": "{count} stödda frågor",
"manage_directories": "Manage directories",
"manage_feedback_sources": "Hantera feedbackkällor",
"metadata": "Metadata",
"metadata_key": "Metadatanyckel",
"metadata_read_only_entries": "Skrivskyddade metadatavärden (icke-sträng)",
"metadata_value": "Metadatavärde",
"missing_feedback_source_title": "Missing feedback source?",
"no_feedback_record_directory_available": "Ingen katalog för feedbackposter tilldelad till den här arbetsytan. Skapa eller tilldela en först.",
"no_feedback_records": "Inga feedbackposter ännu. Poster visas här när dina connectors börjar skicka data.",
"no_source_fields_loaded": "Inga källfält har laddats än",
"no_sources_connected": "Inga källor är anslutna än. Lägg till en källa för att komma igång.",
"no_surveys_found": "Inga enkäter hittades i denna miljö",
"optional": "Valfritt",
"or_drag_and_drop": "eller dra och släpp",
"question_selected": "<strong>{count}</strong> fråga vald. Varje svar på dessa frågor skapar en ny feedbackpost.",
"question_type_not_supported": "Den här frågetypen stöds inte",
"questions_selected": "<strong>{count}</strong> frågor valda. Varje svar på dessa frågor skapar en ny feedbackpost.",
"records_will_go_to": "Poster kommer att hamna i",
"refresh_feedback_records": "Uppdatera feedbackposter",
"refreshing_feedback_records": "Uppdaterar feedbackposter...",
"request_feedback_source": "Request source integration",
"required": "Obligatoriskt",
"save_changes": "Spara ändringar",
"select_a_survey_to_see_questions": "Välj en enkät för att se dess frågor",
"select_a_value": "Välj ett värde...",
"select_all": "Välj alla",
"select_feedback_record_directory": "Välj en katalog",
"select_feedback_record_source_type": "Välj källtyp",
"select_questions": "Välj frågor",
"select_source_type_description": "Välj vilken typ av feedbackkälla du vill ansluta.",
"select_source_type_prompt": "Välj vilken typ av feedbackkälla du vill ansluta:",
"select_survey": "Välj enkät",
"select_survey_and_questions": "Välj enkät & frågor",
"select_survey_questions_description": "Välj vilka enkätfrågor som ska skapa FeedbackRecords.",
@@ -3692,27 +3749,38 @@
"showing_rows": "Visar 3 av {count} rader",
"source": "källa",
"source_connect_csv_description": "Importera feedback från CSV-filer",
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
"source_connect_formbricks_description": "Anslut feedback från dina Formbricks-enkäter",
"source_fields": "Källfält",
"source_id": "Käll-ID",
"source_name": "Källnamn",
"source_type": "Källtyp",
"source_type_cannot_be_changed": "Källtyp kan inte ändras",
"sources": "Källor",
"status_active": "Pågående",
"status_completed": "Slutförd",
"status_draft": "Utkast",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Fel",
"status_paused": "Pausad",
"status_live_sync": "Live sync",
"status_ready": "Ready",
"submission_id": "Inlämnings-ID",
"survey_has_no_questions": "Den här enkäten har inga frågor",
"survey_import_line": "{surveyName}: {responseCount} svar × {questionCount} frågor = {total} feedbackposter",
"total_feedback_records": "Totalt: {checked} av {total} feedbackposter valda i {surveyCount} enkäter",
"topics_and_subtopics": "Ämnen och delämnen",
"unify_feedback": "Samla feedback",
"update_mapping_description": "Uppdatera mappningskonfigurationen för den här källan.",
"updated_at": "Uppdaterad",
"upload_csv_data_description": "Ladda upp en CSV-fil för att importera feedbackdata.",
"upload_csv_file": "Ladda upp CSV-fil",
"user_identifier": "Användare",
"value": "Värde"
"value": "Värde",
"value_boolean": "Värde (booleskt)",
"value_date": "Värde (datum)",
"value_number": "Värde (antal)",
"value_text": "Värde (text)"
},
"xm-templates": {
"ces": "CES",
+161 -93
View File
@@ -125,6 +125,7 @@
"activity": "Etkinlik",
"add": "Ekle",
"add_action": "Eylem ekle",
"add_chart": "Grafik ekle",
"add_charts": "Grafik ekle",
"add_existing_chart_description": "Bu panoya eklemek için grafikleri ara ve seç.",
"add_filter": "Filtre ekle",
@@ -147,6 +148,7 @@
"apply_filters": "Filtreleri uygula",
"archived": "Arşivlenmiş",
"are_you_sure": "Emin misiniz?",
"ask": "Ask",
"attributes": "Öznitelikler",
"back": "Geri",
"billing": "Faturalandırma",
@@ -159,7 +161,7 @@
"change_workspace": "Çalışma alanını değiştir",
"chart": "Grafik",
"charts": "Grafikler",
"choice_n": "Seçenek {{n}}",
"choice_n": "Seçenek {n}",
"choices": "Seçenekler",
"choose_organization": "Organizasyon seç",
"choose_workspace": "Çalışma alanı seç",
@@ -172,7 +174,7 @@
"close": "Kapat",
"code": "Kod",
"collapse_rows": "Satırları daralt",
"column_n": "Sütun {{n}}",
"column_n": "Sütun {n}",
"completed": "Tamamlandı",
"configuration": "Yapılandır",
"confirm": "Onayla",
@@ -212,6 +214,7 @@
"delete_what": "{deleteWhat} sil",
"description": "Açıklama",
"disable": "Devre dışı bırak",
"disabled": "Devre Dışı",
"disallow": "İzin verme",
"discard": "İptal et",
"dismissed": "Reddedildi",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "Organizasyonlar yüklenemedi",
"failed_to_load_workspaces": "Çalışma alanları yüklenemedi",
"failed_to_parse_csv": "CSV ayrıştırılamadı",
"field_placeholder": "{{field}} Yer Tutucu",
"field_placeholder": "{field} Yer Tutucu",
"filter": "Filtre",
"finish": "Bitir",
"first_name": "Ad",
@@ -331,6 +334,7 @@
"not_authenticated": "Bu işlemi gerçekleştirmek için yetkiniz yok.",
"not_authorized": "Yetkisiz",
"not_connected": "Bağlı Değil",
"not_set": "Ayarlanmadı",
"note": "Not",
"notifications": "Bildirimler",
"number": "Sayı",
@@ -390,13 +394,14 @@
"report_survey": "Anketi Raporla",
"request_trial_license": "Deneme lisansı iste",
"reset_to_default": "Varsayılana sıfırla",
"resize": "Yeniden boyutlandır",
"response": "Yanıt",
"response_id": "Yanıt ID",
"responses": "Yanıtlar",
"restart": "Yeniden başlat",
"retry": "Yeniden dene",
"role": "Rol",
"row_n": "Satır {{n}}",
"row_n": "Satır {n}",
"saas": "SaaS",
"sales": "Satış",
"save": "Kaydet",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "Bazı dosyalar yüklenemedi",
"something_went_wrong": "Bir şeyler ters gitti",
"something_went_wrong_please_try_again": "Bir sorun oluştu. Lütfen tekrar deneyin.",
"soon": "Yakında",
"sort_by": "Sıralama",
"start_free_trial": "Ücretsiz denemeyi başlat",
"status": "Durum",
@@ -512,7 +518,7 @@
"yes": "Evet",
"you_are_downgraded_to_the_community_edition": "Topluluk Sürümüne düşürüldünüz.",
"you_are_not_authorized_to_perform_this_action": "Bu işlemi gerçekleştirme yetkiniz yok.",
"you_have_reached_your_limit_of_workspace_limit": "{projectLimit} çalışma alanı sınırınıza ulaştınız.",
"you_have_reached_your_limit_of_workspace_limit": "{workspaceLimit} çalışma alanı limitine ulaştınız.",
"you_have_reached_your_monthly_response_limit_of": "Aylık yanıt sınırınıza ulaştınız:",
"you_will_be_downgraded_to_the_community_edition_on_date": "{date} tarihinde Topluluk Sürümüne düşürüleceksiniz.",
"your_license_has_expired_please_renew": "Kurumsal lisansınızın süresi doldu. Kurumsal özellikleri kullanmaya devam etmek için lütfen yenileyin."
@@ -710,10 +716,10 @@
"book_interview": "Mülakat planla",
"build_product_roadmap_description": "Kullanıcılarınızın en çok istediği TEK şeyi belirleyin ve oluşturun.",
"build_product_roadmap_name": "Ürün Yol Haritası Oluştur",
"build_product_roadmap_question_1_headline": "$[projectName] özelliklerinden ve işlevselliğinden ne kadar memnunsunuz?",
"build_product_roadmap_question_1_headline": "$[workspaceName] ürününün özellikleri ve işlevselliğinden ne kadar memnunsunuz?",
"build_product_roadmap_question_1_lower_label": "Hiç memnun değil",
"build_product_roadmap_question_1_upper_label": "Son derece memnun",
"build_product_roadmap_question_2_headline": "$[projectName] deneyiminizi en çok iyileştirecek TEK değişiklik ne olurdu?",
"build_product_roadmap_question_2_headline": "$[workspaceName] deneyiminizi en çok geliştirebilmemiz için yapabileceğimiz TEK değişiklik nedir?",
"build_product_roadmap_question_2_placeholder": "Cevabınızı buraya yazın…",
"card_abandonment_survey": "Sepet Terk Survey'i",
"card_abandonment_survey_description": "Web mağazanızdaki sepet terk nedenlerini anlayın.",
@@ -746,10 +752,10 @@
"card_abandonment_survey_question_8_headline": "Ek yorum veya önerileriniz var mı?",
"career_development_survey_description": "Çalışanların kariyer gelişimi ve fırsatlarından memnuniyetini değerlendirin.",
"career_development_survey_name": "Kariyer Gelişimi Survey'i",
"career_development_survey_question_1_headline": "$[projectName] bünyesindeki kişisel ve mesleki gelişim fırsatlarından memnunum.",
"career_development_survey_question_1_headline": "$[workspaceName] bünyesinde kişisel ve profesyonel gelişim fırsatlarından memnunum.",
"career_development_survey_question_1_lower_label": "Kesinlikle katılmıyorum",
"career_development_survey_question_1_upper_label": "Kesinlikle katılıyorum",
"career_development_survey_question_2_headline": "$[projectName] bünyesinde bana sunulan kariyer ilerleme fırsatlarından memnunum.",
"career_development_survey_question_2_headline": "$[workspaceName] bünyesinde bana sunulan kariyer ilerleme fırsatlarından memnunum.",
"career_development_survey_question_2_lower_label": "Kesinlikle katılmıyorum",
"career_development_survey_question_2_upper_label": "Kesinlikle katılıyorum",
"career_development_survey_question_3_headline": "Organizasyonumun sunduğu işle ilgili eğitimlerden memnunum.",
@@ -779,7 +785,7 @@
"ces_lower_label": "Çok zor",
"ces_upper_label": "Çok kolay",
"cess_survey_name": "CES Survey",
"cess_survey_question_1_headline": "$[projectName] benim için [HEDEF EKLE] işlemini kolaylaştırıyor",
"cess_survey_question_1_headline": "$[workspaceName], [HEDEF EKLE] konusunda işimi kolaylaştırıyor",
"cess_survey_question_1_lower_label": "Kesinlikle katılmıyorum",
"cess_survey_question_1_upper_label": "Kesinlikle katılıyorum",
"cess_survey_question_2_headline": "Teşekkürler! [HEDEF EKLE] işlemini sizin için nasıl kolaylaştırabiliriz?",
@@ -806,7 +812,7 @@
"churn_survey_question_1_headline": "Aboneliğinizi neden iptal ettiniz?",
"churn_survey_question_1_subheader": "Ayrılmanıza üzüldük. Daha iyisini yapmamıza yardımcı olun:",
"churn_survey_question_2_button_label": "Gönder",
"churn_survey_question_2_headline": "$[projectName] kullanımını sizin için ne kolaylaştırırdı?",
"churn_survey_question_2_headline": "$[workspaceName] ürününü kullanımını kolaylaştıracak ne olabilirdi?",
"churn_survey_question_3_button_label": "Yüzde 30 indirim alın",
"churn_survey_question_3_headline": "Gelecek yıl için %30 indirim kazanın!",
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Sizi müşterimiz olarak tutmak istiyoruz. Gelecek yıl için %30 indirim sunmaktan mutluluk duyarız.</span></p>",
@@ -847,7 +853,7 @@
"csat_name": "Müşteri Memnuniyeti Puanı (CSAT)",
"csat_question_10_headline": "Başka yorumunuz, sorunuz veya endişeniz var mı?",
"csat_question_10_placeholder": "Cevabınızı buraya yazın…",
"csat_question_1_headline": "Bu $[projectName] ürününü bir arkadaşınıza veya iş arkadaşınıza tavsiye etme olasılığınız nedir?",
"csat_question_1_headline": "Bu $[workspaceName] ürününü bir arkadaşına veya meslektaşına tavsiye etme olasılığın ne kadar?",
"csat_question_1_lower_label": "Olası değil",
"csat_question_1_upper_label": "Çok olası",
"csat_question_2_choice_1": "Oldukça memnun",
@@ -855,7 +861,7 @@
"csat_question_2_choice_3": "Ne memnun ne de memnuniyetsiz",
"csat_question_2_choice_4": "Biraz memnuniyetsiz",
"csat_question_2_choice_5": "Çok memnuniyetsiz",
"csat_question_2_headline": "Genel olarak $[projectName] ürünümüzden ne kadar memnun veya memnuniyetsizsiniz?",
"csat_question_2_headline": "Genel olarak $[workspaceName] ürünümüzden ne kadar memnunsunuz veya memnun değilsiniz?",
"csat_question_2_subheader": "Lütfen birini seçin:",
"csat_question_3_choice_1": "Etkisiz",
"csat_question_3_choice_10": "Benzersiz",
@@ -867,28 +873,28 @@
"csat_question_3_choice_7": "Paraya iyi değer",
"csat_question_3_choice_8": "Düşük kalite",
"csat_question_3_choice_9": "Güvenilmez",
"csat_question_3_headline": "$[projectName] ürünümüzü tanımlamak için aşağıdaki kelimelerden hangisini kullanırsınız?",
"csat_question_3_headline": "$[workspaceName] ürünümüzü tanımlamak için aşağıdaki kelimelerden hangilerini kullanırsınız?",
"csat_question_3_subheader": "Lütfen geçerli olanların tümünü seçin:",
"csat_question_4_choice_1": "Son derece iyi",
"csat_question_4_choice_2": "Çok iyi",
"csat_question_4_choice_3": "Oldukça iyi",
"csat_question_4_choice_4": "Pek iyi değil",
"csat_question_4_choice_5": "Hiç iyi değil",
"csat_question_4_headline": "$[projectName] ürünlerimiz ihtiyaçlarınızı ne kadar karşılıyor?",
"csat_question_4_headline": "$[workspaceName] ürünümüz ihtiyaçlarınızı ne kadar karşılıyor?",
"csat_question_4_subheader": "Bir seçenek seçin:",
"csat_question_5_choice_1": "Çok yüksek kalite",
"csat_question_5_choice_2": "Yüksek kalite",
"csat_question_5_choice_3": "Düşük kalite",
"csat_question_5_choice_4": "Çok düşük kalite",
"csat_question_5_choice_5": "Ne yüksek ne düşük",
"csat_question_5_headline": "$[projectName] ürününün kalitesini nasıl değerlendirirsiniz?",
"csat_question_5_headline": "$[workspaceName] ürününün kalitesini nasıl değerlendirirsiniz?",
"csat_question_5_subheader": "Bir seçenek seçin:",
"csat_question_6_choice_1": "Mükemmel",
"csat_question_6_choice_2": "Ortalamanın üstünde",
"csat_question_6_choice_3": "Ortalama",
"csat_question_6_choice_4": "Ortalamanın altında",
"csat_question_6_choice_5": "Zayıf",
"csat_question_6_headline": "$[projectName] ürününün fiyat/performans oranını nasıl değerlendirirsiniz?",
"csat_question_6_headline": "$[workspaceName] ürününün fiyat-performans değerini nasıl değerlendirirsiniz?",
"csat_question_6_subheader": "Lütfen birini seçin:",
"csat_question_7_choice_1": "Son derece duyarlı",
"csat_question_7_choice_2": "Çok duyarlı",
@@ -902,17 +908,17 @@
"csat_question_8_choice_3": "Altı aydan bir yıla kadar",
"csat_question_8_choice_4": "1-2 yıl",
"csat_question_8_choice_5": "3 veya daha fazla yıl",
"csat_question_8_headline": "Ne kadar süredir $[projectName] müşterisisiniz?",
"csat_question_8_headline": "Ne kadar süredir $[workspaceName] müşterisisiniz?",
"csat_question_8_subheader": "Lütfen birini seçin:",
"csat_question_9_choice_1": "Son derece olası",
"csat_question_9_choice_2": "Çok olası",
"csat_question_9_choice_3": "Biraz olası",
"csat_question_9_choice_4": "Pek olası değil",
"csat_question_9_choice_5": "Hiç olası değil",
"csat_question_9_headline": "$[projectName] ürünlerimizden herhangi birini tekrar satın alma olasılığınız nedir?",
"csat_question_9_headline": "$[workspaceName] ürünlerimizden herhangi birini tekrar satın alma olasılığınız nedir?",
"csat_question_9_subheader": "Bir seçenek seçin:",
"csat_survey_name": "$[projectName] CSAT",
"csat_survey_question_1_headline": "$[projectName] deneyiminizden ne kadar memnunsunuz?",
"csat_survey_name": "$[workspaceName] CSAT",
"csat_survey_question_1_headline": "$[workspaceName] deneyiminizden ne kadar memnunsunuz?",
"csat_survey_question_1_lower_label": "Son derece memnuniyetsiz",
"csat_survey_question_1_upper_label": "Son derece memnun",
"csat_survey_question_2_headline": "Harika! Deneyiminizi iyileştirmek için yapabileceğimiz bir şey var mı?",
@@ -927,7 +933,7 @@
"custom_survey_question_1_placeholder": "Cevabınızı buraya yazın…",
"customer_effort_score_description": "Bir özelliğin kullanım kolaylığını belirleyin.",
"customer_effort_score_name": "Müşteri Çaba Puanı (CES)",
"customer_effort_score_question_1_headline": "$[projectName] benim için [HEDEF EKLE] işlemini kolaylaştırıyor",
"customer_effort_score_question_1_headline": "$[workspaceName], [HEDEF EKLE] konusunda işimi kolaylaştırıyor",
"customer_effort_score_question_1_lower_label": "Kesinlikle katılmıyorum",
"customer_effort_score_question_1_upper_label": "Kesinlikle katılıyorum",
"customer_effort_score_question_2_headline": "Teşekkürler! [HEDEF EKLE] işlemini sizin için nasıl kolaylaştırabiliriz?",
@@ -951,14 +957,14 @@
"earned_advocacy_score_name": "Kazanılmış Savunuculuk Puanı (EAS)",
"earned_advocacy_score_question_1_choice_1": "Evet",
"earned_advocacy_score_question_1_choice_2": "Hayır",
"earned_advocacy_score_question_1_headline": "$[projectName] ürününü başkalarına aktif olarak tavsiye ettiniz mi?",
"earned_advocacy_score_question_1_headline": "$[workspaceName] ürününü başkalarına aktif olarak tavsiye ettiniz mi?",
"earned_advocacy_score_question_2_headline": "Bizi neden tavsiye ettiniz?",
"earned_advocacy_score_question_2_placeholder": "Cevabınızı buraya yazın…",
"earned_advocacy_score_question_3_headline": "Çok üzücü. Neden tavsiye etmediniz?",
"earned_advocacy_score_question_3_placeholder": "Cevabınızı buraya yazın…",
"earned_advocacy_score_question_4_choice_1": "Evet",
"earned_advocacy_score_question_4_choice_2": "Hayır",
"earned_advocacy_score_question_4_headline": "Başkalarını $[projectName] tercih etmekten aktif olarak caydırdınız mı?",
"earned_advocacy_score_question_4_headline": "Başkalarını $[workspaceName] ürününü seçmekten aktif olarak caydırdınız mı?",
"earned_advocacy_score_question_5_headline": "Onları caydırmanıza ne sebep oldu?",
"earned_advocacy_score_question_5_placeholder": "Cevabınızı buraya yazın…",
"employee_satisfaction_description": "Çalışan memnuniyetini ölçün ve iyileştirme alanlarını belirleyin.",
@@ -1007,7 +1013,7 @@
"evaluate_a_product_idea_description": "Kullanıcılara ürün veya özellik fikirleri hakkında survey yapın. Hızlıca geri bildirim alın.",
"evaluate_a_product_idea_name": "Ürün Fikrini Değerlendirin",
"evaluate_a_product_idea_question_1_button_label": "Haydi başlayalım!",
"evaluate_a_product_idea_question_1_headline": "$[projectName] ürününü nasıl kullandığınızı çok beğeniyoruz! Bir özellik fikri hakkında düşüncelerinizi almak istiyoruz. Bir dakikanız var mı?",
"evaluate_a_product_idea_question_1_headline": "$[workspaceName] ürününü nasıl kullandığını çok beğeniyoruz! Bir özellik fikri hakkında fikrini almak isteriz. Bir dakikan var mı?",
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Zamanınıza saygı duyuyoruz ve kısa tuttuk 🤸</span></p>",
"evaluate_a_product_idea_question_2_headline": "Teşekkürler! Bugün [PROBLEM ALANI] konusunda ne kadar zor veya kolay?",
"evaluate_a_product_idea_question_2_lower_label": "Çok zor",
@@ -1089,13 +1095,13 @@
"identify_customer_goals_question_1_choice_2": "Üst satış fırsatlarını belirlemek",
"identify_customer_goals_question_1_choice_3": "Mümkün olan en iyi ürünü geliştirmek",
"identify_customer_goals_question_1_choice_4": "Dünyaya hükmedip herkese kahvaltıda brüksel lahanası yedirmek",
"identify_customer_goals_question_1_headline": "$[projectName] kullanmanın birincil hedefin ne?",
"identify_customer_goals_question_1_headline": "$[workspaceName] ürününü kullanmaktaki birincil hedefin nedir?",
"identify_sign_up_barriers_description": "Kayıt engelleri hakkında bilgi toplamak için indirim sunun.",
"identify_sign_up_barriers_name": "Kayıt Engellerini Belirleyin",
"identify_sign_up_barriers_question_1_button_label": "Yüzde 10 indirim alın",
"identify_sign_up_barriers_question_1_headline": "Bu kısa anketi yanıtlayın, %10 indirim kazanın!",
"identify_sign_up_barriers_question_1_html": "Kayıt olmayı düşünüyor gibisiniz. Dört soruyu yanıtlayın ve herhangi bir planda %10 indirim kazanın.",
"identify_sign_up_barriers_question_2_headline": "$[projectName] için kayıt olma olasılığınız ne kadar?",
"identify_sign_up_barriers_question_2_headline": "$[workspaceName] ürününe kaydolma olasılığın ne kadar?",
"identify_sign_up_barriers_question_2_lower_label": "Hiç olası değil",
"identify_sign_up_barriers_question_2_upper_label": "Çok olası",
"identify_sign_up_barriers_question_3_choice_1_label": "Aradığım şeye sahip olmayabilir",
@@ -1103,8 +1109,8 @@
"identify_sign_up_barriers_question_3_choice_3_label": "Karmaşık görünüyor",
"identify_sign_up_barriers_question_3_choice_4_label": "Fiyatlandırma endişe verici",
"identify_sign_up_barriers_question_3_choice_5_label": "Başka bir şey",
"identify_sign_up_barriers_question_3_headline": "$[projectName] ürününü denemenizi engelleyen nedir?",
"identify_sign_up_barriers_question_4_headline": "Neye ihtiyacınız var ama $[projectName] sunmuyor?",
"identify_sign_up_barriers_question_3_headline": "$[workspaceName] ürününü denemekten seni alıkoyan nedir?",
"identify_sign_up_barriers_question_4_headline": "İhtiyacın olup da $[workspaceName] ürününün sunmadığı nedir?",
"identify_sign_up_barriers_question_4_placeholder": "Cevabınızı buraya yazın…",
"identify_sign_up_barriers_question_5_headline": "Hangi seçenekleri değerlendiriyorsunuz?",
"identify_sign_up_barriers_question_5_placeholder": "Cevabınızı buraya yazın…",
@@ -1123,7 +1129,7 @@
"identify_upsell_opportunities_question_1_choice_2": "1-2 saat",
"identify_upsell_opportunities_question_1_choice_3": "3-5 saat",
"identify_upsell_opportunities_question_1_choice_4": "5+ saat",
"identify_upsell_opportunities_question_1_headline": "Ekibiniz $[projectName] kullanarak haftada kaç saat tasarruf ediyor?",
"identify_upsell_opportunities_question_1_headline": "Ekibiniz $[workspaceName] kullanarak haftada kaç saat tasarruf ediyor?",
"improve_activation_rate_description": "Kullanıcı aktivasyonunu artırmak için başlangıç akışınızdaki zayıf noktaları belirleyin.",
"improve_activation_rate_name": "Aktivasyon Oranını İyileştirin",
"improve_activation_rate_question_1_choice_1": "Bana faydalı görünmedi",
@@ -1131,10 +1137,10 @@
"improve_activation_rate_question_1_choice_3": "Özellik/işlevsellik eksikti",
"improve_activation_rate_question_1_choice_4": "Henüz vakit bulamadım",
"improve_activation_rate_question_1_choice_5": "Başka bir şey",
"improve_activation_rate_question_1_headline": "$[projectName] kurulumunu tamamlamamanızın ana nedeni nedir?",
"improve_activation_rate_question_2_headline": "$[projectName] ürününün faydalı olmayacağını düşünmenize ne sebep oldu?",
"improve_activation_rate_question_1_headline": "$[workspaceName] kurulumunu tamamlamamanın ana nedeni nedir?",
"improve_activation_rate_question_2_headline": "$[workspaceName]'in faydalı olmayacağını düşünmenize ne sebep oldu?",
"improve_activation_rate_question_2_placeholder": "Cevabınızı buraya yazın…",
"improve_activation_rate_question_3_headline": "$[projectName] kurulumunda veya kullanımında zor olan neydi?",
"improve_activation_rate_question_3_headline": "$[workspaceName]'i kurarken veya kullanırken neyi zor buldunuz?",
"improve_activation_rate_question_3_placeholder": "Cevabınızı buraya yazın…",
"improve_activation_rate_question_4_headline": "Hangi özellikler veya işlevler eksikti?",
"improve_activation_rate_question_4_placeholder": "Cevabınızı buraya yazın…",
@@ -1163,9 +1169,9 @@
"improve_trial_conversion_question_1_headline": "Deneme sürümünü neden bıraktınız?",
"improve_trial_conversion_question_1_subheader": "Sizi daha iyi anlamamıza yardımcı olun:",
"improve_trial_conversion_question_2_button_label": "Sonraki",
"improve_trial_conversion_question_2_headline": "Üzgünüz. $[projectName] kullanırken en büyük sorun neydi?",
"improve_trial_conversion_question_2_headline": "Bunu duyduğumuza üzüldük. $[workspaceName] kullanırken karşılaştığınız en büyük sorun neydi?",
"improve_trial_conversion_question_3_button_label": "İleri",
"improve_trial_conversion_question_3_headline": "$[projectName]'in ne yapmasını bekliyordun?",
"improve_trial_conversion_question_3_headline": "$[workspaceName]'in ne yapmasını bekliyordunuz?",
"improve_trial_conversion_question_4_button_label": "Yüzde 20 indirim alın",
"improve_trial_conversion_question_4_headline": "Üzgünüz! İlk yıl %20 indirim kazanın.",
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Yıllık planda size %20 indirim sunmaktan mutluluk duyarız.</span></p>",
@@ -1181,7 +1187,7 @@
"integration_setup_survey_question_1_upper_label": "Çok kolay",
"integration_setup_survey_question_2_headline": "Neden zordu?",
"integration_setup_survey_question_2_placeholder": "Cevabınızı buraya yazın…",
"integration_setup_survey_question_3_headline": "$[projectName] ile birlikte hangi araçları kullanmak istersiniz?",
"integration_setup_survey_question_3_headline": "$[workspaceName] ile başka hangi araçları kullanmak istersiniz?",
"integration_setup_survey_question_3_subheader": "Entegrasyonlar geliştirmeye devam ediyoruz, sıradaki sizinki olabilir:",
"interview_prompt_description": "Belirli bir kullanıcı grubunu ürün ekibinizle görüşme yapmaya davet edin.",
"interview_prompt_name": "Görüşme Daveti",
@@ -1192,23 +1198,23 @@
"long_term_retention_check_in_name": "Uzun Vadeli Elde Tutma Kontrolü",
"long_term_retention_check_in_question_10_headline": "Ek geri bildiriminiz veya yorumunuz var mı?",
"long_term_retention_check_in_question_10_placeholder": "İyileştirmemize yardımcı olabilecek düşüncelerinizi paylaşın...",
"long_term_retention_check_in_question_1_headline": "Genel olarak $[projectName] ürününden ne kadar memnunsunuz?",
"long_term_retention_check_in_question_1_headline": "$[workspaceName]'den genel olarak ne kadar memnunsunuz?",
"long_term_retention_check_in_question_1_lower_label": "Memnun değil",
"long_term_retention_check_in_question_1_upper_label": "Çok memnun",
"long_term_retention_check_in_question_2_headline": "$[projectName] ürününde en değerli bulduğunuz şey nedir?",
"long_term_retention_check_in_question_2_headline": "$[workspaceName]'in en değerli bulduğunuz yanı nedir?",
"long_term_retention_check_in_question_2_placeholder": "En çok değer verdiğiniz özellik veya faydayı açıklayın...",
"long_term_retention_check_in_question_3_choice_1": "Özellikler",
"long_term_retention_check_in_question_3_choice_2": "Müşteri desteği",
"long_term_retention_check_in_question_3_choice_3": "Kullanıcı deneyimi",
"long_term_retention_check_in_question_3_choice_4": "Fiyatlandırma",
"long_term_retention_check_in_question_3_choice_5": "Güvenilirlik ve çalışma süresi",
"long_term_retention_check_in_question_3_headline": "$[projectName] ürününün deneyiminiz için en temel yönü hangisi?",
"long_term_retention_check_in_question_4_headline": "$[projectName] beklentilerinizi ne kadar karşılıyor?",
"long_term_retention_check_in_question_3_headline": "$[workspaceName]'in deneyiminiz için en önemli bulduğunuz yönü hangisi?",
"long_term_retention_check_in_question_4_headline": "$[workspaceName] beklentilerinizi ne kadar karşılıyor?",
"long_term_retention_check_in_question_4_lower_label": "Beklentilerin altında",
"long_term_retention_check_in_question_4_upper_label": "Beklentilerin üstünde",
"long_term_retention_check_in_question_5_headline": "$[projectName] kullanırken hangi zorluklarla veya hayal kırıklıklarıyla karşılaştınız?",
"long_term_retention_check_in_question_5_headline": "$[workspaceName]'i kullanırken hangi zorluklar veya hayal kırıklıklarıyla karşılaştınız?",
"long_term_retention_check_in_question_5_placeholder": "Karşılaştığınız zorlukları veya görmek istediğiniz iyileştirmeleri açıklayın...",
"long_term_retention_check_in_question_6_headline": "$[projectName] ürününü bir arkadaşınıza veya meslektaşınıza tavsiye etme olasılığınız nedir?",
"long_term_retention_check_in_question_6_headline": "$[workspaceName]'i bir arkadaşınıza veya iş arkadaşınıza tavsiye etme olasılığınız nedir?",
"long_term_retention_check_in_question_6_lower_label": "Olası değil",
"long_term_retention_check_in_question_6_upper_label": "Çok olası",
"long_term_retention_check_in_question_7_choice_1": "Yeni özellikler ve iyileştirmeler",
@@ -1217,7 +1223,7 @@
"long_term_retention_check_in_question_7_choice_4": "Daha fazla entegrasyon",
"long_term_retention_check_in_question_7_choice_5": "Kullanıcı deneyimi iyileştirmeleri",
"long_term_retention_check_in_question_7_headline": "Uzun vadeli kullanıcı olarak kalma olasılığınızı ne artırır?",
"long_term_retention_check_in_question_8_headline": "$[projectName] hakkında bir şeyi değiştirebilseydiniz, ne olurdu?",
"long_term_retention_check_in_question_8_headline": "$[workspaceName] hakkında bir şeyi değiştirebilseydiniz, bu ne olurdu?",
"long_term_retention_check_in_question_8_placeholder": "Dikkate almamızı istediğiniz değişiklik veya özellikleri paylaşın...",
"long_term_retention_check_in_question_9_headline": "Ürün güncellemelerimizden ve sıklığından ne kadar memnunsunuz?",
"long_term_retention_check_in_question_9_lower_label": "Memnun değilim",
@@ -1236,8 +1242,8 @@
"market_site_clarity_question_1_choice_1": "Evet, kesinlikle",
"market_site_clarity_question_1_choice_2": "Biraz…",
"market_site_clarity_question_1_choice_3": "Hayır, hiç değil",
"market_site_clarity_question_1_headline": "$[projectName] ürününü denemek için ihtiyacınız olan tüm bilgilere sahip misiniz?",
"market_site_clarity_question_2_headline": "$[projectName] hakkında eksik veya belirsiz olan nedir?",
"market_site_clarity_question_1_headline": "$[workspaceName]'i denemek için ihtiyacınız olan tüm bilgilere sahip misiniz?",
"market_site_clarity_question_2_headline": "$[workspaceName] hakkında size eksik veya belirsiz gelen nedir?",
"market_site_clarity_question_3_button_label": "İndirim alın",
"market_site_clarity_question_3_headline": "Yanıtınız için teşekkürler! İlk 6 ayda %25 indirim kazanın:",
"matrix": "Matris",
@@ -1282,12 +1288,12 @@
"nps_description": "Net Tavsiye Skorunu (0-10) ölçün",
"nps_lower_label": "Hiç olası değil",
"nps_name": "Net Tavsiye Skoru (NPS)",
"nps_question_1_headline": "$[projectName] ürününü bir arkadaşınıza veya meslektaşınıza tavsiye etme olasılığınız nedir?",
"nps_question_1_headline": "$[workspaceName]'i bir arkadaşınıza veya iş arkadaşınıza tavsiye etme olasılığınız nedir?",
"nps_question_1_lower_label": "Olası değil",
"nps_question_1_upper_label": "Çok olası",
"nps_question_2_headline": "Bu puanı vermenize ne sebep oldu?",
"nps_survey_name": "NPS Anketi",
"nps_survey_question_1_headline": "$[projectName] ürününü bir arkadaşınıza veya meslektaşınıza tavsiye etme olasılığınız nedir?",
"nps_survey_question_1_headline": "$[workspaceName]'i bir arkadaşınıza veya iş arkadaşınıza tavsiye etme olasılığınız nedir?",
"nps_survey_question_1_lower_label": "Hiç olası değil",
"nps_survey_question_1_upper_label": "Son derece olası",
"nps_survey_question_2_headline": "Gelişmemize yardımcı olmak için puanlama nedeninizi açıklar mısınız?",
@@ -1321,7 +1327,7 @@
"preview_survey_ending_card_description": "Lütfen başlangıç sürecinize devam edin.",
"preview_survey_ending_card_headline": "Başardınız!",
"preview_survey_name": "Yeni Anket",
"preview_survey_question_1_headline": "{projectName} ürününü nasıl değerlendirirsiniz?",
"preview_survey_question_1_headline": "{workspaceName}'i nasıl değerlendirirsiniz?",
"preview_survey_question_1_lower_label": "İyi değil",
"preview_survey_question_1_subheader": "Bu bir anket önizlemesidir.",
"preview_survey_question_1_upper_label": "Çok iyi",
@@ -1345,16 +1351,16 @@
"prioritize_features_question_2_choice_2": "Özellik 2",
"prioritize_features_question_2_choice_3": "Özellik 3",
"prioritize_features_question_2_headline": "Bu özelliklerden hangisi sizin için EN AZ DEĞERLİ olurdu?",
"prioritize_features_question_3_headline": "$[projectName] deneyiminizi başka nasıl iyileştirebiliriz?",
"prioritize_features_question_3_headline": "$[workspaceName] deneyiminizi geliştirmek için başka ne yapabiliriz?",
"prioritize_features_question_3_placeholder": "Cevabınızı buraya yazın…",
"product_market_fit_short_description": "Ürününüz ortadan kalksa kullanıcıların ne kadar hayal kırıklığına uğrayacağını değerlendirerek ÜPU ölçün.",
"product_market_fit_short_name": "Ürün Pazar Uyumu Anketi (Kısa)",
"product_market_fit_short_question_1_choice_1": "Hiç hayal kırıklığına uğramamış",
"product_market_fit_short_question_1_choice_2": "Biraz hayal kırıklığına uğramış",
"product_market_fit_short_question_1_choice_3": "Çok hayal kırıklığına uğramış",
"product_market_fit_short_question_1_headline": "$[projectName] ürününü artık kullanamasanız ne kadar hayal kırıklığına uğrarsınız?",
"product_market_fit_short_question_1_headline": "$[workspaceName]'i artık kullanamıyor olsaydınız ne kadar hayal kırıklığına uğrardınız?",
"product_market_fit_short_question_1_subheader": "Lütfen aşağıdaki seçeneklerden birini seçin:",
"product_market_fit_short_question_2_headline": "$[projectName] ürününü sizin için nasıl geliştirebiliriz?",
"product_market_fit_short_question_2_headline": "$[workspaceName]'i sizin için nasıl geliştirebiliriz?",
"product_market_fit_short_question_2_subheader": "Lütfen mümkün olduğunca detaylı olun.",
"product_market_fit_superhuman": "Ürün Pazar Uyumu (Superhuman)",
"product_market_fit_superhuman_description": "Ürününüz ortadan kalksa kullanıcıların ne kadar hayal kırıklığına uğrayacağını değerlendirerek ÜPU ölçün.",
@@ -1364,7 +1370,7 @@
"product_market_fit_superhuman_question_2_choice_1": "Hiç hayal kırıklığına uğramamış",
"product_market_fit_superhuman_question_2_choice_2": "Biraz hayal kırıklığına uğramış",
"product_market_fit_superhuman_question_2_choice_3": "Çok hayal kırıklığına uğramış",
"product_market_fit_superhuman_question_2_headline": "$[projectName] ürününü artık kullanamasanız ne kadar hayal kırıklığına uğrarsınız?",
"product_market_fit_superhuman_question_2_headline": "$[workspaceName]'i artık kullanamıyor olsaydınız ne kadar hayal kırıklığına uğrardınız?",
"product_market_fit_superhuman_question_2_subheader": "Lütfen aşağıdaki seçeneklerden birini seçin:",
"product_market_fit_superhuman_question_3_choice_1": "Kurucu",
"product_market_fit_superhuman_question_3_choice_2": "Yönetici",
@@ -1373,9 +1379,9 @@
"product_market_fit_superhuman_question_3_choice_5": "Yazılım Mühendisi",
"product_market_fit_superhuman_question_3_headline": "Rolünüz nedir?",
"product_market_fit_superhuman_question_3_subheader": "Lütfen aşağıdaki seçeneklerden birini seçin:",
"product_market_fit_superhuman_question_4_headline": "Sizce $[projectName] ürününden en çok hangi tür insanlar fayda sağlar?",
"product_market_fit_superhuman_question_5_headline": "$[projectName] ürününden aldığınız temel fayda nedir?",
"product_market_fit_superhuman_question_6_headline": "$[projectName] ürününü sizin için nasıl geliştirebiliriz?",
"product_market_fit_superhuman_question_4_headline": "$[workspaceName]'den en çok hangi tür insanların faydalanacağını düşünüyorsunuz?",
"product_market_fit_superhuman_question_5_headline": "$[workspaceName]'den elde ettiğiniz ana fayda nedir?",
"product_market_fit_superhuman_question_6_headline": "$[workspaceName] uygulamasını sizin için nasıl geliştirebiliriz?",
"product_market_fit_superhuman_question_6_subheader": "Lütfen mümkün olduğunca detaylı olun.",
"professional_development_growth_survey_description": "Çalışanların profesyonel gelişim ve büyüme fırsatlarından memnuniyetini değerlendirin.",
"professional_development_growth_survey_name": "Profesyonel Gelişim ve Büyüme Anketi",
@@ -1446,7 +1452,7 @@
"recognition_and_reward_survey_question_4_placeholder": "Cevabınızı buraya yazın…",
"review_prompt_description": "Ürününüzü seven kullanıcıları herkese açık değerlendirme yazmaya davet edin.",
"review_prompt_name": "Değerlendirme Daveti",
"review_prompt_question_1_headline": "$[projectName] ürününü nasıl buluyorsunuz?",
"review_prompt_question_1_headline": "$[workspaceName] hakkında ne düşünüyorsunuz?",
"review_prompt_question_1_lower_label": "İyi değil",
"review_prompt_question_1_upper_label": "Çok memnun",
"review_prompt_question_2_button_label": "Yorum yaz",
@@ -1489,7 +1495,7 @@
"site_abandonment_survey_question_8_headline": "Lütfen email adresinizi paylaşın:",
"site_abandonment_survey_question_9_headline": "Ek yorumlarınız veya önerileriniz var mı?",
"smileys_survey_name": "Gülen Yüz Anketi",
"smileys_survey_question_1_headline": "$[projectName] ürününü nasıl buluyorsunuz?",
"smileys_survey_question_1_headline": "$[workspaceName] hakkında ne düşünüyorsunuz?",
"smileys_survey_question_1_lower_label": "İyi değil",
"smileys_survey_question_1_upper_label": "Çok memnun",
"smileys_survey_question_2_button_label": "Yorum yaz",
@@ -1499,8 +1505,8 @@
"smileys_survey_question_3_headline": "Üzgünüz! Daha iyi yapabileceğimiz BİR şey nedir?",
"smileys_survey_question_3_placeholder": "Cevabınızı buraya yazın…",
"smileys_survey_question_3_subheader": "Deneyiminizi iyileştirmemize yardımcı olun.",
"star_rating_survey_name": "$[projectName] Puanlama Anketi",
"star_rating_survey_question_1_headline": "$[projectName] ürününü nasıl buluyorsunuz?",
"star_rating_survey_name": "$[workspaceName] Değerlendirme Anketi",
"star_rating_survey_question_1_headline": "$[workspaceName] hakkında ne düşünüyorsunuz?",
"star_rating_survey_question_1_lower_label": "Son derece memnuniyetsiz",
"star_rating_survey_question_1_upper_label": "Son derece memnun",
"star_rating_survey_question_2_button_label": "Yorum yaz",
@@ -1533,7 +1539,7 @@
"uncover_strengths_and_weaknesses_question_1_choice_3": "Açık kaynak olması",
"uncover_strengths_and_weaknesses_question_1_choice_4": "Kurucular çok sempatik",
"uncover_strengths_and_weaknesses_question_1_choice_5": "Diğer",
"uncover_strengths_and_weaknesses_question_1_headline": "$[projectName] ürününde en çok neye değer veriyorsunuz?",
"uncover_strengths_and_weaknesses_question_1_headline": "$[workspaceName] uygulamasında en çok neye değer veriyorsunuz?",
"uncover_strengths_and_weaknesses_question_2_choice_1": "Dokümantasyon",
"uncover_strengths_and_weaknesses_question_2_choice_2": "Özelleştirilebilirlik",
"uncover_strengths_and_weaknesses_question_2_choice_3": "Fiyatlandırma",
@@ -1549,8 +1555,8 @@
"understand_low_engagement_question_1_choice_3": "Henüz vakit bulamadım",
"understand_low_engagement_question_1_choice_4": "İhtiyacım olan özellikler eksikti",
"understand_low_engagement_question_1_choice_5": "Diğer",
"understand_low_engagement_question_1_headline": "Son zamanlarda $[projectName] ürününe geri dönmemenizin ana nedeni nedir?",
"understand_low_engagement_question_2_headline": "$[projectName] kullanımında zor olan nedir?",
"understand_low_engagement_question_1_headline": "Son zamanlarda $[workspaceName] uygulamasına geri dönmemenizin ana nedeni nedir?",
"understand_low_engagement_question_2_headline": "$[workspaceName] uygulamasını kullanmakta zorluk çektiğiniz şey nedir?",
"understand_low_engagement_question_2_placeholder": "Cevabınızı buraya yazın…",
"understand_low_engagement_question_3_headline": "Anladım. Onun yerine hangi alternatifi kullanıyorsunuz?",
"understand_low_engagement_question_3_placeholder": "Cevabınızı buraya yazın…",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "Çubuk Grafik",
"chart_type_big_number": "Büyük Sayı",
"chart_type_line": "Çizgi Grafik",
"chart_type_not_supported": "\"{{chartType}}\" grafik türü henüz desteklenmiyor",
"chart_type_not_supported": "\"{chartType}\" grafik türü henüz desteklenmiyor",
"chart_type_pie": "Pasta Grafik",
"chart_updated_successfully": "Grafik başarıyla güncellendi!",
"configure_description": "Bu görselleştirme için grafik türünü ve diğer ayarları değiştir.",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "Sorgu çalıştırılamadı",
"failed_to_load_chart": "Grafik yüklenemedi",
"failed_to_load_chart_data": "Grafik verileri yüklenemedi",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "Grafik kaydedilemedi",
"field": "Alan",
"field_label_average_score": "Ortalama Puan",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "Görüntülenecek geçerli veri yok",
"not_contains": "içermez",
"not_equals": "eşit değildir",
"open_chart": "{{name}} grafiğini aç",
"open_chart": "{name} grafiğini aç",
"open_options": "Grafik seçeneklerini aç",
"or_filter_logic": "VEYA",
"original": "Orijinal",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "Lütfen bir kontrol paneli seç",
"predefined_measures": "Önceden Tanımlanmış Ölçümler",
"preset": "Ön Ayar",
"preview_chart": "Grafiği önizleyin",
"query_executed_successfully": "Sorgu başarıyla çalıştırıldı",
"reset_to_ai_suggestion": "Yapay zeka önerisine sıfırla",
"save_and_add_to_dashboard": "Kaydet ve kontrol paneline ekle",
"save_chart": "Grafiği Kaydet",
"save_chart_dialog_title": "Grafiği Kaydet",
"select_data_source": "Bir veri kaynağı seç",
@@ -1791,11 +1800,12 @@
"select_field": "Alan seç",
"select_measures": "Ölçümleri seç...",
"select_preset": "Hazır ayar seç",
"showing_first_n_of": "{{count}} satırdan ilk {{n}} tanesi gösteriliyor",
"showing_first_n_of": "{count} satırdan ilk {n} tanesi gösteriliyor",
"start_date": "Başlangıç tarihi",
"time_dimension": "Zaman Boyutu",
"time_dimension_title": "Zaman tabanlı gruplama ekle",
"time_dimension_toggle_description": "Zaman içindeki eğilimleri izle."
"time_dimension_toggle_description": "Zaman içindeki eğilimleri izle.",
"update_chart": "Grafiği güncelle"
},
"dashboards": {
"add_count_charts": "{count} grafik ekle",
@@ -1806,6 +1816,7 @@
"create_dashboard": "Pano oluştur",
"create_dashboard_description": "Yeni panon için bir isim gir.",
"create_failed": "Pano oluşturulamadı",
"create_new_chart": "Yeni grafik oluştur",
"create_success": "Pano başarıyla oluşturuldu!",
"dashboard": "Pano",
"dashboard_delete_confirmation": "Bu gösterge panelini silmek istediğinden emin misin? Bu işlem geri alınamaz.",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "Gösterge paneli kopyalanamadı",
"duplicate_success": "Gösterge paneli başarıyla kopyalandı!",
"failed_to_load_chart_data": "Grafik verileri yüklenemedi",
"no_charts_available_description": "Bu gösterge paneline eklenebilecek grafik bulunmuyor. Ya henüz hiç grafik oluşturulmadı ya da mevcut tüm grafikler zaten eklendi. Yeni grafikler oluşturmak için Grafikler sayfasına git.",
"no_charts_to_add_message": "Bu gösterge paneline eklenecek grafik yok.",
"no_dashboards_found": "Gösterge paneli bulunamadı.",
"no_data_message": "Veri Yok. Şu anda görüntülenecek bilgi bulunmuyor. Gösterge panelini oluşturmak için grafik ekle.",
"please_enter_name": "Lütfen bir gösterge paneli adı gir"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "Raporlayabileceğiniz Geri Bildirim Kayıtlarınız yok. Verileri sisteme beslemek için Geri Bildirim Kaynaklarını ayarlayın.",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "Geri bildirim kaynaklarını ayarlayın"
},
"api_keys": {
"add_api_key": "API Anahtarı Ekle",
@@ -1858,6 +1871,9 @@
"app_connection_description": "Uygulamanı veya web siteni Formbricks'e bağla.",
"cache_update_delay_description": "Anketler, kişiler, eylemler veya diğer verilerde güncelleme yaptığında, bu değişikliklerin Formbricks SDK'sını çalıştıran yerel uygulamanda görünmesi 1 dakikaya kadar sürebilir.",
"cache_update_delay_title": "Önbelleğe alma nedeniyle değişiklikler yaklaşık 1 dakika sonra yansıtılacak",
"environment_id_legacy": "Ortam Kimliği (eski)",
"environment_id_legacy_alert": "Mevcut SDK kurulumunuz hala eski bir Ortam Kimliği kullanıyor olabilir.",
"environment_id_legacy_alert_link": "Neden ve nasıl geçiş yapacağınızı öğrenin.",
"formbricks_sdk_connected": "Formbricks SDK bağlandı",
"formbricks_sdk_not_connected": "Formbricks SDK henüz bağlanmadı.",
"formbricks_sdk_not_connected_description": "Web sitenize veya uygulamanıza Formbricks SDK'sını ekleyerek Formbricks ile bağlantı kurun",
@@ -2389,7 +2405,7 @@
"most_popular": "En popüler",
"pending_change_removed": "Planlanmış plan değişikliği kaldırıldı.",
"pending_plan_badge": "Planlandı",
"pending_plan_change_description": "Planın {{date}} tarihinde {{plan}} olarak değişecek.",
"pending_plan_change_description": "Planınız {date} tarihinde {plan} olarak değişecek.",
"pending_plan_change_title": "Planlanmış plan değişikliği",
"pending_plan_cta": "Planlandı",
"per_month": "aylık",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "Bu ada sahip bir geri bildirim kayıt dizini zaten mevcut.",
"error_directory_name_required": "Dizin adı gereklidir.",
"error_directory_workspaces_invalid_org": "Belirtilen çalışma alanlarından bazıları bu organizasyona ait değil.",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "Geri Bildirim Dizinleri",
"no_access": "Geri bildirim kayıt dizinlerini yönetme izniniz yok.",
"no_connectors": "Bu dizine henüz bağlı bağlayıcı yok.",
"pause_connectors_confirmation_description": "Bu bağlayıcıları duraklatırsanız yeni kayıtlar eklenmez.",
"pause_connectors_confirmation_title": "Bağlı bağlayıcılar duraklatılsın mı?",
"select_workspaces_placeholder": "Çalışma alanlarını seç...",
"show_archived": "Arşivlenmişleri göster",
"title": "Geri Bildirim Kayıt Dizinleri",
"unarchive": "Arşivden çıkar"
"unarchive": "Arşivden çıkar",
"unarchive_workspace_conflict": "Atanmış çalışma alanlarından biri veya daha fazlası arşivlendiği için bu dizin arşivden çıkarılamaz.",
"workspace_access": "Çalışma alanı erişimi"
},
"general": {
"ai_data_analysis_disabled_for_organization": "Bu organizasyon için yapay zeka veri analizi devre dışı.",
"ai_data_analysis_enabled": "Veri zenginleştirme ve analiz (Yapay Zeka)",
"ai_data_analysis_enabled_description": "Verilerinden daha fazlasını elde etmek, kontrol panelleri, grafikler, raporlar ve daha fazlasını kurmak için yapay zeka. Deneyim verilerine dokunur.",
"ai_enabled": "Formbricks Yapay Zeka",
"ai_enabled_description": "Bu organizasyon için yapay zeka destekli özellikleri yönet.",
"ai_features_not_enabled_for_organization": "Bu organizasyon için yapay zeka özellikleri etkinleştirilmemiş.",
"ai_instance_not_configured": "Yapay zeka, ortam değişkenleri aracılığıyla instance seviyesinde yapılandırılır. Yapay zeka özelliklerini etkinleştirmeden önce yöneticinden AI_PROVIDER, AI_MODEL ve eşleşen sağlayıcı kimlik bilgilerini ayarlamasını iste.",
"ai_settings_updated_successfully": "Yapay zeka ayarları başarıyla güncellendi",
"ai_smart_tools_disabled_for_organization": "Bu organizasyon için yapay zeka akıllı araçları devre dışı.",
"ai_smart_tools_enabled": "Akıllı işlevsellik (Yapay Zeka)",
"ai_smart_tools_enabled_description": "Daha kısa sürede daha fazlasını başarman için yapay zeka. Formbricks ile toplanan verilere asla dokunmaz. Yalnızca örneğin anketleri diğer dillere çevirmek için kullanılır.",
"bulk_invite_warning_description": "Ücretsiz planda, tüm organizasyon üyeleri her zaman \"Sahip\" rolüne atanır.",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "Buradan kayıt ol.",
"share_invite_link": "Davet Bağlantısını Paylaş",
"share_this_link_to_let_your_organization_member_join_your_organization": "Organizasyon üyelerinin organizasyonuna katılması için bu bağlantıyı paylaş:",
"test_email_sent_successfully": "Test e-postası başarıyla gönderildi"
"test_email_sent_successfully": "Test e-postası başarıyla gönderildi",
"unlock_ai_features_description": "Yapay zeka destekli çeviriler, akıllı araçlar ve veri analizi daha üst planlarda mevcut. Anketlerinizi yapay zeka ile güçlendirmek için yükselt.",
"unlock_ai_features_with_a_higher_plan": "Daha üst bir planla yapay zeka özelliklerinin kilidini aç"
},
"notifications": {
"auto_subscribe_to_new_surveys": "Yeni anketlere otomatik abone ol",
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "\"Anket Kapatıldı\" mesajını düzenle",
"adjust_survey_closed_message_description": "Anket kapalıyken ziyaretçilerin gördüğü mesajı değiştir.",
"adjust_the_theme_in_the": "Temayı şurada düzenle:",
"ai_data_analysis_disabled": "Bu organizasyon için yapay zeka veri analizi devre dışı.",
"ai_features_not_enabled": "Bu organizasyon için yapay zeka özellikleri etkinleştirilmemiş.",
"ai_instance_not_configured": "Yapay zeka yapılandırılmamış. Yöneticinle iletişime geç.",
"ai_smart_tools_disabled": "Bu organizasyon için yapay zeka akıllı araçları devre dışı.",
"ai_translate": "Yapay Zeka ile Çevir",
"ai_translating": "Yapay zeka ile çevriliyor... Lütfen bu pencereyi açık tutun.",
"ai_translation_all_fields_populated": "Tüm alanlar zaten çevrilmiş",
"ai_translation_complete": "Yapay Zeka çevirisi tamamlandı",
"ai_translation_failed": "Çeviri başarısız oldu",
"ai_translation_instance_not_configured": "Bu örnekte AI yapılandırılmamış. Yöneticinizle iletişime geçin.",
"ai_translation_not_available": "Yapay zeka çevirisi mevcut planınızda kullanılamıyor. Bu özelliğin kilidini açmak için yükselt.",
"ai_translation_not_enabled": "Bu organizasyon için AI akıllı araçları devre dışı. Organizasyon ayarlarından etkinleştirin.",
"all_are_true": "tümü doğru",
"all_other_answers_will_continue_to": "Diğer tüm yanıtlar şuna devam edecek:",
"allow_multi_select": "Çoklu seçime izin ver",
@@ -3602,16 +3634,21 @@
"team_settings_description": "Bu çalışma alanına hangi takımların erişebildiğini görün."
},
"unify": {
"add_feedback_record": "Geri bildirim kaydı ekle",
"add_feedback_record_description": "Manuel olarak bir geri bildirim kaydı oluşturun.",
"add_feedback_source": "Geri Bildirim Kaynağı Ekle",
"add_source": "Kaynak ekle",
"allowed_values": "İzin verilen değerler: {values}",
"api_ingestion": "API ingestion",
"api_ingestion_manage_api_keys": "Manage API keys",
"api_ingestion_settings_description": "Send feedback records using the Management API.",
"auto_generated": "Otomatik olarak oluşturuldu",
"change_file": "Dosyayı değiştir",
"click_load_sample_csv": "Sütunları görmek için 'Örnek CSV yükle'ye tıkla",
"click_to_upload": "Yüklemek için tıkla",
"collected_at": "Toplandığı Tarih",
"configure_import": "İçe aktarmayı yapılandır",
"configure_mapping": "Eşleştirmeyi yapılandır",
"connection": "Bağlantı",
"connector_created_successfully": "Bağlayıcı başarıyla oluşturuldu",
"connector_deleted_successfully": "Bağlayıcı başarıyla silindi",
"connector_duplicated_successfully": "Bağlayıcı başarıyla kopyalandı",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "Verileri iki kez içe aktarmak yinelenen kayıtlar oluşturacaktır.",
"csv_inconsistent_columns": "Satır {row} tutarsız sütunlara sahip. Tüm satırlar aynı başlıklara sahip olmalıdır.",
"csv_max_records": "Maksimum {max} kayda izin verilir.",
"custom_source_type": "Özel kaynak türü",
"custom_source_type_placeholder": "Özel kaynak türünü girin",
"default_connector_name_csv": "CSV İçe Aktarma",
"default_connector_name_formbricks": "Formbricks Anket Bağlantısı",
"deselect_all": "Tümünün seçimini kaldır",
"discard_feedback_record_changes_description": "Bu çekmeceyi kapatırsanız değişiklikleriniz kaybolacak.",
"discard_feedback_record_changes_title": "Kaydedilmemiş değişiklikler silinsin mi?",
"drop_a_field_here": "Buraya bir alan bırakın",
"drop_field_or": "Alan bırakın veya",
"edit_csv_mapping": "CSV eşlemesini düzenle",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "Geri bildirim kayıtları yüklenemedi",
"feedback_date": "Geçerli tarih",
"feedback_record_created_successfully": "Geri bildirim kaydı başarıyla oluşturuldu",
"feedback_record_details": "Geri bildirim kaydı ayrıntıları",
"feedback_record_details_description": "Geri bildirim kayıt alanlarını inceleyin ve güncelleyin.",
"feedback_record_directory": "Geri Bildirim Kayıt Dizini",
"feedback_record_fields": "Geri Bildirim Kayıt Alanları",
"feedback_record_mcp": "Feedback Record MCP",
"feedback_record_updated_successfully": "Geri bildirim kaydı başarıyla güncellendi",
"feedback_record_value_required": "Seçilen alan türü için bir değer gerekli",
"feedback_records": "Geri Bildirim Kayıtları",
"feedback_records_refreshed": "Geri bildirim kayıtları yenilendi",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
"field_group_id": "Alan Grubu Kimliği",
"field_group_label": "Alan Grubu Etiketi",
"field_id": "Alan Kimliği",
"field_label": "Alan Etiketi",
"field_type": "Alan Türü",
"formbricks_surveys": "Formbricks Anketleri",
"frd_cannot_be_changed": "Geri bildirim dizini oluşturulduktan sonra değiştirilemez.",
"go_to_feedback_record_directories": "Dizin ayarlarına git",
"historical_import_complete": "İçe aktarma tamamlandı: {successes} başarılı, {failures} başarısız, {skipped} atlandı (veri yok)",
"import_csv_data": "Geri bildirimi içe aktar",
"import_feedback": "Geri bildirimi içe aktar",
"import_historical_responses": "Import historical responses",
"import_historical_responses_description": "Import existing responses from this survey now.",
"import_rows": "{count} satır içe aktar",
"import_via_source_name": "\"{sourceName}\" yoluyla içe aktar",
"importing_data": "Veri içe aktarılıyor...",
"importing_historical_data": "Geçmiş veriler içe aktarılıyor...",
"invalid_enum_values": "{field} alanına eşlenen sütunda geçersiz değerler",
"invalid_values_found": "Bulunan: {values} (satırlar: {rows}) {extra}",
"load_sample_csv": "Örnek CSV yükle",
"n_supported_questions": "{count} desteklenen soru",
"manage_directories": "Manage directories",
"manage_feedback_sources": "Geri bildirim kaynaklarını yönetin",
"metadata": "Meta veriler",
"metadata_key": "Meta veri anahtarı",
"metadata_read_only_entries": "Salt okunur meta veri değerleri (dize dışı)",
"metadata_value": "Meta veri değeri",
"missing_feedback_source_title": "Missing feedback source?",
"no_feedback_record_directory_available": "Bu çalışma alanına atanmış bir geri bildirim kayıt dizini yok. Önce bir tane oluştur veya ata.",
"no_feedback_records": "Henüz geri bildirim kaydı yok. Bağlayıcıların veri göndermeye başlamasıyla kayıtlar burada görünecek.",
"no_source_fields_loaded": "Henüz kaynak alan yüklenmedi",
"no_sources_connected": "Henüz bağlı kaynak yok. Başlamak için bir kaynak ekle.",
"no_surveys_found": "Bu ortamda anket bulunamadı",
"optional": "İsteğe bağlı",
"or_drag_and_drop": "veya sürükle bırak",
"question_selected": "<strong>{count}</strong> soru seçildi. Bu soruya verilen her yanıt yeni bir Geri Bildirim Kaydı oluşturacak.",
"question_type_not_supported": "Bu soru türü desteklenmiyor",
"questions_selected": "<strong>{count}</strong> soru seçildi. Bu sorulara verilen her yanıt yeni bir Geri Bildirim Kaydı oluşturacak.",
"records_will_go_to": "Kayıtlar şuraya gidecek",
"refresh_feedback_records": "Geri bildirim kayıtlarını yenile",
"refreshing_feedback_records": "Geri bildirim kayıtları yenileniyor...",
"request_feedback_source": "Request source integration",
"required": "Gerekli",
"save_changes": "Değişiklikleri kaydet",
"select_a_survey_to_see_questions": "Sorularını görmek için bir anket seç",
"select_a_value": "Bir değer seç...",
"select_all": "Tümünü seç",
"select_feedback_record_directory": "Bir dizin seç",
"select_feedback_record_source_type": "Kaynak türünü seçin",
"select_questions": "Soru seç",
"select_source_type_description": "Bağlamak istediğin geri bildirim kaynağının türünü seç.",
"select_source_type_prompt": "Bağlamak istediğiniz geri bildirim kaynağının türünü seçin:",
"select_survey": "Anket Seç",
"select_survey_and_questions": "Anket ve Soruları Seç",
"select_survey_questions_description": "Hangi anket sorularının GeriBildirimKayıtları oluşturması gerektiğini seçin.",
@@ -3692,27 +3749,38 @@
"showing_rows": "{count} satırdan 3'ü gösteriliyor",
"source": "kaynak",
"source_connect_csv_description": "CSV dosyalarından geri bildirim içe aktar",
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
"source_connect_formbricks_description": "Formbricks anketlerinizdeki geri bildirimleri bağlayın",
"source_fields": "Kaynak Alanları",
"source_id": "Kaynak kimliği",
"source_name": "Kaynak Adı",
"source_type": "Kaynak Türü",
"source_type_cannot_be_changed": "Kaynak türü değiştirilemez",
"sources": "Kaynaklar",
"status_active": "Devam Ediyor",
"status_completed": "Tamamlandı",
"status_draft": "Taslak",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "Hata",
"status_paused": "Duraklatıldı",
"status_live_sync": "Live sync",
"status_ready": "Ready",
"submission_id": "Gönderim Kimliği",
"survey_has_no_questions": "Bu ankette soru yok",
"survey_import_line": "{surveyName}: {responseCount} yanıt × {questionCount} soru = {total} Geri Bildirim Kaydı",
"total_feedback_records": "Toplam: {surveyCount} anket genelinde {total} Geri Bildirim Kaydından {checked} tanesi seçildi",
"topics_and_subtopics": "Konular ve alt konular",
"unify_feedback": "Geri Bildirimleri Birleştir",
"update_mapping_description": "Bu kaynak için eşleme yapılandırmasını güncelle.",
"updated_at": "Güncellenme tarihi",
"upload_csv_data_description": "Geri bildirim verilerini içe aktarmak için bir CSV dosyası yükle.",
"upload_csv_file": "CSV Dosyası Yükle",
"user_identifier": "Kullanıcı",
"value": "Değer"
"value": "Değer",
"value_boolean": "Değer (Boolean)",
"value_date": "Değer (Tarih)",
"value_number": "Değer (Sayı)",
"value_text": "Değer (Metin)"
},
"xm-templates": {
"ces": "CES",
+114 -46
View File
@@ -125,6 +125,7 @@
"activity": "活动",
"add": "添加",
"add_action": "添加 操作",
"add_chart": "添加图表",
"add_charts": "添加图表",
"add_existing_chart_description": "搜索并选择要添加到此仪表板的图表。",
"add_filter": "添加 过滤器",
@@ -147,6 +148,7 @@
"apply_filters": "应用 筛选",
"archived": "已归档",
"are_you_sure": "你 确定 吗?",
"ask": "Ask",
"attributes": "属性",
"back": "返回",
"billing": "账单",
@@ -159,7 +161,7 @@
"change_workspace": "切换工作区",
"chart": "图表",
"charts": "图表",
"choice_n": "选项 {{n}}",
"choice_n": "选项 {n}",
"choices": "选项",
"choose_organization": "选择 组织",
"choose_workspace": "选择工作区",
@@ -172,7 +174,7 @@
"close": "关闭",
"code": "代码",
"collapse_rows": "折叠 行",
"column_n": "列 {{n}}",
"column_n": "{n}",
"completed": "完成",
"configuration": "配置",
"confirm": "确认",
@@ -212,6 +214,7 @@
"delete_what": "删除{deleteWhat}",
"description": "描述",
"disable": "禁用",
"disabled": "已禁用",
"disallow": "不允许",
"discard": "丢弃",
"dismissed": "忽略",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "加载组织失败",
"failed_to_load_workspaces": "加载工作区失败",
"failed_to_parse_csv": "CSV 解析失败",
"field_placeholder": "{{field}} 占位符",
"field_placeholder": "{field} 占位符",
"filter": "筛选",
"finish": "完成",
"first_name": "名字",
@@ -331,6 +334,7 @@
"not_authenticated": "您 未 认证 以 执行 该 操作。",
"not_authorized": "未授权",
"not_connected": "未连接",
"not_set": "未设置",
"note": "注释",
"notifications": "通知",
"number": "数字",
@@ -390,13 +394,14 @@
"report_survey": "报告调查",
"request_trial_license": "申请试用许可证",
"reset_to_default": "重置为 默认",
"resize": "调整大小",
"response": "响应",
"response_id": "响应 ID",
"responses": "反馈",
"restart": "重新启动",
"retry": "重试",
"role": "角色",
"row_n": "行 {{n}}",
"row_n": "{n}",
"saas": "SaaS",
"sales": "销售",
"save": "保存",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "某些文件上传失败",
"something_went_wrong": "出错了",
"something_went_wrong_please_try_again": "出错了 。请 尝试 再次 操作 。",
"soon": "即将推出",
"sort_by": "排序 依据",
"start_free_trial": "开始免费试用",
"status": "状态",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "柱状图",
"chart_type_big_number": "大数字",
"chart_type_line": "折线图",
"chart_type_not_supported": "暂不支持图表类型“{{chartType}}”",
"chart_type_not_supported": "暂不支持图表类型 “{chartType}”",
"chart_type_pie": "饼图",
"chart_updated_successfully": "图表更新成功!",
"configure_description": "修改此可视化的图表类型和其他设置。",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "查询执行失败",
"failed_to_load_chart": "加载图表失败",
"failed_to_load_chart_data": "加载图表数据失败",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "图表保存失败",
"field": "字段",
"field_label_average_score": "平均分",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "无有效数据可显示",
"not_contains": "不包含",
"not_equals": "不等于",
"open_chart": "打开图表 {{name}}",
"open_chart": "打开图表 {name}",
"open_options": "打开图表选项",
"or_filter_logic": "或",
"original": "原始",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "请选择一个 Dashboard",
"predefined_measures": "预设度量",
"preset": "预设",
"preview_chart": "预览图表",
"query_executed_successfully": "查询执行成功",
"reset_to_ai_suggestion": "重置为 AI 建议",
"save_and_add_to_dashboard": "保存并添加到仪表板",
"save_chart": "保存图表",
"save_chart_dialog_title": "保存图表",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "选择字段",
"select_measures": "选择度量...",
"select_preset": "选择预设",
"showing_first_n_of": "显示前 {{n}} 行,共 {{count}} 行",
"showing_first_n_of": "显示前 {n} 行,共 {count} 行",
"start_date": "开始日期",
"time_dimension": "时间维度",
"time_dimension_title": "添加基于时间的分组",
"time_dimension_toggle_description": "监控随时间变化的趋势。"
"time_dimension_toggle_description": "监控随时间变化的趋势。",
"update_chart": "更新图表"
},
"dashboards": {
"add_count_charts": "添加 {count} 个图表",
@@ -1806,6 +1816,7 @@
"create_dashboard": "创建仪表板",
"create_dashboard_description": "请输入新 Dashboard 的名称。",
"create_failed": "创建 Dashboard 失败",
"create_new_chart": "创建新图表",
"create_success": "Dashboard 创建成功!",
"dashboard": "仪表板",
"dashboard_delete_confirmation": "你确定要删除此仪表板吗?此操作无法撤销。",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "复制 Dashboard 失败",
"duplicate_success": "Dashboard 复制成功!",
"failed_to_load_chart_data": "加载图表数据失败",
"no_charts_available_description": "没有可以添加到此仪表板的图表。要么还没有创建任何图表,要么所有现有图表都已添加。请前往图表页面创建新图表。",
"no_charts_to_add_message": "没有可添加到此仪表板的图表。",
"no_dashboards_found": "未找到 Dashboard。",
"no_data_message": "暂无数据。当前没有可显示的信息。请添加图表来构建你的仪表板。",
"please_enter_name": "请输入 Dashboard 名称"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "您没有可供报告的反馈记录。设置反馈源以将数据输入系统。",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "设置反馈源"
},
"api_keys": {
"add_api_key": "添加 API 密钥",
@@ -1858,6 +1871,9 @@
"app_connection_description": "将您的应用或网站连接到 Formbricks。",
"cache_update_delay_description": "当您更新问卷、联系人、操作或其他数据时,这些更改最多可能需要 1 分钟才能在本地运行 Formbricks SDK 的应用中显示。",
"cache_update_delay_title": "由于缓存,变更将在约 1 分钟后生效",
"environment_id_legacy": "环境 ID(旧版)",
"environment_id_legacy_alert": "您现有的 SDK 设置可能仍在使用旧版环境 ID。",
"environment_id_legacy_alert_link": "了解原因以及如何迁移。",
"formbricks_sdk_connected": "Formbricks SDK 已连接",
"formbricks_sdk_not_connected": "Formbricks SDK 尚未连接。",
"formbricks_sdk_not_connected_description": "将 Formbricks SDK 添加到您的网站或应用,以实现与 Formbricks 的连接。",
@@ -2389,7 +2405,7 @@
"most_popular": "最受欢迎",
"pending_change_removed": "已取消预定的方案变更。",
"pending_plan_badge": "已预定",
"pending_plan_change_description": "您的方案将在 {{date}} 切换至 {{plan}}。",
"pending_plan_change_description": "你的套餐将在 {date} 切换{plan}。",
"pending_plan_change_title": "预定的方案变更",
"pending_plan_cta": "已预定",
"per_month": "每月",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "已存在同名的反馈记录目录。",
"error_directory_name_required": "目录名称为必填项。",
"error_directory_workspaces_invalid_org": "某些指定的工作区不属于此组织。",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "反馈目录",
"no_access": "你没有管理反馈记录目录的权限。",
"no_connectors": "此目录尚未链接任何连接器。",
"pause_connectors_confirmation_description": "暂停这些连接器后,将不会再添加新记录。",
"pause_connectors_confirmation_title": "暂停已关联的连接器?",
"select_workspaces_placeholder": "选择工作区...",
"show_archived": "显示已归档",
"title": "反馈记录目录",
"unarchive": "取消归档"
"unarchive": "取消归档",
"unarchive_workspace_conflict": "无法取消归档该目录,因为一个或多个已分配工作区已归档。",
"workspace_access": "工作区访问权限"
},
"general": {
"ai_data_analysis_disabled_for_organization": "此组织的 AI 数据分析和增强功能已禁用。",
"ai_data_analysis_enabled": "数据增强与分析(AI",
"ai_data_analysis_enabled_description": "使用 AI 深度挖掘你的数据,设置仪表盘、图表、报告等。会处理你的体验数据。",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "管理该组织的 AI 驱动功能。",
"ai_features_not_enabled_for_organization": "此组织未启用 AI 功能。",
"ai_instance_not_configured": "AI 通过环境变量在实例级别进行配置。启用 AI 功能前,请让管理员设置 AI_PROVIDER、该提供商的凭据以及对应的模型列表。",
"ai_settings_updated_successfully": "AI 设置已成功更新",
"ai_smart_tools_disabled_for_organization": "此组织的 AI 智能功能已禁用。",
"ai_smart_tools_enabled": "智能功能(AI",
"ai_smart_tools_enabled_description": "AI 帮你更高效地完成更多任务。绝不会接触 Formbricks 收集的数据,仅用于如问卷翻译等功能。",
"bulk_invite_warning_description": "在免费计划中,所有组织成员都会被分配为 \"Owner \"角色。",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "您 不能 离开 此 组织,因为 这是 您 唯一的 组织。请 先 创建一个新的 组织。",
"copy_invite_link_to_clipboard": "复制 invite 链接 到 剪贴板",
"create_new_organization": "创建 新的 组织",
"create_new_organization_description": "创建一个新 组织 来处理不同的 项目 集。",
"create_new_organization_description": "创建一个新组织来管理不同的工作空间集合。",
"customize_email_with_a_higher_plan": "通过更高的计划 自定义电子邮件",
"delete_member_confirmation": "删除的 成员 将无法访问 你的组织 的 所有项目 和 调查。",
"delete_member_confirmation": "删除的成员将失去对你组织中所有工作空间和调查的访问权限。",
"delete_organization": "删除 组织",
"delete_organization_description": "删除 组织 的 所有 项目 包括所有 调查、回、人员、动作 和 属性",
"delete_organization_description": "删除组织及其所有工作空间,包括所有调查、回、人员、操作和属性",
"delete_organization_warning": "在 您 继续 删除 这个 组织 前, 请 注意 以下 后果:",
"delete_organization_warning_1": "永久删除与此组织关联的所有项目。",
"delete_organization_warning_1": "永久删除与此组织关联的所有工作空间。",
"delete_organization_warning_2": "此操作无法撤消。 一旦 消失 就 消失 。",
"delete_organization_warning_3": "请在下列字段中输入 {organizationName} 以确认此组织的最终删除:",
"eliminate_branding_with_whitelabel": "消除 Formbricks 品牌 并启用 额外的 白标 自定义 选项",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "点击此处注册。",
"share_invite_link": "分享邀请链接",
"share_this_link_to_let_your_organization_member_join_your_organization": "分享 这个 链接 以 让 你的 组织 成员 加入 你的 组织:",
"test_email_sent_successfully": "测试 邮件 发送 成功"
"test_email_sent_successfully": "测试 邮件 发送 成功",
"unlock_ai_features_description": "AI 翻译、智能工具和数据分析均在更高级套餐中提供。升级套餐,让你的问卷体验焕然一新!",
"unlock_ai_features_with_a_higher_plan": "升级套餐,解锁 AI 功能"
},
"notifications": {
"auto_subscribe_to_new_surveys": "自动 订阅 新 调查",
@@ -2672,7 +2692,7 @@
"add_workspaces_description": "控制团队成员可以访问哪些工作区。",
"all_members_added": "所有成员已添加到此团队。",
"all_workspaces_added": "该团队已添加所有工作区。",
"are_you_sure_you_want_to_delete_this_team": "您 确定 要 删除 这个 团队 吗?这也会 移除 对 与 这个 团队 相关 的 所有 项目 和 调查 的 访问。",
"are_you_sure_you_want_to_delete_this_team": "你确定要删除这个团队吗?这也会移除对与此团队关联的所有工作空间和调查的访问权限。",
"billing_role_description": "仅 能 访问 账单 信息。",
"bulk_invite": "批量 邀请",
"contributor": "贡献者",
@@ -2688,10 +2708,10 @@
"manage": "管理",
"manage_team": "管理团队",
"manage_team_disabled": "只有 组织 拥有者、经理 和 团队 管理员 可以 管理 团队。",
"manager_role_description": "经理 可以 访问 所有 项目 并 添加 移除 成员。",
"manager_role_description": "管理员可以访问所有工作空间,并可以添加移除成员。",
"member": "成员",
"member_role_description": "成员 可以 在 选定 项目 中 工作。",
"member_role_info_message": "要 给 新 成员 访问 项目 ,请 将 他们 添加 到 下方 的 团队 。通过 团队可以 管理 谁 可以 访问 哪个 项目 。",
"member_role_description": "成员可以在选定的工作空间中工作。",
"member_role_info_message": "要让新成员访问工作空间,请将他们添加到下面的团队。通过团队,你可以管理谁有权访问哪个工作空间。",
"organization_role": "组织角色",
"owner_role_description": "所有者拥有对组织的完全控制权。",
"please_fill_all_member_fields": "请 填写 所有 字段 以 添加 新 成员。",
@@ -2708,8 +2728,8 @@
"team_settings_description": "管理 团队成员、访问 权限 和 更多。",
"team_updated_successfully": "团队 更新 成功",
"teams": "团队",
"teams_description": "将 成员 分配 到 团队 ,并 给 团队 访问 项目 的 权限。",
"unlock_teams_description": "管理 哪些 组织成员 可以 访问 特定 项目 和 调查。",
"teams_description": "将成员分配到团队中,并授予团队访问工作空间的权限。",
"unlock_teams_description": "管理哪些组织成员可以访问特定的工作空间和调查。",
"unlock_teams_title": "通过 更 高级 划解锁 团队",
"upgrade_plan_notice_message": "解锁更多组织角色功能 通过升级计划。",
"you_are_a_member": "你是 会员"
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "调整 \"调查 关闭\" 消息",
"adjust_survey_closed_message_description": "更改 访客 看到 调查 关闭 时 的 消息。",
"adjust_the_theme_in_the": "调整主题在",
"ai_data_analysis_disabled": "此组织已禁用 AI 数据分析。",
"ai_features_not_enabled": "此组织未启用 AI 功能。",
"ai_instance_not_configured": "AI 未配置。请联系您的管理员。",
"ai_smart_tools_disabled": "此组织已禁用 AI 智能工具。",
"ai_translate": "使用 AI 翻译",
"ai_translating": "AI 翻译中...请保持此窗口打开。",
"ai_translation_all_fields_populated": "所有字段均已翻译",
"ai_translation_complete": "AI 翻译完成",
"ai_translation_failed": "翻译失败",
"ai_translation_instance_not_configured": "此实例未配置 AI。请联系您的管理员。",
"ai_translation_not_available": "你当前套餐不支持 AI 翻译。升级即可解锁该功能。",
"ai_translation_not_enabled": "此组织已禁用 AI 智能工具。请在组织设置中启用。",
"all_are_true": "全部为真",
"all_other_answers_will_continue_to": "所有其他答案将继续",
"allow_multi_select": "允许 多选",
@@ -3043,7 +3075,7 @@
"options_used_in_logic_bulk_error": "以下选项在逻辑中被使用:{questionIndexes}。请先从逻辑中删除它们。",
"override_theme_with_individual_styles_for_this_survey": "使用 个性化 样式 替代 这份 问卷 的 主题。",
"overwrite_global_waiting_time": "自定义冷却期",
"overwrite_global_waiting_time_description": "仅此调查覆盖项目配置。",
"overwrite_global_waiting_time_description": "仅针对此调查覆盖工作空间配置。",
"overwrite_placement": "覆盖 放置",
"overwrite_survey_logo": "设置自定义调查 logo",
"overwrite_the_global_placement_of_the_survey": "覆盖 全局 调查 放置",
@@ -3602,16 +3634,21 @@
"team_settings_description": "查看哪些团队可以访问此工作区。"
},
"unify": {
"add_feedback_record": "添加反馈记录",
"add_feedback_record_description": "手动创建反馈记录。",
"add_feedback_source": "添加反馈来源",
"add_source": "添加来源",
"allowed_values": "允许的值:{values}",
"api_ingestion": "API ingestion",
"api_ingestion_manage_api_keys": "Manage API keys",
"api_ingestion_settings_description": "Send feedback records using the Management API.",
"auto_generated": "自动生成",
"change_file": "更换文件",
"click_load_sample_csv": "点击“加载示例 CSV”查看列",
"click_to_upload": "点击上传",
"collected_at": "收集时间",
"configure_import": "配置导入",
"configure_mapping": "配置映射",
"connection": "连接",
"connector_created_successfully": "连接器创建成功",
"connector_deleted_successfully": "连接器删除成功",
"connector_duplicated_successfully": "连接器复制成功",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "重复导入数据会产生重复记录。",
"csv_inconsistent_columns": "第 {row} 行的列数不一致。所有行必须有相同的表头。",
"csv_max_records": "最多允许 {max} 条记录。",
"custom_source_type": "自定义源类型",
"custom_source_type_placeholder": "输入自定义来源类型",
"default_connector_name_csv": "CSV 导入",
"default_connector_name_formbricks": "Formbricks 调查连接",
"deselect_all": "取消全选",
"discard_feedback_record_changes_description": "如果关闭此抽屉,您的更改将会丢失。",
"discard_feedback_record_changes_title": "放弃未保存的更改?",
"drop_a_field_here": "将字段拖到这里",
"drop_field_or": "拖放字段或",
"edit_csv_mapping": "编辑 CSV 映射",
@@ -3642,47 +3682,64 @@
"enum": "枚举",
"failed_to_load_feedback_records": "加载反馈记录失败",
"feedback_date": "当前日期",
"feedback_record_created_successfully": "反馈记录创建成功",
"feedback_record_details": "反馈记录详情",
"feedback_record_details_description": "查看并更新反馈记录字段。",
"feedback_record_directory": "反馈记录目录",
"feedback_record_fields": "反馈记录字段",
"feedback_record_mcp": "Feedback Record MCP",
"feedback_record_updated_successfully": "反馈记录更新成功",
"feedback_record_value_required": "所选字段类型需要一个值",
"feedback_records": "反馈记录",
"feedback_records_refreshed": "反馈记录已刷新",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
"field_group_id": "字段组 ID",
"field_group_label": "字段组标签",
"field_id": "字段ID",
"field_label": "字段标签",
"field_type": "字段类型",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "反馈目录创建后无法更改。",
"go_to_feedback_record_directories": "前往目录设置",
"historical_import_complete": "导入完成:{successes} 个成功,{failures} 个失败,{skipped} 个跳过(无数据)",
"import_csv_data": "导入反馈",
"import_feedback": "导入反馈",
"import_historical_responses": "Import historical responses",
"import_historical_responses_description": "Import existing responses from this survey now.",
"import_rows": "导入{count}行数据",
"import_via_source_name": "通过“{sourceName}”导入",
"importing_data": "正在导入数据…",
"importing_historical_data": "正在导入历史数据…",
"invalid_enum_values": "映射到 {field} 的列中存在无效值",
"invalid_values_found": "发现:{values}(行:{rows}{extra}",
"load_sample_csv": "加载示例 CSV",
"n_supported_questions": "{count} 个支持的问题",
"manage_directories": "Manage directories",
"manage_feedback_sources": "管理反馈来源",
"metadata": "元数据",
"metadata_key": "元数据键",
"metadata_read_only_entries": "只读元数据值(非字符串)",
"metadata_value": "元数据值",
"missing_feedback_source_title": "Missing feedback source?",
"no_feedback_record_directory_available": "此工作区未分配反馈记录目录。请先创建或分配一个。",
"no_feedback_records": "暂无反馈记录。当你的连接器开始发送数据后,记录会显示在这里。",
"no_source_fields_loaded": "尚未加载源字段",
"no_sources_connected": "还没有连接数据源。添加一个数据源开始吧。",
"no_surveys_found": "此环境下未找到调查",
"optional": "可选",
"or_drag_and_drop": "或拖放",
"question_selected": "<strong>{count}</strong> 个问题已选。每个问题的回答都会创建一条新的反馈记录。",
"question_type_not_supported": "不支持此问题类型",
"questions_selected": "<strong>{count}</strong> 个问题已选。每个问题的回答都会创建一条新的反馈记录。",
"records_will_go_to": "记录将发送至",
"refresh_feedback_records": "刷新反馈记录",
"refreshing_feedback_records": "正在刷新反馈记录…",
"request_feedback_source": "Request source integration",
"required": "必填",
"save_changes": "保存更改",
"select_a_survey_to_see_questions": "请选择一个调查以查看其问题",
"select_a_value": "选择一个值...",
"select_all": "全选",
"select_feedback_record_directory": "选择目录",
"select_feedback_record_source_type": "选择来源类型",
"select_questions": "选择问题",
"select_source_type_description": "请选择你想要连接的反馈来源类型。",
"select_source_type_prompt": "请选择你想要连接的反馈来源类型:",
"select_survey": "选择调查",
"select_survey_and_questions": "选择调查和问题",
"select_survey_questions_description": "选择哪些调查问题会创建反馈记录。",
@@ -3692,27 +3749,38 @@
"showing_rows": "显示 {count} 行中的 3 行",
"source": "source",
"source_connect_csv_description": "从 CSV 文件导入反馈",
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
"source_connect_formbricks_description": "连接来自你 Formbricks 调查的反馈",
"source_fields": "来源字段",
"source_id": "源ID",
"source_name": "来源名称",
"source_type": "来源类型",
"source_type_cannot_be_changed": "来源类型无法更改",
"sources": "来源",
"status_active": "进行中",
"status_completed": "已完成",
"status_draft": "草稿",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "错误",
"status_paused": "已暂停",
"status_live_sync": "Live sync",
"status_ready": "Ready",
"submission_id": "提交ID",
"survey_has_no_questions": "该调查没有任何问题",
"survey_import_line": "{surveyName}{responseCount} 份答卷 × {questionCount} 个问题 = {total} 条反馈记录",
"total_feedback_records": "总计:{surveyCount} 个调查中已选 {checked} / {total} 条反馈记录",
"topics_and_subtopics": "主题和子主题",
"unify_feedback": "统一反馈",
"update_mapping_description": "更新此来源的映射配置。",
"updated_at": "更新于",
"upload_csv_data_description": "上传 CSV 文件以导入反馈数据。",
"upload_csv_file": "上传 CSV 文件",
"user_identifier": "用户",
"value": "值"
"value": "值",
"value_boolean": "值(布尔值)",
"value_date": "值(日期)",
"value_number": "值(数量)",
"value_text": "值(文本)"
},
"xm-templates": {
"ces": "客户努力评分",
+114 -46
View File
@@ -125,6 +125,7 @@
"activity": "活動",
"add": "新增",
"add_action": "新增操作",
"add_chart": "新增圖表",
"add_charts": "新增圖表",
"add_existing_chart_description": "搜尋並選擇要新增至此儀表板的圖表。",
"add_filter": "新增篩選器",
@@ -147,6 +148,7 @@
"apply_filters": "套用篩選器",
"archived": "已封存",
"are_you_sure": "您確定嗎?",
"ask": "Ask",
"attributes": "屬性",
"back": "返回",
"billing": "帳單",
@@ -159,7 +161,7 @@
"change_workspace": "變更工作區",
"chart": "圖表",
"charts": "圖表",
"choice_n": "選項 {{n}}",
"choice_n": "選項 {n}",
"choices": "選項",
"choose_organization": "選擇 組織",
"choose_workspace": "選擇工作區",
@@ -172,7 +174,7 @@
"close": "關閉",
"code": "程式碼",
"collapse_rows": "摺疊列",
"column_n": "欄 {{n}}",
"column_n": "欄 {n}",
"completed": "已完成",
"configuration": "設定",
"confirm": "確認",
@@ -212,6 +214,7 @@
"delete_what": "刪除{deleteWhat}",
"description": "描述",
"disable": "停用",
"disabled": "已停用",
"disallow": "不允許",
"discard": "捨棄",
"dismissed": "已關閉",
@@ -243,7 +246,7 @@
"failed_to_load_organizations": "無法載入組織",
"failed_to_load_workspaces": "載入工作區失敗",
"failed_to_parse_csv": "CSV 解析失敗",
"field_placeholder": "{{field}} 預設文字",
"field_placeholder": "{field} 佔位符",
"filter": "篩選",
"finish": "完成",
"first_name": "名字",
@@ -331,6 +334,7 @@
"not_authenticated": "您未經授權執行此操作。",
"not_authorized": "未授權",
"not_connected": "未連線",
"not_set": "未設定",
"note": "筆記",
"notifications": "通知",
"number": "數字",
@@ -390,13 +394,14 @@
"report_survey": "報告問卷",
"request_trial_license": "請求試用授權",
"reset_to_default": "重設為預設值",
"resize": "調整大小",
"response": "回應",
"response_id": "回應 ID",
"responses": "回應",
"restart": "重新開始",
"retry": "重試",
"role": "角色",
"row_n": "列 {{n}}",
"row_n": "列 {n}",
"saas": "SaaS",
"sales": "銷售",
"save": "儲存",
@@ -431,6 +436,7 @@
"some_files_failed_to_upload": "部分檔案上傳失敗",
"something_went_wrong": "發生錯誤",
"something_went_wrong_please_try_again": "發生錯誤。請再試一次。",
"soon": "即將推出",
"sort_by": "排序方式",
"start_free_trial": "開始免費試用",
"status": "狀態",
@@ -1682,7 +1688,7 @@
"chart_type_bar": "長條圖",
"chart_type_big_number": "大數字",
"chart_type_line": "折線圖",
"chart_type_not_supported": "尚支援圖表類型「{{chartType}}」",
"chart_type_not_supported": "尚支援圖表類型「{chartType}」",
"chart_type_pie": "圓餅圖",
"chart_updated_successfully": "圖表已成功更新!",
"configure_description": "修改此視覺化的圖表類型及其他設定。",
@@ -1720,6 +1726,7 @@
"failed_to_execute_query": "查詢執行失敗",
"failed_to_load_chart": "載入圖表失敗",
"failed_to_load_chart_data": "載入圖表資料失敗",
"failed_to_load_dashboards": "Failed to load dashboards",
"failed_to_save_chart": "儲存圖表失敗",
"field": "欄位",
"field_label_average_score": "平均分數",
@@ -1770,7 +1777,7 @@
"no_valid_data_to_display": "沒有可顯示的有效資料",
"not_contains": "不包含",
"not_equals": "不等於",
"open_chart": "開啟圖表 {{name}}",
"open_chart": "開啟圖表 {name}",
"open_options": "開啟圖表選項",
"or_filter_logic": "或",
"original": "原始",
@@ -1781,8 +1788,10 @@
"please_select_dashboard": "請選擇一個儀表板",
"predefined_measures": "預設指標",
"preset": "預設",
"preview_chart": "預覽圖表",
"query_executed_successfully": "查詢執行成功",
"reset_to_ai_suggestion": "重設為 AI 建議",
"save_and_add_to_dashboard": "儲存並新增到儀表板",
"save_chart": "儲存圖表",
"save_chart_dialog_title": "儲存圖表",
"select_data_source": "Select a data source",
@@ -1791,11 +1800,12 @@
"select_field": "選擇欄位",
"select_measures": "選擇指標...",
"select_preset": "選擇預設",
"showing_first_n_of": "顯示前 {{n}} 筆,共 {{count}} 筆資料",
"showing_first_n_of": "顯示 {count} 列中的前 {n} 列",
"start_date": "開始日期",
"time_dimension": "時間維度",
"time_dimension_title": "新增基於時間的分組",
"time_dimension_toggle_description": "監控隨時間變化的趨勢。"
"time_dimension_toggle_description": "監控隨時間變化的趨勢。",
"update_chart": "更新圖表"
},
"dashboards": {
"add_count_charts": "新增 {count} 個圖表",
@@ -1806,6 +1816,7 @@
"create_dashboard": "建立儀表板",
"create_dashboard_description": "請輸入新儀表板的名稱。",
"create_failed": "建立儀表板失敗",
"create_new_chart": "建立新圖表",
"create_success": "儀表板建立成功!",
"dashboard": "儀表板",
"dashboard_delete_confirmation": "確定要刪除此儀表板嗎?此操作無法復原。",
@@ -1820,12 +1831,14 @@
"duplicate_failed": "複製儀表板失敗",
"duplicate_success": "儀表板複製成功!",
"failed_to_load_chart_data": "載入圖表資料失敗",
"no_charts_available_description": "目前沒有可以新增到此儀表板的圖表。可能是尚未建立任何圖表,或所有現有圖表都已新增。請前往圖表頁面建立新的圖表。",
"no_charts_to_add_message": "沒有可新增到此儀表板的圖表。",
"no_dashboards_found": "找不到儀表板。",
"no_data_message": "無資料。目前沒有可顯示的資訊。請新增圖表來建立你的儀表板。",
"please_enter_name": "請輸入儀表板名稱"
}
},
"manage_feedback_sources": "Manage feedback sources",
"no_feedback_records_message": "您沒有可供報告的回饋記錄。設定回饋來源以將資料輸入系統。",
"no_feedback_records_with_sources_message": "No feedback records yet. Records will appear here once your feedback sources start sending data.",
"setup_feedback_source": "設定反饋源"
},
"api_keys": {
"add_api_key": "新增 API 金鑰",
@@ -1858,6 +1871,9 @@
"app_connection_description": "將您的應用程式或網站連接到 Formbricks。",
"cache_update_delay_description": "當您更新問卷、聯絡人、動作或其他資料時,這些變更最多可能需要 1 分鐘才會反映在執行 Formbricks SDK 的本地應用程式中。",
"cache_update_delay_title": "因快取機制,變更約 1 分鐘後才會反映",
"environment_id_legacy": "環境 ID(舊版)",
"environment_id_legacy_alert": "你現有的 SDK 設定可能仍在使用舊版環境 ID。",
"environment_id_legacy_alert_link": "了解原因及遷移方式。",
"formbricks_sdk_connected": "Formbricks SDK 已連線",
"formbricks_sdk_not_connected": "Formbricks SDK 尚未連線。",
"formbricks_sdk_not_connected_description": "將 Formbricks SDK 加入您的網站或應用程式,以連接至 Formbricks",
@@ -2389,7 +2405,7 @@
"most_popular": "最受歡迎",
"pending_change_removed": "已取消預定的方案變更。",
"pending_plan_badge": "已排程",
"pending_plan_change_description": "您的方案將於 {{date}} 切換至 {{plan}}。",
"pending_plan_change_description": "您的方案將於 {date} 切換至 {plan}。",
"pending_plan_change_title": "已排程的方案變更",
"pending_plan_cta": "已排程",
"per_month": "每月",
@@ -2545,24 +2561,26 @@
"error_directory_name_duplicate": "已存在同名的意見回饋記錄目錄。",
"error_directory_name_required": "目錄名稱為必填項目。",
"error_directory_workspaces_invalid_org": "部分指定的工作區不屬於此組織。",
"error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.",
"nav_label": "意見回饋目錄",
"no_access": "您沒有權限管理意見回饋記錄目錄。",
"no_connectors": "此目錄尚未連結任何連接器。",
"pause_connectors_confirmation_description": "暫停這些連接器後,將不會再新增新紀錄。",
"pause_connectors_confirmation_title": "暫停已連結的連接器?",
"select_workspaces_placeholder": "選擇工作區...",
"show_archived": "顯示已封存",
"title": "意見回饋記錄目錄",
"unarchive": "取消封存"
"unarchive": "取消封存",
"unarchive_workspace_conflict": "無法取消封存此目錄,因為一個或多個已指派工作區已封存。",
"workspace_access": "工作區存取權限"
},
"general": {
"ai_data_analysis_disabled_for_organization": "此組織的 AI 資料分析與增強功能已停用。",
"ai_data_analysis_enabled": "資料增強與分析(AI",
"ai_data_analysis_enabled_description": "利用 AI 深入分析你的資料,建立儀表板、圖表、報告等。會處理你的體驗資料。",
"ai_enabled": "Formbricks AI",
"ai_enabled_description": "管理此組織的 AI 功能。",
"ai_features_not_enabled_for_organization": "此組織尚未啟用 AI 功能。",
"ai_instance_not_configured": "AI 會透過環境變數在實例層級進行設定。啟用 AI 功能前,請管理員設定 AI_PROVIDER、該供應商的憑證,以及對應的模型清單。",
"ai_settings_updated_successfully": "AI 設定已成功更新",
"ai_smart_tools_disabled_for_organization": "此組織的 AI 智慧功能已停用。",
"ai_smart_tools_enabled": "智慧功能(AI",
"ai_smart_tools_enabled_description": "AI 幫你更快完成更多事。絕不會接觸 Formbricks 收集的資料,只用於像是將問卷翻譯成其他語言等用途。",
"bulk_invite_warning_description": "在免費方案中,所有組織成員始終會被指派「擁有者」角色。",
@@ -2570,13 +2588,13 @@
"cannot_leave_only_organization": "您無法離開此組織,因為它是您唯一的組織。請先建立新組織。",
"copy_invite_link_to_clipboard": "將邀請連結複製到剪貼簿",
"create_new_organization": "建立新組織",
"create_new_organization_description": "建立新組織以處理一組不同的專案。",
"create_new_organization_description": "建立新組織來管理不同的工作區集合。",
"customize_email_with_a_higher_plan": "使用更高等級的方案自訂電子郵件",
"delete_member_confirmation": "刪除的成員將失去存取您組織的所有專案和問卷的權限。",
"delete_member_confirmation": "刪除的成員將無法存取您組織的所有工作區和調查問卷。",
"delete_organization": "刪除組織",
"delete_organization_description": "刪除包含所有專案的組織,包括所有問卷、回、人員、操作和屬性",
"delete_organization_description": "刪除組織及其所有工作區,包括所有調查問卷、回、人員、操作和屬性",
"delete_organization_warning": "在您繼續刪除此組織之前,請注意以下後果:",
"delete_organization_warning_1": "永久移除與此組織相關聯的所有專案。",
"delete_organization_warning_1": "永久移除與此組織連結的所有工作區。",
"delete_organization_warning_2": "此操作無法復原。一旦刪除,即永久消失。",
"delete_organization_warning_3": "請在下列欄位中輸入 '{'organizationName'}' 以確認永久刪除此組織:",
"eliminate_branding_with_whitelabel": "消除 Formbricks 品牌並啟用其他白標自訂選項。",
@@ -2620,7 +2638,9 @@
"security_list_tip_link": "請在此註冊。",
"share_invite_link": "分享邀請連結",
"share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:",
"test_email_sent_successfully": "測試電子郵件已成功發送"
"test_email_sent_successfully": "測試電子郵件已成功發送",
"unlock_ai_features_description": "AI 驅動的翻譯、智慧工具和資料分析功能在更高級別的方案中提供。立即升級,讓 AI 為您的問卷調查加持。",
"unlock_ai_features_with_a_higher_plan": "升級至更高級別方案以解鎖 AI 功能"
},
"notifications": {
"auto_subscribe_to_new_surveys": "自動訂閱新問卷",
@@ -2672,7 +2692,7 @@
"add_workspaces_description": "控管團隊成員可存取哪些工作區。",
"all_members_added": "所有成員都已新增至此團隊。",
"all_workspaces_added": "所有工作區都已加入此團隊。",
"are_you_sure_you_want_to_delete_this_team": "確定要刪除團隊嗎這也會移除此團隊相關的所有專案和問卷的存取權限。",
"are_you_sure_you_want_to_delete_this_team": "確定要刪除這個團隊嗎?這也會移除此團隊相關的所有工作區和調查問卷的存取權限。",
"billing_role_description": "只能存取帳單資訊。",
"bulk_invite": "大量邀請",
"contributor": "投稿人",
@@ -2688,10 +2708,10 @@
"manage": "管理",
"manage_team": "管理團隊",
"manage_team_disabled": "只有組織擁有者、管理員和團隊管理員才能管理團隊。",
"manager_role_description": "管理可以存取所有專案,並新增移除成員。",
"manager_role_description": "管理可以存取所有工作區,並可新增移除成員。",
"member": "成員",
"member_role_description": "成員可以在選定的專案中工作。",
"member_role_info_message": "若要授予新成員存取專案的權限,請將他們新增至下方的團隊。藉由團隊,您可以管理誰可以存取哪些專案。",
"member_role_description": "成員可以在選定的工作區中工作。",
"member_role_info_message": "若要新成員存取工作區,請在下方將他們加入團隊。透過團隊功能,你可以管理誰存取哪些工作區。",
"organization_role": "組織角色",
"owner_role_description": "擁有者對組織具有完全控制權。",
"please_fill_all_member_fields": "請填寫所有欄位以新增新成員。",
@@ -2708,8 +2728,8 @@
"team_settings_description": "管理團隊成員、存取權限等。",
"team_updated_successfully": "團隊已成功更新",
"teams": "團隊",
"teams_description": "將成員指派到團隊中,並授予團隊存取專案的權限。",
"unlock_teams_description": "管理哪些組織成員可以存取特定專案和問卷。",
"teams_description": "將成員分配到團隊,並授予團隊存取工作區的權限。",
"unlock_teams_description": "管理哪些組織成員可以存取特定的工作區和調查問卷。",
"unlock_teams_title": "使用更高等級的方案解鎖團隊。",
"upgrade_plan_notice_message": "使用更高等級的方案解鎖組織角色。",
"you_are_a_member": "您是成員"
@@ -2758,6 +2778,18 @@
"adjust_survey_closed_message": "調整「問卷已關閉」訊息",
"adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。",
"adjust_the_theme_in_the": "在",
"ai_data_analysis_disabled": "此組織已停用 AI 資料分析。",
"ai_features_not_enabled": "此組織未啟用 AI 功能。",
"ai_instance_not_configured": "AI 未設定。請聯絡您的管理員。",
"ai_smart_tools_disabled": "此組織已停用 AI 智慧工具。",
"ai_translate": "使用 AI 翻譯",
"ai_translating": "正在使用 AI 翻譯...請保持此視窗開啟。",
"ai_translation_all_fields_populated": "所有欄位都已翻譯",
"ai_translation_complete": "AI 翻譯完成",
"ai_translation_failed": "翻譯失敗",
"ai_translation_instance_not_configured": "此執行個體未設定 AI。請聯絡您的管理員。",
"ai_translation_not_available": "您目前的方案不提供 AI 翻譯功能。請升級以解鎖此功能。",
"ai_translation_not_enabled": "此組織已停用 AI 智慧工具。請在組織設定中啟用。",
"all_are_true": "全部為真",
"all_other_answers_will_continue_to": "所有其他答案將繼續",
"allow_multi_select": "允許多重選取",
@@ -3043,7 +3075,7 @@
"options_used_in_logic_bulk_error": "以下選項已用於邏輯中:{questionIndexes}。請先從邏輯中移除它們。",
"override_theme_with_individual_styles_for_this_survey": "使用此問卷的個別樣式覆寫主題。",
"overwrite_global_waiting_time": "自訂冷卻期",
"overwrite_global_waiting_time_description": "僅覆蓋此問卷的專案設定。",
"overwrite_global_waiting_time_description": "僅針對此調查問卷覆寫工作區設定。",
"overwrite_placement": "覆寫位置",
"overwrite_survey_logo": "設定自訂問卷標誌",
"overwrite_the_global_placement_of_the_survey": "覆寫問卷的整體位置",
@@ -3602,16 +3634,21 @@
"team_settings_description": "查看哪些團隊可以存取此工作區。"
},
"unify": {
"add_feedback_record": "新增回饋記錄",
"add_feedback_record_description": "手動建立回饋記錄。",
"add_feedback_source": "新增回饋來源",
"add_source": "新增來源",
"allowed_values": "允許的值:{values}",
"api_ingestion": "API ingestion",
"api_ingestion_manage_api_keys": "Manage API keys",
"api_ingestion_settings_description": "Send feedback records using the Management API.",
"auto_generated": "自動生成",
"change_file": "更換檔案",
"click_load_sample_csv": "點擊「載入範例 CSV」以查看欄位",
"click_to_upload": "點擊以上傳",
"collected_at": "收集時間",
"configure_import": "設定匯入",
"configure_mapping": "設定對應關係",
"connection": "連線",
"connector_created_successfully": "連接器建立成功",
"connector_deleted_successfully": "連接器刪除成功",
"connector_duplicated_successfully": "連接器複製成功",
@@ -3630,9 +3667,12 @@
"csv_import_duplicate_warning": "匯入已經匯入過的資料,可能會產生重複紀錄。",
"csv_inconsistent_columns": "第 {row} 列的欄位數不一致。所有列必須有相同的標題。",
"csv_max_records": "最多允許 {max} 筆紀錄。",
"custom_source_type": "自訂來源類型",
"custom_source_type_placeholder": "輸入自訂來源類型",
"default_connector_name_csv": "CSV 匯入",
"default_connector_name_formbricks": "Formbricks 問卷連線",
"deselect_all": "取消全選",
"discard_feedback_record_changes_description": "如果關閉此抽屜,您的變更將會遺失。",
"discard_feedback_record_changes_title": "放棄未儲存的變更?",
"drop_a_field_here": "請將欄位拖曳到這裡",
"drop_field_or": "拖曳欄位或",
"edit_csv_mapping": "編輯 CSV 對應",
@@ -3642,47 +3682,64 @@
"enum": "enum",
"failed_to_load_feedback_records": "載入回饋紀錄失敗",
"feedback_date": "目前日期",
"feedback_record_created_successfully": "回饋記錄創建成功",
"feedback_record_details": "反饋記錄詳情",
"feedback_record_details_description": "查看並更新回饋記錄欄位。",
"feedback_record_directory": "意見回饋記錄目錄",
"feedback_record_fields": "回饋紀錄欄位",
"feedback_record_mcp": "Feedback Record MCP",
"feedback_record_updated_successfully": "回饋記錄更新成功",
"feedback_record_value_required": "所選欄位類型需要一個值",
"feedback_records": "回饋紀錄",
"feedback_records_refreshed": "回饋紀錄已更新",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "New records from these sources will be stored in: {directoryNames}",
"feedback_sources_directory_access_single": "New records from this source will be stored in: {directoryNames}",
"feedback_sources_settings_description": "Connect and manage all feedback sources for this workspace.",
"field_group_id": "字段組 ID",
"field_group_label": "字段組標籤",
"field_id": "欄位ID",
"field_label": "欄位標籤",
"field_type": "欄位類型",
"formbricks_surveys": "Formbricks 問卷",
"frd_cannot_be_changed": "意見回饋目錄在建立後無法變更。",
"go_to_feedback_record_directories": "前往目錄設定",
"historical_import_complete": "匯入完成:{successes} 筆成功,{failures} 筆失敗,{skipped} 筆略過(無資料)",
"import_csv_data": "匯入 CSV 資料",
"import_feedback": "匯入回饋",
"import_historical_responses": "Import historical responses",
"import_historical_responses_description": "Import existing responses from this survey now.",
"import_rows": "匯入 {count} 筆資料",
"import_via_source_name": "透過“{sourceName}”導入",
"importing_data": "正在匯入資料…",
"importing_historical_data": "正在匯入歷史資料…",
"invalid_enum_values": "對應到 {field} 欄位的值無效",
"invalid_values_found": "發現:{values}(列:{rows}{extra}",
"load_sample_csv": "載入範例 CSV",
"n_supported_questions": "{count} 個支援的問題",
"manage_directories": "Manage directories",
"manage_feedback_sources": "管理回饋來源",
"metadata": "元數據",
"metadata_key": "元資料鍵",
"metadata_read_only_entries": "唯讀元資料值(非字串)",
"metadata_value": "元資料值",
"missing_feedback_source_title": "Missing feedback source?",
"no_feedback_record_directory_available": "此工作區尚未指派意見回饋記錄目錄。請先建立或指派一個目錄。",
"no_feedback_records": "目前尚無回饋紀錄。當你的連接器開始傳送資料時,紀錄會顯示在這裡。",
"no_source_fields_loaded": "尚未載入來源欄位",
"no_sources_connected": "尚未連接任何來源。請新增來源以開始使用。",
"no_surveys_found": "此環境中找不到問卷",
"optional": "選填",
"or_drag_and_drop": "或拖曳檔案",
"question_selected": "已選擇 <strong>{count}</strong> 題。每份這些題目的回應都會建立一筆新的意見紀錄。",
"question_type_not_supported": "不支援此題型",
"questions_selected": "已選擇 <strong>{count}</strong> 題。每份這些題目的回應都會建立一筆新的意見紀錄。",
"records_will_go_to": "記錄將傳送至",
"refresh_feedback_records": "重新整理回饋紀錄",
"refreshing_feedback_records": "正在更新回饋紀錄…",
"request_feedback_source": "Request source integration",
"required": "必填",
"save_changes": "儲存變更",
"select_a_survey_to_see_questions": "請選擇問卷以查看其問題",
"select_a_value": "請選擇一個值...",
"select_all": "全選",
"select_feedback_record_directory": "選擇目錄",
"select_feedback_record_source_type": "選擇來源類型",
"select_questions": "選擇問題",
"select_source_type_description": "請選擇你想要連接的回饋來源類型。",
"select_source_type_prompt": "請選擇你想要連接的回饋來源類型:",
"select_survey": "選擇問卷",
"select_survey_and_questions": "選擇問卷與問題",
"select_survey_questions_description": "請選擇哪些問卷問題要建立 FeedbackRecords。",
@@ -3692,27 +3749,38 @@
"showing_rows": "顯示 {count} 筆資料中的 3 筆",
"source": "來源",
"source_connect_csv_description": "從 CSV 檔案匯入回饋",
"source_connect_feedback_record_mcp_description": "Send feedback records through the MCP integration.",
"source_connect_formbricks_description": "連接來自你 Formbricks 問卷的回饋",
"source_fields": "來源欄位",
"source_id": "來源ID",
"source_name": "來源名稱",
"source_type": "來源類型",
"source_type_cannot_be_changed": "來源類型無法變更",
"sources": "來源",
"status_active": "進行中",
"status_completed": "已完成",
"status_draft": "草稿",
"source_type_label_feedback_form": "Feedback form",
"source_type_label_interview": "Interview",
"source_type_label_nps_campaign": "NPS campaign",
"source_type_label_review": "Review",
"source_type_label_social": "Social",
"source_type_label_support": "Support",
"source_type_label_survey": "Survey",
"source_type_label_usability_test": "Usability test",
"status_error": "錯誤",
"status_paused": "已暫停",
"status_live_sync": "Live sync",
"status_ready": "Ready",
"submission_id": "提交ID",
"survey_has_no_questions": "此問卷沒有任何題目",
"survey_import_line": "{surveyName}{responseCount} 份回應 × {questionCount} 題 = {total} 筆意見紀錄",
"total_feedback_records": "總計:{surveyCount} 份問卷中已選擇 {checked} / {total} 筆意見紀錄",
"topics_and_subtopics": "主題與子主題",
"unify_feedback": "整合回饋",
"update_mapping_description": "更新此來源的對應設定。",
"updated_at": "更新時間",
"upload_csv_data_description": "上傳 CSV 檔案以匯入回饋資料。",
"upload_csv_file": "上傳 CSV 檔案",
"user_identifier": "使用者",
"value": "值"
"value": "值",
"value_boolean": "值(布林值)",
"value_date": "值(日期)",
"value_number": "值(數量)",
"value_text": "值(文字)"
},
"xm-templates": {
"ces": "CES",
@@ -224,7 +224,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
// Fetch updated response with relations for pipeline
const updatedResponseForPipeline = await getResponseForPipeline(params.responseId);
if (updatedResponseForPipeline.ok) {
sendToPipeline({
await sendToPipeline({
event: "responseUpdated",
workspaceId: workspaceIdResult.data.workspaceId,
surveyId: existingResponse.data.surveyId,
@@ -232,7 +232,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
});
if (response.data.finished) {
sendToPipeline({
await sendToPipeline({
event: "responseFinished",
workspaceId: workspaceIdResult.data.workspaceId,
surveyId: existingResponse.data.surveyId,
@@ -1,235 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
const {
mockAuthenticatedApiClient,
mockCreateResponseWithQuotaEvaluation,
mockCreatedResponse,
mockFormatValidationErrorsForV2Api,
mockGetResponseForPipeline,
mockGetSurveyQuestions,
mockGetWorkspaceId,
mockHandleApiError,
mockHasPermission,
mockSendToPipeline,
mockValidateFileUploads,
mockValidateOtherOptionLengthForMultipleChoice,
mockValidateResponseData,
} = vi.hoisted(() => ({
mockAuthenticatedApiClient: vi.fn(),
mockCreateResponseWithQuotaEvaluation: vi.fn(),
mockCreatedResponse: vi.fn(),
mockFormatValidationErrorsForV2Api: vi.fn(),
mockGetResponseForPipeline: vi.fn(),
mockGetSurveyQuestions: vi.fn(),
mockGetWorkspaceId: vi.fn(),
mockHandleApiError: vi.fn(),
mockHasPermission: vi.fn(),
mockSendToPipeline: vi.fn(),
mockValidateFileUploads: vi.fn(),
mockValidateOtherOptionLengthForMultipleChoice: vi.fn(),
mockValidateResponseData: vi.fn(),
}));
vi.mock("@/app/lib/pipelines", () => ({
sendToPipeline: mockSendToPipeline,
}));
vi.mock("@/modules/api/lib/validation", () => ({
formatValidationErrorsForV2Api: mockFormatValidationErrorsForV2Api,
validateResponseData: mockValidateResponseData,
}));
vi.mock("@/modules/api/v2/auth/authenticated-api-client", () => ({
authenticatedApiClient: mockAuthenticatedApiClient,
}));
vi.mock("@/modules/api/v2/lib/element", () => ({
validateOtherOptionLengthForMultipleChoice: mockValidateOtherOptionLengthForMultipleChoice,
}));
vi.mock("@/modules/api/v2/lib/response", () => ({
responses: {
createdResponse: mockCreatedResponse,
successResponse: vi.fn(),
},
}));
vi.mock("@/modules/api/v2/lib/utils", () => ({
handleApiError: mockHandleApiError,
}));
vi.mock("@/modules/api/v2/management/lib/helper", () => ({
getWorkspaceId: mockGetWorkspaceId,
}));
vi.mock("@/modules/api/v2/management/responses/[responseId]/lib/survey", () => ({
getSurveyQuestions: mockGetSurveyQuestions,
}));
vi.mock("@/modules/api/v2/management/responses/[responseId]/lib/response", () => ({
getResponseForPipeline: mockGetResponseForPipeline,
}));
vi.mock("@/modules/organization/settings/api-keys/lib/utils", () => ({
hasPermission: mockHasPermission,
}));
vi.mock("@/modules/storage/utils", () => ({
resolveStorageUrlsInObject: vi.fn((value) => value),
validateFileUploads: mockValidateFileUploads,
}));
vi.mock("./lib/response", () => ({
createResponseWithQuotaEvaluation: mockCreateResponseWithQuotaEvaluation,
getResponses: vi.fn(),
}));
const workspaceId = "cm9workspace000108l4abcz12";
const surveyId = "cm9survey000108l4abcz12zz";
const responseId = "cm9response000108l4abcz12";
const createdAt = new Date("2026-04-13T10:00:00.000Z");
const createdResponse = {
contactAttributes: null,
contactId: null,
createdAt,
data: {},
displayId: null,
endingId: null,
finished: true,
id: responseId,
language: null,
meta: {},
singleUseId: null,
surveyId,
ttc: {},
updatedAt: createdAt,
variables: {},
};
const responseSnapshot = {
contact: null,
contactAttributes: null,
createdAt,
data: {},
displayId: null,
endingId: null,
finished: true,
id: responseId,
language: null,
meta: {},
singleUseId: null,
surveyId,
tags: [],
ttc: {},
updatedAt: createdAt,
variables: {},
};
describe("POST /modules/api/v2/management/responses", () => {
beforeEach(() => {
vi.clearAllMocks();
mockAuthenticatedApiClient.mockImplementation(
async ({ handler }) =>
await handler({
auditLog: undefined,
authentication: {
workspacePermissions: [{ workspaceId, actions: ["POST"] }],
},
parsedInput: {
body: {
data: {},
finished: true,
surveyId,
},
},
})
);
mockGetWorkspaceId.mockResolvedValue({ data: { workspaceId }, ok: true });
mockHasPermission.mockReturnValue(true);
mockGetSurveyQuestions.mockResolvedValue({ data: { blocks: [], questions: [] }, ok: true });
mockValidateFileUploads.mockReturnValue(true);
mockValidateOtherOptionLengthForMultipleChoice.mockReturnValue(undefined);
mockValidateResponseData.mockReturnValue(null);
mockSendToPipeline.mockResolvedValue(undefined);
mockCreateResponseWithQuotaEvaluation.mockResolvedValue({ data: createdResponse, ok: true });
mockGetResponseForPipeline.mockResolvedValue({ data: responseSnapshot, ok: true });
mockCreatedResponse.mockImplementation((body: unknown) => Response.json(body, { status: 201 }));
mockHandleApiError.mockImplementation((_, error) => Response.json({ error }, { status: 400 }));
});
test("passes the freshly hydrated response snapshot to the pipeline", async () => {
const { POST } = await import("./route");
const response = await POST(
new Request("http://localhost/api/v2/management/responses", { method: "POST" })
);
expect(response.status).toBe(201);
expect(mockGetResponseForPipeline).toHaveBeenCalledWith(responseId);
expect(mockSendToPipeline).toHaveBeenNthCalledWith(1, {
event: "responseCreated",
response: responseSnapshot,
surveyId,
workspaceId,
});
expect(mockSendToPipeline).toHaveBeenNthCalledWith(2, {
event: "responseFinished",
response: responseSnapshot,
surveyId,
workspaceId,
});
});
test("returns 201 when loading the pipeline snapshot throws", async () => {
mockGetResponseForPipeline.mockRejectedValueOnce(new Error("snapshot failed"));
const { POST } = await import("./route");
const response = await POST(
new Request("http://localhost/api/v2/management/responses", { method: "POST" })
);
expect(response.status).toBe(201);
expect(mockSendToPipeline).not.toHaveBeenCalled();
});
test("returns 201 when pipeline dispatch rejects", async () => {
mockSendToPipeline.mockRejectedValueOnce(new Error("pipeline failed"));
const { POST } = await import("./route");
const response = await POST(
new Request("http://localhost/api/v2/management/responses", { method: "POST" })
);
await Promise.resolve();
expect(response.status).toBe(201);
expect(mockSendToPipeline).toHaveBeenCalledTimes(2);
});
test("returns the create-response error payload when response creation fails", async () => {
mockCreateResponseWithQuotaEvaluation.mockResolvedValueOnce({
ok: false,
error: {
type: "bad_request",
details: [{ field: "surveyId", issue: "invalid" }],
},
});
const { POST } = await import("./route");
const response = await POST(
new Request("http://localhost/api/v2/management/responses", { method: "POST" })
);
expect(response.status).toBe(400);
expect(mockHandleApiError).toHaveBeenCalledWith(
expect.any(Request),
{
type: "bad_request",
details: [{ field: "surveyId", issue: "invalid" }],
},
undefined
);
expect(mockSendToPipeline).not.toHaveBeenCalled();
});
});
@@ -1,6 +1,5 @@
import { Response } from "@prisma/client";
import { NextRequest } from "next/server";
import { logger } from "@formbricks/logger";
import { sendToPipeline } from "@/app/lib/pipelines";
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
@@ -16,31 +15,6 @@ import { hasPermission } from "@/modules/organization/settings/api-keys/lib/util
import { resolveStorageUrlsInObject, validateFileUploads } from "@/modules/storage/utils";
import { createResponseWithQuotaEvaluation, getResponses } from "./lib/response";
const queueResponsePipelineEvent = ({
event,
response,
surveyId,
workspaceId,
}: Parameters<typeof sendToPipeline>[0]): void => {
void sendToPipeline({
event,
response,
surveyId,
workspaceId,
}).catch((error: unknown) => {
logger.error(
{
err: error,
event,
responseId: response.id,
surveyId,
workspaceId,
},
"Failed to send response event to pipeline"
);
});
};
export const GET = async (request: NextRequest) =>
authenticatedApiClient({
request,
@@ -114,14 +88,13 @@ export const POST = async (request: Request) =>
);
}
// if there is a createdAt but no updatedAt, set updatedAt to createdAt
if (body.createdAt && !body.updatedAt) {
body.updatedAt = body.createdAt;
}
const surveyQuestions = await getSurveyQuestions(body.surveyId);
if (!surveyQuestions.ok) {
return handleApiError(request, surveyQuestions.error as ApiErrorResponseV2, auditLog); // NOSONAR // We need to assert or we get a type error
return handleApiError(request, surveyQuestions.error as ApiErrorResponseV2, auditLog); // NOSONAR
}
if (!validateFileUploads(body.data, surveyQuestions.data.questions)) {
@@ -135,7 +108,6 @@ export const POST = async (request: Request) =>
);
}
// Validate response data for "other" options exceeding character limit
const otherResponseInvalidQuestionId = validateOtherOptionLengthForMultipleChoice({
responseData: body.data,
surveyQuestions: surveyQuestions.data.questions,
@@ -157,7 +129,6 @@ export const POST = async (request: Request) =>
});
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
surveyQuestions.data.blocks,
body.data,
@@ -181,37 +152,27 @@ export const POST = async (request: Request) =>
return handleApiError(request, createResponseResult.error, auditLog);
}
// Fetch created response with relations for pipeline
try {
const createdResponseForPipeline = await getResponseForPipeline(createResponseResult.data.id);
if (createdResponseForPipeline.ok) {
queueResponsePipelineEvent({
event: "responseCreated",
workspaceId,
surveyId: body.surveyId,
response: createdResponseForPipeline.data,
});
if (createResponseResult.data.finished) {
queueResponsePipelineEvent({
event: "responseFinished",
getResponseForPipeline(createResponseResult.data.id)
.then((createdResponseForPipeline) => {
if (createdResponseForPipeline.ok) {
sendToPipeline({
event: "responseCreated",
workspaceId,
surveyId: body.surveyId,
response: createdResponseForPipeline.data,
});
}).catch(() => {});
if (createResponseResult.data.finished) {
sendToPipeline({
event: "responseFinished",
workspaceId,
surveyId: body.surveyId,
response: createdResponseForPipeline.data,
}).catch(() => {});
}
}
}
} catch (error) {
logger.error(
{
err: error,
responseId: createResponseResult.data.id,
surveyId: body.surveyId,
workspaceId,
},
"Failed to load response data for pipeline dispatch"
);
}
})
.catch(() => {});
if (auditLog) {
auditLog.targetId = createResponseResult.data.id;
@@ -0,0 +1,94 @@
"use server";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { assertOrganizationAIConfigured, getOrganizationAIConfig } from "@/lib/ai/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import {
getOrganizationIdFromSurveyId,
getOrganizationIdFromWorkspaceId,
getWorkspaceIdFromSurveyId,
} from "@/lib/utils/helper";
import { ZAITranslationField, translateFields } from "./translate-fields";
const ZCheckAITranslationAvailableAction = z.object({
surveyId: ZId,
});
export const checkAITranslationAvailableAction = authenticatedActionClient
.inputSchema(ZCheckAITranslationAvailableAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
},
],
});
const aiConfig = await getOrganizationAIConfig(organizationId);
if (!aiConfig.isAISmartToolsEntitled) {
return { available: false, reason: "not_in_plan" as const };
}
if (!aiConfig.isAISmartToolsEnabled) {
return { available: false, reason: "not_enabled" as const };
}
if (!aiConfig.isInstanceConfigured) {
return { available: false, reason: "instance_not_configured" as const };
}
return { available: true };
});
const ZTranslateSurveyFieldsAction = z.object({
workspaceId: ZId,
fields: z.array(ZAITranslationField).min(1),
sourceLanguage: z.string().min(1),
targetLanguage: z.string().min(1),
});
export const translateSurveyFieldsAction = authenticatedActionClient
.inputSchema(ZTranslateSurveyFieldsAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId: parsedInput.workspaceId,
minPermission: "readWrite",
},
],
});
await assertOrganizationAIConfigured(organizationId, "smartTools");
const translations = await translateFields({
organizationId,
fields: parsedInput.fields,
sourceLanguage: parsedInput.sourceLanguage,
targetLanguage: parsedInput.targetLanguage,
});
return { translations };
});
@@ -0,0 +1,70 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { type TAITranslationField, translateFields } from "./translate-fields";
vi.mock("server-only", () => ({}));
const mockGenerateOrganizationAIText = vi.fn();
vi.mock("@/lib/ai/service", () => ({
generateOrganizationAIText: (...args: unknown[]) => mockGenerateOrganizationAIText(...args),
}));
const baseInput = {
organizationId: "org-1",
sourceLanguage: "English",
targetLanguage: "German",
};
const fields: TAITranslationField[] = [
{ path: "headline", defaultText: "Welcome", isRichText: false },
{ path: "description", defaultText: "<p>Hello</p>", isRichText: true },
];
describe("translateFields", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("returns translated fields from clean JSON response", async () => {
mockGenerateOrganizationAIText.mockResolvedValue({
text: JSON.stringify({ headline: "Willkommen", description: "<p>Hallo</p>" }),
});
const result = await translateFields({ ...baseInput, fields });
expect(result).toEqual({ headline: "Willkommen", description: "<p>Hallo</p>" });
});
test("strips markdown code fences", async () => {
mockGenerateOrganizationAIText.mockResolvedValue({
text: '```json\n{"headline": "Willkommen"}\n```',
});
const result = await translateFields({ ...baseInput, fields });
expect(result).toEqual({ headline: "Willkommen" });
});
test("extracts JSON from wrapper text", async () => {
mockGenerateOrganizationAIText.mockResolvedValue({
text: 'Here is the translation:\n{"headline": "Willkommen"}\nHope this helps!',
});
const result = await translateFields({ ...baseInput, fields });
expect(result).toEqual({ headline: "Willkommen" });
});
test("filters out unrequested keys and non-string values", async () => {
mockGenerateOrganizationAIText.mockResolvedValue({
text: JSON.stringify({ headline: "Willkommen", extra: "Nein", description: 42 }),
});
const result = await translateFields({ ...baseInput, fields });
expect(result).toEqual({ headline: "Willkommen" });
});
test("throws on unparseable response", async () => {
mockGenerateOrganizationAIText.mockResolvedValue({ text: "not json at all" });
await expect(translateFields({ ...baseInput, fields })).rejects.toThrow(
"Failed to parse AI translation response"
);
});
});
@@ -0,0 +1,82 @@
import "server-only";
import { z } from "zod";
import { logger } from "@formbricks/logger";
import { generateOrganizationAIText } from "@/lib/ai/service";
export const ZAITranslationField = z.object({
path: z.string(),
defaultText: z.string(),
isRichText: z.boolean(),
});
export type TAITranslationField = z.infer<typeof ZAITranslationField>;
interface TranslateFieldsInput {
organizationId: string;
fields: TAITranslationField[];
sourceLanguage: string;
targetLanguage: string;
}
export const translateFields = async ({
organizationId,
fields,
sourceLanguage,
targetLanguage,
}: TranslateFieldsInput): Promise<Record<string, string>> => {
const items = fields.map((f) => ({
key: f.path,
text: f.defaultText,
richText: f.isRichText,
}));
const systemPrompt = `You are a professional translator for survey content. Translate the provided survey fields from ${sourceLanguage} to ${targetLanguage}.
Rules:
- Return ONLY a valid JSON object mapping each "key" to its translated text.
- For rich text fields (richText: true), preserve all HTML tags exactly as they are. Only translate the text content within the tags.
- Preserve any {{variable}} patterns exactly as they are do not translate text inside double curly braces.
- Do not add any explanation, markdown formatting, or extra text return raw JSON only.`;
const result = await generateOrganizationAIText({
organizationId,
capability: "smartTools",
system: systemPrompt,
prompt: JSON.stringify(items),
});
// Parse AI response as JSON.
// 1. Strip markdown code fences if present, then try JSON.parse directly.
// 2. Fall back to extracting the first {...} block for wrapper text.
let parsed: Record<string, string>;
try {
const stripped = result.text.replaceAll(/^```(?:json)?\s*\n?|\n?```\s*$/g, "").trim();
try {
parsed = JSON.parse(stripped);
} catch {
const start = stripped.indexOf("{");
const end = stripped.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) {
throw new Error("No JSON object found in AI response");
}
parsed = JSON.parse(stripped.slice(start, end + 1));
}
} catch (parseError) {
logger.error(
{ rawResponse: result.text.slice(0, 500), parseError },
"Failed to parse AI translation response"
);
throw new Error("Failed to parse AI translation response");
}
// Validate and filter to only requested keys
const validKeys = new Set(fields.map((f) => f.path));
const translations: Record<string, string> = {};
for (const [key, value] of Object.entries(parsed)) {
if (validKeys.has(key) && typeof value === "string") {
translations[key] = value;
}
}
return translations;
};
@@ -31,6 +31,7 @@ interface AddToDashboardDialogProps {
onDashboardSelect: (id: string) => void;
onConfirm: () => void;
isSaving: boolean;
showChartNameField?: boolean;
}
export function AddToDashboardDialog({
@@ -43,6 +44,7 @@ export function AddToDashboardDialog({
onDashboardSelect,
onConfirm,
isSaving,
showChartNameField = true,
}: Readonly<AddToDashboardDialogProps>) {
const { t } = useTranslation();
@@ -57,17 +59,19 @@ export function AddToDashboardDialog({
</DialogHeader>
<DialogBody>
<div className="space-y-4">
<div>
<Label htmlFor="chart-name">{t("workspace.analysis.charts.chart_name")}</Label>
<Input
id="chart-name"
className="mt-2"
placeholder={t("workspace.analysis.charts.chart_name_placeholder")}
value={chartName}
onChange={(e) => onChartNameChange(e.target.value)}
maxLength={255}
/>
</div>
{showChartNameField && (
<div>
<Label htmlFor="chart-name">{t("workspace.analysis.charts.chart_name")}</Label>
<Input
id="chart-name"
className="mt-2"
placeholder={t("workspace.analysis.charts.chart_name_placeholder")}
value={chartName}
onChange={(e) => onChartNameChange(e.target.value)}
maxLength={255}
/>
</div>
)}
<div>
<Label htmlFor="dashboard-select">{t("workspace.analysis.charts.dashboard")}</Label>
<Select value={selectedDashboardId} onValueChange={onDashboardSelect}>
@@ -103,7 +107,10 @@ export function AddToDashboardDialog({
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
{t("common.cancel")}
</Button>
<Button onClick={onConfirm} loading={isSaving} disabled={!selectedDashboardId || !chartName.trim()}>
<Button
onClick={onConfirm}
loading={isSaving}
disabled={!selectedDashboardId || (showChartNameField && !chartName.trim())}>
{t("workspace.analysis.charts.add_to_dashboard")}
</Button>
</DialogFooter>
@@ -31,6 +31,7 @@ interface AdvancedChartBuilderProps {
hidePreview?: boolean;
onChartGenerated?: (data: AnalyticsResponse) => void;
feedbackRecordDirectoryId: string | null;
runQueryCtaLabel?: string;
}
const ACTION = {
@@ -84,6 +85,7 @@ export function AdvancedChartBuilder({
hidePreview = false,
onChartGenerated,
feedbackRecordDirectoryId,
runQueryCtaLabel,
}: Readonly<AdvancedChartBuilderProps>) {
const { t } = useTranslation();
const parsedInitial = initialQuery ? parseQueryToState(initialQuery) : null;
@@ -151,11 +153,7 @@ export function AdvancedChartBuilder({
return (
<div className={hidePreview ? "space-y-2" : "grid gap-4 lg:grid-cols-2"}>
<div className="mx-1 space-y-2">
{!hidePreview && (
<>
<ChartTypeSelector selectedChartType={chartType} onChartTypeSelect={() => {}} />
</>
)}
{!hidePreview && <ChartTypeSelector selectedChartType={chartType} onChartTypeSelect={() => {}} />}
<div className="mt-4 flex w-full flex-col gap-3 overflow-hidden rounded-lg border bg-slate-50 p-4">
<MeasuresPanel
@@ -249,7 +247,11 @@ export function AdvancedChartBuilder({
<div className="flex justify-end">
<Button onClick={handleRunQuery} disabled={isLoading || !hasConfigChanged}>
{isLoading ? <LoadingSpinner /> : t("workspace.analysis.charts.create_chart")}
{isLoading ? (
<LoadingSpinner />
) : (
(runQueryCtaLabel ?? t("workspace.analysis.charts.create_chart"))
)}
</Button>
</div>
</div>
@@ -7,25 +7,31 @@ import { DialogFooter } from "@/modules/ui/components/dialog";
interface ChartDialogFooterProps {
onSaveClick: () => void;
onAddToDashboardClick: () => void;
onAddToDashboardClick?: () => void;
isSaving: boolean;
saveLabel?: string;
showAddToDashboard?: boolean;
}
export function ChartDialogFooter({
onSaveClick,
onAddToDashboardClick,
isSaving,
saveLabel,
showAddToDashboard = true,
}: Readonly<ChartDialogFooterProps>) {
const { t } = useTranslation();
return (
<DialogFooter>
<Button variant="outline" onClick={onAddToDashboardClick} disabled={isSaving}>
<PlusIcon className="mr-2 h-4 w-4" />
{t("workspace.analysis.charts.add_to_dashboard")}
</Button>
{showAddToDashboard && onAddToDashboardClick && (
<Button variant="outline" onClick={onAddToDashboardClick} disabled={isSaving}>
<PlusIcon className="mr-2 h-4 w-4" />
{t("workspace.analysis.charts.add_to_dashboard")}
</Button>
)}
<Button onClick={onSaveClick} disabled={isSaving}>
<SaveIcon className="mr-2 h-4 w-4" />
{t("workspace.analysis.charts.save_chart")}
{saveLabel ?? t("workspace.analysis.charts.save_chart")}
</Button>
</DialogFooter>
);
@@ -1,12 +1,14 @@
"use client";
import { CopyIcon, MoreVertical, SquarePenIcon, TrashIcon } from "lucide-react";
import { CopyIcon, MoreVertical, PlusIcon, SquarePenIcon, TrashIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { deleteChartAction, duplicateChartAction } from "@/modules/ee/analysis/charts/actions";
import { AddToDashboardDialog } from "@/modules/ee/analysis/charts/components/add-to-dashboard-dialog";
import { addChartToDashboardAction, getDashboardsAction } from "@/modules/ee/analysis/dashboards/actions";
import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -31,6 +33,45 @@ export function ChartDropdownMenu({ workspaceId, chart, onEdit }: Readonly<Chart
const [isDeleting, setIsDeleting] = useState(false);
const [isDuplicating, setIsDuplicating] = useState(false);
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
const [isAddToDashboardDialogOpen, setIsAddToDashboardDialogOpen] = useState(false);
const [isAddingToDashboard, setIsAddingToDashboard] = useState(false);
const [dashboards, setDashboards] = useState<Array<{ id: string; name: string }>>([]);
const [selectedDashboardId, setSelectedDashboardId] = useState<string>();
useEffect(() => {
let cancelled = false;
if (!isAddToDashboardDialogOpen) {
return () => {
cancelled = true;
};
}
void getDashboardsAction({ workspaceId })
.then((result) => {
if (cancelled) {
return;
}
if (result?.data) {
setDashboards(result.data.map((dashboard) => ({ id: dashboard.id, name: dashboard.name })));
} else {
toast.error(getFormattedErrorMessage(result));
}
})
.catch((error) => {
if (cancelled) {
return;
}
const message =
error instanceof Error ? error.message : t("workspace.analysis.charts.failed_to_load_dashboards");
toast.error(message);
});
return () => {
cancelled = true;
};
}, [isAddToDashboardDialogOpen, workspaceId]);
const handleDeleteChart = async () => {
setIsDeleting(true);
@@ -70,6 +111,43 @@ export function ChartDropdownMenu({ workspaceId, chart, onEdit }: Readonly<Chart
}
};
const handleAddChartToDashboard = async () => {
if (!selectedDashboardId) {
toast.error(t("workspace.analysis.charts.please_select_dashboard"));
return;
}
setIsAddingToDashboard(true);
try {
const result = await addChartToDashboardAction({
workspaceId,
chartId: chart.id,
dashboardId: selectedDashboardId,
});
if (!result?.data) {
toast.error(
getFormattedErrorMessage(result) || t("workspace.analysis.charts.failed_to_add_chart_to_dashboard")
);
return;
}
toast.success(t("workspace.analysis.charts.chart_added_to_dashboard"));
setIsAddToDashboardDialogOpen(false);
setSelectedDashboardId(undefined);
router.refresh();
} catch (error) {
const message =
error instanceof Error
? error.message
: t("workspace.analysis.charts.failed_to_add_chart_to_dashboard");
toast.error(message);
} finally {
setIsAddingToDashboard(false);
}
};
return (
<div id={`chart-${chart.id}-actions`} data-testid="chart-dropdown-menu">
<DropdownMenu open={isDropDownOpen} onOpenChange={setIsDropDownOpen}>
@@ -102,6 +180,15 @@ export function ChartDropdownMenu({ workspaceId, chart, onEdit }: Readonly<Chart
{t("common.duplicate")}
</DropdownMenuItem>
<DropdownMenuItem
icon={<PlusIcon className="size-4" />}
onClick={() => {
setIsDropDownOpen(false);
setIsAddToDashboardDialogOpen(true);
}}>
{t("workspace.analysis.charts.add_to_dashboard")}
</DropdownMenuItem>
<DropdownMenuItem
icon={<TrashIcon className="size-4" />}
onClick={() => {
@@ -123,6 +210,23 @@ export function ChartDropdownMenu({ workspaceId, chart, onEdit }: Readonly<Chart
text={t("workspace.analysis.charts.delete_chart_confirmation")}
isDeleting={isDeleting}
/>
<AddToDashboardDialog
isOpen={isAddToDashboardDialogOpen}
onOpenChange={(open) => {
setIsAddToDashboardDialogOpen(open);
if (!open) {
setSelectedDashboardId(undefined);
}
}}
chartName={chart.name}
onChartNameChange={() => {}}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onConfirm={handleAddChartToDashboard}
isSaving={isAddingToDashboard}
showChartNameField={false}
/>
</div>
);
}
@@ -1,9 +1,12 @@
import { use } from "react";
import { getConnectorsWithMappings } from "@/lib/connector/service";
import { getTranslate } from "@/lingodotdev/server";
import { ChartsList } from "@/modules/ee/analysis/charts/components/charts-list";
import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button";
import { getChartsWithCreator } from "@/modules/ee/analysis/charts/lib/charts";
import { AnalysisPageLayout } from "@/modules/ee/analysis/components/analysis-page-layout";
import { NoFeedbackRecordsState } from "@/modules/ee/analysis/components/no-feedback-records-state";
import { hasFeedbackRecordsInDirectories } from "@/modules/ee/analysis/lib/feedback-records";
import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
@@ -35,22 +38,38 @@ interface ChartsListPageProps {
export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPageProps>) {
const t = await getTranslate();
const { isReadOnly } = await getWorkspaceAuth(workspaceId);
const chartsPromise = getChartsWithCreator(workspaceId);
const directories = await getFeedbackRecordDirectoriesByWorkspaceId(workspaceId);
const [directories, connectors] = await Promise.all([
getFeedbackRecordDirectoriesByWorkspaceId(workspaceId),
getConnectorsWithMappings(workspaceId),
]);
const hasFeedbackRecords = await hasFeedbackRecordsInDirectories(
directories.map((directory) => directory.id)
);
const chartsPromise = hasFeedbackRecords ? getChartsWithCreator(workspaceId) : null;
return (
<AnalysisPageLayout
pageTitle={t("common.analysis")}
workspaceId={workspaceId}
cta={
isReadOnly ? undefined : <CreateChartButton workspaceId={workspaceId} directories={directories} />
isReadOnly ? undefined : (
<CreateChartButton
workspaceId={workspaceId}
directories={directories}
buttonProps={{ disabled: !hasFeedbackRecords }}
/>
)
}>
<ChartsListContent
chartsPromise={chartsPromise}
workspaceId={workspaceId}
isReadOnly={isReadOnly}
directories={directories}
/>
{hasFeedbackRecords && chartsPromise ? (
<ChartsListContent
chartsPromise={chartsPromise}
workspaceId={workspaceId}
isReadOnly={isReadOnly}
directories={directories}
/>
) : (
<NoFeedbackRecordsState workspaceId={workspaceId} hasFeedbackSources={connectors.length > 0} />
)}
</AnalysisPageLayout>
);
}
@@ -4,28 +4,43 @@ import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { CreateChartDialog } from "@/modules/ee/analysis/charts/components/create-chart-dialog";
import { Button } from "@/modules/ui/components/button";
import { Button, type ButtonProps } from "@/modules/ui/components/button";
interface CreateChartButtonProps {
workspaceId: string;
directories: { id: string; name: string }[];
autoAddToDashboardId?: string;
label?: string;
onSuccess?: () => void;
showIcon?: boolean;
buttonProps?: Omit<ButtonProps, "onClick" | "children">;
}
export function CreateChartButton({ workspaceId, directories }: Readonly<CreateChartButtonProps>) {
export function CreateChartButton({
workspaceId,
directories,
autoAddToDashboardId,
label,
onSuccess,
showIcon = true,
buttonProps,
}: Readonly<CreateChartButtonProps>) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { t } = useTranslation();
return (
<>
<Button size="sm" onClick={() => setIsDialogOpen(true)}>
<PlusIcon className="mr-2 h-4 w-4" />
{t("workspace.analysis.charts.create_chart")}
<Button size="sm" onClick={() => setIsDialogOpen(true)} {...buttonProps}>
{showIcon && <PlusIcon className="mr-2 h-4 w-4" />}
{label ?? t("workspace.analysis.charts.create_chart")}
</Button>
<CreateChartDialog
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
workspaceId={workspaceId}
autoAddToDashboardId={autoAddToDashboardId}
directories={directories}
onSuccess={onSuccess}
/>
</>
);
@@ -1,7 +1,6 @@
"use client";
import { CreateChartView } from "@/modules/ee/analysis/charts/components/create-chart-view";
import { EditChartView } from "@/modules/ee/analysis/charts/components/edit-chart-view";
import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
export interface CreateChartDialogProps {
@@ -9,6 +8,7 @@ export interface CreateChartDialogProps {
onOpenChange: (open: boolean) => void;
workspaceId: string;
chartId?: string;
autoAddToDashboardId?: string;
initialChart?: TChartWithCreator;
onSuccess?: () => void;
directories: { id: string; name: string }[];
@@ -19,29 +19,19 @@ export function CreateChartDialog({
onOpenChange,
workspaceId,
chartId,
autoAddToDashboardId,
initialChart,
onSuccess,
directories,
}: Readonly<CreateChartDialogProps>) {
if (chartId) {
return (
<EditChartView
open={open}
onOpenChange={onOpenChange}
workspaceId={workspaceId}
chartId={chartId}
initialChart={initialChart}
onSuccess={onSuccess}
directories={directories}
/>
);
}
return (
<CreateChartView
open={open}
onOpenChange={onOpenChange}
workspaceId={workspaceId}
chartId={chartId}
initialChart={initialChart}
autoAddToDashboardId={autoAddToDashboardId}
onSuccess={onSuccess}
directories={directories}
/>
@@ -1,16 +1,19 @@
"use client";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { AddToDashboardDialog } from "@/modules/ee/analysis/charts/components/add-to-dashboard-dialog";
import { AdvancedChartBuilder } from "@/modules/ee/analysis/charts/components/advanced-chart-builder";
import { AIQuerySection } from "@/modules/ee/analysis/charts/components/ai-query-section";
import { ChartDialogFooter } from "@/modules/ee/analysis/charts/components/chart-dialog-footer";
import { ChartDialogLoadingView } from "@/modules/ee/analysis/charts/components/chart-dialog-loading-view";
import { ChartPreview } from "@/modules/ee/analysis/charts/components/chart-preview";
import { ManualChartBuilder } from "@/modules/ee/analysis/charts/components/manual-chart-builder";
import { SaveChartDialog } from "@/modules/ee/analysis/charts/components/save-chart-dialog";
import { useChartDialog } from "@/modules/ee/analysis/charts/hooks/use-chart-dialog";
import { FrdPicker } from "@/modules/ee/feedback-record-directory/components/frd-picker";
import { DEFAULT_CHART_TYPE } from "@/modules/ee/analysis/charts/lib/chart-types";
import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
import { Alert } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
@@ -19,11 +22,16 @@ import {
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface CreateChartViewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workspaceId: string;
chartId?: string;
initialChart?: TChartWithCreator;
autoAddToDashboardId?: string;
onSuccess?: () => void;
directories: { id: string; name: string }[];
}
@@ -32,32 +40,39 @@ export function CreateChartView({
open,
onOpenChange,
workspaceId,
chartId,
initialChart,
autoAddToDashboardId,
onSuccess,
directories,
}: Readonly<CreateChartViewProps>) {
const { t } = useTranslation();
const isEditing = !!chartId;
const {
chartData,
initialQuery,
isLoadingChart,
chartLoadError,
chartName,
setChartName,
selectedChartType,
handleChartTypeChange,
handleChartGenerated,
dashboards,
selectedDashboardId,
setSelectedDashboardId,
handleAddToDashboard,
handleSaveChart,
isSaving,
isSaveDialogOpen,
setIsSaveDialogOpen,
isAddToDashboardDialogOpen,
setIsAddToDashboardDialogOpen,
selectedDirectoryId,
setSelectedDirectoryId,
handleClose,
} = useChartDialog({ open, onOpenChange, workspaceId, onSuccess, directories });
} = useChartDialog({
open,
onOpenChange,
workspaceId,
chartId,
initialChart,
autoAddToDashboardId,
onSuccess,
directories,
});
const chartPreviewRef = useRef<HTMLDivElement>(null);
@@ -67,96 +82,139 @@ export function CreateChartView({
}
}, [chartData]);
if (isLoadingChart && isEditing && !initialChart) {
return <ChartDialogLoadingView open={open} onClose={handleClose} />;
}
if (isEditing && !isLoadingChart && !chartData && !initialChart && chartLoadError) {
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
<DialogContent width="wide">
<DialogHeader>
<DialogTitle>{t("common.error")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<DialogBody>
<div className="flex flex-col items-center justify-center gap-4 py-8">
<p className="text-sm text-red-600">{chartLoadError}</p>
<Button variant="outline" onClick={handleClose}>
{t("common.close")}
</Button>
</div>
</DialogBody>
</DialogContent>
</Dialog>
);
}
const chartType = selectedChartType ?? (isEditing ? (initialChart?.type ?? DEFAULT_CHART_TYPE) : undefined);
const hasSelectedDirectory = !!selectedDirectoryId;
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
<DialogContent className="max-h-[90vh] overflow-y-auto" width="wide" disableCloseOnOutsideClick>
<DialogContent
className="max-h-[90vh] overflow-y-auto"
width="wide"
disableCloseOnOutsideClick={!isEditing}>
<DialogHeader>
<DialogTitle>{t("workspace.analysis.charts.create_chart")}</DialogTitle>
<DialogDescription>{t("workspace.analysis.charts.create_chart_description")}</DialogDescription>
<DialogTitle>
{isEditing
? t("workspace.analysis.charts.edit_chart_title")
: t("workspace.analysis.charts.create_chart")}
</DialogTitle>
<DialogDescription>
{isEditing
? t("workspace.analysis.charts.edit_chart_description")
: t("workspace.analysis.charts.create_chart_description")}
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<FrdPicker
directories={directories}
selectedDirectoryId={selectedDirectoryId}
onChange={setSelectedDirectoryId}
workspaceId={workspaceId}
/>
{hasSelectedDirectory && (
{hasSelectedDirectory ? (
<>
<AIQuerySection
workspaceId={workspaceId}
onChartGenerated={handleChartGenerated}
feedbackRecordDirectoryId={selectedDirectoryId}
/>
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center">
<span className="bg-white px-2 text-sm text-gray-500">
{t("workspace.analysis.charts.OR")}
</span>
</div>
<div className="space-y-2">
<Label htmlFor="create-chart-name">{t("workspace.analysis.charts.chart_name")}</Label>
<Input
id="create-chart-name"
value={chartName}
onChange={(event) => setChartName(event.target.value)}
placeholder={t("workspace.analysis.charts.chart_name_placeholder")}
maxLength={255}
required
/>
</div>
<ManualChartBuilder
selectedChartType={selectedChartType}
onChartTypeSelect={handleChartTypeChange}
/>
{!isEditing && (
<>
<AIQuerySection
workspaceId={workspaceId}
onChartGenerated={handleChartGenerated}
feedbackRecordDirectoryId={selectedDirectoryId}
/>
{selectedChartType && (
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center">
<span className="bg-white px-2 text-sm text-gray-500">
{t("workspace.analysis.charts.OR")}
</span>
</div>
</div>
</>
)}
<ManualChartBuilder selectedChartType={chartType} onChartTypeSelect={handleChartTypeChange} />
{chartType && (
<AdvancedChartBuilder
workspaceId={workspaceId}
chartType={selectedChartType}
initialQuery={chartData?.query}
chartType={chartType}
initialQuery={chartData?.query ?? initialQuery}
hidePreview={true}
onChartGenerated={handleChartGenerated}
feedbackRecordDirectoryId={selectedDirectoryId}
runQueryCtaLabel={
chartData
? t("workspace.analysis.charts.update_chart")
: t("workspace.analysis.charts.preview_chart")
}
/>
)}
{chartData && (
{(isEditing || chartData) && (
<div ref={chartPreviewRef}>
<ChartPreview chartData={chartData} />
<ChartPreview chartData={chartData} isLoading={isLoadingChart} error={chartLoadError} />
</div>
)}
</>
) : (
<Alert variant="error" size="small">
<div>
<p>{t("workspace.analysis.charts.no_data_source_available")}</p>
<Link
className="mt-1 inline-block font-medium underline"
href={`/workspaces/${workspaceId}/settings/feedback-record-directories`}>
{t("workspace.analysis.charts.go_to_feedback_record_directories")}
</Link>
</div>
</Alert>
)}
</div>
</DialogBody>
{chartData && (
<>
<ChartDialogFooter
onSaveClick={() => setIsSaveDialogOpen(true)}
onAddToDashboardClick={() => setIsAddToDashboardDialogOpen(true)}
isSaving={isSaving}
/>
<SaveChartDialog
open={isSaveDialogOpen}
onOpenChange={setIsSaveDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
onSave={handleSaveChart}
isSaving={isSaving}
/>
<AddToDashboardDialog
isOpen={isAddToDashboardDialogOpen}
onOpenChange={setIsAddToDashboardDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onConfirm={handleAddToDashboard}
isSaving={isSaving}
/>
</>
<ChartDialogFooter
onSaveClick={handleSaveChart}
isSaving={isSaving}
showAddToDashboard={false}
saveLabel={
autoAddToDashboardId
? t("workspace.analysis.charts.save_and_add_to_dashboard")
: t("workspace.analysis.charts.save_chart")
}
/>
)}
</DialogContent>
</Dialog>
@@ -1,158 +0,0 @@
"use client";
import { useTranslation } from "react-i18next";
import { AddToDashboardDialog } from "@/modules/ee/analysis/charts/components/add-to-dashboard-dialog";
import { AdvancedChartBuilder } from "@/modules/ee/analysis/charts/components/advanced-chart-builder";
import { ChartDialogFooter } from "@/modules/ee/analysis/charts/components/chart-dialog-footer";
import { ChartDialogLoadingView } from "@/modules/ee/analysis/charts/components/chart-dialog-loading-view";
import { ChartPreview } from "@/modules/ee/analysis/charts/components/chart-preview";
import { ManualChartBuilder } from "@/modules/ee/analysis/charts/components/manual-chart-builder";
import { useChartDialog } from "@/modules/ee/analysis/charts/hooks/use-chart-dialog";
import { DEFAULT_CHART_TYPE } from "@/modules/ee/analysis/charts/lib/chart-types";
import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface EditChartViewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workspaceId: string;
chartId: string;
initialChart?: TChartWithCreator;
onSuccess?: () => void;
directories: { id: string; name: string }[];
}
export function EditChartView({
open,
onOpenChange,
workspaceId,
chartId,
initialChart,
onSuccess,
directories,
}: Readonly<EditChartViewProps>) {
const { t } = useTranslation();
const {
chartData,
initialQuery,
isLoadingChart,
chartLoadError,
chartName,
setChartName,
selectedChartType,
handleChartTypeChange,
handleChartGenerated,
dashboards,
selectedDashboardId,
setSelectedDashboardId,
handleAddToDashboard,
handleSaveChart,
isSaving,
isAddToDashboardDialogOpen,
setIsAddToDashboardDialogOpen,
selectedDirectoryId,
handleClose,
} = useChartDialog({ open, onOpenChange, workspaceId, chartId, initialChart, onSuccess, directories });
if (isLoadingChart && !initialChart) {
return <ChartDialogLoadingView open={open} onClose={handleClose} />;
}
if (!isLoadingChart && !chartData && !initialChart && chartLoadError) {
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
<DialogContent width="wide">
<DialogHeader>
<DialogTitle>{t("common.error")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<DialogBody>
<div className="flex flex-col items-center justify-center gap-4 py-8">
<p className="text-sm text-red-600">{chartLoadError}</p>
<Button variant="outline" onClick={handleClose}>
{t("common.close")}
</Button>
</div>
</DialogBody>
</DialogContent>
</Dialog>
);
}
const chartType = selectedChartType ?? DEFAULT_CHART_TYPE;
const directoryName = directories.find((d) => d.id === selectedDirectoryId)?.name;
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
<DialogContent className="max-h-[90vh] overflow-y-auto" width="wide">
<DialogHeader>
<DialogTitle>{t("workspace.analysis.charts.edit_chart_title")}</DialogTitle>
<DialogDescription>{t("workspace.analysis.charts.edit_chart_description")}</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4 px-1">
<div className="space-y-2">
<label htmlFor="edit-chart-name" className="text-sm">
{t("workspace.analysis.charts.chart_name")}
</label>
<Input
id="edit-chart-name"
value={chartName}
onChange={(e) => setChartName(e.target.value)}
placeholder={t("workspace.analysis.charts.chart_name_placeholder")}
className="w-full"
/>
</div>
{directoryName && (
<div className="space-y-2">
<Label>{t("workspace.analysis.charts.data_source")}</Label>
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600">
{directoryName}
</div>
</div>
)}
<div className="space-y-2">
<ManualChartBuilder selectedChartType={chartType} onChartTypeSelect={handleChartTypeChange} />
</div>
<AdvancedChartBuilder
workspaceId={workspaceId}
chartType={chartType}
initialQuery={chartData?.query ?? initialQuery}
hidePreview={true}
onChartGenerated={handleChartGenerated}
feedbackRecordDirectoryId={selectedDirectoryId}
/>
<ChartPreview chartData={chartData} isLoading={isLoadingChart} error={chartLoadError} />
</div>
</DialogBody>
<ChartDialogFooter
onSaveClick={handleSaveChart}
onAddToDashboardClick={() => setIsAddToDashboardDialogOpen(true)}
isSaving={isSaving}
/>
<AddToDashboardDialog
isOpen={isAddToDashboardDialogOpen}
onOpenChange={setIsAddToDashboardDialogOpen}
chartName={chartName}
onChartNameChange={setChartName}
dashboards={dashboards}
selectedDashboardId={selectedDashboardId}
onDashboardSelect={setSelectedDashboardId}
onConfirm={handleAddToDashboard}
isSaving={isSaving}
/>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,370 @@
/**
* @vitest-environment jsdom
*/
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
const mockCreateChartAction = vi.fn();
const mockUpdateChartAction = vi.fn();
const mockDeleteChartAction = vi.fn();
const mockGetChartAction = vi.fn();
const mockExecuteQueryAction = vi.fn();
const mockAddChartToDashboardAction = vi.fn();
const mockGetDashboardsAction = vi.fn();
const mockToastSuccess = vi.fn();
const mockToastError = vi.fn();
const mockRouterPush = vi.fn();
const mockRouterRefresh = vi.fn();
vi.mock("@/modules/ee/analysis/charts/actions", () => ({
createChartAction: (...args: any[]) => mockCreateChartAction(...args),
updateChartAction: (...args: any[]) => mockUpdateChartAction(...args),
deleteChartAction: (...args: any[]) => mockDeleteChartAction(...args),
getChartAction: (...args: any[]) => mockGetChartAction(...args),
executeQueryAction: (...args: any[]) => mockExecuteQueryAction(...args),
}));
vi.mock("@/modules/ee/analysis/dashboards/actions", () => ({
addChartToDashboardAction: (...args: any[]) => mockAddChartToDashboardAction(...args),
getDashboardsAction: (...args: any[]) => mockGetDashboardsAction(...args),
}));
vi.mock("@/modules/ee/analysis/charts/lib/chart-utils", () => ({
resolveChartType: (type: string) => type,
}));
vi.mock("@/lib/utils/helper", () => ({
getFormattedErrorMessage: (result: any) => result?.serverError ?? "formatted-error",
}));
vi.mock("react-hot-toast", () => ({
default: {
success: (...args: any[]) => mockToastSuccess(...args),
error: (...args: any[]) => mockToastError(...args),
},
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockRouterPush, refresh: mockRouterRefresh }),
}));
const { useChartDialog } = await import("./use-chart-dialog");
const WORKSPACE_ID = "ws-123";
const DIRECTORY_ID = "frd-1";
const CHART_ID = "chart-1";
const NEW_CHART_ID = "chart-new";
const DASHBOARD_ID = "dash-1";
const baseProps = {
open: true,
onOpenChange: vi.fn(),
workspaceId: WORKSPACE_ID,
directories: [{ id: DIRECTORY_ID, name: "Dir 1" }],
};
const sampleChartData = {
query: { foo: "bar" },
chartType: "bar" as const,
data: [],
};
const setHookReady = async (result: { current: ReturnType<typeof useChartDialog> }, withChartData = true) => {
await act(async () => {
if (withChartData) {
result.current.handleChartGenerated(sampleChartData as any);
}
result.current.setChartName("My Chart");
});
};
describe("useChartDialog", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe("handleSaveChart - create + auto-add", () => {
test("creates chart and adds to dashboard on success without cleanup", async () => {
mockCreateChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
mockAddChartToDashboardAction.mockResolvedValue({ data: { ok: true } });
const onOpenChange = vi.fn();
const onSuccess = vi.fn();
const { result } = renderHook(() =>
useChartDialog({
...baseProps,
onOpenChange,
onSuccess,
autoAddToDashboardId: DASHBOARD_ID,
})
);
await setHookReady(result);
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockCreateChartAction).toHaveBeenCalledTimes(1);
expect(mockAddChartToDashboardAction).toHaveBeenCalledWith({
workspaceId: WORKSPACE_ID,
chartId: NEW_CHART_ID,
dashboardId: DASHBOARD_ID,
});
expect(mockDeleteChartAction).not.toHaveBeenCalled();
expect(mockToastSuccess).toHaveBeenCalledWith("workspace.analysis.charts.chart_added_to_dashboard");
expect(onOpenChange).toHaveBeenCalledWith(false);
expect(mockRouterPush).toHaveBeenCalledWith(`/workspaces/${WORKSPACE_ID}/dashboards/${DASHBOARD_ID}`);
expect(mockRouterRefresh).toHaveBeenCalledTimes(1);
expect(onSuccess).toHaveBeenCalledTimes(1);
});
test("cleans up newly created chart when auto-add fails", async () => {
mockCreateChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
mockAddChartToDashboardAction.mockResolvedValue({ serverError: "boom" });
mockDeleteChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
const { result } = renderHook(() =>
useChartDialog({
...baseProps,
autoAddToDashboardId: DASHBOARD_ID,
})
);
await setHookReady(result);
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockCreateChartAction).toHaveBeenCalledTimes(1);
expect(mockDeleteChartAction).toHaveBeenCalledWith({
workspaceId: WORKSPACE_ID,
chartId: NEW_CHART_ID,
});
expect(mockToastError).toHaveBeenCalled();
});
test("cleans up newly created chart when auto-add throws unexpectedly", async () => {
mockCreateChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
mockAddChartToDashboardAction.mockRejectedValue(new Error("network down"));
mockDeleteChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
const { result } = renderHook(() =>
useChartDialog({
...baseProps,
autoAddToDashboardId: DASHBOARD_ID,
})
);
await setHookReady(result);
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockDeleteChartAction).toHaveBeenCalledWith({
workspaceId: WORKSPACE_ID,
chartId: NEW_CHART_ID,
});
expect(mockToastError).toHaveBeenCalledWith("network down");
});
test("does not delete pre-existing chart when auto-add fails on update path", async () => {
mockUpdateChartAction.mockResolvedValue({ data: { id: CHART_ID } });
mockAddChartToDashboardAction.mockResolvedValue({ serverError: "boom" });
const { result } = renderHook(() =>
useChartDialog({
...baseProps,
chartId: CHART_ID,
autoAddToDashboardId: DASHBOARD_ID,
})
);
await setHookReady(result);
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockUpdateChartAction).toHaveBeenCalledTimes(1);
expect(mockCreateChartAction).not.toHaveBeenCalled();
expect(mockDeleteChartAction).not.toHaveBeenCalled();
expect(mockToastError).toHaveBeenCalled();
});
});
describe("handleAddToDashboard - cleanup behavior", () => {
test("cleans up newly created chart when widget add fails", async () => {
mockCreateChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
mockAddChartToDashboardAction.mockResolvedValue({ serverError: "boom" });
mockDeleteChartAction.mockResolvedValue({ data: { id: NEW_CHART_ID } });
const { result } = renderHook(() => useChartDialog(baseProps));
await setHookReady(result);
await act(async () => {
result.current.setSelectedDashboardId(DASHBOARD_ID);
});
await act(async () => {
await result.current.handleAddToDashboard();
});
expect(mockCreateChartAction).toHaveBeenCalledTimes(1);
expect(mockAddChartToDashboardAction).toHaveBeenCalledWith({
workspaceId: WORKSPACE_ID,
chartId: NEW_CHART_ID,
dashboardId: DASHBOARD_ID,
});
expect(mockDeleteChartAction).toHaveBeenCalledWith({
workspaceId: WORKSPACE_ID,
chartId: NEW_CHART_ID,
});
});
test("does not delete pre-existing chart when widget add fails", async () => {
mockAddChartToDashboardAction.mockResolvedValue({ serverError: "boom" });
const { result } = renderHook(() =>
useChartDialog({
...baseProps,
chartId: CHART_ID,
})
);
// Pre-existing chart has currentChartId set via init. Skip the load-chart branch
// by providing initialChart so the effect short-circuits.
await act(async () => {
result.current.setCurrentChartId(CHART_ID);
result.current.handleChartGenerated(sampleChartData as any);
result.current.setChartName("My Chart");
result.current.setSelectedDashboardId(DASHBOARD_ID);
});
await act(async () => {
await result.current.handleAddToDashboard();
});
expect(mockCreateChartAction).not.toHaveBeenCalled();
expect(mockDeleteChartAction).not.toHaveBeenCalled();
expect(mockToastError).toHaveBeenCalled();
});
});
describe("handleAddToDashboard - validation", () => {
test("toasts and skips when name is empty for new chart", async () => {
const { result } = renderHook(() => useChartDialog(baseProps));
await act(async () => {
result.current.handleChartGenerated(sampleChartData as any);
result.current.setSelectedDashboardId(DASHBOARD_ID);
});
await act(async () => {
await result.current.handleAddToDashboard();
});
expect(mockToastError).toHaveBeenCalledWith("workspace.analysis.charts.please_enter_chart_name");
expect(mockCreateChartAction).not.toHaveBeenCalled();
expect(mockAddChartToDashboardAction).not.toHaveBeenCalled();
});
test("toasts when no dashboard selected", async () => {
const { result } = renderHook(() => useChartDialog(baseProps));
await setHookReady(result);
await act(async () => {
await result.current.handleAddToDashboard();
});
expect(mockToastError).toHaveBeenCalledWith("workspace.analysis.charts.please_select_dashboard");
expect(mockAddChartToDashboardAction).not.toHaveBeenCalled();
});
test("toasts when no directory available for new chart creation", async () => {
const { result } = renderHook(() =>
useChartDialog({
open: true,
onOpenChange: vi.fn(),
workspaceId: WORKSPACE_ID,
directories: [],
})
);
await setHookReady(result);
await act(async () => {
result.current.setSelectedDashboardId(DASHBOARD_ID);
});
await act(async () => {
await result.current.handleAddToDashboard();
});
expect(mockToastError).toHaveBeenCalledWith("workspace.analysis.charts.select_data_source_first");
expect(mockCreateChartAction).not.toHaveBeenCalled();
});
});
describe("handleSaveChart - validation + error paths", () => {
test("toasts when chartName is empty", async () => {
const { result } = renderHook(() => useChartDialog(baseProps));
await act(async () => {
result.current.handleChartGenerated(sampleChartData as any);
});
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockToastError).toHaveBeenCalledWith("workspace.analysis.charts.please_enter_chart_name");
expect(mockCreateChartAction).not.toHaveBeenCalled();
});
test("toasts when create fails on the create branch", async () => {
mockCreateChartAction.mockResolvedValue({ serverError: "create-failed" });
const { result } = renderHook(() => useChartDialog(baseProps));
await setHookReady(result);
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockToastError).toHaveBeenCalledWith("create-failed");
expect(mockAddChartToDashboardAction).not.toHaveBeenCalled();
});
test("toasts when update fails on the update branch", async () => {
mockUpdateChartAction.mockResolvedValue({ serverError: "update-failed" });
const { result } = renderHook(() =>
useChartDialog({
...baseProps,
chartId: CHART_ID,
})
);
// Skip async load-chart branch by setting currentChartId directly
await act(async () => {
result.current.setCurrentChartId(CHART_ID);
result.current.handleChartGenerated(sampleChartData as any);
result.current.setChartName("My Chart");
});
await act(async () => {
await result.current.handleSaveChart();
});
expect(mockUpdateChartAction).toHaveBeenCalledTimes(1);
expect(mockToastError).toHaveBeenCalledWith("update-failed");
});
});
});
@@ -26,6 +26,7 @@ export interface UseChartDialogProps {
onOpenChange: (open: boolean) => void;
workspaceId: string;
chartId?: string;
autoAddToDashboardId?: string;
/** Pre-loaded chart metadata; when provided for edit, skips getChartAction */
initialChart?: TChartWithCreator;
onSuccess?: () => void;
@@ -37,6 +38,7 @@ export function useChartDialog({
onOpenChange,
workspaceId,
chartId,
autoAddToDashboardId,
initialChart,
onSuccess,
directories,
@@ -45,7 +47,6 @@ export function useChartDialog({
const router = useRouter();
const [selectedChartType, setSelectedChartType] = useState<TChartType | undefined>();
const [chartData, setChartData] = useState<AnalyticsResponse | null>(null);
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
const [isAddToDashboardDialogOpen, setIsAddToDashboardDialogOpen] = useState(false);
const [chartName, setChartName] = useState("");
const [dashboards, setDashboards] = useState<Array<{ id: string; name: string }>>([]);
@@ -54,9 +55,7 @@ export function useChartDialog({
const [isLoadingChart, setIsLoadingChart] = useState(false);
const [chartLoadError, setChartLoadError] = useState<string | null>(null);
const [currentChartId, setCurrentChartId] = useState<string | undefined>(chartId);
const [selectedDirectoryId, setSelectedDirectoryId] = useState<string | null>(
directories?.length === 1 ? directories[0].id : null
);
const [selectedDirectoryId, setSelectedDirectoryId] = useState<string | null>(directories?.[0]?.id ?? null);
useEffect(() => {
let cancelled = false;
@@ -85,7 +84,7 @@ export function useChartDialog({
setChartName("");
setSelectedChartType(undefined);
setCurrentChartId(undefined);
setSelectedDirectoryId(directories?.length === 1 ? directories[0].id : null);
setSelectedDirectoryId(directories?.[0]?.id ?? null);
return;
}
@@ -159,11 +158,6 @@ export function useChartDialog({
const handleChartGenerated = (data: AnalyticsResponse) => {
setChartData(data);
if (!currentChartId) {
setChartName(
data.chartType ? `${t("workspace.analysis.charts.chart")} ${new Date().toLocaleString()}` : ""
);
}
setSelectedChartType(data.chartType);
};
@@ -179,7 +173,10 @@ export function useChartDialog({
}
setIsSaving(true);
let newlyCreatedChartId: string | null = null;
try {
let savedChartId = currentChartId;
if (currentChartId) {
const result = await updateChartAction({
workspaceId,
@@ -218,17 +215,41 @@ export function useChartDialog({
}
setCurrentChartId(result.data.id);
savedChartId = result.data.id;
newlyCreatedChartId = result.data.id;
toast.success(t("workspace.analysis.charts.chart_saved_successfully"));
}
setIsSaveDialogOpen(false);
if (autoAddToDashboardId && savedChartId) {
const addResult = await addChartToDashboardAction({
workspaceId,
chartId: savedChartId,
dashboardId: autoAddToDashboardId,
});
if (!addResult?.data) {
toast.error(
getFormattedErrorMessage(addResult) ||
t("workspace.analysis.charts.failed_to_add_chart_to_dashboard")
);
if (newlyCreatedChartId) await cleanupOrphanChart(newlyCreatedChartId);
return;
}
toast.success(t("workspace.analysis.charts.chart_added_to_dashboard"));
}
onOpenChange(false);
if (autoAddToDashboardId) {
router.push(`/workspaces/${workspaceId}/dashboards/${autoAddToDashboardId}`);
}
router.refresh();
onSuccess?.();
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : t("workspace.analysis.charts.failed_to_save_chart");
toast.error(message);
if (autoAddToDashboardId && newlyCreatedChartId) await cleanupOrphanChart(newlyCreatedChartId);
} finally {
setIsSaving(false);
}
@@ -328,7 +349,7 @@ export function useChartDialog({
setSelectedChartType(undefined);
setCurrentChartId(undefined);
setChartLoadError(null);
setSelectedDirectoryId(directories?.length === 1 ? directories[0].id : null);
setSelectedDirectoryId(directories?.[0]?.id ?? null);
onOpenChange(false);
}
};
@@ -349,8 +370,6 @@ export function useChartDialog({
setSelectedChartType,
currentChartId,
setCurrentChartId,
isSaveDialogOpen,
setIsSaveDialogOpen,
isAddToDashboardDialogOpen,
setIsAddToDashboardDialogOpen,
dashboards,

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