Compare commits

...

20 Commits

Author SHA1 Message Date
Dhruwang
68b9f06b9a chore: sync latest changes from #6737 2025-10-31 15:03:50 +05:30
Dhruwang
9cba619ac7 revertd changes in unrelated files 2025-10-31 15:00:43 +05:30
Dhruwang
73776f3430 fixed linting and added nl-NL translations for survey languages 2025-10-31 14:44:39 +05:30
Dhruwang
754f1cded2 removed en-US from target langauges in i18n.json 2025-10-31 14:43:59 +05:30
Dhruwang
73ebd22efe feat: add Dutch language support (duplicate of #6737) 2025-10-31 14:19:58 +05:30
Dhruwang
12e51c2307 added missing translations 2025-10-31 14:17:01 +05:30
Thomas Brugman
8739165101 Merge branch 'formbricks:main' into main 2025-10-31 08:37:35 +01:00
dependabot[bot]
b1b94eaa66 chore(deps): bump next-auth from 4.24.11 to 4.24.12 in /apps/web in the npm_and_yarn group across 1 directory (#6751)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-10-30 13:09:31 +00:00
Marc T.
67cc96449d fix: allow access of /animated-bgs/** from public url (#6748)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-10-30 12:21:50 +00:00
Dhruwang Jariwala
bf41a53b86 fix: survey ui loading issue (#6755) 2025-10-30 07:32:44 +00:00
Anshuman Pandey
26292ecf39 fix: welcome card headline in survey title (#6749) 2025-10-29 07:57:27 +00:00
Thomas Brugman
ee3b7656cd Merge pull request #2 from Githubguy132010/codex/fix-tests-and-add-comments
Align locale configurations and stabilize auth tests
2025-10-24 18:23:15 +02:00
Thomas Brugman
86571c77e6 Merge branch 'main' into codex/fix-tests-and-add-comments 2025-10-24 18:23:07 +02:00
Thomas Brugman
1cf643d6b0 fix: align auth test timeout formatting 2025-10-24 18:22:13 +02:00
Thomas Brugman
74b0305383 chore: resolve merge conflict in i18n utils 2025-10-24 18:10:19 +02:00
Thomas Brugman
9d647d3862 fix: align locale config lists 2025-10-24 18:02:35 +02:00
Thomas Brugman
478186c79c feat: add i18n utils tests and update multi-language survey components 2025-10-24 13:29:19 +02:00
Thomas Brugman
94531953da Merge pull request #1 from Githubguy132010/codex/vertaal-app-naar-het-nederlands
feat: add Dutch locale support
2025-10-24 10:12:11 +02:00
Thomas Brugman
6aa374a533 fix: add fallback labels for language selector 2025-10-24 10:03:56 +02:00
Thomas Brugman
d06688e414 feat: add Dutch locale support 2025-10-24 09:09:42 +02:00
25 changed files with 5554 additions and 3149 deletions

View File

@@ -1,3 +0,0 @@
import { LinkSurveyLoading } from "@/modules/survey/link/loading";
export default LinkSurveyLoading;

View File

@@ -7,7 +7,7 @@
},
"locale": {
"source": "en-US",
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW"]
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW", "nl-NL"]
},
"version": 1.8
}

View File

@@ -915,15 +915,12 @@ checksums:
environments/settings/billing/manage_subscription: 31cafd367fc70d656d8dd979d537dc96
environments/settings/billing/monthly: 818f1192e32bb855597f930d3e78806e
environments/settings/billing/monthly_identified_users: 0795735f6b241d31edac576a77dd7e55
environments/settings/billing/per_month: 64e96490ee2d7811496cf04adae30aa4
environments/settings/billing/per_year: bf02408d157486e53c15a521a5645617
environments/settings/billing/plan_upgraded_successfully: 52e2a258cc9ca8a512c288bf6f18cf37
environments/settings/billing/premium_support_with_slas: 2e33d4442c16bfececa6cae7b2081e5d
environments/settings/billing/remove_branding: 88b6b818750e478bfa153b33dd658280
environments/settings/billing/startup: 4c4ac5a0b9dc62100bca6c6465f31c4c
environments/settings/billing/startup_description: 964fcb2c77f49b80266c94606e3f4506
environments/settings/billing/switch_plan: fb3e1941051a4273ca29224803570f4b
environments/settings/billing/switch_plan_confirmation_text: 910a6df56964619975c6ed5651a55db7
environments/settings/billing/team_access_roles: 1cc4af14e589f6c09ab92a4f21958049
environments/settings/billing/unable_to_upgrade_plan: 50fc725609411d139e534c85eeb2879e
environments/settings/billing/unlimited_miu: 29c3f5bd01c2a09fdf1d3601665ce90f

View File

@@ -170,6 +170,7 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",

View File

@@ -137,6 +137,7 @@ export const appLanguages = [
"ro-RO": "Engleză (SUA)",
"ja-JP": "英語(米国)",
"zh-Hans-CN": "英语(美国)",
"nl-NL": "Engels (VS)",
},
},
{
@@ -151,6 +152,7 @@ export const appLanguages = [
"ro-RO": "Germană",
"ja-JP": "ドイツ語",
"zh-Hans-CN": "德语",
"nl-NL": "Duits",
},
},
{
@@ -165,6 +167,7 @@ export const appLanguages = [
"ro-RO": "Portugheză (Brazilia)",
"ja-JP": "ポルトガル語(ブラジル)",
"zh-Hans-CN": "葡萄牙语(巴西)",
"nl-NL": "Portugees (Brazilië)",
},
},
{
@@ -179,6 +182,7 @@ export const appLanguages = [
"ro-RO": "Franceză",
"ja-JP": "フランス語",
"zh-Hans-CN": "法语",
"nl-NL": "Frans",
},
},
{
@@ -193,6 +197,7 @@ export const appLanguages = [
"ro-RO": "Chineză (Tradicională)",
"ja-JP": "中国語(繁体字)",
"zh-Hans-CN": "繁体中文",
"nl-NL": "Chinees (Traditioneel)",
},
},
{
@@ -207,6 +212,7 @@ export const appLanguages = [
"ro-RO": "Portugheză (Portugalia)",
"ja-JP": "ポルトガル語(ポルトガル)",
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
"nl-NL": "Portugees (Portugal)",
},
},
{
@@ -221,6 +227,7 @@ export const appLanguages = [
"ro-RO": "Română",
"ja-JP": "ルーマニア語",
"zh-Hans-CN": "罗马尼亚语",
"nl-NL": "Roemeens",
},
},
{
@@ -235,6 +242,7 @@ export const appLanguages = [
"ro-RO": "Japoneză",
"ja-JP": "日本語",
"zh-Hans-CN": "日语",
"nl-NL": "Japans",
},
},
{
@@ -249,6 +257,22 @@ export const appLanguages = [
"ro-RO": "Chineză (Simplificată)",
"ja-JP": "中国語(簡体字)",
"zh-Hans-CN": "简体中文",
"nl-NL": "Chinees (Vereenvoudigd)",
},
},
{
code: "nl-NL",
label: {
"en-US": "Dutch",
"de-DE": "Niederländisch",
"pt-BR": "Holandês",
"fr-FR": "Néerlandais",
"zh-Hant-TW": "荷蘭語",
"pt-PT": "Holandês",
"ro-RO": "Olandeză",
"ja-JP": "オランダ語",
"zh-Hans-CN": "荷兰语",
"nl-NL": "Nederlands",
},
},
];

View File

@@ -1,5 +1,5 @@
import { formatDistance, intlFormat } from "date-fns";
import { de, enUS, fr, ja, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
import { de, enUS, fr, ja, nl, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
import { TUserLocale } from "@formbricks/types/user";
export const convertDateString = (dateString: string | null) => {
@@ -91,6 +91,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
return ptBR;
case "fr-FR":
return fr;
case "nl-NL":
return nl;
case "zh-Hant-TW":
return zhTW;
case "pt-PT":

View File

@@ -53,9 +53,9 @@ export const I18nProvider = ({ children, language, defaultLanguage }: I18nProvid
initializeI18n();
}, [locale, defaultLanguage]);
// Don't render children until i18n is ready to prevent hydration issues
// Don't render children until i18n is ready to prevent race conditions
if (!isReady) {
return <div style={{ visibility: "hidden" }}>{children}</div>;
return null;
}
return (

2911
apps/web/locales/nl-NL.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,6 @@ export const middleware = async (originalRequest: NextRequest) => {
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public).*)",
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public|animated-bgs).*)",
],
};

View File

@@ -62,7 +62,10 @@ const validateLanguages = (languages: Language[], t: TFunction) => {
return false;
}
// Check if the chosen alias matches an ISO identifier of a language that hasn't been added
// Prevent choosing an alias that clashes with the ISO code of some other
// language. Without this guard users could create ambiguous language entries
// (e.g. alias "nl" pointing to a non-Dutch language) which later breaks the
// dropdowns that rely on ISO identifiers.
for (const alias of languageAliases) {
if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) {
toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), {

View File

@@ -43,8 +43,13 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
setIsOpen(false);
};
// Most ISO entries don't ship with every locale translation, so fall back to
// English to keep the dropdown readable for locales such as Dutch that were
// added recently.
const getLabelForLocale = (item: TIso639Language) => item.label[locale] ?? item.label["en-US"];
const filteredItems = items.filter((item) =>
item.label[locale].toLowerCase().includes(searchTerm.toLowerCase())
getLabelForLocale(item).toLowerCase().includes(searchTerm.toLowerCase())
);
// Focus the input when the dropdown is opened
@@ -61,7 +66,9 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
disabled={disabled}
onClick={toggleDropdown}
variant="ghost">
<span className="mr-2">{selectedOption?.label[locale] ?? t("common.select")}</span>
<span className="mr-2">
{selectedOption ? getLabelForLocale(selectedOption) : t("common.select")}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
<div
@@ -84,7 +91,7 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
onClick={() => {
handleOptionSelect(item);
}}>
{item.label[locale]}
{getLabelForLocale(item)}
</button>
))}
</div>

View File

@@ -32,6 +32,16 @@ vi.mock("@/lib/styling/constants", () => ({
},
}));
// Mock recall utility
vi.mock("@/lib/utils/recall", () => ({
recallToHeadline: vi.fn((headline) => headline),
}));
// Mock text content extraction
vi.mock("@formbricks/types/surveys/validation", () => ({
getTextContent: vi.fn((text) => text),
}));
describe("Metadata Utils", () => {
// Reset all mocks before each test
beforeEach(() => {
@@ -173,6 +183,75 @@ describe("Metadata Utils", () => {
WEBAPP_URL: "https://test.formbricks.com",
}));
});
test("handles welcome card headline with HTML content", async () => {
const { getTextContent } = await import("@formbricks/types/surveys/validation");
const mockSurvey = {
id: mockSurveyId,
environmentId: mockEnvironmentId,
name: "Test Survey",
metadata: {},
languages: [],
welcomeCard: {
enabled: true,
timeToFinish: false,
showResponseCount: false,
headline: {
default: "<p>Welcome <strong>Headline</strong></p>",
},
html: {
default: "Welcome Description",
},
} as TSurveyWelcomeCard,
} as TSurvey;
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(getTextContent).mockReturnValue("Welcome Headline");
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(getTextContent).toHaveBeenCalled();
expect(result.title).toBe("Welcome Headline");
});
test("handles welcome card headline with recall variables", async () => {
const { recallToHeadline } = await import("@/lib/utils/recall");
const mockSurvey = {
id: mockSurveyId,
environmentId: mockEnvironmentId,
name: "Test Survey",
metadata: {},
languages: [],
welcomeCard: {
enabled: true,
timeToFinish: false,
showResponseCount: false,
headline: {
default: "Welcome #recall:name/fallback:User#",
},
html: {
default: "Welcome Description",
},
} as TSurveyWelcomeCard,
} as TSurvey;
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(recallToHeadline).mockReturnValue({
default: "Welcome @User",
});
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(recallToHeadline).toHaveBeenCalledWith(
mockSurvey.welcomeCard.headline,
mockSurvey,
false,
"default"
);
expect(result.title).toBe("Welcome @User");
});
});
describe("getSurveyOpenGraphMetadata", () => {

View File

@@ -1,8 +1,10 @@
import { Metadata } from "next";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
import { recallToHeadline } from "@/lib/utils/recall";
import { getSurvey } from "@/modules/survey/lib/survey";
type TBasicSurveyMetadata = {
@@ -48,7 +50,9 @@ export const getBasicSurveyMetadata = async (
const titleFromMetadata = metadata?.title ? getLocalizedValue(metadata.title, langCode) || "" : undefined;
const titleFromWelcome =
welcomeCard?.enabled && welcomeCard.headline
? getLocalizedValue(welcomeCard.headline, langCode) || ""
? getTextContent(
getLocalizedValue(recallToHeadline(welcomeCard.headline, survey, false, langCode), langCode)
) || ""
: undefined;
let title = titleFromMetadata || titleFromWelcome || survey.name;

View File

@@ -1,11 +0,0 @@
"use client";
export const LinkSurveyLoading = () => {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-1/2 w-3/4 flex-col sm:w-1/2 lg:w-1/4">
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-slate-200 font-medium text-slate-900"></div>
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-slate-200 text-slate-900"></div>
</div>
</div>
);
};

View File

@@ -102,7 +102,7 @@
"markdown-it": "14.1.0",
"mime-types": "3.0.1",
"next": "15.5.6",
"next-auth": "4.24.11",
"next-auth": "4.24.12",
"next-safe-action": "7.10.8",
"node-fetch": "3.3.2",
"nodemailer": "7.0.9",

View File

@@ -211,7 +211,18 @@ vi.mock("@/lib/constants", () => ({
SESSION_MAX_AGE: 1000,
MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT: 100,
MAX_OTHER_OPTION_LENGTH: 250,
AVAILABLE_LOCALES: ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"],
AVAILABLE_LOCALES: [
"en-US",
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ja-JP",
"zh-Hans-CN",
],
DEFAULT_LOCALE: "en-US",
BREVO_API_KEY: "mock-brevo-api-key",
ITEMS_PER_PAGE: 30,

View File

@@ -102,6 +102,7 @@ When PUBLIC_URL is configured, the following routes are automatically served fro
- `/fonts/*` - Font files
- `/icons/*` - Icon assets
- `/public/*` - Public static files
- `/animated-bgs/*` - Animated Background assets
#### Storage Routes

View File

@@ -79,7 +79,7 @@
},
"pnpm": {
"patchedDependencies": {
"next-auth@4.24.11": "patches/next-auth@4.24.11.patch"
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
},
"overrides": {
"axios": ">=1.12.2",

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
},
"locale": {
"source": "en",
"targets": ["de", "it", "fr", "es", "ar", "pt", "ru", "uz", "ro", "ja", "zh-Hans", "hi"]
"targets": ["de", "it", "fr", "es", "ar", "pt", "ru", "uz", "ro", "ja", "zh-Hans", "hi", "nl"]
},
"version": 1.8
}

View File

@@ -0,0 +1,73 @@
{
"common": {
"and": "en",
"apply": "Toepassen",
"auto_close_wrapper": "Automatisch sluitende wrapper",
"back": "Terug",
"click_or_drag_to_upload_files": "Klik of sleep om bestanden te uploaden.",
"close_survey": "Enquête sluiten",
"company_logo": "Bedrijfslogo",
"delete_file": "Bestand verwijderen",
"file_upload": "Bestand uploaden",
"finish": "Voltooien",
"language_switch": "Taalschakelaar",
"less_than_x_minutes": "{count, plural, one {minder dan 1 minuut} other {minder dan {count} minuten}}",
"move_down": "Verplaats {item} naar beneden",
"move_up": "Verplaats {item} omhoog",
"next": "Volgende",
"open_in_new_tab": "Openen in nieuw tabblad",
"optional": "Optioneel",
"options": "Opties",
"people_responded": "{count, plural, one {1 persoon heeft gereageerd} other {{count} mensen hebben gereageerd}}",
"please_retry_now_or_try_again_later": "Probeer het nu opnieuw of probeer het later opnieuw.",
"powered_by": "Aangedreven door",
"privacy_policy": "Privacybeleid",
"protected_by_reCAPTCHA_and_the_Google": "Beschermd door reCAPTCHA en Google",
"question": "Vraag",
"question_video": "Vraagvideo",
"ranking_items": "Items rangschikken",
"respondents_will_not_see_this_card": "Respondenten zien deze kaart niet",
"retry": "Opnieuw proberen",
"select_a_date": "Selecteer een datum",
"select_for_ranking": "Selecteer {item} voor rangschikking",
"sending_responses": "Reacties verzenden...",
"takes": "Neemt",
"terms_of_service": "Servicevoorwaarden",
"the_servers_cannot_be_reached_at_the_moment": "De servers zijn momenteel niet bereikbaar.",
"they_will_be_redirected_immediately": "Ze worden onmiddellijk doorgestuurd",
"upload_files_by_clicking_or_dragging_them_here": "Upload bestanden door ze hierheen te klikken of te slepen",
"uploading": "Uploaden",
"x_minutes": "{count, plural, one {1 minuut} other {{count} minuten}}",
"x_plus_minutes": "{count}+ minuten",
"you_have_selected_x_date": "Je hebt {date} geselecteerd",
"you_have_successfully_uploaded_the_file": "Je hebt het bestand {fileName} succesvol geüpload",
"your_feedback_is_stuck": "Je feedback blijft hangen :("
},
"errors": {
"file_input": {
"duplicate_files": "De volgende bestanden zijn al geüpload: {duplicateNames}. Dubbele bestanden zijn niet toegestaan.",
"file_size_exceeded": "De volgende bestanden overschrijden de maximale grootte van {maxSizeInMB} MB en zijn verwijderd: {fileNames}",
"file_size_exceeded_alert": "Het bestand moet kleiner zijn dan {maxSizeInMB} MB",
"no_valid_file_types_selected": "Geen geldige bestandstypen geselecteerd. Selecteer een geldig bestandstype.",
"only_one_file_can_be_uploaded_at_a_time": "Er kan slechts één bestand tegelijk worden geüpload.",
"upload_failed": "Uploaden mislukt! Probeer het opnieuw.",
"you_can_only_upload_a_maximum_of_files": "Je kunt maximaal {FILE_LIMIT} bestanden uploaden."
},
"invalid_device_error": {
"message": "Schakel de spambeveiliging uit in de enquête-instellingen om dit apparaat te blijven gebruiken.",
"title": "Dit apparaat ondersteunt geen spambeveiliging."
},
"please_book_an_appointment": "Maak een afspraak",
"please_enter_a_valid_email_address": "Voer een geldig e-mailadres in",
"please_enter_a_valid_phone_number": "Voer een geldig telefoonnummer in",
"please_enter_a_valid_url": "Voer een geldige URL in",
"please_fill_out_this_field": "Vul dit veld in",
"please_rank_all_items_before_submitting": "Rangschik alle items voordat u ze verzendt",
"please_select_a_date": "Selecteer een datum",
"please_upload_a_file": "Upload een bestand",
"recaptcha_error": {
"message": "Uw reactie kan niet worden verzonden omdat deze is gemarkeerd als geautomatiseerde activiteit. Als u ademhaalt, probeer het dan opnieuw.",
"title": "We konden niet verifiëren dat je een mens bent."
}
}
}

View File

@@ -9,6 +9,7 @@ import frTranslations from "../../locales/fr.json";
import hiTranslations from "../../locales/hi.json";
import itTranslations from "../../locales/it.json";
import jaTranslations from "../../locales/ja.json";
import nlTranslations from "../../locales/nl.json";
import ptTranslations from "../../locales/pt.json";
import roTranslations from "../../locales/ro.json";
import ruTranslations from "../../locales/ru.json";
@@ -20,7 +21,7 @@ i18n
.use(initReactI18next)
.init({
fallbackLng: "en",
supportedLngs: ["en", "de", "it", "fr", "es", "ar", "pt", "ro", "ja", "ru", "uz", "zh-Hans", "hi"],
supportedLngs: ["en", "de", "it", "fr", "es", "ar", "pt", "ro", "ja", "ru", "uz", "zh-Hans", "hi", "nl"],
resources: {
en: { translation: enTranslations },
@@ -32,6 +33,7 @@ i18n
pt: { translation: ptTranslations },
ro: { translation: roTranslations },
ja: { translation: jaTranslations },
nl: { translation: nlTranslations },
ru: { translation: ruTranslations },
uz: { translation: uzTranslations },
"zh-Hans": { translation: zhHansTranslations },

View File

@@ -5,6 +5,7 @@ export const ZUserLocale = z.enum([
"de-DE",
"pt-BR",
"fr-FR",
"nl-NL",
"zh-Hant-TW",
"pt-PT",
"ro-RO",

5351
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff