Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruwang
acd6d7185e feat: czech translations 2025-11-19 17:45:41 +05:30
16 changed files with 3195 additions and 59 deletions

View File

@@ -89,7 +89,7 @@ jobs:
- check-latest-release
with:
IS_PRERELEASE: ${{ github.event.release.prerelease }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
docker-build-cloud:
name: Build & push Formbricks Cloud to ECR
@@ -101,7 +101,7 @@ jobs:
with:
image_tag: ${{ needs.docker-build-community.outputs.VERSION }}
IS_PRERELEASE: ${{ github.event.release.prerelease }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
needs:
- check-latest-release
- docker-build-community
@@ -154,4 +154,4 @@ jobs:
release_tag: ${{ github.event.release.tag_name }}
commit_sha: ${{ github.sha }}
is_prerelease: ${{ github.event.release.prerelease }}
make_latest: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
make_latest: ${{ needs.check-latest-release.outputs.is_latest }}

View File

@@ -96,21 +96,14 @@ export const ResponsePage = ({
}
}, [searchParams, resetState]);
// Only fetch if filters are applied (not on initial mount with no filters)
const hasFilters =
selectedFilter?.responseStatus !== "all" ||
(selectedFilter?.filter && selectedFilter.filter.length > 0) ||
(dateRange.from && dateRange.to);
useEffect(() => {
const fetchFilteredResponses = async () => {
try {
// skip call for initial mount
if (page === null && !hasFilters) {
if (page === null) {
setPage(1);
return;
}
setPage(1);
setIsFetchingFirstPage(true);
let responses: TResponseWithQuotas[] = [];
@@ -133,7 +126,15 @@ export const ResponsePage = ({
setIsFetchingFirstPage(false);
}
};
fetchFilteredResponses();
// Only fetch if filters are applied (not on initial mount with no filters)
const hasFilters =
(selectedFilter && Object.keys(selectedFilter).length > 0) ||
(dateRange && (dateRange.from || dateRange.to));
if (hasFilters) {
fetchFilteredResponses();
}
}, [filters, responsesPerPage, selectedFilter, dateRange, surveyId]);
return (

View File

@@ -4,7 +4,7 @@ import clsx from "clsx";
import { ChevronDown, ChevronUp, X } from "lucide-react";
import { useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { TI18nString, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
@@ -26,8 +26,8 @@ import {
import { Input } from "@/modules/ui/components/input";
type QuestionFilterComboBoxProps = {
filterOptions: (string | TI18nString)[] | undefined;
filterComboBoxOptions: (string | TI18nString)[] | undefined;
filterOptions: string[] | undefined;
filterComboBoxOptions: string[] | undefined;
filterValue: string | undefined;
filterComboBoxValue: string | string[] | undefined;
onChangeFilterValue: (o: string) => void;
@@ -74,7 +74,7 @@ export const QuestionFilterComboBox = ({
if (!isMultiple) return filterComboBoxOptions;
return filterComboBoxOptions?.filter((o) => {
const optionValue = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
return !filterComboBoxValue?.includes(optionValue);
});
}, [isMultiple, filterComboBoxOptions, filterComboBoxValue, defaultLanguageCode]);
@@ -91,15 +91,14 @@ export const QuestionFilterComboBox = ({
const filteredOptions = useMemo(
() =>
options?.filter((o) => {
const optionValue =
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
return optionValue.toLowerCase().includes(searchQuery.toLowerCase());
}),
[options, searchQuery, defaultLanguageCode]
);
const handleCommandItemSelect = (o: string | TI18nString) => {
const value = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
const handleCommandItemSelect = (o: string) => {
const value = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
if (isMultiple) {
const newValue = Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, value] : [value];
@@ -201,18 +200,14 @@ export const QuestionFilterComboBox = ({
)}
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-white">
{filterOptions?.map((o, index) => {
const optionValue =
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
return (
<DropdownMenuItem
key={`${optionValue}-${index}`}
className="cursor-pointer"
onClick={() => onChangeFilterValue(optionValue)}>
{optionValue}
</DropdownMenuItem>
);
})}
{filterOptions?.map((o, index) => (
<DropdownMenuItem
key={`${o}-${index}`}
className="cursor-pointer"
onClick={() => onChangeFilterValue(o)}>
{o}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -274,8 +269,7 @@ export const QuestionFilterComboBox = ({
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
<CommandGroup>
{filteredOptions?.map((o) => {
const optionValue =
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o;
return (
<CommandItem
key={optionValue}

View File

@@ -4,7 +4,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { TI18nString, TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import {
SelectedFilterValue,
TResponseStatus,
@@ -13,7 +13,6 @@ import {
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { Button } from "@/modules/ui/components/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
import {
@@ -27,8 +26,8 @@ import { OptionsType, QuestionOption, QuestionsComboBox } from "./QuestionsCombo
export type QuestionFilterOptions = {
type: TSurveyQuestionTypeEnum | "Attributes" | "Tags" | "Languages" | "Quotas";
filterOptions: (string | TI18nString)[];
filterComboBoxOptions: (string | TI18nString)[];
filterOptions: string[];
filterComboBoxOptions: string[];
id: string;
};
@@ -70,12 +69,6 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [filterValue, setFilterValue] = useState<SelectedFilterValue>(selectedFilter);
const getDefaultFilterValue = (option?: QuestionFilterOptions): string | undefined => {
if (!option || option.filterOptions.length === 0) return undefined;
const firstOption = option.filterOptions[0];
return typeof firstOption === "object" ? getLocalizedValue(firstOption, "default") : firstOption;
};
useEffect(() => {
// Fetch the initial data for the filter and load it into the state
const handleInitialData = async () => {
@@ -101,18 +94,15 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
}, [isOpen, setSelectedOptions, survey]);
const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => {
const matchingFilterOption = selectedOptions.questionFilterOptions.find(
(q) => q.type === value.type || q.type === value.questionType
);
const defaultFilterValue = getDefaultFilterValue(matchingFilterOption);
if (filterValue.filter[index].questionType) {
// Create a new array and copy existing values from SelectedFilter
filterValue.filter[index] = {
questionType: value,
filterType: {
filterComboBoxValue: undefined,
filterValue: defaultFilterValue,
filterValue: selectedOptions.questionFilterOptions.find(
(q) => q.type === value.type || q.type === value.questionType
)?.filterOptions[0],
},
};
setFilterValue({ filter: [...filterValue.filter], responseStatus: filterValue.responseStatus });
@@ -121,7 +111,9 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
filterValue.filter[index].questionType = value;
filterValue.filter[index].filterType = {
filterComboBoxValue: undefined,
filterValue: defaultFilterValue,
filterValue: selectedOptions.questionFilterOptions.find(
(q) => q.type === value.type || q.type === value.questionType
)?.filterOptions[0],
};
setFilterValue({ ...filterValue });
}

View File

@@ -213,8 +213,8 @@ describe("surveys", () => {
id: "q8",
type: TSurveyQuestionTypeEnum.Matrix,
headline: { default: "Matrix" },
rows: [{ id: "r1", label: { default: "Row 1" } }],
columns: [{ id: "c1", label: { default: "Column 1" } }],
rows: [{ id: "r1", label: "Row 1" }],
columns: [{ id: "c1", label: "Column 1" }],
} as unknown as TSurveyQuestion,
],
createdAt: new Date(),

View File

@@ -121,8 +121,8 @@ export const generateQuestionAndFilterOptions = (
} else if (q.type === TSurveyQuestionTypeEnum.Matrix) {
questionFilterOptions.push({
type: q.type,
filterOptions: q.rows.map((row) => getLocalizedValue(row.label, "default")),
filterComboBoxOptions: q.columns.map((column) => getLocalizedValue(column.label, "default")),
filterOptions: q.rows.flatMap((row) => Object.values(row)),
filterComboBoxOptions: q.columns.flatMap((column) => Object.values(column)),
id: q.id,
});
} else {

View File

@@ -17,7 +17,8 @@
"zh-Hans-CN",
"zh-Hant-TW",
"nl-NL",
"es-ES"
"es-ES",
"cs-CZ"
]
},
"version": 1.8

View File

@@ -176,6 +176,7 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [
"ja-JP",
"zh-Hans-CN",
"es-ES",
"cs-CZ",
];
// Billing constants

View File

@@ -139,6 +139,7 @@ export const appLanguages = [
"zh-Hans-CN": "英语(美国)",
"nl-NL": "Engels (VS)",
"es-ES": "Inglés (EE.UU.)",
"cs-CZ": "Angličtina (USA)",
},
},
{
@@ -155,6 +156,7 @@ export const appLanguages = [
"zh-Hans-CN": "德语",
"nl-NL": "Duits",
"es-ES": "Alemán",
"cs-CZ": "Němčina",
},
},
{
@@ -171,6 +173,7 @@ export const appLanguages = [
"zh-Hans-CN": "葡萄牙语(巴西)",
"nl-NL": "Portugees (Brazilië)",
"es-ES": "Portugués (Brasil)",
"cs-CZ": "Portugalština (Brazílie)",
},
},
{
@@ -187,6 +190,7 @@ export const appLanguages = [
"zh-Hans-CN": "法语",
"nl-NL": "Frans",
"es-ES": "Francés",
"cs-CZ": "Francouzština",
},
},
{
@@ -203,6 +207,7 @@ export const appLanguages = [
"zh-Hans-CN": "繁体中文",
"nl-NL": "Chinees (Traditioneel)",
"es-ES": "Chino (Tradicional)",
"cs-CZ": "Čínština (tradiční)",
},
},
{
@@ -219,6 +224,7 @@ export const appLanguages = [
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
"nl-NL": "Portugees (Portugal)",
"es-ES": "Portugués (Portugal)",
"cs-CZ": "Portugalština (Portugalsko)",
},
},
{
@@ -235,6 +241,7 @@ export const appLanguages = [
"zh-Hans-CN": "罗马尼亚语",
"nl-NL": "Roemeens",
"es-ES": "Rumano",
"cs-CZ": "Rumunština",
},
},
{
@@ -251,6 +258,7 @@ export const appLanguages = [
"zh-Hans-CN": "日语",
"nl-NL": "Japans",
"es-ES": "Japonés",
"cs-CZ": "Japonština",
},
},
{
@@ -267,6 +275,7 @@ export const appLanguages = [
"zh-Hans-CN": "简体中文",
"nl-NL": "Chinees (Vereenvoudigd)",
"es-ES": "Chino (Simplificado)",
"cs-CZ": "Čínština (zjednodušená)",
},
},
{
@@ -283,6 +292,7 @@ export const appLanguages = [
"zh-Hans-CN": "荷兰语",
"nl-NL": "Nederlands",
"es-ES": "Neerlandés",
"cs-CZ": "Holandština",
},
},
{
@@ -299,6 +309,24 @@ export const appLanguages = [
"zh-Hans-CN": "西班牙语",
"nl-NL": "Spaans",
"es-ES": "Español",
"cs-CZ": "Španělština",
},
},
{
code: "cs-CZ",
label: {
"en-US": "Czech",
"de-DE": "Tschechisch",
"pt-BR": "Tcheco",
"fr-FR": "Tchèque",
"zh-Hant-TW": "捷克語",
"pt-PT": "Checo",
"ro-RO": "Cehă",
"ja-JP": "チェコ語",
"zh-Hans-CN": "捷克语",
"nl-NL": "Tsjechisch",
"es-ES": "Checo",
"cs-CZ": "Čeština",
},
},
];

View File

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

2929
apps/web/locales/cs-CZ.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,6 @@ export const getSurveyWithMetadata = reactCache(async (surveyId: string) => {
surveyClosedMessage: true,
showLanguageSwitch: true,
recaptcha: true,
metadata: true,
// Related data
languages: {

View File

@@ -80,7 +80,7 @@ export const SurveyInline = (props: Omit<SurveyContainerProps, "containerId">) =
loadScript();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props]);
}, []);
useEffect(() => {
if (isScriptLoaded) {

View File

@@ -223,6 +223,7 @@ vi.mock("@/lib/constants", () => ({
"ja-JP",
"zh-Hans-CN",
"es-ES",
"cs-CZ",
],
DEFAULT_LOCALE: "en-US",
BREVO_API_KEY: "mock-brevo-api-key",

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ export const ZUserLocale = z.enum([
"ja-JP",
"zh-Hans-CN",
"es-ES",
"cs-CZ",
]);
export type TUserLocale = z.infer<typeof ZUserLocale>;