feat: integrate hub feedback records into unify workspace (#7828)

This commit is contained in:
Dhruwang Jariwala
2026-04-29 11:23:12 +05:30
committed by GitHub
29 changed files with 2426 additions and 376 deletions
@@ -0,0 +1,234 @@
"use server";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
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";
const ZFeedbackRecordId = z.uuid();
const ZFeedbackRecordFieldType = z.enum([
"text",
"categorical",
"nps",
"csat",
"ces",
"rating",
"number",
"boolean",
"date",
]);
const ZFeedbackRecordMetadata = z.record(z.string(), z.unknown());
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(),
});
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"
);
const ZRetrieveFeedbackRecordAction = z.object({
workspaceId: ZId,
recordId: ZFeedbackRecordId,
});
const ZCreateFeedbackRecordAction = z.object({
workspaceId: ZId,
recordInput: ZFeedbackRecordCreateInput,
});
const ZUpdateFeedbackRecordAction = z.object({
workspaceId: ZId,
recordId: ZFeedbackRecordId,
updateInput: ZFeedbackRecordUpdateInput,
});
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: z.infer<typeof ZRetrieveFeedbackRecordAction>;
}) => {
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: z.infer<typeof ZCreateFeedbackRecordAction>;
}) => {
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: z.infer<typeof ZUpdateFeedbackRecordAction>;
}) => {
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,814 @@
"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";
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);
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, 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 handleSubmit = form.handleSubmit(async (values) => {
form.clearErrors();
if (mode === "create") {
const requiredValueError = t("workspace.unify.feedback_record_value_required");
if (selectedValueField === "value_text" && !values.value_text?.trim()) {
setStrictValueValidationError(requiredValueError);
return;
}
if (selectedValueField === "value_number" && parseNumberValue(values.value_number ?? "") == null) {
setStrictValueValidationError(requiredValueError);
return;
}
if (selectedValueField === "value_boolean" && values.value_boolean === undefined) {
setStrictValueValidationError(requiredValueError);
return;
}
if (selectedValueField === "value_date" && !toISOOrUndefined(values.value_date)) {
setStrictValueValidationError(requiredValueError);
return;
}
}
const metadata = 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])
);
setIsSubmitting(true);
try {
if (mode === "create") {
const sourceTypeValue =
sourceTypeMode === SOURCE_TYPE_CUSTOM_VALUE ? customSourceType.trim() : values.source_type;
const createResult = 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),
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,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
language: values.language?.trim() || undefined,
user_identifier: values.user_identifier?.trim() || undefined,
},
});
if (!createResult?.data) {
toast.error(getFormattedErrorMessage(createResult));
setIsSubmitting(false);
return;
}
} else {
if (!recordId) {
setIsSubmitting(false);
return;
}
const preservedMetadata = Object.fromEntries(
Object.entries(record?.metadata ?? {}).filter(([, value]) => typeof value !== "string")
);
const updatePayload: {
language: string | null;
user_identifier: string | null;
metadata: Record<string, unknown>;
value_text?: string;
value_number?: number | null;
value_boolean?: boolean | null;
value_date?: string | null;
} = {
language: values.language?.trim() || null,
user_identifier: values.user_identifier?.trim() || null,
metadata: { ...preservedMetadata, ...metadata },
};
if (selectedValueField === "value_text") {
updatePayload.value_text = values.value_text?.trim() ?? "";
} else if (selectedValueField === "value_number") {
updatePayload.value_number = parseNumberValue(values.value_number ?? "");
} else if (selectedValueField === "value_boolean") {
updatePayload.value_boolean = values.value_boolean ?? null;
} else if (selectedValueField === "value_date") {
updatePayload.value_date = toISOOrUndefined(values.value_date) ?? null;
}
const updateResult = await updateFeedbackRecordAction({
workspaceId,
recordId,
updateInput: updatePayload,
});
if (!updateResult?.data) {
toast.error(getFormattedErrorMessage(updateResult));
setIsSubmitting(false);
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}>
{option}
</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,364 @@
"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,
})
)
);
const successfulRecords = results.flatMap((result) => result?.data?.data ?? []);
if (directoryIds.length > 0 && successfulRecords.length === 0) {
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 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 hover:bg-slate-50"
onClick={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().min(1),
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,143 @@
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");
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}
/>
);
}
+35 -1
View File
@@ -304,6 +304,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
@@ -404,6 +405,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
@@ -2441,10 +2443,14 @@ checksums:
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/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
@@ -3453,12 +3459,15 @@ 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
@@ -3483,8 +3492,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/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
@@ -3494,15 +3507,23 @@ 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
@@ -3513,12 +3534,18 @@ checksums:
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/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
@@ -3535,6 +3562,7 @@ checksums:
workspace/unify/select_a_survey_to_see_questions: 792eba3d2f6d210231a2266401111a20
workspace/unify/select_a_value: 115002bf2d9eec536165a7b7efc62862
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_survey: bac52e59c7847417bef6fe7b7096b475
@@ -3549,15 +3577,17 @@ checksums:
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_error: 3c95bcb32c2104b99a46f5b3dd015248
workspace/unify/status_live_sync: 7e794257419414f57d34845ef38d0939
workspace/unify/status_paused: edb1f7b7219e1c9b7aa67159090d6991
workspace/unify/status_ready: 437c0eea608e15ad5cdab94bde2f4b48
workspace/unify/submission_id: 02edf76883b47079dbe20f3f36b7c1a7
workspace/unify/survey_has_no_questions: c08514b6bce5eb464a4492239be5934d
workspace/unify/topics_and_subtopics: 1148eca01a1993fadca932efcdea7641
workspace/unify/unify_feedback: cd68c8ce0445767e7dcfb4de789903d5
workspace/unify/update_mapping_description: 58d5966c0c9b406c037dff3aa8bcb396
workspace/unify/updated_at: 8fdb85248e591254973403755dcc3724
@@ -3565,6 +3595,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
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"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_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_survey": "Umfrage auswählen",
@@ -3714,14 +3737,15 @@
"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_error": "Fehler",
"status_live_sync": "Live-Synchronisierung",
"status_paused": "Pausiert",
"status_ready": "Bereit",
"submission_id": "Einreichungs-ID",
"survey_has_no_questions": "Diese Umfrage hat keine Fragen",
"topics_and_subtopics": "Themen & Unterthemen",
"unify_feedback": "Feedback vereinheitlichen",
@@ -3730,7 +3754,11 @@
"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",
+30 -1
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Select a survey to see its questions",
"select_a_value": "Select a value...",
"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_survey": "Select Survey",
@@ -3714,6 +3737,7 @@
"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",
@@ -3721,6 +3745,7 @@
"status_live_sync": "Live sync",
"status_paused": "Paused",
"status_ready": "Ready",
"submission_id": "Submission ID",
"survey_has_no_questions": "This survey has no questions",
"topics_and_subtopics": "Topics & Subtopics",
"unify_feedback": "Unify Feedback",
@@ -3729,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Selecciona una encuesta para ver sus preguntas",
"select_a_value": "Selecciona un valor...",
"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_survey": "Seleccionar encuesta",
@@ -3714,14 +3737,15 @@
"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_error": "Error",
"status_live_sync": "Sincronización en vivo",
"status_paused": "Pausado",
"status_ready": "Listo",
"submission_id": "ID de envío",
"survey_has_no_questions": "Esta encuesta no tiene preguntas",
"topics_and_subtopics": "Temas y subtemas",
"unify_feedback": "Unificar feedback",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Sélectionnez une enquête pour voir ses questions",
"select_a_value": "Sélectionnez une valeur...",
"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_survey": "Sélectionner l'enquête",
@@ -3714,14 +3737,15 @@
"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_error": "Erreur",
"status_live_sync": "Synchronisation en direct",
"status_paused": "En pause",
"status_ready": "Prêt",
"submission_id": "ID de soumission",
"survey_has_no_questions": "Ce sondage n'a pas de questions",
"topics_and_subtopics": "Thèmes et sous-thèmes",
"unify_feedback": "Unifier les retours",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"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_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_survey": "Kérdőív kiválasztása",
@@ -3714,14 +3737,15 @@
"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_error": "Hiba",
"status_live_sync": "Élő szinkronizálás",
"status_paused": "Szüneteltetve",
"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",
"topics_and_subtopics": "Témák és altémák",
"unify_feedback": "Visszajelzések egyesítése",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,7 @@
"not_authenticated": "このアクションを実行するための認証がされていません。",
"not_authorized": "権限がありません",
"not_connected": "未接続",
"not_set": "未設定",
"note": "メモ",
"notifications": "通知",
"number": "数値",
@@ -3618,12 +3619,15 @@
"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": "クリックしてアップロード",
@@ -3648,8 +3652,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 フォーム接続",
"discard_feedback_record_changes_description": "このドロワーを閉じると、変更内容は失われます。",
"discard_feedback_record_changes_title": "保存されていない変更を破棄しますか?",
"drop_a_field_here": "ここにフィールドをドロップ",
"drop_field_or": "フィールドをドロップまたは",
"edit_csv_mapping": "CSVマッピングを編集",
@@ -3659,15 +3667,23 @@
"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 フォーム",
@@ -3678,12 +3694,18 @@
"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を読み込む",
"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": "フィードバックレコードはまだありません。コネクタがデータの送信を開始すると、ここにレコードが表示されます。",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "フォームを選択して質問を表示",
"select_a_value": "値を選択...",
"select_feedback_record_directory": "ディレクトリを選択",
"select_feedback_record_source_type": "ソースタイプを選択してください",
"select_questions": "質問を選択",
"select_source_type_description": "接続するフィードバックソースの種類を選択してください。",
"select_survey": "フォームを選択",
@@ -3714,14 +3737,15 @@
"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_error": "エラー",
"status_live_sync": "リアルタイム同期",
"status_paused": "一時停止",
"status_ready": "準備完了",
"submission_id": "提出ID",
"survey_has_no_questions": "このアンケートには質問がありません",
"topics_and_subtopics": "トピックとサブトピック",
"unify_feedback": "フィードバックを統合",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Selecteer een enquête om de vragen te zien",
"select_a_value": "Selecteer een waarde...",
"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_survey": "Selecteer enquête",
@@ -3714,14 +3737,15 @@
"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_error": "Fout",
"status_live_sync": "Live synchronisatie",
"status_paused": "Gepauzeerd",
"status_ready": "Klaar",
"submission_id": "Inzendings-ID",
"survey_has_no_questions": "Deze enquête heeft geen vragen",
"topics_and_subtopics": "Onderwerpen en subonderwerpen",
"unify_feedback": "Feedback verenigen",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Selecione uma pesquisa para ver suas perguntas",
"select_a_value": "Selecione um valor...",
"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_survey": "Selecionar pesquisa",
@@ -3714,14 +3737,15 @@
"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_error": "Erro",
"status_live_sync": "Sincronização ao vivo",
"status_paused": "Pausado",
"status_ready": "Pronto",
"submission_id": "ID de envio",
"survey_has_no_questions": "Esta pesquisa não possui perguntas",
"topics_and_subtopics": "Tópicos e subtópicos",
"unify_feedback": "Unificar feedback",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Selecione um inquérito para ver as suas perguntas",
"select_a_value": "Selecione um valor...",
"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_survey": "Selecionar inquérito",
@@ -3714,14 +3737,15 @@
"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_error": "Erro",
"status_live_sync": "Sincronização em direto",
"status_paused": "Em pausa",
"status_ready": "Pronto",
"submission_id": "ID de envio",
"survey_has_no_questions": "Este inquérito não tem perguntas",
"topics_and_subtopics": "Tópicos e subtópicos",
"unify_feedback": "Unificar feedback",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Selectează un chestionar pentru a vedea întrebările",
"select_a_value": "Selectează o valoare...",
"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_survey": "Selectează chestionar",
@@ -3714,14 +3737,15 @@
"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_error": "Eroare",
"status_live_sync": "Sincronizare în timp real",
"status_paused": "Pauzat",
"status_ready": "Gata",
"submission_id": "ID-ul trimiterii",
"survey_has_no_questions": "Acest sondaj nu are întrebări",
"topics_and_subtopics": "Subiecte și subiecte secundare",
"unify_feedback": "Unify Feedback",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,7 @@
"not_authenticated": "У вас нет прав для выполнения этого действия.",
"not_authorized": "Нет доступа",
"not_connected": "Нет подключения",
"not_set": "Не установлено",
"note": "Примечание",
"notifications": "Уведомления",
"number": "Номер",
@@ -3618,12 +3619,15 @@
"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": "Кликните для загрузки",
@@ -3648,8 +3652,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",
"discard_feedback_record_changes_description": "Ваши изменения будут потеряны, если вы закроете этот ящик.",
"discard_feedback_record_changes_title": "Отменить несохраненные изменения?",
"drop_a_field_here": "Перетащи сюда поле",
"drop_field_or": "Перетащи поле или",
"edit_csv_mapping": "Редактировать сопоставление CSV",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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": "Пока нет записей отзывов. Они появятся здесь, когда коннекторы начнут отправлять данные.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Выберите опрос, чтобы увидеть его вопросы",
"select_a_value": "Выберите значение...",
"select_feedback_record_directory": "Выберите каталог",
"select_feedback_record_source_type": "Выберите тип источника",
"select_questions": "Выберите вопросы",
"select_source_type_description": "Выберите тип источника отзывов, который хотите подключить.",
"select_survey": "Выбрать опрос",
@@ -3714,14 +3737,15 @@
"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_error": "Ошибка",
"status_live_sync": "Синхронизация в реальном времени",
"status_paused": "Приостановлен",
"status_ready": "Готово",
"submission_id": "Идентификатор отправки",
"survey_has_no_questions": "В этом опросе нет вопросов",
"topics_and_subtopics": "Темы и подтемы",
"unify_feedback": "Обратная связь Unify",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"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_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_survey": "Välj enkät",
@@ -3714,14 +3737,15 @@
"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_error": "Fel",
"status_live_sync": "Live sync",
"status_paused": "Pausad",
"status_ready": "Ready",
"submission_id": "Inlämnings-ID",
"survey_has_no_questions": "Den här enkäten har inga frågor",
"topics_and_subtopics": "Ämnen och delämnen",
"unify_feedback": "Samla feedback",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,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ı",
@@ -3618,12 +3619,15 @@
"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",
@@ -3648,8 +3652,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ı",
"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",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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.",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "Sorularını görmek için bir anket seç",
"select_a_value": "Bir değer 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_survey": "Anket Seç",
@@ -3714,14 +3737,15 @@
"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_error": "Hata",
"status_live_sync": "Live sync",
"status_paused": "Duraklatıldı",
"status_ready": "Ready",
"submission_id": "Gönderim Kimliği",
"survey_has_no_questions": "Bu ankette soru yok",
"topics_and_subtopics": "Konular ve alt konular",
"unify_feedback": "Geri Bildirimleri Birleştir",
@@ -3730,7 +3754,11 @@
"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",
+30 -2
View File
@@ -331,6 +331,7 @@
"not_authenticated": "您 未 认证 以 执行 该 操作。",
"not_authorized": "未授权",
"not_connected": "未连接",
"not_set": "未设置",
"note": "注释",
"notifications": "通知",
"number": "数字",
@@ -3618,12 +3619,15 @@
"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": "点击上传",
@@ -3648,8 +3652,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 调查连接",
"discard_feedback_record_changes_description": "如果关闭此抽屉,您的更改将会丢失。",
"discard_feedback_record_changes_title": "放弃未保存的更改?",
"drop_a_field_here": "将字段拖到这里",
"drop_field_or": "拖放字段或",
"edit_csv_mapping": "编辑 CSV 映射",
@@ -3659,15 +3667,23 @@
"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",
@@ -3678,12 +3694,18 @@
"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",
"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": "暂无反馈记录。当你的连接器开始发送数据后,记录会显示在这里。",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "请选择一个调查以查看其问题",
"select_a_value": "选择一个值...",
"select_feedback_record_directory": "选择目录",
"select_feedback_record_source_type": "选择来源类型",
"select_questions": "选择问题",
"select_source_type_description": "请选择你想要连接的反馈来源类型。",
"select_survey": "选择调查",
@@ -3714,14 +3737,15 @@
"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_error": "错误",
"status_live_sync": "Live sync",
"status_paused": "已暂停",
"status_ready": "Ready",
"submission_id": "提交ID",
"survey_has_no_questions": "该调查没有任何问题",
"topics_and_subtopics": "主题和子主题",
"unify_feedback": "统一反馈",
@@ -3730,7 +3754,11 @@
"upload_csv_data_description": "上传 CSV 文件以导入反馈数据。",
"upload_csv_file": "上传 CSV 文件",
"user_identifier": "用户",
"value": "值"
"value": "值",
"value_boolean": "值(布尔值)",
"value_date": "值(日期)",
"value_number": "值(数量)",
"value_text": "值(文本)"
},
"xm-templates": {
"ces": "客户努力评分",
+30 -2
View File
@@ -331,6 +331,7 @@
"not_authenticated": "您未經授權執行此操作。",
"not_authorized": "未授權",
"not_connected": "未連線",
"not_set": "未設定",
"note": "筆記",
"notifications": "通知",
"number": "數字",
@@ -3618,12 +3619,15 @@
"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": "點擊以上傳",
@@ -3648,8 +3652,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 問卷連線",
"discard_feedback_record_changes_description": "如果關閉此抽屜,您的變更將會遺失。",
"discard_feedback_record_changes_title": "放棄未儲存的變更?",
"drop_a_field_here": "請將欄位拖曳到這裡",
"drop_field_or": "拖曳欄位或",
"edit_csv_mapping": "編輯 CSV 對應",
@@ -3659,15 +3667,23 @@
"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 問卷",
@@ -3678,12 +3694,18 @@
"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",
"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": "目前尚無回饋紀錄。當你的連接器開始傳送資料時,紀錄會顯示在這裡。",
@@ -3700,6 +3722,7 @@
"select_a_survey_to_see_questions": "請選擇問卷以查看其問題",
"select_a_value": "請選擇一個值...",
"select_feedback_record_directory": "選擇目錄",
"select_feedback_record_source_type": "選擇來源類型",
"select_questions": "選擇問題",
"select_source_type_description": "請選擇你想要連接的回饋來源類型。",
"select_survey": "選擇問卷",
@@ -3714,14 +3737,15 @@
"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_error": "錯誤",
"status_live_sync": "Live sync",
"status_paused": "已暫停",
"status_ready": "Ready",
"submission_id": "提交ID",
"survey_has_no_questions": "此問卷沒有任何題目",
"topics_and_subtopics": "主題與子主題",
"unify_feedback": "整合回饋",
@@ -3730,7 +3754,11 @@
"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",
+4 -1
View File
@@ -3,7 +3,9 @@ export {
createFeedbackRecord,
createFeedbackRecordsBatch,
listFeedbackRecords,
type CreateFeedbackRecordResult,
retrieveFeedbackRecord,
updateFeedbackRecord,
type HubFeedbackRecordResult,
type ListFeedbackRecordsResult,
} from "./service";
export type {
@@ -11,4 +13,5 @@ export type {
FeedbackRecordData,
FeedbackRecordListParams,
FeedbackRecordListResponse,
FeedbackRecordUpdateParams,
} from "./types";
+64 -2
View File
@@ -1,5 +1,11 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { createFeedbackRecord, createFeedbackRecordsBatch, listFeedbackRecords } from "./service";
import {
createFeedbackRecord,
createFeedbackRecordsBatch,
listFeedbackRecords,
retrieveFeedbackRecord,
updateFeedbackRecord,
} from "./service";
import type { FeedbackRecordCreateParams } from "./types";
vi.mock("@formbricks/logger", () => ({
@@ -15,7 +21,7 @@ const { getHubClient } = await import("./hub-client");
const sampleInput: FeedbackRecordCreateParams = {
field_id: "el-1",
field_type: "rating",
source_type: "formbricks",
source_type: "formbricks_survey",
source_id: "survey-1",
source_name: "Test Survey",
field_label: "Question?",
@@ -121,6 +127,62 @@ describe("hub service", () => {
});
});
describe("retrieveFeedbackRecord", () => {
test("returns error when client is null", async () => {
vi.mocked(getHubClient).mockReturnValue(null);
const result = await retrieveFeedbackRecord("rec-1");
expect(result.data).toBeNull();
expect(result.error?.message).toContain("HUB_API_KEY");
});
test("returns data on success", async () => {
const record = { id: "rec-1", field_id: "f1" };
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { retrieve: vi.fn().mockResolvedValue(record) },
} as any);
const result = await retrieveFeedbackRecord("rec-1");
expect(result.data).toEqual(record);
expect(result.error).toBeNull();
});
test("returns error on throw", async () => {
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { retrieve: vi.fn().mockRejectedValue(new Error("Not found")) },
} as any);
const result = await retrieveFeedbackRecord("rec-1");
expect(result.data).toBeNull();
expect(result.error).toMatchObject({ message: "Not found" });
});
});
describe("updateFeedbackRecord", () => {
test("returns error when client is null", async () => {
vi.mocked(getHubClient).mockReturnValue(null);
const result = await updateFeedbackRecord("rec-1", { value_text: "new" });
expect(result.data).toBeNull();
expect(result.error?.message).toContain("HUB_API_KEY");
});
test("returns data on success", async () => {
const updated = { id: "rec-1", value_text: "new" };
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { update: vi.fn().mockResolvedValue(updated) },
} as any);
const result = await updateFeedbackRecord("rec-1", { value_text: "new" });
expect(result.data).toEqual(updated);
expect(result.error).toBeNull();
});
test("returns error on throw", async () => {
vi.mocked(getHubClient).mockReturnValue({
feedbackRecords: { update: vi.fn().mockRejectedValue(new Error("Forbidden")) },
} as any);
const result = await updateFeedbackRecord("rec-1", { value_text: "new" });
expect(result.data).toBeNull();
expect(result.error).toMatchObject({ message: "Forbidden" });
});
});
describe("createFeedbackRecordsBatch", () => {
test("returns all errors when getHubClient returns null", async () => {
vi.mocked(getHubClient).mockReturnValue(null);
+49 -7
View File
@@ -7,11 +7,14 @@ import type {
FeedbackRecordData,
FeedbackRecordListParams,
FeedbackRecordListResponse,
FeedbackRecordUpdateParams,
} from "./types";
export type CreateFeedbackRecordResult = {
type HubError = { status: number; message: string; detail: string };
export type HubFeedbackRecordResult = {
data: FeedbackRecordData | null;
error: { status: number; message: string; detail: string } | null;
error: HubError | null;
};
const NO_CONFIG_ERROR = {
@@ -20,7 +23,7 @@ const NO_CONFIG_ERROR = {
detail: "HUB_API_KEY is not set; Hub integration is disabled.",
} as const;
const createResultFromError = (err: unknown): CreateFeedbackRecordResult => {
const createResultFromError = (err: unknown): HubFeedbackRecordResult => {
const status = err instanceof FormbricksHub.APIError ? err.status : 0;
const message = err instanceof Error ? err.message : String(err);
return { data: null, error: { status, message, detail: message } };
@@ -32,7 +35,7 @@ const createResultFromError = (err: unknown): CreateFeedbackRecordResult => {
*/
export const createFeedbackRecord = async (
input: FeedbackRecordCreateParams
): Promise<CreateFeedbackRecordResult> => {
): Promise<HubFeedbackRecordResult> => {
const client = getHubClient();
if (!client) {
return { data: null, error: { ...NO_CONFIG_ERROR } };
@@ -46,9 +49,48 @@ export const createFeedbackRecord = async (
}
};
/**
* Retrieve a single feedback record from the Hub by id.
*/
export const retrieveFeedbackRecord = async (id: string): Promise<HubFeedbackRecordResult> => {
const client = getHubClient();
if (!client) {
return { data: null, error: { ...NO_CONFIG_ERROR } };
}
try {
const data = await client.feedbackRecords.retrieve(id);
return { data, error: null };
} catch (err) {
logger.warn({ err, id }, "Hub: retrieveFeedbackRecord failed");
return createResultFromError(err);
}
};
/**
* Update a single feedback record in the Hub by id.
*/
export const updateFeedbackRecord = async (
id: string,
input: FeedbackRecordUpdateParams
): Promise<HubFeedbackRecordResult> => {
const client = getHubClient();
if (!client) {
return { data: null, error: { ...NO_CONFIG_ERROR } };
}
try {
const data = await client.feedbackRecords.update(id, input);
return { data, error: null };
} catch (err) {
logger.warn({ err, id }, "Hub: updateFeedbackRecord failed");
return createResultFromError(err);
}
};
export type ListFeedbackRecordsResult = {
data: FeedbackRecordListResponse | null;
error: { status: number; message: string; detail: string } | null;
error: HubError | null;
};
/**
@@ -78,7 +120,7 @@ export const listFeedbackRecords = async (
*/
export const createFeedbackRecordsBatch = async (
inputs: FeedbackRecordCreateParams[]
): Promise<{ results: CreateFeedbackRecordResult[] }> => {
): Promise<{ results: HubFeedbackRecordResult[] }> => {
const client = getHubClient();
if (!client) {
return {
@@ -90,7 +132,7 @@ export const createFeedbackRecordsBatch = async (
inputs.map(async (input) => {
try {
const data = await client.feedbackRecords.create(input);
return { data, error: null as CreateFeedbackRecordResult["error"] };
return { data, error: null as HubFeedbackRecordResult["error"] };
} catch (err) {
logger.warn({ err, fieldId: input.field_id }, "Hub: createFeedbackRecord failed");
return createResultFromError(err);
+1
View File
@@ -4,3 +4,4 @@ export type FeedbackRecordCreateParams = FormbricksHub.FeedbackRecordCreateParam
export type FeedbackRecordData = FormbricksHub.FeedbackRecordData;
export type FeedbackRecordListParams = FormbricksHub.FeedbackRecordListParams;
export type FeedbackRecordListResponse = FormbricksHub.FeedbackRecordListResponse;
export type FeedbackRecordUpdateParams = FormbricksHub.FeedbackRecordUpdateParams;