diff --git a/.env.example b/.env.example index aab263280f..3f2db08e55 100644 --- a/.env.example +++ b/.env.example @@ -64,6 +64,9 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu HUB_API_KEY=dev-api-key HUB_API_URL=http://localhost:8080 HUB_DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable +# Hub image tag used by docker-compose.dev.yml (hub + hub-migrate). Leave unset to use the +# pinned default in the compose file; override here when testing a specific Hub release. +# HUB_IMAGE_TAG=0.2.0 ########################### # CUBE ANALYTICS (XM V5) # diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/actions.ts b/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/actions.ts new file mode 100644 index 0000000000..ed0b0c3dc7 --- /dev/null +++ b/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/actions.ts @@ -0,0 +1,121 @@ +"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 { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory"; +import { semanticSearchFeedbackRecords } from "@/modules/hub/service"; +import type { SemanticSearchResultItem } from "@/modules/hub/types"; + +const TOPICS_PREVIEW_LIMIT = 10; +const SEARCH_CONCURRENCY = 4; + +const ZSemanticSearchFeedbackRecordsAction = z.object({ + workspaceId: ZId, + query: z.string().trim().min(1).max(500), + limit: z.number().min(1).max(50).optional(), + minScore: z.number().min(0).max(1).optional(), +}); + +export type TTopicsPreviewSearchResult = SemanticSearchResultItem & { + tenant_id: string; + directory_name: string; +}; + +export type TTopicsPreviewSearchActionResult = { + results: TTopicsPreviewSearchResult[]; + unavailable: boolean; + unavailableMessage?: string; +}; + +const ensureReadAccess = async (userId: string, workspaceId: string): Promise => { + const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId); + await checkAuthorizationUpdated({ + userId, + organizationId, + access: [ + { + type: "organization", + roles: ["owner", "manager"], + }, + { + type: "workspaceTeam", + minPermission: "read", + workspaceId, + }, + ], + }); +}; + +export const semanticSearchFeedbackRecordsAction = authenticatedActionClient + .inputSchema(ZSemanticSearchFeedbackRecordsAction) + .action( + async ({ + ctx, + parsedInput, + }: { + ctx: AuthenticatedActionClientCtx; + parsedInput: z.infer; + }): Promise => { + await ensureReadAccess(ctx.user.id, parsedInput.workspaceId); + + const directories = await getFeedbackDirectoriesByWorkspaceId(parsedInput.workspaceId); + if (directories.length === 0) { + return { results: [], unavailable: false }; + } + + const limit = parsedInput.limit ?? TOPICS_PREVIEW_LIMIT; + const searches: { + directory: (typeof directories)[number]; + result: Awaited>; + }[] = []; + for (let i = 0; i < directories.length; i += SEARCH_CONCURRENCY) { + const chunk = directories.slice(i, i + SEARCH_CONCURRENCY); + const chunkResults = await Promise.all( + chunk.map(async (directory) => { + const result = await semanticSearchFeedbackRecords({ + tenant_id: directory.id, + query: parsedInput.query, + limit, + min_score: parsedInput.minScore, + }); + return { directory, result }; + }) + ); + searches.push(...chunkResults); + } + + const successfulResults = searches.flatMap(({ directory, result }) => + (result.data?.data ?? []).map((item) => ({ + ...item, + tenant_id: directory.id, + directory_name: directory.name, + })) + ); + + if (successfulResults.length > 0) { + return { + results: successfulResults.toSorted((a, b) => b.score - a.score).slice(0, limit), + unavailable: false, + }; + } + + const firstError = searches.find(({ result }) => result.error)?.result.error; + if (firstError?.status === 0 || firstError?.status === 503) { + return { + results: [], + unavailable: true, + unavailableMessage: firstError.message, + }; + } + + if (firstError) { + throw new Error(firstError.message); + } + + return { results: [], unavailable: false }; + } + ); diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/components/topics-subtopics-preview.tsx b/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/components/topics-subtopics-preview.tsx new file mode 100644 index 0000000000..007b48ae47 --- /dev/null +++ b/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/components/topics-subtopics-preview.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { SearchIcon, SparklesIcon } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { Badge } from "@/modules/ui/components/badge"; +import { Button } from "@/modules/ui/components/button"; +import { Input } from "@/modules/ui/components/input"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { PageHeader } from "@/modules/ui/components/page-header"; +import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation"; +import { semanticSearchFeedbackRecordsAction } from "../actions"; +import type { TTopicsPreviewSearchResult } from "../actions"; + +interface TopicsSubtopicsPreviewProps { + workspaceId: string; + directoryMap: Record; +} + +export const TopicsSubtopicsPreview = ({ + workspaceId, + directoryMap, +}: Readonly) => { + const { t } = useTranslation(); + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + const [hasSearched, setHasSearched] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [error, setError] = useState(null); + const [unavailableMessage, setUnavailableMessage] = useState(null); + + const hasDirectories = Object.keys(directoryMap).length > 0; + + const exampleSearches = [ + t("workspace.unify.semantic_topics_example_slow_checkout"), + t("workspace.unify.semantic_topics_example_pricing_complaints"), + t("workspace.unify.semantic_topics_example_confusing_onboarding"), + ]; + + const runSearch = async (searchQuery: string) => { + const trimmedQuery = searchQuery.trim(); + if (!trimmedQuery || isSearching) return; + + setQuery(trimmedQuery); + setIsSearching(true); + setHasSearched(true); + setError(null); + setUnavailableMessage(null); + + try { + const response = await semanticSearchFeedbackRecordsAction({ + workspaceId, + query: trimmedQuery, + limit: 10, + minScore: 0.7, + }); + + if (response?.data) { + setResults(response.data.results); + setUnavailableMessage(response.data.unavailable ? (response.data.unavailableMessage ?? "") : null); + } else { + setResults([]); + setError(getFormattedErrorMessage(response) ?? t("workspace.unify.semantic_search_failed")); + } + } catch { + setResults([]); + setError(t("workspace.unify.semantic_search_failed")); + } finally { + setIsSearching(false); + } + }; + + const handleSubmit = async (event: { preventDefault: () => void }) => { + event.preventDefault(); + await runSearch(query); + }; + + return ( + + + + + +
+
+
+
+
+
+

+ {t("workspace.unify.semantic_topics_preview_description")} +

+
+
+ +
+ setQuery(event.target.value)} + placeholder={t("workspace.unify.semantic_search_placeholder")} + disabled={!hasDirectories || isSearching} + aria-label={t("workspace.unify.semantic_search_input_label")} + /> + +
+ +
+ {t("workspace.unify.try_searching_for")} + {exampleSearches.map((label) => ( + + ))} +
+
+ + {!hasDirectories && ( +
+

{t("workspace.unify.semantic_search_no_directories")}

+ +
+ )} + + {error && ( +
{error}
+ )} + + {unavailableMessage !== null && ( +
+ {t("workspace.unify.semantic_search_unavailable")} +
+ )} + + {hasSearched && !isSearching && !error && unavailableMessage === null && results.length === 0 && ( +
+

{t("workspace.unify.semantic_search_no_results")}

+
+ )} + + {results.length > 0 && ( +
+
+

+ {t("workspace.unify.semantic_search_results_count", { count: results.length })} +

+
+
+ {results.map((result) => ( +
+
+ + + {t("workspace.unify.semantic_search_relevance", { + score: Math.round(result.score * 100), + })} + +
+

+ {result.field_label || t("workspace.unify.field_label")} +

+

+ {result.value_text || t("workspace.unify.semantic_search_missing_text")} +

+
+ ))} +
+
+ )} +
+
+ ); +}; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/page.tsx b/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/page.tsx new file mode 100644 index 0000000000..bd1522ca35 --- /dev/null +++ b/apps/web/app/(app)/workspaces/[workspaceId]/unify/topics-subtopics/page.tsx @@ -0,0 +1,29 @@ +import { notFound } from "next/navigation"; +import { getTranslate } from "@/lingodotdev/server"; +import { getFeedbackDirectoriesByWorkspaceId } from "@/modules/ee/feedback-directory/lib/feedback-directory"; +import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; +import { TopicsSubtopicsPreview } from "./components/topics-subtopics-preview"; + +export default async function UnifyTopicsSubtopicsPage( + props: Readonly<{ params: Promise<{ workspaceId: string }> }> +) { + const t = await getTranslate(); + const params = await props.params; + + const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session } = + await getWorkspaceAuth(params.workspaceId); + + if (!session) { + throw new Error(t("common.session_not_found")); + } + + const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess; + if (!hasAccess) { + return notFound(); + } + + const directories = await getFeedbackDirectoriesByWorkspaceId(params.workspaceId); + const directoryMap = Object.fromEntries(directories.map((directory) => [directory.id, directory.name])); + + return ; +} diff --git a/apps/web/app/lib/api/with-api-logging.ts b/apps/web/app/lib/api/with-api-logging.ts index 7a1f49f8d3..a71755212c 100644 --- a/apps/web/app/lib/api/with-api-logging.ts +++ b/apps/web/app/lib/api/with-api-logging.ts @@ -5,7 +5,6 @@ import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { authenticateRequest } from "@/app/api/v1/auth"; import { reportApiError } from "@/app/lib/api/api-error-reporter"; import { responses } from "@/app/lib/api/response"; -import { getApiKeyFromHeaders } from "@/modules/api/lib/api-key-auth"; import { AuthenticationMethod, isClientSideApiRoute, @@ -13,6 +12,7 @@ import { isManagementApiRoute, } from "@/app/middleware/endpoint-validator"; import { AUDIT_LOG_ENABLED } from "@/lib/constants"; +import { getApiKeyFromHeaders } from "@/modules/api/lib/api-key-auth"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { TEnvoyRateLimitAuthType, diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 767cdd9254..70a10ca627 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -407,7 +407,6 @@ 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 @@ -3579,6 +3578,7 @@ checksums: workspace/unify/request_feedback_source: 51045caa2c81dee971d23a1841d19a7e workspace/unify/required: 04d7fb6f37ffe0a6ca97d49e2a8b6eb5 workspace/unify/save_changes: 53dd9f4f0a4accc822fa5c1f2f6d118a + workspace/unify/search_feedback: db1e8dd05944bb928b96e3822aee3379 workspace/unify/select_a_survey_to_see_questions: 792eba3d2f6d210231a2266401111a20 workspace/unify/select_a_value: 115002bf2d9eec536165a7b7efc62862 workspace/unify/select_feedback_directory: 88afbf2c2a322249908ee5d00ec5f65d @@ -3588,6 +3588,20 @@ checksums: workspace/unify/select_survey: bac52e59c7847417bef6fe7b7096b475 workspace/unify/select_survey_and_questions: 53914988a2f48caecea23f3b3b868b9f workspace/unify/select_survey_questions_description: 3386ed56085eabebefa3cc453269fc5b + workspace/unify/semantic_search_failed: 6adf5f85d453ef2923861ad7b188787a + workspace/unify/semantic_search_input_label: 3b8af322b080da8b8cb4fcce6c3f3d1e + workspace/unify/semantic_search_missing_text: e1ad1ba8f3ab2e05f4f73732543d0ed5 + workspace/unify/semantic_search_no_directories: 2bcebe10f5898f5422ee17ed66295044 + workspace/unify/semantic_search_no_results: 50f0572ad7584c91af5a0a28523f40f2 + workspace/unify/semantic_search_placeholder: b5dbff2cdd334d7b86f18a12c56ffbb1 + workspace/unify/semantic_search_relevance: ddd1a91cd29944d5af7b899168b988a2 + workspace/unify/semantic_search_results_count: 199b822e4f709787b79dd42ccd70e58f + workspace/unify/semantic_search_unavailable: eb66fd42fc327627e74fe54a76f33b16 + workspace/unify/semantic_topics_example_confusing_onboarding: ac612953829e6a7f58e34796a472ca71 + workspace/unify/semantic_topics_example_pricing_complaints: fdf96d24d56620f79c31de48e6c1936b + workspace/unify/semantic_topics_example_slow_checkout: 42579a662e637ffe40de7078def55805 + workspace/unify/semantic_topics_preview_description: 330871cf6f36128bfdbc7d9d20c500a4 + workspace/unify/semantic_topics_preview_title: 2d59d672921b4807a40770e5d40b485e workspace/unify/set_value: b8a86f8da957ebd599ece4b1b1936a78 workspace/unify/setup_connection: cce7d9c488d737d04e70bed929a46f8a workspace/unify/showing_count_loaded: f443aae08223b65fbd5521d6e69534a4 @@ -3615,6 +3629,7 @@ checksums: workspace/unify/submission_id: 02edf76883b47079dbe20f3f36b7c1a7 workspace/unify/survey_has_no_questions: c08514b6bce5eb464a4492239be5934d workspace/unify/topics_and_subtopics: 1148eca01a1993fadca932efcdea7641 + workspace/unify/try_searching_for: 8bc02885a2efdc53d7323aac26ae1110 workspace/unify/unify_feedback: cd68c8ce0445767e7dcfb4de789903d5 workspace/unify/update_mapping_description: 58d5966c0c9b406c037dff3aa8bcb396 workspace/unify/updated_at: 8fdb85248e591254973403755dcc3724 diff --git a/apps/web/lib/jwt.test.ts b/apps/web/lib/jwt.test.ts index 7b95056e84..892aec608f 100644 --- a/apps/web/lib/jwt.test.ts +++ b/apps/web/lib/jwt.test.ts @@ -3,17 +3,17 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import * as crypto from "@/lib/crypto"; import { - createGatewayServiceToken, - createFeedbackRecordsGatewayToken, createEmailChangeToken, createEmailToken, + createFeedbackRecordsGatewayToken, + createGatewayServiceToken, createInviteToken, createToken, createTokenForLinkSurvey, getEmailFromEmailToken, - verifyGatewayServiceToken, - verifyFeedbackRecordsGatewayToken, verifyEmailChangeToken, + verifyFeedbackRecordsGatewayToken, + verifyGatewayServiceToken, verifyInviteToken, verifyToken, verifyTokenForLinkSurvey, diff --git a/apps/web/lib/jwt.ts b/apps/web/lib/jwt.ts index 2e4f6eacf6..91ee8848f4 100644 --- a/apps/web/lib/jwt.ts +++ b/apps/web/lib/jwt.ts @@ -3,7 +3,7 @@ import { prisma } from "@formbricks/database"; import { logger } from "@formbricks/logger"; import { ENCRYPTION_KEY, NEXTAUTH_SECRET } from "@/lib/constants"; import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto"; -import { getGatewayAuthServiceTokenPurpose, TGatewayAuthService } from "@/modules/gateway-auth/lib/service"; +import { TGatewayAuthService, getGatewayAuthServiceTokenPurpose } from "@/modules/gateway-auth/lib/service"; const FEEDBACK_RECORDS_GATEWAY_TOKEN_TTL_SECONDS = 60 * 10; @@ -29,7 +29,10 @@ export const createToken = (userId: string, options = {}): string => { return jwt.sign({ id: encryptedUserId }, NEXTAUTH_SECRET, options); }; -export const createGatewayServiceToken = (userId: string, service: TGatewayAuthService): { +export const createGatewayServiceToken = ( + userId: string, + service: TGatewayAuthService +): { token: string; expiresAt: string; } => { @@ -104,7 +107,10 @@ export const verifyEmailChangeToken = async (token: string): Promise<{ id: strin }; }; -export const verifyGatewayServiceToken = (token: string, service: TGatewayAuthService): { +export const verifyGatewayServiceToken = ( + token: string, + service: TGatewayAuthService +): { userId: string; } => { if (!NEXTAUTH_SECRET) { diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index af8d6cc78b..b85ffacfcd 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden", "something_went_wrong": "Etwas ist schiefgelaufen", "something_went_wrong_please_try_again": "Etwas ist schiefgelaufen. Bitte versuche es erneut.", - "soon": "Bald", "sort_by": "Sortieren nach", "start_free_trial": "Kostenlose Testversion starten", "status": "Status", @@ -1746,7 +1745,7 @@ "filter_data": "Daten filtern", "filters": "Filter", "filters_toggle_description": "Nur Daten einbeziehen, die die folgenden Bedingungen erfüllen.", - "go_to_feedback_directories": "Zu Feedback-Verzeichnissen", + "go_to_feedback_directories": "Zu Feedback-Verzeichnissen gehen", "granularity": "Granularität", "granularity_day": "Tag", "granularity_hour": "Stunde", @@ -1852,9 +1851,9 @@ "delete_api_key_confirmation": "Alle Anwendungen, die diesen Schlüssel verwenden, können nicht mehr auf deine Formbricks-Daten zugreifen.", "duplicate_access": "Doppelter Workspace-Zugriff ist nicht erlaubt", "duplicate_directory_access": "Doppelter Zugriff auf Feedback-Verzeichnis nicht erlaubt", - "feedback_directory_access": "Zugriff auf Feedback-Verzeichnis", + "feedback_directory_access": "Feedback-Verzeichnis-Zugriff", "no_api_keys_yet": "Du hast noch keine API-Schlüssel", - "no_directory_permissions_found": "Keine Berechtigungen für Feedback-Verzeichnis gefunden", + "no_directory_permissions_found": "Keine Berechtigungen für Feedback-Verzeichnisse gefunden", "no_workspace_permissions_found": "Keine Workspace-Berechtigungen gefunden", "organization_access": "Organisations-Zugriff", "organization_access_description": "Wähle Lese- oder Schreibrechte für organisationsweite Ressourcen aus.", @@ -2547,10 +2546,10 @@ "archive_directory": "Verzeichnis archivieren", "archive_not_allowed": "Du darfst dieses Verzeichnis nicht archivieren.", "are_you_sure_you_want_to_archive": "Bist du sicher, dass du dieses Verzeichnis archivieren möchtest? Workspaces haben dann keinen Zugriff mehr darauf.", - "assign_workspaces_description": "Lege fest, welche Workspaces auf dieses Feedback-Verzeichnis zugreifen können.", + "assign_workspaces_description": "Steuere, welche Workspaces auf dieses Feedback-Verzeichnis zugreifen können.", "connectors_description": "Connectoren, die Feedback-Datensätze an dieses Verzeichnis senden.", "create_feedback_directory": "Feedback-Verzeichnis erstellen", - "description": "Verwalte Feedback-Verzeichnisse und ihre Workspace-Zuordnungen.", + "description": "Verwalte Feedback-Verzeichnisse und ihre Workspace-Zuweisungen.", "directory_archived_successfully": "Verzeichnis erfolgreich archiviert", "directory_created_successfully": "Verzeichnis erfolgreich erstellt", "directory_id": "Verzeichnis-ID", @@ -3727,7 +3726,7 @@ "metadata_read_only_entries": "Schreibgeschützte Metadatenwerte (keine Zeichenfolge)", "metadata_value": "Metadatenwert", "missing_feedback_source_title": "Feedback-Quelle fehlt?", - "no_feedback_directory_available": "Diesem Workspace ist kein Feedback-Verzeichnis zugewiesen. Erstelle oder weise zuerst eines zu.", + "no_feedback_directory_available": "Diesem Workspace ist kein Feedback-Verzeichnis zugewiesen. Erstelle oder weise zuerst eins zu.", "no_feedback_records": "Noch keine Feedback-Einträge vorhanden. Einträge erscheinen hier, sobald deine Konnektoren Daten senden.", "no_source_fields_loaded": "Noch keine Quellfelder geladen", "no_sources_connected": "Noch keine Quellen verbunden. Füge eine Quelle hinzu, um loszulegen.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Quellen-Integration anfragen", "required": "Erforderlich", "save_changes": "Änderungen speichern", + "search_feedback": "Feedback durchsuchen", "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_directory": "Verzeichnis auswählen", @@ -3748,6 +3748,20 @@ "select_survey": "Umfrage auswählen", "select_survey_and_questions": "Umfrage & Fragen auswählen", "select_survey_questions_description": "Wähle aus, welche Umfragefragen FeedbackRecords erstellen sollen.", + "semantic_search_failed": "Suche nach Feedback-Einträgen fehlgeschlagen", + "semantic_search_input_label": "Feedback-Einträge nach Thema durchsuchen", + "semantic_search_missing_text": "Dieser Feedback-Eintrag hat keinen anzuzeigenden Text.", + "semantic_search_no_directories": "Diesem Workspace ist noch kein Feedback-Verzeichnis zugewiesen. Füge eine Feedback-Quelle hinzu, um Feedback nach Bedeutung zu durchsuchen.", + "semantic_search_no_results": "Keine passenden Feedback-Einträge gefunden. Versuche ein breiteres Thema oder eine andere Formulierung.", + "semantic_search_placeholder": "Suche nach einem Thema, z. B. Preisbeschwerden", + "semantic_search_relevance": "{score} % Relevanz", + "semantic_search_results_count": "{count, plural, one {# passender Feedback-Eintrag} other {# passende Feedback-Einträge}}", + "semantic_search_unavailable": "Semantische Suche ist noch nicht verfügbar. Konfiguriere Hub-Embeddings, um diese Vorschau zu nutzen.", + "semantic_topics_example_confusing_onboarding": "verwirrende Einführung", + "semantic_topics_example_pricing_complaints": "Preisbeschwerden", + "semantic_topics_example_slow_checkout": "langsamer Checkout", + "semantic_topics_preview_description": "Gib ein Thema oder einen Begriff ein, um Feedback-Einträge nach Bedeutung zu finden. Dies ist eine frühe Vorschau auf zukünftige Themen & Unterthemen.", + "semantic_topics_preview_title": "Feedback nach Thema durchsuchen", "set_value": "Wert festlegen", "setup_connection": "Verbindung einrichten", "showing_count_loaded": "{count} Datensätze werden angezeigt", @@ -3775,6 +3789,7 @@ "submission_id": "Einreichungs-ID", "survey_has_no_questions": "Diese Umfrage hat keine Fragen", "topics_and_subtopics": "Themen & Unterthemen", + "try_searching_for": "Versuche zu suchen nach", "unify_feedback": "Feedback vereinheitlichen", "update_mapping_description": "Aktualisiere die Zuordnungskonfiguration für diese Quelle.", "updated_at": "Aktualisiert am", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 7ffe96acb7..a1fde8469f 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Some files failed to upload", "something_went_wrong": "Something went wrong", "something_went_wrong_please_try_again": "Something went wrong. Please try again.", - "soon": "Soon", "sort_by": "Sort by", "start_free_trial": "Start free trial", "status": "Status", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Request source integration", "required": "Required", "save_changes": "Save changes", + "search_feedback": "Search feedback", "select_a_survey_to_see_questions": "Select a survey to see its questions", "select_a_value": "Select a value...", "select_feedback_directory": "Select a directory", @@ -3748,6 +3748,20 @@ "select_survey": "Select Survey", "select_survey_and_questions": "Select Survey & Questions", "select_survey_questions_description": "Choose which survey questions should create FeedbackRecords.", + "semantic_search_failed": "Failed to search feedback records", + "semantic_search_input_label": "Search feedback records by topic", + "semantic_search_missing_text": "This feedback record has no text to display.", + "semantic_search_no_directories": "No feedback record directory is assigned to this workspace yet. Add a feedback source to start searching feedback by meaning.", + "semantic_search_no_results": "No matching feedback records found. Try a broader topic or a different phrase.", + "semantic_search_placeholder": "Search for a topic, e.g. pricing complaints", + "semantic_search_relevance": "{score}% relevance", + "semantic_search_results_count": "{count, plural, one {# matching feedback record} other {# matching feedback records}}", + "semantic_search_unavailable": "Semantic search is not available yet. Configure Hub embeddings to use this preview.", + "semantic_topics_example_confusing_onboarding": "confusing onboarding", + "semantic_topics_example_pricing_complaints": "pricing complaints", + "semantic_topics_example_slow_checkout": "slow checkout", + "semantic_topics_preview_description": "Enter a topic or phrase to surface feedback records by meaning. This is an early preview of future Topics & Subtopics.", + "semantic_topics_preview_title": "Search feedback by topic", "set_value": "set value", "setup_connection": "Setup connection", "showing_count_loaded": "Showing {count} records", @@ -3775,6 +3789,7 @@ "submission_id": "Submission ID", "survey_has_no_questions": "This survey has no questions", "topics_and_subtopics": "Topics & Subtopics", + "try_searching_for": "Try searching for", "unify_feedback": "Unify Feedback", "update_mapping_description": "Update the mapping configuration for this source.", "updated_at": "Updated at", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index b9c7f5a2b2..f827935948 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Algunos archivos no se han podido subir", "something_went_wrong": "Algo ha salido mal", "something_went_wrong_please_try_again": "Algo ha salido mal. Por favor, inténtalo de nuevo.", - "soon": "Próximamente", "sort_by": "Ordenar por", "start_free_trial": "Iniciar prueba gratuita", "status": "Estado", @@ -1746,7 +1745,7 @@ "filter_data": "Filtrar datos", "filters": "Filtros", "filters_toggle_description": "Incluye solo los datos que cumplan las siguientes condiciones.", - "go_to_feedback_directories": "Ir a Directorios de Comentarios", + "go_to_feedback_directories": "Ir a directorios de feedback", "granularity": "Granularidad", "granularity_day": "Día", "granularity_hour": "Hora", @@ -1770,7 +1769,7 @@ "no_data_available": "No hay datos disponibles", "no_data_returned": "La consulta no ha devuelto datos", "no_data_returned_for_chart": "No se han devuelto datos para el gráfico", - "no_data_source_available": "No hay ningún directorio de comentarios asignado a este espacio de trabajo.", + "no_data_source_available": "No hay ningún directorio de feedback asignado a este espacio de trabajo.", "no_grouping": "Ninguno (solo filtro)", "no_valid_data_to_display": "No hay datos válidos para mostrar", "not_contains": "no contiene", @@ -1851,10 +1850,10 @@ "api_key_updated": "Clave API actualizada", "delete_api_key_confirmation": "Cualquier aplicación que use esta clave ya no podrá acceder a tus datos de Formbricks.", "duplicate_access": "No se permite el acceso duplicado al espacio de trabajo", - "duplicate_directory_access": "No se permite el acceso duplicado al directorio de comentarios", - "feedback_directory_access": "Acceso al Directorio de Comentarios", + "duplicate_directory_access": "No se permite el acceso duplicado al directorio de feedback", + "feedback_directory_access": "Acceso al directorio de feedback", "no_api_keys_yet": "Aún no tienes ninguna clave API", - "no_directory_permissions_found": "No se encontraron permisos de directorio de comentarios", + "no_directory_permissions_found": "No se encontraron permisos para el directorio de feedback", "no_workspace_permissions_found": "No se encontraron permisos del espacio de trabajo", "organization_access": "Acceso a la organización", "organization_access_description": "Selecciona privilegios de lectura o escritura para recursos de toda la organización.", @@ -2566,13 +2565,13 @@ "error_directory_workspaces_invalid_org": "Algunos de los espacios de trabajo especificados no pertenecen a esta organización.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "Directorios de Feedback", - "no_access": "No tienes permiso para gestionar los directorios de feedback.", + "no_access": "No tienes permiso para gestionar directorios de feedback.", "no_connectors": "Aún no hay conectores vinculados a este directorio.", "pause_connectors_confirmation_description": "Si pausas estos conectores, no se añadirán nuevos registros.", "pause_connectors_confirmation_title": "¿Pausar conectores vinculados?", "select_workspaces_placeholder": "Selecciona espacios de trabajo...", "show_archived": "Mostrar archivados", - "title": "Directorios de Feedback", + "title": "Directorios de feedback", "unarchive": "Desarchivar", "unarchive_workspace_conflict": "No se puede desarchivar este directorio porque uno o más espacios de trabajo asignados están archivados.", "upgrade_prompt_description": "Organiza los registros de feedback en directorios y dirige los datos al espacio de trabajo adecuado. Disponible en los planes Pro y Scale.", @@ -3687,7 +3686,7 @@ "enum": "enum", "failed_to_load_feedback_records": "Error al cargar los registros de comentarios", "feedback_date": "Fecha actual", - "feedback_directory": "Directorio de Feedback", + "feedback_directory": "Directorio de feedback", "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.", @@ -3727,7 +3726,7 @@ "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_directory_available": "No hay ningún directorio de feedback asignado a este espacio de trabajo. Crea o asigna uno primero.", + "no_feedback_directory_available": "No hay ningún directorio de feedback asignado a este espacio de trabajo. Primero crea o asigna uno.", "no_feedback_records": "Aún no hay registros de comentarios. Los registros aparecerán aquí una vez que tus conectores empiecen a enviar datos.", "no_source_fields_loaded": "Aún no se han cargado campos de origen", "no_sources_connected": "Aún no hay fuentes conectadas. Añade una fuente para empezar.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Solicitar integración de fuente", "required": "Obligatorio", "save_changes": "Guardar cambios", + "search_feedback": "Buscar feedback", "select_a_survey_to_see_questions": "Selecciona una encuesta para ver sus preguntas", "select_a_value": "Selecciona un valor...", "select_feedback_directory": "Selecciona un directorio", @@ -3748,6 +3748,20 @@ "select_survey": "Seleccionar encuesta", "select_survey_and_questions": "Seleccionar encuesta y preguntas", "select_survey_questions_description": "Elige qué preguntas de la encuesta deben crear FeedbackRecords.", + "semantic_search_failed": "No se pudieron buscar los registros de feedback", + "semantic_search_input_label": "Buscar registros de feedback por tema", + "semantic_search_missing_text": "Este registro de feedback no tiene texto para mostrar.", + "semantic_search_no_directories": "Todavía no hay ningún directorio de registros de feedback asignado a este espacio de trabajo. Añade una fuente de feedback para empezar a buscar feedback por significado.", + "semantic_search_no_results": "No se encontraron registros de feedback coincidentes. Prueba con un tema más amplio o una frase diferente.", + "semantic_search_placeholder": "Busca un tema, por ejemplo, quejas sobre precios", + "semantic_search_relevance": "{score}% de relevancia", + "semantic_search_results_count": "{count, plural, one {# registro de feedback coincidente} other {# registros de feedback coincidentes}}", + "semantic_search_unavailable": "La búsqueda semántica aún no está disponible. Configura los embeddings del Hub para usar esta vista previa.", + "semantic_topics_example_confusing_onboarding": "onboarding confuso", + "semantic_topics_example_pricing_complaints": "quejas sobre precios", + "semantic_topics_example_slow_checkout": "proceso de pago lento", + "semantic_topics_preview_description": "Introduce un tema o frase para encontrar registros de comentarios por significado. Esta es una vista previa temprana de futuros Temas y Subtemas.", + "semantic_topics_preview_title": "Buscar comentarios por tema", "set_value": "establecer valor", "setup_connection": "Configurar conexión", "showing_count_loaded": "Mostrando {count} registros", @@ -3775,6 +3789,7 @@ "submission_id": "ID de envío", "survey_has_no_questions": "Esta encuesta no tiene preguntas", "topics_and_subtopics": "Temas y subtemas", + "try_searching_for": "Prueba a buscar", "unify_feedback": "Unificar feedback", "update_mapping_description": "Actualiza la configuración de mapeo para esta fuente.", "updated_at": "Actualizado el", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index ae3deb1861..dcf6f00fbb 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Certains fichiers n'ont pas pu être téléchargés", "something_went_wrong": "Quelque chose s'est mal passé.", "something_went_wrong_please_try_again": "Une erreur s'est produite. Veuillez réessayer.", - "soon": "Bientôt", "sort_by": "Trier par", "start_free_trial": "Commencer l'essai gratuit", "status": "Statut", @@ -1746,7 +1745,7 @@ "filter_data": "Filtrer les données", "filters": "Filtres", "filters_toggle_description": "Inclure uniquement les données qui répondent aux conditions suivantes.", - "go_to_feedback_directories": "Accéder aux répertoires de commentaires", + "go_to_feedback_directories": "Accéder aux répertoires de retours", "granularity": "Granularité", "granularity_day": "Jour", "granularity_hour": "Heure", @@ -1770,7 +1769,7 @@ "no_data_available": "Aucune donnée disponible", "no_data_returned": "Aucune donnée retournée par la requête", "no_data_returned_for_chart": "Aucune donnée retournée pour le graphique", - "no_data_source_available": "Aucun répertoire de commentaires n'est attribué à cet espace de travail.", + "no_data_source_available": "Aucun répertoire de retours n'est attribué à cet espace de travail.", "no_grouping": "Aucun (filtre uniquement)", "no_valid_data_to_display": "Aucune donnée valide à afficher", "not_contains": "ne contient pas", @@ -1851,10 +1850,10 @@ "api_key_updated": "Clé API mise à jour", "delete_api_key_confirmation": "Toute application utilisant cette clé ne pourra plus accéder à vos données Formbricks.", "duplicate_access": "Accès en double à l'espace de travail non autorisé", - "duplicate_directory_access": "L'accès en double au répertoire de commentaires n'est pas autorisé", - "feedback_directory_access": "Accès au répertoire de commentaires", + "duplicate_directory_access": "L'accès en double au répertoire de retours n'est pas autorisé", + "feedback_directory_access": "Accès au répertoire de retours", "no_api_keys_yet": "Vous n'avez pas encore de clés API", - "no_directory_permissions_found": "Aucune autorisation de répertoire de commentaires trouvée", + "no_directory_permissions_found": "Aucune autorisation de répertoire de retours trouvée", "no_workspace_permissions_found": "Aucune autorisation d'espace de travail trouvée", "organization_access": "Accès à l'organisation", "organization_access_description": "Sélectionnez les privilèges de lecture ou d'écriture pour les ressources à l'échelle de l'organisation.", @@ -2547,10 +2546,10 @@ "archive_directory": "Archiver le répertoire", "archive_not_allowed": "Vous n'êtes pas autorisé à archiver ce répertoire.", "are_you_sure_you_want_to_archive": "Es-tu sûr de vouloir archiver ce répertoire ? Les espaces de travail n'y auront plus accès.", - "assign_workspaces_description": "Contrôle quels espaces de travail peuvent accéder à ce répertoire de feedback.", + "assign_workspaces_description": "Contrôle quels espaces de travail peuvent accéder à ce répertoire de retours.", "connectors_description": "Connecteurs qui envoient des enregistrements de retour d'expérience vers ce répertoire.", "create_feedback_directory": "Créer un répertoire de commentaires", - "description": "Gère les répertoires de feedback et leurs affectations aux espaces de travail.", + "description": "Gère les répertoires de retours et leurs attributions d'espaces de travail.", "directory_archived_successfully": "Répertoire archivé avec succès", "directory_created_successfully": "Répertoire créé avec succès", "directory_id": "ID du répertoire", @@ -2559,20 +2558,20 @@ "directory_settings_title": "Paramètres de {directoryName}", "directory_unarchived_successfully": "Répertoire désarchivé avec succès", "directory_updated_successfully": "Répertoire mis à jour avec succès", - "empty_state": "Aucun répertoire de feedback trouvé. Crée-en un pour commencer.", + "empty_state": "Aucun répertoire de retours trouvé. Crée-en un pour commencer.", "error_directory_has_connectors": "Impossible d'archiver un répertoire auquel des connecteurs sont liés. Supprimez d'abord tous les connecteurs.", - "error_directory_name_duplicate": "Un répertoire de feedback avec ce nom existe déjà.", + "error_directory_name_duplicate": "Un répertoire de retours avec ce nom existe déjà.", "error_directory_name_required": "Le nom du répertoire est requis.", "error_directory_workspaces_invalid_org": "Certains espaces de travail spécifiés n'appartiennent pas à cette organisation.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "Répertoires de feedback", - "no_access": "Tu n'as pas la permission de gérer les répertoires de feedback.", + "no_access": "Tu n'as pas la permission de gérer les répertoires de retours.", "no_connectors": "Aucun connecteur lié à ce répertoire pour le moment.", "pause_connectors_confirmation_description": "Si vous mettez ces connecteurs en pause, aucun nouvel enregistrement ne sera ajouté.", "pause_connectors_confirmation_title": "Mettre en pause les connecteurs liés ?", "select_workspaces_placeholder": "Sélectionner des espaces de travail...", "show_archived": "Afficher les éléments archivés", - "title": "Répertoires de feedback", + "title": "Répertoires de retours", "unarchive": "Désarchiver", "unarchive_workspace_conflict": "Impossible de désarchiver ce répertoire, car un ou plusieurs espaces de travail attribués sont archivés.", "upgrade_prompt_description": "Organisez les enregistrements de feedback dans des répertoires et dirigez les données vers le bon espace de travail. Disponible avec les forfaits Pro et Scale.", @@ -3687,7 +3686,7 @@ "enum": "enum", "failed_to_load_feedback_records": "Échec du chargement des enregistrements de feedback", "feedback_date": "Date actuelle", - "feedback_directory": "Répertoire de feedback", + "feedback_directory": "Répertoire de retours", "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 d’enregistrement des commentaires.", @@ -3727,7 +3726,7 @@ "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_directory_available": "Aucun répertoire de feedback n'est assigné à cet espace de travail. Créez-en un ou assignez-en un d'abord.", + "no_feedback_directory_available": "Aucun répertoire de retours attribué à cet espace de travail. Crée-en un ou attribue-en un d'abord.", "no_feedback_records": "Aucun enregistrement de feedback pour le moment. Les enregistrements apparaîtront ici une fois que vos connecteurs commenceront à envoyer des données.", "no_source_fields_loaded": "Aucun champ source chargé pour le moment", "no_sources_connected": "Aucune source connectée pour le moment. Ajoutez une source pour commencer.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Demander une intégration de source", "required": "Requis", "save_changes": "Enregistrer les modifications", + "search_feedback": "Rechercher des retours", "select_a_survey_to_see_questions": "Sélectionnez une enquête pour voir ses questions", "select_a_value": "Sélectionnez une valeur...", "select_feedback_directory": "Sélectionner un répertoire", @@ -3748,6 +3748,20 @@ "select_survey": "Sélectionner l'enquête", "select_survey_and_questions": "Sélectionner l'enquête et les questions", "select_survey_questions_description": "Choisissez quelles questions d'enquête doivent créer des FeedbackRecords.", + "semantic_search_failed": "Échec de la recherche des enregistrements de retours", + "semantic_search_input_label": "Recherche des enregistrements de retours par sujet", + "semantic_search_missing_text": "Cet enregistrement de retours n'a pas de texte à afficher.", + "semantic_search_no_directories": "Aucun répertoire d'enregistrements de retours n'est encore attribué à cet espace de travail. Ajoute une source de retours pour commencer à rechercher des retours par signification.", + "semantic_search_no_results": "Aucun enregistrement de retours correspondant trouvé. Essaie un sujet plus large ou une formulation différente.", + "semantic_search_placeholder": "Recherche un sujet, par ex. plaintes sur les prix", + "semantic_search_relevance": "{score} % de pertinence", + "semantic_search_results_count": "{count, plural, one {# enregistrement de retours correspondant} other {# enregistrements de retours correspondants}}", + "semantic_search_unavailable": "La recherche sémantique n'est pas encore disponible. Configure les embeddings Hub pour utiliser cet aperçu.", + "semantic_topics_example_confusing_onboarding": "intégration confuse", + "semantic_topics_example_pricing_complaints": "plaintes sur les prix", + "semantic_topics_example_slow_checkout": "passage en caisse lent", + "semantic_topics_preview_description": "Saisissez un sujet ou une phrase pour retrouver des retours clients par signification. Ceci est un aperçu des futurs Sujets et Sous-sujets.", + "semantic_topics_preview_title": "Rechercher des retours par sujet", "set_value": "définir la valeur", "setup_connection": "Configurer la connexion", "showing_count_loaded": "Affichage de {count} enregistrements", @@ -3775,6 +3789,7 @@ "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", + "try_searching_for": "Essayez de rechercher", "unify_feedback": "Unifier les retours", "update_mapping_description": "Mettre à jour la configuration de mappage pour cette source.", "updated_at": "Mis à jour à", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 7ba5440f20..12bc84176d 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Néhány fájlt nem sikerült feltölteni", "something_went_wrong": "Valami probléma történt", "something_went_wrong_please_try_again": "Valami probléma történt. Próbálja meg újra.", - "soon": "Hamarosan", "sort_by": "Rendezési sorrend", "start_free_trial": "Ingyenes próbaidőszak indítása", "status": "Állapot", @@ -1770,7 +1769,7 @@ "no_data_available": "Nincsenek elérhető adatok", "no_data_returned": "A lekérdezés nem adott vissza adatokat", "no_data_returned_for_chart": "A diagram nem adott vissza adatokat", - "no_data_source_available": "Ehhez a munkaterülethez nem tartozik visszajelzési könyvtár.", + "no_data_source_available": "Ehhez a munkaterülethez nincs visszajelzési könyvtár hozzárendelve.", "no_grouping": "Nincs (csak szűrés)", "no_valid_data_to_display": "Nincsenek megjeleníthető érvényes adatok", "not_contains": "nem tartalmazza", @@ -2559,20 +2558,20 @@ "directory_settings_title": "{directoryName} beállításai", "directory_unarchived_successfully": "A könyvtár archiválása sikeresen visszavonva", "directory_updated_successfully": "A könyvtár sikeresen frissítve", - "empty_state": "Nem található visszajelzési könyvtár. Hozzon létre egyet a kezdéshez.", + "empty_state": "Nem találhatók visszajelzési könyvtárak. Hozzon létre egyet a kezdéshez.", "error_directory_has_connectors": "Nem archiválható olyan könyvtár, amelyhez csatlakozók vannak társítva. Először távolítson el minden csatlakozót.", - "error_directory_name_duplicate": "Ezzel a névvel már létezik visszajelzési könyvtár.", + "error_directory_name_duplicate": "Már létezik visszajelzési könyvtár ezzel a névvel.", "error_directory_name_required": "A könyvtár neve kötelező megadni.", "error_directory_workspaces_invalid_org": "Egyes megadott munkaterületek nem ehhez a szervezethez tartoznak.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "Visszajelzési könyvtárak", - "no_access": "Nem rendelkezik jogosultsággal a visszajelzési könyvtárak kezeléséhez.", + "no_access": "Önnek nincs jogosultsága a visszajelzési könyvtárak kezeléséhez.", "no_connectors": "Még nincsenek csatlakozók társítva ehhez a könyvtárhoz.", "pause_connectors_confirmation_description": "Ha szünetelteti ezeket a csatlakozókat, nem kerülnek be új rekordok.", "pause_connectors_confirmation_title": "Szünetelteti a kapcsolódó csatlakozókat?", "select_workspaces_placeholder": "Munkaterületek kiválasztása...", "show_archived": "Archivált elemek megjelenítése", - "title": "Visszajelzési Könyvtárak", + "title": "Visszajelzési könyvtárak", "unarchive": "Archiválás visszavonása", "unarchive_workspace_conflict": "A könyvtár nem állítható vissza, mert egy vagy több hozzárendelt munkaterület archiválva van.", "upgrade_prompt_description": "Szervezze a visszajelzési rekordokat könyvtárakba, és irányítsa az adatokat a megfelelő munkaterületre. A Pro és Scale csomagokban érhető el.", @@ -3687,7 +3686,7 @@ "enum": "felsorolás", "failed_to_load_feedback_records": "Nem sikerült betölteni a visszajelzési rekordokat", "feedback_date": "Aktuális dátum", - "feedback_directory": "Visszajelzési Könyvtár", + "feedback_directory": "Visszajelzési könyvtár", "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.", @@ -3727,7 +3726,7 @@ "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_directory_available": "Ehhez a munkaterülethez nem tartozik visszajelzési könyvtár. Először hozzon létre vagy rendeljen hozzá egyet.", + "no_feedback_directory_available": "Ehhez a munkaterülethez nincs visszajelzési könyvtár hozzárendelve. Hozzon létre vagy rendeljen hozzá egyet először.", "no_feedback_records": "Még nincsenek visszajelzési rekordok. A rekordok itt fognak megjelenni, amint a csatlakozók elkezdik küldeni az adatokat.", "no_source_fields_loaded": "Még nincsenek forrás mezők betöltve", "no_sources_connected": "Még nincsenek források csatlakoztatva. Adj hozzá egy forrást a kezdéshez.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Forrásintegráció kérése", "required": "Kötelező", "save_changes": "Változtatások mentése", + "search_feedback": "Visszajelzések keresése", "select_a_survey_to_see_questions": "Válassz egy kérdőívet a kérdések megtekintéséhez", "select_a_value": "Válassz egy értéket...", "select_feedback_directory": "Válasszon egy könyvtárat", @@ -3748,6 +3748,20 @@ "select_survey": "Kérdőív kiválasztása", "select_survey_and_questions": "Kérdőív és kérdések kiválasztása", "select_survey_questions_description": "Válassza ki, mely kérdőívkérdések hozzanak létre visszajelzési rekordokat.", + "semantic_search_failed": "A visszajelzési rekordok keresése sikertelen", + "semantic_search_input_label": "Visszajelzési rekordok keresése téma szerint", + "semantic_search_missing_text": "Ez a visszajelzési rekord nem tartalmaz megjeleníthető szöveget.", + "semantic_search_no_directories": "Ehhez a munkaterülethez még nincs visszajelzési rekord könyvtár hozzárendelve. Adjon hozzá egy visszajelzési forrást a jelentés szerinti keresés megkezdéséhez.", + "semantic_search_no_results": "Nem találhatók egyező visszajelzési rekordok. Próbálkozzon tágabb témával vagy más kifejezéssel.", + "semantic_search_placeholder": "Keressen egy témát, például árazási panaszok", + "semantic_search_relevance": "{score}% relevancia", + "semantic_search_results_count": "{count, plural, one {# egyező visszajelzési rekord} other {# egyező visszajelzési rekord}}", + "semantic_search_unavailable": "A szemantikai keresés még nem érhető el. Konfigurálja a Hub beágyazásokat az előnézet használatához.", + "semantic_topics_example_confusing_onboarding": "zavaró regisztráció", + "semantic_topics_example_pricing_complaints": "árazási panaszok", + "semantic_topics_example_slow_checkout": "lassú pénztári folyamat", + "semantic_topics_preview_description": "Adjon meg egy témát vagy kifejezést, hogy tartalmilag kapcsolódó visszajelzési bejegyzéseket jelenítsen meg. Ez egy korai előnézet a jövőbeli Témák és Altémák funkcióról.", + "semantic_topics_preview_title": "Visszajelzések keresése téma szerint", "set_value": "érték beállítása", "setup_connection": "Kapcsolat beállítása", "showing_count_loaded": "{count} rekord megjelenítése", @@ -3775,6 +3789,7 @@ "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", + "try_searching_for": "Próbálja ki a következő keresést", "unify_feedback": "Visszajelzések egyesítése", "update_mapping_description": "Frissítse a leképezési konfigurációt ehhez a forráshoz.", "updated_at": "Frissítve", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index ff632a6f5d..98f9abf910 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "一部のファイルのアップロードに失敗しました", "something_went_wrong": "問題が発生しました", "something_went_wrong_please_try_again": "問題が発生しました。もう一度お試しください。", - "soon": "近日公開", "sort_by": "並び替え", "start_free_trial": "無料トライアルを開始", "status": "ステータス", @@ -1746,7 +1745,7 @@ "filter_data": "データをフィルター", "filters": "フィルター", "filters_toggle_description": "以下の条件を満たすデータのみを含めます。", - "go_to_feedback_directories": "フィードバックディレクトリへ移動", + "go_to_feedback_directories": "フィードバックディレクトリに移動", "granularity": "粒度", "granularity_day": "日", "granularity_hour": "時間", @@ -2550,7 +2549,7 @@ "assign_workspaces_description": "このフィードバックディレクトリにアクセスできるワークスペースを管理します。", "connectors_description": "このディレクトリにフィードバックレコードを送信するコネクタ。", "create_feedback_directory": "フィードバックディレクトリを作成", - "description": "フィードバックディレクトリとワークスペースの割り当てを管理します。", + "description": "フィードバックディレクトリとワークスペースへの割り当てを管理します。", "directory_archived_successfully": "ディレクトリをアーカイブしました", "directory_created_successfully": "ディレクトリを作成しました", "directory_id": "ディレクトリID", @@ -2559,9 +2558,9 @@ "directory_settings_title": "{directoryName}の設定", "directory_unarchived_successfully": "ディレクトリのアーカイブを解除しました", "directory_updated_successfully": "ディレクトリを更新しました", - "empty_state": "フィードバックディレクトリが見つかりません。最初のディレクトリを作成してください。", + "empty_state": "フィードバックディレクトリが見つかりません。作成して始めましょう。", "error_directory_has_connectors": "コネクタがリンクされているディレクトリはアーカイブできません。まずすべてのコネクタを削除してください。", - "error_directory_name_duplicate": "この名前のフィードバックディレクトリは既に存在します。", + "error_directory_name_duplicate": "この名前のフィードバックディレクトリはすでに存在します。", "error_directory_name_required": "ディレクトリ名は必須です。", "error_directory_workspaces_invalid_org": "指定されたワークスペースの一部がこの組織に属していません。", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", @@ -3727,7 +3726,7 @@ "metadata_read_only_entries": "読み取り専用メタデータ値 (非文字列)", "metadata_value": "メタデータ値", "missing_feedback_source_title": "フィードバックソースが見つかりませんか?", - "no_feedback_directory_available": "このワークスペースにフィードバックディレクトリが割り当てられていません。まず作成または割り当てを行ってください。", + "no_feedback_directory_available": "このワークスペースにはフィードバックディレクトリが割り当てられていません。まずディレクトリを作成または割り当ててください。", "no_feedback_records": "フィードバックレコードはまだありません。コネクタがデータの送信を開始すると、ここにレコードが表示されます。", "no_source_fields_loaded": "ソースフィールドがまだ読み込まれていません", "no_sources_connected": "ソースがまだ接続されていません。開始するにはソースを追加してください。", @@ -3739,6 +3738,7 @@ "request_feedback_source": "ソース統合をリクエスト", "required": "必須", "save_changes": "変更を保存", + "search_feedback": "フィードバックを検索", "select_a_survey_to_see_questions": "フォームを選択して質問を表示", "select_a_value": "値を選択...", "select_feedback_directory": "ディレクトリを選択", @@ -3748,6 +3748,20 @@ "select_survey": "フォームを選択", "select_survey_and_questions": "フォームと質問を選択", "select_survey_questions_description": "フィードバックレコードを作成するフォームの質問を選択してください。", + "semantic_search_failed": "フィードバック記録の検索に失敗しました", + "semantic_search_input_label": "トピック別にフィードバック記録を検索", + "semantic_search_missing_text": "このフィードバック記録には表示するテキストがありません。", + "semantic_search_no_directories": "このワークスペースにはまだフィードバック記録ディレクトリが割り当てられていません。フィードバックソースを追加して、意味による検索を開始しましょう。", + "semantic_search_no_results": "一致するフィードバック記録が見つかりませんでした。より広いトピックや別のフレーズを試してください。", + "semantic_search_placeholder": "トピックを検索、例: 価格に関する苦情", + "semantic_search_relevance": "関連性 {score}%", + "semantic_search_results_count": "{count, plural, other {# 件のフィードバック記録}}", + "semantic_search_unavailable": "セマンティック検索はまだ利用できません。このプレビューを使用するには、Hub埋め込みを設定してください。", + "semantic_topics_example_confusing_onboarding": "わかりにくいオンボーディング", + "semantic_topics_example_pricing_complaints": "価格に関する苦情", + "semantic_topics_example_slow_checkout": "チェックアウトが遅い", + "semantic_topics_preview_description": "トピックやフレーズを入力すると、意味に基づいてフィードバック記録を表示できます。これは、今後のトピックとサブトピック機能の早期プレビューです。", + "semantic_topics_preview_title": "トピック別にフィードバックを検索", "set_value": "値を設定", "setup_connection": "接続を設定", "showing_count_loaded": "{count}件のレコードを表示中", @@ -3775,6 +3789,7 @@ "submission_id": "提出ID", "survey_has_no_questions": "このアンケートには質問がありません", "topics_and_subtopics": "トピックとサブトピック", + "try_searching_for": "検索してみる", "unify_feedback": "フィードバックを統合", "update_mapping_description": "このソースのマッピング設定を更新します。", "updated_at": "更新日時", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index 44fe80a1c4..c5845709e9 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Sommige bestanden konden niet worden geüpload", "something_went_wrong": "Er is iets misgegaan", "something_went_wrong_please_try_again": "Er is iets misgegaan. Probeer het opnieuw.", - "soon": "Binnenkort", "sort_by": "Sorteer op", "start_free_trial": "Start gratis proefperiode", "status": "Status", @@ -1746,7 +1745,7 @@ "filter_data": "Data filteren", "filters": "Filters", "filters_toggle_description": "Neem alleen gegevens op die aan de volgende voorwaarden voldoen.", - "go_to_feedback_directories": "Ga naar Feedbackmappen", + "go_to_feedback_directories": "Ga naar feedbackmappen", "granularity": "Granulariteit", "granularity_day": "Dag", "granularity_hour": "Uur", @@ -2550,7 +2549,7 @@ "assign_workspaces_description": "Bepaal welke workspaces toegang hebben tot deze feedbackmap.", "connectors_description": "Connectoren die feedbackrecords naar deze map sturen.", "create_feedback_directory": "Feedbackmap maken", - "description": "Beheer feedbackmappen en hun workspace-toewijzingen.", + "description": "Beheer feedbackmappen en hun workspacetoewijzingen.", "directory_archived_successfully": "Map succesvol gearchiveerd", "directory_created_successfully": "Map succesvol aangemaakt", "directory_id": "Map-ID", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Bronintegratie aanvragen", "required": "Vereist", "save_changes": "Wijzigingen opslaan", + "search_feedback": "Zoek feedback", "select_a_survey_to_see_questions": "Selecteer een enquête om de vragen te zien", "select_a_value": "Selecteer een waarde...", "select_feedback_directory": "Selecteer een map", @@ -3748,6 +3748,20 @@ "select_survey": "Selecteer enquête", "select_survey_and_questions": "Selecteer enquête & vragen", "select_survey_questions_description": "Kies welke enquêtevragen FeedbackRecords moeten aanmaken.", + "semantic_search_failed": "Feedbackrecords zoeken is mislukt", + "semantic_search_input_label": "Zoek feedbackrecords op onderwerp", + "semantic_search_missing_text": "Dit feedbackrecord heeft geen tekst om weer te geven.", + "semantic_search_no_directories": "Er is nog geen feedbackmap toegewezen aan deze workspace. Voeg een feedbackbron toe om te beginnen met zoeken op betekenis.", + "semantic_search_no_results": "Geen overeenkomende feedbackrecords gevonden. Probeer een breder onderwerp of een andere zoekopdracht.", + "semantic_search_placeholder": "Zoek naar een onderwerp, bijv. klachten over prijzen", + "semantic_search_relevance": "{score}% relevant", + "semantic_search_results_count": "{count, plural, one {# overeenkomend feedbackrecord} other {# overeenkomende feedbackrecords}}", + "semantic_search_unavailable": "Semantisch zoeken is nog niet beschikbaar. Configureer Hub-embeddings om deze preview te gebruiken.", + "semantic_topics_example_confusing_onboarding": "verwarrende onboarding", + "semantic_topics_example_pricing_complaints": "klachten over prijzen", + "semantic_topics_example_slow_checkout": "trage afrekening", + "semantic_topics_preview_description": "Voer een onderwerp of zin in om feedbackrecords op betekenis te vinden. Dit is een vroege preview van toekomstige Onderwerpen & Subonderwerpen.", + "semantic_topics_preview_title": "Zoek feedback op onderwerp", "set_value": "waarde instellen", "setup_connection": "Verbinding instellen", "showing_count_loaded": "Er worden {count} records weergegeven", @@ -3775,6 +3789,7 @@ "submission_id": "Inzendings-ID", "survey_has_no_questions": "Deze enquête heeft geen vragen", "topics_and_subtopics": "Onderwerpen en subonderwerpen", + "try_searching_for": "Probeer te zoeken naar", "unify_feedback": "Feedback verenigen", "update_mapping_description": "Werk de mappingconfiguratie voor deze bron bij.", "updated_at": "Bijgewerkt op", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 8dbc4e7117..5017d54370 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Alguns arquivos falharam ao enviar", "something_went_wrong": "Algo deu errado", "something_went_wrong_please_try_again": "Algo deu errado. Tente novamente.", - "soon": "Em breve", "sort_by": "Ordenar por", "start_free_trial": "Iniciar teste gratuito", "status": "status", @@ -2547,10 +2546,10 @@ "archive_directory": "Arquivar Diretório", "archive_not_allowed": "Você não tem permissão para arquivar este diretório.", "are_you_sure_you_want_to_archive": "Tem certeza de que deseja arquivar este diretório? Os espaços de trabalho não terão mais acesso a ele.", - "assign_workspaces_description": "Controle quais espaços de trabalho podem acessar este diretório de feedback.", + "assign_workspaces_description": "Controle quais workspaces podem acessar este diretório de feedback.", "connectors_description": "Conectores que enviam registros de feedback para este diretório.", "create_feedback_directory": "Criar diretório de feedback", - "description": "Gerencie diretórios de feedback e suas atribuições de espaços de trabalho.", + "description": "Gerencie diretórios de feedback e suas atribuições de workspace.", "directory_archived_successfully": "Diretório arquivado com sucesso", "directory_created_successfully": "Diretório criado com sucesso", "directory_id": "ID do Diretório", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Solicitar integração de fonte", "required": "Obrigatório", "save_changes": "Salvar alterações", + "search_feedback": "Buscar feedback", "select_a_survey_to_see_questions": "Selecione uma pesquisa para ver suas perguntas", "select_a_value": "Selecione um valor...", "select_feedback_directory": "Selecione um diretório", @@ -3748,6 +3748,20 @@ "select_survey": "Selecionar pesquisa", "select_survey_and_questions": "Selecionar pesquisa e perguntas", "select_survey_questions_description": "Escolha quais perguntas da pesquisa devem criar FeedbackRecords.", + "semantic_search_failed": "Falha ao buscar registros de feedback", + "semantic_search_input_label": "Busque registros de feedback por tópico", + "semantic_search_missing_text": "Este registro de feedback não possui texto para exibir.", + "semantic_search_no_directories": "Nenhum diretório de registro de feedback está atribuído a este workspace ainda. Adicione uma fonte de feedback para começar a buscar feedback por significado.", + "semantic_search_no_results": "Nenhum registro de feedback correspondente encontrado. Tente um tópico mais amplo ou uma frase diferente.", + "semantic_search_placeholder": "Busque por um tópico, ex.: reclamações sobre preço", + "semantic_search_relevance": "{score}% de relevância", + "semantic_search_results_count": "{count, plural, one {# registro de feedback correspondente} other {# registros de feedback correspondentes}}", + "semantic_search_unavailable": "A busca semântica ainda não está disponível. Configure os embeddings do Hub para usar esta prévia.", + "semantic_topics_example_confusing_onboarding": "onboarding confuso", + "semantic_topics_example_pricing_complaints": "reclamações sobre preço", + "semantic_topics_example_slow_checkout": "checkout lento", + "semantic_topics_preview_description": "Digite um tópico ou frase para encontrar registros de feedback por significado. Esta é uma prévia antecipada dos futuros Tópicos e Subtópicos.", + "semantic_topics_preview_title": "Buscar feedback por tópico", "set_value": "definir valor", "setup_connection": "Configurar conexão", "showing_count_loaded": "Mostrando {count} registros", @@ -3775,6 +3789,7 @@ "submission_id": "ID de envio", "survey_has_no_questions": "Esta pesquisa não possui perguntas", "topics_and_subtopics": "Tópicos e subtópicos", + "try_searching_for": "Tente buscar por", "unify_feedback": "Unificar feedback", "update_mapping_description": "Atualize a configuração de mapeamento para esta fonte.", "updated_at": "Atualizado em", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 4d05964c0a..57469a2e0d 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar", "something_went_wrong": "Algo correu mal", "something_went_wrong_please_try_again": "Algo correu mal. Por favor, tente novamente.", - "soon": "Em breve", "sort_by": "Ordem", "start_free_trial": "Iniciar teste gratuito", "status": "Estado", @@ -1770,7 +1769,7 @@ "no_data_available": "Nenhum dado disponível", "no_data_returned": "Nenhum dado devolvido pela consulta", "no_data_returned_for_chart": "Nenhum dado devolvido para o gráfico", - "no_data_source_available": "Nenhum diretório de feedback está atribuído a este espaço de trabalho.", + "no_data_source_available": "Nenhum diretório de feedback está atribuído a este workspace.", "no_grouping": "Nenhum (apenas filtro)", "no_valid_data_to_display": "Nenhum dado válido para exibir", "not_contains": "não contém", @@ -1854,7 +1853,7 @@ "duplicate_directory_access": "Não é permitido acesso duplicado ao diretório de feedback", "feedback_directory_access": "Acesso ao Diretório de Feedback", "no_api_keys_yet": "Ainda não tem nenhuma chave de API", - "no_directory_permissions_found": "Não foram encontradas permissões de diretório de feedback", + "no_directory_permissions_found": "Nenhuma permissão de diretório de feedback encontrada", "no_workspace_permissions_found": "Não foram encontradas permissões de Espaço de Trabalho", "organization_access": "Acesso à organização", "organization_access_description": "Selecione privilégios de leitura ou escrita para recursos de toda a organização.", @@ -2547,10 +2546,10 @@ "archive_directory": "Arquivar Diretório", "archive_not_allowed": "Não tens permissão para arquivar este diretório.", "are_you_sure_you_want_to_archive": "Tens a certeza de que queres arquivar este diretório? Os espaços de trabalho deixarão de ter acesso ao mesmo.", - "assign_workspaces_description": "Controla quais os espaços de trabalho que podem aceder a este diretório de feedback.", + "assign_workspaces_description": "Controla quais workspaces podem aceder a este diretório de feedback.", "connectors_description": "Conectores que enviam registos de feedback para este diretório.", "create_feedback_directory": "Criar diretório de feedback", - "description": "Gere diretórios de feedback e as suas atribuições de espaços de trabalho.", + "description": "Gere diretórios de feedback e as suas atribuições de workspace.", "directory_archived_successfully": "Diretório arquivado com sucesso", "directory_created_successfully": "Diretório criado com sucesso", "directory_id": "ID do Diretório", @@ -2559,7 +2558,7 @@ "directory_settings_title": "Definições de {directoryName}", "directory_unarchived_successfully": "Diretório desarquivado com sucesso", "directory_updated_successfully": "Diretório atualizado com sucesso", - "empty_state": "Não foram encontrados diretórios de feedback. Cria um para começar.", + "empty_state": "Nenhum diretório de feedback encontrado. Cria um para começar.", "error_directory_has_connectors": "Não é possível arquivar um diretório que tem conectores associados. Remove todos os conectores primeiro.", "error_directory_name_duplicate": "Já existe um diretório de feedback com este nome.", "error_directory_name_required": "O nome do diretório é obrigatório.", @@ -3727,7 +3726,7 @@ "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_directory_available": "Não há nenhum diretório de feedback atribuído a este espaço de trabalho. Cria ou atribui um primeiro.", + "no_feedback_directory_available": "Nenhum diretório de feedback atribuído a este workspace. Cria ou atribui um primeiro.", "no_feedback_records": "Ainda não há registos de feedback. Os registos aparecerão aqui assim que os teus conectores começarem a enviar dados.", "no_source_fields_loaded": "Ainda não foram carregados campos de origem", "no_sources_connected": "Ainda não há origens ligadas. Adicione uma origem para começar.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Solicitar integração de fonte", "required": "Obrigatório", "save_changes": "Guardar alterações", + "search_feedback": "Pesquisar feedback", "select_a_survey_to_see_questions": "Selecione um inquérito para ver as suas perguntas", "select_a_value": "Selecione um valor...", "select_feedback_directory": "Selecionar um diretório", @@ -3748,6 +3748,20 @@ "select_survey": "Selecionar inquérito", "select_survey_and_questions": "Selecionar inquérito e perguntas", "select_survey_questions_description": "Escolha quais perguntas do inquérito devem criar FeedbackRecords.", + "semantic_search_failed": "Falha ao pesquisar registos de feedback", + "semantic_search_input_label": "Pesquisar registos de feedback por tópico", + "semantic_search_missing_text": "Este registo de feedback não tem texto para mostrar.", + "semantic_search_no_directories": "Ainda não há nenhum diretório de registos de feedback atribuído a este workspace. Adiciona uma fonte de feedback para começar a pesquisar por significado.", + "semantic_search_no_results": "Nenhum registo de feedback correspondente encontrado. Tenta um tópico mais abrangente ou uma frase diferente.", + "semantic_search_placeholder": "Pesquisar por um tópico, ex: reclamações sobre preços", + "semantic_search_relevance": "{score}% de relevância", + "semantic_search_results_count": "{count, plural, one {# registo de feedback correspondente} other {# registos de feedback correspondentes}}", + "semantic_search_unavailable": "A pesquisa semântica ainda não está disponível. Configura os embeddings do Hub para usar esta pré-visualização.", + "semantic_topics_example_confusing_onboarding": "onboarding confuso", + "semantic_topics_example_pricing_complaints": "reclamações sobre preços", + "semantic_topics_example_slow_checkout": "checkout lento", + "semantic_topics_preview_description": "Introduz um tópico ou frase para encontrar registos de feedback por significado. Esta é uma pré-visualização de futuros Tópicos e Subtópicos.", + "semantic_topics_preview_title": "Pesquisar feedback por tópico", "set_value": "definir valor", "setup_connection": "Configurar ligação", "showing_count_loaded": "A mostrar {count} registos", @@ -3775,6 +3789,7 @@ "submission_id": "ID de envio", "survey_has_no_questions": "Este inquérito não tem perguntas", "topics_and_subtopics": "Tópicos e subtópicos", + "try_searching_for": "Experimenta pesquisar por", "unify_feedback": "Unificar feedback", "update_mapping_description": "Atualiza a configuração de mapeamento para esta origem.", "updated_at": "Atualizado em", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 96eadd0c5d..ddcee66ab1 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Unele fișiere nu au reușit să se încarce", "something_went_wrong": "Ceva nu a mers bine", "something_went_wrong_please_try_again": "Ceva nu a mers bine. Vă rugăm să încercați din nou.", - "soon": "În curând", "sort_by": "Sortare după", "start_free_trial": "Începe perioada de probă gratuită", "status": "Stare", @@ -1746,7 +1745,7 @@ "filter_data": "Filtrează datele", "filters": "Filtre", "filters_toggle_description": "Include doar datele care îndeplinesc următoarele condiții.", - "go_to_feedback_directories": "Mergi la Directoarele de Feedback", + "go_to_feedback_directories": "Mergi la Directoare de Feedback", "granularity": "Granularitate", "granularity_day": "Zi", "granularity_hour": "Oră", @@ -1854,7 +1853,7 @@ "duplicate_directory_access": "Accesul duplicat la directorul de feedback nu este permis", "feedback_directory_access": "Acces la Directorul de Feedback", "no_api_keys_yet": "Nu ai încă nicio cheie API", - "no_directory_permissions_found": "Nu s-au găsit permisiuni pentru directorul de feedback", + "no_directory_permissions_found": "Nu au fost găsite permisiuni pentru directoare de feedback", "no_workspace_permissions_found": "Nu s-au găsit permisiuni pentru Workspace", "organization_access": "Acces organizație", "organization_access_description": "Selectează privilegii de citire sau scriere pentru resursele la nivel de organizație.", @@ -2547,7 +2546,7 @@ "archive_directory": "Arhivează directorul", "archive_not_allowed": "Nu ai permisiunea să arhivezi acest director.", "are_you_sure_you_want_to_archive": "Ești sigur că vrei să arhivezi acest director? Spațiile de lucru nu vor mai avea acces la el.", - "assign_workspaces_description": "Controlează care spații de lucru pot accesa acest director de feedback.", + "assign_workspaces_description": "Controlează ce spații de lucru pot accesa acest director de feedback.", "connectors_description": "Conectori care trimit înregistrări de feedback către acest director.", "create_feedback_directory": "Creează director de feedback", "description": "Gestionează directoarele de feedback și atribuirile lor la spații de lucru.", @@ -3687,7 +3686,7 @@ "enum": "enum", "failed_to_load_feedback_records": "Nu s-au putut încărca înregistrările de feedback", "feedback_date": "Data curentă", - "feedback_directory": "Director de feedback", + "feedback_directory": "Director de Feedback", "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.", @@ -3727,7 +3726,7 @@ "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_directory_available": "Niciun director de feedback atribuit acestui spațiu de lucru. Creează sau atribuie unul mai întâi.", + "no_feedback_directory_available": "Niciun director de feedback nu este atribuit acestui spațiu de lucru. Creează sau atribuie unul mai întâi.", "no_feedback_records": "Nu există încă înregistrări de feedback. Înregistrările vor apărea aici după ce conectorii tăi vor începe să trimită date.", "no_source_fields_loaded": "Nu au fost încă încărcate câmpuri sursă", "no_sources_connected": "Nicio sursă conectată încă. Adaugă o sursă pentru a începe.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Solicită integrarea sursei", "required": "Obligatoriu", "save_changes": "Salvează modificările", + "search_feedback": "Caută feedback", "select_a_survey_to_see_questions": "Selectează un chestionar pentru a vedea întrebările", "select_a_value": "Selectează o valoare...", "select_feedback_directory": "Selectează un director", @@ -3748,6 +3748,20 @@ "select_survey": "Selectează chestionar", "select_survey_and_questions": "Selectează chestionar și întrebări", "select_survey_questions_description": "Alege ce întrebări din chestionar vor crea FeedbackRecords.", + "semantic_search_failed": "Căutarea înregistrărilor de feedback a eșuat", + "semantic_search_input_label": "Caută înregistrări de feedback după subiect", + "semantic_search_missing_text": "Această înregistrare de feedback nu conține text de afișat.", + "semantic_search_no_directories": "Încă nu este atribuit niciun director de înregistrări de feedback acestui spațiu de lucru. Adaugă o sursă de feedback pentru a începe căutarea după înțeles.", + "semantic_search_no_results": "Nu au fost găsite înregistrări de feedback corespunzătoare. Încearcă un subiect mai general sau o altă formulare.", + "semantic_search_placeholder": "Caută un subiect, de ex. plângeri despre prețuri", + "semantic_search_relevance": "{score}% relevanță", + "semantic_search_results_count": "{count, plural, one {# înregistrare de feedback corespunzătoare} few {# înregistrări de feedback corespunzătoare} other {# de înregistrări de feedback corespunzătoare}}", + "semantic_search_unavailable": "Căutarea semantică nu este disponibilă încă. Configurează încorporările Hub pentru a folosi această previzualizare.", + "semantic_topics_example_confusing_onboarding": "onboarding confuz", + "semantic_topics_example_pricing_complaints": "plângeri despre prețuri", + "semantic_topics_example_slow_checkout": "finalizare lentă", + "semantic_topics_preview_description": "Introdu un subiect sau o frază pentru a identifica înregistrările de feedback după sens. Aceasta este o previzualizare timpurie a viitoarelor Subiecte și Subsubiecte.", + "semantic_topics_preview_title": "Caută feedback după subiect", "set_value": "setează valoare", "setup_connection": "Configurează conexiunea", "showing_count_loaded": "Se afișează {count} înregistrări", @@ -3775,6 +3789,7 @@ "submission_id": "ID-ul trimiterii", "survey_has_no_questions": "Acest sondaj nu are întrebări", "topics_and_subtopics": "Subiecte și subiecte secundare", + "try_searching_for": "Încearcă să cauți", "unify_feedback": "Unify Feedback", "update_mapping_description": "Actualizează configurația de mapare pentru această sursă.", "updated_at": "Actualizat la", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index 489e3a26c0..7d717f3e0e 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Не удалось загрузить некоторые файлы", "something_went_wrong": "Что-то пошло не так", "something_went_wrong_please_try_again": "Что-то пошло не так. Пожалуйста, попробуйте ещё раз.", - "soon": "Скоро", "sort_by": "Сортировать по", "start_free_trial": "Начать бесплатный пробный период", "status": "Статус", @@ -1746,7 +1745,7 @@ "filter_data": "Фильтровать данные", "filters": "Фильтры", "filters_toggle_description": "Включай только те данные, которые соответствуют следующим условиям.", - "go_to_feedback_directories": "Перейти к директориям отзывов", + "go_to_feedback_directories": "Перейти к директориям обратной связи", "granularity": "Детализация", "granularity_day": "День", "granularity_hour": "Час", @@ -1770,7 +1769,7 @@ "no_data_available": "Нет доступных данных", "no_data_returned": "Запрос не вернул данных", "no_data_returned_for_chart": "Для графика не получено данных", - "no_data_source_available": "К этому рабочему пространству не назначена директория отзывов.", + "no_data_source_available": "К этому рабочему пространству не привязана директория обратной связи.", "no_grouping": "Нет (только фильтр)", "no_valid_data_to_display": "Нет корректных данных для отображения", "not_contains": "не содержит", @@ -1851,10 +1850,10 @@ "api_key_updated": "API-ключ обновлён", "delete_api_key_confirmation": "Любые приложения, использующие этот ключ, больше не смогут получить доступ к вашим данным Formbricks.", "duplicate_access": "Дублированный доступ к рабочему пространству не разрешён", - "duplicate_directory_access": "Дублирование доступа к директории отзывов не разрешено", - "feedback_directory_access": "Доступ к директории отзывов", + "duplicate_directory_access": "Дублирование доступа к директории обратной связи запрещено", + "feedback_directory_access": "Доступ к директории обратной связи", "no_api_keys_yet": "У вас ещё нет API-ключей", - "no_directory_permissions_found": "Разрешения для директории отзывов не найдены", + "no_directory_permissions_found": "Разрешения для директорий обратной связи не найдены", "no_workspace_permissions_found": "Разрешения для рабочего пространства не найдены", "organization_access": "Доступ к организации", "organization_access_description": "Выберите права на чтение или запись для ресурсов всей организации.", @@ -2547,10 +2546,10 @@ "archive_directory": "Архивировать каталог", "archive_not_allowed": "У тебя нет прав для архивирования этого каталога.", "are_you_sure_you_want_to_archive": "Ты уверен, что хочешь архивировать этот каталог? Рабочие пространства больше не будут иметь к нему доступа.", - "assign_workspaces_description": "Управляй тем, какие рабочие пространства могут получить доступ к этому каталогу отзывов.", + "assign_workspaces_description": "Управляй, какие рабочие пространства могут получить доступ к этой директории обратной связи.", "connectors_description": "Коннекторы, которые отправляют записи обратной связи в этот каталог.", "create_feedback_directory": "Создать директорию для отзывов", - "description": "Управляй каталогами отзывов и их назначением рабочим пространствам.", + "description": "Управляй директориями обратной связи и их привязками к рабочим пространствам.", "directory_archived_successfully": "Каталог успешно архивирован", "directory_created_successfully": "Каталог успешно создан", "directory_id": "ID каталога", @@ -2559,14 +2558,14 @@ "directory_settings_title": "Настройки {directoryName}", "directory_unarchived_successfully": "Каталог успешно разархивирован", "directory_updated_successfully": "Каталог успешно обновлён", - "empty_state": "Каталоги отзывов не найдены. Создай один, чтобы начать.", + "empty_state": "Директории обратной связи не найдены. Создай одну, чтобы начать.", "error_directory_has_connectors": "Невозможно архивировать каталог, к которому привязаны коннекторы. Сначала удалите все коннекторы.", - "error_directory_name_duplicate": "Директория обратной связи с таким именем уже существует.", + "error_directory_name_duplicate": "Директория обратной связи с таким названием уже существует.", "error_directory_name_required": "Необходимо указать имя директории.", "error_directory_workspaces_invalid_org": "Некоторые указанные рабочие пространства не принадлежат этой организации.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "Каталоги отзывов", - "no_access": "У тебя нет прав для управления каталогами отзывов.", + "no_access": "У тебя нет прав для управления директориями обратной связи.", "no_connectors": "К этому каталогу пока не привязано ни одного коннектора.", "pause_connectors_confirmation_description": "Если приостановить эти коннекторы, новые записи больше не будут добавляться.", "pause_connectors_confirmation_title": "Приостановить связанные коннекторы?", @@ -3687,7 +3686,7 @@ "enum": "enum", "failed_to_load_feedback_records": "Не удалось загрузить отзывы", "feedback_date": "Текущая дата", - "feedback_directory": "Каталог обратной связи", + "feedback_directory": "Директория обратной связи", "feedback_record_created_successfully": "Запись отзыва успешно создана", "feedback_record_details": "Детали записи обратной связи", "feedback_record_details_description": "Просмотрите и обновите поля записи отзыва.", @@ -3727,7 +3726,7 @@ "metadata_read_only_entries": "Значения метаданных только для чтения (нестроковые)", "metadata_value": "Значение метаданных", "missing_feedback_source_title": "Не нашли нужный источник обратной связи?", - "no_feedback_directory_available": "К этому рабочему пространству не назначен каталог обратной связи. Сначала создайте или назначьте каталог.", + "no_feedback_directory_available": "К этому рабочему пространству не привязана директория обратной связи. Сначала создай или привяжи её.", "no_feedback_records": "Пока нет записей отзывов. Они появятся здесь, когда коннекторы начнут отправлять данные.", "no_source_fields_loaded": "Поля источника ещё не загружены", "no_sources_connected": "Нет подключённых источников. Добавьте источник, чтобы начать.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Запросить интеграцию источника", "required": "Обязательно", "save_changes": "Сохранить изменения", + "search_feedback": "Искать обратную связь", "select_a_survey_to_see_questions": "Выберите опрос, чтобы увидеть его вопросы", "select_a_value": "Выберите значение...", "select_feedback_directory": "Выберите каталог", @@ -3748,6 +3748,20 @@ "select_survey": "Выбрать опрос", "select_survey_and_questions": "Выбрать опрос и вопросы", "select_survey_questions_description": "Выберите, какие вопросы опроса должны создавать FeedbackRecords.", + "semantic_search_failed": "Не удалось найти записи обратной связи", + "semantic_search_input_label": "Ищи записи обратной связи по теме", + "semantic_search_missing_text": "В этой записи обратной связи нет текста для отображения.", + "semantic_search_no_directories": "К этому рабочему пространству пока не привязана директория с записями обратной связи. Добавь источник обратной связи, чтобы начать поиск по смыслу.", + "semantic_search_no_results": "Подходящие записи обратной связи не найдены. Попробуй более широкую тему или другую фразу.", + "semantic_search_placeholder": "Ищи по теме, например, жалобы на цены", + "semantic_search_relevance": "Релевантность {score}%", + "semantic_search_results_count": "{count, plural, one {# подходящая запись обратной связи} few {# подходящие записи обратной связи} many {# подходящих записей обратной связи} other {# подходящих записей обратной связи}}", + "semantic_search_unavailable": "Семантический поиск пока недоступен. Настрой эмбеддинги Hub, чтобы использовать эту функцию.", + "semantic_topics_example_confusing_onboarding": "запутанная регистрация", + "semantic_topics_example_pricing_complaints": "жалобы на цены", + "semantic_topics_example_slow_checkout": "медленное оформление заказа", + "semantic_topics_preview_description": "Введите тему или фразу, чтобы найти записи отзывов по смыслу. Это ранний предварительный просмотр будущих Тем и Подтем.", + "semantic_topics_preview_title": "Поиск отзывов по теме", "set_value": "установить значение", "setup_connection": "Настроить подключение", "showing_count_loaded": "Показано записей: {count}", @@ -3775,6 +3789,7 @@ "submission_id": "Идентификатор отправки", "survey_has_no_questions": "В этом опросе нет вопросов", "topics_and_subtopics": "Темы и подтемы", + "try_searching_for": "Попробуйте поискать", "unify_feedback": "Обратная связь Unify", "update_mapping_description": "Обнови настройки сопоставления для этого источника.", "updated_at": "Обновлено", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index c87d4f8277..9deaf0cd35 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Några filer misslyckades att laddas upp", "something_went_wrong": "Något gick fel", "something_went_wrong_please_try_again": "Något gick fel. Försök igen.", - "soon": "Snart", "sort_by": "Sortera efter", "start_free_trial": "Starta gratis provperiod", "status": "Status", @@ -1746,7 +1745,7 @@ "filter_data": "Filtrera data", "filters": "Filter", "filters_toggle_description": "Inkludera bara data som uppfyller följande villkor.", - "go_to_feedback_directories": "Gå till Feedbackkataloger", + "go_to_feedback_directories": "Gå till feedback-kataloger", "granularity": "Detaljnivå", "granularity_day": "Dag", "granularity_hour": "timme", @@ -1770,7 +1769,7 @@ "no_data_available": "Ingen data tillgänglig", "no_data_returned": "Ingen data returnerades från frågan", "no_data_returned_for_chart": "Ingen data returnerades för diagrammet", - "no_data_source_available": "Ingen feedbackkatalog är tilldelad till den här arbetsytan.", + "no_data_source_available": "Ingen feedback-katalog är tilldelad till denna arbetsyta.", "no_grouping": "Ingen (endast filter)", "no_valid_data_to_display": "Ingen giltig data att visa", "not_contains": "innehåller inte", @@ -1851,10 +1850,10 @@ "api_key_updated": "API-nyckel uppdaterad", "delete_api_key_confirmation": "Alla applikationer som använder denna nyckel kommer inte längre att kunna komma åt din Formbricks-data.", "duplicate_access": "Duplicerad arbetsyteåtkomst är inte tillåten", - "duplicate_directory_access": "Duplicerad feedbackkatalogåtkomst är inte tillåten", - "feedback_directory_access": "Feedbackkatalogåtkomst", + "duplicate_directory_access": "Duplicerad åtkomst till feedback-katalog är inte tillåten", + "feedback_directory_access": "Åtkomst till feedback-katalog", "no_api_keys_yet": "Du har inga API-nycklar ännu", - "no_directory_permissions_found": "Inga feedbackkatalogbehörigheter hittades", + "no_directory_permissions_found": "Inga behörigheter för feedback-katalog hittades", "no_workspace_permissions_found": "Inga behörigheter för arbetsytan hittades", "organization_access": "Organisationsåtkomst", "organization_access_description": "Välj läs- eller skrivbehörighet för resurser på organisationsnivå.", @@ -2547,10 +2546,10 @@ "archive_directory": "Arkivera katalog", "archive_not_allowed": "Du har inte behörighet att arkivera den här katalogen.", "are_you_sure_you_want_to_archive": "Är du säker på att du vill arkivera den här katalogen? Arbetsytor kommer inte längre ha tillgång till den.", - "assign_workspaces_description": "Styr vilka arbetsytor som kan komma åt den här feedbackkatalogen.", + "assign_workspaces_description": "Styr vilka arbetsytor som kan komma åt denna feedback-katalog.", "connectors_description": "Kopplingar som skickar feedbackposter till den här katalogen.", "create_feedback_directory": "Skapa feedbackkatalog", - "description": "Hantera feedbackkataloger och deras arbetsytstilldelningar.", + "description": "Hantera feedback-kataloger och deras arbetsytetilldelningar.", "directory_archived_successfully": "Katalogen arkiverades", "directory_created_successfully": "Katalogen skapades", "directory_id": "Katalog-ID", @@ -2559,20 +2558,20 @@ "directory_settings_title": "Inställningar för {directoryName}", "directory_unarchived_successfully": "Katalogen återställdes från arkivet", "directory_updated_successfully": "Katalogen uppdaterades", - "empty_state": "Inga feedbackkataloger hittades. Skapa en för att komma igång.", + "empty_state": "Inga feedback-kataloger hittades. Skapa en för att komma igång.", "error_directory_has_connectors": "Kan inte arkivera en katalog som har kopplingar kopplade till den. Ta bort alla kopplingar först.", - "error_directory_name_duplicate": "En feedbackkatalog med detta namn finns redan.", + "error_directory_name_duplicate": "En feedback-katalog med detta namn finns redan.", "error_directory_name_required": "Katalognamn krävs.", "error_directory_workspaces_invalid_org": "Vissa angivna arbetsytor tillhör inte denna organisation.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "Feedbackkataloger", - "no_access": "Du har inte behörighet att hantera feedbackkataloger.", + "no_access": "Du har inte behörighet att hantera feedback-kataloger.", "no_connectors": "Inga kopplingar länkade till den här katalogen ännu.", "pause_connectors_confirmation_description": "Om du pausar dessa kopplingar kommer inga nya poster att läggas till.", "pause_connectors_confirmation_title": "Pausa länkade kopplingar?", "select_workspaces_placeholder": "Välj arbetsytor...", "show_archived": "Visa arkiverade", - "title": "Feedbackkataloger", + "title": "Feedback-kataloger", "unarchive": "Avarkivera", "unarchive_workspace_conflict": "Den här katalogen kan inte avarkiveras eftersom en eller flera tilldelade arbetsytor är arkiverade.", "upgrade_prompt_description": "Organisera feedbackposter i kataloger och dirigera data till rätt arbetsyta. Tillgängligt på Pro- och Scale-planerna.", @@ -3687,7 +3686,7 @@ "enum": "enum", "failed_to_load_feedback_records": "Det gick inte att ladda feedbackposter", "feedback_date": "Aktuellt datum", - "feedback_directory": "Feedbackkatalog", + "feedback_directory": "Feedback-katalog", "feedback_record_created_successfully": "Feedbackposten har skapats", "feedback_record_details": "Feedbackpostdetaljer", "feedback_record_details_description": "Granska och uppdatera fält för feedbackposter.", @@ -3727,7 +3726,7 @@ "metadata_read_only_entries": "Skrivskyddade metadatavärden (icke-sträng)", "metadata_value": "Metadatavärde", "missing_feedback_source_title": "Missing feedback source?", - "no_feedback_directory_available": "Ingen feedbackkatalog tilldelad till den här arbetsytan. Skapa eller tilldela en först.", + "no_feedback_directory_available": "Ingen feedback-katalog är tilldelad till denna arbetsyta. Skapa eller tilldela en först.", "no_feedback_records": "Inga feedbackposter ännu. Poster visas här när dina connectors börjar skicka data.", "no_source_fields_loaded": "Inga källfält har laddats än", "no_sources_connected": "Inga källor är anslutna än. Lägg till en källa för att komma igång.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Request source integration", "required": "Obligatoriskt", "save_changes": "Spara ändringar", + "search_feedback": "Sök feedback", "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_directory": "Välj en katalog", @@ -3748,6 +3748,20 @@ "select_survey": "Välj enkät", "select_survey_and_questions": "Välj enkät & frågor", "select_survey_questions_description": "Välj vilka enkätfrågor som ska skapa FeedbackRecords.", + "semantic_search_failed": "Misslyckades med att söka i feedback-poster", + "semantic_search_input_label": "Sök feedback-poster efter ämne", + "semantic_search_missing_text": "Denna feedback-post har ingen text att visa.", + "semantic_search_no_directories": "Ingen feedback-katalog är tilldelad till denna arbetsyta ännu. Lägg till en feedback-källa för att börja söka feedback efter mening.", + "semantic_search_no_results": "Inga matchande feedback-poster hittades. Prova ett bredare ämne eller en annan fras.", + "semantic_search_placeholder": "Sök efter ett ämne, t.ex. klagomål om priser", + "semantic_search_relevance": "{score}% relevans", + "semantic_search_results_count": "{count, plural, one {# matchande feedback-post} other {# matchande feedback-poster}}", + "semantic_search_unavailable": "Semantisk sökning är inte tillgänglig ännu. Konfigurera Hub-inbäddningar för att använda denna förhandsvisning.", + "semantic_topics_example_confusing_onboarding": "förvirrande onboarding", + "semantic_topics_example_pricing_complaints": "klagomål om priser", + "semantic_topics_example_slow_checkout": "långsam utcheckning", + "semantic_topics_preview_description": "Ange ett ämne eller en fras för att hitta feedbackposter baserat på betydelse. Detta är en tidig förhandsgranskning av framtida Ämnen & Underämnen.", + "semantic_topics_preview_title": "Sök feedback efter ämne", "set_value": "ange värde", "setup_connection": "Ställ in anslutning", "showing_count_loaded": "Visar {count} poster", @@ -3775,6 +3789,7 @@ "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", + "try_searching_for": "Prova att söka efter", "unify_feedback": "Samla feedback", "update_mapping_description": "Uppdatera mappningskonfigurationen för den här källan.", "updated_at": "Uppdaterad", diff --git a/apps/web/locales/tr-TR.json b/apps/web/locales/tr-TR.json index 3b17b4b8c5..2100ae75f8 100644 --- a/apps/web/locales/tr-TR.json +++ b/apps/web/locales/tr-TR.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "Bazı dosyalar yüklenemedi", "something_went_wrong": "Bir şeyler ters gitti", "something_went_wrong_please_try_again": "Bir sorun oluştu. Lütfen tekrar deneyin.", - "soon": "Yakında", "sort_by": "Sıralama", "start_free_trial": "Ücretsiz denemeyi başlat", "status": "Durum", @@ -2561,12 +2560,12 @@ "directory_updated_successfully": "Dizin başarıyla güncellendi", "empty_state": "Geri bildirim dizini bulunamadı. Başlamak için bir tane oluştur.", "error_directory_has_connectors": "Bağlayıcıları bağlı olan bir dizin arşivlenemez. Önce tüm bağlayıcıları kaldır.", - "error_directory_name_duplicate": "Bu ada sahip bir geri bildirim dizini zaten mevcut.", + "error_directory_name_duplicate": "Bu adda bir geri bildirim dizini zaten mevcut.", "error_directory_name_required": "Dizin adı gereklidir.", "error_directory_workspaces_invalid_org": "Belirtilen çalışma alanlarından bazıları bu organizasyona ait değil.", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "Geri Bildirim Dizinleri", - "no_access": "Geri bildirim dizinlerini yönetme izniniz yok.", + "no_access": "Geri bildirim dizinlerini yönetme yetkin yok.", "no_connectors": "Bu dizine henüz bağlı bağlayıcı yok.", "pause_connectors_confirmation_description": "Bu bağlayıcıları duraklatırsanız yeni kayıtlar eklenmez.", "pause_connectors_confirmation_title": "Bağlı bağlayıcılar duraklatılsın mı?", @@ -3727,7 +3726,7 @@ "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_directory_available": "Bu çalışma alanına atanmış bir geri bildirim dizini yok. Önce bir tane oluştur veya ata.", + "no_feedback_directory_available": "Bu çalışma alanına atanmış geri bildirim dizini yok. Önce bir tane oluştur veya ata.", "no_feedback_records": "Henüz geri bildirim kaydı yok. Bağlayıcıların veri göndermeye başlamasıyla kayıtlar burada görünecek.", "no_source_fields_loaded": "Henüz kaynak alan yüklenmedi", "no_sources_connected": "Henüz bağlı kaynak yok. Başlamak için bir kaynak ekle.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Request source integration", "required": "Gerekli", "save_changes": "Değişiklikleri kaydet", + "search_feedback": "Geri bildirim ara", "select_a_survey_to_see_questions": "Sorularını görmek için bir anket seç", "select_a_value": "Bir değer seç...", "select_feedback_directory": "Bir dizin seç", @@ -3748,6 +3748,20 @@ "select_survey": "Anket Seç", "select_survey_and_questions": "Anket ve Soruları Seç", "select_survey_questions_description": "Hangi anket sorularının GeriBildirimKayıtları oluşturması gerektiğini seçin.", + "semantic_search_failed": "Geri bildirim kayıtları aranamadı", + "semantic_search_input_label": "Geri bildirim kayıtlarını konuya göre ara", + "semantic_search_missing_text": "Bu geri bildirim kaydında görüntülenecek metin yok.", + "semantic_search_no_directories": "Bu çalışma alanına henüz geri bildirim kayıt dizini atanmamış. Anlamsal aramaya başlamak için bir geri bildirim kaynağı ekle.", + "semantic_search_no_results": "Eşleşen geri bildirim kaydı bulunamadı. Daha geniş bir konu veya farklı bir ifade dene.", + "semantic_search_placeholder": "Bir konu ara, örn. fiyatlandırma şikayetleri", + "semantic_search_relevance": "%{score} uygunluk", + "semantic_search_results_count": "{count, plural, one {# eşleşen geri bildirim kaydı} other {# eşleşen geri bildirim kaydı}}", + "semantic_search_unavailable": "Anlamsal arama henüz kullanıma sunulmadı. Bu önizlemeyi kullanmak için Hub gömme ayarlarını yapılandır.", + "semantic_topics_example_confusing_onboarding": "kafa karıştırıcı onboarding", + "semantic_topics_example_pricing_complaints": "fiyatlandırma şikayetleri", + "semantic_topics_example_slow_checkout": "yavaş ödeme", + "semantic_topics_preview_description": "Anlam bazında geri bildirim kayıtlarını ortaya çıkarmak için bir konu veya ifade gir. Bu, gelecekteki Konular ve Alt Konular özelliğinin erken bir ön izlemesidir.", + "semantic_topics_preview_title": "Konuya göre geri bildirim ara", "set_value": "değer belirle", "setup_connection": "Bağlantıyı kur", "showing_count_loaded": "{count} kayıt gösteriliyor", @@ -3775,6 +3789,7 @@ "submission_id": "Gönderim Kimliği", "survey_has_no_questions": "Bu ankette soru yok", "topics_and_subtopics": "Konular ve alt konular", + "try_searching_for": "Şunu aramayı dene", "unify_feedback": "Geri Bildirimleri Birleştir", "update_mapping_description": "Bu kaynak için eşleme yapılandırmasını güncelle.", "updated_at": "Güncellenme tarihi", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 859ce42091..6a155e59e9 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "某些文件上传失败", "something_went_wrong": "出错了", "something_went_wrong_please_try_again": "出错了 。请 尝试 再次 操作 。", - "soon": "即将推出", "sort_by": "排序 依据", "start_free_trial": "开始免费试用", "status": "状态", @@ -1851,8 +1850,8 @@ "api_key_updated": "API 密钥已更新", "delete_api_key_confirmation": "使用此密钥的任何应用将无法再访问您的 Formbricks 数据。", "duplicate_access": "不允许重复的工作区访问权限", - "duplicate_directory_access": "不允许重复的反馈目录访问", - "feedback_directory_access": "反馈目录访问", + "duplicate_directory_access": "不允许重复的反馈目录访问权限", + "feedback_directory_access": "反馈目录访问权限", "no_api_keys_yet": "您还没有任何 API 密钥", "no_directory_permissions_found": "未找到反馈目录权限", "no_workspace_permissions_found": "未找到工作区权限", @@ -2559,9 +2558,9 @@ "directory_settings_title": "{directoryName} 设置", "directory_unarchived_successfully": "目录已成功取消归档", "directory_updated_successfully": "目录已成功更新", - "empty_state": "未找到反馈目录。创建一个开始使用吧。", + "empty_state": "未找到反馈目录。创建一个以开始使用。", "error_directory_has_connectors": "无法归档已链接连接器的目录。请先移除所有连接器。", - "error_directory_name_duplicate": "已存在同名的反馈目录。", + "error_directory_name_duplicate": "已存在使用此名称的反馈目录。", "error_directory_name_required": "目录名称为必填项。", "error_directory_workspaces_invalid_org": "某些指定的工作区不属于此组织。", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Request source integration", "required": "必填", "save_changes": "保存更改", + "search_feedback": "搜索反馈", "select_a_survey_to_see_questions": "请选择一个调查以查看其问题", "select_a_value": "选择一个值...", "select_feedback_directory": "选择目录", @@ -3748,6 +3748,20 @@ "select_survey": "选择调查", "select_survey_and_questions": "选择调查和问题", "select_survey_questions_description": "选择哪些调查问题会创建反馈记录。", + "semantic_search_failed": "搜索反馈记录失败", + "semantic_search_input_label": "按主题搜索反馈记录", + "semantic_search_missing_text": "此反馈记录没有可显示的文本。", + "semantic_search_no_directories": "此工作区尚未分配反馈记录目录。添加反馈来源以开始按含义搜索反馈。", + "semantic_search_no_results": "未找到匹配的反馈记录。尝试使用更宽泛的主题或不同的短语。", + "semantic_search_placeholder": "搜索主题,例如:定价投诉", + "semantic_search_relevance": "相关度 {score}%", + "semantic_search_results_count": "{count, plural, other {# 条匹配的反馈记录}}", + "semantic_search_unavailable": "语义搜索暂不可用。配置 Hub 嵌入以使用此预览功能。", + "semantic_topics_example_confusing_onboarding": "令人困惑的引导流程", + "semantic_topics_example_pricing_complaints": "定价投诉", + "semantic_topics_example_slow_checkout": "结账慢", + "semantic_topics_preview_description": "输入主题或短语,按含义查找反馈记录。这是未来主题和子主题功能的早期预览。", + "semantic_topics_preview_title": "按主题搜索反馈", "set_value": "设置值", "setup_connection": "设置连接", "showing_count_loaded": "显示 {count} 条记录", @@ -3775,6 +3789,7 @@ "submission_id": "提交ID", "survey_has_no_questions": "该调查没有任何问题", "topics_and_subtopics": "主题和子主题", + "try_searching_for": "尝试搜索", "unify_feedback": "统一反馈", "update_mapping_description": "更新此来源的映射配置。", "updated_at": "更新于", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index fb493f13db..1cd620ff71 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -434,7 +434,6 @@ "some_files_failed_to_upload": "部分檔案上傳失敗", "something_went_wrong": "發生錯誤", "something_went_wrong_please_try_again": "發生錯誤。請再試一次。", - "soon": "即將推出", "sort_by": "排序方式", "start_free_trial": "開始免費試用", "status": "狀態", @@ -1770,7 +1769,7 @@ "no_data_available": "沒有可用資料", "no_data_returned": "查詢沒有回傳資料", "no_data_returned_for_chart": "此圖表沒有回傳資料", - "no_data_source_available": "此工作區尚未指派意見回饋目錄。", + "no_data_source_available": "此工作區未指派任何意見回饋目錄。", "no_grouping": "無(僅篩選)", "no_valid_data_to_display": "沒有可顯示的有效資料", "not_contains": "不包含", @@ -2550,7 +2549,7 @@ "assign_workspaces_description": "控制哪些工作區可以存取此意見回饋目錄。", "connectors_description": "將意見回饋記錄傳送至此目錄的連接器。", "create_feedback_directory": "建立意見回饋目錄", - "description": "管理意見回饋目錄及其工作區配置。", + "description": "管理意見回饋目錄及其工作區指派。", "directory_archived_successfully": "目錄已成功封存", "directory_created_successfully": "目錄已成功建立", "directory_id": "目錄 ID", @@ -2559,14 +2558,14 @@ "directory_settings_title": "{directoryName} 設定", "directory_unarchived_successfully": "目錄已成功取消封存", "directory_updated_successfully": "目錄已成功更新", - "empty_state": "找不到任何意見回饋目錄。建立一個開始使用吧。", + "empty_state": "找不到意見回饋目錄。建立一個以開始使用。", "error_directory_has_connectors": "無法封存已連結連接器的目錄。請先移除所有連接器。", "error_directory_name_duplicate": "已存在同名的意見回饋目錄。", "error_directory_name_required": "目錄名稱為必填項目。", "error_directory_workspaces_invalid_org": "部分指定的工作區不屬於此組織。", "error_workspace_already_assigned": "One or more workspaces are already linked to a different active directory. Reassign them first.", "nav_label": "意見回饋目錄", - "no_access": "您沒有權限管理意見回饋目錄。", + "no_access": "你沒有權限管理意見回饋目錄。", "no_connectors": "此目錄尚未連結任何連接器。", "pause_connectors_confirmation_description": "暫停這些連接器後,將不會再新增新紀錄。", "pause_connectors_confirmation_title": "暫停已連結的連接器?", @@ -3727,7 +3726,7 @@ "metadata_read_only_entries": "唯讀元資料值(非字串)", "metadata_value": "元資料值", "missing_feedback_source_title": "Missing feedback source?", - "no_feedback_directory_available": "此工作區尚未指派意見回饋目錄。請先建立或指派一個目錄。", + "no_feedback_directory_available": "此工作區未指派意見回饋目錄。請先建立或指派一個。", "no_feedback_records": "目前尚無回饋紀錄。當你的連接器開始傳送資料時,紀錄會顯示在這裡。", "no_source_fields_loaded": "尚未載入來源欄位", "no_sources_connected": "尚未連接任何來源。請新增來源以開始使用。", @@ -3739,6 +3738,7 @@ "request_feedback_source": "Request source integration", "required": "必填", "save_changes": "儲存變更", + "search_feedback": "搜尋意見回饋", "select_a_survey_to_see_questions": "請選擇問卷以查看其問題", "select_a_value": "請選擇一個值...", "select_feedback_directory": "選擇目錄", @@ -3748,6 +3748,20 @@ "select_survey": "選擇問卷", "select_survey_and_questions": "選擇問卷與問題", "select_survey_questions_description": "請選擇哪些問卷問題要建立 FeedbackRecords。", + "semantic_search_failed": "搜尋意見回饋記錄失敗", + "semantic_search_input_label": "依主題搜尋意見回饋記錄", + "semantic_search_missing_text": "此意見回饋記錄沒有可顯示的文字。", + "semantic_search_no_directories": "此工作區尚未指派意見回饋記錄目錄。新增意見回饋來源以開始依語意搜尋意見回饋。", + "semantic_search_no_results": "找不到相符的意見回饋記錄。試試更廣泛的主題或不同的描述方式。", + "semantic_search_placeholder": "搜尋主題,例如:價格投訴", + "semantic_search_relevance": "相關度 {score}%", + "semantic_search_results_count": "{count, plural, other {找到 # 筆相符的意見回饋記錄}}", + "semantic_search_unavailable": "語意搜尋尚無法使用。請設定 Hub 嵌入功能以使用此預覽功能。", + "semantic_topics_example_confusing_onboarding": "令人困惑的入門流程", + "semantic_topics_example_pricing_complaints": "價格投訴", + "semantic_topics_example_slow_checkout": "結帳速度慢", + "semantic_topics_preview_description": "輸入主題或詞彙,以語意方式找出相關的意見回饋記錄。這是未來主題與子主題功能的早期預覽版本。", + "semantic_topics_preview_title": "依主題搜尋意見回饋", "set_value": "設定值", "setup_connection": "設定連線", "showing_count_loaded": "顯示 {count} 筆記錄", @@ -3775,6 +3789,7 @@ "submission_id": "提交ID", "survey_has_no_questions": "此問卷沒有任何題目", "topics_and_subtopics": "主題與子主題", + "try_searching_for": "試試搜尋", "unify_feedback": "整合回饋", "update_mapping_description": "更新此來源的對應設定。", "updated_at": "更新時間", diff --git a/apps/web/modules/ee/unify-feedback/components/unify-config-navigation.tsx b/apps/web/modules/ee/unify-feedback/components/unify-config-navigation.tsx index e58d3df6f2..e4326964ae 100644 --- a/apps/web/modules/ee/unify-feedback/components/unify-config-navigation.tsx +++ b/apps/web/modules/ee/unify-feedback/components/unify-config-navigation.tsx @@ -31,10 +31,10 @@ export const UnifyConfigNavigation = ({ label: ( {t("workspace.unify.topics_and_subtopics")} - + ), - disabled: true, + href: `${baseHref}/topics-subtopics`, }, ]; diff --git a/apps/web/modules/envoy-auth/service.ts b/apps/web/modules/envoy-auth/service.ts index 1e5862b41e..528d774db1 100644 --- a/apps/web/modules/envoy-auth/service.ts +++ b/apps/web/modules/envoy-auth/service.ts @@ -2,10 +2,10 @@ import "server-only"; import { NextRequest } from "next/server"; import { feedbackRecordsEnvoyAuthorizer } from "@/modules/hub/feedback-records-gateway"; import { + TEnvoyRequestAuthorizer, authenticateEnvoyRequest, buildStatusResponse, parseEnvoyRequestMetadata, - TEnvoyRequestAuthorizer, } from "./shared"; const envoyAuthorizers: TEnvoyRequestAuthorizer[] = [feedbackRecordsEnvoyAuthorizer]; @@ -16,9 +16,7 @@ export const authorizeEnvoyRequest = async (request: NextRequest): Promise - candidate.matches(requestMetadata.originalRequest) - ); + const authorizer = envoyAuthorizers.find((candidate) => candidate.matches(requestMetadata.originalRequest)); if (!authorizer) { return buildStatusResponse(400, "Unsupported Envoy auth route"); } diff --git a/apps/web/modules/gateway-auth/lib/service.test.ts b/apps/web/modules/gateway-auth/lib/service.test.ts index 38bf595799..8afa00149e 100644 --- a/apps/web/modules/gateway-auth/lib/service.test.ts +++ b/apps/web/modules/gateway-auth/lib/service.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getGatewayAuthServiceTokenPurpose, ZGatewayAuthService } from "./service"; +import { ZGatewayAuthService, getGatewayAuthServiceTokenPurpose } from "./service"; describe("gateway auth service registry", () => { test("returns the configured token purpose for feedbackRecords", () => { diff --git a/apps/web/modules/hub/service.test.ts b/apps/web/modules/hub/service.test.ts index c187bed847..08a4e7ef69 100644 --- a/apps/web/modules/hub/service.test.ts +++ b/apps/web/modules/hub/service.test.ts @@ -1,5 +1,15 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; import { createCacheKey } from "@formbricks/cache"; +import FormbricksHub from "@formbricks/hub"; +import { + createFeedbackRecord, + createFeedbackRecordsBatch, + getFeedbackRecordTenant, + listFeedbackRecords, + retrieveFeedbackRecord, + semanticSearchFeedbackRecords, + updateFeedbackRecord, +} from "./service"; import type { FeedbackRecordCreateParams } from "./types"; vi.mock("@formbricks/logger", () => ({ @@ -31,14 +41,6 @@ vi.mock("@/lib/cache", () => ({ const { getHubClient } = await import("./hub-client"); const { cache } = await import("@/lib/cache"); -const { - createFeedbackRecord, - createFeedbackRecordsBatch, - getFeedbackRecordTenant, - listFeedbackRecords, - retrieveFeedbackRecord, - updateFeedbackRecord, -} = await import("./service"); const sampleInput: FeedbackRecordCreateParams = { field_id: "el-1", @@ -49,6 +51,8 @@ const sampleInput: FeedbackRecordCreateParams = { field_label: "Question?", value_number: 5, collected_at: "2026-02-24T10:00:00.000Z", + submission_id: "sub-1", + tenant_id: "tenant-1", }; describe("hub service", () => { @@ -117,7 +121,7 @@ describe("hub service", () => { feedbackRecords: { list: vi.fn().mockResolvedValue(listResponse) }, } as any); - const result = await listFeedbackRecords({ tenant_id: "env-1", limit: 50, offset: 0 }); + const result = await listFeedbackRecords({ tenant_id: "env-1", limit: 50 }); expect(result.error).toBeNull(); expect(result.data).toEqual(listResponse); @@ -133,7 +137,6 @@ describe("hub service", () => { expect(result.data).toBeNull(); expect(result.error).toMatchObject({ status: 0, message: "Network error" }); }); - }); describe("retrieveFeedbackRecord", () => { @@ -164,6 +167,89 @@ describe("hub service", () => { }); }); + describe("semanticSearchFeedbackRecords", () => { + test("returns error result when getHubClient returns null", async () => { + vi.mocked(getHubClient).mockReturnValue(null); + + const result = await semanticSearchFeedbackRecords({ + tenant_id: "env-1", + query: "slow checkout", + }); + + expect(result.data).toBeNull(); + expect(result.error).toMatchObject({ + status: 0, + message: "HUB_API_KEY is not set; Hub integration is disabled.", + }); + }); + + test("returns data when client.search.performSemanticSearch succeeds", async () => { + const searchResponse = { + data: [ + { + feedback_record_id: "018e1234-5678-9abc-def0-123456789abc", + score: 0.91, + field_label: "What can we improve?", + value_text: "Checkout feels slow.", + }, + ], + limit: 10, + }; + const performSemanticSearch = vi.fn().mockResolvedValue(searchResponse); + vi.mocked(getHubClient).mockReturnValue({ + feedbackRecords: { search: { performSemanticSearch } }, + } as any); + + const input = { + tenant_id: "env-1", + query: "slow checkout", + limit: 10, + min_score: 0.7, + }; + const result = await semanticSearchFeedbackRecords(input); + + expect(result.error).toBeNull(); + expect(result.data).toEqual(searchResponse); + expect(performSemanticSearch).toHaveBeenCalledWith(input); + }); + + test("returns error with status when client.search.performSemanticSearch throws APIError", async () => { + const apiError = new (FormbricksHub.APIError as any)("Embeddings are not configured", 503); + vi.mocked(getHubClient).mockReturnValue({ + feedbackRecords: { + search: { performSemanticSearch: vi.fn().mockRejectedValue(apiError) }, + }, + } as any); + + const result = await semanticSearchFeedbackRecords({ + tenant_id: "env-1", + query: "slow checkout", + }); + + expect(result.data).toBeNull(); + expect(result.error).toMatchObject({ + status: 503, + message: "Embeddings are not configured", + }); + }); + + test("returns error result when call throws non-API error", async () => { + vi.mocked(getHubClient).mockReturnValue({ + feedbackRecords: { + search: { performSemanticSearch: vi.fn().mockRejectedValue(new Error("Network error")) }, + }, + } as any); + + const result = await semanticSearchFeedbackRecords({ + tenant_id: "env-1", + query: "slow checkout", + }); + + expect(result.data).toBeNull(); + expect(result.error).toMatchObject({ status: 0, message: "Network error" }); + }); + }); + describe("updateFeedbackRecord", () => { test("returns error when client is null", async () => { vi.mocked(getHubClient).mockReturnValue(null); diff --git a/apps/web/modules/hub/service.ts b/apps/web/modules/hub/service.ts index a954bf1f89..1d976f9162 100644 --- a/apps/web/modules/hub/service.ts +++ b/apps/web/modules/hub/service.ts @@ -1,6 +1,6 @@ import "server-only"; -import FormbricksHub from "@formbricks/hub"; import { createCacheKey } from "@formbricks/cache"; +import FormbricksHub from "@formbricks/hub"; import { logger } from "@formbricks/logger"; import { cache } from "@/lib/cache"; import { getHubClient } from "./hub-client"; @@ -10,6 +10,8 @@ import type { FeedbackRecordListParams, FeedbackRecordListResponse, FeedbackRecordUpdateParams, + SemanticSearchInput, + SemanticSearchResponse, } from "./types"; type HubError = { status: number; message: string; detail: string }; @@ -25,9 +27,15 @@ const NO_CONFIG_ERROR = { detail: "HUB_API_KEY is not set; Hub integration is disabled.", } as const; +const getErrorMessage = (err: unknown): string => { + if (err instanceof Error) return err.message; + if (typeof err === "string") return err; + return "Unknown error"; +}; + const createResultFromError = (err: unknown): HubFeedbackRecordResult => { const status = err instanceof FormbricksHub.APIError ? err.status : 0; - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); return { data: null, error: { status, message, detail: message } }; }; @@ -95,6 +103,11 @@ export type ListFeedbackRecordsResult = { error: HubError | null; }; +export type SemanticSearchFeedbackRecordsResult = { + data: SemanticSearchResponse | null; + error: HubError | null; +}; + export type FeedbackRecordTenantResult = { data: { tenantId: string } | null; error: { status: number; message: string; detail: string } | null; @@ -116,7 +129,25 @@ export const listFeedbackRecords = async ( } catch (err) { logger.warn({ err }, "Hub: listFeedbackRecords failed"); const status = err instanceof FormbricksHub.APIError ? err.status : 0; - const message = err instanceof Error ? err.message : String(err); + const message = getErrorMessage(err); + return { data: null, error: { status, message, detail: message } }; + } +}; + +export const semanticSearchFeedbackRecords = async ( + input: SemanticSearchInput +): Promise => { + const client = getHubClient(); + if (!client) { + return { data: null, error: { ...NO_CONFIG_ERROR } }; + } + try { + const data = await client.feedbackRecords.search.performSemanticSearch(input); + return { data, error: null }; + } catch (err) { + logger.warn({ err, tenantId: input.tenant_id }, "Hub: semanticSearchFeedbackRecords failed"); + const status = err instanceof FormbricksHub.APIError ? err.status : 0; + const message = getErrorMessage(err); return { data: null, error: { status, message, detail: message } }; } }; diff --git a/apps/web/modules/hub/types.ts b/apps/web/modules/hub/types.ts index eb22e799c0..2ed2880c05 100644 --- a/apps/web/modules/hub/types.ts +++ b/apps/web/modules/hub/types.ts @@ -5,3 +5,7 @@ export type FeedbackRecordData = FormbricksHub.FeedbackRecordData; export type FeedbackRecordListParams = FormbricksHub.FeedbackRecordListParams; export type FeedbackRecordListResponse = FormbricksHub.FeedbackRecordListResponse; export type FeedbackRecordUpdateParams = FormbricksHub.FeedbackRecordUpdateParams; + +export type SemanticSearchInput = FormbricksHub.FeedbackRecords.SearchPerformSemanticSearchParams; +export type SemanticSearchResponse = FormbricksHub.FeedbackRecords.SearchPerformSemanticSearchResponse; +export type SemanticSearchResultItem = FormbricksHub.FeedbackRecords.SearchPerformSemanticSearchResponse.Data; diff --git a/charts/formbricks/README.md b/charts/formbricks/README.md index 8c60fe0b96..acd2f2b38f 100644 --- a/charts/formbricks/README.md +++ b/charts/formbricks/README.md @@ -142,9 +142,10 @@ This chart does not deploy Cube.js. XM Suite v5 dashboard and analysis features | hub.enabled | bool | `true` | | | hub.env | object | `{}` | | | hub.existingSecret | string | `""` | | +| hub.image.digest | string | `"sha256:14db7b3d285b6e9165b55693f9b83d08beff840a255fd77dd12882ee0a62f5cb"` | When set, takes precedence over tag (immutable pin). | | hub.image.pullPolicy | string | `"IfNotPresent"` | | | hub.image.repository | string | `"ghcr.io/formbricks/hub"` | | -| hub.image.tag | string | `"1.0.0"` | | +| hub.image.tag | string | `"0.2.0"` | Fallback when digest is empty. | | hub.migration.activeDeadlineSeconds | int | `900` | | | hub.migration.backoffLimit | int | `3` | | | hub.migration.ttlSecondsAfterFinished | int | `300` | | diff --git a/charts/formbricks/templates/_helpers.tpl b/charts/formbricks/templates/_helpers.tpl index cd3224ad80..1680915681 100644 --- a/charts/formbricks/templates/_helpers.tpl +++ b/charts/formbricks/templates/_helpers.tpl @@ -101,6 +101,19 @@ If `namespaceOverride` is provided, it will be used; otherwise, it defaults to ` {{- default (include "formbricks.appSecretName" .) .Values.hub.existingSecret -}} {{- end }} +{{/* +Hub image reference. Pin by digest in production (hub.image.digest = "sha256:..."); falls back to +hub.image.tag for local/dev. All Hub workloads (deployment, init container, migration job, future +hub-worker) must use this helper so they cannot drift apart. +*/}} +{{- define "formbricks.hubImage" -}} +{{- if .Values.hub.image.digest -}} +{{- printf "%s@%s" .Values.hub.image.repository .Values.hub.image.digest -}} +{{- else -}} +{{- printf "%s:%s" .Values.hub.image.repository (.Values.hub.image.tag | default "latest") -}} +{{- end -}} +{{- end }} + {{- define "formbricks.postgresAdminPassword" -}} {{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "formbricks.appSecretName" .)) }} diff --git a/charts/formbricks/templates/hub-deployment.yaml b/charts/formbricks/templates/hub-deployment.yaml index a8864cc85f..c79e0ff6a8 100644 --- a/charts/formbricks/templates/hub-deployment.yaml +++ b/charts/formbricks/templates/hub-deployment.yaml @@ -32,7 +32,7 @@ spec: {{- end }} initContainers: - name: hub-migrate - image: {{ .Values.hub.image.repository }}:{{ .Values.hub.image.tag | default "latest" }} + image: {{ include "formbricks.hubImage" . }} imagePullPolicy: {{ .Values.hub.image.pullPolicy }} securityContext: readOnlyRootFilesystem: true @@ -48,7 +48,7 @@ spec: name: {{ include "formbricks.hubSecretName" . }} containers: - name: hub - image: {{ .Values.hub.image.repository }}:{{ .Values.hub.image.tag | default "latest" }} + image: {{ include "formbricks.hubImage" . }} imagePullPolicy: {{ .Values.hub.image.pullPolicy }} securityContext: readOnlyRootFilesystem: true diff --git a/charts/formbricks/templates/hub-migration-job.yaml b/charts/formbricks/templates/hub-migration-job.yaml index 8ee202418b..3c5970ec72 100644 --- a/charts/formbricks/templates/hub-migration-job.yaml +++ b/charts/formbricks/templates/hub-migration-job.yaml @@ -37,7 +37,7 @@ spec: {{- end }} containers: - name: hub-migrate - image: {{ .Values.hub.image.repository }}:{{ .Values.hub.image.tag | default "latest" }} + image: {{ include "formbricks.hubImage" . }} imagePullPolicy: {{ .Values.hub.image.pullPolicy }} securityContext: readOnlyRootFilesystem: true diff --git a/charts/formbricks/values.yaml b/charts/formbricks/values.yaml index ddc5fb111e..9a83d7ff67 100644 --- a/charts/formbricks/values.yaml +++ b/charts/formbricks/values.yaml @@ -568,8 +568,13 @@ hub: image: repository: "ghcr.io/formbricks/hub" - # Pin to a semver tag for reproducible deployments; update on each Hub release. - tag: "1.0.0" + # Pinned by digest for immutable, reproducible deployments. When digest is set it takes + # precedence over tag, and deployment, init container, and migration job all resolve to the + # same immutable image. Update on each Hub release. + # Current digest corresponds to ghcr.io/formbricks/hub:0.2.0. + digest: "sha256:14db7b3d285b6e9165b55693f9b83d08beff840a255fd77dd12882ee0a62f5cb" + # Tag is a fallback for dev/non-prod when digest is cleared; keep aligned with the digest above. + tag: "0.2.0" pullPolicy: IfNotPresent # Optional override for the secret Hub reads from. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2930b48d49..17c25f28bb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -47,8 +47,11 @@ services: - minio-data:/data # Run Hub DB migrations (goose + river) before the API starts. Idempotent; runs on every compose up. + # Hub image pinned via HUB_IMAGE_TAG so docker does not silently reuse a stale :latest cache. + # Keep hub, hub-migrate, and any future hub-worker on the same tag — they share one image and + # drift breaks migrations or job processing. hub-migrate: - image: ghcr.io/formbricks/hub:latest + image: ghcr.io/formbricks/hub:${HUB_IMAGE_TAG:-0.2.0} restart: "no" entrypoint: ["sh", "-c"] command: @@ -63,7 +66,7 @@ services: # Formbricks Hub API (ghcr.io/formbricks/hub). Shares the same Postgres database as Formbricks by default. hub: - image: ghcr.io/formbricks/hub:latest + image: ghcr.io/formbricks/hub:${HUB_IMAGE_TAG:-0.2.0} depends_on: hub-migrate: condition: service_completed_successfully diff --git a/docker/README.md b/docker/README.md index db7ed68e92..5a4f6dfdfe 100644 --- a/docker/README.md +++ b/docker/README.md @@ -33,7 +33,7 @@ That's it! After running the command and providing the required information, vis The stack includes the [Formbricks Hub](https://github.com/formbricks/hub) API (`ghcr.io/formbricks/hub`) and a bundled Cube.js service for XM Suite v5 analytics. Hub and Cube share the same database as Formbricks by default. - **Migrations**: A `hub-migrate` service runs Hub's database migrations (goose + river) before the Hub API starts. It runs on every `docker compose up` and is idempotent. -- **Production** (`docker/docker-compose.yml`): Set `HUB_API_KEY` and `CUBEJS_API_SECRET` (required). `HUB_API_URL` defaults to `http://hub:8080` and `CUBEJS_API_URL` defaults to `http://cube:4000` so the Formbricks app can reach both services inside the compose network. Override `HUB_DATABASE_URL` and `CUBEJS_DB_*` only if Hub or Cube should use a separate database. -- **Development** (`docker-compose.dev.yml`): Hub and Cube use the same local Postgres database. `HUB_API_KEY` defaults to `dev-api-key`, `CUBEJS_API_URL` defaults to `http://localhost:4000`, and `pnpm dev:setup` generates `CUBEJS_API_SECRET` in the repo root `.env`. +- **Production** (`docker/docker-compose.yml`): Set `HUB_API_KEY` and `CUBEJS_API_SECRET` (required). `HUB_API_URL` defaults to `http://hub:8080` and `CUBEJS_API_URL` defaults to `http://cube:4000` so the Formbricks app can reach both services inside the compose network. Override `HUB_DATABASE_URL` and `CUBEJS_DB_*` only if Hub or Cube should use a separate database. The Hub image tracks `:latest` by default so `formbricks.sh update` advances Hub in lockstep with the app. `hub` and `hub-migrate` always resolve to the same image. To pin to an immutable reference, set `HUB_IMAGE_REF` in `docker/.env` to either a tag (e.g. `:0.2.0`) or a digest (e.g. `@sha256:14db7b3d…`). +- **Development** (`docker-compose.dev.yml`): Hub and Cube use the same local Postgres database. `HUB_API_KEY` defaults to `dev-api-key`, `CUBEJS_API_URL` defaults to `http://localhost:4000`, and `pnpm dev:setup` generates `CUBEJS_API_SECRET` in the repo root `.env`. The Hub image is pinned to a semver tag (`hub` and `hub-migrate` share the same value); override `HUB_IMAGE_TAG` in the repo root `.env` to test a specific Hub release. In development, Hub is exposed locally on port **8080** and Cube on **4000** (with the Cube playground on **4001**). In production Docker Compose, Hub and Cube stay internal to the compose network and are reached via `http://hub:8080` and `http://cube:4000`. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4a2a3d2980..88f9d2f4b5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -262,8 +262,11 @@ services: <<: *environment # Run Hub DB migrations (goose + river) before the API starts. Uses same image; migrations are idempotent. + # Default tracks :latest so self-host updates (compose pull) advance Hub alongside the app image. + # Operators who want an immutable pin can set HUB_IMAGE_REF in docker/.env to either ":" + # (e.g. ":0.2.0") or "@sha256:". hub and hub-migrate share the same value — no drift. hub-migrate: - image: ghcr.io/formbricks/hub:latest + image: ghcr.io/formbricks/hub${HUB_IMAGE_REF:-:latest} restart: "no" entrypoint: ["sh", "-c"] command: @@ -279,7 +282,7 @@ services: # Formbricks Hub API (ghcr.io/formbricks/hub). Set HUB_API_KEY. By default shares the Formbricks database; set HUB_DATABASE_URL to use a separate DB. hub: restart: always - image: ghcr.io/formbricks/hub:latest + image: ghcr.io/formbricks/hub${HUB_IMAGE_REF:-:latest} depends_on: hub-migrate: condition: service_completed_successfully diff --git a/docs/self-hosting/advanced/migration.mdx b/docs/self-hosting/advanced/migration.mdx index 839eaa3316..938ab2e5cf 100644 --- a/docs/self-hosting/advanced/migration.mdx +++ b/docs/self-hosting/advanced/migration.mdx @@ -6,22 +6,245 @@ icon: "arrow-right" ## v5 -**Rate Limit** +Formbricks v5 changes the self-hosted runtime contract. If you are upgrading an existing Formbricks 4.x +deployment, review this section before starting the new version. -Formbricks v5 changes how rate limiting is enforced: +### What Changes In v5 -- several public and API-key routes are no longer rate-limited inside the application server -- those routes are now expected to be protected by Envoy Gateway or an equivalent edge rate limiter -- the remaining session-based routes, server actions, and uncovered APIs still use the in-app limiter +- **Formbricks Hub is now mandatory** for self-hosted Formbricks v5 deployments. +- **Edge rate limiting is now required** for specific public and API-key routes. Those routes are no longer + throttled inside the application server. +- **AI features are configured at the instance level** via `AI_*` environment variables. +- **XM Suite v5 analytics depends on Cube.js**. The Docker and one-click stack bundle it, while Helm + deployments still need a separate reachable Cube.js instance and `CUBEJS_API_SECRET`. - If you self-host Formbricks without Envoy or another equivalent edge rate limiter, upgrade planning for v5 - must include new edge protection for the covered routes. Otherwise those routes will no longer be throttled - by the application server after the upgrade. + Formbricks v5 removes application-level rate limiting for several routes that are now expected to be + protected by Envoy Gateway or an equivalent edge rate limiter. If your self-hosted instance does not + already have equivalent edge protection, add it before exposing the v5 stack. -See the [rate-limiting guide](/self-hosting/advanced/rate-limiting) for the exact covered route groups, thresholds, -and the remaining app-enforced limits. +### Before You Upgrade + +Before you restart your instance on Formbricks v5: + +- back up your database +- identify your current deployment type: one-click, manual Docker Compose, or Kubernetes/Helm +- confirm Redis/Valkey and your file storage setup are already healthy from your v4 baseline +- identify whether file uploads use external S3-compatible storage or a legacy bundled MinIO service +- decide whether this instance needs AI features, dashboards/analysis, or only core survey flows +- verify whether you already run Envoy Gateway or another equivalent edge rate limiter for the covered routes + +### Required Config And Infrastructure Changes + +#### Formbricks Hub + +Formbricks v5 expects Hub to be part of the self-hosted stack. + +- `HUB_API_KEY` is required +- `HUB_API_URL` must point to the Hub service the Formbricks app can reach +- `HUB_DATABASE_URL` is optional; when unset, Hub can share the same PostgreSQL database as Formbricks +- bundled Docker and Helm assets run Hub database migrations automatically during startup or upgrade, and the + migration steps are idempotent + + + Hub-specific source code and standalone deployment assets live in the + [Formbricks Hub repository](https://github.com/formbricks/hub). For a normal Formbricks v5 upgrade, use that + repository as reference material only; the canonical migration steps stay in these Formbricks docs. + + +#### Edge Rate Limiting + +Formbricks v5 splits rate limiting across two layers: + +- Envoy Gateway, or an equivalent edge rate limiter, for covered public and API-key routes +- the application server for the remaining session-authenticated routes, server actions, and uncovered APIs + +Keep Redis/Valkey enabled for the remaining application-enforced limits. For the covered route groups and exact +thresholds, use the [rate-limiting guide](/self-hosting/advanced/rate-limiting) as the source of truth. + +#### AI Features + +AI features are optional and only need instance-level configuration if you want to enable the related +enterprise functionality. + +- `AI_PROVIDER` and `AI_MODEL` are the base settings +- `AI_PROVIDER=aws` requires `AI_AWS_REGION`, `AI_AWS_ACCESS_KEY_ID`, and `AI_AWS_SECRET_ACCESS_KEY` +- `AI_PROVIDER=google` requires `AI_GOOGLE_CLOUD_PROJECT`, `AI_GOOGLE_CLOUD_LOCATION`, and either + `AI_GOOGLE_CLOUD_CREDENTIALS_JSON` or `AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS` +- `AI_PROVIDER=azure` requires `AI_AZURE_API_KEY` and either `AI_AZURE_BASE_URL` or + `AI_AZURE_RESOURCE_NAME` + +#### Cube.js Analytics + +XM Suite v5 dashboard and analysis features require Cube.js. + +- the Docker and one-click stack bundle the `cube` service and expect `CUBEJS_API_SECRET` +- Helm deployments still need a separate reachable Cube.js instance +- the Formbricks app expects `CUBEJS_API_URL` and `CUBEJS_API_SECRET` +- if you run Cube yourself, you may also need to override `CUBEJS_DB_*` values for the Cube service + +### Upgrade Steps By Deployment Type + +**1. Back up your database** + + + + From the `formbricks` directory created by the installer: + + ```bash + cd formbricks + docker compose exec postgres pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v5_$(date +%Y%m%d_%H%M%S).dump + ``` + + + If your PostgreSQL service name differs, run docker compose ps first and adjust the command. + + + + From the directory that contains your `docker-compose.yml`: + + ```bash + docker compose exec postgres pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v5_$(date +%Y%m%d_%H%M%S).dump + ``` + + + If your service is not named postgres, use docker compose ps to find the correct + name. + + + + If you are using the in-cluster PostgreSQL released by the Helm chart: + + ```bash + kubectl exec -n formbricks formbricks-postgresql-0 -- pg_dump -Fc -U formbricks -d formbricks > formbricks_pre_v5_$(date +%Y%m%d_%H%M%S).dump + ``` + + If you use a managed PostgreSQL service, create a provider snapshot or run `pg_dump` directly against the + external host before continuing. + + + +**2. Update your deployment config and restart on v5** + + + + The one-click `./formbricks.sh update` command only pulls new images. It does **not** rewrite your existing + `formbricks/docker-compose.yml`, so you must merge the v5 stack changes before the first v5 restart. + + ```bash + curl -fsSL -o formbricks/docker-compose.v5.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/docker-compose.yml + ``` + + Then compare `formbricks/docker-compose.v5.yml` with your existing `formbricks/docker-compose.yml` and merge + the v5 additions: + + - add a non-empty `HUB_API_KEY` and reuse the same value wherever your deployment resolves Hub auth + - keep `HUB_API_URL` at `http://hub:8080` unless Hub runs elsewhere + - include the bundled `hub-migrate` and `hub` services + - if you use the bundled XM Suite v5 analytics stack, sync `formbricks/cube/cube.js` and + `formbricks/cube/schema/FeedbackRecords.js` from the current release and ensure + `formbricks/.env` contains `CUBEJS_API_SECRET` + - if your older setup still uses bundled MinIO for uploads, review that storage path separately before the + first v5 restart; newer self-hosting updates move the bundled object-storage path to RustFS, while + external S3-compatible storage keeps the same `S3_*` app contract + - add any `AI_*` variables you need + - if you do not run the bundled Docker analytics path, point `CUBEJS_API_URL` at your external Cube.js + instance and provide the matching `CUBEJS_API_SECRET` + + After the compose file is updated and your edge rate limiter is in place: + + ```bash + ./formbricks.sh update + ``` + + + Pull the current production compose file and merge the v5 changes into your existing deployment config before + only updating images: + + ```bash + curl -fsSL -o docker-compose.v5.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/docker-compose.yml + ``` + + At minimum, confirm: + + - `HUB_API_KEY` is configured and the same value is available wherever your deployment resolves Hub auth + - `HUB_API_URL` points to the Hub service the app can reach + - the compose stack includes `hub-migrate` and `hub` + - the XM Suite v5 Docker stack also includes `cube`, `cube/cube.js`, and + `cube/schema/FeedbackRecords.js`, with `CUBEJS_API_SECRET` available through your `.env` or shell + environment + - if your legacy Compose file still includes bundled MinIO for uploads, treat that as a separate storage + review when comparing files; newer bundled storage guidance uses RustFS, while external S3-compatible + storage keeps the same `S3_*` app contract + - any `AI_*` variables you need are set + - if you override the bundled analytics path, point `CUBEJS_API_URL` at your external Cube.js instance and + supply the matching `CUBEJS_API_SECRET` + + Then restart the stack: + + ```bash + docker compose pull + docker compose down + docker compose up -d + ``` + + + The XM Suite v5 Docker Compose stack bundles Hub and Cube.js. Keep the bundled `cube/` config files in + sync with `docker-compose.yml` when you update this path. + + + + Upgrade using the current Formbricks chart: + + ```bash + helm upgrade formbricks oci://ghcr.io/formbricks/helm-charts/formbricks \ + -n formbricks \ + -f values.yaml + ``` + + Before running the upgrade, confirm these v5 expectations in your values: + + - keep `hub.enabled=true`; Hub is mandatory in v5 + - if you want the bundled Envoy path, configure `envoy.enabled=true` and then choose: + - `envoy.controller.enabled=true` for the bundled controller mode + - `envoy.controller.enabled=false` when the cluster already has a compatible Envoy Gateway controller + - if you use bundled Envoy rate limiting, enable a dedicated backend with `envoyRedis.enabled=true` + - if you already have an equivalent edge rate limiter outside the chart, keep that protection in place + - if the instance needs XM Suite v5 analytics or dashboards, provide `CUBEJS_API_URL` and + `CUBEJS_API_SECRET` for the external Cube.js deployment + + + +### Post-Upgrade Verification + +After the upgrade: + +- confirm the Formbricks app starts and `GET /health` returns successfully +- confirm the Hub service is healthy and reachable from the Formbricks app +- verify any Hub-backed connector or feedback flows you use +- verify covered routes are rate-limited at the edge layer +- verify AI features only if you configured the required `AI_*` variables +- verify dashboards and analysis flows only if your deployment path includes Cube.js or points to an external + Cube.js instance + +### Troubleshooting And Rollback + +Common upgrade issues: + +- **Missing `HUB_API_KEY`**: Formbricks cannot authenticate to Hub, and Hub itself may fail to start +- **Bad `HUB_API_URL`**: the app starts, but Hub-backed features fail because the service cannot be reached +- **No edge rate limiter in front of covered routes**: the v5 deployment runs, but those routes are no longer + protected by the legacy in-app limiter +- **Missing AI credentials**: AI features remain unavailable until `AI_PROVIDER`, `AI_MODEL`, and the matching + provider credentials are set correctly +- **Cube not configured**: dashboards or analysis queries fail even though the core Formbricks app is healthy + +If you need to roll back: + +- restore the pre-upgrade database backup if schema or data changes require it +- revert your image tags or Helm release values to the last known-good Formbricks 4.x version +- revert compose or Helm configuration changes introduced for the v5 rollout **Workspaces and Environment IDs** @@ -1372,7 +1595,8 @@ For a seamless migration, below is a shell script for your self-hosted instance ### Docker & Single Script Setup Now that these variables can be defined at runtime, you can append them inside your `x-environment` in the `docker-compose.yml` itself. -For a more detailed guide on these environment variables, please refer to the [Important Runtime Variables](/self-hosting/setup/docker#important-run-time-variables) section. +For a more detailed guide on these environment variables, please refer to the +[Environment Variables](/self-hosting/configuration/environment-variables) page. ```yaml docker-compose.yml version: "3.3" diff --git a/docs/self-hosting/advanced/rate-limiting.mdx b/docs/self-hosting/advanced/rate-limiting.mdx index aa3a4819cc..9d0a653557 100644 --- a/docs/self-hosting/advanced/rate-limiting.mdx +++ b/docs/self-hosting/advanced/rate-limiting.mdx @@ -18,6 +18,11 @@ Starting with Formbricks v5, rate limiting is split across two layers: those routes will no longer be throttled by the application server after upgrading. + + For the full self-hosted upgrade checklist, use this page together with the + [v5 migration guide](/self-hosting/advanced/migration#v5). + + Rate limits are scoped by identifier, depending on the endpoint and enforcement layer: - IP hash (for unauthenticated/client-side routes and public actions) @@ -36,7 +41,7 @@ Before upgrading to Formbricks v5: - expect covered routes to emit gateway `429`s instead of the legacy app JSON `429`s For the current source of truth on covered routes and thresholds, use this page together with your deployment -configuration. +configuration and the [v5 migration guide](/self-hosting/advanced/migration#v5). ## Envoy-Managed Limits diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 5d4087c99c..1df0c5f5d6 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -8,102 +8,108 @@ icon: "code" These variables are present inside your machine's docker-compose file. Restart the docker containers if you change any variables for them to take effect. + + Upgrading from Formbricks 4.x to 5.0? Read the [migration guide](/self-hosting/advanced/migration#v5) first. + Formbricks v5 makes Hub part of the standard self-hosted runtime and changes how rate limiting is enforced. + + For `AI_PROVIDER=google`, use a Gemini model ID such as `gemini-2.5-flash` together with Google Cloud credentials. Formbricks uses Google Cloud naming here, even though the underlying SDK still talks to Vertex AI endpoints for Gemini model access. -| Variable | Description | Required | Default | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| WEBAPP_URL | Base URL of the site. | required | http://localhost:3000 | -| PUBLIC_URL | Base URL for the public domain where surveys and public-facing content are served. If not set, uses WEBAPP_URL. | optional | WEBAPP_URL | -| NEXTAUTH_URL | Location of the auth server. This should normally be the same as WEBAPP_URL | required | http://localhost:3000 | -| DATABASE_URL | Database URL with credentials. | required | | -| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | -| ENCRYPTION_KEY | Secret used by Formbricks for data encryption and audit log hashing. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | -| CRON_SECRET | API Secret for running cron jobs. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | -| LOG_LEVEL | Minimum log level (debug, info, warn, error, fatal) | optional | info | -| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) | -| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) | -| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) | -| S3_BUCKET_NAME | S3 bucket name for data storage. Formbricks enables S3 storage when this is set. | optional (required if S3 is enabled) | | -| S3_ENDPOINT_URL | Endpoint for S3. | optional | (resolved by the AWS SDK) | -| SAML_DATABASE_URL | Database URL for SAML. | optional | postgres://postgres:@localhost:5432/formbricks-saml | -| PRIVACY_URL | URL for privacy policy. | optional | | -| TERMS_URL | URL for terms of service. | optional | | -| IMPRINT_URL | URL for imprint. | optional | | -| IMPRINT_ADDRESS | Address for imprint. | optional | | -| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to 1. | optional | | -| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to 1. | optional | | -| PASSWORD_RESET_TOKEN_LIFETIME_MINUTES | Configures how long password reset links remain valid in minutes. Accepted values are integers from 5 to 120. | optional | 30 | -| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to 1. | optional | | -| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | | -| TELEMETRY_DISABLED | Disables telemetry reporting if set to 1. Ignored when an Enterprise License is active. | optional | | -| DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS | Allows webhook URLs to point to internal/private network addresses (e.g. localhost, 192.168.x.x) if set to 1. Useful for self-hosted instances that need to send webhooks to internal services. | optional | | -| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | | -| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | -| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | | -| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | -| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | -| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | -| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | | -| SMTP_AUTHENTICATED | If set to 0, the server will not require SMTP_USER and SMTP_PASSWORD(default is 1) | optional | | -| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to 1 else to 0. | optional (required if email services are to be enabled) | | -| SMTP_REJECT_UNAUTHORIZED_TLS | If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs. | optional | 1 | -| TURNSTILE_SITE_KEY | Site key for Turnstile. | optional | | -| TURNSTILE_SECRET_KEY | Secret key for Turnstile. | optional | | -| RECAPTCHA_SITE_KEY | Site key for survey responses recaptcha bot protection | optional | | -| RECAPTCHA_SECRET_KEY | Secret key for recaptcha bot protection. | optional | | -| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | | -| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | | -| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | | -| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | | -| AI_PROVIDER | Instance-level AI provider used in the background. Supported values: `aws`, `google`, `azure`. | optional (required if AI is enabled) | | -| AI_MODEL | Instance-level AI model or deployment name used by the active provider. | optional (required if `AI_PROVIDER` is set) | | -| AI_GOOGLE_CLOUD_PROJECT | Google Cloud project ID for the `google` AI provider. | optional (required if `AI_PROVIDER=google`) | | -| AI_GOOGLE_CLOUD_LOCATION | Google Cloud location for `google` AI requests. | optional (required if `AI_PROVIDER=google`) | | -| AI_GOOGLE_CLOUD_CREDENTIALS_JSON | Service account credentials JSON for the `google` AI provider. | optional (one of this or `AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS` required if `AI_PROVIDER=google`) | | -| AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS | Path to Google Application Default Credentials used by the `google` AI provider. | optional (one of this or `AI_GOOGLE_CLOUD_CREDENTIALS_JSON` required if `AI_PROVIDER=google`) | | -| AI_AWS_REGION | AWS region for Amazon Bedrock. | optional (required if `AI_PROVIDER=aws`) | | -| AI_AWS_ACCESS_KEY_ID | AWS access key ID for Amazon Bedrock. | optional (required if `AI_PROVIDER=aws`) | | -| AI_AWS_SECRET_ACCESS_KEY | AWS secret access key for Amazon Bedrock. | optional (required if `AI_PROVIDER=aws`) | | -| AI_AWS_SESSION_TOKEN | AWS session token for Amazon Bedrock temporary credentials. | optional | | -| AI_AZURE_BASE_URL | Azure OpenAI / Foundry base URL. When set, this is preferred over `AI_AZURE_RESOURCE_NAME`. | optional | | -| AI_AZURE_RESOURCE_NAME | Azure resource name used to assemble the Azure OpenAI URL. | optional | | -| AI_AZURE_API_KEY | API key for Azure OpenAI / Foundry. | optional (required if `AI_PROVIDER=azure`) | | -| AI_AZURE_API_VERSION | Azure API version for OpenAI-compatible calls. | optional | v1 | -| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | -| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | -| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b | -| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | | -| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | | -| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | | -| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | | -| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | | -| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 | -| OTEL_EXPORTER_OTLP_ENDPOINT | Base OTLP HTTP endpoint for traces and metrics export (e.g. http://collector:4318). | optional | | -| OTEL_EXPORTER_OTLP_PROTOCOL | OTLP protocol to use for export. | optional | http/protobuf | -| OTEL_SERVICE_NAME | Service name reported in OpenTelemetry resource attributes. | optional | formbricks | -| OTEL_RESOURCE_ATTRIBUTES | Comma-separated resource attributes in OTel format (`key=value,key2=value2`). | optional | | -| OTEL_TRACES_SAMPLER | Trace sampler strategy (`always_on`, `always_off`, `traceidratio`, `parentbased_traceidratio`). | optional | always_on | -| OTEL_TRACES_SAMPLER_ARG | Sampling argument used by ratio-based samplers (`0` to `1`). | optional | | -| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | | -| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 | -| DEFAULT_TEAM_ID | Default team ID for new users. | optional | | -| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional | | -| SENTRY_ENVIRONMENT | Set this to identify the environment in Sentry | optional | | -| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional | | -| SESSION_MAX_AGE | Configure the maximum age for the session in seconds. | optional | 86400 (24 hours) | -| USER_MANAGEMENT_MINIMUM_ROLE | Set this to control which roles can access user management features. Accepted values: "owner", "manager", "disabled" | optional | manager | -| REDIS_URL | Redis URL for caching, rate limiting, and audit logging. Application will not start without this. | required | redis://localhost:6379 | -| AUDIT_LOG_ENABLED | Set this to 1 to enable audit logging. Requires Redis to be configured with the REDIS_URL env variable. | optional | 0 | -| AUDIT_LOG_GET_USER_IP | Set to 1 to include user IP addresses in audit logs from request headers | optional | 0 | +| Variable | Description | Required | Default | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| WEBAPP_URL | Base URL of the site. | required | http://localhost:3000 | +| PUBLIC_URL | Base URL for the public domain where surveys and public-facing content are served. If not set, uses WEBAPP_URL. | optional | WEBAPP_URL | +| NEXTAUTH_URL | Location of the auth server. This should normally be the same as WEBAPP_URL | required | http://localhost:3000 | +| DATABASE_URL | Database URL with credentials. | required | | +| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | +| ENCRYPTION_KEY | Secret used by Formbricks for data encryption and audit log hashing. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | +| CRON_SECRET | API Secret for running cron jobs. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) | +| LOG_LEVEL | Minimum log level (debug, info, warn, error, fatal) | optional | info | +| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) | +| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) | +| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) | +| S3_BUCKET_NAME | S3 bucket name for data storage. Formbricks enables S3 storage when this is set. | optional (required if S3 is enabled) | | +| S3_ENDPOINT_URL | Endpoint for S3. | optional | (resolved by the AWS SDK) | +| SAML_DATABASE_URL | Database URL for SAML. | optional | postgres://postgres:@localhost:5432/formbricks-saml | +| PRIVACY_URL | URL for privacy policy. | optional | | +| TERMS_URL | URL for terms of service. | optional | | +| IMPRINT_URL | URL for imprint. | optional | | +| IMPRINT_ADDRESS | Address for imprint. | optional | | +| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to 1. | optional | | +| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to 1. | optional | | +| PASSWORD_RESET_TOKEN_LIFETIME_MINUTES | Configures how long password reset links remain valid in minutes. Accepted values are integers from 5 to 120. | optional | 30 | +| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to 1. | optional | | +| RATE_LIMITING_DISABLED | Disables only the application-level rate limiter if set to 1. It does not disable Envoy or an equivalent edge rate limiter. | optional | | +| TELEMETRY_DISABLED | Disables telemetry reporting if set to 1. Ignored when an Enterprise License is active. | optional | | +| DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS | Allows webhook URLs to point to internal/private network addresses (e.g. localhost, 192.168.x.x) if set to 1. Useful for self-hosted instances that need to send webhooks to internal services. | optional | | +| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | | +| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | +| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | | +| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | +| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | +| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | +| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | | +| SMTP_AUTHENTICATED | If set to 0, the server will not require SMTP_USER and SMTP_PASSWORD(default is 1) | optional | | +| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to 1 else to 0. | optional (required if email services are to be enabled) | | +| SMTP_REJECT_UNAUTHORIZED_TLS | If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs. | optional | 1 | +| TURNSTILE_SITE_KEY | Site key for Turnstile. | optional | | +| TURNSTILE_SECRET_KEY | Secret key for Turnstile. | optional | | +| RECAPTCHA_SITE_KEY | Site key for survey responses recaptcha bot protection | optional | | +| RECAPTCHA_SECRET_KEY | Secret key for recaptcha bot protection. | optional | | +| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | | +| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | | +| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | | +| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | | +| AI_PROVIDER | Instance-level AI provider used in the background. Supported values: `aws`, `google`, `azure`. | optional (required if AI is enabled) | | +| AI_MODEL | Instance-level AI model or deployment name used by the active provider. | optional (required if `AI_PROVIDER` is set) | | +| AI_GOOGLE_CLOUD_PROJECT | Google Cloud project ID for the `google` AI provider. | optional (required if `AI_PROVIDER=google`) | | +| AI_GOOGLE_CLOUD_LOCATION | Google Cloud location for `google` AI requests. | optional (required if `AI_PROVIDER=google`) | | +| AI_GOOGLE_CLOUD_CREDENTIALS_JSON | Service account credentials JSON for the `google` AI provider. | optional (one of this or `AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS` required if `AI_PROVIDER=google`) | | +| AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS | Path to Google Application Default Credentials used by the `google` AI provider. | optional (one of this or `AI_GOOGLE_CLOUD_CREDENTIALS_JSON` required if `AI_PROVIDER=google`) | | +| AI_AWS_REGION | AWS region for Amazon Bedrock. | optional (required if `AI_PROVIDER=aws`) | | +| AI_AWS_ACCESS_KEY_ID | AWS access key ID for Amazon Bedrock. | optional (required if `AI_PROVIDER=aws`) | | +| AI_AWS_SECRET_ACCESS_KEY | AWS secret access key for Amazon Bedrock. | optional (required if `AI_PROVIDER=aws`) | | +| AI_AWS_SESSION_TOKEN | AWS session token for Amazon Bedrock temporary credentials. | optional | | +| AI_AZURE_BASE_URL | Azure OpenAI / Foundry base URL. When set, this is preferred over `AI_AZURE_RESOURCE_NAME`. | optional (one of this or `AI_AZURE_RESOURCE_NAME` required if `AI_PROVIDER=azure`) | | +| AI_AZURE_RESOURCE_NAME | Azure resource name used to assemble the Azure OpenAI URL. | optional (one of this or `AI_AZURE_BASE_URL` required if `AI_PROVIDER=azure`) | | +| AI_AZURE_API_KEY | API key for Azure OpenAI / Foundry. | optional (required if `AI_PROVIDER=azure`) | | +| AI_AZURE_API_VERSION | Azure API version for OpenAI-compatible calls. | optional | v1 | +| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | +| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | +| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b | +| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | | +| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | | +| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | | +| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | | +| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | | +| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 | +| OTEL_EXPORTER_OTLP_ENDPOINT | Base OTLP HTTP endpoint for traces and metrics export (e.g. http://collector:4318). | optional | | +| OTEL_EXPORTER_OTLP_PROTOCOL | OTLP protocol to use for export. | optional | http/protobuf | +| OTEL_SERVICE_NAME | Service name reported in OpenTelemetry resource attributes. | optional | formbricks | +| OTEL_RESOURCE_ATTRIBUTES | Comma-separated resource attributes in OTel format (`key=value,key2=value2`). | optional | | +| OTEL_TRACES_SAMPLER | Trace sampler strategy (`always_on`, `always_off`, `traceidratio`, `parentbased_traceidratio`). | optional | always_on | +| OTEL_TRACES_SAMPLER_ARG | Sampling argument used by ratio-based samplers (`0` to `1`). | optional | | +| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | | +| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 | +| DEFAULT_TEAM_ID | Default team ID for new users. | optional | | +| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional | | +| SENTRY_ENVIRONMENT | Set this to identify the environment in Sentry | optional | | +| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional | | +| SESSION_MAX_AGE | Configure the maximum age for the session in seconds. | optional | 86400 (24 hours) | +| USER_MANAGEMENT_MINIMUM_ROLE | Set this to control which roles can access user management features. Accepted values: "owner", "manager", "disabled" | optional | manager | +| REDIS_URL | Redis URL for caching, rate limiting, and audit logging. Application will not start without this. | required | redis://localhost:6379 | +| AUDIT_LOG_ENABLED | Set this to 1 to enable audit logging. Requires Redis to be configured with the REDIS_URL env variable. | optional | 0 | +| AUDIT_LOG_GET_USER_IP | Set to 1 to include user IP addresses in audit logs from request headers | optional | 0 | #### Formbricks Hub -When running the stack with [Formbricks Hub](https://github.com/formbricks/hub) (for example via Docker Compose or Helm), the following variables apply: +Starting with Formbricks v5, Hub is part of the standard self-hosted runtime. When you run Formbricks with the +bundled Docker Compose or Helm assets, the following variables apply: | Variable | Description | Required | Default | | ---------------- | ---------------------------------------------------------------------------------- | -------- | --------------------------------------------------- | -| HUB_API_KEY | API key used by the Formbricks Hub API (port 8080). | required | (e.g. `openssl rand -hex 32`) | -| HUB_API_URL | Base URL the Formbricks app uses to call Hub. Use `http://localhost:8080` locally. | required | `http://localhost:8080` in local dev | +| HUB_API_KEY | API key used by the Formbricks Hub API. Generate a strong secret and use the same value wherever your deployment supplies Hub auth configuration. | required | (e.g. `openssl rand -hex 32`) | +| HUB_API_URL | Base URL the Formbricks app uses to call Hub. With the bundled Docker stack, keep this at `http://hub:8080` unless Hub runs elsewhere. | required | `http://hub:8080` (bundled Docker), `http://localhost:8080` (local dev) | | HUB_DATABASE_URL | PostgreSQL connection URL for Hub. Omit to use the same database as Formbricks. | optional | Same as Formbricks `DATABASE_URL` (shared database) | #### Cube.js Analytics for XM Suite v5 @@ -113,8 +119,8 @@ Cube JWT from `CUBEJS_API_SECRET`, so `CUBEJS_API_TOKEN` is not part of the supp | Variable | Description | Required | Default | | ----------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------ | -| CUBEJS_API_URL | Base URL the Formbricks app uses to call Cube. Use `http://localhost:4000` locally. | required for XM Suite v5 analytics | `http://localhost:4000` in local dev | -| CUBEJS_API_SECRET | Shared secret Formbricks uses to sign Cube API JWTs. Generate with `openssl rand -hex 32`. | required for XM Suite v5 analytics | | +| CUBEJS_API_URL | Base URL the Formbricks app uses to call Cube. Use `http://localhost:4000` locally. | required for XM Suite v5 analytics | `http://localhost:4000` in local dev | +| CUBEJS_API_SECRET | Shared secret Formbricks uses to sign Cube API JWTs. Generate with `openssl rand -hex 32`. | required for XM Suite v5 analytics | | | CUBEJS_DB_HOST | Database host for the Cube service. Only needed when you run Cube yourself and override defaults. | optional | Depends on your Cube deployment | | CUBEJS_DB_PORT | Database port for the Cube service. Only needed when you run Cube yourself and override defaults. | optional | Depends on your Cube deployment | | CUBEJS_DB_NAME | Database name for the Cube service. Only needed when you run Cube yourself and override defaults. | optional | Depends on your Cube deployment | diff --git a/docs/self-hosting/setup/cluster-setup.mdx b/docs/self-hosting/setup/cluster-setup.mdx index 79af4dd897..80f439435a 100644 --- a/docs/self-hosting/setup/cluster-setup.mdx +++ b/docs/self-hosting/setup/cluster-setup.mdx @@ -1,197 +1,162 @@ --- title: "Cluster Setup" -description: "How to set up Formbricks in a High-Availability Cluster" +description: "Run Formbricks in a high-availability cluster." icon: "circle-nodes" --- ## Overview -Running Formbricks as a cluster of multiple instances offers several key advantages: +Running Formbricks as a cluster of multiple instances gives you: -- **High Availability**: Ensure your surveys remain accessible even if some instances fail - -- **Load Distribution**: Handle higher traffic by distributing requests across multiple instances - -- **Scalability**: Easily scale horizontally by adding more instances as your needs grow - -- **Zero-Downtime Updates**: Rolling updates without service interruption +- **High Availability**: surveys remain accessible even if one app pod becomes unavailable +- **Load Distribution**: traffic can be spread across multiple stateless Formbricks app instances +- **Scalability**: you can scale app replicas horizontally as usage grows +- **Zero-Downtime Updates**: rolling deployments are possible with the right orchestration setup ## Requirements -To run Formbricks in a cluster setup, you'll need: +For a Formbricks v5 cluster setup, plan for: -- Shared PostgreSQL database - -- Shared Redis cache for session management and caching - -- Load balancer to distribute traffic +- shared PostgreSQL for Formbricks and Hub, with `pgvector` support if Hub shares the same database +- shared Redis/Valkey for caching, rate limiting, and audit-related flows +- shared S3-compatible storage if you use file uploads or organization branding assets +- a load balancer or ingress layer in front of the app +- Formbricks Hub as part of the runtime +- Envoy Gateway or an equivalent external edge rate limiter for the v5-covered public and API-key routes ## Architecture -The Formbricks cluster setup consists of multiple components working together to provide a scalable and highly available system. Here's a detailed overview of the architecture: - ```mermaid graph TD - subgraph Load Balancer - LB[Load Balancer/Ingress] + subgraph Edge + LB["Load Balancer / Ingress"] + RL["Envoy Or Equivalent Edge Rate Limiter"] end subgraph Formbricks Cluster - FB1[Formbricks Instance 1] - FB2[Formbricks Instance 2] - FB3[Formbricks Instance n] + FB1["Formbricks App 1"] + FB2["Formbricks App 2"] + FB3["Formbricks App n"] + HUB["Formbricks Hub"] end subgraph Data Storage - subgraph PostgreSQL HA - PSQL_P[(PostgreSQL Primary)] - PSQL_R[(PostgreSQL Replica)] - end - - subgraph Redis Cluster - RC_P[(Redis Primary)] - RC_R[(Redis Replica)] - end - - S3[S3 Compatible Storage] + PSQL["PostgreSQL"] + REDIS["Redis / Valkey"] + S3["S3 Compatible Storage"] end - %% Connections - LB --> FB1 - LB --> FB2 - LB --> FB3 + LB --> RL + RL --> FB1 + RL --> FB2 + RL --> FB3 - FB1 --> PSQL_P - FB2 --> PSQL_P - FB3 --> PSQL_P - PSQL_P --> PSQL_R + FB1 --> PSQL + FB2 --> PSQL + FB3 --> PSQL + HUB --> PSQL - FB1 --> RC_P - FB2 --> RC_P - FB3 --> RC_P - RC_P --> RC_R + FB1 --> REDIS + FB2 --> REDIS + FB3 --> REDIS + + FB1 --> HUB + FB2 --> HUB + FB3 --> HUB FB1 --> S3 FB2 --> S3 FB3 --> S3 - - style PSQL_P fill:#00C4B8,color:#ffffff - style PSQL_R fill:#00C4B8,color:#ffffff - style RC_P fill:#FF6B6B,color:#ffffff - style RC_R fill:#FF6B6B,color:#ffffff - style S3 fill:#FFA94D,color:#ffffff - style FB1,FB2,FB3 fill:#0D9373,color:#ffffff - style LB fill:#4C6EF5,color:#ffffff ``` ### Component Description -1. **Formbricks Cluster** +1. **Formbricks App Replicas** - - Multiple Formbricks instances (1..n) running in parallel - - Each instance is stateless and can handle any incoming request - - Automatic failover if any instance becomes unavailable + - stateless application instances that serve the UI, APIs, and survey flows + - can be scaled horizontally behind a load balancer -2. **PostgreSQL Database** +2. **Formbricks Hub** - - Primary database storing all survey, response, and contact data - - Optional high-availability setup with primary-replica configuration - - Handles all persistent data storage needs + - required in Formbricks v5 + - stores and serves Hub-backed feedback record data + - can share the same PostgreSQL database as the main app when configured that way -3. **Redis Cluster** +3. **PostgreSQL** - - Acts as a distributed cache layer - - Improves performance by caching frequently accessed data - - Can be configured in HA mode with primary-replica setup - - Handles session management and real-time features + - primary persistent store for the Formbricks app and, by default, Hub + - should be backed up and monitored like any other stateful production dependency -4. **S3 Compatible Storage** +4. **Redis / Valkey** - - Stores file uploads and attachments - - Can be any S3-compatible storage service (AWS S3, MinIO, etc.) - - Provides reliable and scalable file storage + - required for caching, remaining application-enforced rate limits, and audit-related flows + - should be shared across all app replicas -5. **Load Balancer** - - Distributes incoming traffic across all Formbricks instances - - Performs health checks and removes unhealthy instances - - Ensures even load distribution and high availability +5. **S3-Compatible Storage** + + - used for file uploads and media-related features + - should be shared across all replicas + +6. **Edge Layer** + + - terminates or routes incoming traffic + - enforces rate limiting for the route groups that moved out of the application server in v5 ## Redis Configuration - Redis is required for Formbricks to function. The application will not start without a Redis URL configured. + Redis/Valkey is required for Formbricks to function. The application will not start without `REDIS_URL`. -Configure Redis by adding the following **required** environment variable to your instances: - ```sh env REDIS_URL=redis://your-redis-host:6379 ``` ## S3 Configuration -Configure S3 storage by adding the following environment variables to your instances: - ```sh env -# Required for file uploads in serverless environments S3_ACCESS_KEY=your-access-key S3_SECRET_KEY=your-secret-key S3_REGION=your-region S3_BUCKET_NAME=your-bucket-name - -# For S3-compatible storage (e.g., StorJ, MinIO) -# Leave empty for Amazon S3 S3_ENDPOINT_URL=https://your-s3-compatible-endpoint - -# Enable for S3-compatible storage that requires path style -# 0 for disabled, 1 for enabled S3_FORCE_PATH_STYLE=0 ``` -When using S3 in a cluster setup, ensure that: +When using S3 in a cluster setup, ensure: -- All Formbricks instances have access to the same S3 bucket -- The bucket has appropriate CORS settings configured -- IAM roles/users have sufficient permissions for read/write operations +- all replicas use the same bucket +- the bucket has the required CORS settings +- the credentials have read/write access for the assets you expect Formbricks to manage + +## v5 Cluster Notes + +### Hub Is Mandatory + +Formbricks v5 self-hosting requires Hub. Do not plan a cluster upgrade that keeps Hub disabled. + +### Edge Rate Limiting Must Exist Somewhere + +You do not have to use the Formbricks Helm chart's Envoy bundle, but you do need equivalent edge protection for +the v5-covered public and API-key routes. Use the +[rate-limiting guide](/self-hosting/advanced/rate-limiting) for the exact route coverage. + +### Cube Is Optional + +Cube is only needed for analytics dashboards or other analysis flows that depend on Cube queries. It is not part +of the baseline Formbricks v5 cluster runtime. ## Kubernetes Setup -Formbricks provides an official Helm chart for deploying the entire cluster stack on Kubernetes. The Helm chart is available in the [Formbricks GitHub repository](https://github.com/formbricks/formbricks/tree/main/helm-chart). - -### Features of the Helm Chart - -The Helm chart provides a complete deployment solution that includes: - -- Formbricks application with configurable replicas -- PostgreSQL database (with optional HA configuration) -- Redis cluster for caching -- Optional Traefik ingress controller for routing and SSL termination -- Automatic configuration of dependencies and networking - -### Installation Steps - -1. Add the Formbricks Helm repository: +The current Kubernetes deployment path uses the OCI chart published from +[`charts/formbricks`](https://github.com/formbricks/formbricks/tree/main/charts/formbricks): ```sh -helm repo add formbricks https://raw.githubusercontent.com/formbricks/formbricks/main/helm-chart -helm repo update +helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks \ + -n formbricks \ + --create-namespace \ + -f values.yaml ``` -2. Install the chart: - -```sh -helm install formbricks formbricks/formbricks -``` - -### Configuration Options - -The Helm chart can be customized using a `values.yaml` file to configure: - -- Number of Formbricks replicas -- Resource limits and requests -- Database configuration -- Redis settings -- Ingress rules and TLS -- Environment variables and secrets - -Refer to the [Helm chart documentation](https://github.com/formbricks/formbricks/tree/main/helm-chart) for detailed configuration options and examples. +For the Kubernetes-specific installation flow, mandatory Hub behavior, and Envoy bundle modes, use the dedicated +[Kubernetes deployment guide](/self-hosting/setup/kubernetes). diff --git a/docs/self-hosting/setup/docker.mdx b/docs/self-hosting/setup/docker.mdx index ae3e72e031..99ab5bb80e 100644 --- a/docs/self-hosting/setup/docker.mdx +++ b/docs/self-hosting/setup/docker.mdx @@ -15,6 +15,13 @@ Make sure Docker and Docker Compose are installed on your system. These are usua Docker documentation. + + Starting with Formbricks v5, the production Docker Compose stack includes Formbricks Hub and the XM Suite v5 + Cube.js services. Generate `HUB_API_KEY` and `CUBEJS_API_SECRET` during setup, keep `HUB_API_URL` at its + internal default unless Hub runs elsewhere, and use the [migration guide](/self-hosting/advanced/migration#v5) + when upgrading an existing 4.x instance. + + ## Start 1. **Create a New Directory for Formbricks** @@ -95,6 +102,29 @@ Make sure Docker and Docker Compose are installed on your system. These are usua sed -i '' "s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml ``` +1. **Generate Hub API Key** + + Formbricks v5 requires a Hub API key for the bundled Hub service. + + For Linux: + + ```bash + sed -i "/HUB_API_KEY:$/s/HUB_API_KEY:.*/HUB_API_KEY: $(openssl rand -hex 32)/" docker-compose.yml + ``` + + For macOS: + + ```bash + sed -i '' "s/HUB_API_KEY:.*/HUB_API_KEY: $(openssl rand -hex 32)/" docker-compose.yml + ``` + + + The bundled production stack already sets HUB_API_URL to http://hub:8080. Only + change that value if your Formbricks app needs to reach Hub at a different address. If your deployment also + resolves Compose variables from a shell environment or .env file, keep the same + HUB_API_KEY available there as well. + + 1. **Start the Docker Setup** Now, you're ready to run Formbricks with Docker. Use the command below to start Formbricks together with PostgreSQL, Redis, Formbricks Hub, and Cube.js: @@ -118,6 +148,12 @@ Make sure Docker and Docker Compose are installed on your system. These are usua Please take a look at our [migration guide](/self-hosting/advanced/migration) for version specific steps to update Formbricks. + + For a major migration such as Formbricks 4.x to 5.0, update your compose structure and configuration first. + Pulling images alone is not enough if your stack does not yet include Hub, `HUB_API_KEY`, the bundled + `cube/` config files plus `CUBEJS_API_SECRET`, or the new edge rate-limiting setup. + + 1. Pull the latest Formbricks image ```bash @@ -163,7 +199,8 @@ The fastest way to test MinIO with Formbricks is to use the included `docker-com docker compose -f docker-compose.dev.yml up -d ``` - This starts PostgreSQL, Valkey (Redis), MinIO, and Mailhog. + This starts PostgreSQL, Valkey (Redis), MinIO, Mailhog, Formbricks Hub, and a local Cube instance for + analytics testing. 2. **Access MinIO Console** diff --git a/docs/self-hosting/setup/kubernetes.mdx b/docs/self-hosting/setup/kubernetes.mdx index 97a862cd1b..b23259ec73 100644 --- a/docs/self-hosting/setup/kubernetes.mdx +++ b/docs/self-hosting/setup/kubernetes.mdx @@ -1,211 +1,167 @@ --- title: "Kubernetes Deployment" -description: "Deploy the new Helm chart on a Kubernetes cluster using Helm." +description: "Deploy Formbricks on Kubernetes with the current OCI Helm chart." icon: "circle-nodes" --- +Deploy Formbricks on Kubernetes using the current OCI Helm chart published from the `charts/formbricks` +directory in the Formbricks repository. + + + Formbricks v5 self-hosting expects Hub to be part of the runtime. The chart handles that by default. Use the + [migration guide](/self-hosting/advanced/migration#v5) before upgrading an existing 4.x deployment. + + ## Prerequisites + Ensure you have the following before proceeding: -- A running Kubernetes cluster (EKS, GKE, AKS, Minikube, etc.) -- An **Ingress Controller** (e.g., Traefik, Nginx) -- **Helm installed** on your local machine -- **Production setup requires managed PostgreSQL and Redis services** +- a running Kubernetes cluster +- Helm 3 installed locally +- a public hostname for `formbricks.webappUrl` +- a plan for PostgreSQL and Redis/Valkey, either in-cluster or managed externally +- an edge rate-limiting plan for the v5-covered routes: the chart's Envoy bundle or an equivalent external edge + solution -> **Note:** Redis is required for **session handling** when deploying multiple pods. - ---- - -## 1. Installation Steps +## 1. Install The Chart - -```sh -helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --create-namespace -``` -> **Note:** To specify specific version use `--version` flag. E.g., `--version 1.0.0`. Starting from 3.5.0, the chart is available on the GitHub Container Registry (GHCR). + -By default: -- PostgreSQL and Redis are deployed within the cluster. -- Secrets are dynamically generated and stored as Kubernetes Secrets. +```yaml +formbricks: + webappUrl: https://surveys.example.com +``` + +Add any additional overrides you need for ingress, external services, secrets, or Enterprise license features. - + + ```sh -helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --create-namespace --set enterprise.licenseKey="YOUR_LICENSE_KEY" +helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks \ + -n formbricks \ + --create-namespace \ + -f values.yaml ``` + +By default, the chart deploys: + +- the Formbricks application +- Formbricks Hub +- PostgreSQL +- Redis +- generated Kubernetes Secrets ---- +## 2. Configure Secrets And External Services -## 2. Configuring Secrets +### Using Generated Secrets -### Using Kubernetes Secrets (Default) -By default, **secrets are stored as Kubernetes Secrets**. -The chart automatically generates **random values** for required secrets. +The default chart path keeps `secret.enabled: true`, which lets the chart generate the required application +secrets for you. -Modify `values.yaml`: -```yaml -secret: - enabled: true -``` +### Using Managed PostgreSQL And Redis ---- +For production workloads, many teams prefer managed services: -### Using External Secrets (AWS Secrets Manager, Vault, etc.) -To use an **external secrets manager**, enable `externalSecret` in `values.yaml`: -```yaml -secret: - enabled: false # Disable default secret generation - -externalSecret: - enabled: true - secretStore: - name: aws-secrets-manager - kind: ClusterSecretStore - refreshInterval: "1h" - files: - redis: - data: - REDIS_PASSWORD: - remoteRef: - key: "prod/formbricks/secrets" - property: REDIS_PASSWORD - postgres: - data: - POSTGRES_ADMIN_PASSWORD: - remoteRef: - key: "prod/formbricks/secrets" - property: POSTGRES_ADMIN_PASSWORD - POSTGRES_USER_PASSWORD: - remoteRef: - key: "prod/formbricks/secrets" - property: POSTGRES_USER_PASSWORD - app-secrets: - data: - DATABASE_URL: - remoteRef: - key: "prod/formbricks/secrets" - property: DATABASE_URL - REDIS_URL: - remoteRef: - key: "prod/formbricks/secrets" - property: REDIS_URL - ENCRYPTION_KEY: - remoteRef: - key: "prod/formbricks/secrets" - property: ENCRYPTION_KEY -``` -**Ensure ExternalSecrets Operator is installed:** -[https://external-secrets.io/latest/](https://external-secrets.io/latest/) - -Install with: -```sh -helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --create-namespace -f values.yaml -``` - ---- - -## 3. Configuring PostgreSQL and Redis - -### Using Managed PostgreSQL and Redis -For production, we recommend using **managed database and cache services**. - -Modify `values.yaml`: ```yaml postgresql: enabled: false - externalDatabaseUrl: "postgresql://user:password@your-postgres-host:5432/mydb" + externalDatabaseUrl: "postgresql://user:password@your-postgres-host:5432/formbricks" redis: enabled: false externalRedisUrl: "redis://your-redis-host:6379" ``` -Install with: -```sh -helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --create-namespace -f values.yaml -``` ---- +### Using External Secrets -### Using In-Cluster PostgreSQL and Redis (Default) -By default, PostgreSQL and Redis are **deployed inside the cluster**. -To **ensure in-cluster deployment**, use: +If your cluster already uses an external secret manager, enable `externalSecret` and point it at your existing +SecretStore. Ensure the resulting app secret exposes the values your deployment needs, including `DATABASE_URL`, +`REDIS_URL`, and `HUB_API_KEY`. + +## 3. v5-Specific Deployment Notes + +### Hub Is Mandatory + +Formbricks v5 does not support `hub.enabled=false`. Keep the default `hub.enabled=true` behavior in place. + +Use `hub.image.tag`, `hub.resources`, and `hub.existingSecret` only when you need to pin or customize the Hub +deployment details. + +### Envoy Bundle Modes + +The chart supports three edge patterns for the v5-covered routes: + +- **Bundled Envoy controller**: set `envoy.enabled=true` and `envoy.controller.enabled=true` +- **Existing cluster Envoy controller**: set `envoy.enabled=true` and `envoy.controller.enabled=false` +- **Equivalent external edge protection**: keep using your platform's own ingress or gateway layer if it already + provides equivalent rate-limiting coverage + +If you use the chart-managed Envoy rate-limiting path, enable a dedicated backend with: ```yaml -postgresql: - enabled: true - -redis: +envoyRedis: enabled: true ``` -Apply with: + +This keeps Envoy rate-limiting state separate from the application's own Redis traffic. + +### Cube Is Optional + +Cube is only needed for analytics dashboards or other analysis flows that depend on Cube queries. + +- deploy Cube separately when you need it +- configure `CUBEJS_API_URL` and `CUBEJS_API_SECRET` for the Formbricks app +- do not expect the main Formbricks chart to provision Cube automatically + +## 4. Upgrade The Deployment + +For normal chart upgrades: + ```sh -helm install formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --create-namespace -f values.yaml +helm upgrade formbricks oci://ghcr.io/formbricks/helm-charts/formbricks \ + -n formbricks \ + -f values.yaml ``` ---- +For a Formbricks 4.x to 5.0 migration, confirm the following before running the upgrade: -## 4. Upgrading the Deployment -To apply changes: -```sh -helm upgrade formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks -``` +- Hub remains enabled +- `HUB_API_KEY` is present +- your edge rate-limiting plan is in place +- any required `AI_*` variables are added +- Cube is configured only if this instance needs analytics dashboards or analysis queries -### Scaling Resources -```sh -helm upgrade formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --set deployment.resources.limits.cpu=2 --set deployment.resources.limits.memory=4Gi -``` +## 5. Key Values -### Enabling Autoscaling -```sh -helm upgrade formbricks oci://ghcr.io/formbricks/helm-charts/formbricks -n formbricks --set autoscaling.enabled=true --set autoscaling.minReplicas=3 --set autoscaling.maxReplicas=10 -``` +| Field | Description | +| ----------------------------- | ------------------------------------------------------------- | +| `formbricks.webappUrl` | Public base URL for the Formbricks app | +| `deployment.image.tag` | Formbricks image tag override | +| `hub.enabled` | Must stay `true` in Formbricks v5 | +| `hub.image.tag` | Hub image tag override | +| `envoy.enabled` | Enables chart-managed Envoy Gateway resources | +| `envoy.controller.enabled` | Installs the bundled Envoy controller when `true` | +| `envoyRedis.enabled` | Deploys a dedicated Redis backend for Envoy rate limiting | +| `postgresql.externalDatabaseUrl` | Uses an external PostgreSQL service instead of in-cluster | +| `redis.externalRedisUrl` | Uses an external Redis/Valkey service instead of in-cluster | ---- +For the complete values surface, refer to the chart README in the repository: +[charts/formbricks/README.md](https://github.com/formbricks/formbricks/tree/main/charts/formbricks). -## 5. Key Configuration Values +## 6. Uninstalling The Deployment -| Field | Description | Default Value | -|--------------------------------|--------------------------------------|--------------| -| `deployment.replicas` | Number of application replicas | `1` | -| `deployment.image.repository` | Docker image repository | `"ghcr.io/formbricks/formbricks"` | -| `deployment.image.tag` | Docker image tag | `"latest"` | -| `autoscaling.enabled` | Enable autoscaling | `false` | -| `postgresql.enabled` | Deploy PostgreSQL in cluster | `true` | -| `postgresql.externalDatabaseUrl` | External PostgreSQL URL | `""` | -| `redis.enabled` | Deploy Redis in cluster | `true` | -| `redis.externalRedisUrl` | External Redis URL | `""` | -| `externalSecret.enabled` | Enable external secrets manager | `false` | - -**Refer to the Helm chart repository for full configuration options.** - ---- - -## 6. Uninstalling the Deployment To remove the deployment: + ```sh helm uninstall formbricks -n formbricks ``` -### Removing Persistent Volumes (PVCs) -By default, **PVCs are not deleted** with Helm. -To manually remove them: +If you also want to remove in-cluster persistent volumes: + ```sh kubectl delete pvc --all -n formbricks ``` - -To completely delete the namespace: -```sh -kubectl delete namespace formbricks -``` - ---- - -## Additional Notes -- **Ingress Setup:** If using an ingress controller, make sure to configure `ingress.enabled: true` in `values.yaml`. -- **Environment Variables:** Pass custom environment variables via `envFrom` in `values.yaml`. -- **Backup Strategy:** Ensure you have a backup policy for PostgreSQL if running in-cluster. - -**Your Formbricks deployment is now ready!** diff --git a/docs/self-hosting/setup/one-click.mdx b/docs/self-hosting/setup/one-click.mdx index 30c65eb770..ff3f66f228 100644 --- a/docs/self-hosting/setup/one-click.mdx +++ b/docs/self-hosting/setup/one-click.mdx @@ -32,6 +32,14 @@ Run this command in your terminal: curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/formbricks.sh -o formbricks.sh && chmod +x formbricks.sh && ./formbricks.sh install ``` + + The current v5 one-click stack is based on the production Docker Compose file and includes Formbricks Hub plus + the bundled XM Suite v5 Cube.js files under `formbricks/cube/`. Ensure your generated + `formbricks/docker-compose.yml` contains a non-empty `HUB_API_KEY` and that `formbricks/.env` contains + `CUBEJS_API_SECRET` before treating the v5 stack as ready. If either value is missing after the script + finishes, add it manually. `HUB_API_URL` should normally stay at `http://hub:8080`. + + ### Script Prompts During installation, the script will prompt you to provide some details: @@ -284,13 +292,22 @@ Y ## Update -To update Formbricks, simply run the following command: +To update Formbricks for a minor or patch release, run: ``` ./formbricks.sh update ``` -The script will automatically pull the latest version of Formbricks from GitHub Container Registry and restart the containers. +The script pulls the latest images, stops the running containers, and starts the stack again. + + + `./formbricks.sh update` does **not** rewrite your existing `formbricks/docker-compose.yml`. For a major + migration such as Formbricks 4.x to 5.0, follow the [migration guide](/self-hosting/advanced/migration#v5) + first, merge the current v5 Compose changes into your existing deployment, confirm `HUB_API_KEY` is set, and + only then run the update command. If your older one-click install also uses bundled MinIO for file uploads, + review that storage path separately before the first v5 restart; newer self-hosting updates move the bundled + object-storage path to RustFS, while external S3-compatible storage keeps the same `S3_*` app contract. + ## Stop