mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-23 02:45:21 -05:00
chore: assorted improvements
This commit is contained in:
@@ -1 +0,0 @@
|
||||
{"sessionId":"f77248e2-8840-41c6-968b-c3b7d8a9e913","pid":49125,"acquiredAt":1776168010367}
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
problemInternalError,
|
||||
successResponse,
|
||||
} from "@/app/api/v3/lib/response";
|
||||
import { V3SurveyLanguageError, serializeV3SurveyResource } from "@/app/api/v3/surveys/serializers";
|
||||
import {
|
||||
V3SurveyLanguageError,
|
||||
V3SurveyUnsupportedShapeError,
|
||||
serializeV3SurveyResource,
|
||||
} from "@/app/api/v3/surveys/serializers";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { deleteSurvey } from "@/modules/survey/lib/surveys";
|
||||
import { normalizeV3SurveyLanguageTag } from "../language";
|
||||
@@ -131,6 +135,19 @@ export const GET = withV3ApiWrapper({
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof V3SurveyUnsupportedShapeError) {
|
||||
log.warn({ statusCode: 400 }, "Unsupported v3 survey shape");
|
||||
return problemBadRequest(requestId, error.message, {
|
||||
instance,
|
||||
invalid_params: [
|
||||
{
|
||||
name: "survey",
|
||||
reason: error.message,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -31,12 +31,8 @@ describe("resolveV3SurveyLanguageCode", () => {
|
||||
expect(resolveV3SurveyLanguageCode("de", languages)).toEqual({ ok: true, code: "de-DE" });
|
||||
});
|
||||
|
||||
test("returns disabled when the resolved language is disabled", () => {
|
||||
expect(resolveV3SurveyLanguageCode("fr", languages)).toEqual({
|
||||
ok: false,
|
||||
reason: "disabled",
|
||||
message: "Language 'fr-FR' is disabled for this survey",
|
||||
});
|
||||
test("resolves disabled configured languages for management reads", () => {
|
||||
expect(resolveV3SurveyLanguageCode("fr", languages)).toEqual({ ok: true, code: "fr-FR" });
|
||||
});
|
||||
|
||||
test("returns ambiguous when language-only tags match multiple configured languages", () => {
|
||||
@@ -59,4 +55,11 @@ describe("resolveV3SurveyLanguageCode", () => {
|
||||
message: "Language 'es-ES' is not configured for this survey",
|
||||
});
|
||||
});
|
||||
|
||||
test("resolves the implicit default language for surveys without configured languages", () => {
|
||||
expect(resolveV3SurveyLanguageCode("en", [{ code: "en-US", enabled: true }])).toEqual({
|
||||
ok: true,
|
||||
code: "en-US",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ type TV3SurveyLanguageInput = {
|
||||
|
||||
type TResolveV3SurveyLanguageCodeResult =
|
||||
| { ok: true; code: string }
|
||||
| { ok: false; reason: "invalid" | "unknown" | "disabled" | "ambiguous"; message: string };
|
||||
| { ok: false; reason: "invalid" | "unknown" | "ambiguous"; message: string };
|
||||
|
||||
export function normalizeV3SurveyLanguageTag(value: string): string | null {
|
||||
const normalizedSeparators = value.trim().replaceAll("_", "-");
|
||||
@@ -44,14 +44,6 @@ export function resolveV3SurveyLanguageCode(
|
||||
);
|
||||
|
||||
if (exactMatch) {
|
||||
if (!exactMatch.enabled) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "disabled",
|
||||
message: `Language '${exactMatch.code}' is disabled for this survey`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, code: exactMatch.code };
|
||||
}
|
||||
|
||||
@@ -71,14 +63,6 @@ export function resolveV3SurveyLanguageCode(
|
||||
|
||||
const languageMatch = matchingLanguages[0];
|
||||
if (languageMatch) {
|
||||
if (!languageMatch.enabled) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "disabled",
|
||||
message: `Language '${languageMatch.code}' is disabled for this survey`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, code: languageMatch.code };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { V3SurveyLanguageError, serializeV3SurveyResource } from "./serializers";
|
||||
import { V3SurveyUnsupportedShapeError, serializeV3SurveyResource } from "./serializers";
|
||||
|
||||
const baseSurvey = {
|
||||
id: "survey_1",
|
||||
@@ -28,6 +28,7 @@ const baseSurvey = {
|
||||
language: { id: "lang_3", code: "fr-FR", alias: "fr", createdAt: new Date(), updatedAt: new Date() },
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
headline: { default: "Welcome", "de-DE": "Willkommen", "fr-FR": "Bienvenue" },
|
||||
@@ -74,6 +75,73 @@ describe("serializeV3SurveyResource", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("does not expose the internal default pseudo-locale for surveys without configured languages", () => {
|
||||
const survey = {
|
||||
...baseSurvey,
|
||||
languages: [],
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
headline: { default: "Welcome" },
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: "block_1",
|
||||
name: "Intro",
|
||||
elements: [
|
||||
{
|
||||
id: "satisfaction",
|
||||
type: "openText",
|
||||
headline: { default: "What should we improve?" },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const resource = serializeV3SurveyResource(survey);
|
||||
|
||||
expect(resource.defaultLanguage).toBe("en-US");
|
||||
expect(resource.languages).toEqual([{ code: "en-US", default: true, enabled: true }]);
|
||||
expect((resource.welcomeCard as any).headline).toEqual({ "en-US": "Welcome" });
|
||||
expect((resource.blocks as any)[0].elements[0].headline).toEqual({
|
||||
"en-US": "What should we improve?",
|
||||
});
|
||||
});
|
||||
|
||||
test("localizes the implicit default language for surveys without configured languages", () => {
|
||||
const survey = {
|
||||
...baseSurvey,
|
||||
languages: [],
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
headline: { default: "Welcome" },
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const resource = serializeV3SurveyResource(survey, { lang: "en" });
|
||||
|
||||
expect(resource.language).toBe("en-US");
|
||||
expect((resource.welcomeCard as any).headline).toBe("Welcome");
|
||||
});
|
||||
|
||||
test("preserves stored locale variants when their keys use non-canonical casing or separators", () => {
|
||||
const survey = {
|
||||
...baseSurvey,
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
headline: { default: "Welcome", de_de: "Willkommen" },
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const resource = serializeV3SurveyResource(survey);
|
||||
|
||||
expect((resource.welcomeCard as any).headline).toEqual({
|
||||
"en-US": "Welcome",
|
||||
"de-DE": "Willkommen",
|
||||
});
|
||||
});
|
||||
|
||||
test("localizes fields for case-insensitive underscore language selectors", () => {
|
||||
const resource = serializeV3SurveyResource(baseSurvey, { lang: "DE_de" });
|
||||
|
||||
@@ -90,11 +158,11 @@ describe("serializeV3SurveyResource", () => {
|
||||
expect((resource.welcomeCard as any).headline).toBe("Willkommen");
|
||||
});
|
||||
|
||||
test("rejects disabled language selectors", () => {
|
||||
expect(() => serializeV3SurveyResource(baseSurvey, { lang: "fr" })).toThrow(V3SurveyLanguageError);
|
||||
expect(() => serializeV3SurveyResource(baseSurvey, { lang: "fr" })).toThrow(
|
||||
"Language 'fr-FR' is disabled for this survey"
|
||||
);
|
||||
test("localizes disabled configured languages for management reads", () => {
|
||||
const resource = serializeV3SurveyResource(baseSurvey, { lang: "fr" });
|
||||
|
||||
expect(resource.language).toBe("fr-FR");
|
||||
expect((resource.welcomeCard as any).headline).toBe("Bienvenue");
|
||||
});
|
||||
|
||||
test("rejects ambiguous language-only selectors", () => {
|
||||
@@ -130,4 +198,17 @@ describe("serializeV3SurveyResource", () => {
|
||||
"Language 'pt' is ambiguous for this survey; use one of pt-BR, pt-PT"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects legacy question-based survey shapes instead of returning an incomplete block resource", () => {
|
||||
const survey = {
|
||||
...baseSurvey,
|
||||
questions: [{ id: "legacy_question", type: "openText", headline: { default: "Legacy question" } }],
|
||||
blocks: [],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
expect(() => serializeV3SurveyResource(survey)).toThrow(V3SurveyUnsupportedShapeError);
|
||||
expect(() => serializeV3SurveyResource(survey)).toThrow(
|
||||
"Legacy question-based surveys are not supported by the v3 survey management API"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TSurvey as TSurveyListRecord } from "@/modules/survey/list/types/s
|
||||
import { normalizeV3SurveyLanguageTag, resolveV3SurveyLanguageCode } from "./language";
|
||||
|
||||
export type TV3SurveyListItem = Omit<TSurveyListRecord, "singleUse">;
|
||||
const DEFAULT_V3_SURVEY_LANGUAGE = "en-US";
|
||||
|
||||
type TV3SurveyLanguage = {
|
||||
code: string;
|
||||
@@ -25,6 +26,13 @@ export class V3SurveyLanguageError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class V3SurveyUnsupportedShapeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "V3SurveyUnsupportedShapeError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the v3 API contract isolated from internal persistence naming.
|
||||
* Surveys are scoped by workspaceId.
|
||||
@@ -40,11 +48,17 @@ function toIsoString(value: Date | string): string {
|
||||
}
|
||||
|
||||
function getSurveyLanguages(survey: TInternalSurvey): TV3SurveyLanguage[] {
|
||||
return (survey.languages ?? []).map((surveyLanguage) => ({
|
||||
const languages = (survey.languages ?? []).map((surveyLanguage) => ({
|
||||
code: normalizeV3SurveyLanguageTag(surveyLanguage.language.code) ?? surveyLanguage.language.code,
|
||||
default: surveyLanguage.default,
|
||||
enabled: surveyLanguage.enabled,
|
||||
}));
|
||||
|
||||
if (languages.length === 0) {
|
||||
return [{ code: DEFAULT_V3_SURVEY_LANGUAGE, default: true, enabled: true }];
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
function getDefaultLanguage(survey: TInternalSurvey): string {
|
||||
@@ -52,7 +66,7 @@ function getDefaultLanguage(survey: TInternalSurvey): string {
|
||||
.code;
|
||||
return defaultLanguageCode
|
||||
? (normalizeV3SurveyLanguageTag(defaultLanguageCode) ?? defaultLanguageCode)
|
||||
: "default";
|
||||
: DEFAULT_V3_SURVEY_LANGUAGE;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
@@ -67,6 +81,17 @@ function isI18nString(value: unknown): value is Record<string, string> {
|
||||
);
|
||||
}
|
||||
|
||||
function getI18nValueForLanguage(value: Record<string, string>, languageCode: string): string | undefined {
|
||||
if (typeof value[languageCode] === "string") {
|
||||
return value[languageCode];
|
||||
}
|
||||
|
||||
const matchingKey = Object.keys(value).find(
|
||||
(key) => normalizeV3SurveyLanguageTag(key)?.toLowerCase() === languageCode.toLowerCase()
|
||||
);
|
||||
return matchingKey ? value[matchingKey] : undefined;
|
||||
}
|
||||
|
||||
function serializeCanonicalValue(
|
||||
value: unknown,
|
||||
defaultLanguage: string,
|
||||
@@ -78,8 +103,9 @@ function serializeCanonicalValue(
|
||||
};
|
||||
|
||||
for (const languageCode of configuredLanguageCodes) {
|
||||
if (languageCode !== defaultLanguage && typeof value[languageCode] === "string") {
|
||||
result[languageCode] = value[languageCode];
|
||||
const translatedValue = getI18nValueForLanguage(value, languageCode);
|
||||
if (languageCode !== defaultLanguage && translatedValue !== undefined) {
|
||||
result[languageCode] = translatedValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +130,7 @@ function serializeCanonicalValue(
|
||||
|
||||
function serializeLocalizedValue(value: unknown, language: string): TSerializedValue {
|
||||
if (isI18nString(value)) {
|
||||
return value[language] ?? value.default;
|
||||
return getI18nValueForLanguage(value, language) ?? value.default;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
@@ -131,6 +157,12 @@ function resolveRequestedLanguage(languages: TV3SurveyLanguage[], language: stri
|
||||
}
|
||||
|
||||
export function serializeV3SurveyResource(survey: TInternalSurvey, options?: { lang?: string }) {
|
||||
if (Array.isArray(survey.questions) && survey.questions.length > 0) {
|
||||
throw new V3SurveyUnsupportedShapeError(
|
||||
"Legacy question-based surveys are not supported by the v3 survey management API"
|
||||
);
|
||||
}
|
||||
|
||||
const defaultLanguage = getDefaultLanguage(survey);
|
||||
const languages = getSurveyLanguages(survey);
|
||||
const configuredLanguageCodes = new Set(languages.map((language) => language.code));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
@@ -28,32 +28,6 @@ const SelectTrigger = React.forwardRef<
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1 text-slate-500", className)}
|
||||
{...props}>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1 text-slate-500", className)}
|
||||
{...props}>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent: React.ComponentType<SelectPrimitive.SelectContentProps> = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
@@ -65,16 +39,18 @@ const SelectContent: React.ComponentType<SelectPrimitive.SelectContentProps> = R
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white text-slate-700 shadow-md animate-in fade-in-80 dark:bg-slate-700 dark:text-slate-300",
|
||||
position === "popper" &&
|
||||
"max-h-[var(--radix-select-content-available-height)] data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn("p-1", position === "popper" && "w-full min-w-[var(--radix-select-trigger-width)]")}>
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
@@ -122,8 +98,6 @@ export {
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
|
||||
@@ -184,74 +184,6 @@ services:
|
||||
- ./docker/cube/schema:/cube/conf/model:ro
|
||||
restart: on-failure
|
||||
|
||||
# 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:${HUB_IMAGE_TAG:-0.3.0}
|
||||
restart: "no"
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
[
|
||||
'if [ -x /usr/local/bin/goose ] && [ -x /usr/local/bin/river ]; then /usr/local/bin/goose -dir /app/migrations postgres "$$DATABASE_URL" up && /usr/local/bin/river migrate-up --database-url "$$DATABASE_URL"; else echo ''Migration tools (goose/river) not in image.''; exit 1; fi',
|
||||
]
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
# Formbricks Hub API (ghcr.io/formbricks/hub). Shares the same Postgres database as Formbricks by default.
|
||||
hub:
|
||||
image: ghcr.io/formbricks/hub:${HUB_IMAGE_TAG:-0.3.0}
|
||||
depends_on:
|
||||
hub-migrate:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
API_KEY: ${HUB_API_KEY:-dev-api-key}
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
# Explicit Postgres env so migrations and any libpq fallback use the service host, not localhost
|
||||
PGHOST: postgres
|
||||
PGPORT: "5432"
|
||||
PGUSER: postgres
|
||||
PGPASSWORD: postgres
|
||||
PGDATABASE: postgres
|
||||
PGSSLMODE: disable
|
||||
|
||||
cube:
|
||||
profiles: ["xm"]
|
||||
image: cubejs/cube:v1.6.6
|
||||
env_file:
|
||||
- apps/web/.env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
hub-migrate:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- 4000:4000
|
||||
- 4001:4001 # Cube Playground UI (dev only)
|
||||
environment:
|
||||
CUBEJS_DB_TYPE: postgres
|
||||
CUBEJS_DB_HOST: ${CUBEJS_DB_HOST:-postgres}
|
||||
CUBEJS_DB_NAME: ${CUBEJS_DB_NAME:-postgres}
|
||||
CUBEJS_DB_USER: ${CUBEJS_DB_USER:-postgres}
|
||||
CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres}
|
||||
CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432}
|
||||
CUBEJS_DEV_MODE: "true"
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:-}
|
||||
CUBEJS_JWT_ISSUER: ${CUBEJS_JWT_ISSUER:-formbricks-web}
|
||||
CUBEJS_JWT_AUDIENCE: ${CUBEJS_JWT_AUDIENCE:-formbricks-cube}
|
||||
CUBEJS_DEFAULT_API_SCOPES: meta,data
|
||||
CUBEJS_CACHE_AND_QUEUE_DRIVER: memory
|
||||
volumes:
|
||||
- ./docker/cube/cube.js:/cube/conf/cube.js:ro
|
||||
- ./docker/cube/schema:/cube/conf/model:ro
|
||||
restart: on-failure
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
|
||||
@@ -225,7 +225,8 @@ paths:
|
||||
Locale code for a localized read projection. The parser is case-insensitive, accepts
|
||||
`_` or `-` separators, and normalizes to canonical BCP 47 casing (`de_DE`, `DE-de` → `de-DE`).
|
||||
A language-only selector (`de`) resolves to the matching configured survey language when exactly
|
||||
one exists; otherwise it returns `400`. Aliases are not accepted.
|
||||
one exists; otherwise it returns `400`. Disabled-but-configured languages are readable in the
|
||||
management API so unfinished translations can be completed. Aliases are not accepted.
|
||||
- in: query
|
||||
name: version
|
||||
required: false
|
||||
@@ -251,7 +252,7 @@ paths:
|
||||
data:
|
||||
$ref: "#/components/schemas/SurveyResource"
|
||||
"400":
|
||||
description: Invalid survey id, unsupported query parameter, unsupported version selector, or unknown/disabled language
|
||||
description: Invalid survey id, unsupported query parameter, unsupported version selector, unknown language, or unsupported legacy survey shape
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
@@ -422,7 +423,7 @@ components:
|
||||
additionalProperties: true
|
||||
defaultLanguage:
|
||||
type: string
|
||||
description: Real locale code for the survey default language when configured; otherwise `default`.
|
||||
description: Real locale code for the survey default language. The internal `default` translation key is never exposed.
|
||||
language:
|
||||
type: string
|
||||
description: Present only on localized `?lang=` responses.
|
||||
|
||||
Reference in New Issue
Block a user