Compare commits

...

15 Commits

Author SHA1 Message Date
Theodór Tómas
e75cba7098 Merge branch 'main' into feat/css-variables 2026-01-22 11:12:13 +07:00
Dhruwang Jariwala
1db1271e7f feat: validation rules (#7140) 2026-01-21 15:23:09 +00:00
Matti Nannt
9ec1964106 fix(security): upgrade react-email packages to fix transitive next.js vulnerability (#7145) 2026-01-21 16:00:33 +01:00
Dhruwang Jariwala
d5a70796dd chore: tweaked validation of ending card url (#7139)
Co-authored-by: Johannes <johannes@formbricks.com>
2026-01-21 14:41:36 +00:00
Dhruwang Jariwala
246351b3e6 fix: quotas not working for multi lang surveys (#7141) 2026-01-21 14:23:16 +00:00
Dhruwang Jariwala
22ea7302bb fix: removed validation from button labels (#7138) 2026-01-21 14:14:22 +00:00
TheodorTomas
2f4dd38057 chore: fixing rabbitai issues 2026-01-21 13:07:12 +07:00
TheodorTomas
8e12645e99 chore: adding test coverage to styles.ts 2026-01-20 23:20:11 +07:00
TheodorTomas
1e7f4411fc chore: adding more styles.test.ts unit tests 2026-01-20 23:10:21 +07:00
TheodorTomas
5ef65ac3ad chore: fix unit test 2026-01-20 22:59:16 +07:00
TheodorTomas
0e9fd70b24 chore: cleanup after self review 2026-01-20 22:38:05 +07:00
TheodorTomas
3959492f53 chore: cleanup after self review 2026-01-20 22:17:44 +07:00
Theodór Tómas
9891991374 Merge branch 'main' into feat/css-variables 2026-01-20 22:15:33 +07:00
TheodorTomas
321473a26f chore: cleanup after self review 2026-01-20 22:13:49 +07:00
TheodorTomas
c2faf39217 feat: advance css vars 2026-01-20 19:22:22 +07:00
143 changed files with 9577 additions and 1789 deletions

View File

@@ -125,6 +125,9 @@ RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
COPY --from=installer /app/node_modules/uuid ./node_modules/uuid
RUN chmod -R 755 ./node_modules/uuid
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
RUN chmod -R 755 ./node_modules/@noble/hashes

View File

@@ -8,6 +8,10 @@ import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib
import { sendToPipeline } from "@/app/lib/pipelines";
import { deleteResponse, getResponse } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import {
formatValidationErrorsForV1Api,
validateResponseData,
} from "@/modules/api/v2/management/responses/lib/validation";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import { updateResponseWithQuotaEvaluation } from "./lib/response";
@@ -140,6 +144,24 @@ export const PUT = withV1ApiWrapper({
};
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
result.survey.blocks,
responseUpdate.data,
responseUpdate.language ?? "en",
result.survey.questions
);
if (validationErrors) {
return {
response: responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
),
};
}
const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
if (!inputValidation.success) {
return {

View File

@@ -7,6 +7,10 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getSurvey } from "@/lib/survey/service";
import {
formatValidationErrorsForV1Api,
validateResponseData,
} from "@/modules/api/v2/management/responses/lib/validation";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import {
@@ -149,6 +153,24 @@ export const POST = withV1ApiWrapper({
};
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
surveyResult.survey.blocks,
responseInput.data,
responseInput.language ?? "en",
surveyResult.survey.questions
);
if (validationErrors) {
return {
response: responses.badRequestResponse(
"Validation failed",
formatValidationErrorsForV1Api(validationErrors),
true
),
};
}
if (responseInput.createdAt && !responseInput.updatedAt) {
responseInput.updatedAt = responseInput.createdAt;
}

View File

@@ -216,7 +216,6 @@ checksums:
common/imprint: c4e5f2a1994d3cc5896b200709cc499c
common/in_progress: 3de9afebcb9d4ce8ac42e14995f79ffd
common/inactive_surveys: 324b8e1844739cdc2a3bc71aef143a76
common/input_type: df4865b5d0a598a8d7f563dcec104df5
common/integration: 40d02f65c4356003e0e90ffb944907d2
common/integrations: 0ccce343287704cd90150c32e2fcad36
common/invalid_date: 4c18c82f7317d4a02f8d5fef611e82b7
@@ -240,13 +239,11 @@ checksums:
common/look_and_feel: 9125503712626d495cedec7a79f1418c
common/manage: a3d40c0267b81ae53c9598eaeb05087d
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
common/maximum: 4c07541dd1f093775bdc61b559cca6c8
common/member: 1606dc30b369856b9dba1fe9aec425d2
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
common/metadata: 695d4f7da261ba76e3be4de495491028
common/minimum: d9759235086d0169928b3c1401115e22
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
common/mobile_overlay_surveys_look_good: 6d73b635018b4a5a89cce58e1d2497f5
common/mobile_overlay_title: 42f52259b7527989fb3a3240f5352a8b
@@ -299,7 +296,7 @@ checksums:
common/placeholder: 88c2c168aff12ca70148fcb5f6b4c7b1
common/please_select_at_least_one_survey: fb1cbeb670480115305e23444c347e50
common/please_select_at_least_one_trigger: e88e64a1010a039745e80ed2e30951fe
common/please_upgrade_your_plan: bfe98d41cd7383ad42169785d8c818fc
common/please_upgrade_your_plan: 03d54a21ecd27723c72a13644837e5ed
common/preview: 3173ee1f0f1d4e50665ca4a84c38e15d
common/preview_survey: 7409e9c118e3e5d5f2a86201c2b354f2
common/privacy: 7459744a63ef8af4e517a09024bd7c08
@@ -1083,7 +1080,6 @@ checksums:
environments/surveys/edit/add_fallback_placeholder: 0e77ea487ddd7bc7fc2f1574b018dc08
environments/surveys/edit/add_hidden_field_id: a8f55b51b790cf5f4d898af7770ad1ed
environments/surveys/edit/add_highlight_border: 66f52b21fbb9aa6561c98a090abaaf8f
environments/surveys/edit/add_highlight_border_description: 1c04654a393c0fa31d2b58abb6f85b4b
environments/surveys/edit/add_logic: f234c9f1393a9ed4792dfbd15838c951
environments/surveys/edit/add_none_of_the_above: 8e8c3f404204f6ddac2f52e682153202
environments/surveys/edit/add_option: 143c54f0b201067fe5159284d6daeca2
@@ -1101,7 +1097,6 @@ checksums:
environments/surveys/edit/adjust_survey_closed_message_description: e906aebd9af6451a2a39c73287927299
environments/surveys/edit/adjust_the_theme_in_the: bccdafda8af5871513266f668b55d690
environments/surveys/edit/all_other_answers_will_continue_to: 9a5d09eea42ff5fd1c18cc58a14dcabd
environments/surveys/edit/allow_file_type: ec4f1e0c5b764990c3b1560d0d8dc2af
environments/surveys/edit/allow_multi_select: 7b4b83f7a0205e2a0a8971671a69a174
environments/surveys/edit/allow_multiple_files: dbd99f9d1026e4f7c5a5d03f71ba379d
environments/surveys/edit/allow_users_to_select_more_than_one_image: d683e0b538d1366400292a771f3fbd08
@@ -1156,19 +1151,10 @@ checksums:
environments/surveys/edit/change_background: fa71a993869f7d3ac553c547c12c3e9b
environments/surveys/edit/change_question_type: 2d555ae48df8dbedfc6a4e1ad492f4aa
environments/surveys/edit/change_survey_type: c26322043a476da6d94adb8b4efe1e93
environments/surveys/edit/change_the_background_color_of_the_card: 41d805ef753a7d1e272b48519967bbd4
environments/surveys/edit/change_the_background_color_of_the_input_fields: 4edbc9a9f5d145ed096cf5b4f8bdaac0
environments/surveys/edit/change_the_background_to_a_color_image_or_animation: f1b9c9eb61497dd91b2550dd50c77836
environments/surveys/edit/change_the_border_color_of_the_card: 64d76b247ab192343bb327f92a5f220c
environments/surveys/edit/change_the_border_color_of_the_input_fields: bb687f41af15a1dd9494c14f97b10425
environments/surveys/edit/change_the_border_radius_of_the_card_and_the_inputs: 9eccf688a7a67dfeeeed3de5209058b0
environments/surveys/edit/change_the_brand_color_of_the_survey: ecc420c641fb58daaf4d2d0086357b7f
environments/surveys/edit/change_the_placement_of_this_survey: 64359611bfb23bacc614ffe0b08fbe5d
environments/surveys/edit/change_the_question_color_of_the_survey: ab6942138a8c5fc6c8c3b9f8dd95e980
environments/surveys/edit/changes_saved: 90aab363c9e96eaa1295a997c48f97f6
environments/surveys/edit/changing_survey_type_will_remove_existing_distribution_channels: 9ce817be04f13f2f0db981145ec48df4
environments/surveys/edit/character_limit_toggle_description: d15a6895eaaf4d4c7212d9240c6bf45d
environments/surveys/edit/character_limit_toggle_title: fdc45bcc6335e5116aec895fecda0d87
environments/surveys/edit/checkbox_label: 12a07d6bdf38e283a2e95892ec49b7f8
environments/surveys/edit/choose_the_actions_which_trigger_the_survey: 773b311a148a112243f3b139506b9987
environments/surveys/edit/choose_the_first_question_on_your_block: bdece06ca04f89d0c445ba1554dd5b80
@@ -1188,7 +1174,6 @@ checksums:
environments/surveys/edit/contact_fields: 0d4e3f4d2eb3481aabe3ac60a692fa74
environments/surveys/edit/contains: 41c8c25407527a5336404313f4c8d650
environments/surveys/edit/continue_to_settings: b9853a7eedb3ae295088268fe5a44824
environments/surveys/edit/control_which_file_types_can_be_uploaded: 97144e65d91e2ca0114af923ba5924f4
environments/surveys/edit/convert_to_multiple_choice: e5396019ae897f6ec4c4295394c115e3
environments/surveys/edit/convert_to_single_choice: 8ecabfcb9276f29e6ac962ffcbc1ba64
environments/surveys/edit/country: 73581fc33a1e83e6a56db73558e7b5c6
@@ -1304,7 +1289,6 @@ checksums:
environments/surveys/edit/hide_progress_bar: 7eefe7db6a051105bded521d94204933
environments/surveys/edit/hide_question_settings: 99127cd016db2f7fc80333b36473c0ef
environments/surveys/edit/hostname: 9bdaa7692869999df51bb60d58d9ef62
environments/surveys/edit/how_funky_do_you_want_your_cards_in_survey_type_derived_surveys: 3cb16b37510c01af20a80f51b598346e
environments/surveys/edit/if_you_need_more_please: a7d208c283caf6b93800b809fca80768
environments/surveys/edit/if_you_really_want_that_answer_ask_until_you_get_it: 31c18a8c7c578db2ba49eed663d1739f
environments/surveys/edit/ignore_global_waiting_time: e08db543ace4935625e0961cc6e60489
@@ -1342,8 +1326,7 @@ checksums:
environments/surveys/edit/key: 3d1065ab98a1c2f1210507fd5c7bf515
environments/surveys/edit/last_name: 2c9a7de7738ca007ba9023c385149c26
environments/surveys/edit/let_people_upload_up_to_25_files_at_the_same_time: 44110eeba2b63049a84d69927846ea3c
environments/surveys/edit/limit_file_types: 2ee563bc98c65f565014945d6fef389c
environments/surveys/edit/limit_the_maximum_file_size: f3f8682de34eaae30351d570805ba172
environments/surveys/edit/limit_the_maximum_file_size: 6ae5944fe490b9acdaaee92b30381ec0
environments/surveys/edit/limit_upload_file_size_to: 949c48d25ae45259cc19464e95752d29
environments/surveys/edit/link_survey_description: f45569b5e6b78be6bc02bc6a46da948b
environments/surveys/edit/load_segment: 5341d3de37ff10f7526152e38e25e3c5
@@ -1389,7 +1372,6 @@ checksums:
environments/surveys/edit/picture_idx: 55e053ad1ade5d17c582406706036028
environments/surveys/edit/pin_can_only_contain_numbers: 417c854d44620a7229ebd9ab8cbb3613
environments/surveys/edit/pin_must_be_a_four_digit_number: 9f9c8c55d99f7b24fbcf6e7e377b726f
environments/surveys/edit/please_enter_a_file_extension: 60ad12bce720593482809c002a542a97
environments/surveys/edit/please_enter_a_valid_url: 25d43dfb802c31cb59dc88453ea72fc4
environments/surveys/edit/please_set_a_survey_trigger: 0358142df37dd1724f629008a1db453a
environments/surveys/edit/please_specify: e1faa6cd085144f7339c7e74dc6fb366
@@ -1400,7 +1382,6 @@ checksums:
environments/surveys/edit/protect_survey_with_pin_description: 0e55d19b6f3578b1024e03606172a5d2
environments/surveys/edit/publish: 4aa95ba4793bb293e771bd73b4f87c0f
environments/surveys/edit/question: 0576462ce60d4263d7c482463fcc9547
environments/surveys/edit/question_color: 6e69cb5699368bc68b2e1e1501f555c9
environments/surveys/edit/question_deleted: ecdeb22b81ae2d732656a7742c1eec7b
environments/surveys/edit/question_duplicated: 3f02439fd0a8b818bc84c1b1b473898c
environments/surveys/edit/question_id_updated: e8d94dbefcbad00c7464b3d1fb0ee81a
@@ -1466,6 +1447,7 @@ checksums:
environments/surveys/edit/search_for_images: 8b1bc3561d126cc49a1ee185c07e7aaf
environments/surveys/edit/seconds_after_trigger_the_survey_will_be_closed_if_no_response: 3584be059fe152e93895ef9885f8e8a7
environments/surveys/edit/seconds_before_showing_the_survey: 4b03756dd5f06df732bf62b2c7968b82
environments/surveys/edit/select_field: 45665a44f7d5707506364f17f28db3bf
environments/surveys/edit/select_or_type_value: a99c307b2cc3f9f6f893babd546d7296
environments/surveys/edit/select_ordering: c8f632a17fe78d8b7f87e82df9351ff9
environments/surveys/edit/select_saved_action: de31ab9cbb2bb67a050df717de7cdde4
@@ -1499,7 +1481,6 @@ checksums:
environments/surveys/edit/styling_set_to_theme_styles: f2c108bf422372b00cf7c87f1b042f69
environments/surveys/edit/subheading: c0f6f57155692fd8006381518ce4fef0
environments/surveys/edit/subtract: 2d83b8b9ef35110f2583ddc155b6c486
environments/surveys/edit/suggest_colors: ddc4543b416ab774007b10a3434343cd
environments/surveys/edit/survey_completed_heading: dae5ac4a02a886dc9d9fc40927091919
environments/surveys/edit/survey_completed_subheading: db537c356c3ab6564d24de0d11a0fee2
environments/surveys/edit/survey_display_settings: 8ed19e6a8e1376f7a1ba037d82c4ae11
@@ -1513,8 +1494,6 @@ checksums:
environments/surveys/edit/the_survey_will_be_shown_once_even_if_person_doesnt_respond: 6062aaa5cf8e58e79b75b6b588ae9598
environments/surveys/edit/then: 5e941fb7dd51a18651fcfb865edd5ba6
environments/surveys/edit/this_action_will_remove_all_the_translations_from_this_survey: 3340c89696f10bdc01b9a1047ff0b987
environments/surveys/edit/this_extension_is_already_added: 201d636539836c95958e28cecd8f3240
environments/surveys/edit/this_file_type_is_not_supported: f365b9a2e05aa062ab0bc1af61f642e2
environments/surveys/edit/three_points: d7f299aec752d7d690ef0ab6373327ae
environments/surveys/edit/times: 5ab156c13df6bfd75c0b17ad0a92c78a
environments/surveys/edit/to_keep_the_placement_over_all_surveys_consistent_you_can: 7a078e6a39d4c30b465137d2b6ef3e67
@@ -1535,6 +1514,46 @@ checksums:
environments/surveys/edit/upper_label: 1fa48bce3fade6ffc1a52d9fdddf9e17
environments/surveys/edit/url_filters: e524879d2eb74463d7fd06a7e0f53421
environments/surveys/edit/url_not_supported: af8a753467c617b596aadef1aaaed664
environments/surveys/edit/validation/add_validation_rule: e0c3208977140e5475df3e9b08927dbf
environments/surveys/edit/validation/answer_all_rows: 5ca73b038ac41922a09802fef4b5afc0
environments/surveys/edit/validation/characters: e26d6bb531181ec1ed551e264bc86259
environments/surveys/edit/validation/contains: 41c8c25407527a5336404313f4c8d650
environments/surveys/edit/validation/delete_validation_rule: cc92081eda4dcffd9f746c5628fa2636
environments/surveys/edit/validation/does_not_contain: d618eb0f854f7efa0d7c644e6628fa42
environments/surveys/edit/validation/email: a481cd9fba3e145252458ee1eaa9bd3b
environments/surveys/edit/validation/end_date: acbea5a9fd7a6fadf5aa1b4f47188203
environments/surveys/edit/validation/file_extension_is: c102e4962dd7b8b17faec31ecda6c9bd
environments/surveys/edit/validation/file_extension_is_not: e5067a8ad6b89cd979651c9d8ee7c614
environments/surveys/edit/validation/is: 1940eeb4f6f0189788fde5403c6e9e9a
environments/surveys/edit/validation/is_between: 5721c877c60f0005dc4ce78d4c0d3fdc
environments/surveys/edit/validation/is_earlier_than: 3829d0a060cfc2c7f5f0281a55759612
environments/surveys/edit/validation/is_greater_than: b9542ab0e0ea0ee18e82931b160b1385
environments/surveys/edit/validation/is_later_than: 315eba60c6b8ca4cb3dd95c564ada456
environments/surveys/edit/validation/is_less_than: 6109d595ba21497c59b1c91d7fd09a13
environments/surveys/edit/validation/is_not: 8c7817ecdb08e6fa92fdf3487e0c8c9d
environments/surveys/edit/validation/is_not_between: 4579a41b4e74d940eb036e13b3c63258
environments/surveys/edit/validation/kb: 476c6cddd277e93a1bb7af4a763e95dc
environments/surveys/edit/validation/max_length: 6edf9e1149c3893da102d9464138da22
environments/surveys/edit/validation/max_selections: 6edf9e1149c3893da102d9464138da22
environments/surveys/edit/validation/max_value: 6edf9e1149c3893da102d9464138da22
environments/surveys/edit/validation/mb: dbcf612f2d898197a764a442747b5c06
environments/surveys/edit/validation/min_length: 204dbf1f1b3aa34c8b981642b1694262
environments/surveys/edit/validation/min_selections: 204dbf1f1b3aa34c8b981642b1694262
environments/surveys/edit/validation/min_value: 204dbf1f1b3aa34c8b981642b1694262
environments/surveys/edit/validation/minimum_options_ranked: 2dca1fb216c977a044987c65a0ca95c9
environments/surveys/edit/validation/minimum_rows_answered: a8766a986cd73db0bb9daff49b271ed6
environments/surveys/edit/validation/options_selected: a7f72a7059a49a2a6d5b90f7a2a8aa44
environments/surveys/edit/validation/pattern: c6f01d7bc9baa21a40ea38fa986bd5a0
environments/surveys/edit/validation/phone: bcd7bd37a475ab1f80ea4c5b4d4d0bb5
environments/surveys/edit/validation/rank_all_options: a885523e9d7820c9b0529bca37e48ccc
environments/surveys/edit/validation/select_file_extensions: 208ccb7bd4dde20b0d79bdd1fa763076
environments/surveys/edit/validation/select_option: 53ba37697cca1f6c7d57ecca53ea063e
environments/surveys/edit/validation/start_date: 881de78c79b56f5ceb9b7103bf23cb2c
environments/surveys/edit/validation/url: 4006a4d8dfac013758f0053f6aa67cdd
environments/surveys/edit/validation_logic_and: 83bb027b15e28b3dc1d6e16c7fc86056
environments/surveys/edit/validation_logic_or: 32c9f3998984fd32a2b5bc53f2d97429
environments/surveys/edit/validation_rules: 0cd99f02684d633196c8b249e857d207
environments/surveys/edit/validation_rules_description: a0a7cee05e18efd462148698e3a93399
environments/surveys/edit/variable_is_used_in_logic_of_question_please_remove_it_from_logic_first: bd9d9c7cf0be671c4e8cf67e2ae6659e
environments/surveys/edit/variable_is_used_in_quota_please_remove_it_from_quota_first: 0d36e5b2713f5450fe346e0af0aaa29c
environments/surveys/edit/variable_name_is_already_taken_please_choose_another: 6da42fe8733c6379158bce9a176f76d7
@@ -1900,6 +1919,31 @@ checksums:
environments/workspace/languages/translate: 59f9803b27e2030ba7323ed239116cf7
environments/workspace/look/add_background_color: 9be512ee1246e32d3958c56097d202d9
environments/workspace/look/add_background_color_description: adb6fcb392862b3d0e9420d9b5405ddb
environments/workspace/look/advanced_styling_field_border_radius: 63b8f3541a9792d705e67d5aca7b6451
environments/workspace/look/advanced_styling_field_button_bg: fc103ab926721e6213d39cc1f913c018
environments/workspace/look/advanced_styling_field_button_text: 3304e88bcc3869f3a306634b541e1e07
environments/workspace/look/advanced_styling_field_description_color: e2f4cbc96d3f0b75837a9edc95a5eeda
environments/workspace/look/advanced_styling_field_description_size: a0d51c3ab7dc56320ecedc2b27917842
environments/workspace/look/advanced_styling_field_font_size: ca44d14429b2175a1b194793b4ab8f6b
environments/workspace/look/advanced_styling_field_font_weight: bfef83778146cf40550df9650d8a07da
environments/workspace/look/advanced_styling_field_headline_color: 4ccf3935ad90c88ad4add24f498673ce
environments/workspace/look/advanced_styling_field_headline_size: ddc49fa27fc97ed286d5c4309edd9a3c
environments/workspace/look/advanced_styling_field_headline_weight: 0c8b8262945c61f8e2978502362e0a42
environments/workspace/look/advanced_styling_field_height: f4da6d7ecd26e3fa75cfea03abb60c00
environments/workspace/look/advanced_styling_field_indicator_bg: 00febda2901af0f1b0c17e44f9917c38
environments/workspace/look/advanced_styling_field_input_text: 4999bfded16b7d0bbcc858b399745eaa
environments/workspace/look/advanced_styling_field_option_bg: 0ceaed10d99ed4ad83cb0934ab970174
environments/workspace/look/advanced_styling_field_option_label: 2767a5db32742073a01aac16488e93dc
environments/workspace/look/advanced_styling_field_padding_x: 74b440237b4ba662c9898d92e2e06217
environments/workspace/look/advanced_styling_field_padding_y: 441d777bdc1cd1e792bf9815cc937c6a
environments/workspace/look/advanced_styling_field_placeholder_opacity: fddcbc6e4fc5757aab807a6282d26627
environments/workspace/look/advanced_styling_field_shadow: 7b4af1b447ece2b19b5d7717b2e15c4e
environments/workspace/look/advanced_styling_field_track_bg: e569155b24616ba6d0a89a07bc85955c
environments/workspace/look/advanced_styling_field_track_height: 9ce57cb4583039c224a37e013efb6b8f
environments/workspace/look/advanced_styling_section_buttons: 3b44d6e2800e7bf3f133f1bce435f4c2
environments/workspace/look/advanced_styling_section_headlines: 6def704c0ac2ecb5951400c806856a41
environments/workspace/look/advanced_styling_section_inputs: 76bbeb561122a72fd3ec8c49eff7c563
environments/workspace/look/advanced_styling_section_options: a92819a15bc8c3eb44bdd82a5075c9e2
environments/workspace/look/app_survey_placement: f09cddac6bbb77d4694df223c6edf6b6
environments/workspace/look/app_survey_placement_settings_description: d81bcff7a866a2f83ff76936dbad4770
environments/workspace/look/centered_modal_overlay_color: 1124ba61ee2ecb18a7175ff780dc3b60
@@ -1913,6 +1957,9 @@ checksums:
environments/workspace/look/formbricks_branding_hidden: fda9ba81f8d7fdaacf8dc1642034e145
environments/workspace/look/formbricks_branding_settings_description: 5bb39206c6412c703895593f465a01f9
environments/workspace/look/formbricks_branding_shown: 6c9861cf8f95e8a68c5c64b2630d96cd
environments/workspace/look/generate_theme_btn: 0345bf322c191e70d01fd6607ec5c2f8
environments/workspace/look/generate_theme_confirmation: f119dbb85fb2bda1c0bcdc581724ef3b
environments/workspace/look/generate_theme_header: 4df5f30a20cf78e248465915f222fd1b
environments/workspace/look/logo_removed_successfully: f3a7f9d226affa91121e90ff360553aa
environments/workspace/look/logo_settings_description: da155953f55cb44d0e563d9e740241aa
environments/workspace/look/logo_updated_successfully: 170250f18062b79be6ac0481ec9d4368
@@ -1927,6 +1974,7 @@ checksums:
environments/workspace/look/show_formbricks_branding_in: 80fabfec9b34a13c0445d02b923216ed
environments/workspace/look/show_powered_by_formbricks: 02b84acc3156de24e1aff8321d77603f
environments/workspace/look/styling_updated_successfully: b8b74b50dde95abcd498633e9d0c891f
environments/workspace/look/suggest_colors: ddc4543b416ab774007b10a3434343cd
environments/workspace/look/theme: 21fe00b7a518089576fb83c08631107a
environments/workspace/look/theme_settings_description: 9fc45322818c3774ab4a44ea14d7836e
environments/workspace/tags/add: 87c4a663507f2bcbbf79934af8164e13

View File

@@ -11,6 +11,43 @@ export const COLOR_DEFAULTS = {
highlightBorderColor: "#64748b",
} as const;
export const ADVANCED_DEFAULTS = {
accentBgColor: "#e2e8f0",
accentBgColorSelected: "#f1f5f9",
buttonBgColor: "#0f172a",
buttonTextColor: "#f8fafc",
buttonBorderRadius: 10,
buttonHeight: 36,
buttonFontSize: 14,
buttonFontWeight: "500",
buttonPaddingX: 16,
buttonPaddingY: 8,
inputBgColor: "#f8fafc",
inputBorderColor: "#64748b",
inputTextColor: "#0f172a",
inputBorderRadius: 10,
inputHeight: 40,
inputFontSize: 14,
inputPaddingX: 16,
inputPaddingY: 16,
inputPlaceholderOpacity: 0.5,
inputShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
optionBgColor: "#f8fafc",
optionLabelColor: "#0f172a",
optionBorderRadius: 10,
optionPaddingX: 16,
optionPaddingY: 16,
optionFontSize: 14,
elementHeadlineFontSize: 16,
elementHeadlineFontWeight: "400",
elementHeadlineColor: "#0f172a",
elementDescriptionFontSize: 14,
elementDescriptionColor: "#0f172a",
progressTrackHeight: 8,
progressTrackBgColor: "#0f172a33",
progressIndicatorBgColor: "#0f172a",
} as const;
export const defaultStyling: TProjectStyling = {
allowStyleOverwrite: true,
brandColor: {
@@ -39,4 +76,58 @@ export const defaultStyling: TProjectStyling = {
linkSurveys: "straight",
appSurveys: "straight",
},
accentBgColor: {
light: ADVANCED_DEFAULTS.accentBgColor,
},
accentBgColorSelected: {
light: ADVANCED_DEFAULTS.accentBgColorSelected,
},
buttonBgColor: {
light: ADVANCED_DEFAULTS.buttonBgColor,
},
buttonTextColor: {
light: ADVANCED_DEFAULTS.buttonTextColor,
},
buttonBorderRadius: ADVANCED_DEFAULTS.buttonBorderRadius,
buttonHeight: ADVANCED_DEFAULTS.buttonHeight,
buttonFontSize: ADVANCED_DEFAULTS.buttonFontSize,
buttonFontWeight: ADVANCED_DEFAULTS.buttonFontWeight,
buttonPaddingX: ADVANCED_DEFAULTS.buttonPaddingX,
buttonPaddingY: ADVANCED_DEFAULTS.buttonPaddingY,
inputTextColor: {
light: ADVANCED_DEFAULTS.inputTextColor,
},
inputBorderRadius: ADVANCED_DEFAULTS.inputBorderRadius,
inputHeight: ADVANCED_DEFAULTS.inputHeight,
inputFontSize: ADVANCED_DEFAULTS.inputFontSize,
inputPaddingX: ADVANCED_DEFAULTS.inputPaddingX,
inputPaddingY: ADVANCED_DEFAULTS.inputPaddingY,
inputPlaceholderOpacity: ADVANCED_DEFAULTS.inputPlaceholderOpacity,
inputShadow: ADVANCED_DEFAULTS.inputShadow,
optionBgColor: {
light: ADVANCED_DEFAULTS.optionBgColor,
},
optionLabelColor: {
light: ADVANCED_DEFAULTS.optionLabelColor,
},
optionBorderRadius: ADVANCED_DEFAULTS.optionBorderRadius,
optionPaddingX: ADVANCED_DEFAULTS.optionPaddingX,
optionPaddingY: ADVANCED_DEFAULTS.optionPaddingY,
optionFontSize: ADVANCED_DEFAULTS.optionFontSize,
elementHeadlineFontSize: ADVANCED_DEFAULTS.elementHeadlineFontSize,
elementHeadlineFontWeight: ADVANCED_DEFAULTS.elementHeadlineFontWeight,
elementHeadlineColor: {
light: ADVANCED_DEFAULTS.elementHeadlineColor,
},
elementDescriptionFontSize: ADVANCED_DEFAULTS.elementDescriptionFontSize,
elementDescriptionColor: {
light: ADVANCED_DEFAULTS.elementDescriptionColor,
},
progressTrackHeight: ADVANCED_DEFAULTS.progressTrackHeight,
progressTrackBgColor: {
light: ADVANCED_DEFAULTS.progressTrackBgColor,
},
progressIndicatorBgColor: {
light: ADVANCED_DEFAULTS.progressIndicatorBgColor,
},
};

View File

@@ -2,8 +2,8 @@ import { createInstance } from "i18next";
import ICU from "i18next-icu";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { DEFAULT_LOCALE } from "@/lib/constants";
import { TUserLocale } from "@formbricks/types/user";
import { DEFAULT_LOCALE } from "@/lib/constants";
import { getLocale } from "@/lingodotdev/language";
const initI18next = async (lng: string) => {

View File

@@ -243,7 +243,6 @@
"imprint": "Impressum",
"in_progress": "Im Gange",
"inactive_surveys": "Inaktive Umfragen",
"input_type": "Eingabetyp",
"integration": "Integration",
"integrations": "Integrationen",
"invalid_date": "Ungültiges Datum",
@@ -267,13 +266,11 @@
"look_and_feel": "Darstellung",
"manage": "Verwalten",
"marketing": "Marketing",
"maximum": "Maximal",
"member": "Mitglied",
"members": "Mitglieder",
"members_and_teams": "Mitglieder & Teams",
"membership_not_found": "Mitgliedschaft nicht gefunden",
"metadata": "Metadaten",
"minimum": "Minimum",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funktioniert am besten auf einem größeren Bildschirm. Um Umfragen zu verwalten oder zu erstellen, wechsle zu einem anderen Gerät.",
"mobile_overlay_surveys_look_good": "Keine Sorge deine Umfragen sehen auf jedem Gerät und jeder Bildschirmgröße großartig aus!",
"mobile_overlay_title": "Oops, Bildschirm zu klein erkannt!",
@@ -326,7 +323,7 @@
"placeholder": "Platzhalter",
"please_select_at_least_one_survey": "Bitte wähle mindestens eine Umfrage aus",
"please_select_at_least_one_trigger": "Bitte wähle mindestens einen Auslöser aus",
"please_upgrade_your_plan": "Bitte upgrade deinen Plan.",
"please_upgrade_your_plan": "Bitte aktualisieren Sie Ihren Plan",
"preview": "Vorschau",
"preview_survey": "Umfragevorschau",
"privacy": "Datenschutz",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Platzhalter hinzufügen, falls kein Wert zur Verfügung steht.",
"add_hidden_field_id": "Verstecktes Feld ID hinzufügen",
"add_highlight_border": "Rahmen hinzufügen",
"add_highlight_border_description": "Füge deiner Umfragekarte einen äußeren Rahmen hinzu.",
"add_logic": "Logik hinzufügen",
"add_none_of_the_above": "Füge \"Keine der oben genannten Optionen\" hinzu",
"add_option": "Option hinzufügen",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Ändere die Nachricht, die Besucher sehen, wenn die Umfrage geschlossen ist.",
"adjust_the_theme_in_the": "Passe das Thema an in den",
"all_other_answers_will_continue_to": "Alle anderen Antworten werden weiterhin",
"allow_file_type": "Dateityp begrenzen",
"allow_multi_select": "Mehrfachauswahl erlauben",
"allow_multiple_files": "Mehrere Dateien zulassen",
"allow_users_to_select_more_than_one_image": "Erlaube Nutzern, mehr als ein Bild auszuwählen",
@@ -1227,19 +1222,10 @@
"change_background": "Hintergrund ändern",
"change_question_type": "Fragetyp ändern",
"change_survey_type": "Die Änderung des Umfragetypen kann vorhandenen Zugriff beeinträchtigen",
"change_the_background_color_of_the_card": "Hintergrundfarbe der Karte ändern.",
"change_the_background_color_of_the_input_fields": "Hintergrundfarbe der Eingabefelder ändern.",
"change_the_background_to_a_color_image_or_animation": "Hintergrund zu einer Farbe, einem Bild oder einer Animation ändern.",
"change_the_border_color_of_the_card": "Randfarbe der Karte ändern.",
"change_the_border_color_of_the_input_fields": "Randfarbe der Eingabefelder ändern.",
"change_the_border_radius_of_the_card_and_the_inputs": "Radius der Ränder der Karte und der Eingabefelder ändern.",
"change_the_brand_color_of_the_survey": "Markenfarbe der Umfrage ändern.",
"change_the_placement_of_this_survey": "Platzierung dieser Umfrage ändern.",
"change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.",
"changes_saved": "Änderungen gespeichert.",
"changing_survey_type_will_remove_existing_distribution_channels": "\"Das Ändern des Umfragetypen beeinflusst, wie er geteilt werden kann. Wenn Teilnehmer bereits Zugriffslinks für den aktuellen Typ haben, könnten sie das Zugriffsrecht nach dem Wechsel verlieren.\"",
"character_limit_toggle_description": "Begrenzen Sie, wie kurz oder lang eine Antwort sein kann.",
"character_limit_toggle_title": "Fügen Sie Zeichenbeschränkungen hinzu",
"checkbox_label": "Checkbox-Beschriftung",
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
"choose_the_first_question_on_your_block": "Wählen sie die erste frage in ihrem block",
@@ -1259,7 +1245,6 @@
"contact_fields": "Kontaktfelder",
"contains": "enthält",
"continue_to_settings": "Weiter zu den Einstellungen",
"control_which_file_types_can_be_uploaded": "Steuere, welche Dateitypen hochgeladen werden können.",
"convert_to_multiple_choice": "In Multiple-Choice umwandeln",
"convert_to_single_choice": "In Einzelauswahl umwandeln",
"country": "Land",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "Frageeinstellungen ausblenden",
"hostname": "Hostname",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein",
"if_you_need_more_please": "Wenn Du mehr brauchst, bitte",
"if_you_need_more_please": "Wenn Sie mehr benötigen, bitte",
"if_you_really_want_that_answer_ask_until_you_get_it": "Weiterhin anzeigen, wenn ausgelöst, bis eine Antwort abgegeben wird.",
"ignore_global_waiting_time": "Abkühlphase ignorieren",
"ignore_global_waiting_time_description": "Diese Umfrage kann angezeigt werden, wenn ihre Bedingungen erfüllt sind, auch wenn kürzlich eine andere Umfrage angezeigt wurde.",
@@ -1413,9 +1398,8 @@
"key": "Schlüssel",
"last_name": "Nachname",
"let_people_upload_up_to_25_files_at_the_same_time": "Erlaube bis zu 25 Dateien gleichzeitig hochzuladen.",
"limit_file_types": "Dateitypen einschränken",
"limit_the_maximum_file_size": "Maximale Dateigröße begrenzen",
"limit_upload_file_size_to": "Maximale Dateigröße für Uploads",
"limit_the_maximum_file_size": "Begrenzen Sie die maximale Dateigröße für Uploads.",
"limit_upload_file_size_to": "Upload-Dateigröße begrenzen auf",
"link_survey_description": "Teile einen Link zu einer Umfrageseite oder bette ihn in eine Webseite oder E-Mail ein.",
"load_segment": "Segment laden",
"logic_error_warning": "Änderungen werden zu Logikfehlern führen",
@@ -1427,8 +1411,8 @@
"manage_languages": "Sprachen verwalten",
"matrix_all_fields": "Alle Felder",
"matrix_rows": "Zeilen",
"max_file_size": "Max. Dateigröße",
"max_file_size_limit_is": "Max. Dateigröße ist",
"max_file_size": "Maximale Dateigröße",
"max_file_size_limit_is": "Die maximale Dateigrößenbeschränkung beträgt",
"move_question_to_block": "Frage in Block verschieben",
"multiply": "Multiplizieren *",
"needed_for_self_hosted_cal_com_instance": "Benötigt für eine selbstgehostete Cal.com-Instanz",
@@ -1460,7 +1444,6 @@
"picture_idx": "Bild {idx}",
"pin_can_only_contain_numbers": "PIN darf nur Zahlen enthalten.",
"pin_must_be_a_four_digit_number": "Die PIN muss eine vierstellige Zahl sein.",
"please_enter_a_file_extension": "Bitte gib eine Dateierweiterung ein.",
"please_enter_a_valid_url": "Bitte geben Sie eine gültige URL ein (z. B. https://beispiel.de)",
"please_set_a_survey_trigger": "Bitte richte einen Umfrage-Trigger ein",
"please_specify": "Bitte angeben",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "Nur Benutzer, die die PIN haben, können auf die Umfrage zugreifen.",
"publish": "Veröffentlichen",
"question": "Frage",
"question_color": "Fragefarbe",
"question_deleted": "Frage gelöscht.",
"question_duplicated": "Frage dupliziert.",
"question_id_updated": "Frage-ID aktualisiert",
@@ -1539,6 +1521,7 @@
"search_for_images": "Nach Bildern suchen",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "Sekunden nach dem Auslösen wird die Umfrage geschlossen, wenn keine Antwort erfolgt.",
"seconds_before_showing_the_survey": "Sekunden, bevor die Umfrage angezeigt wird.",
"select_field": "Feld auswählen",
"select_or_type_value": "Auswählen oder Wert eingeben",
"select_ordering": "Anordnung auswählen",
"select_saved_action": "Gespeicherte Aktion auswählen",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "Styling auf Themenstile eingestellt",
"subheading": "Zwischenüberschrift",
"subtract": "Subtrahieren -",
"suggest_colors": "Farben vorschlagen",
"survey_completed_heading": "Umfrage abgeschlossen",
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Einmal anzeigen, auch wenn sie nicht antworten.",
"then": "dann",
"this_action_will_remove_all_the_translations_from_this_survey": "Diese Aktion entfernt alle Übersetzungen aus dieser Umfrage.",
"this_extension_is_already_added": "Diese Erweiterung ist bereits hinzugefügt.",
"this_file_type_is_not_supported": "Dieser Dateityp wird nicht unterstützt.",
"three_points": "3 Punkte",
"times": "Zeiten",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Um die Platzierung über alle Umfragen hinweg konsistent zu halten, kannst du",
@@ -1608,6 +1588,48 @@
"upper_label": "Oberes Label",
"url_filters": "URL-Filter",
"url_not_supported": "URL nicht unterstützt",
"validation": {
"add_validation_rule": "Validierungsregel hinzufügen",
"answer_all_rows": "Alle Zeilen beantworten",
"characters": "Zeichen",
"contains": "enthält",
"delete_validation_rule": "Validierungsregel löschen",
"does_not_contain": "enthält nicht",
"email": "Ist gültige E-Mail",
"end_date": "Enddatum",
"file_extension_is": "Dateierweiterung ist",
"file_extension_is_not": "Dateierweiterung ist nicht",
"is": "ist",
"is_between": "ist zwischen",
"is_earlier_than": "ist früher als",
"is_greater_than": "ist größer als",
"is_later_than": "ist später als",
"is_less_than": "ist weniger als",
"is_not": "ist nicht",
"is_not_between": "ist nicht zwischen",
"kb": "KB",
"max_length": "Höchstens",
"max_selections": "Höchstens",
"max_value": "Höchstens",
"mb": "MB",
"min_length": "Mindestens",
"min_selections": "Mindestens",
"min_value": "Mindestens",
"minimum_options_ranked": "Mindestanzahl bewerteter Optionen",
"minimum_rows_answered": "Mindestanzahl beantworteter Zeilen",
"options_selected": "Optionen ausgewählt",
"pattern": "Entspricht Regex-Muster",
"phone": "Ist gültige Telefonnummer",
"rank_all_options": "Alle Optionen bewerten",
"select_file_extensions": "Dateierweiterungen auswählen...",
"select_option": "Option auswählen",
"start_date": "Startdatum",
"url": "Ist gültige URL"
},
"validation_logic_and": "Alle sind wahr",
"validation_logic_or": "mindestens eine ist wahr",
"validation_rules": "Validierungsregeln",
"validation_rules_description": "Nur Antworten akzeptieren, die die folgenden Kriterien erfüllen",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" wird in der \"{quotaName}\" Quote verwendet",
"variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "Hintergrundfarbe hinzufügen",
"add_background_color_description": "Füge dem Logo-Container eine Hintergrundfarbe hinzu.",
"advanced_styling_field_border_radius": "Rahmenradius",
"advanced_styling_field_button_bg": "Button-Hintergrund",
"advanced_styling_field_button_text": "Button-Text",
"advanced_styling_field_description_color": "Beschreibungsfarbe",
"advanced_styling_field_description_size": "Schriftgröße der Beschreibung",
"advanced_styling_field_font_size": "Schriftgröße",
"advanced_styling_field_font_weight": "Schriftstärke",
"advanced_styling_field_headline_color": "Überschriftenfarbe",
"advanced_styling_field_headline_size": "Schriftgröße der Überschrift",
"advanced_styling_field_headline_weight": "Schriftstärke der Überschrift",
"advanced_styling_field_height": "Höhe",
"advanced_styling_field_indicator_bg": "Indikator-Hintergrund",
"advanced_styling_field_input_text": "Eingabefeld-Text",
"advanced_styling_field_option_bg": "Hintergrund",
"advanced_styling_field_option_label": "Beschriftungsfarbe",
"advanced_styling_field_padding_x": "Innenabstand X",
"advanced_styling_field_padding_y": "Innenabstand Y",
"advanced_styling_field_placeholder_opacity": "Platzhalter-Deckkraft",
"advanced_styling_field_shadow": "Schatten",
"advanced_styling_field_track_bg": "Track-Hintergrund",
"advanced_styling_field_track_height": "Track-Höhe",
"advanced_styling_section_buttons": "Buttons",
"advanced_styling_section_headlines": "Überschriften & Beschreibungen",
"advanced_styling_section_inputs": "Eingabefelder",
"advanced_styling_section_options": "Optionen (Radio/Checkbox)",
"app_survey_placement": "Platzierung der App-Umfrage",
"app_survey_placement_settings_description": "Ändere, wo Umfragen in deiner Web-App oder Website angezeigt werden.",
"centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Formbricks-Branding ist ausgeblendet.",
"formbricks_branding_settings_description": "Wir freuen uns über deine Unterstützung, haben aber Verständnis, wenn du es ausschaltest.",
"formbricks_branding_shown": "Formbricks-Branding wird angezeigt.",
"generate_theme_btn": "Generieren",
"generate_theme_confirmation": "Möchten Sie ein passendes Farbschema basierend auf Ihrer Markenfarbe generieren? Dies überschreibt Ihre aktuellen Farbeinstellungen.",
"generate_theme_header": "Farbschema generieren?",
"logo_removed_successfully": "Logo erfolgreich entfernt",
"logo_settings_description": "Lade dein Firmenlogo hoch, um Umfragen und Link-Vorschauen zu branden.",
"logo_updated_successfully": "Logo erfolgreich aktualisiert",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "Formbricks-Branding in {type}-Umfragen anzeigen",
"show_powered_by_formbricks": "\"Powered by Formbricks\"-Signatur anzeigen",
"styling_updated_successfully": "Styling erfolgreich aktualisiert",
"suggest_colors": "Farben vorschlagen",
"theme": "Theme",
"theme_settings_description": "Erstelle ein Style-Theme für alle Umfragen. Du kannst für jede Umfrage individuelles Styling aktivieren."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Imprint",
"in_progress": "In Progress",
"inactive_surveys": "Inactive surveys",
"input_type": "Input type",
"integration": "integration",
"integrations": "Integrations",
"invalid_date": "Invalid date",
@@ -267,13 +266,11 @@
"look_and_feel": "Look & Feel",
"manage": "Manage",
"marketing": "Marketing",
"maximum": "Maximum",
"member": "Member",
"members": "Members",
"members_and_teams": "Members & Teams",
"membership_not_found": "Membership not found",
"metadata": "Metadata",
"minimum": "Minimum",
"mobile_overlay_app_works_best_on_desktop": "Formbricks works best on a bigger screen. To manage or build surveys, switch to another device.",
"mobile_overlay_surveys_look_good": "Don't worry your surveys look great on every device and screen size!",
"mobile_overlay_title": "Oops, tiny screen detected!",
@@ -326,7 +323,7 @@
"placeholder": "Placeholder",
"please_select_at_least_one_survey": "Please select at least one survey",
"please_select_at_least_one_trigger": "Please select at least one trigger",
"please_upgrade_your_plan": "Please upgrade your plan.",
"please_upgrade_your_plan": "Please upgrade your plan",
"preview": "Preview",
"preview_survey": "Preview Survey",
"privacy": "Privacy Policy",
@@ -1154,7 +1151,7 @@
"add_fallback_placeholder": "Add a placeholder to show if there is no value to recall.",
"add_hidden_field_id": "Add hidden field ID",
"add_highlight_border": "Add highlight border",
"add_highlight_border_description": "Add an outer border to your survey card.",
"add_logic": "Add logic",
"add_none_of_the_above": "Add \"None of the Above\"",
"add_option": "Add option",
@@ -1172,7 +1169,6 @@
"adjust_survey_closed_message_description": "Change the message visitors see when the survey is closed.",
"adjust_the_theme_in_the": "Adjust the theme in the",
"all_other_answers_will_continue_to": "All other answers will continue to",
"allow_file_type": "Allow file type",
"allow_multi_select": "Allow multi-select",
"allow_multiple_files": "Allow multiple files",
"allow_users_to_select_more_than_one_image": "Allow users to select more than one image",
@@ -1227,19 +1223,13 @@
"change_background": "Change background",
"change_question_type": "Change question type",
"change_survey_type": "Switching survey type affects existing access",
"change_the_background_color_of_the_card": "Change the background color of the card.",
"change_the_background_color_of_the_input_fields": "Change the background color of the input fields.",
"change_the_background_to_a_color_image_or_animation": "Change the background to a color, image or animation.",
"change_the_border_color_of_the_card": "Change the border color of the card.",
"change_the_border_color_of_the_input_fields": "Change the border color of the input fields.",
"change_the_border_radius_of_the_card_and_the_inputs": "Change the border radius of the card and the inputs.",
"change_the_brand_color_of_the_survey": "Change the brand color of the survey.",
"change_the_placement_of_this_survey": "Change the placement of this survey.",
"change_the_question_color_of_the_survey": "Change the question color of the survey.",
"changes_saved": "Changes saved.",
"changing_survey_type_will_remove_existing_distribution_channels": "Changing the survey type will affect how it can be shared. If respondents already have access links for the current type, they may lose access after the switch.",
"character_limit_toggle_description": "Limit how short or long an answer can be.",
"character_limit_toggle_title": "Add character limits",
"checkbox_label": "Checkbox Label",
"choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.",
"choose_the_first_question_on_your_block": "Choose the first question on your Block",
@@ -1259,7 +1249,6 @@
"contact_fields": "Contact Fields",
"contains": "Contains",
"continue_to_settings": "Continue to Settings",
"control_which_file_types_can_be_uploaded": "Control which file types can be uploaded.",
"convert_to_multiple_choice": "Convert to Multi-select",
"convert_to_single_choice": "Convert to Single-select",
"country": "Country",
@@ -1375,7 +1364,7 @@
"hide_progress_bar": "Hide progress bar",
"hide_question_settings": "Hide Question settings",
"hostname": "Hostname",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys",
"if_you_need_more_please": "If you need more, please",
"if_you_really_want_that_answer_ask_until_you_get_it": "Keep showing whenever triggered until a response is submitted.",
"ignore_global_waiting_time": "Ignore Cooldown Period",
@@ -1413,8 +1402,7 @@
"key": "Key",
"last_name": "Last Name",
"let_people_upload_up_to_25_files_at_the_same_time": "Let people upload up to 25 files at the same time.",
"limit_file_types": "Limit file types",
"limit_the_maximum_file_size": "Limit the maximum file size",
"limit_the_maximum_file_size": "Limit the maximum file size for uploads.",
"limit_upload_file_size_to": "Limit upload file size to",
"link_survey_description": "Share a link to a survey page or embed it in a web page or email.",
"load_segment": "Load segment",
@@ -1460,7 +1448,6 @@
"picture_idx": "Picture {idx}",
"pin_can_only_contain_numbers": "PIN can only contain numbers.",
"pin_must_be_a_four_digit_number": "PIN must be a four digit number.",
"please_enter_a_file_extension": "Please enter a file extension.",
"please_enter_a_valid_url": "Please enter a valid URL (e.g., https://example.com)",
"please_set_a_survey_trigger": "Please set a survey trigger",
"please_specify": "Please specify",
@@ -1471,7 +1458,7 @@
"protect_survey_with_pin_description": "Only users who have the PIN can access the survey.",
"publish": "Publish",
"question": "Question",
"question_color": "Question color",
"question_deleted": "Question deleted.",
"question_duplicated": "Question duplicated.",
"question_id_updated": "Question ID updated",
@@ -1539,6 +1526,7 @@
"search_for_images": "Search for images",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "seconds after trigger the survey will be closed if no response",
"seconds_before_showing_the_survey": "seconds before showing the survey.",
"select_field": "Select field",
"select_or_type_value": "Select or type value",
"select_ordering": "Select ordering",
"select_saved_action": "Select saved action",
@@ -1572,7 +1560,7 @@
"styling_set_to_theme_styles": "Styling set to theme styles",
"subheading": "Subheading",
"subtract": "Subtract -",
"suggest_colors": "Suggest colors",
"survey_completed_heading": "Survey Completed",
"survey_completed_subheading": "This free & open-source survey has been closed",
"survey_display_settings": "Survey Display Settings",
@@ -1586,8 +1574,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Show a single time, even if they don't respond.",
"then": "Then",
"this_action_will_remove_all_the_translations_from_this_survey": "This action will remove all the translations from this survey.",
"this_extension_is_already_added": "This extension is already added.",
"this_file_type_is_not_supported": "This file type is not supported.",
"three_points": "3 points",
"times": "times",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "To keep the placement over all surveys consistent, you can",
@@ -1608,6 +1594,48 @@
"upper_label": "Upper Label",
"url_filters": "URL Filters",
"url_not_supported": "URL not supported",
"validation": {
"add_validation_rule": "Add validation rule",
"answer_all_rows": "Answer all rows",
"characters": "Characters",
"contains": "Contains",
"delete_validation_rule": "Delete validation rule",
"does_not_contain": "Does not contain",
"email": "Is valid email",
"end_date": "End date",
"file_extension_is": "File extension is",
"file_extension_is_not": "File extension is not",
"is": "Is",
"is_between": "Is between",
"is_earlier_than": "Is earlier than",
"is_greater_than": "Is greater than",
"is_later_than": "Is later than",
"is_less_than": "Is less than",
"is_not": "Is not",
"is_not_between": "Is not between",
"kb": "KB",
"max_length": "At most",
"max_selections": "At most",
"max_value": "At most",
"mb": "MB",
"min_length": "At least",
"min_selections": "At least",
"min_value": "At least",
"minimum_options_ranked": "Minimum options ranked",
"minimum_rows_answered": "Minimum rows answered",
"options_selected": "Options selected",
"pattern": "Matches regex pattern",
"phone": "Is valid phone",
"rank_all_options": "Rank all options",
"select_file_extensions": "Select file extensions...",
"select_option": "Select option",
"start_date": "Start date",
"url": "Is valid URL"
},
"validation_logic_and": "All are true",
"validation_logic_or": "any is true",
"validation_rules": "Validation rules",
"validation_rules_description": "Only accept responses that meet the following criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} is used in logic of question {questionIndex}. Please remove it from logic first.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" is being used in \"{quotaName}\" quota",
"variable_name_is_already_taken_please_choose_another": "Variable name is already taken, please choose another.",
@@ -2015,6 +2043,37 @@
"look": {
"add_background_color": "Add background color",
"add_background_color_description": "Add a background color to the logo container.",
"advanced_styling_field_border_radius": "Border Radius",
"advanced_styling_field_button_bg": "Button Background",
"advanced_styling_field_button_text": "Button Text",
"advanced_styling_field_description_color": "Description Color",
"advanced_styling_field_description_size": "Description Font Size",
"advanced_styling_field_font_size": "Font Size",
"advanced_styling_field_font_weight": "Font Weight",
"advanced_styling_field_headline_color": "Headline Color",
"advanced_styling_field_headline_size": "Headline Font Size",
"advanced_styling_field_headline_weight": "Headline Font Weight",
"advanced_styling_field_height": "Height",
"advanced_styling_field_indicator_bg": "Indicator Background",
"advanced_styling_field_input_text": "Input Text",
"advanced_styling_field_option_bg": "Background",
"advanced_styling_field_option_label": "Label Color",
"advanced_styling_field_padding_x": "Padding X",
"advanced_styling_field_padding_y": "Padding Y",
"advanced_styling_field_placeholder_opacity": "Placeholder Opacity",
"advanced_styling_field_shadow": "Shadow",
"advanced_styling_field_track_bg": "Track Background",
"advanced_styling_field_track_height": "Track Height",
"advanced_styling_section_buttons": "Buttons",
"advanced_styling_section_headlines": "Headlines & Descriptions",
"advanced_styling_section_inputs": "Inputs",
"advanced_styling_section_options": "Options (Radio/Checkbox)",
"app_survey_placement": "App Survey Placement",
"app_survey_placement_settings_description": "Change where surveys will be shown in your web app or website.",
"centered_modal_overlay_color": "Centered modal overlay color",
@@ -2028,6 +2087,9 @@
"formbricks_branding_hidden": "Formbricks branding is hidden.",
"formbricks_branding_settings_description": "We love your support but understand if you toggle it off.",
"formbricks_branding_shown": "Formbricks branding is shown.",
"generate_theme_btn": "Generate",
"generate_theme_confirmation": "Would you like to generate a matching color theme based on your brand color? This will overwrite your current color settings.",
"generate_theme_header": "Generate Color Theme?",
"logo_removed_successfully": "Logo removed successfully",
"logo_settings_description": "Upload your company logo to brand surveys and link previews.",
"logo_updated_successfully": "Logo updated successfully",
@@ -2042,6 +2104,7 @@
"show_formbricks_branding_in": "Show Formbricks Branding in {type} surveys",
"show_powered_by_formbricks": "Show 'Powered by Formbricks' Signature",
"styling_updated_successfully": "Styling updated successfully",
"suggest_colors": "Suggest colors",
"theme": "Theme",
"theme_settings_description": "Create a style theme for all surveys. You can enable custom styling for each survey."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Aviso legal",
"in_progress": "En progreso",
"inactive_surveys": "Encuestas inactivas",
"input_type": "Tipo de entrada",
"integration": "integración",
"integrations": "Integraciones",
"invalid_date": "Fecha no válida",
@@ -267,13 +266,11 @@
"look_and_feel": "Apariencia",
"manage": "Gestionar",
"marketing": "Marketing",
"maximum": "Máximo",
"member": "Miembro",
"members": "Miembros",
"members_and_teams": "Miembros y equipos",
"membership_not_found": "Membresía no encontrada",
"metadata": "Metadatos",
"minimum": "Mínimo",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona mejor en una pantalla más grande. Para gestionar o crear encuestas, cambia a otro dispositivo.",
"mobile_overlay_surveys_look_good": "No te preocupes ¡tus encuestas se ven geniales en todos los dispositivos y tamaños de pantalla!",
"mobile_overlay_title": "¡Ups, pantalla pequeña detectada!",
@@ -326,7 +323,7 @@
"placeholder": "Marcador de posición",
"please_select_at_least_one_survey": "Por favor, selecciona al menos una encuesta",
"please_select_at_least_one_trigger": "Por favor, selecciona al menos un disparador",
"please_upgrade_your_plan": "Por favor, actualiza tu plan.",
"please_upgrade_your_plan": "Por favor, actualiza tu plan",
"preview": "Vista previa",
"preview_survey": "Vista previa de la encuesta",
"privacy": "Política de privacidad",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Añadir un marcador de posición para mostrar si no hay valor que recuperar.",
"add_hidden_field_id": "Añadir ID de campo oculto",
"add_highlight_border": "Añadir borde destacado",
"add_highlight_border_description": "Añadir un borde exterior a tu tarjeta de encuesta.",
"add_logic": "Añadir lógica",
"add_none_of_the_above": "Añadir \"Ninguna de las anteriores\"",
"add_option": "Añadir opción",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Cambiar el mensaje que ven los visitantes cuando la encuesta está cerrada.",
"adjust_the_theme_in_the": "Ajustar el tema en el",
"all_other_answers_will_continue_to": "Todas las demás respuestas continuarán",
"allow_file_type": "Permitir tipo de archivo",
"allow_multi_select": "Permitir selección múltiple",
"allow_multiple_files": "Permitir múltiples archivos",
"allow_users_to_select_more_than_one_image": "Permitir a los usuarios seleccionar más de una imagen",
@@ -1227,19 +1222,10 @@
"change_background": "Cambiar fondo",
"change_question_type": "Cambiar tipo de pregunta",
"change_survey_type": "Cambiar el tipo de encuesta afecta al acceso existente",
"change_the_background_color_of_the_card": "Cambiar el color de fondo de la tarjeta.",
"change_the_background_color_of_the_input_fields": "Cambiar el color de fondo de los campos de entrada.",
"change_the_background_to_a_color_image_or_animation": "Cambiar el fondo a un color, imagen o animación.",
"change_the_border_color_of_the_card": "Cambiar el color del borde de la tarjeta.",
"change_the_border_color_of_the_input_fields": "Cambiar el color del borde de los campos de entrada.",
"change_the_border_radius_of_the_card_and_the_inputs": "Cambiar el radio del borde de la tarjeta y las entradas.",
"change_the_brand_color_of_the_survey": "Cambiar el color de marca de la encuesta.",
"change_the_placement_of_this_survey": "Cambiar la ubicación de esta encuesta.",
"change_the_question_color_of_the_survey": "Cambiar el color de las preguntas de la encuesta.",
"changes_saved": "Cambios guardados.",
"changing_survey_type_will_remove_existing_distribution_channels": "Cambiar el tipo de encuesta afectará a cómo se puede compartir. Si los encuestados ya tienen enlaces de acceso para el tipo actual, podrían perder el acceso después del cambio.",
"character_limit_toggle_description": "Limitar lo corta o larga que puede ser una respuesta.",
"character_limit_toggle_title": "Añadir límites de caracteres",
"checkbox_label": "Etiqueta de casilla de verificación",
"choose_the_actions_which_trigger_the_survey": "Elige las acciones que activan la encuesta.",
"choose_the_first_question_on_your_block": "Elige la primera pregunta en tu bloque",
@@ -1259,7 +1245,6 @@
"contact_fields": "Campos de contacto",
"contains": "Contiene",
"continue_to_settings": "Continuar a ajustes",
"control_which_file_types_can_be_uploaded": "Controla qué tipos de archivos se pueden subir.",
"convert_to_multiple_choice": "Convertir a selección múltiple",
"convert_to_single_choice": "Convertir a selección única",
"country": "País",
@@ -1375,7 +1360,6 @@
"hide_progress_bar": "Ocultar barra de progreso",
"hide_question_settings": "Ocultar ajustes de la pregunta",
"hostname": "Nombre de host",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "¿Cuánto estilo quieres darle a tus tarjetas en las encuestas de tipo {surveyTypeDerived}?",
"if_you_need_more_please": "Si necesitas más, por favor",
"if_you_really_want_that_answer_ask_until_you_get_it": "Seguir mostrando cuando se active hasta que se envíe una respuesta.",
"ignore_global_waiting_time": "Ignorar periodo de espera",
@@ -1413,9 +1397,8 @@
"key": "Clave",
"last_name": "Apellido",
"let_people_upload_up_to_25_files_at_the_same_time": "Permitir que las personas suban hasta 25 archivos al mismo tiempo.",
"limit_file_types": "Limitar tipos de archivo",
"limit_the_maximum_file_size": "Limitar el tamaño máximo de archivo",
"limit_upload_file_size_to": "Limitar tamaño de subida de archivos a",
"limit_the_maximum_file_size": "Limita el tamaño máximo de archivo para las subidas.",
"limit_upload_file_size_to": "Limitar el tamaño de archivo de subida a",
"link_survey_description": "Comparte un enlace a una página de encuesta o incrústala en una página web o correo electrónico.",
"load_segment": "Cargar segmento",
"logic_error_warning": "El cambio causará errores lógicos",
@@ -1460,7 +1443,6 @@
"picture_idx": "Imagen {idx}",
"pin_can_only_contain_numbers": "El PIN solo puede contener números.",
"pin_must_be_a_four_digit_number": "El PIN debe ser un número de cuatro dígitos.",
"please_enter_a_file_extension": "Por favor, introduce una extensión de archivo.",
"please_enter_a_valid_url": "Por favor, introduce una URL válida (p. ej., https://example.com)",
"please_set_a_survey_trigger": "Establece un disparador de encuesta",
"please_specify": "Por favor, especifica",
@@ -1471,7 +1453,6 @@
"protect_survey_with_pin_description": "Solo los usuarios que tengan el PIN pueden acceder a la encuesta.",
"publish": "Publicar",
"question": "Pregunta",
"question_color": "Color de la pregunta",
"question_deleted": "Pregunta eliminada.",
"question_duplicated": "Pregunta duplicada.",
"question_id_updated": "ID de pregunta actualizado",
@@ -1539,6 +1520,7 @@
"search_for_images": "Buscar imágenes",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos después de activarse, la encuesta se cerrará si no hay respuesta",
"seconds_before_showing_the_survey": "segundos antes de mostrar la encuesta.",
"select_field": "Seleccionar campo",
"select_or_type_value": "Selecciona o escribe un valor",
"select_ordering": "Seleccionar ordenación",
"select_saved_action": "Seleccionar acción guardada",
@@ -1572,7 +1554,6 @@
"styling_set_to_theme_styles": "Estilo configurado según los estilos del tema",
"subheading": "Subtítulo",
"subtract": "Restar -",
"suggest_colors": "Sugerir colores",
"survey_completed_heading": "Encuesta completada",
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
"survey_display_settings": "Ajustes de visualización de la encuesta",
@@ -1586,8 +1567,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar una sola vez, incluso si no responden.",
"then": "Entonces",
"this_action_will_remove_all_the_translations_from_this_survey": "Esta acción eliminará todas las traducciones de esta encuesta.",
"this_extension_is_already_added": "Esta extensión ya está añadida.",
"this_file_type_is_not_supported": "Este tipo de archivo no es compatible.",
"three_points": "3 puntos",
"times": "veces",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para mantener la ubicación coherente en todas las encuestas, puedes",
@@ -1608,6 +1587,48 @@
"upper_label": "Etiqueta superior",
"url_filters": "Filtros de URL",
"url_not_supported": "URL no compatible",
"validation": {
"add_validation_rule": "Añadir regla de validación",
"answer_all_rows": "Responde todas las filas",
"characters": "Caracteres",
"contains": "Contiene",
"delete_validation_rule": "Eliminar regla de validación",
"does_not_contain": "No contiene",
"email": "Es un correo electrónico válido",
"end_date": "Fecha de finalización",
"file_extension_is": "La extensión del archivo es",
"file_extension_is_not": "La extensión del archivo no es",
"is": "Es",
"is_between": "Está entre",
"is_earlier_than": "Es anterior a",
"is_greater_than": "Es mayor que",
"is_later_than": "Es posterior a",
"is_less_than": "Es menor que",
"is_not": "No es",
"is_not_between": "No está entre",
"kb": "KB",
"max_length": "Como máximo",
"max_selections": "Como máximo",
"max_value": "Como máximo",
"mb": "MB",
"min_length": "Al menos",
"min_selections": "Al menos",
"min_value": "Al menos",
"minimum_options_ranked": "Opciones mínimas clasificadas",
"minimum_rows_answered": "Filas mínimas respondidas",
"options_selected": "Opciones seleccionadas",
"pattern": "Coincide con el patrón regex",
"phone": "Es un teléfono válido",
"rank_all_options": "Clasificar todas las opciones",
"select_file_extensions": "Selecciona extensiones de archivo...",
"select_option": "Seleccionar opción",
"start_date": "Fecha de inicio",
"url": "Es una URL válida"
},
"validation_logic_and": "Todas son verdaderas",
"validation_logic_or": "alguna es verdadera",
"validation_rules": "Reglas de validación",
"validation_rules_description": "Solo aceptar respuestas que cumplan los siguientes criterios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} se usa en la lógica de la pregunta {questionIndex}. Por favor, elimínala primero de la lógica.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" se está utilizando en la cuota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "El nombre de la variable ya está en uso, por favor elige otro.",
@@ -2015,6 +2036,31 @@
"look": {
"add_background_color": "Añadir color de fondo",
"add_background_color_description": "Añade un color de fondo al contenedor del logotipo.",
"advanced_styling_field_border_radius": "Radio del borde",
"advanced_styling_field_button_bg": "Fondo del botón",
"advanced_styling_field_button_text": "Texto del botón",
"advanced_styling_field_description_color": "Color de la descripción",
"advanced_styling_field_description_size": "Tamaño de fuente de la descripción",
"advanced_styling_field_font_size": "Tamaño de fuente",
"advanced_styling_field_font_weight": "Grosor de fuente",
"advanced_styling_field_headline_color": "Color del titular",
"advanced_styling_field_headline_size": "Tamaño de fuente del titular",
"advanced_styling_field_headline_weight": "Grosor de fuente del titular",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_indicator_bg": "Fondo del indicador",
"advanced_styling_field_input_text": "Texto de entrada",
"advanced_styling_field_option_bg": "Fondo",
"advanced_styling_field_option_label": "Color de la etiqueta",
"advanced_styling_field_padding_x": "Relleno X",
"advanced_styling_field_padding_y": "Relleno Y",
"advanced_styling_field_placeholder_opacity": "Opacidad del marcador de posición",
"advanced_styling_field_shadow": "Sombra",
"advanced_styling_field_track_bg": "Fondo de la pista",
"advanced_styling_field_track_height": "Altura de la pista",
"advanced_styling_section_buttons": "Botones",
"advanced_styling_section_headlines": "Títulos y descripciones",
"advanced_styling_section_inputs": "Campos de entrada",
"advanced_styling_section_options": "Opciones (radio/casilla de verificación)",
"app_survey_placement": "Ubicación de encuesta de aplicación",
"app_survey_placement_settings_description": "Cambia dónde se mostrarán las encuestas en tu aplicación web o sitio web.",
"centered_modal_overlay_color": "Color de superposición del modal centrado",
@@ -2028,6 +2074,9 @@
"formbricks_branding_hidden": "La marca de Formbricks está oculta.",
"formbricks_branding_settings_description": "Nos encanta tu apoyo, pero lo entendemos si lo desactivas.",
"formbricks_branding_shown": "La marca de Formbricks se muestra.",
"generate_theme_btn": "Generar",
"generate_theme_confirmation": "¿Te gustaría generar un tema de color a juego basado en el color de tu marca? Esto sobrescribirá tu configuración de color actual.",
"generate_theme_header": "¿Generar tema de color?",
"logo_removed_successfully": "Logotipo eliminado correctamente",
"logo_settings_description": "Sube el logotipo de tu empresa para personalizar las encuestas y las vistas previas de enlaces.",
"logo_updated_successfully": "Logotipo actualizado correctamente",
@@ -2042,6 +2091,7 @@
"show_formbricks_branding_in": "Mostrar marca de Formbricks en encuestas de {type}",
"show_powered_by_formbricks": "Mostrar firma 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo actualizado correctamente",
"suggest_colors": "Sugerir colores",
"theme": "Tema",
"theme_settings_description": "Crea un tema de estilo para todas las encuestas. Puedes activar el estilo personalizado para cada encuesta."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Empreinte",
"in_progress": "En cours",
"inactive_surveys": "Sondages inactifs",
"input_type": "Type d'entrée",
"integration": "intégration",
"integrations": "Intégrations",
"invalid_date": "Date invalide",
@@ -267,13 +266,11 @@
"look_and_feel": "Apparence",
"manage": "Gérer",
"marketing": "Marketing",
"maximum": "Max",
"member": "Membre",
"members": "Membres",
"members_and_teams": "Membres & Équipes",
"membership_not_found": "Abonnement non trouvé",
"metadata": "Métadonnées",
"minimum": "Min",
"mobile_overlay_app_works_best_on_desktop": "Formbricks fonctionne mieux sur un écran plus grand. Pour gérer ou créer des sondages, passez à un autre appareil.",
"mobile_overlay_surveys_look_good": "Ne t'inquiète pas tes enquêtes sont superbes sur tous les appareils et tailles d'écran!",
"mobile_overlay_title": "Oups, écran minuscule détecté!",
@@ -326,7 +323,7 @@
"placeholder": "Remplaçant",
"please_select_at_least_one_survey": "Veuillez sélectionner au moins une enquête.",
"please_select_at_least_one_trigger": "Veuillez sélectionner au moins un déclencheur.",
"please_upgrade_your_plan": "Veuillez mettre à niveau votre plan.",
"please_upgrade_your_plan": "Veuillez mettre à niveau votre plan",
"preview": "Aperçu",
"preview_survey": "Aperçu de l'enquête",
"privacy": "Politique de confidentialité",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Ajouter un espace réservé à afficher s'il n'y a pas de valeur à rappeler.",
"add_hidden_field_id": "Ajouter un champ caché ID",
"add_highlight_border": "Ajouter une bordure de surlignage",
"add_highlight_border_description": "Ajoutez une bordure extérieure à votre carte d'enquête.",
"add_logic": "Ajouter de la logique",
"add_none_of_the_above": "Ajouter \"Aucun des éléments ci-dessus\"",
"add_option": "Ajouter une option",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
"adjust_the_theme_in_the": "Ajustez le thème dans le",
"all_other_answers_will_continue_to": "Toutes les autres réponses continueront à",
"allow_file_type": "Autoriser le type de fichier",
"allow_multi_select": "Autoriser la sélection multiple",
"allow_multiple_files": "Autoriser plusieurs fichiers",
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plusieurs images",
@@ -1227,19 +1222,10 @@
"change_background": "Changer l'arrière-plan",
"change_question_type": "Changer le type de question",
"change_survey_type": "Le changement de type de sondage affecte l'accès existant",
"change_the_background_color_of_the_card": "Changez la couleur de fond de la carte.",
"change_the_background_color_of_the_input_fields": "Vous pouvez modifier la couleur d'arrière-plan des champs de saisie.",
"change_the_background_to_a_color_image_or_animation": "Changez l'arrière-plan en une couleur, une image ou une animation.",
"change_the_border_color_of_the_card": "Changez la couleur de la bordure de la carte.",
"change_the_border_color_of_the_input_fields": "Vous pouvez modifier la couleur de la bordure des champs de saisie.",
"change_the_border_radius_of_the_card_and_the_inputs": "Vous pouvez arrondir la bordure des encadrés et des champs de saisie.",
"change_the_brand_color_of_the_survey": "Vous pouvez modifier la couleur dominante d'une enquête.",
"change_the_placement_of_this_survey": "Changez le placement de cette enquête.",
"change_the_question_color_of_the_survey": "Vous pouvez modifier la couleur des questions d'une enquête.",
"changes_saved": "Modifications enregistrées.",
"changing_survey_type_will_remove_existing_distribution_channels": "Le changement du type de sondage affectera la façon dont il peut être partagé. Si les répondants ont déjà des liens d'accès pour le type actuel, ils peuvent perdre l'accès après le changement.",
"character_limit_toggle_description": "Limitez la longueur des réponses.",
"character_limit_toggle_title": "Ajouter des limites de caractères",
"checkbox_label": "Étiquette de case à cocher",
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
"choose_the_first_question_on_your_block": "Choisissez la première question de votre bloc",
@@ -1259,7 +1245,6 @@
"contact_fields": "Champs de contact",
"contains": "Contient",
"continue_to_settings": "Continuer vers les paramètres",
"control_which_file_types_can_be_uploaded": "Contrôlez quels types de fichiers peuvent être téléchargés.",
"convert_to_multiple_choice": "Convertir en choix multiples",
"convert_to_single_choice": "Convertir en choix unique",
"country": "Pays",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "Masquer les paramètres de la question",
"hostname": "Nom d'hôte",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}",
"if_you_need_more_please": "Si vous en avez besoin de plus, s'il vous plaît",
"if_you_need_more_please": "Si vous avez besoin de plus, veuillez",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuer à afficher à chaque déclenchement jusqu'à ce qu'une réponse soit soumise.",
"ignore_global_waiting_time": "Ignorer la période de refroidissement",
"ignore_global_waiting_time_description": "Cette enquête peut s'afficher chaque fois que ses conditions sont remplies, même si une autre enquête a été affichée récemment.",
@@ -1413,9 +1398,8 @@
"key": "Clé",
"last_name": "Nom de famille",
"let_people_upload_up_to_25_files_at_the_same_time": "Permettre aux utilisateurs de télécharger jusqu'à 25 fichiers en même temps.",
"limit_file_types": "Limiter les types de fichiers",
"limit_the_maximum_file_size": "Limiter la taille maximale du fichier",
"limit_upload_file_size_to": "Limiter la taille des fichiers téléchargés à",
"limit_the_maximum_file_size": "Limiter la taille maximale des fichiers pour les téléversements.",
"limit_upload_file_size_to": "Limiter la taille de téléversement des fichiers à",
"link_survey_description": "Partagez un lien vers une page d'enquête ou intégrez-le dans une page web ou un e-mail.",
"load_segment": "Segment de chargement",
"logic_error_warning": "Changer causera des erreurs logiques",
@@ -1428,7 +1412,7 @@
"matrix_all_fields": "Tous les champs",
"matrix_rows": "Lignes",
"max_file_size": "Taille maximale du fichier",
"max_file_size_limit_is": "La taille maximale du fichier est",
"max_file_size_limit_is": "La limite de taille maximale du fichier est",
"move_question_to_block": "Déplacer la question vers le bloc",
"multiply": "Multiplier *",
"needed_for_self_hosted_cal_com_instance": "Nécessaire pour une instance Cal.com auto-hébergée",
@@ -1460,7 +1444,6 @@
"picture_idx": "Image {idx}",
"pin_can_only_contain_numbers": "Le code PIN ne peut contenir que des chiffres.",
"pin_must_be_a_four_digit_number": "Le code PIN doit être un numéro à quatre chiffres.",
"please_enter_a_file_extension": "Veuillez entrer une extension de fichier.",
"please_enter_a_valid_url": "Veuillez entrer une URL valide (par exemple, https://example.com)",
"please_set_a_survey_trigger": "Veuillez définir un déclencheur d'enquête.",
"please_specify": "Veuillez préciser",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "Seules les personnes ayant le code PIN peuvent accéder à l'enquête.",
"publish": "Publier",
"question": "Question",
"question_color": "Couleur des questions",
"question_deleted": "Question supprimée.",
"question_duplicated": "Question dupliquée.",
"question_id_updated": "ID de la question mis à jour",
@@ -1539,6 +1521,7 @@
"search_for_images": "Rechercher des images",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "Les secondes après le déclenchement, l'enquête sera fermée si aucune réponse n'est donnée.",
"seconds_before_showing_the_survey": "secondes avant de montrer l'enquête.",
"select_field": "Sélectionner un champ",
"select_or_type_value": "Sélectionnez ou saisissez une valeur",
"select_ordering": "Choisir l'ordre",
"select_saved_action": "Sélectionner une action enregistrée",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "Style défini sur les styles du thème",
"subheading": "Sous-titre",
"subtract": "Soustraire -",
"suggest_colors": "Suggérer des couleurs",
"survey_completed_heading": "Enquête terminée",
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
"survey_display_settings": "Paramètres d'affichage de l'enquête",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Afficher une seule fois, même si la personne ne répond pas.",
"then": "Alors",
"this_action_will_remove_all_the_translations_from_this_survey": "Cette action supprimera toutes les traductions de cette enquête.",
"this_extension_is_already_added": "Cette extension est déjà ajoutée.",
"this_file_type_is_not_supported": "Ce type de fichier n'est pas pris en charge.",
"three_points": "3 points",
"times": "fois",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Pour maintenir la cohérence du placement sur tous les sondages, vous pouvez",
@@ -1608,6 +1588,48 @@
"upper_label": "Étiquette supérieure",
"url_filters": "Filtres d'URL",
"url_not_supported": "URL non supportée",
"validation": {
"add_validation_rule": "Ajouter une règle de validation",
"answer_all_rows": "Répondre à toutes les lignes",
"characters": "Caractères",
"contains": "Contient",
"delete_validation_rule": "Supprimer la règle de validation",
"does_not_contain": "Ne contient pas",
"email": "Est un e-mail valide",
"end_date": "Date de fin",
"file_extension_is": "L'extension de fichier est",
"file_extension_is_not": "L'extension de fichier n'est pas",
"is": "Est",
"is_between": "Est entre",
"is_earlier_than": "Est antérieur à",
"is_greater_than": "Est supérieur à",
"is_later_than": "Est postérieur à",
"is_less_than": "Est inférieur à",
"is_not": "N'est pas",
"is_not_between": "N'est pas entre",
"kb": "Ko",
"max_length": "Au maximum",
"max_selections": "Au maximum",
"max_value": "Au maximum",
"mb": "Mo",
"min_length": "Au moins",
"min_selections": "Au moins",
"min_value": "Au moins",
"minimum_options_ranked": "Nombre minimum d'options classées",
"minimum_rows_answered": "Nombre minimum de lignes répondues",
"options_selected": "Options sélectionnées",
"pattern": "Correspond au modèle d'expression régulière",
"phone": "Est un numéro de téléphone valide",
"rank_all_options": "Classer toutes les options",
"select_file_extensions": "Sélectionner les extensions de fichier...",
"select_option": "Sélectionner une option",
"start_date": "Date de début",
"url": "Est une URL valide"
},
"validation_logic_and": "Toutes sont vraies",
"validation_logic_or": "au moins une est vraie",
"validation_rules": "Règles de validation",
"validation_rules_description": "Accepter uniquement les réponses qui répondent aux critères suivants",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" est utilisée dans le quota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "Le nom de la variable est déjà pris, veuillez en choisir un autre.",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "Ajouter une couleur d'arrière-plan",
"add_background_color_description": "Ajoutez une couleur d'arrière-plan au conteneur du logo.",
"advanced_styling_field_border_radius": "Rayon de bordure",
"advanced_styling_field_button_bg": "Arrière-plan du bouton",
"advanced_styling_field_button_text": "Texte du bouton",
"advanced_styling_field_description_color": "Couleur de description",
"advanced_styling_field_description_size": "Taille de police de description",
"advanced_styling_field_font_size": "Taille de police",
"advanced_styling_field_font_weight": "Graisse de police",
"advanced_styling_field_headline_color": "Couleur du titre",
"advanced_styling_field_headline_size": "Taille de police du titre",
"advanced_styling_field_headline_weight": "Graisse de police du titre",
"advanced_styling_field_height": "Hauteur",
"advanced_styling_field_indicator_bg": "Arrière-plan de l'indicateur",
"advanced_styling_field_input_text": "Texte du champ de saisie",
"advanced_styling_field_option_bg": "Arrière-plan",
"advanced_styling_field_option_label": "Couleur du libellé",
"advanced_styling_field_padding_x": "Marge intérieure X",
"advanced_styling_field_padding_y": "Marge intérieure Y",
"advanced_styling_field_placeholder_opacity": "Opacité du texte indicatif",
"advanced_styling_field_shadow": "Ombre",
"advanced_styling_field_track_bg": "Arrière-plan de la piste",
"advanced_styling_field_track_height": "Hauteur de la piste",
"advanced_styling_section_buttons": "Boutons",
"advanced_styling_section_headlines": "Titres et descriptions",
"advanced_styling_section_inputs": "Champs de saisie",
"advanced_styling_section_options": "Options (bouton radio/case à cocher)",
"app_survey_placement": "Placement du sondage d'application",
"app_survey_placement_settings_description": "Modifiez l'emplacement où les sondages seront affichés dans votre application web ou site web.",
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Le logo Formbricks est masqué.",
"formbricks_branding_settings_description": "Nous apprécions votre soutien mais comprenons si vous choisissez de le désactiver.",
"formbricks_branding_shown": "Le logo Formbricks est affiché.",
"generate_theme_btn": "Générer",
"generate_theme_confirmation": "Souhaitez-vous générer un thème de couleurs correspondant basé sur la couleur de votre marque? Cela écrasera vos paramètres de couleur actuels.",
"generate_theme_header": "Générer un thème de couleurs?",
"logo_removed_successfully": "Logo supprimé avec succès",
"logo_settings_description": "Téléchargez le logo de votre entreprise pour personnaliser les enquêtes et les aperçus de liens.",
"logo_updated_successfully": "Logo mis à jour avec succès",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "Afficher le logo Formbricks dans les enquêtes {type}",
"show_powered_by_formbricks": "Afficher la signature «Propulsé par Formbricks»",
"styling_updated_successfully": "Style mis à jour avec succès",
"suggest_colors": "Suggérer des couleurs",
"theme": "Thème",
"theme_settings_description": "Créez un thème de style pour toutes les enquêtes. Vous pouvez activer un style personnalisé pour chaque enquête."
},

View File

@@ -243,7 +243,6 @@
"imprint": "企業情報",
"in_progress": "進行中",
"inactive_surveys": "非アクティブなフォーム",
"input_type": "入力タイプ",
"integration": "連携",
"integrations": "連携",
"invalid_date": "無効な日付です",
@@ -267,13 +266,11 @@
"look_and_feel": "デザイン",
"manage": "管理",
"marketing": "マーケティング",
"maximum": "最大",
"member": "メンバー",
"members": "メンバー",
"members_and_teams": "メンバー&チーム",
"membership_not_found": "メンバーシップが見つかりません",
"metadata": "メタデータ",
"minimum": "最小",
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
"mobile_overlay_title": "おっと、 小さな 画面 が 検出されました!",
@@ -326,7 +323,7 @@
"placeholder": "プレースホルダー",
"please_select_at_least_one_survey": "少なくとも1つのフォームを選択してください",
"please_select_at_least_one_trigger": "少なくとも1つのトリガーを選択してください",
"please_upgrade_your_plan": "プランをアップグレードしてください",
"please_upgrade_your_plan": "プランをアップグレードしてください",
"preview": "プレビュー",
"preview_survey": "フォームをプレビュー",
"privacy": "プライバシーポリシー",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "質問がスキップされた場合に表示するプレースホルダーを追加:",
"add_hidden_field_id": "非表示フィールドIDを追加",
"add_highlight_border": "ハイライトボーダーを追加",
"add_highlight_border_description": "フォームカードに外側のボーダーを追加します。",
"add_logic": "ロジックを追加",
"add_none_of_the_above": "\"いずれも該当しません\" を追加",
"add_option": "オプションを追加",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "フォームがクローズしたときに訪問者が見るメッセージを変更します。",
"adjust_the_theme_in_the": "テーマを",
"all_other_answers_will_continue_to": "他のすべての回答は引き続き",
"allow_file_type": "ファイルタイプを許可",
"allow_multi_select": "複数選択を許可",
"allow_multiple_files": "複数のファイルを許可",
"allow_users_to_select_more_than_one_image": "ユーザーが複数の画像を選択できるようにする",
@@ -1227,19 +1222,10 @@
"change_background": "背景を変更",
"change_question_type": "質問の種類を変更",
"change_survey_type": "フォームの種類を変更すると、既存のアクセスに影響します",
"change_the_background_color_of_the_card": "カードの背景色を変更します。",
"change_the_background_color_of_the_input_fields": "入力フィールドの背景色を変更します。",
"change_the_background_to_a_color_image_or_animation": "背景を色、画像、またはアニメーションに変更します。",
"change_the_border_color_of_the_card": "カードの枠線の色を変更します。",
"change_the_border_color_of_the_input_fields": "入力フィールドの枠線の色を変更します。",
"change_the_border_radius_of_the_card_and_the_inputs": "カードと入力の角丸を変更します。",
"change_the_brand_color_of_the_survey": "フォームのブランドカラーを変更します。",
"change_the_placement_of_this_survey": "このフォームの配置を変更します。",
"change_the_question_color_of_the_survey": "フォームの質問の色を変更します。",
"changes_saved": "変更を保存しました。",
"changing_survey_type_will_remove_existing_distribution_channels": "フォームの種類を変更すると、共有方法に影響します。回答者が現在のタイプのアクセスリンクをすでに持っている場合、切り替え後にアクセスを失う可能性があります。",
"character_limit_toggle_description": "回答の長さの上限・下限を設定します。",
"character_limit_toggle_title": "文字数制限を追加",
"checkbox_label": "チェックボックスのラベル",
"choose_the_actions_which_trigger_the_survey": "フォームをトリガーするアクションを選択してください。",
"choose_the_first_question_on_your_block": "ブロックの最初の質問を選択してください",
@@ -1259,7 +1245,6 @@
"contact_fields": "連絡先フィールド",
"contains": "を含む",
"continue_to_settings": "設定に進む",
"control_which_file_types_can_be_uploaded": "アップロードできるファイルの種類を制御します。",
"convert_to_multiple_choice": "複数選択に変換",
"convert_to_single_choice": "単一選択に変換",
"country": "国",
@@ -1375,7 +1360,6 @@
"hide_progress_bar": "プログレスバーを非表示",
"hide_question_settings": "質問設定を非表示",
"hostname": "ホスト名",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか",
"if_you_need_more_please": "さらに必要な場合は、",
"if_you_really_want_that_answer_ask_until_you_get_it": "回答が提出されるまで、トリガーされるたびに表示し続けます。",
"ignore_global_waiting_time": "クールダウン期間を無視",
@@ -1413,9 +1397,8 @@
"key": "キー",
"last_name": "姓",
"let_people_upload_up_to_25_files_at_the_same_time": "一度に最大25個のファイルをアップロードできるようにする。",
"limit_file_types": "ファイルタイプを制限",
"limit_the_maximum_file_size": "最大ファイルサイズを制限",
"limit_upload_file_size_to": "アップロードファイルサイズを以下に制限",
"limit_the_maximum_file_size": "アップロードの最大ファイルサイズを制限します。",
"limit_upload_file_size_to": "アップロードファイルサイズの上限",
"link_survey_description": "フォームページへのリンクを共有するか、ウェブページやメールに埋め込みます。",
"load_segment": "セグメントを読み込み",
"logic_error_warning": "変更するとロジックエラーが発生します",
@@ -1460,7 +1443,6 @@
"picture_idx": "写真 {idx}",
"pin_can_only_contain_numbers": "PINは数字のみでなければなりません。",
"pin_must_be_a_four_digit_number": "PINは4桁の数字でなければなりません。",
"please_enter_a_file_extension": "ファイル拡張子を入力してください。",
"please_enter_a_valid_url": "有効な URL を入力してください (例https://example.com)",
"please_set_a_survey_trigger": "フォームのトリガーを設定してください",
"please_specify": "具体的に指定してください",
@@ -1471,7 +1453,6 @@
"protect_survey_with_pin_description": "PINを持つユーザーのみがフォームにアクセスできます。",
"publish": "公開",
"question": "質問",
"question_color": "質問の色",
"question_deleted": "質問を削除しました。",
"question_duplicated": "質問を複製しました。",
"question_id_updated": "質問IDを更新しました",
@@ -1539,6 +1520,7 @@
"search_for_images": "画像を検索",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "トリガーから数秒後に回答がない場合、フォームは閉じられます",
"seconds_before_showing_the_survey": "秒後にフォームを表示します。",
"select_field": "フィールドを選択",
"select_or_type_value": "値を選択または入力",
"select_ordering": "順序を選択",
"select_saved_action": "保存済みのアクションを選択",
@@ -1572,7 +1554,6 @@
"styling_set_to_theme_styles": "スタイルをテーマのスタイルに設定しました",
"subheading": "サブ見出し",
"subtract": "減算 -",
"suggest_colors": "色を提案",
"survey_completed_heading": "フォームが完了しました",
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
"survey_display_settings": "フォーム表示設定",
@@ -1586,8 +1567,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "回答がなくても1回だけ表示します。",
"then": "その後",
"this_action_will_remove_all_the_translations_from_this_survey": "このアクションは、このフォームからすべての翻訳を削除します。",
"this_extension_is_already_added": "この拡張機能はすでに追加されています。",
"this_file_type_is_not_supported": "このファイルタイプはサポートされていません。",
"three_points": "3点",
"times": "回",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "すべてのフォームの配置を一貫させるために、",
@@ -1608,6 +1587,48 @@
"upper_label": "上限ラベル",
"url_filters": "URLフィルター",
"url_not_supported": "URLはサポートされていません",
"validation": {
"add_validation_rule": "検証ルールを追加",
"answer_all_rows": "すべての行に回答してください",
"characters": "文字数",
"contains": "を含む",
"delete_validation_rule": "検証ルールを削除",
"does_not_contain": "を含まない",
"email": "有効なメールアドレスである",
"end_date": "終了日",
"file_extension_is": "ファイル拡張子が次と一致",
"file_extension_is_not": "ファイル拡張子が次と一致しない",
"is": "である",
"is_between": "の間である",
"is_earlier_than": "より前である",
"is_greater_than": "より大きい",
"is_later_than": "より後である",
"is_less_than": "より小さい",
"is_not": "ではない",
"is_not_between": "の間ではない",
"kb": "KB",
"max_length": "最大",
"max_selections": "最大",
"max_value": "最大",
"mb": "MB",
"min_length": "最小",
"min_selections": "最小",
"min_value": "最小",
"minimum_options_ranked": "ランク付けされた最小オプション数",
"minimum_rows_answered": "回答された最小行数",
"options_selected": "選択されたオプション",
"pattern": "正規表現パターンに一致する",
"phone": "有効な電話番号である",
"rank_all_options": "すべてのオプションをランク付け",
"select_file_extensions": "ファイル拡張子を選択...",
"select_option": "オプションを選択",
"start_date": "開始日",
"url": "有効なURLである"
},
"validation_logic_and": "すべてが真である",
"validation_logic_or": "いずれかが真",
"validation_rules": "検証ルール",
"validation_rules_description": "次の条件を満たす回答のみを受け付ける",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "変数 \"{variableName}\" は \"{quotaName}\" クォータ で使用されています",
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
@@ -2015,6 +2036,31 @@
"look": {
"add_background_color": "背景色を追加",
"add_background_color_description": "ロゴコンテナに背景色を追加します。",
"advanced_styling_field_border_radius": "角の丸み",
"advanced_styling_field_button_bg": "ボタンの背景",
"advanced_styling_field_button_text": "ボタンのテキスト",
"advanced_styling_field_description_color": "説明の色",
"advanced_styling_field_description_size": "説明のフォントサイズ",
"advanced_styling_field_font_size": "フォントサイズ",
"advanced_styling_field_font_weight": "フォントの太さ",
"advanced_styling_field_headline_color": "見出しの色",
"advanced_styling_field_headline_size": "見出しのフォントサイズ",
"advanced_styling_field_headline_weight": "見出しのフォントの太さ",
"advanced_styling_field_height": "高さ",
"advanced_styling_field_indicator_bg": "インジケーターの背景",
"advanced_styling_field_input_text": "入力のテキスト",
"advanced_styling_field_option_bg": "背景",
"advanced_styling_field_option_label": "ラベルの色",
"advanced_styling_field_padding_x": "水平方向の余白",
"advanced_styling_field_padding_y": "垂直方向の余白",
"advanced_styling_field_placeholder_opacity": "プレースホルダーの不透明度",
"advanced_styling_field_shadow": "シャドウ",
"advanced_styling_field_track_bg": "トラック背景",
"advanced_styling_field_track_height": "トラック高さ",
"advanced_styling_section_buttons": "ボタン",
"advanced_styling_section_headlines": "見出し&説明",
"advanced_styling_section_inputs": "入力フィールド",
"advanced_styling_section_options": "選択肢(ラジオ/チェックボックス)",
"app_survey_placement": "アプリ内フォームの配置",
"app_survey_placement_settings_description": "Webアプリまたはウェブサイトでフォームを表示する場所を変更します。",
"centered_modal_overlay_color": "中央モーダルのオーバーレイ色",
@@ -2028,6 +2074,9 @@
"formbricks_branding_hidden": "Formbricksブランディングは非表示です。",
"formbricks_branding_settings_description": "あなたのサポートに感謝していますが、オフにすることもご理解いただけます。",
"formbricks_branding_shown": "Formbricksブランディングは表示されています。",
"generate_theme_btn": "生成",
"generate_theme_confirmation": "ブランドカラーに基づいて、マッチングカラーテーマを生成しますか?現在のカラー設定は上書きされます。",
"generate_theme_header": "カラーテーマを生成しますか?",
"logo_removed_successfully": "ロゴを正常に削除しました",
"logo_settings_description": "会社のロゴをアップロードして、アンケートとリンクプレビューにブランディングを適用します。",
"logo_updated_successfully": "ロゴを正常に更新しました",
@@ -2042,6 +2091,7 @@
"show_formbricks_branding_in": "{type}アンケートにFormbricksブランディングを表示",
"show_powered_by_formbricks": "「Powered by Formbricks」署名を表示",
"styling_updated_successfully": "スタイルを正常に更新しました",
"suggest_colors": "色を提案",
"theme": "テーマ",
"theme_settings_description": "すべてのアンケート用のスタイルテーマを作成します。各アンケートでカスタムスタイルを有効にできます。"
},

View File

@@ -243,7 +243,6 @@
"imprint": "Afdruk",
"in_progress": "In uitvoering",
"inactive_surveys": "Inactieve enquêtes",
"input_type": "Invoertype",
"integration": "integratie",
"integrations": "Integraties",
"invalid_date": "Ongeldige datum",
@@ -267,13 +266,11 @@
"look_and_feel": "Kijk & voel",
"manage": "Beheren",
"marketing": "Marketing",
"maximum": "Maximaal",
"member": "Lid",
"members": "Leden",
"members_and_teams": "Leden & teams",
"membership_not_found": "Lidmaatschap niet gevonden",
"metadata": "Metagegevens",
"minimum": "Minimum",
"mobile_overlay_app_works_best_on_desktop": "Formbricks werkt het beste op een groter scherm. Schakel over naar een ander apparaat om enquêtes te beheren of samen te stellen.",
"mobile_overlay_surveys_look_good": "Maakt u zich geen zorgen: uw enquêtes zien er geweldig uit op elk apparaat en schermformaat!",
"mobile_overlay_title": "Oeps, klein scherm gedetecteerd!",
@@ -326,7 +323,7 @@
"placeholder": "Tijdelijke aanduiding",
"please_select_at_least_one_survey": "Selecteer ten minste één enquête",
"please_select_at_least_one_trigger": "Selecteer ten minste één trigger",
"please_upgrade_your_plan": "Upgrade uw abonnement.",
"please_upgrade_your_plan": "Upgrade je abonnement",
"preview": "Voorbeeld",
"preview_survey": "Voorbeeld van enquête",
"privacy": "Privacybeleid",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Voeg een tijdelijke aanduiding toe om aan te geven of er geen waarde is om te onthouden.",
"add_hidden_field_id": "Voeg een verborgen veld-ID toe",
"add_highlight_border": "Markeerrand toevoegen",
"add_highlight_border_description": "Voeg een buitenrand toe aan uw enquêtekaart.",
"add_logic": "Voeg logica toe",
"add_none_of_the_above": "Voeg 'Geen van bovenstaande' toe",
"add_option": "Optie toevoegen",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Wijzig het bericht dat bezoekers zien wanneer de enquête wordt gesloten.",
"adjust_the_theme_in_the": "Pas het thema aan in de",
"all_other_answers_will_continue_to": "Alle andere antwoorden blijven hetzelfde",
"allow_file_type": "Bestandstype toestaan",
"allow_multi_select": "Multi-select toestaan",
"allow_multiple_files": "Meerdere bestanden toestaan",
"allow_users_to_select_more_than_one_image": "Sta gebruikers toe meer dan één afbeelding te selecteren",
@@ -1227,19 +1222,10 @@
"change_background": "Achtergrond wijzigen",
"change_question_type": "Vraagtype wijzigen",
"change_survey_type": "Als u van enquêtetype verandert, heeft dit invloed op de bestaande toegang",
"change_the_background_color_of_the_card": "Verander de achtergrondkleur van de kaart.",
"change_the_background_color_of_the_input_fields": "Verander de achtergrondkleur van de invoervelden.",
"change_the_background_to_a_color_image_or_animation": "Verander de achtergrond in een kleur, afbeelding of animatie.",
"change_the_border_color_of_the_card": "Verander de randkleur van de kaart.",
"change_the_border_color_of_the_input_fields": "Wijzig de randkleur van de invoervelden.",
"change_the_border_radius_of_the_card_and_the_inputs": "Wijzig de randradius van de kaart en de ingangen.",
"change_the_brand_color_of_the_survey": "Wijzig de merkkleur van de enquête.",
"change_the_placement_of_this_survey": "Wijzig de plaatsing van deze enquête.",
"change_the_question_color_of_the_survey": "Verander de vraagkleur van de enquête.",
"changes_saved": "Wijzigingen opgeslagen.",
"changing_survey_type_will_remove_existing_distribution_channels": "Het wijzigen van het enquêtetype heeft invloed op de manier waarop deze kan worden gedeeld. Als respondenten al toegangslinks hebben voor het huidige type, verliezen ze mogelijk de toegang na de overstap.",
"character_limit_toggle_description": "Beperk hoe kort of lang een antwoord mag zijn.",
"character_limit_toggle_title": "Tekenlimieten toevoegen",
"checkbox_label": "Selectievakje-label",
"choose_the_actions_which_trigger_the_survey": "Kies de acties die de enquête activeren.",
"choose_the_first_question_on_your_block": "Kies de eerste vraag in je blok",
@@ -1259,7 +1245,6 @@
"contact_fields": "Contactvelden",
"contains": "Bevat",
"continue_to_settings": "Ga verder naar Instellingen",
"control_which_file_types_can_be_uploaded": "Bepaal welke bestandstypen kunnen worden geüpload.",
"convert_to_multiple_choice": "Converteren naar Multi-select",
"convert_to_single_choice": "Converteren naar Enkele selectie",
"country": "Land",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "Vraaginstellingen verbergen",
"hostname": "Hostnaam",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hoe funky wil je je kaarten hebben in {surveyTypeDerived} Enquêtes",
"if_you_need_more_please": "Als u meer nodig heeft, alstublieft",
"if_you_need_more_please": "Als je meer nodig hebt,",
"if_you_really_want_that_answer_ask_until_you_get_it": "Blijf tonen wanneer geactiveerd totdat een reactie is ingediend.",
"ignore_global_waiting_time": "Afkoelperiode negeren",
"ignore_global_waiting_time_description": "Deze enquête kan worden getoond wanneer aan de voorwaarden wordt voldaan, zelfs als er onlangs een andere enquête is getoond.",
@@ -1413,9 +1398,8 @@
"key": "Sleutel",
"last_name": "Achternaam",
"let_people_upload_up_to_25_files_at_the_same_time": "Laat mensen maximaal 25 bestanden tegelijk uploaden.",
"limit_file_types": "Beperk bestandstypen",
"limit_the_maximum_file_size": "Beperk de maximale bestandsgrootte",
"limit_upload_file_size_to": "Beperk de uploadbestandsgrootte tot",
"limit_the_maximum_file_size": "Beperk de maximale bestandsgrootte voor uploads.",
"limit_upload_file_size_to": "Beperk uploadbestandsgrootte tot",
"link_survey_description": "Deel een link naar een enquêtepagina of sluit deze in op een webpagina of e-mail.",
"load_segment": "Laadsegment",
"logic_error_warning": "Wijzigen zal logische fouten veroorzaken",
@@ -1428,7 +1412,7 @@
"matrix_all_fields": "Alle velden",
"matrix_rows": "Rijen",
"max_file_size": "Maximale bestandsgrootte",
"max_file_size_limit_is": "De maximale bestandsgrootte is",
"max_file_size_limit_is": "Maximale bestandsgroottelimiet is",
"move_question_to_block": "Vraag naar blok verplaatsen",
"multiply": "Vermenigvuldig *",
"needed_for_self_hosted_cal_com_instance": "Nodig voor een zelf-gehoste Cal.com-instantie",
@@ -1460,7 +1444,6 @@
"picture_idx": "Afbeelding {idx}",
"pin_can_only_contain_numbers": "De pincode kan alleen cijfers bevatten.",
"pin_must_be_a_four_digit_number": "De pincode moet uit vier cijfers bestaan.",
"please_enter_a_file_extension": "Voer een bestandsextensie in.",
"please_enter_a_valid_url": "Voer een geldige URL in (bijvoorbeeld https://example.com)",
"please_set_a_survey_trigger": "Stel een enquêtetrigger in",
"please_specify": "Gelieve te specificeren",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "Alleen gebruikers die de pincode hebben, hebben toegang tot de enquête.",
"publish": "Publiceren",
"question": "Vraag",
"question_color": "Vraag kleur",
"question_deleted": "Vraag verwijderd.",
"question_duplicated": "Vraag dubbel gesteld.",
"question_id_updated": "Vraag-ID bijgewerkt",
@@ -1539,6 +1521,7 @@
"search_for_images": "Zoek naar afbeeldingen",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "seconden na trigger wordt de enquête gesloten als er geen reactie is",
"seconds_before_showing_the_survey": "seconden voordat de enquête wordt weergegeven.",
"select_field": "Selecteer veld",
"select_or_type_value": "Selecteer of typ een waarde",
"select_ordering": "Selecteer bestellen",
"select_saved_action": "Selecteer opgeslagen actie",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "Styling ingesteld op themastijlen",
"subheading": "Ondertitel",
"subtract": "Aftrekken -",
"suggest_colors": "Stel kleuren voor",
"survey_completed_heading": "Enquête voltooid",
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
"survey_display_settings": "Enquêteweergave-instellingen",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Toon één keer, zelfs als ze niet reageren.",
"then": "Dan",
"this_action_will_remove_all_the_translations_from_this_survey": "Met deze actie worden alle vertalingen uit deze enquête verwijderd.",
"this_extension_is_already_added": "Deze extensie is al toegevoegd.",
"this_file_type_is_not_supported": "Dit bestandstype wordt niet ondersteund.",
"three_points": "3 punten",
"times": "keer",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Om de plaatsing over alle enquêtes consistent te houden, kunt u dat doen",
@@ -1608,6 +1588,48 @@
"upper_label": "Bovenste etiket",
"url_filters": "URL-filters",
"url_not_supported": "URL niet ondersteund",
"validation": {
"add_validation_rule": "Validatieregel toevoegen",
"answer_all_rows": "Beantwoord alle rijen",
"characters": "Tekens",
"contains": "Bevat",
"delete_validation_rule": "Validatieregel verwijderen",
"does_not_contain": "Bevat niet",
"email": "Is geldig e-mailadres",
"end_date": "Einddatum",
"file_extension_is": "Bestandsextensie is",
"file_extension_is_not": "Bestandsextensie is niet",
"is": "Is",
"is_between": "Is tussen",
"is_earlier_than": "Is eerder dan",
"is_greater_than": "Is groter dan",
"is_later_than": "Is later dan",
"is_less_than": "Is minder dan",
"is_not": "Is niet",
"is_not_between": "Is niet tussen",
"kb": "KB",
"max_length": "Maximaal",
"max_selections": "Maximaal",
"max_value": "Maximaal",
"mb": "MB",
"min_length": "Minimaal",
"min_selections": "Minimaal",
"min_value": "Minimaal",
"minimum_options_ranked": "Minimaal aantal gerangschikte opties",
"minimum_rows_answered": "Minimaal aantal beantwoorde rijen",
"options_selected": "Opties geselecteerd",
"pattern": "Komt overeen met regex-patroon",
"phone": "Is geldig telefoonnummer",
"rank_all_options": "Rangschik alle opties",
"select_file_extensions": "Selecteer bestandsextensies...",
"select_option": "Optie selecteren",
"start_date": "Startdatum",
"url": "Is geldige URL"
},
"validation_logic_and": "Alle zijn waar",
"validation_logic_or": "een is waar",
"validation_rules": "Validatieregels",
"validation_rules_description": "Accepteer alleen antwoorden die voldoen aan de volgende criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabele \"{variableName}\" wordt gebruikt in het \"{quotaName}\" quotum",
"variable_name_is_already_taken_please_choose_another": "Variabelenaam is al in gebruik, kies een andere.",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "Achtergrondkleur toevoegen",
"add_background_color_description": "Voeg een achtergrondkleur toe aan de logocontainer.",
"advanced_styling_field_border_radius": "Hoekradius",
"advanced_styling_field_button_bg": "Knop achtergrond",
"advanced_styling_field_button_text": "Knop tekst",
"advanced_styling_field_description_color": "Beschrijving kleur",
"advanced_styling_field_description_size": "Beschrijving lettergrootte",
"advanced_styling_field_font_size": "Lettergrootte",
"advanced_styling_field_font_weight": "Letterdikte",
"advanced_styling_field_headline_color": "Kop kleur",
"advanced_styling_field_headline_size": "Kop lettergrootte",
"advanced_styling_field_headline_weight": "Kop letterdikte",
"advanced_styling_field_height": "Hoogte",
"advanced_styling_field_indicator_bg": "Indicator achtergrond",
"advanced_styling_field_input_text": "Invoer tekst",
"advanced_styling_field_option_bg": "Achtergrond",
"advanced_styling_field_option_label": "Label kleur",
"advanced_styling_field_padding_x": "Padding X",
"advanced_styling_field_padding_y": "Padding Y",
"advanced_styling_field_placeholder_opacity": "Placeholder transparantie",
"advanced_styling_field_shadow": "Schaduw",
"advanced_styling_field_track_bg": "Achtergrond van balk",
"advanced_styling_field_track_height": "Hoogte van balk",
"advanced_styling_section_buttons": "Knoppen",
"advanced_styling_section_headlines": "Koppen & beschrijvingen",
"advanced_styling_section_inputs": "Invoervelden",
"advanced_styling_section_options": "Opties (radio/checkbox)",
"app_survey_placement": "App-enquête plaatsing",
"app_survey_placement_settings_description": "Wijzig waar enquêtes worden weergegeven in uw web-app of website.",
"centered_modal_overlay_color": "Gecentreerde modale overlaykleur",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Formbricks-branding is verborgen.",
"formbricks_branding_settings_description": "We waarderen uw steun, maar begrijpen het als u dit uitschakelt.",
"formbricks_branding_shown": "Formbricks-branding wordt weergegeven.",
"generate_theme_btn": "Genereren",
"generate_theme_confirmation": "Wil je een bijpassend kleurthema genereren op basis van je merkkleur? Dit overschrijft je huidige kleurinstellingen.",
"generate_theme_header": "Kleurthema genereren?",
"logo_removed_successfully": "Logo succesvol verwijderd",
"logo_settings_description": "Upload uw bedrijfslogo om enquêtes en linkvoorbeelden te voorzien van uw huisstijl.",
"logo_updated_successfully": "Logo succesvol bijgewerkt",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "Toon Formbricks-branding in {type} enquêtes",
"show_powered_by_formbricks": "Toon 'Powered by Formbricks' handtekening",
"styling_updated_successfully": "Styling succesvol bijgewerkt",
"suggest_colors": "Stel kleuren voor",
"theme": "Thema",
"theme_settings_description": "Maak een stijlthema voor alle enquêtes. Je kunt aangepaste styling inschakelen voor elke enquête."
},

View File

@@ -243,7 +243,6 @@
"imprint": "impressão",
"in_progress": "Em andamento",
"inactive_surveys": "Pesquisas inativas",
"input_type": "Tipo de entrada",
"integration": "integração",
"integrations": "Integrações",
"invalid_date": "Data inválida",
@@ -267,13 +266,11 @@
"look_and_feel": "Aparência e Experiência",
"manage": "gerenciar",
"marketing": "marketing",
"maximum": "Máximo",
"member": "Membros",
"members": "Membros",
"members_and_teams": "Membros e equipes",
"membership_not_found": "Assinatura não encontrada",
"metadata": "metadados",
"minimum": "Mínimo",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor em uma tela maior. Para gerenciar ou criar pesquisas, mude para outro dispositivo.",
"mobile_overlay_surveys_look_good": "Não se preocupe suas pesquisas ficam ótimas em qualquer dispositivo e tamanho de tela!",
"mobile_overlay_title": "Eita, tela pequena detectada!",
@@ -326,7 +323,7 @@
"placeholder": "Espaço reservado",
"please_select_at_least_one_survey": "Por favor, selecione pelo menos uma pesquisa",
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
"please_upgrade_your_plan": "Por favor, atualize seu plano.",
"please_upgrade_your_plan": "Por favor, atualize seu plano",
"preview": "Prévia",
"preview_survey": "Prévia da Pesquisa",
"privacy": "Política de Privacidade",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Adicionar um texto padrão para mostrar se a pergunta for ignorada:",
"add_hidden_field_id": "Adicionar campo oculto ID",
"add_highlight_border": "Adicionar borda de destaque",
"add_highlight_border_description": "Adicione uma borda externa ao seu cartão de pesquisa.",
"add_logic": "Adicionar lógica",
"add_none_of_the_above": "Adicionar \"Nenhuma das opções acima\"",
"add_option": "Adicionar opção",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Mude a mensagem que os visitantes veem quando a pesquisa está fechada.",
"adjust_the_theme_in_the": "Ajuste o tema no",
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
"allow_file_type": "Permitir tipo de arquivo",
"allow_multi_select": "Permitir seleção múltipla",
"allow_multiple_files": "Permitir vários arquivos",
"allow_users_to_select_more_than_one_image": "Permitir que os usuários selecionem mais de uma imagem",
@@ -1227,19 +1222,10 @@
"change_background": "Mudar fundo",
"change_question_type": "Mudar tipo de pergunta",
"change_survey_type": "Alterar o tipo de pesquisa afeta o acesso existente",
"change_the_background_color_of_the_card": "Muda a cor de fundo do cartão.",
"change_the_background_color_of_the_input_fields": "Mude a cor de fundo dos campos de entrada.",
"change_the_background_to_a_color_image_or_animation": "Mude o fundo para uma cor, imagem ou animação.",
"change_the_border_color_of_the_card": "Muda a cor da borda do cartão.",
"change_the_border_color_of_the_input_fields": "Mude a cor da borda dos campos de entrada.",
"change_the_border_radius_of_the_card_and_the_inputs": "Muda o raio da borda do card e dos inputs.",
"change_the_brand_color_of_the_survey": "Muda a cor da marca da pesquisa.",
"change_the_placement_of_this_survey": "Muda a posição dessa pesquisa.",
"change_the_question_color_of_the_survey": "Muda a cor da pergunta da pesquisa.",
"changes_saved": "Mudanças salvas.",
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de pesquisa afetará a forma como ela pode ser compartilhada. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
"character_limit_toggle_description": "Limite o quão curta ou longa uma resposta pode ser.",
"character_limit_toggle_title": "Adicionar limites de caracteres",
"checkbox_label": "Rótulo da Caixa de Seleção",
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.",
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta do seu bloco",
@@ -1259,7 +1245,6 @@
"contact_fields": "Campos de Contato",
"contains": "contém",
"continue_to_settings": "Continuar para Configurações",
"control_which_file_types_can_be_uploaded": "Controlar quais tipos de arquivos podem ser enviados.",
"convert_to_multiple_choice": "Converter para Múltipla Escolha",
"convert_to_single_choice": "Converter para Escolha Única",
"country": "país",
@@ -1375,7 +1360,6 @@
"hide_progress_bar": "Esconder barra de progresso",
"hide_question_settings": "Ocultar configurações da pergunta",
"hostname": "nome do host",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}",
"if_you_need_more_please": "Se você precisar de mais, por favor",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar mostrando sempre que acionada até que uma resposta seja enviada.",
"ignore_global_waiting_time": "Ignorar período de espera",
@@ -1413,9 +1397,8 @@
"key": "chave",
"last_name": "Sobrenome",
"let_people_upload_up_to_25_files_at_the_same_time": "Deixe as pessoas fazerem upload de até 25 arquivos ao mesmo tempo.",
"limit_file_types": "Limitar tipos de arquivos",
"limit_the_maximum_file_size": "Limitar o tamanho máximo do arquivo",
"limit_upload_file_size_to": "Limitar tamanho do arquivo de upload para",
"limit_the_maximum_file_size": "Limitar o tamanho máximo de arquivo para uploads.",
"limit_upload_file_size_to": "Limitar tamanho de arquivo de upload para",
"link_survey_description": "Compartilhe um link para a página da pesquisa ou incorpore-a em uma página da web ou e-mail.",
"load_segment": "segmento de carga",
"logic_error_warning": "Mudar vai causar erros de lógica",
@@ -1428,7 +1411,7 @@
"matrix_all_fields": "Todos os campos",
"matrix_rows": "Linhas",
"max_file_size": "Tamanho máximo do arquivo",
"max_file_size_limit_is": "Tamanho máximo do arquivo é",
"max_file_size_limit_is": "O limite de tamanho máximo do arquivo é",
"move_question_to_block": "Mover pergunta para o bloco",
"multiply": "Multiplicar *",
"needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com",
@@ -1460,7 +1443,6 @@
"picture_idx": "Imagem {idx}",
"pin_can_only_contain_numbers": "O PIN só pode conter números.",
"pin_must_be_a_four_digit_number": "O PIN deve ser um número de quatro dígitos.",
"please_enter_a_file_extension": "Por favor, insira uma extensão de arquivo.",
"please_enter_a_valid_url": "Por favor, insira uma URL válida (ex.: https://example.com)",
"please_set_a_survey_trigger": "Por favor, configure um gatilho para a pesquisa",
"please_specify": "Por favor, especifique",
@@ -1471,7 +1453,6 @@
"protect_survey_with_pin_description": "Somente usuários que têm o PIN podem acessar a pesquisa.",
"publish": "Publicar",
"question": "Pergunta",
"question_color": "Cor da pergunta",
"question_deleted": "Pergunta deletada.",
"question_duplicated": "Pergunta duplicada.",
"question_id_updated": "ID da pergunta atualizado",
@@ -1539,6 +1520,7 @@
"search_for_images": "Buscar imagens",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos após acionar, a pesquisa será encerrada se não houver resposta",
"seconds_before_showing_the_survey": "segundos antes de mostrar a pesquisa.",
"select_field": "Selecionar campo",
"select_or_type_value": "Selecionar ou digitar valor",
"select_ordering": "Selecionar pedido",
"select_saved_action": "Selecionar ação salva",
@@ -1572,7 +1554,6 @@
"styling_set_to_theme_styles": "Estilo definido para os estilos do tema",
"subheading": "Subtítulo",
"subtract": "Subtrair -",
"suggest_colors": "Sugerir cores",
"survey_completed_heading": "Pesquisa Concluída",
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
"survey_display_settings": "Configurações de Exibição da Pesquisa",
@@ -1586,8 +1567,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar uma única vez, mesmo que não respondam.",
"then": "Então",
"this_action_will_remove_all_the_translations_from_this_survey": "Essa ação vai remover todas as traduções dessa pesquisa.",
"this_extension_is_already_added": "Essa extensão já foi adicionada.",
"this_file_type_is_not_supported": "Esse tipo de arquivo não é suportado.",
"three_points": "3 pontos",
"times": "times",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todas as pesquisas, você pode",
@@ -1608,6 +1587,48 @@
"upper_label": "Etiqueta Superior",
"url_filters": "Filtros de URL",
"url_not_supported": "URL não suportada",
"validation": {
"add_validation_rule": "Adicionar regra de validação",
"answer_all_rows": "Responda todas as linhas",
"characters": "Caracteres",
"contains": "Contém",
"delete_validation_rule": "Excluir regra de validação",
"does_not_contain": "Não contém",
"email": "É um e-mail válido",
"end_date": "Data final",
"file_extension_is": "A extensão do arquivo é",
"file_extension_is_not": "A extensão do arquivo não é",
"is": "É",
"is_between": "Está entre",
"is_earlier_than": "É anterior a",
"is_greater_than": "É maior que",
"is_later_than": "É posterior a",
"is_less_than": "É menor que",
"is_not": "Não é",
"is_not_between": "Não está entre",
"kb": "KB",
"max_length": "No máximo",
"max_selections": "No máximo",
"max_value": "No máximo",
"mb": "MB",
"min_length": "No mínimo",
"min_selections": "No mínimo",
"min_value": "No mínimo",
"minimum_options_ranked": "Mínimo de opções classificadas",
"minimum_rows_answered": "Mínimo de linhas respondidas",
"options_selected": "Opções selecionadas",
"pattern": "Corresponde ao padrão regex",
"phone": "É um telefone válido",
"rank_all_options": "Classificar todas as opções",
"select_file_extensions": "Selecionar extensões de arquivo...",
"select_option": "Selecionar opção",
"start_date": "Data inicial",
"url": "É uma URL válida"
},
"validation_logic_and": "Todas são verdadeiras",
"validation_logic_or": "qualquer uma é verdadeira",
"validation_rules": "Regras de validação",
"validation_rules_description": "Aceitar apenas respostas que atendam aos seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} está sendo usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está sendo usada na cota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
@@ -2015,6 +2036,31 @@
"look": {
"add_background_color": "Adicionar cor de fundo",
"add_background_color_description": "Adicione uma cor de fundo ao container do logo.",
"advanced_styling_field_border_radius": "Raio da borda",
"advanced_styling_field_button_bg": "Fundo do botão",
"advanced_styling_field_button_text": "Texto do botão",
"advanced_styling_field_description_color": "Cor da descrição",
"advanced_styling_field_description_size": "Tamanho da fonte da descrição",
"advanced_styling_field_font_size": "Tamanho da fonte",
"advanced_styling_field_font_weight": "Peso da fonte",
"advanced_styling_field_headline_color": "Cor do título",
"advanced_styling_field_headline_size": "Tamanho da fonte do título",
"advanced_styling_field_headline_weight": "Peso da fonte do título",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_indicator_bg": "Fundo do indicador",
"advanced_styling_field_input_text": "Texto da entrada",
"advanced_styling_field_option_bg": "Fundo",
"advanced_styling_field_option_label": "Cor do rótulo",
"advanced_styling_field_padding_x": "Espaçamento X",
"advanced_styling_field_padding_y": "Espaçamento Y",
"advanced_styling_field_placeholder_opacity": "Opacidade do placeholder",
"advanced_styling_field_shadow": "Sombra",
"advanced_styling_field_track_bg": "Fundo da trilha",
"advanced_styling_field_track_height": "Altura da trilha",
"advanced_styling_section_buttons": "Botões",
"advanced_styling_section_headlines": "Títulos e descrições",
"advanced_styling_section_inputs": "Campos de entrada",
"advanced_styling_section_options": "Opções (radio/checkbox)",
"app_survey_placement": "Posicionamento da pesquisa de app",
"app_survey_placement_settings_description": "Altere onde as pesquisas serão exibidas em seu aplicativo web ou site.",
"centered_modal_overlay_color": "Cor de sobreposição modal centralizada",
@@ -2028,6 +2074,9 @@
"formbricks_branding_hidden": "A marca Formbricks está oculta.",
"formbricks_branding_settings_description": "Adoramos seu apoio, mas entendemos se você desativar.",
"formbricks_branding_shown": "A marca Formbricks está visível.",
"generate_theme_btn": "Gerar",
"generate_theme_confirmation": "Você gostaria de gerar um tema de cores correspondente baseado na cor da sua marca? Isso substituirá suas configurações de cores atuais.",
"generate_theme_header": "Gerar tema de cores?",
"logo_removed_successfully": "Logo removido com sucesso",
"logo_settings_description": "Faça upload do logo da sua empresa para personalizar pesquisas e pré-visualizações de links.",
"logo_updated_successfully": "Logo atualizado com sucesso",
@@ -2042,6 +2091,7 @@
"show_formbricks_branding_in": "Mostrar marca Formbricks em pesquisas {type}",
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo atualizado com sucesso",
"suggest_colors": "Sugerir cores",
"theme": "Tema",
"theme_settings_description": "Crie um tema de estilo para todas as pesquisas. Você pode ativar estilo personalizado para cada pesquisa."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Impressão",
"in_progress": "Em Progresso",
"inactive_surveys": "Inquéritos inativos",
"input_type": "Tipo de entrada",
"integration": "integração",
"integrations": "Integrações",
"invalid_date": "Data inválida",
@@ -267,13 +266,11 @@
"look_and_feel": "Aparência e Sensação",
"manage": "Gerir",
"marketing": "Marketing",
"maximum": "Máximo",
"member": "Membro",
"members": "Membros",
"members_and_teams": "Membros e equipas",
"membership_not_found": "Associação não encontrada",
"metadata": "Metadados",
"minimum": "Mínimo",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor num ecrã maior. Para gerir ou criar inquéritos, mude de dispositivo.",
"mobile_overlay_surveys_look_good": "Não se preocupe os seus inquéritos têm uma ótima aparência em todos os dispositivos e tamanhos de ecrã!",
"mobile_overlay_title": "Oops, ecrã pequeno detectado!",
@@ -326,7 +323,7 @@
"placeholder": "Espaço reservado",
"please_select_at_least_one_survey": "Por favor, selecione pelo menos um inquérito",
"please_select_at_least_one_trigger": "Por favor, selecione pelo menos um gatilho",
"please_upgrade_your_plan": "Por favor, atualize o seu plano.",
"please_upgrade_your_plan": "Por favor, atualize o seu plano",
"preview": "Pré-visualização",
"preview_survey": "Pré-visualização do inquérito",
"privacy": "Política de Privacidade",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Adicionar um espaço reservado para mostrar se não houver valor para recordar.",
"add_hidden_field_id": "Adicionar ID do campo oculto",
"add_highlight_border": "Adicionar borda de destaque",
"add_highlight_border_description": "Adicione uma borda externa ao seu cartão de inquérito.",
"add_logic": "Adicionar lógica",
"add_none_of_the_above": "Adicionar \"Nenhuma das Opções Acima\"",
"add_option": "Adicionar opção",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Alterar a mensagem que os visitantes veem quando o inquérito está fechado.",
"adjust_the_theme_in_the": "Ajustar o tema no",
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
"allow_file_type": "Permitir tipo de ficheiro",
"allow_multi_select": "Permitir seleção múltipla",
"allow_multiple_files": "Permitir vários ficheiros",
"allow_users_to_select_more_than_one_image": "Permitir aos utilizadores selecionar mais do que uma imagem",
@@ -1227,19 +1222,10 @@
"change_background": "Alterar fundo",
"change_question_type": "Alterar tipo de pergunta",
"change_survey_type": "Alterar o tipo de inquérito afeta o acesso existente",
"change_the_background_color_of_the_card": "Alterar a cor de fundo do cartão",
"change_the_background_color_of_the_input_fields": "Alterar a cor de fundo dos campos de entrada",
"change_the_background_to_a_color_image_or_animation": "Altere o fundo para uma cor, imagem ou animação",
"change_the_border_color_of_the_card": "Alterar a cor da borda do cartão.",
"change_the_border_color_of_the_input_fields": "Alterar a cor da borda dos campos de entrada",
"change_the_border_radius_of_the_card_and_the_inputs": "Alterar o raio da borda do cartão e dos campos de entrada",
"change_the_brand_color_of_the_survey": "Alterar a cor da marca do inquérito",
"change_the_placement_of_this_survey": "Alterar a colocação deste inquérito.",
"change_the_question_color_of_the_survey": "Alterar a cor da pergunta do inquérito",
"changes_saved": "Alterações guardadas.",
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de inquérito afetará como ele pode ser partilhado. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
"character_limit_toggle_description": "Limitar o quão curta ou longa uma resposta pode ser.",
"character_limit_toggle_title": "Adicionar limites de caracteres",
"checkbox_label": "Rótulo da Caixa de Seleção",
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.",
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta no seu bloco",
@@ -1259,7 +1245,6 @@
"contact_fields": "Campos de Contacto",
"contains": "Contém",
"continue_to_settings": "Continuar para Definições",
"control_which_file_types_can_be_uploaded": "Controlar quais tipos de ficheiros podem ser carregados.",
"convert_to_multiple_choice": "Converter para Seleção Múltipla",
"convert_to_single_choice": "Converter para Seleção Única",
"country": "País",
@@ -1375,7 +1360,6 @@
"hide_progress_bar": "Ocultar barra de progresso",
"hide_question_settings": "Ocultar definições da pergunta",
"hostname": "Nome do host",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}",
"if_you_need_more_please": "Se precisar de mais, por favor",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar a mostrar sempre que acionado até que uma resposta seja submetida.",
"ignore_global_waiting_time": "Ignorar período de espera",
@@ -1413,9 +1397,8 @@
"key": "Chave",
"last_name": "Apelido",
"let_people_upload_up_to_25_files_at_the_same_time": "Permitir que as pessoas carreguem até 25 ficheiros ao mesmo tempo.",
"limit_file_types": "Limitar tipos de ficheiros",
"limit_the_maximum_file_size": "Limitar o tamanho máximo do ficheiro",
"limit_upload_file_size_to": "Limitar tamanho do ficheiro carregado a",
"limit_the_maximum_file_size": "Limitar o tamanho máximo de ficheiro para carregamentos.",
"limit_upload_file_size_to": "Limitar o tamanho de ficheiro de carregamento para",
"link_survey_description": "Partilhe um link para uma página de inquérito ou incorpore-o numa página web ou email.",
"load_segment": "Carregar segmento",
"logic_error_warning": "A alteração causará erros de lógica",
@@ -1427,8 +1410,8 @@
"manage_languages": "Gerir Idiomas",
"matrix_all_fields": "Todos os campos",
"matrix_rows": "Linhas",
"max_file_size": "Tamanho máximo do ficheiro",
"max_file_size_limit_is": "O limite do tamanho máximo do ficheiro é",
"max_file_size": "Tamanho máximo de ficheiro",
"max_file_size_limit_is": "O limite de tamanho máximo de ficheiro é",
"move_question_to_block": "Mover pergunta para o bloco",
"multiply": "Multiplicar *",
"needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com",
@@ -1460,7 +1443,6 @@
"picture_idx": "Imagem {idx}",
"pin_can_only_contain_numbers": "O PIN só pode conter números.",
"pin_must_be_a_four_digit_number": "O PIN deve ser um número de quatro dígitos.",
"please_enter_a_file_extension": "Por favor, insira uma extensão de ficheiro.",
"please_enter_a_valid_url": "Por favor, insira um URL válido (por exemplo, https://example.com)",
"please_set_a_survey_trigger": "Por favor, defina um desencadeador de inquérito",
"please_specify": "Por favor, especifique",
@@ -1471,7 +1453,6 @@
"protect_survey_with_pin_description": "Apenas utilizadores com o PIN podem aceder ao inquérito.",
"publish": "Publicar",
"question": "Pergunta",
"question_color": "Cor da pergunta",
"question_deleted": "Pergunta eliminada.",
"question_duplicated": "Pergunta duplicada.",
"question_id_updated": "ID da pergunta atualizado",
@@ -1539,6 +1520,7 @@
"search_for_images": "Procurar imagens",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "segundos após o acionamento o inquérito será fechado se não houver resposta",
"seconds_before_showing_the_survey": "segundos antes de mostrar o inquérito.",
"select_field": "Selecionar campo",
"select_or_type_value": "Selecionar ou digitar valor",
"select_ordering": "Selecionar ordem",
"select_saved_action": "Selecionar ação guardada",
@@ -1572,7 +1554,6 @@
"styling_set_to_theme_styles": "Estilo definido para estilos do tema",
"subheading": "Subtítulo",
"subtract": "Subtrair -",
"suggest_colors": "Sugerir cores",
"survey_completed_heading": "Inquérito Concluído",
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
"survey_display_settings": "Configurações de Exibição do Inquérito",
@@ -1586,8 +1567,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Mostrar uma única vez, mesmo que não respondam.",
"then": "Então",
"this_action_will_remove_all_the_translations_from_this_survey": "Esta ação irá remover todas as traduções deste inquérito.",
"this_extension_is_already_added": "Esta extensão já está adicionada.",
"this_file_type_is_not_supported": "Este tipo de ficheiro não é suportado.",
"three_points": "3 pontos",
"times": "tempos",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Para manter a colocação consistente em todos os questionários, pode",
@@ -1608,6 +1587,48 @@
"upper_label": "Etiqueta Superior",
"url_filters": "Filtros de URL",
"url_not_supported": "URL não suportado",
"validation": {
"add_validation_rule": "Adicionar regra de validação",
"answer_all_rows": "Responda a todas as linhas",
"characters": "Caracteres",
"contains": "Contém",
"delete_validation_rule": "Eliminar regra de validação",
"does_not_contain": "Não contém",
"email": "É um email válido",
"end_date": "Data de fim",
"file_extension_is": "A extensão do ficheiro é",
"file_extension_is_not": "A extensão do ficheiro não é",
"is": "É",
"is_between": "Está entre",
"is_earlier_than": "É anterior a",
"is_greater_than": "É maior que",
"is_later_than": "É posterior a",
"is_less_than": "É menor que",
"is_not": "Não é",
"is_not_between": "Não está entre",
"kb": "KB",
"max_length": "No máximo",
"max_selections": "No máximo",
"max_value": "No máximo",
"mb": "MB",
"min_length": "Pelo menos",
"min_selections": "Pelo menos",
"min_value": "Pelo menos",
"minimum_options_ranked": "Opções mínimas classificadas",
"minimum_rows_answered": "Linhas mínimas respondidas",
"options_selected": "Opções selecionadas",
"pattern": "Coincide com o padrão regex",
"phone": "É um telefone válido",
"rank_all_options": "Classificar todas as opções",
"select_file_extensions": "Selecionar extensões de ficheiro...",
"select_option": "Selecionar opção",
"start_date": "Data de início",
"url": "É um URL válido"
},
"validation_logic_and": "Todas são verdadeiras",
"validation_logic_or": "qualquer uma é verdadeira",
"validation_rules": "Regras de validação",
"validation_rules_description": "Aceitar apenas respostas que cumpram os seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está a ser utilizada na quota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
@@ -2015,6 +2036,31 @@
"look": {
"add_background_color": "Adicionar cor de fundo",
"add_background_color_description": "Adicione uma cor de fundo ao contentor do logótipo.",
"advanced_styling_field_border_radius": "Raio da borda",
"advanced_styling_field_button_bg": "Fundo do botão",
"advanced_styling_field_button_text": "Texto do botão",
"advanced_styling_field_description_color": "Cor da descrição",
"advanced_styling_field_description_size": "Tamanho da fonte da descrição",
"advanced_styling_field_font_size": "Tamanho da fonte",
"advanced_styling_field_font_weight": "Espessura da fonte",
"advanced_styling_field_headline_color": "Cor do título",
"advanced_styling_field_headline_size": "Tamanho da fonte do título",
"advanced_styling_field_headline_weight": "Espessura da fonte do título",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_indicator_bg": "Fundo do indicador",
"advanced_styling_field_input_text": "Texto do campo de entrada",
"advanced_styling_field_option_bg": "Fundo",
"advanced_styling_field_option_label": "Cor da etiqueta",
"advanced_styling_field_padding_x": "Espaçamento X",
"advanced_styling_field_padding_y": "Espaçamento Y",
"advanced_styling_field_placeholder_opacity": "Opacidade do marcador de posição",
"advanced_styling_field_shadow": "Sombra",
"advanced_styling_field_track_bg": "Fundo da barra",
"advanced_styling_field_track_height": "Altura da barra",
"advanced_styling_section_buttons": "Botões",
"advanced_styling_section_headlines": "Títulos e descrições",
"advanced_styling_section_inputs": "Campos de entrada",
"advanced_styling_section_options": "Opções (botões de opção/caixas de verificação)",
"app_survey_placement": "Colocação do inquérito (app)",
"app_survey_placement_settings_description": "Altere onde os inquéritos serão apresentados na sua aplicação web ou website.",
"centered_modal_overlay_color": "Cor da sobreposição modal centralizada",
@@ -2028,6 +2074,9 @@
"formbricks_branding_hidden": "A marca Formbricks está oculta.",
"formbricks_branding_settings_description": "Adoramos o seu apoio, mas compreendemos se preferir desativar.",
"formbricks_branding_shown": "A marca Formbricks está visível.",
"generate_theme_btn": "Gerar",
"generate_theme_confirmation": "Deseja gerar um tema de cores correspondente com base na cor da sua marca? Isto irá substituir as suas definições de cor atuais.",
"generate_theme_header": "Gerar tema de cores?",
"logo_removed_successfully": "Logótipo removido com sucesso",
"logo_settings_description": "Carregue o logótipo da sua empresa para personalizar inquéritos e pré-visualizações de links.",
"logo_updated_successfully": "Logótipo atualizado com sucesso",
@@ -2042,6 +2091,7 @@
"show_formbricks_branding_in": "Mostrar marca Formbricks em inquéritos {type}",
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
"styling_updated_successfully": "Estilo atualizado com sucesso",
"suggest_colors": "Sugerir cores",
"theme": "Tema",
"theme_settings_description": "Crie um tema de estilo para todos os inquéritos. Pode ativar estilos personalizados para cada inquérito."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Amprentă",
"in_progress": "În progres",
"inactive_surveys": "Sondaje inactive",
"input_type": "Tipul de intrare",
"integration": "integrare",
"integrations": "Integrări",
"invalid_date": "Dată invalidă",
@@ -267,13 +266,11 @@
"look_and_feel": "Aspect și Comportament",
"manage": "Gestionați",
"marketing": "Marketing",
"maximum": "Maximum",
"member": "Membru",
"members": "Membri",
"members_and_teams": "Membri și echipe",
"membership_not_found": "Apartenența nu a fost găsită",
"metadata": "Metadate",
"minimum": "Minim",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funcționează cel mai bine pe un ecran mai mare. Pentru a gestiona sau crea chestionare, treceți la un alt dispozitiv.",
"mobile_overlay_surveys_look_good": "Nu vă faceți griji chestionarele dumneavoastră arată grozav pe orice dispozitiv și dimensiune a ecranului!",
"mobile_overlay_title": "Ups, ecran mic detectat!",
@@ -326,7 +323,7 @@
"placeholder": "Marcaj substituent",
"please_select_at_least_one_survey": "Vă rugăm să selectați cel puțin un sondaj",
"please_select_at_least_one_trigger": "Vă rugăm să selectați cel puțin un declanșator",
"please_upgrade_your_plan": "Vă rugăm să vă actualizați planul.",
"please_upgrade_your_plan": "Vă rugăm să faceți upgrade la planul dumneavoastră",
"preview": "Previzualizare",
"preview_survey": "Previzualizare Chestionar",
"privacy": "Politica de Confidențialitate",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Adaugă un placeholder pentru a afișa dacă nu există valoare de reamintit",
"add_hidden_field_id": "Adăugați ID câmp ascuns",
"add_highlight_border": "Adaugă bordură evidențiată",
"add_highlight_border_description": "Adaugă o margine exterioară cardului tău de sondaj.",
"add_logic": "Adaugă logică",
"add_none_of_the_above": "Adăugați \"Niciuna dintre cele de mai sus\"",
"add_option": "Adăugați opțiune",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Schimbați mesajul pe care îl văd vizitatorii atunci când sondajul este închis.",
"adjust_the_theme_in_the": "Ajustați tema în",
"all_other_answers_will_continue_to": "Toate celelalte răspunsuri vor continua să",
"allow_file_type": "Permite tipul de fișier",
"allow_multi_select": "Permite selectare multiplă",
"allow_multiple_files": "Permite fișiere multiple",
"allow_users_to_select_more_than_one_image": "Permite utilizatorilor să selecteze mai mult de o imagine",
@@ -1227,19 +1222,10 @@
"change_background": "Schimbați fundalul",
"change_question_type": "Schimbă tipul întrebării",
"change_survey_type": "Schimbarea tipului chestionarului afectează accesul existent",
"change_the_background_color_of_the_card": "Schimbați culoarea de fundal a cardului.",
"change_the_background_color_of_the_input_fields": "Schimbați culoarea de fundal a câmpurilor de introducere.",
"change_the_background_to_a_color_image_or_animation": "Schimbați fundalul cu o culoare, imagine sau animație.",
"change_the_border_color_of_the_card": "Schimbați culoarea bordurii cardului.",
"change_the_border_color_of_the_input_fields": "Schimbați culoarea bordurii câmpurilor de introducere.",
"change_the_border_radius_of_the_card_and_the_inputs": "Schimbați raza de rotunjire a cardului și a câmpurilor de introducere.",
"change_the_brand_color_of_the_survey": "Schimbați culoarea brandului chestionarului",
"change_the_placement_of_this_survey": "Schimbă amplasarea acestui sondaj.",
"change_the_question_color_of_the_survey": "Schimbați culoarea întrebării chestionarului.",
"changes_saved": "Modificările au fost salvate",
"changing_survey_type_will_remove_existing_distribution_channels": "Schimbarea tipului chestionarului va afecta modul în care acesta poate fi distribuit. Dacă respondenții au deja linkuri de acces pentru tipul curent, aceștia ar putea pierde accesul după schimbare.",
"character_limit_toggle_description": "Limitați cât de scurt sau lung poate fi un răspuns.",
"character_limit_toggle_title": "Adăugați limite de caractere",
"checkbox_label": "Etichetă casetă de selectare",
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
"choose_the_first_question_on_your_block": "Alege prima întrebare din blocul tău",
@@ -1259,7 +1245,6 @@
"contact_fields": "Câmpuri de contact",
"contains": "Conține",
"continue_to_settings": "Continuă către Setări",
"control_which_file_types_can_be_uploaded": "Controlează ce tipuri de fișiere pot fi încărcate.",
"convert_to_multiple_choice": "Convertiți la selectare multiplă",
"convert_to_single_choice": "Convertiți la selectare unică",
"country": "Țară",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "Ascunde setările întrebării",
"hostname": "Nume gazdă",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}",
"if_you_need_more_please": "Dacă aveți nevoie de mai multe, vă rugăm",
"if_you_need_more_please": "Dacă aveți nevoie de mai mult, vă rugăm",
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuă afișarea ori de câte ori este declanșat până când se trimite un răspuns.",
"ignore_global_waiting_time": "Ignoră perioada de răcire",
"ignore_global_waiting_time_description": "Acest sondaj poate fi afișat ori de câte ori condițiile sale sunt îndeplinite, chiar dacă un alt sondaj a fost afișat recent.",
@@ -1413,9 +1398,8 @@
"key": "Cheie",
"last_name": "Nume de familie",
"let_people_upload_up_to_25_files_at_the_same_time": "Permiteți utilizatorilor să încarce până la 25 de fișiere simultan.",
"limit_file_types": "Limitare tipuri de fișiere",
"limit_the_maximum_file_size": "Limitează dimensiunea maximă a fișierului",
"limit_upload_file_size_to": "Limitați dimensiunea fișierului de încărcare la",
"limit_the_maximum_file_size": "Limitați dimensiunea maximă a fișierului pentru încărcări.",
"limit_upload_file_size_to": "Limitați dimensiunea fișierului încărcat la",
"link_survey_description": "Partajați un link către o pagină de chestionar sau încorporați-l într-o pagină web sau email.",
"load_segment": "Încarcă segment",
"logic_error_warning": "Schimbarea va provoca erori de logică",
@@ -1428,7 +1412,7 @@
"matrix_all_fields": "Toate câmpurile",
"matrix_rows": "Rânduri",
"max_file_size": "Dimensiune maximă fișier",
"max_file_size_limit_is": "Limita dimensiunii maxime a fișierului este",
"max_file_size_limit_is": "Limita maximă pentru dimensiunea fișierului este",
"move_question_to_block": "Mută întrebarea în bloc",
"multiply": "Multiplicare",
"needed_for_self_hosted_cal_com_instance": "Necesar pentru un exemplu autogăzduit Cal.com",
@@ -1460,7 +1444,6 @@
"picture_idx": "Poză {idx}",
"pin_can_only_contain_numbers": "PIN-ul poate conține doar numere.",
"pin_must_be_a_four_digit_number": "PIN-ul trebuie să fie un număr de patru cifre",
"please_enter_a_file_extension": "Vă rugăm să introduceți o extensie de fișier.",
"please_enter_a_valid_url": "Vă rugăm să introduceți un URL valid (de exemplu, https://example.com)",
"please_set_a_survey_trigger": "Vă rugăm să setați un declanșator sondaj",
"please_specify": "Vă rugăm să specificați",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "Doar utilizatorii care cunosc PIN-ul pot accesa sondajul.",
"publish": "Publică",
"question": "Întrebare",
"question_color": "Culoarea întrebării",
"question_deleted": "Întrebare ștearsă.",
"question_duplicated": "Întrebare duplicată.",
"question_id_updated": "ID întrebare actualizat",
@@ -1539,6 +1521,7 @@
"search_for_images": "Căutare de imagini",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "secunde după declanșare sondajul va fi închis dacă nu există niciun răspuns",
"seconds_before_showing_the_survey": "secunde înainte de afișarea sondajului",
"select_field": "Selectează câmpul",
"select_or_type_value": "Selectați sau introduceți valoarea",
"select_ordering": "Selectează ordonarea",
"select_saved_action": "Selectați acțiunea salvată",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "Stilizare setată la stilurile temei",
"subheading": "Subtitlu",
"subtract": "Scade -",
"suggest_colors": "Sugerați culori",
"survey_completed_heading": "Sondaj Completat",
"survey_completed_subheading": "Acest sondaj gratuit și open-source a fost închis",
"survey_display_settings": "Setări de afișare a sondajului",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Afișează o singură dată, chiar dacă persoana nu răspunde.",
"then": "Apoi",
"this_action_will_remove_all_the_translations_from_this_survey": "Această acțiune va elimina toate traducerile din acest sondaj.",
"this_extension_is_already_added": "Această extensie este deja adăugată.",
"this_file_type_is_not_supported": "Acest tip de fișier nu este acceptat.",
"three_points": "3 puncte",
"times": "ori",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Pentru a menține amplasarea consecventă pentru toate sondajele, puteți",
@@ -1608,6 +1588,48 @@
"upper_label": "Etichetă superioară",
"url_filters": "Filtre URL",
"url_not_supported": "URL nesuportat",
"validation": {
"add_validation_rule": "Adaugă regulă de validare",
"answer_all_rows": "Răspunde la toate rândurile",
"characters": "Caractere",
"contains": "Conține",
"delete_validation_rule": "Șterge regula de validare",
"does_not_contain": "Nu conține",
"email": "Este un email valid",
"end_date": "Data de sfârșit",
"file_extension_is": "Extensia fișierului este",
"file_extension_is_not": "Extensia fișierului nu este",
"is": "Este",
"is_between": "Este între",
"is_earlier_than": "Este mai devreme decât",
"is_greater_than": "Este mai mare decât",
"is_later_than": "Este mai târziu decât",
"is_less_than": "Este mai mic decât",
"is_not": "Nu este",
"is_not_between": "Nu este între",
"kb": "KB",
"max_length": "Cel mult",
"max_selections": "Cel mult",
"max_value": "Cel mult",
"mb": "MB",
"min_length": "Cel puțin",
"min_selections": "Cel puțin",
"min_value": "Cel puțin",
"minimum_options_ranked": "Număr minim de opțiuni ordonate",
"minimum_rows_answered": "Număr minim de rânduri completate",
"options_selected": "Opțiuni selectate",
"pattern": "Se potrivește cu un șablon regex",
"phone": "Este un număr de telefon valid",
"rank_all_options": "Ordonați toate opțiunile",
"select_file_extensions": "Selectați extensiile de fișier...",
"select_option": "Selectează opțiunea",
"start_date": "Data de început",
"url": "Este un URL valid"
},
"validation_logic_and": "Toate sunt adevărate",
"validation_logic_or": "oricare este adevărată",
"validation_rules": "Reguli de validare",
"validation_rules_description": "Acceptă doar răspunsurile care îndeplinesc următoarele criterii",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} este folosit în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabila \"{variableName}\" este folosită în cota \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "Numele variabilei este deja utilizat, vă rugăm să alegeți altul.",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "Adăugați culoare de fundal",
"add_background_color_description": "Adăugați o culoare de fundal la containerul siglei.",
"advanced_styling_field_border_radius": "Raza colțurilor",
"advanced_styling_field_button_bg": "Fundal buton",
"advanced_styling_field_button_text": "Text buton",
"advanced_styling_field_description_color": "Culoare descriere",
"advanced_styling_field_description_size": "Dimensiune font descriere",
"advanced_styling_field_font_size": "Dimensiune font",
"advanced_styling_field_font_weight": "Grosime font",
"advanced_styling_field_headline_color": "Culoare titlu",
"advanced_styling_field_headline_size": "Dimensiune font titlu",
"advanced_styling_field_headline_weight": "Grosime font titlu",
"advanced_styling_field_height": "Înălțime",
"advanced_styling_field_indicator_bg": "Fundal indicator",
"advanced_styling_field_input_text": "Text câmp introducere",
"advanced_styling_field_option_bg": "Fundal",
"advanced_styling_field_option_label": "Culoare etichetă",
"advanced_styling_field_padding_x": "Spațiere X",
"advanced_styling_field_padding_y": "Spațiere Y",
"advanced_styling_field_placeholder_opacity": "Opacitate placeholder",
"advanced_styling_field_shadow": "Umbră",
"advanced_styling_field_track_bg": "Fundal pistă",
"advanced_styling_field_track_height": "Înălțime pistă",
"advanced_styling_section_buttons": "Butoane",
"advanced_styling_section_headlines": "Titluri și descrieri",
"advanced_styling_section_inputs": "Câmpuri de introducere",
"advanced_styling_section_options": "Opțiuni (Radio/Checkbox)",
"app_survey_placement": "Amplasarea sondajului în aplicație",
"app_survey_placement_settings_description": "Schimbați unde vor fi afișate sondajele în aplicația sau site-ul dvs. web.",
"centered_modal_overlay_color": "Culoare suprapunere modală centralizată",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Brandingul Formbricks este ascuns.",
"formbricks_branding_settings_description": "Ne bucurăm de susținerea ta, dar înțelegem dacă vrei să dezactivezi această opțiune.",
"formbricks_branding_shown": "Brandingul Formbricks este afișat.",
"generate_theme_btn": "Generează",
"generate_theme_confirmation": "Doriți să generați o temă de culori asortată pe baza culorii brandului dumneavoastră? Aceasta va suprascrie setările actuale de culoare.",
"generate_theme_header": "Generați temă de culori?",
"logo_removed_successfully": "Sigla a fost eliminată cu succes",
"logo_settings_description": "Încarcă sigla companiei pentru a personaliza sondajele și previzualizările de linkuri.",
"logo_updated_successfully": "Sigla a fost actualizată cu succes",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "Afișează brandingul Formbricks în sondajele de tip {type}",
"show_powered_by_formbricks": "Afișează semnătura „Powered by Formbricks”",
"styling_updated_successfully": "Stilizarea a fost actualizată cu succes",
"suggest_colors": "Sugerați culori",
"theme": "Temă",
"theme_settings_description": "Creează o temă de stil pentru toate sondajele. Poți activa stilizare personalizată pentru fiecare sondaj."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Выходные данные",
"in_progress": "В процессе",
"inactive_surveys": "Неактивные опросы",
"input_type": "Тип ввода",
"integration": "интеграция",
"integrations": "Интеграции",
"invalid_date": "Неверная дата",
@@ -267,13 +266,11 @@
"look_and_feel": "Внешний вид",
"manage": "Управление",
"marketing": "Маркетинг",
"maximum": "Максимум",
"member": "Участник",
"members": "Участники",
"members_and_teams": "Участники и команды",
"membership_not_found": "Участие не найдено",
"metadata": "Метаданные",
"minimum": "Минимум",
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
"mobile_overlay_title": "Ой, обнаружен маленький экран!",
@@ -326,7 +323,7 @@
"placeholder": "Заполнитель",
"please_select_at_least_one_survey": "Пожалуйста, выберите хотя бы один опрос",
"please_select_at_least_one_trigger": "Пожалуйста, выберите хотя бы один триггер",
"please_upgrade_your_plan": "Пожалуйста, обновите ваш тарифный план.",
"please_upgrade_your_plan": "Пожалуйста, обновите ваш тарифный план",
"preview": "Предпросмотр",
"preview_survey": "Предпросмотр опроса",
"privacy": "Политика конфиденциальности",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Добавить плейсхолдер, который будет показан, если нет значения для отображения.",
"add_hidden_field_id": "Добавить скрытый ID поля",
"add_highlight_border": "Добавить выделяющую рамку",
"add_highlight_border_description": "Добавьте внешнюю рамку к карточке опроса.",
"add_logic": "Добавить логику",
"add_none_of_the_above": "Добавить вариант «Ничего из вышеперечисленного»",
"add_option": "Добавить вариант",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Измените сообщение, которое видят посетители, когда опрос закрыт.",
"adjust_the_theme_in_the": "Настройте тему в",
"all_other_answers_will_continue_to": "Все остальные ответы будут продолжать",
"allow_file_type": "Разрешить тип файла",
"allow_multi_select": "Разрешить множественный выбор",
"allow_multiple_files": "Разрешить несколько файлов",
"allow_users_to_select_more_than_one_image": "Разрешить пользователям выбирать более одного изображения",
@@ -1227,19 +1222,10 @@
"change_background": "Изменить фон",
"change_question_type": "Изменить тип вопроса",
"change_survey_type": "Смена типа опроса влияет на существующий доступ",
"change_the_background_color_of_the_card": "Изменить цвет фона карточки.",
"change_the_background_color_of_the_input_fields": "Изменить цвет фона полей ввода.",
"change_the_background_to_a_color_image_or_animation": "Изменить фон на цвет, изображение или анимацию.",
"change_the_border_color_of_the_card": "Изменить цвет рамки карточки.",
"change_the_border_color_of_the_input_fields": "Изменить цвет рамки полей ввода.",
"change_the_border_radius_of_the_card_and_the_inputs": "Изменить скругление углов карточки и полей ввода.",
"change_the_brand_color_of_the_survey": "Изменить фирменный цвет опроса.",
"change_the_placement_of_this_survey": "Изменить размещение этого опроса.",
"change_the_question_color_of_the_survey": "Изменить цвет вопросов в опросе.",
"changes_saved": "Изменения сохранены.",
"changing_survey_type_will_remove_existing_distribution_channels": "Изменение типа опроса повлияет на способы его распространения. Если у респондентов уже есть ссылки для доступа к текущему типу, после смены они могут потерять доступ.",
"character_limit_toggle_description": "Ограничьте минимальную и максимальную длину ответа.",
"character_limit_toggle_title": "Добавить ограничения на количество символов",
"checkbox_label": "Метка флажка",
"choose_the_actions_which_trigger_the_survey": "Выберите действия, которые запускают опрос.",
"choose_the_first_question_on_your_block": "Выберите первый вопрос в вашем блоке",
@@ -1259,7 +1245,6 @@
"contact_fields": "Поля контакта",
"contains": "Содержит",
"continue_to_settings": "Перейти к настройкам",
"control_which_file_types_can_be_uploaded": "Управляйте типами файлов, которые можно загружать.",
"convert_to_multiple_choice": "Преобразовать в мультивыбор",
"convert_to_single_choice": "Преобразовать в одиночный выбор",
"country": "Страна",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "Скрыть настройки вопроса",
"hostname": "Имя хоста",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Насколько необычными вы хотите сделать карточки в опросах типа {surveyTypeDerived}",
"if_you_need_more_please": "Если нужно больше, пожалуйста",
"if_you_need_more_please": "Если вам нужно больше, пожалуйста",
"if_you_really_want_that_answer_ask_until_you_get_it": "Показывать каждый раз при срабатывании, пока не будет получен ответ.",
"ignore_global_waiting_time": "Игнорировать период ожидания",
"ignore_global_waiting_time_description": "Этот опрос может отображаться при выполнении условий, даже если недавно уже был показан другой опрос.",
@@ -1413,8 +1398,7 @@
"key": "Ключ",
"last_name": "Фамилия",
"let_people_upload_up_to_25_files_at_the_same_time": "Разрешить загружать до 25 файлов одновременно.",
"limit_file_types": "Ограничить типы файлов",
"limit_the_maximum_file_size": "Ограничить максимальный размер файла",
"limit_the_maximum_file_size": "Ограничьте максимальный размер загружаемых файлов.",
"limit_upload_file_size_to": "Ограничить размер загружаемого файла до",
"link_survey_description": "Поделитесь ссылкой на страницу опроса или вставьте её на веб-страницу или в электронное письмо.",
"load_segment": "Загрузить сегмент",
@@ -1460,7 +1444,6 @@
"picture_idx": "Изображение {idx}",
"pin_can_only_contain_numbers": "PIN-код может содержать только цифры.",
"pin_must_be_a_four_digit_number": "PIN-код должен состоять из четырёх цифр.",
"please_enter_a_file_extension": "Пожалуйста, введите расширение файла.",
"please_enter_a_valid_url": "Пожалуйста, введите корректный URL (например, https://example.com)",
"please_set_a_survey_trigger": "Пожалуйста, установите триггер опроса",
"please_specify": "Пожалуйста, уточните",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "Только пользователи, у которых есть PIN-код, могут получить доступ к опросу.",
"publish": "Опубликовать",
"question": "Вопрос",
"question_color": "Цвет вопроса",
"question_deleted": "Вопрос удалён.",
"question_duplicated": "Вопрос дублирован.",
"question_id_updated": "ID вопроса обновлён",
@@ -1539,6 +1521,7 @@
"search_for_images": "Поиск изображений",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "секунд после запуска — опрос будет закрыт, если не будет ответа",
"seconds_before_showing_the_survey": "секунд до показа опроса.",
"select_field": "Выберите поле",
"select_or_type_value": "Выберите или введите значение",
"select_ordering": "Выберите порядок",
"select_saved_action": "Выберите сохранённое действие",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "Оформление установлено в соответствии с темой",
"subheading": "Подзаголовок",
"subtract": "Вычесть -",
"suggest_colors": "Предложить цвета",
"survey_completed_heading": "Опрос завершён",
"survey_completed_subheading": "Этот бесплатный и открытый опрос был закрыт",
"survey_display_settings": "Настройки отображения опроса",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Показать один раз, даже если не будет ответа.",
"then": "Затем",
"this_action_will_remove_all_the_translations_from_this_survey": "Это действие удалит все переводы из этого опроса.",
"this_extension_is_already_added": "Это расширение уже добавлено.",
"this_file_type_is_not_supported": "Этот тип файла не поддерживается.",
"three_points": "3 балла",
"times": "раз",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "Чтобы сохранить единое расположение во всех опросах, вы можете",
@@ -1608,6 +1588,48 @@
"upper_label": "Верхняя метка",
"url_filters": "Фильтры URL",
"url_not_supported": "URL не поддерживается",
"validation": {
"add_validation_rule": "Добавить правило проверки",
"answer_all_rows": "Ответьте на все строки",
"characters": "Символы",
"contains": "Содержит",
"delete_validation_rule": "Удалить правило проверки",
"does_not_contain": "Не содержит",
"email": "Корректный email",
"end_date": "Дата окончания",
"file_extension_is": "Расширение файла —",
"file_extension_is_not": "Расширение файла не является",
"is": "Является",
"is_between": "Находится между",
"is_earlier_than": "Ранее чем",
"is_greater_than": "Больше чем",
"is_later_than": "Позже чем",
"is_less_than": "Меньше чем",
"is_not": "Не является",
"is_not_between": "Не находится между",
"kb": "КБ",
"max_length": "Не более",
"max_selections": "Не более",
"max_value": "Не более",
"mb": "МБ",
"min_length": "Не менее",
"min_selections": "Не менее",
"min_value": "Не менее",
"minimum_options_ranked": "Минимальное количество ранжированных вариантов",
"minimum_rows_answered": "Минимальное количество заполненных строк",
"options_selected": "Выбранные опции",
"pattern": "Соответствует шаблону regex",
"phone": "Корректный телефон",
"rank_all_options": "Ранжируйте все опции",
"select_file_extensions": "Выберите расширения файлов...",
"select_option": "Выберите вариант",
"start_date": "Дата начала",
"url": "Корректный URL"
},
"validation_logic_and": "Все условия выполняются",
"validation_logic_or": "выполняется хотя бы одно условие",
"validation_rules": "Правила валидации",
"validation_rules_description": "Принимать только ответы, соответствующие следующим критериям",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Переменная «{variableName}» используется в квоте «{quotaName}»",
"variable_name_is_already_taken_please_choose_another": "Это имя переменной уже занято, выберите другое.",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "Добавить цвет фона",
"add_background_color_description": "Добавьте цвет фона для контейнера с логотипом.",
"advanced_styling_field_border_radius": "Радиус скругления",
"advanced_styling_field_button_bg": "Фон кнопки",
"advanced_styling_field_button_text": "Текст кнопки",
"advanced_styling_field_description_color": "Цвет описания",
"advanced_styling_field_description_size": "Размер шрифта описания",
"advanced_styling_field_font_size": "Размер шрифта",
"advanced_styling_field_font_weight": "Толщина шрифта",
"advanced_styling_field_headline_color": "Цвет заголовка",
"advanced_styling_field_headline_size": "Размер шрифта заголовка",
"advanced_styling_field_headline_weight": "Толщина шрифта заголовка",
"advanced_styling_field_height": "Высота",
"advanced_styling_field_indicator_bg": "Фон индикатора",
"advanced_styling_field_input_text": "Текст поля ввода",
"advanced_styling_field_option_bg": "Фон",
"advanced_styling_field_option_label": "Цвет метки",
"advanced_styling_field_padding_x": "Внутренний отступ по X",
"advanced_styling_field_padding_y": "Внутренний отступ по Y",
"advanced_styling_field_placeholder_opacity": "Прозрачность плейсхолдера",
"advanced_styling_field_shadow": "Тень",
"advanced_styling_field_track_bg": "Фон трека",
"advanced_styling_field_track_height": "Высота трека",
"advanced_styling_section_buttons": "Кнопки",
"advanced_styling_section_headlines": "Заголовки и описания",
"advanced_styling_section_inputs": "Поля ввода",
"advanced_styling_section_options": "Опции (радиокнопки/чекбоксы)",
"app_survey_placement": "Размещение опроса в приложении",
"app_survey_placement_settings_description": "Измените, где будут отображаться опросы в вашем веб-приложении или на сайте.",
"centered_modal_overlay_color": "Цвет оверлея центрированного модального окна",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Брендинг Formbricks скрыт.",
"formbricks_branding_settings_description": "Мы ценим вашу поддержку, но понимаем, если вы захотите отключить это.",
"formbricks_branding_shown": "Брендинг Formbricks отображается.",
"generate_theme_btn": "Сгенерировать",
"generate_theme_confirmation": "Хотите сгенерировать подходящую цветовую тему на основе цвета вашего бренда? Это действие перезапишет ваши текущие цветовые настройки.",
"generate_theme_header": "Сгенерировать цветовую тему?",
"logo_removed_successfully": "Логотип успешно удалён",
"logo_settings_description": "Загрузите логотип вашей компании для брендирования опросов и предпросмотра ссылок.",
"logo_updated_successfully": "Логотип успешно обновлён",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "Показывать брендинг Formbricks в опросах типа {type}",
"show_powered_by_formbricks": "Показывать подпись «Работает на Formbricks»",
"styling_updated_successfully": "Стили успешно обновлены",
"suggest_colors": "Предложить цвета",
"theme": "Тема",
"theme_settings_description": "Создайте стиль для всех опросов. Вы можете включить индивидуальное оформление для каждого опроса."
},

View File

@@ -243,7 +243,6 @@
"imprint": "Impressum",
"in_progress": "Pågående",
"inactive_surveys": "Inaktiva enkäter",
"input_type": "Inmatningstyp",
"integration": "integration",
"integrations": "Integrationer",
"invalid_date": "Ogiltigt datum",
@@ -267,13 +266,11 @@
"look_and_feel": "Utseende",
"manage": "Hantera",
"marketing": "Marknadsföring",
"maximum": "Maximum",
"member": "Medlem",
"members": "Medlemmar",
"members_and_teams": "Medlemmar och team",
"membership_not_found": "Medlemskap hittades inte",
"metadata": "Metadata",
"minimum": "Minimum",
"mobile_overlay_app_works_best_on_desktop": "Formbricks fungerar bäst på en större skärm. Byt till en annan enhet för att hantera eller bygga enkäter.",
"mobile_overlay_surveys_look_good": "Oroa dig inte dina enkäter ser bra ut på alla enheter och skärmstorlekar!",
"mobile_overlay_title": "Hoppsan, liten skärm upptäckt!",
@@ -326,7 +323,7 @@
"placeholder": "Platshållare",
"please_select_at_least_one_survey": "Vänligen välj minst en enkät",
"please_select_at_least_one_trigger": "Vänligen välj minst en utlösare",
"please_upgrade_your_plan": "Vänligen uppgradera din plan.",
"please_upgrade_your_plan": "Vänligen uppgradera din plan",
"preview": "Förhandsgranska",
"preview_survey": "Förhandsgranska enkät",
"privacy": "Integritetspolicy",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "Lägg till en platshållare att visa om det inte finns något värde att återkalla.",
"add_hidden_field_id": "Lägg till dolt fält-ID",
"add_highlight_border": "Lägg till markerad kant",
"add_highlight_border_description": "Lägg till en yttre kant till ditt enkätkort.",
"add_logic": "Lägg till logik",
"add_none_of_the_above": "Lägg till \"Inget av ovanstående\"",
"add_option": "Lägg till alternativ",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "Ändra meddelandet besökare ser när enkäten är stängd.",
"adjust_the_theme_in_the": "Justera temat i",
"all_other_answers_will_continue_to": "Alla andra svar fortsätter till",
"allow_file_type": "Tillåt filtyp",
"allow_multi_select": "Tillåt flerval",
"allow_multiple_files": "Tillåt flera filer",
"allow_users_to_select_more_than_one_image": "Tillåt användare att välja mer än en bild",
@@ -1227,19 +1222,10 @@
"change_background": "Ändra bakgrund",
"change_question_type": "Ändra frågetyp",
"change_survey_type": "Byte av enkättyp påverkar befintlig åtkomst",
"change_the_background_color_of_the_card": "Ändra kortets bakgrundsfärg.",
"change_the_background_color_of_the_input_fields": "Ändra inmatningsfältens bakgrundsfärg.",
"change_the_background_to_a_color_image_or_animation": "Ändra bakgrunden till en färg, bild eller animering.",
"change_the_border_color_of_the_card": "Ändra kortets kantfärg.",
"change_the_border_color_of_the_input_fields": "Ändra inmatningsfältens kantfärg.",
"change_the_border_radius_of_the_card_and_the_inputs": "Ändra kantradie för kortet och inmatningsfälten.",
"change_the_brand_color_of_the_survey": "Ändra enkätens varumärkesfärg.",
"change_the_placement_of_this_survey": "Ändra placeringen av denna enkät.",
"change_the_question_color_of_the_survey": "Ändra enkätens frågefärg.",
"changes_saved": "Ändringar sparade.",
"changing_survey_type_will_remove_existing_distribution_channels": "Att ändra enkättypen påverkar hur den kan delas. Om respondenter redan har åtkomstlänkar för den nuvarande typen kan de förlora åtkomst efter bytet.",
"character_limit_toggle_description": "Begränsa hur kort eller långt ett svar kan vara.",
"character_limit_toggle_title": "Lägg till teckengränser",
"checkbox_label": "Kryssruteetikett",
"choose_the_actions_which_trigger_the_survey": "Välj de åtgärder som utlöser enkäten.",
"choose_the_first_question_on_your_block": "Välj den första frågan i ditt block",
@@ -1259,7 +1245,6 @@
"contact_fields": "Kontaktfält",
"contains": "Innehåller",
"continue_to_settings": "Fortsätt till inställningar",
"control_which_file_types_can_be_uploaded": "Kontrollera vilka filtyper som kan laddas upp.",
"convert_to_multiple_choice": "Konvertera till flerval",
"convert_to_single_choice": "Konvertera till enkelval",
"country": "Land",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "Dölj frågeinställningar",
"hostname": "Värdnamn",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hur coola vill du att dina kort ska vara i {surveyTypeDerived}-enkäter",
"if_you_need_more_please": "Om du behöver fler, vänligen",
"if_you_need_more_please": "Om du behöver mer, vänligen",
"if_you_really_want_that_answer_ask_until_you_get_it": "Fortsätt visa när villkoren är uppfyllda tills ett svar skickas in.",
"ignore_global_waiting_time": "Ignorera väntetid",
"ignore_global_waiting_time_description": "Denna enkät kan visas när dess villkor är uppfyllda, även om en annan enkät nyligen visats.",
@@ -1413,9 +1398,8 @@
"key": "Nyckel",
"last_name": "Efternamn",
"let_people_upload_up_to_25_files_at_the_same_time": "Låt personer ladda upp upp till 25 filer samtidigt.",
"limit_file_types": "Begränsa filtyper",
"limit_the_maximum_file_size": "Begränsa maximal filstorlek",
"limit_upload_file_size_to": "Begränsa uppladdningsfilstorlek till",
"limit_the_maximum_file_size": "Begränsa den maximala filstorleken för uppladdningar.",
"limit_upload_file_size_to": "Begränsa uppladdad filstorlek till",
"link_survey_description": "Dela en länk till en enkätsida eller bädda in den på en webbsida eller i e-post.",
"load_segment": "Ladda segment",
"logic_error_warning": "Ändring kommer att orsaka logikfel",
@@ -1428,7 +1412,7 @@
"matrix_all_fields": "Alla fält",
"matrix_rows": "Rader",
"max_file_size": "Max filstorlek",
"max_file_size_limit_is": "Maxgräns för filstorlek är",
"max_file_size_limit_is": "Maximal filstorleksgräns är",
"move_question_to_block": "Flytta fråga till block",
"multiply": "Multiplicera *",
"needed_for_self_hosted_cal_com_instance": "Behövs för en självhostad Cal.com-instans",
@@ -1460,7 +1444,6 @@
"picture_idx": "Bild {idx}",
"pin_can_only_contain_numbers": "PIN kan endast innehålla siffror.",
"pin_must_be_a_four_digit_number": "PIN måste vara ett fyrsiffrigt nummer.",
"please_enter_a_file_extension": "Vänligen ange en filändelse.",
"please_enter_a_valid_url": "Vänligen ange en giltig URL (t.ex. https://example.com)",
"please_set_a_survey_trigger": "Vänligen ställ in en enkätutlösare",
"please_specify": "Vänligen specificera",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "Endast användare som har PIN-koden kan komma åt enkäten.",
"publish": "Publicera",
"question": "Fråga",
"question_color": "Frågefärg",
"question_deleted": "Fråga borttagen.",
"question_duplicated": "Fråga duplicerad.",
"question_id_updated": "Fråge-ID uppdaterat",
@@ -1539,6 +1521,7 @@
"search_for_images": "Sök efter bilder",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "sekunder efter utlösning stängs enkäten om inget svar",
"seconds_before_showing_the_survey": "sekunder innan enkäten visas.",
"select_field": "Välj fält",
"select_or_type_value": "Välj eller skriv värde",
"select_ordering": "Välj ordning",
"select_saved_action": "Välj sparad åtgärd",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "Styling inställd på temastil",
"subheading": "Underrubrik",
"subtract": "Subtrahera -",
"suggest_colors": "Föreslå färger",
"survey_completed_heading": "Enkät slutförd",
"survey_completed_subheading": "Denna gratis och öppenkällkodsenkät har stängts",
"survey_display_settings": "Visningsinställningar för enkät",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "Visa en enda gång, även om de inte svarar.",
"then": "Sedan",
"this_action_will_remove_all_the_translations_from_this_survey": "Denna åtgärd kommer att ta bort alla översättningar från denna enkät.",
"this_extension_is_already_added": "Denna filändelse är redan tillagd.",
"this_file_type_is_not_supported": "Denna filtyp stöds inte.",
"three_points": "3 poäng",
"times": "gånger",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "För att hålla placeringen konsekvent över alla enkäter kan du",
@@ -1608,6 +1588,48 @@
"upper_label": "Övre etikett",
"url_filters": "URL-filter",
"url_not_supported": "URL stöds inte",
"validation": {
"add_validation_rule": "Lägg till valideringsregel",
"answer_all_rows": "Svara på alla rader",
"characters": "Tecken",
"contains": "Innehåller",
"delete_validation_rule": "Ta bort valideringsregel",
"does_not_contain": "Innehåller inte",
"email": "Är en giltig e-postadress",
"end_date": "Slutdatum",
"file_extension_is": "Filändelsen är",
"file_extension_is_not": "Filändelsen är inte",
"is": "Är",
"is_between": "Är mellan",
"is_earlier_than": "Är tidigare än",
"is_greater_than": "Är större än",
"is_later_than": "Är senare än",
"is_less_than": "Är mindre än",
"is_not": "Är inte",
"is_not_between": "Är inte mellan",
"kb": "KB",
"max_length": "Högst",
"max_selections": "Högst",
"max_value": "Högst",
"mb": "MB",
"min_length": "Minst",
"min_selections": "Minst",
"min_value": "Minst",
"minimum_options_ranked": "Minsta antal rangordnade alternativ",
"minimum_rows_answered": "Minsta antal besvarade rader",
"options_selected": "Valda alternativ",
"pattern": "Matchar regexmönster",
"phone": "Är ett giltigt telefonnummer",
"rank_all_options": "Rangordna alla alternativ",
"select_file_extensions": "Välj filändelser...",
"select_option": "Välj alternativ",
"start_date": "Startdatum",
"url": "Är en giltig URL"
},
"validation_logic_and": "Alla är sanna",
"validation_logic_or": "någon är sann",
"validation_rules": "Valideringsregler",
"validation_rules_description": "Acceptera endast svar som uppfyller följande kriterier",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabel \"{variableName}\" används i kvoten \"{quotaName}\"",
"variable_name_is_already_taken_please_choose_another": "Variabelnamnet är redan taget, vänligen välj ett annat.",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "Lägg till bakgrundsfärg",
"add_background_color_description": "Lägg till en bakgrundsfärg i logobehållaren.",
"advanced_styling_field_border_radius": "Hörnradie",
"advanced_styling_field_button_bg": "Knappbakgrund",
"advanced_styling_field_button_text": "Knapptext",
"advanced_styling_field_description_color": "Beskrivningsfärg",
"advanced_styling_field_description_size": "Beskrivningens teckenstorlek",
"advanced_styling_field_font_size": "Teckenstorlek",
"advanced_styling_field_font_weight": "Teckentjocklek",
"advanced_styling_field_headline_color": "Rubrikfärg",
"advanced_styling_field_headline_size": "Rubrikens teckenstorlek",
"advanced_styling_field_headline_weight": "Rubrikens teckentjocklek",
"advanced_styling_field_height": "Höjd",
"advanced_styling_field_indicator_bg": "Indikatorbakgrund",
"advanced_styling_field_input_text": "Inmatningstext",
"advanced_styling_field_option_bg": "Bakgrund",
"advanced_styling_field_option_label": "Etikettfärg",
"advanced_styling_field_padding_x": "Horisontell utfyllnad",
"advanced_styling_field_padding_y": "Vertikal utfyllnad",
"advanced_styling_field_placeholder_opacity": "Platshållarens opacitet",
"advanced_styling_field_shadow": "Skugga",
"advanced_styling_field_track_bg": "Spårbakgrund",
"advanced_styling_field_track_height": "Spårhöjd",
"advanced_styling_section_buttons": "Knappar",
"advanced_styling_section_headlines": "Rubriker och beskrivningar",
"advanced_styling_section_inputs": "Inmatningsfält",
"advanced_styling_section_options": "Alternativ (radio/checkbox)",
"app_survey_placement": "App-enkätplacering",
"app_survey_placement_settings_description": "Ändra var enkäter visas i din webbapp eller på din webbplats.",
"centered_modal_overlay_color": "Centrerad modal överläggsfärg",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Formbricks-varumärket är dolt.",
"formbricks_branding_settings_description": "Vi uppskattar ditt stöd men förstår om du vill stänga av det.",
"formbricks_branding_shown": "Formbricks-varumärket visas.",
"generate_theme_btn": "Generera",
"generate_theme_confirmation": "Vill du skapa ett matchande färgtema baserat på din varumärkesfärg? Detta kommer att skriva över dina nuvarande färginställningar.",
"generate_theme_header": "Generera färgtema?",
"logo_removed_successfully": "Logotyp borttagen",
"logo_settings_description": "Ladda upp företagets logotyp för att profilera enkäter och länkförhandsvisningar.",
"logo_updated_successfully": "Logotyp uppdaterad",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "Visa Formbricks-varumärket i {type}-enkäter",
"show_powered_by_formbricks": "Visa 'Powered by Formbricks'-signatur",
"styling_updated_successfully": "Stiluppdatering lyckades",
"suggest_colors": "Föreslå färger",
"theme": "Tema",
"theme_settings_description": "Skapa ett stilmall för alla undersökningar. Du kan aktivera anpassad stil för varje undersökning."
},

View File

@@ -243,7 +243,6 @@
"imprint": "印记",
"in_progress": "进行中",
"inactive_surveys": "不 活跃 调查",
"input_type": "输入类型",
"integration": "集成",
"integrations": "集成",
"invalid_date": "无效 日期",
@@ -267,13 +266,11 @@
"look_and_feel": "外观 & 感觉",
"manage": "管理",
"marketing": "市场营销",
"maximum": "最大值",
"member": "成员",
"members": "成员",
"members_and_teams": "成员和团队",
"membership_not_found": "未找到会员资格",
"metadata": "元数据",
"minimum": "最低",
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
"mobile_overlay_surveys_look_good": "别 担心 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
"mobile_overlay_title": "噢, 检测 到 小 屏幕!",
@@ -326,7 +323,7 @@
"placeholder": "占位符",
"please_select_at_least_one_survey": "请选择至少 一个调查",
"please_select_at_least_one_trigger": "请选择至少 一个触发条件",
"please_upgrade_your_plan": "请 升级 您的 计划",
"please_upgrade_your_plan": "请升级您的计划",
"preview": "预览",
"preview_survey": "预览 Survey",
"privacy": "隐私政策",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "添加 占位符 显示 如果 没有 值以 回忆",
"add_hidden_field_id": "添加 隐藏 字段 ID",
"add_highlight_border": "添加 高亮 边框",
"add_highlight_border_description": "在 你的 调查 卡片 添加 外 边框。",
"add_logic": "添加逻辑",
"add_none_of_the_above": "添加 “以上 都 不 是”",
"add_option": "添加 选项",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "更改 访客 看到 调查 关闭 时 的 消息。",
"adjust_the_theme_in_the": "调整主题在",
"all_other_answers_will_continue_to": "所有其他答案将继续",
"allow_file_type": "允许 文件类型",
"allow_multi_select": "允许 多选",
"allow_multiple_files": "允许 多 个 文件",
"allow_users_to_select_more_than_one_image": "允许 用户 选择 多于 一个 图片",
@@ -1227,19 +1222,10 @@
"change_background": "更改 背景",
"change_question_type": "更改 问题类型",
"change_survey_type": "更改 调查 类型 会影 响 现有 访问",
"change_the_background_color_of_the_card": "更改 卡片 的 背景 颜色",
"change_the_background_color_of_the_input_fields": "更改 输入字段 的 背景颜色",
"change_the_background_to_a_color_image_or_animation": "将 背景 更改为 颜色 、 图像 或 动画。",
"change_the_border_color_of_the_card": "更改 卡片 的 边框 颜色",
"change_the_border_color_of_the_input_fields": "更改 输入字段 的边框颜色。",
"change_the_border_radius_of_the_card_and_the_inputs": "更改 卡片 和 输入 的 边框 半径",
"change_the_brand_color_of_the_survey": "更改调查的品牌颜色",
"change_the_placement_of_this_survey": "更改 此 调查 的 放置。",
"change_the_question_color_of_the_survey": "更改调查的 问题颜色",
"changes_saved": "更改 已 保存",
"changing_survey_type_will_remove_existing_distribution_channels": "更改 调查 类型 会影 响 分享 方式 。 如果 受访者 已经 拥有 当前 类型 的 访问 链接 在 更改 之后 ,他们 可能 会 失去 访问 权限 。",
"character_limit_toggle_description": "限制 答案的短或长程度。",
"character_limit_toggle_title": "添加 字符限制",
"checkbox_label": "复选框 标签",
"choose_the_actions_which_trigger_the_survey": "选择 触发 调查 的 动作 。",
"choose_the_first_question_on_your_block": "选择区块中的第一个问题",
@@ -1259,7 +1245,6 @@
"contact_fields": "联络字段",
"contains": "包含",
"continue_to_settings": "继续 到 设置",
"control_which_file_types_can_be_uploaded": "控制 可以 上传的 文件 类型",
"convert_to_multiple_choice": "转换为 多选",
"convert_to_single_choice": "转换为 单选",
"country": "国家",
@@ -1376,7 +1361,7 @@
"hide_question_settings": "隐藏问题设置",
"hostname": "主 机 名",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣",
"if_you_need_more_please": "如果需要更多,请",
"if_you_need_more_please": "如果需要更多,请",
"if_you_really_want_that_answer_ask_until_you_get_it": "每次触发时都会显示,直到提交回应为止。",
"ignore_global_waiting_time": "忽略冷却期",
"ignore_global_waiting_time_description": "只要满足条件,此调查即可显示,即使最近刚显示过其他调查。",
@@ -1413,9 +1398,8 @@
"key": "键",
"last_name": "姓",
"let_people_upload_up_to_25_files_at_the_same_time": "允许 人们 同时 上传 最多 25 个 文件",
"limit_file_types": "限制 文件 类型",
"limit_the_maximum_file_size": "限制 最大 文件 大小",
"limit_upload_file_size_to": "将 上传 文件 大小 限制 为",
"limit_the_maximum_file_size": "限制上传文件的最大大小。",
"limit_upload_file_size_to": "将上传文件大小限制为",
"link_survey_description": "分享 问卷 页面 链接 或 将其 嵌入 网页 或 电子邮件 中。",
"load_segment": "载入 段落",
"logic_error_warning": "更改 将 导致 逻辑 错误",
@@ -1427,8 +1411,8 @@
"manage_languages": "管理 语言",
"matrix_all_fields": "所有字段",
"matrix_rows": "行",
"max_file_size": "最大 文件 大小",
"max_file_size_limit_is": "最大 文件 大小 限制",
"max_file_size": "最大文件大小",
"max_file_size_limit_is": "最大文件大小限制",
"move_question_to_block": "将问题移动到区块",
"multiply": "乘 *",
"needed_for_self_hosted_cal_com_instance": "需要用于 自建 Cal.com 实例",
@@ -1460,7 +1444,6 @@
"picture_idx": "图片 {idx}",
"pin_can_only_contain_numbers": "PIN 只能包含数字。",
"pin_must_be_a_four_digit_number": "PIN 必须是 四 位数字。",
"please_enter_a_file_extension": "请输入 文件 扩展名。",
"please_enter_a_valid_url": "请输入有效的 URL例如 https://example.com ",
"please_set_a_survey_trigger": "请 设置 一个 调查 触发",
"please_specify": "请 指定",
@@ -1471,7 +1454,6 @@
"protect_survey_with_pin_description": "只有 拥有 PIN 的 用户 可以 访问 调查。",
"publish": "发布",
"question": "问题",
"question_color": "问题颜色",
"question_deleted": "问题 已删除",
"question_duplicated": "问题重复。",
"question_id_updated": "问题 ID 更新",
@@ -1539,6 +1521,7 @@
"search_for_images": "搜索 图片",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "触发后 如果 没有 应答 将 在 几秒 后 关闭 调查",
"seconds_before_showing_the_survey": "显示问卷前 几秒",
"select_field": "选择字段",
"select_or_type_value": "选择 或 输入 值",
"select_ordering": "选择排序",
"select_saved_action": "选择 保存的 操作",
@@ -1572,7 +1555,6 @@
"styling_set_to_theme_styles": "样式 设置 为 主题 风格",
"subheading": "子标题",
"subtract": "减 -",
"suggest_colors": "建议颜色",
"survey_completed_heading": "调查 完成",
"survey_completed_subheading": "此 免费 & 开源 调查 已 关闭",
"survey_display_settings": "调查显示设置",
@@ -1586,8 +1568,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "仅显示一次,即使他们未回应。",
"then": "然后",
"this_action_will_remove_all_the_translations_from_this_survey": "此操作将删除该调查中的所有翻译。",
"this_extension_is_already_added": "此扩展已经添加。",
"this_file_type_is_not_supported": "此 文件 类型 不 支持。",
"three_points": "3 分",
"times": "次数",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "为了 保持 所有 调查 的 放置 一致,您 可以",
@@ -1608,6 +1588,48 @@
"upper_label": "上限标签",
"url_filters": "URL 过滤器",
"url_not_supported": "URL 不支持",
"validation": {
"add_validation_rule": "添加验证规则",
"answer_all_rows": "请填写所有行",
"characters": "字符",
"contains": "包含",
"delete_validation_rule": "删除验证规则",
"does_not_contain": "不包含",
"email": "是有效的邮箱地址",
"end_date": "结束日期",
"file_extension_is": "文件扩展名为",
"file_extension_is_not": "文件扩展名不是",
"is": "等于",
"is_between": "介于",
"is_earlier_than": "早于",
"is_greater_than": "大于",
"is_later_than": "晚于",
"is_less_than": "小于",
"is_not": "不等于",
"is_not_between": "不介于",
"kb": "KB",
"max_length": "最多",
"max_selections": "最多",
"max_value": "最多",
"mb": "MB",
"min_length": "至少",
"min_selections": "至少",
"min_value": "至少",
"minimum_options_ranked": "最少排序选项数",
"minimum_rows_answered": "最少回答行数",
"options_selected": "已选择的选项",
"pattern": "匹配正则表达式模式",
"phone": "是有效的手机号",
"rank_all_options": "对所有选项进行排序",
"select_file_extensions": "选择文件扩展名...",
"select_option": "选择选项",
"start_date": "开始日期",
"url": "是有效的URL"
},
"validation_logic_and": "全部为真",
"validation_logic_or": "任一为真",
"validation_rules": "校验规则",
"validation_rules_description": "仅接受符合以下条件的回复",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{variable} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "变量 \"{variableName}\" 正在 被 \"{quotaName}\" 配额 使用",
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
@@ -2015,6 +2037,31 @@
"look": {
"add_background_color": "添加背景色",
"add_background_color_description": "为 logo 容器添加背景色。",
"advanced_styling_field_border_radius": "边框圆角",
"advanced_styling_field_button_bg": "按钮背景色",
"advanced_styling_field_button_text": "按钮文字",
"advanced_styling_field_description_color": "描述颜色",
"advanced_styling_field_description_size": "描述字体大小",
"advanced_styling_field_font_size": "字体大小",
"advanced_styling_field_font_weight": "字体粗细",
"advanced_styling_field_headline_color": "标题颜色",
"advanced_styling_field_headline_size": "标题字体大小",
"advanced_styling_field_headline_weight": "标题字体粗细",
"advanced_styling_field_height": "高度",
"advanced_styling_field_indicator_bg": "指示器背景色",
"advanced_styling_field_input_text": "输入框文字",
"advanced_styling_field_option_bg": "选项背景色",
"advanced_styling_field_option_label": "选项标签颜色",
"advanced_styling_field_padding_x": "横向内边距",
"advanced_styling_field_padding_y": "纵向内边距",
"advanced_styling_field_placeholder_opacity": "占位符透明度",
"advanced_styling_field_shadow": "阴影",
"advanced_styling_field_track_bg": "轨道背景",
"advanced_styling_field_track_height": "轨道高度",
"advanced_styling_section_buttons": "按钮",
"advanced_styling_section_headlines": "标题与描述",
"advanced_styling_section_inputs": "输入框",
"advanced_styling_section_options": "选项(单选/多选)",
"app_survey_placement": "应用调查放置位置",
"app_survey_placement_settings_description": "更改调查在您的 Web 应用或网站中显示的位置。",
"centered_modal_overlay_color": "居中模态遮罩层颜色",
@@ -2028,6 +2075,9 @@
"formbricks_branding_hidden": "Formbricks 品牌标识已隐藏。",
"formbricks_branding_settings_description": "我们很感谢您的支持,但如果您关闭它,我们也能理解。",
"formbricks_branding_shown": "Formbricks 品牌标识已显示。",
"generate_theme_btn": "生成",
"generate_theme_confirmation": "是否要根据您的品牌色生成匹配的配色方案?这将覆盖您当前的颜色设置。",
"generate_theme_header": "生成配色方案?",
"logo_removed_successfully": "logo 移除成功",
"logo_settings_description": "上传您的公司 logo用于品牌调查和链接预览。",
"logo_updated_successfully": "logo 更新成功",
@@ -2042,6 +2092,7 @@
"show_formbricks_branding_in": "在 {type} 调查中显示 Formbricks 品牌标识",
"show_powered_by_formbricks": "显示“Powered by Formbricks”标识",
"styling_updated_successfully": "样式更新成功",
"suggest_colors": "建议颜色",
"theme": "主题",
"theme_settings_description": "为所有问卷创建一个样式主题。你可以为每个问卷启用自定义样式。"
},

View File

@@ -243,7 +243,6 @@
"imprint": "版本訊息",
"in_progress": "進行中",
"inactive_surveys": "停用中的問卷",
"input_type": "輸入類型",
"integration": "整合",
"integrations": "整合",
"invalid_date": "無效日期",
@@ -267,13 +266,11 @@
"look_and_feel": "外觀與風格",
"manage": "管理",
"marketing": "行銷",
"maximum": "最大值",
"member": "成員",
"members": "成員",
"members_and_teams": "成員與團隊",
"membership_not_found": "找不到成員資格",
"metadata": "元數據",
"minimum": "最小值",
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
"mobile_overlay_title": "糟糕 ,偵測到小螢幕!",
@@ -326,7 +323,7 @@
"placeholder": "提示文字",
"please_select_at_least_one_survey": "請選擇至少一個問卷",
"please_select_at_least_one_trigger": "請選擇至少一個觸發器",
"please_upgrade_your_plan": "請升級您的方案",
"please_upgrade_your_plan": "請升級您的方案",
"preview": "預覽",
"preview_survey": "預覽問卷",
"privacy": "隱私權政策",
@@ -1154,7 +1151,6 @@
"add_fallback_placeholder": "新增 預設 以顯示是否沒 有 值 可 回憶 。",
"add_hidden_field_id": "新增隱藏欄位 ID",
"add_highlight_border": "新增醒目提示邊框",
"add_highlight_border_description": "在您的問卷卡片新增外邊框。",
"add_logic": "新增邏輯",
"add_none_of_the_above": "新增 \"以上皆非\"",
"add_option": "新增選項",
@@ -1172,7 +1168,6 @@
"adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。",
"adjust_the_theme_in_the": "在",
"all_other_answers_will_continue_to": "所有其他答案將繼續",
"allow_file_type": "允許檔案類型",
"allow_multi_select": "允許多重選取",
"allow_multiple_files": "允許上傳多個檔案",
"allow_users_to_select_more_than_one_image": "允許使用者選取多張圖片",
@@ -1227,19 +1222,10 @@
"change_background": "變更背景",
"change_question_type": "變更問題類型",
"change_survey_type": "切換問卷類型會影響現有訪問",
"change_the_background_color_of_the_card": "變更卡片的背景顏色。",
"change_the_background_color_of_the_input_fields": "變更輸入欄位的背景顏色。",
"change_the_background_to_a_color_image_or_animation": "將背景變更為顏色、圖片或動畫。",
"change_the_border_color_of_the_card": "變更卡片的邊框顏色。",
"change_the_border_color_of_the_input_fields": "變更輸入欄位的邊框顏色。",
"change_the_border_radius_of_the_card_and_the_inputs": "變更卡片和輸入的邊框半徑。",
"change_the_brand_color_of_the_survey": "變更問卷的品牌顏色。",
"change_the_placement_of_this_survey": "變更此問卷的位置。",
"change_the_question_color_of_the_survey": "變更問卷的問題顏色。",
"changes_saved": "已儲存變更。",
"changing_survey_type_will_remove_existing_distribution_channels": "更改問卷類型會影響其共享方式。如果受訪者已擁有當前類型的存取連結,則在切換後可能會失去存取權限。",
"character_limit_toggle_description": "限制答案的長度或短度。",
"character_limit_toggle_title": "新增字元限制",
"checkbox_label": "核取方塊標籤",
"choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。",
"choose_the_first_question_on_your_block": "選擇此區塊的第一個問題",
@@ -1259,7 +1245,6 @@
"contact_fields": "聯絡人欄位",
"contains": "包含",
"continue_to_settings": "繼續設定",
"control_which_file_types_can_be_uploaded": "控制可以上傳哪些檔案類型。",
"convert_to_multiple_choice": "轉換為多選",
"convert_to_single_choice": "轉換為單選",
"country": "國家/地區",
@@ -1375,7 +1360,6 @@
"hide_progress_bar": "隱藏進度列",
"hide_question_settings": "隱藏問題設定",
"hostname": "主機名稱",
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫",
"if_you_need_more_please": "如果您需要更多,請",
"if_you_really_want_that_answer_ask_until_you_get_it": "每次觸發時都顯示,直到提交回應為止。",
"ignore_global_waiting_time": "忽略冷卻期",
@@ -1413,9 +1397,8 @@
"key": "金鑰",
"last_name": "姓氏",
"let_people_upload_up_to_25_files_at_the_same_time": "允許使用者同時上傳最多 25 個檔案。",
"limit_file_types": "限制檔案類型",
"limit_the_maximum_file_size": "限制最大檔案大小",
"limit_upload_file_size_to": "限制上傳檔案大小為",
"limit_the_maximum_file_size": "限制上傳檔案的最大大小。",
"limit_upload_file_size_to": "將上傳檔案大小限制為",
"link_survey_description": "分享問卷頁面的連結或將其嵌入網頁或電子郵件中。",
"load_segment": "載入區隔",
"logic_error_warning": "變更將導致邏輯錯誤",
@@ -1460,7 +1443,6 @@
"picture_idx": "圖片 '{'idx'}'",
"pin_can_only_contain_numbers": "PIN 碼只能包含數字。",
"pin_must_be_a_four_digit_number": "PIN 碼必須是四位數的數字。",
"please_enter_a_file_extension": "請輸入檔案副檔名。",
"please_enter_a_valid_url": "請輸入有效的 URL例如https://example.com",
"please_set_a_survey_trigger": "請設定問卷觸發器",
"please_specify": "請指定",
@@ -1471,7 +1453,6 @@
"protect_survey_with_pin_description": "只有擁有 PIN 碼的使用者才能存取問卷。",
"publish": "發布",
"question": "問題",
"question_color": "問題顏色",
"question_deleted": "問題已刪除。",
"question_duplicated": "問題已複製。",
"question_id_updated": "問題 ID 已更新",
@@ -1539,6 +1520,7 @@
"search_for_images": "搜尋圖片",
"seconds_after_trigger_the_survey_will_be_closed_if_no_response": "如果沒有回應,則在觸發後幾秒關閉問卷",
"seconds_before_showing_the_survey": "秒後顯示問卷。",
"select_field": "選擇欄位",
"select_or_type_value": "選取或輸入值",
"select_ordering": "選取排序",
"select_saved_action": "選取已儲存的操作",
@@ -1572,7 +1554,6 @@
"styling_set_to_theme_styles": "樣式設定為主題樣式",
"subheading": "副標題",
"subtract": "減 -",
"suggest_colors": "建議顏色",
"survey_completed_heading": "問卷已完成",
"survey_completed_subheading": "此免費且開源的問卷已關閉",
"survey_display_settings": "問卷顯示設定",
@@ -1586,8 +1567,6 @@
"the_survey_will_be_shown_once_even_if_person_doesnt_respond": "僅顯示一次,即使他們未回應。",
"then": "然後",
"this_action_will_remove_all_the_translations_from_this_survey": "此操作將從此問卷中移除所有翻譯。",
"this_extension_is_already_added": "已新增此擴充功能。",
"this_file_type_is_not_supported": "不支援此檔案類型。",
"three_points": "3 分",
"times": "次",
"to_keep_the_placement_over_all_surveys_consistent_you_can": "若要保持所有問卷的位置一致,您可以",
@@ -1608,6 +1587,48 @@
"upper_label": "上標籤",
"url_filters": "網址篩選器",
"url_not_supported": "不支援網址",
"validation": {
"add_validation_rule": "新增驗證規則",
"answer_all_rows": "請填答所有列",
"characters": "字元",
"contains": "包含",
"delete_validation_rule": "刪除驗證規則",
"does_not_contain": "不包含",
"email": "是有效的電子郵件",
"end_date": "結束日期",
"file_extension_is": "檔案副檔名為",
"file_extension_is_not": "檔案副檔名不是",
"is": "等於",
"is_between": "介於",
"is_earlier_than": "早於",
"is_greater_than": "大於",
"is_later_than": "晚於",
"is_less_than": "小於",
"is_not": "不等於",
"is_not_between": "不介於",
"kb": "KB",
"max_length": "最多",
"max_selections": "最多",
"max_value": "最多",
"mb": "MB",
"min_length": "至少",
"min_selections": "至少",
"min_value": "至少",
"minimum_options_ranked": "最少排序選項數",
"minimum_rows_answered": "最少作答列數",
"options_selected": "已選擇的選項",
"pattern": "符合正則表達式樣式",
"phone": "是有效的電話號碼",
"rank_all_options": "請為所有選項排序",
"select_file_extensions": "請選擇檔案副檔名...",
"select_option": "選擇選項",
"start_date": "開始日期",
"url": "是有效的 URL"
},
"validation_logic_and": "全部為真",
"validation_logic_or": "任一為真",
"validation_rules": "驗證規則",
"validation_rules_description": "僅接受符合下列條件的回應",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "變數 \"{variableName}\" 正被使用於 \"{quotaName}\" 配額中",
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
@@ -2015,6 +2036,31 @@
"look": {
"add_background_color": "新增背景顏色",
"add_background_color_description": "為標誌容器新增背景顏色。",
"advanced_styling_field_border_radius": "邊框圓角",
"advanced_styling_field_button_bg": "按鈕背景",
"advanced_styling_field_button_text": "按鈕文字",
"advanced_styling_field_description_color": "說明文字顏色",
"advanced_styling_field_description_size": "說明字體大小",
"advanced_styling_field_font_size": "字體大小",
"advanced_styling_field_font_weight": "字體粗細",
"advanced_styling_field_headline_color": "標題顏色",
"advanced_styling_field_headline_size": "標題字體大小",
"advanced_styling_field_headline_weight": "標題字體粗細",
"advanced_styling_field_height": "高度",
"advanced_styling_field_indicator_bg": "指示器背景",
"advanced_styling_field_input_text": "輸入文字",
"advanced_styling_field_option_bg": "選項背景",
"advanced_styling_field_option_label": "選項標籤顏色",
"advanced_styling_field_padding_x": "水平內距",
"advanced_styling_field_padding_y": "垂直內距",
"advanced_styling_field_placeholder_opacity": "預設提示透明度",
"advanced_styling_field_shadow": "陰影",
"advanced_styling_field_track_bg": "軌道背景",
"advanced_styling_field_track_height": "軌道高度",
"advanced_styling_section_buttons": "按鈕",
"advanced_styling_section_headlines": "標題與說明",
"advanced_styling_section_inputs": "輸入欄位",
"advanced_styling_section_options": "選項(單選/複選)",
"app_survey_placement": "應用程式問卷位置",
"app_survey_placement_settings_description": "變更問卷在您的網頁應用程式或網站中顯示的位置。",
"centered_modal_overlay_color": "置中彈窗覆蓋顏色",
@@ -2028,6 +2074,9 @@
"formbricks_branding_hidden": "Formbricks 品牌標示已隱藏。",
"formbricks_branding_settings_description": "我們很感謝您的支持,但若您選擇關閉我們也能理解。",
"formbricks_branding_shown": "Formbricks 品牌標示已顯示。",
"generate_theme_btn": "產生",
"generate_theme_confirmation": "您要根據您的品牌色產生一組相符的色彩主題嗎?這將會覆蓋您目前的色彩設定。",
"generate_theme_header": "要產生色彩主題嗎?",
"logo_removed_successfully": "標誌已成功移除",
"logo_settings_description": "上傳您的公司標誌,以用於問卷和連結預覽的品牌展示。",
"logo_updated_successfully": "標誌已成功更新",
@@ -2042,6 +2091,7 @@
"show_formbricks_branding_in": "在 {type} 問卷中顯示 Formbricks 品牌標示",
"show_powered_by_formbricks": "顯示「Powered by Formbricks」標記",
"styling_updated_successfully": "樣式已成功更新",
"suggest_colors": "建議顏色",
"theme": "主題",
"theme_settings_description": "為所有調查建立樣式主題。您可以為每個調查啟用自訂樣式。"
},

View File

@@ -15,6 +15,7 @@ import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[respo
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import { formatValidationErrorsForApi, validateResponseData } from "../lib/validation";
import { ZResponseIdSchema, ZResponseUpdateSchema } from "./types/responses";
export const GET = async (request: Request, props: { params: Promise<{ responseId: string }> }) =>
@@ -192,6 +193,25 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
});
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
questionsResponse.data.blocks,
body.data,
body.language ?? "en",
questionsResponse.data.questions
);
if (validationErrors) {
return handleApiError(
request,
{
type: "bad_request",
details: formatValidationErrorsForApi(validationErrors),
},
auditLog
);
}
const response = await updateResponseWithQuotaEvaluation(params.responseId, body);
if (!response.ok) {

View File

@@ -0,0 +1,210 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TValidationErrorMap } from "@formbricks/types/surveys/validation-rules";
import {
formatValidationErrorsForApi,
formatValidationErrorsForV1Api,
validateResponseData,
} from "./validation";
const mockTransformQuestionsToBlocks = vi.fn();
const mockGetElementsFromBlocks = vi.fn();
const mockValidateBlockResponses = vi.fn();
vi.mock("@/app/lib/api/survey-transformation", () => ({
transformQuestionsToBlocks: (...args: unknown[]) => mockTransformQuestionsToBlocks(...args),
}));
vi.mock("@/lib/survey/utils", () => ({
getElementsFromBlocks: (...args: unknown[]) => mockGetElementsFromBlocks(...args),
}));
vi.mock("@formbricks/surveys/validation", () => ({
validateBlockResponses: (...args: unknown[]) => mockValidateBlockResponses(...args),
}));
describe("validateResponseData", () => {
beforeEach(() => vi.clearAllMocks());
const mockBlocks: TSurveyBlock[] = [
{
id: "block1",
name: "Block 1",
elements: [
{
id: "element1",
type: TSurveyElementTypeEnum.OpenText,
headline: { default: "Q1" },
required: false,
inputType: "text",
charLimit: { enabled: false },
},
],
},
];
const mockQuestions: TSurveyQuestion[] = [
{
id: "q1",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Q1" },
required: false,
inputType: "text",
} as unknown as TSurveyQuestion,
];
const mockResponseData: TResponseData = { element1: "test" };
const mockElements = [
{
id: "element1",
type: TSurveyElementTypeEnum.OpenText,
headline: { default: "Q1" },
required: false,
inputType: "text",
charLimit: { enabled: false },
},
];
test("should use blocks when provided", () => {
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
const result = validateResponseData(mockBlocks, mockResponseData, "en");
expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(mockBlocks);
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en");
expect(result).toBeNull();
});
test("should return error map when validation fails", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
};
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue(errorMap);
expect(validateResponseData(mockBlocks, mockResponseData, "en")).toEqual(errorMap);
});
test("should transform questions to blocks when blocks are empty", () => {
const transformedBlocks = [{ ...mockBlocks[0] }];
mockTransformQuestionsToBlocks.mockReturnValue(transformedBlocks);
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData([], mockResponseData, "en", mockQuestions);
expect(mockTransformQuestionsToBlocks).toHaveBeenCalledWith(mockQuestions, []);
expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(transformedBlocks);
});
test("should prefer blocks over questions", () => {
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData(mockBlocks, mockResponseData, "en", mockQuestions);
expect(mockTransformQuestionsToBlocks).not.toHaveBeenCalled();
});
test("should return null when both blocks and questions are empty", () => {
expect(validateResponseData([], mockResponseData, "en", [])).toBeNull();
expect(validateResponseData(null, mockResponseData, "en", [])).toBeNull();
expect(validateResponseData(undefined, mockResponseData, "en", null)).toBeNull();
});
test("should use default language code", () => {
mockGetElementsFromBlocks.mockReturnValue(mockElements);
mockValidateBlockResponses.mockReturnValue({});
validateResponseData(mockBlocks, mockResponseData);
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en");
});
});
describe("formatValidationErrorsForApi", () => {
test("should convert error map to V2 API format", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
};
const result = formatValidationErrorsForApi(errorMap);
expect(result).toEqual([
{
field: "response.data.element1",
issue: "Min length required",
meta: { elementId: "element1", ruleId: "minLength", ruleType: "minLength" },
},
]);
});
test("should handle multiple errors per element", () => {
const errorMap: TValidationErrorMap = {
element1: [
{ ruleId: "minLength", ruleType: "minLength", message: "Min length" },
{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" },
],
};
const result = formatValidationErrorsForApi(errorMap);
expect(result).toHaveLength(2);
expect(result[0].field).toBe("response.data.element1");
expect(result[1].field).toBe("response.data.element1");
});
test("should handle multiple elements", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length" }],
element2: [{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" }],
};
const result = formatValidationErrorsForApi(errorMap);
expect(result).toHaveLength(2);
expect(result[0].field).toBe("response.data.element1");
expect(result[1].field).toBe("response.data.element2");
});
});
describe("formatValidationErrorsForV1Api", () => {
test("should convert error map to V1 API format", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
};
expect(formatValidationErrorsForV1Api(errorMap)).toEqual({
"response.data.element1": "Min length required",
});
});
test("should combine multiple errors with semicolon", () => {
const errorMap: TValidationErrorMap = {
element1: [
{ ruleId: "minLength", ruleType: "minLength", message: "Min length" },
{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" },
],
};
expect(formatValidationErrorsForV1Api(errorMap)).toEqual({
"response.data.element1": "Min length; Max length",
});
});
test("should handle multiple elements", () => {
const errorMap: TValidationErrorMap = {
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length" }],
element2: [{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" }],
};
expect(formatValidationErrorsForV1Api(errorMap)).toEqual({
"response.data.element1": "Min length",
"response.data.element2": "Max length",
});
});
});

View File

@@ -0,0 +1,92 @@
import "server-only";
import { validateBlockResponses } from "@formbricks/surveys/validation";
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
import { TValidationErrorMap } from "@formbricks/types/surveys/validation-rules";
import { transformQuestionsToBlocks } from "@/app/lib/api/survey-transformation";
import { getElementsFromBlocks } from "@/lib/survey/utils";
import { ApiErrorDetails } from "@/modules/api/v2/types/api-error";
/**
* Validates response data against survey validation rules
*
* @param blocks - Survey blocks containing elements with validation rules (preferred)
* @param questions - Survey questions (legacy format, used as fallback if blocks are empty)
* @param responseData - Response data to validate (keyed by element ID)
* @param languageCode - Language code for error messages (defaults to "en")
* @returns Validation error map keyed by element ID, or null if validation passes
*/
export const validateResponseData = (
blocks: TSurveyBlock[] | undefined | null,
responseData: TResponseData,
languageCode: string = "en",
questions?: TSurveyQuestion[] | undefined | null
): TValidationErrorMap | null => {
// Use blocks if available, otherwise transform questions to blocks
let blocksToUse: TSurveyBlock[] = [];
if (blocks && blocks.length > 0) {
blocksToUse = blocks;
} else if (questions && questions.length > 0) {
// Transform legacy questions format to blocks for validation
blocksToUse = transformQuestionsToBlocks(questions, []);
} else {
// No blocks or questions to validate against
return null;
}
// Extract elements from blocks
const elements = getElementsFromBlocks(blocksToUse);
// Validate all elements
const errorMap = validateBlockResponses(elements, responseData, languageCode);
// Return null if no errors (validation passed), otherwise return error map
return Object.keys(errorMap).length === 0 ? null : errorMap;
};
/**
* Converts validation error map to API error response format (V2)
*
* @param errorMap - Validation error map from validateResponseData
* @returns API error response details
*/
export const formatValidationErrorsForApi = (errorMap: TValidationErrorMap) => {
const details: ApiErrorDetails = [];
for (const [elementId, errors] of Object.entries(errorMap)) {
// Include all error messages for each element
for (const error of errors) {
details.push({
field: `response.data.${elementId}`,
issue: error.message,
meta: {
elementId,
ruleId: error.ruleId,
ruleType: error.ruleType,
},
});
}
}
return details;
};
/**
* Converts validation error map to V1 API error response format
*
* @param errorMap - Validation error map from validateResponseData
* @returns V1 API error details as Record<string, string>
*/
export const formatValidationErrorsForV1Api = (errorMap: TValidationErrorMap): Record<string, string> => {
const details: Record<string, string> = {};
for (const [elementId, errors] of Object.entries(errorMap)) {
// Combine all error messages for each element
const errorMessages = errors.map((error) => error.message).join("; ");
details[`response.data.${elementId}`] = errorMessages;
}
return details;
};

View File

@@ -13,6 +13,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { validateFileUploads } from "@/modules/storage/utils";
import { createResponseWithQuotaEvaluation, getResponses } from "./lib/response";
import { formatValidationErrorsForApi, validateResponseData } from "./lib/validation";
export const GET = async (request: NextRequest) =>
authenticatedApiClient({
@@ -128,6 +129,25 @@ export const POST = async (request: Request) =>
});
}
// Validate response data against validation rules
const validationErrors = validateResponseData(
surveyQuestions.data.blocks,
body.data,
body.language ?? "en",
surveyQuestions.data.questions
);
if (validationErrors) {
return handleApiError(
request,
{
type: "bad_request",
details: formatValidationErrorsForApi(validationErrors),
},
auditLog
);
}
const createResponseResult = await createResponseWithQuotaEvaluation(environmentId, body);
if (!createResponseResult.ok) {
return handleApiError(request, createResponseResult.error, auditLog);

View File

@@ -62,7 +62,7 @@ export const QuotaConditionBuilder = ({
);
return (
<div className="space-y-4">
<div className="max-h-[150px] space-y-4 overflow-y-auto">
<ConditionsEditor
conditions={genericConditions}
config={config}

View File

@@ -438,5 +438,45 @@ describe("Quota Evaluation Service", () => {
"Error evaluating quotas for response"
);
});
test("should use 'default' language when provided language matches default language", async () => {
const surveyWithLanguages = {
...mockSurvey,
languages: [
{ default: true, language: { code: "en", flag: "🇺🇸" } },
{ default: false, language: { code: "fr", flag: "🇫🇷" } },
],
};
const input: QuotaEvaluationInput = {
surveyId: mockSurveyId,
responseId: mockResponseId,
data: mockResponseData,
variables: mockVariablesData,
language: "en",
responseFinished: true,
tx: mockTx,
};
const evaluateResult = {
passedQuotas: [mockQuota],
failedQuotas: [],
};
vi.mocked(getQuotas).mockResolvedValue([mockQuota]);
vi.mocked(getSurvey).mockResolvedValue(surveyWithLanguages as unknown as TSurvey);
vi.mocked(evaluateQuotas).mockReturnValue(evaluateResult);
vi.mocked(handleQuotas).mockResolvedValue(null);
await evaluateResponseQuotas(input);
expect(evaluateQuotas).toHaveBeenCalledWith(
surveyWithLanguages,
mockResponseData,
mockVariablesData,
[mockQuota],
"default"
);
});
});
});

View File

@@ -51,8 +51,8 @@ export const evaluateResponseQuotas = async (input: QuotaEvaluationInput): Promi
if (!survey) {
return { shouldEndSurvey: false };
}
const result = evaluateQuotas(survey, data, variables, quotas, language);
const isDefaultLanguage = survey.languages.find((lang) => lang.default)?.language.code === language;
const result = evaluateQuotas(survey, data, variables, quotas, isDefaultLanguage ? "default" : language);
const quotaFull = await handleQuotas(surveyId, responseId, result, responseFinished, prismaClient);

View File

@@ -1,5 +1,6 @@
import { Project } from "@prisma/client";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { cn } from "@/lib/cn";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { EditBranding } from "@/modules/ee/whitelabel/remove-branding/components/edit-branding";
@@ -39,6 +40,7 @@ export const BrandingSettingsCard = async ({
return (
<SettingsCard
title={t("environments.workspace.look.formbricks_branding")}
className={cn(!isReadOnly && "max-w-7xl")}
description={t("environments.workspace.look.formbricks_branding_settings_description")}>
{canRemoveBranding ? (
<div className="space-y-4">

View File

@@ -213,8 +213,8 @@ export async function PreviewEmailTemplate({
{ "rounded-l-lg border-l": i === 0 },
{ "rounded-r-lg": i === firstQuestion.range - 1 },
firstQuestion.isColorCodingEnabled &&
firstQuestion.scale === "number" &&
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
firstQuestion.scale === "number" &&
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
firstQuestion.scale === "star" && "border-transparent"
)}
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}

View File

@@ -71,12 +71,12 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
...(SMTP_AUTHENTICATED
? {
auth: {
type: "LOGIN",
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
}
auth: {
type: "LOGIN",
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
}
: {}),
tls: {
rejectUnauthorized: SMTP_REJECT_UNAUTHORIZED_TLS,
@@ -257,12 +257,12 @@ export const sendResponseFinishedEmail = async (
to: email,
subject: personEmail
? t("emails.response_finished_email_subject_with_email", {
personEmail,
surveyName: survey.name,
})
personEmail,
surveyName: survey.name,
})
: t("emails.response_finished_email_subject", {
surveyName: survey.name,
}),
surveyName: survey.name,
}),
replyTo: personEmail?.toString() ?? MAIL_FROM,
html,
});

View File

@@ -2,7 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Project } from "@prisma/client";
import { RotateCcwIcon } from "lucide-react";
import { RotateCcwIcon, SparklesIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
@@ -12,6 +12,7 @@ import { TProjectStyling, ZProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
import { previewSurvey } from "@/app/lib/templates";
import { defaultStyling } from "@/lib/styling/constants";
import { isLight, mixColor } from "@/lib/utils/colors";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { updateProjectAction } from "@/modules/projects/settings/actions";
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
@@ -20,6 +21,7 @@ import { AlertDialog } from "@/modules/ui/components/alert-dialog";
import { BackgroundStylingCard } from "@/modules/ui/components/background-styling-card";
import { Button } from "@/modules/ui/components/button";
import { CardStylingSettings } from "@/modules/ui/components/card-styling-settings";
import { ColorPicker } from "@/modules/ui/components/color-picker";
import {
FormControl,
FormDescription,
@@ -60,11 +62,11 @@ export const ThemeStyling = ({
const [previewSurveyType, setPreviewSurveyType] = useState<TSurveyType>("link");
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
const [confirmSuggestColorsOpen, setConfirmSuggestColorsOpen] = useState(false);
const [formStylingOpen, setFormStylingOpen] = useState(false);
const [cardStylingOpen, setCardStylingOpen] = useState(false);
const [backgroundStylingOpen, setBackgroundStylingOpen] = useState(false);
const onReset = useCallback(async () => {
const updatedProjectResponse = await updateProjectAction({
projectId: project.id,
@@ -81,7 +83,81 @@ export const ThemeStyling = ({
const errorMessage = getFormattedErrorMessage(updatedProjectResponse);
toast.error(errorMessage);
}
}, [form, project.id, router]);
}, [form, project.id, router, t]);
const handleSuggestColors = () => {
const brandColor = form.getValues().brandColor?.light ?? "#64748b";
// Derive everything from Brand Color
const derivedInputBg = "#ffffff";
const derivedQuestionColor = "#2b2524";
const derivedCardBg = "#ffffff";
const derivedCardBorder = mixColor(brandColor, "#ffffff", 0.9) ?? "#f8fafc";
const derivedPageBg = mixColor(brandColor, "#ffffff", 0.95) ?? "#f8fafc";
const isBrandLight = isLight(brandColor);
// Accent derived from Brand (Monochromatic)
const accentColor = brandColor;
const derivedAccentSelected =
mixColor(accentColor, isBrandLight ? "#000000" : "#ffffff", 0.1) ?? accentColor;
// Accent for text (darkened if light)
const derivedAccentText = isBrandLight ? mixColor(accentColor, "#000000", 0.6) : accentColor;
// 1. General
form.setValue("brandColor.light", brandColor, { shouldDirty: true });
form.setValue("questionColor.light", derivedQuestionColor, { shouldDirty: true });
form.setValue("isLogoHidden", false, { shouldDirty: true });
// Accents (Synced with Brand)
form.setValue("accentBgColor.light", accentColor, { shouldDirty: true });
form.setValue("accentBgColorSelected.light", derivedAccentSelected, { shouldDirty: true });
// Headlines & Descriptions (Using Brand/Accent)
form.setValue("elementHeadlineColor.light", derivedAccentText, { shouldDirty: true });
form.setValue("elementDescriptionColor.light", derivedAccentText, { shouldDirty: true });
// 2. Buttons
form.setValue("buttonBgColor.light", brandColor, { shouldDirty: true });
form.setValue("buttonTextColor.light", isBrandLight ? "#0f172a" : "#ffffff", { shouldDirty: true });
form.setValue("buttonBorderRadius", 4, { shouldDirty: true });
// 3. Inputs (Card-like style)
form.setValue("inputColor.light", derivedInputBg, { shouldDirty: true });
form.setValue("inputBorderColor.light", derivedCardBorder, { shouldDirty: true }); // Match card border
form.setValue("inputTextColor.light", "#0f172a", { shouldDirty: true });
form.setValue("inputBorderRadius", 8, { shouldDirty: true }); // Match roundness
form.setValue("inputShadow", "0 1px 2px 0 rgb(0 0 0 / 0.05)", { shouldDirty: true }); // Add shadow
form.setValue("inputPaddingY", 16, { shouldDirty: true }); // More padding
// 4. Options (Checkboxes/Radio)
form.setValue("optionBgColor.light", derivedInputBg, { shouldDirty: true });
form.setValue("optionLabelColor.light", "#0f172a", { shouldDirty: true });
form.setValue("optionBorderRadius", 8, { shouldDirty: true }); // Match roundness
form.setValue("optionPaddingY", 16, { shouldDirty: true });
// 5. Card Styling
form.setValue("cardBackgroundColor.light", derivedCardBg, { shouldDirty: true });
form.setValue("cardBorderColor.light", derivedCardBorder, { shouldDirty: true });
form.setValue("roundness", 8, { shouldDirty: true });
// 6. Highlight / Accent (Focus states)
form.setValue("highlightBorderColor.light", accentColor, { shouldDirty: true });
// 7. Progress Bar
form.setValue("progressIndicatorBgColor.light", brandColor, { shouldDirty: true });
form.setValue("progressTrackBgColor.light", mixColor(brandColor, "#ffffff", 0.8), { shouldDirty: true });
// 8. Background Styling (Page Background)
form.setValue(
"background",
{ bg: derivedPageBg, bgType: "color", brightness: 100 },
{ shouldDirty: true }
);
toast.success(t("environments.workspace.look.styling_updated_successfully"));
setConfirmSuggestColorsOpen(false);
};
const onSubmit: SubmitHandler<TProjectStyling> = async (data) => {
const updatedProjectResponse = await updateProjectAction({
@@ -144,7 +220,37 @@ export const ThemeStyling = ({
</div>
</div>
<div className="flex flex-col gap-3 rounded-lg bg-slate-50 p-4">
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
<div className="grid grid-cols-2 items-end gap-4">
<FormField
control={form.control}
name="brandColor.light"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel className="text-xs">
{t("environments.surveys.edit.brand_color")}
</FormLabel>
<FormControl>
<ColorPicker
color={field.value ?? "#64748b"}
onChange={(color) => field.onChange(color)}
containerClass="w-full"
/>
</FormControl>
</FormItem>
)}
/>
<div className="flex flex-col gap-1">
<Button
type="button"
variant="secondary"
className="h-10 w-full justify-center gap-2 border border-slate-300 bg-white text-slate-700 shadow-sm hover:bg-slate-50"
onClick={() => setConfirmSuggestColorsOpen(true)}>
<SparklesIcon className="mr-2 h-4 w-4" />
{t("environments.workspace.look.suggest_colors")}
</Button>
</div>
</div>
<FormStylingSettings
open={formStylingOpen}
setOpen={setFormStylingOpen}
@@ -192,7 +298,7 @@ export const ThemeStyling = ({
{/* Survey Preview */}
<div className="relative w-1/2 rounded-lg bg-slate-100 pt-4">
<div className="sticky top-4 mb-4 h-[600px]">
<div className="sticky top-4 mb-4 max-h-[calc(100vh-2rem)]">
<ThemeStylingPreviewSurvey
survey={previewSurvey(project.name, t)}
project={{
@@ -206,6 +312,18 @@ export const ThemeStyling = ({
</div>
</div>
{/* Confirm reset styling modal */}
<AlertDialog
open={confirmSuggestColorsOpen}
setOpen={setConfirmSuggestColorsOpen}
headerText={t("environments.workspace.look.generate_theme_header")}
mainText={t("environments.workspace.look.generate_theme_confirmation")}
confirmBtnLabel={t("environments.workspace.look.generate_theme_btn")}
declineBtnLabel={t("common.cancel")}
onConfirm={handleSuggestColors}
onDecline={() => setConfirmSuggestColorsOpen(false)}
/>
{/* Confirm reset styling modal */}
<AlertDialog
open={confirmResetStylingModalOpen}

View File

@@ -56,6 +56,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
</SettingsCard>
<SettingsCard
title={t("common.logo")}
className={cn(!isReadOnly && "max-w-7xl")}
description={t("environments.workspace.look.logo_settings_description")}>
<EditLogo
project={project}
@@ -66,6 +67,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
</SettingsCard>
<SettingsCard
title={t("environments.workspace.look.app_survey_placement")}
className={cn(!isReadOnly && "max-w-7xl")}
description={t("environments.workspace.look.app_survey_placement_settings_description")}>
<EditPlacementForm project={project} environmentId={params.environmentId} isReadOnly={isReadOnly} />
</SettingsCard>

View File

@@ -153,9 +153,9 @@ export const ElementFormInput = ({
(currentElement &&
(id.includes(".")
? // Handle nested properties
(currentElement[id.split(".")[0] as keyof TSurveyElement] as any)?.[id.split(".")[1]]
(currentElement[id.split(".")[0] as keyof TSurveyElement] as any)?.[id.split(".")[1]]
: // Original behavior
(currentElement[id as keyof TSurveyElement] as TI18nString))) ||
(currentElement[id as keyof TSurveyElement] as TI18nString))) ||
createI18nString("", surveyLanguageCodes)
);
}, [
@@ -391,7 +391,7 @@ export const ElementFormInput = ({
return (
<div className="w-full">
{label && (
<div className="mb-2 mt-3 flex items-center justify-between">
<div className="mt-3 mb-2 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && currentElement && updateElement && (
<div className="flex items-center space-x-2">
@@ -521,23 +521,8 @@ export const ElementFormInput = ({
return (
<div className="w-full">
{label && (
<div className="mb-2 mt-3 flex items-center justify-between">
<div className="mt-3 mb-2 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && currentElement && updateElement && (
<div className="flex items-center space-x-2">
<Label htmlFor="required-toggle" className="text-sm">
{t("environments.surveys.edit.required")}
</Label>
<Switch
id="required-toggle"
checked={currentElement.required}
disabled={getIsRequiredToggleDisabled()}
onCheckedChange={(checked) => {
updateElement(elementIdx, { required: checked });
}}
/>
</div>
)}
</div>
)}
<MultiLangWrapper
@@ -583,8 +568,9 @@ export const ElementFormInput = ({
<div className="h-10 w-full"></div>
<div
ref={highlightContainerRef}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${localSurvey.languages?.length > 1 ? "pr-24" : ""
}`}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent ${
localSurvey.languages?.length > 1 ? "pr-24" : ""
}`}
dir="auto"
key={highlightedJSX.toString()}>
{highlightedJSX}
@@ -611,8 +597,9 @@ export const ElementFormInput = ({
maxLength={maxLength}
ref={inputRef}
onBlur={onBlur}
className={`absolute top-0 text-black caret-black ${localSurvey.languages?.length > 1 ? "pr-24" : ""
} ${className}`}
className={`absolute top-0 text-black caret-black ${
localSurvey.languages?.length > 1 ? "pr-24" : ""
} ${className}`}
isInvalid={
isInvalid &&
text[usedLanguageCode]?.trim() === "" &&

View File

@@ -4,11 +4,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { type JSX, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyAddressElement } from "@formbricks/types/surveys/elements";
import type { TSurveyAddressElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { Button } from "@/modules/ui/components/button";
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
@@ -159,6 +160,17 @@ export const AddressElementForm = ({
isStorageConfigured={isStorageConfigured}
/>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
/>
</form>
);
};

View File

@@ -36,7 +36,6 @@ import { PictureSelectionForm } from "@/modules/survey/editor/components/picture
import { RankingElementForm } from "@/modules/survey/editor/components/ranking-element-form";
import { RatingElementForm } from "@/modules/survey/editor/components/rating-element-form";
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
import { isLabelValidForAllLanguages } from "@/modules/survey/editor/lib/validation";
import { getElementIconMap, getTSurveyElementTypeEnumName } from "@/modules/survey/lib/elements";
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
@@ -128,25 +127,7 @@ export const BlockCard = ({
const isBlockOpen = block.elements.some((element) => element.id === activeElementId);
const hasInvalidElement = block.elements.some((element) => invalidElements?.includes(element.id));
// Check if button labels have incomplete translations for any enabled language
// A button label is invalid if it exists but doesn't have valid text for all enabled languages
const surveyLanguages = localSurvey.languages ?? [];
const hasInvalidButtonLabel =
block.buttonLabel !== undefined &&
block.buttonLabel["default"]?.trim() !== "" &&
!isLabelValidForAllLanguages(block.buttonLabel, surveyLanguages);
// Check if back button label is invalid
// Back button label should exist for all blocks except the first one
const hasInvalidBackButtonLabel =
blockIdx > 0 &&
block.backButtonLabel !== undefined &&
block.backButtonLabel["default"]?.trim() !== "" &&
!isLabelValidForAllLanguages(block.backButtonLabel, surveyLanguages);
// Block should be highlighted if it has invalid elements OR invalid button labels
const isBlockInvalid = hasInvalidElement || hasInvalidButtonLabel || hasInvalidBackButtonLabel;
const isBlockInvalid = hasInvalidElement;
const [isBlockCollapsed, setIsBlockCollapsed] = useState(false);
const [openAdvanced, setOpenAdvanced] = useState(blockLogic.length > 0);

View File

@@ -4,11 +4,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { type JSX, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyContactInfoElement } from "@formbricks/types/surveys/elements";
import type { TSurveyContactInfoElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { Button } from "@/modules/ui/components/button";
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
@@ -156,6 +157,16 @@ export const ContactInfoElementForm = ({
isStorageConfigured={isStorageConfigured}
/>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);
};

View File

@@ -4,11 +4,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { type JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyDateElement } from "@formbricks/types/surveys/elements";
import type { TSurveyDateElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
@@ -126,6 +127,16 @@ export const DateElementForm = ({
/>
</div>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
/>
</form>
);
};

View File

@@ -112,6 +112,7 @@ export const EditorCardMenu = ({
choices: card.choices,
type,
logic: undefined,
validation: undefined,
});
return;
@@ -128,6 +129,7 @@ export const EditorCardMenu = ({
buttonLabel,
backButtonLabel,
logic: undefined,
validation: undefined,
});
};

View File

@@ -46,6 +46,9 @@ export const EndScreenForm = ({
const questions = getElementsFromBlocks(localSurvey.blocks);
const defaultLanguageCode = localSurvey.languages.find((lang) => lang.default)?.language.code ?? "default";
const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode;
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
endingCard.type === "endScreen" &&
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
@@ -136,7 +139,7 @@ export const EndScreenForm = ({
</Label>
</div>
{showEndingCardCTA && (
<div className="border-1 mt-4 space-y-4 rounded-md border bg-slate-100 p-4 pt-2">
<div className="mt-4 space-y-4 rounded-md border border-1 bg-slate-100 p-4 pt-2">
<div className="space-y-2">
<ElementFormInput
id="buttonLabel"
@@ -174,13 +177,13 @@ export const EndScreenForm = ({
}}
isRecallAllowed
localSurvey={localSurvey}
usedLanguageCode={"default"}
usedLanguageCode={usedLanguageCode}
render={({ value, onChange, highlightedJSX, children }) => {
return (
<div className="group relative">
{/* The highlight container is absolutely positioned behind the input */}
<div
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent`}
dir="auto"
key={highlightedJSX.toString()}>
{highlightedJSX}
@@ -194,12 +197,12 @@ export const EndScreenForm = ({
value={
recallToHeadline(
{
[selectedLanguageCode]: value,
[usedLanguageCode]: value,
},
localSurvey,
false,
"default"
)[selectedLanguageCode]
usedLanguageCode
)[usedLanguageCode]
}
onChange={(e) => isExternalUrlsAllowed && onChange(e.target.value)}
disabled={!isExternalUrlsAllowed}

View File

@@ -2,17 +2,17 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Project } from "@prisma/client";
import { PlusIcon, XCircleIcon } from "lucide-react";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { type JSX, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/storage";
import { TSurveyFileUploadElement } from "@formbricks/types/surveys/elements";
import type { TSurveyFileUploadElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
@@ -20,7 +20,7 @@ import { useGetBillingInfo } from "@/modules/utils/hooks/useGetBillingInfo";
interface FileUploadFormProps {
localSurvey: TSurvey;
project?: Project;
project: Project;
element: TSurveyFileUploadElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyFileUploadElement>) => void;
@@ -47,72 +47,15 @@ export const FileUploadElementForm = ({
isStorageConfigured = true,
isExternalUrlsAllowed,
}: FileUploadFormProps): JSX.Element => {
const [extension, setExtension] = useState("");
const { t } = useTranslation();
const [isMaxSizeError, setMaxSizeError] = useState(false);
const [isMaxSizeError, setIsMaxSizeError] = useState(false);
const {
billingInfo,
error: billingInfoError,
isLoading: billingInfoLoading,
} = useGetBillingInfo(project?.organizationId ?? "");
} = useGetBillingInfo(project.organizationId);
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const handleInputChange = (event) => {
setExtension(event.target.value);
};
const addExtension = (event) => {
event.preventDefault();
event.stopPropagation();
let rawExtension = extension.trim();
// Remove the dot at the start if it exists
if (rawExtension.startsWith(".")) {
rawExtension = rawExtension.substring(1);
}
if (!rawExtension) {
toast.error(t("environments.surveys.edit.please_enter_a_file_extension"));
return;
}
// Convert to lowercase before validation and adding
const modifiedExtension = rawExtension.toLowerCase() as TAllowedFileExtension;
const parsedExtensionResult = ZAllowedFileExtension.safeParse(modifiedExtension);
if (!parsedExtensionResult.success) {
// This error should now be less likely unless the extension itself is invalid (e.g., "exe")
toast.error(t("environments.surveys.edit.this_file_type_is_not_supported"));
return;
}
const currentExtensions = element.allowedFileExtensions || [];
// Check if the lowercase extension already exists
if (!currentExtensions.includes(modifiedExtension)) {
updateElement(elementIdx, {
allowedFileExtensions: [...currentExtensions, modifiedExtension],
});
setExtension(""); // Clear the input field
} else {
toast.error(t("environments.surveys.edit.this_extension_is_already_added"));
}
};
const removeExtension = (event, index: number) => {
event.preventDefault();
if (element.allowedFileExtensions) {
const updatedExtensions = [...(element.allowedFileExtensions || [])];
updatedExtensions.splice(index, 1);
// Ensure array is set to undefined if empty, matching toggle behavior
updateElement(elementIdx, {
allowedFileExtensions: updatedExtensions.length > 0 ? updatedExtensions : undefined,
});
}
};
const maxSizeInMBLimit = useMemo(() => {
if (billingInfoError || billingInfoLoading || !billingInfo) {
return 10;
@@ -216,20 +159,20 @@ export const FileUploadElementForm = ({
id="fileSizeLimit"
value={element.maxSizeInMB}
onChange={(e) => {
const parsedValue = parseInt(e.target.value, 10);
const parsedValue = Number.parseInt(e.target.value, 10);
if (isFormbricksCloud && parsedValue > maxSizeInMBLimit) {
toast.error(
`${t("environments.surveys.edit.max_file_size_limit_is")} ${maxSizeInMBLimit} MB`
);
setMaxSizeError(true);
setIsMaxSizeError(true);
updateElement(elementIdx, { maxSizeInMB: maxSizeInMBLimit });
return;
}
updateElement(elementIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
updateElement(elementIdx, { maxSizeInMB: Number.parseInt(e.target.value, 10) });
}}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
/>
MB
</p>
@@ -247,49 +190,18 @@ export const FileUploadElementForm = ({
)}
</label>
</AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={!!element.allowedFileExtensions}
onToggle={(checked) =>
updateElement(elementIdx, { allowedFileExtensions: checked ? [] : undefined })
}
htmlId="limitFileType"
title={t("environments.surveys.edit.limit_file_types")}
description={t("environments.surveys.edit.control_which_file_types_can_be_uploaded")}
childBorder
customContainerClass="p-0">
<div className="p-4">
<div className="flex flex-row flex-wrap gap-2">
{element.allowedFileExtensions?.map((item, index) => (
<div
key={item}
className="mb-2 flex h-8 items-center space-x-2 rounded-full bg-slate-200 px-2">
<p className="text-sm text-slate-800">{item}</p>
<Button
className="inline-flex px-0"
variant="ghost"
onClick={(e) => removeExtension(e, index)}>
<XCircleIcon className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="flex items-center">
<Input
autoFocus
className="mr-2 w-20 rounded-md bg-white placeholder:text-sm"
placeholder=".pdf"
value={extension}
onChange={handleInputChange}
type="text"
/>
<Button size="sm" variant="secondary" onClick={(e) => addExtension(e)}>
{t("environments.surveys.edit.allow_file_type")}
</Button>
</div>
</div>
</AdvancedOptionToggle>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
/>
</form>
);
};

View File

@@ -2,18 +2,20 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon, SparklesIcon } from "lucide-react";
import React from "react";
import { CheckIcon } from "lucide-react";
import React, { useState } from "react";
import { UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn";
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
import { mixColor } from "@/lib/utils/colors";
import { Button } from "@/modules/ui/components/button";
import { ColorPicker } from "@/modules/ui/components/color-picker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import {
ColorField,
DimensionInput,
NumberField,
StylingSection,
TextField,
} from "@/modules/ui/components/styling-fields";
type FormStylingSettingsProps = {
open: boolean;
@@ -31,45 +33,12 @@ export const FormStylingSettings = ({
form,
}: FormStylingSettingsProps) => {
const { t } = useTranslation();
const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor;
const background = form.watch("background");
const highlightBorderColor = form.watch("highlightBorderColor");
const setQuestionColor = (color: string) => form.setValue("questionColor.light", color);
const setInputColor = (color: string) => form.setValue("inputColor.light", color);
const setInputBorderColor = (color: string) => form.setValue("inputBorderColor.light", color);
const setCardBackgroundColor = (color: string) => form.setValue("cardBackgroundColor.light", color);
const setCardBorderColor = (color: string) => form.setValue("cardBorderColor.light", color);
const setBackgroundColor = (color: string) => {
form.setValue("background", {
bg: color,
bgType: "color",
});
};
const setHighlightBorderColor = (color: string) => {
form.setValue("highlightBorderColor", { light: mixColor(color, "#ffffff", 0.25) });
};
const suggestColors = () => {
// mix the brand color with different weights of white and set the result as the other colors
setQuestionColor(mixColor(brandColor, "#000000", 0.35));
setInputColor(mixColor(brandColor, "#ffffff", 0.92));
setInputBorderColor(mixColor(brandColor, "#ffffff", 0.6));
setCardBackgroundColor(mixColor(brandColor, "#ffffff", 0.97));
setCardBorderColor(mixColor(brandColor, "#ffffff", 0.8));
if (!background || background?.bgType === "color") {
setBackgroundColor(mixColor(brandColor, "#ffffff", 0.855));
}
if (highlightBorderColor) {
setHighlightBorderColor(brandColor);
}
};
const [parent] = useAutoAnimate();
const [headlinesOpen, setHeadlinesOpen] = useState(false);
const [inputsOpen, setInputsOpen] = useState(false);
const [buttonsOpen, setButtonsOpen] = useState(false);
const [optionsOpen, setOptionsOpen] = useState(false);
return (
<Collapsible.Root
@@ -88,7 +57,7 @@ export const FormStylingSettings = ({
)}>
<div className="inline-flex px-4 py-4">
{!isSettingsPage && (
<div className="flex items-center pl-2 pr-5">
<div className="flex items-center pr-5 pl-2">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
@@ -110,110 +79,190 @@ export const FormStylingSettings = ({
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
<hr className="py-1 text-slate-600" />
<div className="flex flex-col gap-6 p-6 pt-2">
<div className="flex flex-col gap-2">
<FormField
control={form.control}
name="brandColor.light"
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.brand_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_brand_color_of_the_survey")}
</FormDescription>
</div>
<div className="flex flex-col gap-6 p-6">
{/* Headlines & Descriptions */}
<StylingSection
title={t("environments.workspace.look.advanced_styling_section_headlines")}
open={headlinesOpen}
setOpen={setHeadlinesOpen}>
<div className="grid grid-cols-2 gap-4">
<ColorField
form={form}
name="elementHeadlineColor.light"
label={t("environments.workspace.look.advanced_styling_field_headline_color")}
/>
<ColorField
form={form}
name="elementDescriptionColor.light"
label={t("environments.workspace.look.advanced_styling_field_description_color")}
/>
<DimensionInput
form={form}
name="elementHeadlineFontSize"
label={t("environments.workspace.look.advanced_styling_field_headline_size")}
/>
<DimensionInput
form={form}
name="elementDescriptionFontSize"
label={t("environments.workspace.look.advanced_styling_field_description_size")}
/>
<NumberField
form={form}
name="elementHeadlineFontWeight"
label={t("environments.workspace.look.advanced_styling_field_headline_weight")}
/>
</div>
</StylingSection>
<FormControl>
<ColorPicker
color={field.value || COLOR_DEFAULTS.brandColor}
onChange={(color) => field.onChange(color)}
containerClass="max-w-xs"
/>
</FormControl>
</FormItem>
)}
/>
{/* Inputs */}
<StylingSection
title={t("environments.workspace.look.advanced_styling_section_inputs")}
open={inputsOpen}
setOpen={setInputsOpen}>
<div className="grid grid-cols-2 gap-4">
<ColorField
form={form}
name="inputColor.light"
label={t("environments.surveys.edit.input_color")}
/>
<ColorField
form={form}
name="inputBorderColor.light"
label={t("environments.surveys.edit.input_border_color")}
/>
<ColorField
form={form}
name="inputTextColor.light"
label={t("environments.workspace.look.advanced_styling_field_input_text")}
/>
<div className="hidden" /> {/* Spacer if needed, or remove for auto flow */}
<DimensionInput
form={form}
name="inputBorderRadius"
label={t("environments.workspace.look.advanced_styling_field_border_radius")}
/>
<DimensionInput
form={form}
name="inputHeight"
label={t("environments.workspace.look.advanced_styling_field_height")}
/>
<DimensionInput
form={form}
name="inputFontSize"
label={t("environments.workspace.look.advanced_styling_field_font_size")}
/>
<DimensionInput
form={form}
name="inputPaddingX"
label={t("environments.workspace.look.advanced_styling_field_padding_x")}
/>
<DimensionInput
form={form}
name="inputPaddingY"
label={t("environments.workspace.look.advanced_styling_field_padding_y")}
/>
<NumberField
form={form}
name="inputPlaceholderOpacity"
label={t("environments.workspace.look.advanced_styling_field_placeholder_opacity")}
step={0.1}
max={1}
/>
<TextField
form={form}
name="inputShadow"
label={t("environments.workspace.look.advanced_styling_field_shadow")}
/>
</div>
</StylingSection>
<Button
type="button"
variant="secondary"
size="sm"
className="w-fit"
onClick={() => suggestColors()}>
{t("environments.surveys.edit.suggest_colors")}
<SparklesIcon />
</Button>
</div>
{/* Buttons */}
<StylingSection
title={t("environments.workspace.look.advanced_styling_section_buttons")}
open={buttonsOpen}
setOpen={setButtonsOpen}>
<div className="grid grid-cols-2 gap-4">
<ColorField
form={form}
name="buttonBgColor.light"
label={t("environments.workspace.look.advanced_styling_field_button_bg")}
/>
<ColorField
form={form}
name="buttonTextColor.light"
label={t("environments.workspace.look.advanced_styling_field_button_text")}
/>
<DimensionInput
form={form}
name="buttonBorderRadius"
label={t("environments.workspace.look.advanced_styling_field_border_radius")}
/>
<DimensionInput
form={form}
name="buttonHeight"
label={t("environments.workspace.look.advanced_styling_field_height")}
/>
<DimensionInput
form={form}
name="buttonFontSize"
label={t("environments.workspace.look.advanced_styling_field_font_size")}
/>
<NumberField
form={form}
name="buttonFontWeight"
label={t("environments.workspace.look.advanced_styling_field_font_weight")}
/>
<DimensionInput
form={form}
name="buttonPaddingX"
label={t("environments.workspace.look.advanced_styling_field_padding_x")}
/>
<DimensionInput
form={form}
name="buttonPaddingY"
label={t("environments.workspace.look.advanced_styling_field_padding_y")}
/>
</div>
</StylingSection>
<FormField
control={form.control}
name="questionColor.light"
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.question_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_question_color_of_the_survey")}
</FormDescription>
</div>
<FormControl>
<ColorPicker
color={field.value || COLOR_DEFAULTS.questionColor}
onChange={(color) => field.onChange(color)}
containerClass="max-w-xs"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="inputColor.light"
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.input_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_background_color_of_the_input_fields")}
</FormDescription>
</div>
<FormControl>
<ColorPicker
color={field.value || COLOR_DEFAULTS.inputColor}
onChange={(color: string) => field.onChange(color)}
containerClass="max-w-xs"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="inputBorderColor.light"
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.input_border_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_color_of_the_input_fields")}
</FormDescription>
</div>
<FormControl>
<ColorPicker
color={field.value || COLOR_DEFAULTS.inputBorderColor}
onChange={(color: string) => field.onChange(color)}
containerClass="max-w-xs"
/>
</FormControl>
</FormItem>
)}
/>
{/* Options */}
<StylingSection
title={t("environments.workspace.look.advanced_styling_section_options")}
open={optionsOpen}
setOpen={setOptionsOpen}>
<div className="grid grid-cols-2 gap-4">
<ColorField
form={form}
name="optionBgColor.light"
label={t("environments.workspace.look.advanced_styling_field_option_bg")}
/>
<ColorField
form={form}
name="optionLabelColor.light"
label={t("environments.workspace.look.advanced_styling_field_option_label")}
/>
<DimensionInput
form={form}
name="optionBorderRadius"
label={t("environments.workspace.look.advanced_styling_field_border_radius")}
/>
<DimensionInput
form={form}
name="optionPaddingX"
label={t("environments.workspace.look.advanced_styling_field_padding_x")}
/>
<DimensionInput
form={form}
name="optionPaddingY"
label={t("environments.workspace.look.advanced_styling_field_padding_y")}
/>
<DimensionInput
form={form}
name="optionFontSize"
label={t("environments.workspace.look.advanced_styling_field_font_size")}
/>
</div>
</StylingSection>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>

View File

@@ -9,12 +9,13 @@ import { type JSX, useCallback } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TI18nString } from "@formbricks/types/i18n";
import { TSurveyMatrixElement } from "@formbricks/types/surveys/elements";
import type { TSurveyMatrixElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { MatrixSortableItem } from "@/modules/survey/editor/components/matrix-sortable-item";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
@@ -347,6 +348,16 @@ export const MatrixElementForm = ({
</div>
</div>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
/>
</form>
);
};

View File

@@ -17,6 +17,7 @@ import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { BulkEditOptionsModal } from "@/modules/survey/editor/components/bulk-edit-options-modal";
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
@@ -398,6 +399,19 @@ export const MultipleChoiceElementForm = ({
surveyLanguageCodes={surveyLanguageCodes}
locale={locale}
/>
{element.type === TSurveyElementTypeEnum.MultipleChoiceMulti && (
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
/>
)}
</form>
);
};

View File

@@ -1,19 +1,20 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
import { JSX, useEffect, useState } from "react";
import { PlusIcon } from "lucide-react";
import { JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyOpenTextElement, TSurveyOpenTextElementInputType } from "@formbricks/types/surveys/elements";
import type {
TSurveyOpenTextElement,
TSurveyOpenTextElementInputType,
} from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
interface OpenElementFormProps {
localSurvey: TSurvey;
@@ -42,43 +43,10 @@ export const OpenElementForm = ({
isExternalUrlsAllowed,
}: OpenElementFormProps): JSX.Element => {
const { t } = useTranslation();
const elementTypes = [
{ value: "text", label: t("common.text"), icon: <MessageSquareTextIcon className="h-4 w-4" /> },
{ value: "email", label: t("common.email"), icon: <MailIcon className="h-4 w-4" /> },
{ value: "url", label: t("common.url"), icon: <LinkIcon className="h-4 w-4" /> },
{ value: "number", label: t("common.number"), icon: <HashIcon className="h-4 w-4" /> },
{ value: "phone", label: t("common.phone"), icon: <PhoneIcon className="h-4 w-4" /> },
];
const defaultPlaceholder = getPlaceholderByInputType(element.inputType ?? "text");
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const [showCharLimits, setShowCharLimits] = useState(element.inputType === "text");
const handleInputChange = (inputType: TSurveyOpenTextElementInputType) => {
const updatedAttributes = {
inputType: inputType,
placeholder: createI18nString(getPlaceholderByInputType(inputType), surveyLanguageCodes),
longAnswer: inputType === "text" ? element.longAnswer : false,
charLimit: {
min: undefined,
max: undefined,
},
};
setIsCharLimitEnabled(false);
setShowCharLimits(inputType === "text");
updateElement(elementIdx, updatedAttributes);
};
const [parent] = useAutoAnimate();
const [isCharLimitEnabled, setIsCharLimitEnabled] = useState(false);
useEffect(() => {
if (element?.charLimit?.min !== undefined || element?.charLimit?.max !== undefined) {
setIsCharLimitEnabled(true);
} else {
setIsCharLimitEnabled(false);
}
}, [element?.charLimit?.max, element?.charLimit?.min]);
return (
<form>
@@ -156,80 +124,7 @@ export const OpenElementForm = ({
/>
</div>
{/* Add a dropdown to select the element type */}
<div className="mt-3">
<Label htmlFor="elementType">{t("common.input_type")}</Label>
<div className="mt-2 flex items-center">
<OptionsSwitch
options={elementTypes}
currentOption={element.inputType}
handleOptionChange={handleInputChange} // Use the merged function
/>
</div>
</div>
<div className="mt-6 space-y-6">
{showCharLimits && (
<AdvancedOptionToggle
isChecked={isCharLimitEnabled}
onToggle={(checked: boolean) => {
setIsCharLimitEnabled(checked);
updateElement(elementIdx, {
charLimit: {
enabled: checked,
min: undefined,
max: undefined,
},
});
}}
htmlId={`charLimit-${element.id}`}
description={t("environments.surveys.edit.character_limit_toggle_description")}
childBorder
title={t("environments.surveys.edit.character_limit_toggle_title")}
customContainerClass="p-0">
<div className="flex gap-4 p-4">
<div className="flex items-center gap-2">
<Label htmlFor="minLength">{t("common.minimum")}</Label>
<Input
id="minLength"
name="minLength"
type="number"
min={0}
value={element?.charLimit?.min || ""}
aria-label={t("common.minimum")}
className="bg-white"
onChange={(e) =>
updateElement(elementIdx, {
charLimit: {
...element?.charLimit,
min: e.target.value ? parseInt(e.target.value) : undefined,
},
})
}
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="maxLength">{t("common.maximum")}</Label>
<Input
id="maxLength"
name="maxLength"
type="number"
min={0}
aria-label={t("common.maximum")}
value={element?.charLimit?.max || ""}
className="bg-white"
onChange={(e) =>
updateElement(elementIdx, {
charLimit: {
...element?.charLimit,
max: e.target.value ? parseInt(e.target.value) : undefined,
},
})
}
/>
</div>
</div>
</AdvancedOptionToggle>
)}
<div className="mt-4">
<AdvancedOptionToggle
isChecked={element.longAnswer !== false}
@@ -245,6 +140,23 @@ export const OpenElementForm = ({
customContainerClass="p-0"
/>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
inputType={element.inputType ?? "text"}
onUpdateInputType={(newInputType) => {
updateElement(elementIdx, {
inputType: newInputType,
longAnswer: newInputType === "text",
});
}}
/>
</div>
</form>
);

View File

@@ -5,12 +5,13 @@ import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { type JSX } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyPictureSelectionElement } from "@formbricks/types/surveys/elements";
import type { TSurveyPictureSelectionElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { Button } from "@/modules/ui/components/button";
import { FileInput } from "@/modules/ui/components/file-input";
import { Label } from "@/modules/ui/components/label";
@@ -156,7 +157,19 @@ export const PictureSelectionForm = ({
checked={element.allowMulti}
onClick={(e) => {
e.stopPropagation();
updateElement(elementIdx, { allowMulti: !element.allowMulti });
const newAllowMulti = !element.allowMulti;
// If switching to single-select (allowMulti = false), remove validation rules
// since they're only applicable for multi-select
const updatedAttributes: Partial<TSurveyPictureSelectionElement> = {
allowMulti: newAllowMulti,
};
if (!newAllowMulti && element.validation?.rules && element.validation.rules.length > 0) {
updatedAttributes.validation = {
...element.validation,
rules: [],
};
}
updateElement(elementIdx, updatedAttributes);
}}
/>
<Label htmlFor="multi-select-toggle" className="cursor-pointer">
@@ -170,6 +183,18 @@ export const PictureSelectionForm = ({
</div>
</Label>
</div>
{element.allowMulti && (
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
/>
)}
</form>
);
};

View File

@@ -8,12 +8,13 @@ import { PlusIcon } from "lucide-react";
import { type JSX, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { TI18nString } from "@formbricks/types/i18n";
import { TSurveyRankingElement } from "@formbricks/types/surveys/elements";
import type { TSurveyRankingElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
@@ -246,6 +247,17 @@ export const RankingElementForm = ({
</div>
</div>
</div>
<ValidationRulesEditor
elementType={element.type}
validation={element.validation}
onUpdateValidation={(validation) => {
updateElement(elementIdx, {
validation,
});
}}
element={element}
/>
</form>
);
};

View File

@@ -45,7 +45,7 @@ export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: Redir
<div className="group relative">
{/* The highlight container is absolutely positioned behind the input */}
<div
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent`}
dir="auto"
key={highlightedJSX.toString()}>
{highlightedJSX}
@@ -81,7 +81,7 @@ export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: Redir
name="redirectUrlLabel"
className="bg-white"
placeholder="Formbricks App"
value={endingCard.label}
value={endingCard.label ?? ""}
onChange={(e) => updateSurvey({ label: e.target.value })}
/>
</div>

View File

@@ -0,0 +1,34 @@
"use client";
import { useTranslation } from "react-i18next";
import type { TValidationLogic } from "@formbricks/types/surveys/elements";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
interface ValidationLogicSelectorProps {
value: TValidationLogic;
onChange: (value: TValidationLogic) => void;
}
export const ValidationLogicSelector = ({ value, onChange }: ValidationLogicSelectorProps) => {
const { t } = useTranslation();
return (
<div className="flex w-full items-center gap-2">
<Select value={value} onValueChange={(val) => onChange(val as TValidationLogic)}>
<SelectTrigger className="h-8 w-fit bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="and">{t("environments.surveys.edit.validation_logic_and")}</SelectItem>
<SelectItem value="or">{t("environments.surveys.edit.validation_logic_or")}</SelectItem>
</SelectContent>
</Select>
</div>
);
};

View File

@@ -0,0 +1,42 @@
"use client";
import { useTranslation } from "react-i18next";
import { TAddressField, TContactInfoField } from "@formbricks/types/surveys/validation-rules";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
interface ValidationRuleFieldSelectorProps {
value: TAddressField | TContactInfoField | undefined;
onChange: (value: TAddressField | TContactInfoField | undefined) => void;
fieldOptions: { value: TAddressField | TContactInfoField; label: string }[];
}
export const ValidationRuleFieldSelector = ({
value,
onChange,
fieldOptions,
}: ValidationRuleFieldSelectorProps) => {
const { t } = useTranslation();
return (
<Select
value={value ?? ""}
onValueChange={(val) => onChange(val ? (val as TAddressField | TContactInfoField) : undefined)}>
<SelectTrigger className="h-9 min-w-[140px] bg-white">
<SelectValue placeholder={t("environments.surveys.edit.select_field")} />
</SelectTrigger>
<SelectContent>
{fieldOptions.map((field) => (
<SelectItem key={field.value} value={field.value}>
{field.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@@ -0,0 +1,45 @@
"use client";
import { useTranslation } from "react-i18next";
import { TSurveyOpenTextElementInputType } from "@formbricks/types/surveys/elements";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { cn } from "@/modules/ui/lib/utils";
interface ValidationRuleInputTypeSelectorProps {
value: TSurveyOpenTextElementInputType;
onChange?: (value: TSurveyOpenTextElementInputType) => void;
disabled?: boolean;
}
export const ValidationRuleInputTypeSelector = ({
value,
onChange,
disabled = false,
}: ValidationRuleInputTypeSelectorProps) => {
const { t } = useTranslation();
return (
<Select
value={value}
onValueChange={onChange ? (val) => onChange(val as TSurveyOpenTextElementInputType) : undefined}
disabled={disabled}>
<SelectTrigger
className={cn("h-9 min-w-[120px]", disabled ? "cursor-not-allowed bg-slate-100" : "bg-white")}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">{t("common.text")}</SelectItem>
<SelectItem value="email">{t("common.email")}</SelectItem>
<SelectItem value="url">{t("common.url")}</SelectItem>
<SelectItem value="phone">{t("common.phone")}</SelectItem>
<SelectItem value="number">{t("common.number")}</SelectItem>
</SelectContent>
</Select>
);
};

View File

@@ -0,0 +1,170 @@
"use client";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TAllowedFileExtension } from "@formbricks/types/storage";
import {
TSurveyElement,
TSurveyElementTypeEnum,
TSurveyOpenTextElementInputType,
} from "@formbricks/types/surveys/elements";
import {
TAddressField,
TContactInfoField,
TValidationRule,
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
import { Button } from "@/modules/ui/components/button";
import { RULE_TYPE_CONFIG } from "../lib/validation-rules-config";
import { getAvailableRuleTypes, getRuleValue } from "../lib/validation-rules-utils";
import { ValidationRuleFieldSelector } from "./validation-rule-field-selector";
import { ValidationRuleInputTypeSelector } from "./validation-rule-input-type-selector";
import { ValidationRuleTypeSelector } from "./validation-rule-type-selector";
import { ValidationRuleUnitSelector } from "./validation-rule-unit-selector";
import { ValidationRuleValueInput } from "./validation-rule-value-input";
interface ValidationRuleRowProps {
rule: TValidationRule;
index: number;
elementType: TSurveyElementTypeEnum;
element?: TSurveyElement;
inputType?: TSurveyOpenTextElementInputType;
onInputTypeChange?: (inputType: TSurveyOpenTextElementInputType) => void;
fieldOptions: { value: TAddressField | TContactInfoField; label: string }[];
needsFieldSelector: boolean;
validationRules: TValidationRule[];
ruleLabels: Record<string, string>;
onFieldChange: (ruleId: string, field: TAddressField | TContactInfoField | undefined) => void;
onRuleTypeChange: (ruleId: string, newType: TValidationRuleType) => void;
onRuleValueChange: (ruleId: string, value: string) => void;
onFileExtensionChange: (ruleId: string, extensions: TAllowedFileExtension[]) => void;
onDelete: (ruleId: string) => void;
onAdd: (insertAfterIndex: number) => void;
canAddMore: boolean;
}
export const ValidationRuleRow = ({
rule,
index,
elementType,
element,
inputType,
onInputTypeChange,
fieldOptions,
needsFieldSelector,
validationRules,
ruleLabels,
onFieldChange,
onRuleTypeChange,
onRuleValueChange,
onFileExtensionChange,
onDelete,
onAdd,
canAddMore,
}: ValidationRuleRowProps) => {
const { t } = useTranslation();
const ruleType = rule.type;
const config = RULE_TYPE_CONFIG[ruleType];
const currentValue = getRuleValue(rule);
// Get available types for this rule (current type + unused types, no duplicates)
// For address/contact info, filter by selected field
const ruleField = rule.field;
const otherAvailableTypes = getAvailableRuleTypes(
elementType,
validationRules.filter((r) => r.id !== rule.id),
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
ruleField
).filter((t) => t !== ruleType);
const availableTypesForSelect = [ruleType, ...otherAvailableTypes];
// Check if this is OpenText and first rule - show input type selector
const isOpenText = elementType === TSurveyElementTypeEnum.OpenText;
const isFirstRule = index === 0;
const showInputTypeSelector = isOpenText && isFirstRule;
const handleFileExtensionChange = (extensions: TAllowedFileExtension[]) => {
onFileExtensionChange(rule.id, extensions);
};
return (
<div className="flex w-full items-center gap-2">
{/* Field Selector (for Address and Contact Info elements) */}
{needsFieldSelector && (
<ValidationRuleFieldSelector
value={rule.field}
onChange={(value) => onFieldChange(rule.id, value)}
fieldOptions={fieldOptions}
/>
)}
{/* Input Type Selector (only for OpenText, first rule) */}
{showInputTypeSelector && inputType !== undefined && onInputTypeChange && (
<ValidationRuleInputTypeSelector value={inputType} onChange={onInputTypeChange} />
)}
{/* Input Type Display (disabled, for subsequent rules) */}
{isOpenText && !isFirstRule && inputType !== undefined && (
<ValidationRuleInputTypeSelector value={inputType} disabled />
)}
{/* Rule Type Selector */}
<ValidationRuleTypeSelector
value={ruleType}
onChange={(value) => onRuleTypeChange(rule.id, value)}
availableTypes={availableTypesForSelect}
ruleLabels={ruleLabels}
needsValue={config.needsValue}
/>
{/* Value Input (if needed) */}
{config.needsValue && (
<div className="flex w-full items-center gap-2">
<ValidationRuleValueInput
rule={rule}
ruleType={ruleType}
config={config}
currentValue={currentValue}
onChange={(value) => onRuleValueChange(rule.id, value)}
onFileExtensionChange={handleFileExtensionChange}
element={element}
/>
{/* Unit selector (if applicable) */}
{config.unitOptions && config.unitOptions.length > 0 && (
<ValidationRuleUnitSelector
value={config.unitOptions[0].value}
unitOptions={config.unitOptions}
ruleLabels={ruleLabels}
disabled={config.unitOptions.length === 1}
/>
)}
</div>
)}
{/* Delete button */}
<Button
variant="outline"
size="icon"
type="button"
onClick={() => onDelete(rule.id)}
className="shrink-0 bg-white"
aria-label={t("environments.surveys.edit.validation.delete_validation_rule")}>
<TrashIcon className="h-4 w-4" />
</Button>
{/* Add button */}
{canAddMore && (
<Button
variant="outline"
size="icon"
type="button"
onClick={() => onAdd(index)}
className="shrink-0 bg-white"
aria-label={t("environments.surveys.edit.validation.add_validation_rule")}>
<PlusIcon className="h-4 w-4" />
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,44 @@
"use client";
import { capitalize } from "lodash";
import { TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { cn } from "@/modules/ui/lib/utils";
import { RULE_TYPE_CONFIG } from "../lib/validation-rules-config";
interface ValidationRuleTypeSelectorProps {
value: TValidationRuleType;
onChange: (value: TValidationRuleType) => void;
availableTypes: TValidationRuleType[];
ruleLabels: Record<string, string>;
needsValue: boolean;
}
export const ValidationRuleTypeSelector = ({
value,
onChange,
availableTypes,
ruleLabels,
needsValue,
}: ValidationRuleTypeSelectorProps) => {
return (
<Select value={value} onValueChange={(val) => onChange(val as TValidationRuleType)}>
<SelectTrigger className={cn("bg-white", needsValue ? "min-w-[200px]" : "flex-1")}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableTypes.map((type) => (
<SelectItem key={type} value={type}>
{capitalize(ruleLabels[RULE_TYPE_CONFIG[type].labelKey])}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@@ -0,0 +1,44 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { cn } from "@/modules/ui/lib/utils";
interface UnitOption {
value: string;
labelKey: string;
}
interface ValidationRuleUnitSelectorProps {
value: string;
unitOptions: UnitOption[];
ruleLabels: Record<string, string>;
disabled?: boolean;
}
export const ValidationRuleUnitSelector = ({
value,
unitOptions,
ruleLabels,
disabled = false,
}: ValidationRuleUnitSelectorProps) => {
return (
<Select value={value} onValueChange={() => {}} disabled={disabled || unitOptions.length === 1}>
<SelectTrigger className={cn("h-9 min-w-[180px] flex-1 bg-white", disabled && "cursor-not-allowed")}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{unitOptions.map((unit) => (
<SelectItem key={unit.value} value={unit.value} className="truncate">
{ruleLabels[unit.labelKey]}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@@ -0,0 +1,137 @@
"use client";
import { useTranslation } from "react-i18next";
import { ALLOWED_FILE_EXTENSIONS, TAllowedFileExtension } from "@formbricks/types/storage";
import { TSurveyElement } from "@formbricks/types/surveys/elements";
import { TValidationRule, TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
import { Input } from "@/modules/ui/components/input";
import { MultiSelect } from "@/modules/ui/components/multi-select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { RULE_TYPE_CONFIG } from "../lib/validation-rules-config";
interface ValidationRuleValueInputProps {
rule: TValidationRule;
ruleType: TValidationRuleType;
config: (typeof RULE_TYPE_CONFIG)[TValidationRuleType];
currentValue: number | string | undefined;
onChange: (value: string) => void;
onFileExtensionChange: (extensions: TAllowedFileExtension[]) => void;
element?: TSurveyElement;
}
export const ValidationRuleValueInput = ({
rule,
ruleType,
config,
currentValue,
onChange,
onFileExtensionChange,
element,
}: ValidationRuleValueInputProps) => {
const { t } = useTranslation();
// Determine HTML input type for value inputs
let htmlInputType: "number" | "date" | "text" = "text";
if (config.valueType === "number") {
htmlInputType = "number";
} else if (
ruleType.startsWith("is") &&
(ruleType.includes("Later") || ruleType.includes("Earlier") || ruleType.includes("On"))
) {
htmlInputType = "date";
}
// Special handling for date range inputs
if (ruleType === "isBetween" || ruleType === "isNotBetween") {
return (
<div className="flex w-full items-center gap-2">
<Input
type="date"
value={(currentValue as string)?.split(",")?.[0] ?? ""}
onChange={(e) => {
const currentEndDate = (currentValue as string)?.split(",")?.[1] ?? "";
onChange(`${e.target.value},${currentEndDate}`);
}}
placeholder={t("environments.surveys.edit.validation.start_date")}
className="h-9 flex-1 bg-white"
/>
<span className="text-sm text-slate-500">{t("common.and")}</span>
<Input
type="date"
value={(currentValue as string)?.split(",")?.[1] ?? ""}
onChange={(e) => {
const currentStartDate = (currentValue as string)?.split(",")?.[0] ?? "";
onChange(`${currentStartDate},${e.target.value}`);
}}
placeholder={t("environments.surveys.edit.validation.end_date")}
className="h-9 flex-1 bg-white"
/>
</div>
);
}
// Option selector for single select validation rules
if (config.valueType === "option") {
const optionValue = typeof currentValue === "string" ? currentValue : "";
return (
<Select value={optionValue} onValueChange={onChange}>
<SelectTrigger className="h-9 min-w-[200px] bg-white">
<SelectValue placeholder={t("environments.surveys.edit.validation.select_option")} />
</SelectTrigger>
<SelectContent>
{element &&
"choices" in element &&
element.choices
.filter((choice) => choice.id !== "other" && choice.id !== "none" && "label" in choice)
.map((choice) => {
const choiceLabel =
"label" in choice
? choice.label.default || Object.values(choice.label)[0] || choice.id
: choice.id;
return (
<SelectItem key={choice.id} value={choice.id}>
{choiceLabel}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
}
// File extension MultiSelect
if (ruleType === "fileExtensionIs" || ruleType === "fileExtensionIsNot") {
const extensionOptions = ALLOWED_FILE_EXTENSIONS.map((ext) => ({
value: ext,
label: `.${ext}`,
}));
const selectedExtensions = (rule.params as { extensions: string[] })?.extensions || [];
return (
<MultiSelect
options={extensionOptions}
value={selectedExtensions as TAllowedFileExtension[]}
onChange={onFileExtensionChange}
placeholder={t("environments.surveys.edit.validation.select_file_extensions")}
disabled={false}
/>
);
}
// Default text/number input
return (
<Input
type={htmlInputType}
value={currentValue ?? ""}
onChange={(e) => onChange(e.target.value)}
placeholder={config.valuePlaceholder}
className="h-9 min-w-[80px] bg-white"
min={config.valueType === "number" ? 0 : ""}
/>
);
};

View File

@@ -0,0 +1,342 @@
"use client";
import { useTranslation } from "react-i18next";
import { v7 as uuidv7 } from "uuid";
import { TAllowedFileExtension } from "@formbricks/types/storage";
import {
TSurveyElement,
TSurveyElementTypeEnum,
TSurveyOpenTextElementInputType,
TValidationLogic,
} from "@formbricks/types/surveys/elements";
import {
TAddressField,
TContactInfoField,
TValidationRule,
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { RULE_TYPE_CONFIG } from "../lib/validation-rules-config";
import {
getAddressFields,
getContactInfoFields,
getDefaultRuleValue,
getRuleLabels,
parseRuleValue,
} from "../lib/validation-rules-helpers";
import { RULES_BY_INPUT_TYPE, createRuleParams, getAvailableRuleTypes } from "../lib/validation-rules-utils";
import { ValidationLogicSelector } from "./validation-logic-selector";
import { ValidationRuleRow } from "./validation-rule-row";
type TValidationField = TAddressField | TContactInfoField | undefined;
interface ValidationRulesEditorProps {
elementType: TSurveyElementTypeEnum;
validation?: { rules: TValidationRule[]; logic?: TValidationLogic };
onUpdateValidation: (validation: { rules: TValidationRule[]; logic: TValidationLogic }) => void;
element?: TSurveyElement;
// For OpenText: input type and callback to update it
inputType?: TSurveyOpenTextElementInputType;
onUpdateInputType?: (inputType: TSurveyOpenTextElementInputType) => void;
}
export const ValidationRulesEditor = ({
elementType,
validation,
onUpdateValidation,
element,
inputType,
onUpdateInputType,
}: ValidationRulesEditorProps) => {
const validationRules = validation?.rules ?? [];
const validationLogic = validation?.logic ?? "and";
const { t } = useTranslation();
// Field options for address and contact info elements
const isAddress = elementType === TSurveyElementTypeEnum.Address;
const isContactInfo = elementType === TSurveyElementTypeEnum.ContactInfo;
const needsFieldSelector = isAddress || isContactInfo;
let fieldOptions: { value: TAddressField | TContactInfoField; label: string }[] = [];
if (isAddress) {
fieldOptions = getAddressFields(t);
} else if (isContactInfo) {
fieldOptions = getContactInfoFields(t);
}
const ruleLabels = getRuleLabels(t);
const isEnabled = validationRules.length > 0;
const handleEnable = () => {
// For address/contact info, get rules for first field
const defaultField = needsFieldSelector && fieldOptions.length > 0 ? fieldOptions[0].value : undefined;
const availableRules = getAvailableRuleTypes(
elementType,
[],
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
defaultField
);
if (availableRules.length > 0) {
const defaultRuleType = availableRules[0];
const config = RULE_TYPE_CONFIG[defaultRuleType];
const defaultValue = getDefaultRuleValue(config, element);
const newRule: TValidationRule = {
id: uuidv7(),
type: defaultRuleType,
params: createRuleParams(defaultRuleType, defaultValue),
// For address/contact info, set field to first available field if not set
field: needsFieldSelector && fieldOptions.length > 0 ? fieldOptions[0].value : undefined,
} as TValidationRule;
onUpdateValidation({ rules: [newRule], logic: validationLogic });
}
};
const handleDisable = () => {
onUpdateValidation({ rules: [], logic: validationLogic });
};
const handleAddRule = (insertAfterIndex: number) => {
// For address/contact info, get rules for the field of the rule we're inserting after (or first field)
const insertAfterRule = validationRules[insertAfterIndex];
let fieldForNewRule: TValidationField;
if (insertAfterRule?.field) {
fieldForNewRule = insertAfterRule.field;
} else if (needsFieldSelector && fieldOptions.length > 0) {
fieldForNewRule = fieldOptions[0].value;
}
const availableRules = getAvailableRuleTypes(
elementType,
validationRules,
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
fieldForNewRule
);
if (availableRules.length === 0) return;
const newRuleType = availableRules[0];
const config = RULE_TYPE_CONFIG[newRuleType];
const defaultValue = getDefaultRuleValue(config, element);
let defaultField: TValidationField;
if (needsFieldSelector && fieldOptions.length > 0) {
defaultField = fieldOptions[0].value;
}
const newRule: TValidationRule = {
id: uuidv7(),
type: newRuleType,
params: createRuleParams(newRuleType, defaultValue),
field: defaultField,
} as TValidationRule;
const newRules = [...validationRules];
newRules.splice(insertAfterIndex + 1, 0, newRule);
onUpdateValidation({ rules: newRules, logic: validationLogic });
};
const handleDeleteRule = (ruleId: string) => {
const updated = validationRules.filter((r) => r.id !== ruleId);
onUpdateValidation({ rules: updated, logic: validationLogic });
};
const handleRuleTypeChange = (ruleId: string, newType: TValidationRuleType) => {
const ruleToUpdate = validationRules.find((r) => r.id === ruleId);
if (!ruleToUpdate) return;
// For address/contact info, verify the new rule type is valid for the selected field
if (needsFieldSelector && ruleToUpdate.field) {
const availableRulesForField = getAvailableRuleTypes(
elementType,
validationRules.filter((r) => r.id !== ruleId),
undefined,
ruleToUpdate.field
);
// If the new rule type is not available for this field, don't change it
if (!availableRulesForField.includes(newType)) {
return;
}
}
const updated = validationRules.map((rule) => {
if (rule.id !== ruleId) return rule;
return {
...rule,
type: newType,
params: createRuleParams(newType),
} as TValidationRule;
});
onUpdateValidation({ rules: updated, logic: validationLogic });
};
const handleFieldChange = (ruleId: string, field: TValidationField) => {
const ruleToUpdate = validationRules.find((r) => r.id === ruleId);
if (!ruleToUpdate) return;
// If changing field, check if current rule type is still valid for the new field
// If not, change to first available rule type for that field
let updatedRule = { ...ruleToUpdate, field } as TValidationRule;
if (field) {
const availableRulesForField = getAvailableRuleTypes(
elementType,
validationRules.filter((r) => r.id !== ruleId),
undefined,
field
);
// If current rule type is not available for the new field, change it
if (!availableRulesForField.includes(ruleToUpdate.type) && availableRulesForField.length > 0) {
updatedRule = {
...updatedRule,
type: availableRulesForField[0],
params: createRuleParams(availableRulesForField[0]),
} as TValidationRule;
}
}
const updated = validationRules.map((rule) => {
if (rule.id !== ruleId) return rule;
return updatedRule;
});
onUpdateValidation({ rules: updated, logic: validationLogic });
};
const handleRuleValueChange = (ruleId: string, value: string) => {
const updated = validationRules.map((rule) => {
if (rule.id !== ruleId) return rule;
const ruleType = rule.type;
const config = RULE_TYPE_CONFIG[ruleType];
const parsedValue = parseRuleValue(ruleType, value, config);
return {
...rule,
params: createRuleParams(ruleType, parsedValue),
} as TValidationRule;
});
onUpdateValidation({ rules: updated, logic: validationLogic });
};
const handleFileExtensionChange = (ruleId: string, extensions: TAllowedFileExtension[]) => {
const updated = validationRules.map((r) => {
if (r.id !== ruleId) return r;
return {
...r,
params: {
extensions,
},
} as TValidationRule;
});
onUpdateValidation({ rules: updated, logic: validationLogic });
};
// Handle input type change for OpenText
const handleInputTypeChange = (newInputType: TSurveyOpenTextElementInputType) => {
if (!onUpdateInputType) return;
// Update element input type
onUpdateInputType(newInputType);
// Filter out incompatible rules based on new input type
// Also remove redundant "email"/"url"/"phone" rules when inputType matches
const compatibleRules = RULES_BY_INPUT_TYPE[newInputType] ?? [];
const filteredRules = validationRules.filter((rule) => {
// Remove rules that aren't compatible with the new input type
if (!compatibleRules.includes(rule.type)) {
return false;
}
// Remove redundant validation rules when inputType matches
if (newInputType === "email" && rule.type === "email") {
return false;
}
if (newInputType === "url" && rule.type === "url") {
return false;
}
if (newInputType === "phone" && rule.type === "phone") {
return false;
}
return true;
});
// If no compatible rules remain, add a default rule
if (filteredRules.length === 0 && compatibleRules.length > 0) {
const defaultRuleType = compatibleRules[0];
const config = RULE_TYPE_CONFIG[defaultRuleType];
let defaultValue: number | string | undefined = undefined;
if (config.needsValue && config.valueType === "number") {
defaultValue = 0;
} else if (config.needsValue && config.valueType === "text") {
defaultValue = "";
}
const defaultRule: TValidationRule = {
id: uuidv7(),
type: defaultRuleType,
params: createRuleParams(defaultRuleType, defaultValue),
} as TValidationRule;
onUpdateValidation({ rules: [defaultRule], logic: validationLogic });
} else if (filteredRules.length !== validationRules.length) {
onUpdateValidation({ rules: filteredRules, logic: validationLogic });
}
};
// For address/contact info, use first field if no rules exist, or use the field from last rule
let defaultField: TValidationField;
if (needsFieldSelector && validationRules.length > 0) {
defaultField = validationRules.at(-1)?.field;
} else if (needsFieldSelector && fieldOptions.length > 0) {
defaultField = fieldOptions[0].value;
} else {
defaultField = undefined;
}
const availableRulesForAdd = getAvailableRuleTypes(
elementType,
validationRules,
elementType === TSurveyElementTypeEnum.OpenText ? inputType : undefined,
defaultField
);
const canAddMore = availableRulesForAdd.length > 0;
return (
<AdvancedOptionToggle
isChecked={isEnabled}
onToggle={(checked) => (checked ? handleEnable() : handleDisable())}
htmlId="validation-rules-toggle"
title={t("environments.surveys.edit.validation_rules")}
description={t("environments.surveys.edit.validation_rules_description")}
customContainerClass="p-0 mt-4"
childrenContainerClass="flex-col p-3 gap-2">
{/* Validation Logic Selector - only show when there are 2+ rules */}
{validationRules.length >= 2 && (
<ValidationLogicSelector
value={validationLogic}
onChange={(value) => onUpdateValidation({ rules: validationRules, logic: value })}
/>
)}
<div className="flex w-full flex-col gap-2">
{validationRules.map((rule, index) => (
<ValidationRuleRow
key={rule.id}
rule={rule}
index={index}
elementType={elementType}
element={element}
inputType={inputType}
onInputTypeChange={handleInputTypeChange}
fieldOptions={fieldOptions}
needsFieldSelector={needsFieldSelector}
validationRules={validationRules}
ruleLabels={ruleLabels}
onFieldChange={handleFieldChange}
onRuleTypeChange={handleRuleTypeChange}
onRuleValueChange={handleRuleValueChange}
onFileExtensionChange={handleFileExtensionChange}
onDelete={handleDeleteRule}
onAdd={handleAddRule}
canAddMore={canAddMore}
/>
))}
</div>
</AdvancedOptionToggle>
);
};

View File

@@ -0,0 +1,176 @@
import { describe, expect, test } from "vitest";
import { TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
import { RULE_TYPE_CONFIG } from "./validation-rules-config";
describe("RULE_TYPE_CONFIG", () => {
test("should have config for all validation rule types", () => {
const allRuleTypes: TValidationRuleType[] = [
"minLength",
"maxLength",
"pattern",
"email",
"url",
"phone",
"minValue",
"maxValue",
"minSelections",
"maxSelections",
];
allRuleTypes.forEach((ruleType) => {
expect(RULE_TYPE_CONFIG[ruleType]).toBeDefined();
expect(RULE_TYPE_CONFIG[ruleType].labelKey).toBeDefined();
expect(typeof RULE_TYPE_CONFIG[ruleType].labelKey).toBe("string");
expect(typeof RULE_TYPE_CONFIG[ruleType].needsValue).toBe("boolean");
});
});
describe("minLength rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.minLength;
expect(config.labelKey).toBe("min_length");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("number");
expect(config.valuePlaceholder).toBe("100");
expect(config.unitOptions).toEqual([{ value: "characters", labelKey: "characters" }]);
});
});
describe("maxLength rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.maxLength;
expect(config.labelKey).toBe("max_length");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("number");
expect(config.valuePlaceholder).toBe("500");
expect(config.unitOptions).toEqual([{ value: "characters", labelKey: "characters" }]);
});
});
describe("pattern rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.pattern;
expect(config.labelKey).toBe("pattern");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("text");
expect(config.valuePlaceholder).toBe("^[A-Z].*");
expect(config.unitOptions).toBeUndefined();
});
});
describe("email rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.email;
expect(config.labelKey).toBe("email");
expect(config.needsValue).toBe(false);
expect(config.valueType).toBeUndefined();
expect(config.valuePlaceholder).toBeUndefined();
expect(config.unitOptions).toBeUndefined();
});
});
describe("url rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.url;
expect(config.labelKey).toBe("url");
expect(config.needsValue).toBe(false);
expect(config.valueType).toBeUndefined();
expect(config.valuePlaceholder).toBeUndefined();
expect(config.unitOptions).toBeUndefined();
});
});
describe("phone rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.phone;
expect(config.labelKey).toBe("phone");
expect(config.needsValue).toBe(false);
expect(config.valueType).toBeUndefined();
expect(config.valuePlaceholder).toBeUndefined();
expect(config.unitOptions).toBeUndefined();
});
});
describe("minValue rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.minValue;
expect(config.labelKey).toBe("min_value");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("number");
expect(config.valuePlaceholder).toBe("0");
expect(config.unitOptions).toBeUndefined();
});
});
describe("maxValue rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.maxValue;
expect(config.labelKey).toBe("max_value");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("number");
expect(config.valuePlaceholder).toBe("100");
expect(config.unitOptions).toBeUndefined();
});
});
describe("minSelections rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.minSelections;
expect(config.labelKey).toBe("min_selections");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("number");
expect(config.valuePlaceholder).toBe("1");
expect(config.unitOptions).toEqual([{ value: "options", labelKey: "options_selected" }]);
});
});
describe("maxSelections rule", () => {
test("should have correct config", () => {
const config = RULE_TYPE_CONFIG.maxSelections;
expect(config.labelKey).toBe("max_selections");
expect(config.needsValue).toBe(true);
expect(config.valueType).toBe("number");
expect(config.valuePlaceholder).toBe("3");
expect(config.unitOptions).toEqual([{ value: "options", labelKey: "options_selected" }]);
});
});
describe("valueType validation", () => {
test("should have valueType 'number' for numeric rules", () => {
expect(RULE_TYPE_CONFIG.minLength.valueType).toBe("number");
expect(RULE_TYPE_CONFIG.maxLength.valueType).toBe("number");
expect(RULE_TYPE_CONFIG.minValue.valueType).toBe("number");
expect(RULE_TYPE_CONFIG.maxValue.valueType).toBe("number");
expect(RULE_TYPE_CONFIG.minSelections.valueType).toBe("number");
expect(RULE_TYPE_CONFIG.maxSelections.valueType).toBe("number");
});
test("should have valueType 'text' for text rules", () => {
expect(RULE_TYPE_CONFIG.pattern.valueType).toBe("text");
});
test("should not have valueType for rules that don't need values", () => {
expect(RULE_TYPE_CONFIG.email.valueType).toBeUndefined();
expect(RULE_TYPE_CONFIG.url.valueType).toBeUndefined();
expect(RULE_TYPE_CONFIG.phone.valueType).toBeUndefined();
});
});
describe("unitOptions validation", () => {
test("should have unitOptions for length and selection rules", () => {
expect(RULE_TYPE_CONFIG.minLength.unitOptions).toBeDefined();
expect(RULE_TYPE_CONFIG.maxLength.unitOptions).toBeDefined();
expect(RULE_TYPE_CONFIG.minSelections.unitOptions).toBeDefined();
expect(RULE_TYPE_CONFIG.maxSelections.unitOptions).toBeDefined();
});
test("should not have unitOptions for other rules", () => {
expect(RULE_TYPE_CONFIG.pattern.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.email.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.url.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.phone.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.minValue.unitOptions).toBeUndefined();
expect(RULE_TYPE_CONFIG.maxValue.unitOptions).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,158 @@
import { TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
// Rule type definitions with i18n keys
export const RULE_TYPE_CONFIG: Record<
TValidationRuleType,
{
labelKey: string;
needsValue: boolean;
valueType?: "number" | "text" | "option" | "ranking";
valuePlaceholder?: string;
unitOptions?: { value: string; labelKey: string }[];
}
> = {
minLength: {
labelKey: "min_length",
needsValue: true,
valueType: "number",
valuePlaceholder: "100",
unitOptions: [{ value: "characters", labelKey: "characters" }],
},
maxLength: {
labelKey: "max_length",
needsValue: true,
valueType: "number",
valuePlaceholder: "500",
unitOptions: [{ value: "characters", labelKey: "characters" }],
},
pattern: {
labelKey: "pattern",
needsValue: true,
valueType: "text",
valuePlaceholder: "^[A-Z].*",
},
email: {
labelKey: "email",
needsValue: false,
},
url: {
labelKey: "url",
needsValue: false,
},
phone: {
labelKey: "phone",
needsValue: false,
},
minValue: {
labelKey: "min_value",
needsValue: true,
valueType: "number",
valuePlaceholder: "0",
},
maxValue: {
labelKey: "max_value",
needsValue: true,
valueType: "number",
valuePlaceholder: "100",
},
minSelections: {
labelKey: "min_selections",
needsValue: true,
valueType: "number",
valuePlaceholder: "1",
unitOptions: [{ value: "options", labelKey: "options_selected" }],
},
maxSelections: {
labelKey: "max_selections",
needsValue: true,
valueType: "number",
valuePlaceholder: "3",
unitOptions: [{ value: "options", labelKey: "options_selected" }],
},
equals: {
labelKey: "is",
needsValue: true,
valueType: "text",
},
doesNotEqual: {
labelKey: "is_not",
needsValue: true,
valueType: "text",
},
contains: {
labelKey: "contains",
needsValue: true,
valueType: "text",
},
doesNotContain: {
labelKey: "does_not_contain",
needsValue: true,
valueType: "text",
},
isGreaterThan: {
labelKey: "is_greater_than",
needsValue: true,
valueType: "number",
valuePlaceholder: "0",
},
isLessThan: {
labelKey: "is_less_than",
needsValue: true,
valueType: "number",
valuePlaceholder: "100",
},
isLaterThan: {
labelKey: "is_later_than",
needsValue: true,
valueType: "text",
valuePlaceholder: "YYYY-MM-DD",
},
isEarlierThan: {
labelKey: "is_earlier_than",
needsValue: true,
valueType: "text",
valuePlaceholder: "YYYY-MM-DD",
},
isBetween: {
labelKey: "is_between",
needsValue: true,
valueType: "text",
valuePlaceholder: "YYYY-MM-DD,YYYY-MM-DD",
},
isNotBetween: {
labelKey: "is_not_between",
needsValue: true,
valueType: "text",
valuePlaceholder: "YYYY-MM-DD,YYYY-MM-DD",
},
minRanked: {
labelKey: "minimum_options_ranked",
needsValue: true,
valueType: "number",
valuePlaceholder: "1",
},
rankAll: {
labelKey: "rank_all_options",
needsValue: false,
},
minRowsAnswered: {
labelKey: "minimum_rows_answered",
needsValue: true,
valueType: "number",
valuePlaceholder: "1",
},
answerAllRows: {
labelKey: "answer_all_rows",
needsValue: false,
},
fileExtensionIs: {
labelKey: "file_extension_is",
needsValue: true,
valueType: "text",
},
fileExtensionIsNot: {
labelKey: "file_extension_is_not",
needsValue: true,
valueType: "text",
},
};

View File

@@ -0,0 +1,237 @@
import { describe, expect, test } from "vitest";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import type {
TSurveyElement,
TSurveyMultipleChoiceElement,
TSurveyRankingElement,
} from "@formbricks/types/surveys/elements";
import { RULE_TYPE_CONFIG } from "./validation-rules-config";
import {
getAddressFields,
getContactInfoFields,
getDefaultRuleValue,
getRuleLabels,
normalizeFileExtension,
parseRuleValue,
} from "./validation-rules-helpers";
// Mock translation function
const mockT = (key: string): string => key;
describe("getAddressFields", () => {
test("should return all address fields with correct labels", () => {
const fields = getAddressFields(mockT);
expect(fields).toHaveLength(6);
expect(fields.map((f) => f.value)).toEqual([
"addressLine1",
"addressLine2",
"city",
"state",
"zip",
"country",
]);
expect(fields[0].label).toBe("environments.surveys.edit.address_line_1");
});
});
describe("getContactInfoFields", () => {
test("should return all contact info fields with correct labels", () => {
const fields = getContactInfoFields(mockT);
expect(fields).toHaveLength(5);
expect(fields.map((f) => f.value)).toEqual(["firstName", "lastName", "email", "phone", "company"]);
expect(fields[0].label).toBe("environments.surveys.edit.first_name");
});
});
describe("getRuleLabels", () => {
test("should return all rule labels", () => {
const labels = getRuleLabels(mockT);
expect(labels).toHaveProperty("min_length");
expect(labels).toHaveProperty("max_length");
expect(labels).toHaveProperty("pattern");
expect(labels).toHaveProperty("email");
expect(labels).toHaveProperty("url");
expect(labels).toHaveProperty("phone");
expect(labels).toHaveProperty("min_value");
expect(labels).toHaveProperty("max_value");
expect(labels).toHaveProperty("min_selections");
expect(labels).toHaveProperty("max_selections");
expect(labels).toHaveProperty("characters");
expect(labels).toHaveProperty("options_selected");
expect(labels).toHaveProperty("is");
expect(labels).toHaveProperty("is_not");
expect(labels).toHaveProperty("contains");
expect(labels).toHaveProperty("does_not_contain");
expect(labels).toHaveProperty("is_greater_than");
expect(labels).toHaveProperty("is_less_than");
expect(labels).toHaveProperty("is_later_than");
expect(labels).toHaveProperty("is_earlier_than");
expect(labels).toHaveProperty("is_between");
expect(labels).toHaveProperty("is_not_between");
expect(labels).toHaveProperty("minimum_options_ranked");
expect(labels).toHaveProperty("rank_all_options");
expect(labels).toHaveProperty("minimum_rows_answered");
expect(labels).toHaveProperty("file_extension_is");
expect(labels).toHaveProperty("file_extension_is_not");
expect(labels).toHaveProperty("kb");
expect(labels).toHaveProperty("mb");
});
test("should return correct translation keys", () => {
const labels = getRuleLabels(mockT);
expect(labels.min_length).toBe("environments.surveys.edit.validation.min_length");
expect(labels.email).toBe("environments.surveys.edit.validation.email");
expect(labels.rank_all_options).toBe("environments.surveys.edit.validation.rank_all_options");
});
});
describe("getDefaultRuleValue", () => {
test("should return undefined when config does not need value", () => {
const config = RULE_TYPE_CONFIG.email;
const value = getDefaultRuleValue(config);
expect(value).toBeUndefined();
});
test("should return empty string for text value type", () => {
const config = RULE_TYPE_CONFIG.pattern;
const value = getDefaultRuleValue(config);
expect(value).toBe("");
});
test("should return empty string for equals rule (has valueType: text, not option)", () => {
const element: TSurveyElement = {
id: "multi1",
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
choices: [
{ id: "opt1", label: { default: "Option 1" } },
{ id: "opt2", label: { default: "Option 2" } },
{ id: "other", label: { default: "Other" } },
],
} as TSurveyMultipleChoiceElement;
const config = RULE_TYPE_CONFIG.equals;
const value = getDefaultRuleValue(config, element);
// equals has valueType: "text", not "option", so it returns "" (empty string for text type)
expect(value).toBe("");
});
test("should return empty string when config valueType is text (not option)", () => {
const element: TSurveyElement = {
id: "multi1",
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
choices: [
{ id: "other", label: { default: "Other" } },
{ id: "none", label: { default: "None" } },
{ id: "opt1", label: { default: "Option 1" } },
],
} as TSurveyMultipleChoiceElement;
const config = RULE_TYPE_CONFIG.equals;
const value = getDefaultRuleValue(config, element);
// equals has valueType: "text", so it returns "" regardless of element choices
expect(value).toBe("");
});
test("should return empty string when no valid choices found for option value type", () => {
const element: TSurveyElement = {
id: "multi1",
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
choices: [
{ id: "other", label: { default: "Other" } },
{ id: "none", label: { default: "None" } },
],
} as TSurveyMultipleChoiceElement;
const config = RULE_TYPE_CONFIG.equals;
const value = getDefaultRuleValue(config, element);
expect(value).toBe("");
});
test("should return empty string for option value type when element is not provided", () => {
const config = RULE_TYPE_CONFIG.equals;
const value = getDefaultRuleValue(config);
expect(value).toBe("");
});
test("should return undefined for number value type (minRanked uses number, not ranking)", () => {
const element: TSurveyElement = {
id: "rank1",
type: TSurveyElementTypeEnum.Ranking,
choices: [
{ id: "opt1", label: { default: "Option 1" } },
{ id: "opt2", label: { default: "Option 2" } },
],
} as TSurveyRankingElement;
const config = RULE_TYPE_CONFIG.minRanked;
const value = getDefaultRuleValue(config, element);
// minRanked has valueType: "number", not "ranking", so it returns undefined
expect(value).toBeUndefined();
});
test("should return undefined for number value type when element is not provided", () => {
const config = RULE_TYPE_CONFIG.minRanked;
const value = getDefaultRuleValue(config);
expect(value).toBeUndefined();
});
});
describe("normalizeFileExtension", () => {
test("should add dot prefix when missing", () => {
expect(normalizeFileExtension("pdf")).toBe(".pdf");
expect(normalizeFileExtension("jpg")).toBe(".jpg");
});
test("should not add dot prefix when already present", () => {
expect(normalizeFileExtension(".pdf")).toBe(".pdf");
expect(normalizeFileExtension(".jpg")).toBe(".jpg");
});
test("should handle empty string", () => {
expect(normalizeFileExtension("")).toBe(".");
});
});
describe("parseRuleValue", () => {
test("should normalize file extension for fileExtensionIs", () => {
const config = RULE_TYPE_CONFIG.fileExtensionIs;
const value = parseRuleValue("fileExtensionIs", "pdf", config);
expect(value).toBe(".pdf");
});
test("should normalize file extension for fileExtensionIsNot", () => {
const config = RULE_TYPE_CONFIG.fileExtensionIsNot;
const value = parseRuleValue("fileExtensionIsNot", "jpg", config);
expect(value).toBe(".jpg");
});
test("should not add dot if already present for file extension", () => {
const config = RULE_TYPE_CONFIG.fileExtensionIs;
const value = parseRuleValue("fileExtensionIs", ".pdf", config);
expect(value).toBe(".pdf");
});
test("should parse number for number value type", () => {
const config = RULE_TYPE_CONFIG.minLength;
const value = parseRuleValue("minLength", "10", config);
expect(value).toBe(10);
});
test("should return 0 for invalid number string", () => {
const config = RULE_TYPE_CONFIG.minLength;
const value = parseRuleValue("minLength", "invalid", config);
expect(value).toBe(0);
});
test("should return string as-is for text value type", () => {
const config = RULE_TYPE_CONFIG.pattern;
const value = parseRuleValue("pattern", "test-pattern", config);
expect(value).toBe("test-pattern");
});
test("should return string as-is for equals rule", () => {
const config = RULE_TYPE_CONFIG.equals;
const value = parseRuleValue("equals", "test-value", config);
expect(value).toBe("test-value");
});
});

View File

@@ -0,0 +1,117 @@
import { TSurveyElement } from "@formbricks/types/surveys/elements";
import {
TAddressField,
TContactInfoField,
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
import { RULE_TYPE_CONFIG } from "./validation-rules-config";
// Field options for address elements
export const getAddressFields = (t: (key: string) => string): { value: TAddressField; label: string }[] => [
{ value: "addressLine1", label: t("environments.surveys.edit.address_line_1") },
{ value: "addressLine2", label: t("environments.surveys.edit.address_line_2") },
{ value: "city", label: t("environments.surveys.edit.city") },
{ value: "state", label: t("environments.surveys.edit.state") },
{ value: "zip", label: t("environments.surveys.edit.zip") },
{ value: "country", label: t("environments.surveys.edit.country") },
];
// Field options for contact info elements
export const getContactInfoFields = (
t: (key: string) => string
): { value: TContactInfoField; label: string }[] => [
{ value: "firstName", label: t("environments.surveys.edit.first_name") },
{ value: "lastName", label: t("environments.surveys.edit.last_name") },
{ value: "email", label: t("common.email") },
{ value: "phone", label: t("common.phone") },
{ value: "company", label: t("environments.surveys.edit.company") },
];
// Rule labels mapping
export const getRuleLabels = (t: (key: string) => string): Record<string, string> => ({
min_length: t("environments.surveys.edit.validation.min_length"),
max_length: t("environments.surveys.edit.validation.max_length"),
pattern: t("environments.surveys.edit.validation.pattern"),
email: t("environments.surveys.edit.validation.email"),
url: t("environments.surveys.edit.validation.url"),
phone: t("environments.surveys.edit.validation.phone"),
min_value: t("environments.surveys.edit.validation.min_value"),
max_value: t("environments.surveys.edit.validation.max_value"),
min_selections: t("environments.surveys.edit.validation.min_selections"),
max_selections: t("environments.surveys.edit.validation.max_selections"),
characters: t("environments.surveys.edit.validation.characters"),
options_selected: t("environments.surveys.edit.validation.options_selected"),
is: t("environments.surveys.edit.validation.is"),
is_not: t("environments.surveys.edit.validation.is_not"),
contains: t("environments.surveys.edit.validation.contains"),
does_not_contain: t("environments.surveys.edit.validation.does_not_contain"),
is_greater_than: t("environments.surveys.edit.validation.is_greater_than"),
is_less_than: t("environments.surveys.edit.validation.is_less_than"),
is_later_than: t("environments.surveys.edit.validation.is_later_than"),
is_earlier_than: t("environments.surveys.edit.validation.is_earlier_than"),
is_between: t("environments.surveys.edit.validation.is_between"),
is_not_between: t("environments.surveys.edit.validation.is_not_between"),
minimum_options_ranked: t("environments.surveys.edit.validation.minimum_options_ranked"),
rank_all_options: t("environments.surveys.edit.validation.rank_all_options"),
minimum_rows_answered: t("environments.surveys.edit.validation.minimum_rows_answered"),
answer_all_rows: t("environments.surveys.edit.validation.answer_all_rows"),
file_extension_is: t("environments.surveys.edit.validation.file_extension_is"),
file_extension_is_not: t("environments.surveys.edit.validation.file_extension_is_not"),
kb: t("environments.surveys.edit.validation.kb"),
mb: t("environments.surveys.edit.validation.mb"),
});
// Helper function to get default value for a validation rule based on its config and element
export const getDefaultRuleValue = (
config: (typeof RULE_TYPE_CONFIG)[TValidationRuleType],
element?: TSurveyElement
): number | string | undefined => {
if (!config.needsValue) {
return undefined;
}
if (config.valueType === "text") {
return "";
}
if (config.valueType === "option") {
if (element && "choices" in element) {
const firstChoice = element.choices.find((c) => c.id !== "other" && c.id !== "none");
return firstChoice?.id ?? "";
}
return "";
}
if (config.valueType === "ranking") {
if (element && "choices" in element) {
const firstChoice = element.choices.find((c) => c.id !== "other" && c.id !== "none");
return firstChoice ? `${firstChoice.id},1` : ",1";
}
return ",1";
}
return undefined;
};
// Helper function to normalize file extension format
export const normalizeFileExtension = (value: string): string => {
return value.startsWith(".") ? value : `.${value}`;
};
// Helper function to parse and validate rule value based on rule type
export const parseRuleValue = (
ruleType: TValidationRuleType,
value: string,
config: (typeof RULE_TYPE_CONFIG)[TValidationRuleType]
): string | number => {
// Handle file extension formatting: auto-add dot if missing
if (ruleType === "fileExtensionIs" || ruleType === "fileExtensionIsNot") {
return normalizeFileExtension(value);
}
if (config.valueType === "number") {
return Number(value) || 0;
}
return value;
};

View File

@@ -0,0 +1,480 @@
import { describe, expect, test } from "vitest";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TValidationRule } from "@formbricks/types/surveys/validation-rules";
import { createRuleParams, getAvailableRuleTypes, getRuleValue } from "./validation-rules-utils";
describe("getAvailableRuleTypes", () => {
test("should return text rules for openText element with text inputType when no rules exist", () => {
const elementType = TSurveyElementTypeEnum.OpenText;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules, "text");
expect(available).toContain("minLength");
expect(available).toContain("maxLength");
expect(available).toContain("pattern");
expect(available).not.toContain("email"); // Excluded - redundant
expect(available).not.toContain("url"); // Excluded - redundant
expect(available).not.toContain("phone"); // Excluded - redundant
expect(available).not.toContain("minValue"); // Only for number inputType
});
test("should return text rules for openText element with email inputType", () => {
const elementType = TSurveyElementTypeEnum.OpenText;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules, "email");
expect(available).toContain("minLength");
expect(available).toContain("maxLength");
expect(available).not.toContain("email"); // Excluded - redundant when inputType=email
expect(available).not.toContain("minValue"); // Only for number inputType
});
test("should return numeric rules for openText element with number inputType", () => {
const elementType = TSurveyElementTypeEnum.OpenText;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules, "number");
expect(available).toContain("minValue");
expect(available).toContain("maxValue");
expect(available).not.toContain("isGreaterThan"); // Removed - redundant with minValue
expect(available).not.toContain("isLessThan"); // Removed - redundant with maxValue
expect(available).not.toContain("minLength"); // Only for text inputType
expect(available).not.toContain("email"); // Excluded
});
test("should filter out already added rules", () => {
const elementType = TSurveyElementTypeEnum.OpenText;
const existingRules: TValidationRule[] = [
{
id: "rule2",
type: "minLength",
params: { min: 10 },
},
];
const available = getAvailableRuleTypes(elementType, existingRules, "text");
expect(available).not.toContain("minLength");
expect(available).toContain("maxLength");
expect(available).toContain("pattern");
});
test("should return empty array for multipleChoiceSingle element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.MultipleChoiceSingle;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return minSelections, maxSelections for multipleChoiceMulti element", () => {
const elementType = TSurveyElementTypeEnum.MultipleChoiceMulti;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toContain("minSelections");
expect(available).toContain("maxSelections");
expect(available.length).toBe(2);
});
test("should return empty array for rating element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.Rating;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return empty array for nps element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.NPS;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return date validation rules for date element", () => {
const elementType = TSurveyElementTypeEnum.Date;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toContain("isLaterThan");
expect(available).toContain("isEarlierThan");
expect(available).toContain("isBetween");
expect(available).toContain("isNotBetween");
});
test("should return empty array for consent element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.Consent;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return matrix validation rules for matrix element", () => {
const elementType = TSurveyElementTypeEnum.Matrix;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toContain("minRowsAnswered");
expect(available).toContain("answerAllRows");
expect(available.length).toBe(2);
});
test("should return ranking validation rules for ranking element", () => {
const elementType = TSurveyElementTypeEnum.Ranking;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toContain("minRanked");
expect(available).toContain("rankAll");
expect(available.length).toBe(2);
});
test("should return file validation rules for fileUpload element", () => {
const elementType = TSurveyElementTypeEnum.FileUpload;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toContain("fileExtensionIs");
expect(available).toContain("fileExtensionIsNot");
});
test("should return minSelections, maxSelections for pictureSelection element", () => {
const elementType = TSurveyElementTypeEnum.PictureSelection;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toContain("minSelections");
expect(available).toContain("maxSelections");
expect(available.length).toBe(2);
});
test("should return empty array for address element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.Address;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return empty array for contactInfo element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.ContactInfo;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return empty array for cal element (no validation rules)", () => {
const elementType = TSurveyElementTypeEnum.Cal;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should return empty array for cta element", () => {
const elementType = TSurveyElementTypeEnum.CTA;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
test("should handle unknown element type gracefully", () => {
const elementType = "unknown" as TSurveyElementTypeEnum;
const existingRules: TValidationRule[] = [];
const available = getAvailableRuleTypes(elementType, existingRules);
expect(available).toEqual([]);
});
});
describe("getRuleValue", () => {
test("should return min value for minLength rule", () => {
const rule: TValidationRule = {
id: "rule1",
type: "minLength",
params: { min: 10 },
};
expect(getRuleValue(rule)).toBe(10);
});
test("should return max value for maxLength rule", () => {
const rule: TValidationRule = {
id: "rule2",
type: "maxLength",
params: { max: 100 },
};
expect(getRuleValue(rule)).toBe(100);
});
test("should return pattern string for pattern rule", () => {
const rule: TValidationRule = {
id: "rule3",
type: "pattern",
params: { pattern: "^[A-Z].*" },
};
expect(getRuleValue(rule)).toBe("^[A-Z].*");
});
test("should return pattern string with flags for pattern rule", () => {
const rule: TValidationRule = {
id: "rule3",
type: "pattern",
params: { pattern: "^[A-Z].*", flags: "i" },
};
expect(getRuleValue(rule)).toBe("^[A-Z].*");
});
test("should return min value for minValue rule", () => {
const rule: TValidationRule = {
id: "rule4",
type: "minValue",
params: { min: 5 },
};
expect(getRuleValue(rule)).toBe(5);
});
test("should return max value for maxValue rule", () => {
const rule: TValidationRule = {
id: "rule5",
type: "maxValue",
params: { max: 50 },
};
expect(getRuleValue(rule)).toBe(50);
});
test("should return min value for minSelections rule", () => {
const rule: TValidationRule = {
id: "rule6",
type: "minSelections",
params: { min: 2 },
};
expect(getRuleValue(rule)).toBe(2);
});
test("should return max value for maxSelections rule", () => {
const rule: TValidationRule = {
id: "rule7",
type: "maxSelections",
params: { max: 5 },
};
expect(getRuleValue(rule)).toBe(5);
});
test("should return undefined for email rule", () => {
const rule: TValidationRule = {
id: "rule9",
type: "email",
params: {},
};
expect(getRuleValue(rule)).toBeUndefined();
});
test("should return undefined for url rule", () => {
const rule: TValidationRule = {
id: "rule10",
type: "url",
params: {},
};
expect(getRuleValue(rule)).toBeUndefined();
});
test("should return undefined for phone rule", () => {
const rule: TValidationRule = {
id: "rule11",
type: "phone",
params: {},
};
expect(getRuleValue(rule)).toBeUndefined();
});
test("should return empty string for pattern rule with empty pattern", () => {
const rule: TValidationRule = {
id: "rule12",
type: "pattern",
params: { pattern: "" },
};
expect(getRuleValue(rule)).toBe("");
});
});
describe("createRuleParams", () => {
test("should create params for minLength rule with value", () => {
const params = createRuleParams("minLength", 10);
expect(params).toEqual({ min: 10 });
});
test("should create params for minLength rule without value (defaults to 0)", () => {
const params = createRuleParams("minLength");
expect(params).toEqual({ min: 0 });
});
test("should create params for maxLength rule with value", () => {
const params = createRuleParams("maxLength", 100);
expect(params).toEqual({ max: 100 });
});
test("should create params for maxLength rule without value (defaults to 100)", () => {
const params = createRuleParams("maxLength");
expect(params).toEqual({ max: 100 });
});
test("should create params for pattern rule with string value", () => {
const params = createRuleParams("pattern", "^[A-Z].*");
expect(params).toEqual({ pattern: "^[A-Z].*" });
});
test("should create params for pattern rule without value (defaults to empty string)", () => {
const params = createRuleParams("pattern");
expect(params).toEqual({ pattern: "" });
});
test("should create empty params for email rule", () => {
const params = createRuleParams("email");
expect(params).toEqual({});
});
test("should create empty params for url rule", () => {
const params = createRuleParams("url");
expect(params).toEqual({});
});
test("should create empty params for phone rule", () => {
const params = createRuleParams("phone");
expect(params).toEqual({});
});
test("should create params for minValue rule with value", () => {
const params = createRuleParams("minValue", 5);
expect(params).toEqual({ min: 5 });
});
test("should create params for minValue rule without value (defaults to 0)", () => {
const params = createRuleParams("minValue");
expect(params).toEqual({ min: 0 });
});
test("should create params for maxValue rule with value", () => {
const params = createRuleParams("maxValue", 50);
expect(params).toEqual({ max: 50 });
});
test("should create params for maxValue rule without value (defaults to 100)", () => {
const params = createRuleParams("maxValue");
expect(params).toEqual({ max: 100 });
});
test("should create params for minSelections rule with value", () => {
const params = createRuleParams("minSelections", 2);
expect(params).toEqual({ min: 2 });
});
test("should create params for minSelections rule without value (defaults to 1)", () => {
const params = createRuleParams("minSelections");
expect(params).toEqual({ min: 1 });
});
test("should create params for maxSelections rule with value", () => {
const params = createRuleParams("maxSelections", 5);
expect(params).toEqual({ max: 5 });
});
test("should create params for maxSelections rule without value (defaults to 3)", () => {
const params = createRuleParams("maxSelections");
expect(params).toEqual({ max: 3 });
});
test("should convert string number to number for minLength", () => {
const params = createRuleParams("minLength", "10");
expect(params).toEqual({ min: 10 });
});
test("should convert string number to number for maxLength", () => {
const params = createRuleParams("maxLength", "100");
expect(params).toEqual({ max: 100 });
});
test("should convert string number to number for minValue", () => {
const params = createRuleParams("minValue", "5");
expect(params).toEqual({ min: 5 });
});
test("should convert string number to number for maxValue", () => {
const params = createRuleParams("maxValue", "50");
expect(params).toEqual({ max: 50 });
});
test("should convert string number to number for minSelections", () => {
const params = createRuleParams("minSelections", "2");
expect(params).toEqual({ min: 2 });
});
test("should convert string number to number for maxSelections", () => {
const params = createRuleParams("maxSelections", "5");
expect(params).toEqual({ max: 5 });
});
test("should handle invalid string number (defaults to 0 for minLength)", () => {
const params = createRuleParams("minLength", "invalid");
expect(params).toEqual({ min: 0 });
});
test("should handle invalid string number (defaults to 100 for maxLength)", () => {
const params = createRuleParams("maxLength", "invalid");
expect(params).toEqual({ max: 100 });
});
test("should handle invalid string number (defaults to 0 for minValue)", () => {
const params = createRuleParams("minValue", "invalid");
expect(params).toEqual({ min: 0 });
});
test("should handle invalid string number (defaults to 100 for maxValue)", () => {
const params = createRuleParams("maxValue", "invalid");
expect(params).toEqual({ max: 100 });
});
test("should handle invalid string number (defaults to 1 for minSelections)", () => {
const params = createRuleParams("minSelections", "invalid");
expect(params).toEqual({ min: 1 });
});
test("should handle invalid string number (defaults to 3 for maxSelections)", () => {
const params = createRuleParams("maxSelections", "invalid");
expect(params).toEqual({ max: 3 });
});
});

View File

@@ -0,0 +1,288 @@
import { TSurveyElementTypeEnum, TSurveyOpenTextElementInputType } from "@formbricks/types/surveys/elements";
import {
APPLICABLE_RULES,
TAddressField,
TContactInfoField,
TValidationRule,
TValidationRuleType,
} from "@formbricks/types/surveys/validation-rules";
const stringRules: TValidationRuleType[] = [
"minLength",
"maxLength",
"pattern",
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
];
// Rules applicable per field for Address elements
// General text fields don't support format-specific validators (email, url, phone)
export const RULES_BY_ADDRESS_FIELD: Record<TAddressField, TValidationRuleType[]> = {
addressLine1: stringRules,
addressLine2: stringRules,
city: stringRules,
state: stringRules,
zip: stringRules,
country: stringRules,
};
// Rules applicable per field for Contact Info elements
// Note: "email" and "phone" validation are automatically enforced for their respective fields
// and should not appear as selectable options in the UI
export const RULES_BY_CONTACT_INFO_FIELD: Record<TContactInfoField, TValidationRuleType[]> = {
firstName: stringRules,
lastName: stringRules,
email: stringRules,
phone: ["equals", "doesNotEqual", "contains", "doesNotContain"],
company: stringRules,
};
// Rules applicable per input type for OpenText
export const RULES_BY_INPUT_TYPE: Record<TSurveyOpenTextElementInputType, TValidationRuleType[]> = {
text: [
"minLength",
"maxLength",
"pattern",
// "email", "url", "phone" excluded - redundant for text inputType
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
],
email: [
"minLength",
"maxLength",
"pattern",
// "email" rule excluded - redundant when inputType=email (HTML5 already validates)
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
],
url: [
"minLength",
"maxLength",
"pattern",
// "url" rule excluded - redundant when inputType=url (HTML5 already validates)
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
],
phone: [
"minLength",
"maxLength",
"pattern",
// "phone" rule excluded - redundant when inputType=phone (HTML5 already validates)
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
],
number: ["minValue", "maxValue", "equals", "doesNotEqual"],
};
/**
* Get available rule types for an element type, excluding already added rules
* For OpenText elements, filters rules based on inputType
* For Address/ContactInfo elements, filters rules based on field
*/
export const getAvailableRuleTypes = (
elementType: TSurveyElementTypeEnum,
existingRules: TValidationRule[],
inputType?: TSurveyOpenTextElementInputType,
field?: TAddressField | TContactInfoField
): TValidationRuleType[] => {
const elementTypeKey = elementType.toString();
// For OpenText, use input-type-based filtering
if (elementType === TSurveyElementTypeEnum.OpenText && inputType) {
const applicable = RULES_BY_INPUT_TYPE[inputType] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => !existingTypes.has(ruleType));
}
// For Address elements, use field-based filtering
if (elementType === TSurveyElementTypeEnum.Address) {
if (!field) {
// Address elements require a field to be specified for validation rules
return [];
}
const applicable = RULES_BY_ADDRESS_FIELD[field as TAddressField] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => !existingTypes.has(ruleType));
}
// For Contact Info elements, use field-based filtering
if (elementType === TSurveyElementTypeEnum.ContactInfo) {
if (!field) {
// Contact Info elements require a field to be specified for validation rules
return [];
}
const applicable = RULES_BY_CONTACT_INFO_FIELD[field as TContactInfoField] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => !existingTypes.has(ruleType));
}
if (elementType === TSurveyElementTypeEnum.PictureSelection) {
const applicable = APPLICABLE_RULES[elementTypeKey] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => !existingTypes.has(ruleType));
}
// For other element types, use standard filtering
const applicable = APPLICABLE_RULES[elementTypeKey] ?? [];
const existingTypes = new Set(existingRules.map((r) => r.type));
return applicable.filter((ruleType) => {
// Allow only one of each rule type
return !existingTypes.has(ruleType);
});
};
/**
* Get the value from rule params based on rule type
*/
export const getRuleValue = (rule: TValidationRule): number | string | undefined => {
const params = rule.params;
if ("min" in params) return params.min;
if ("max" in params) return params.max;
if ("pattern" in params) {
const pattern = params.pattern;
return pattern ?? "";
}
if ("value" in params) {
return params.value;
}
if ("date" in params) {
return params.date;
}
if ("startDate" in params && "endDate" in params) {
return `${params.startDate},${params.endDate}`;
}
if ("extensions" in params) {
// For file extension rules, return extensions array as comma-separated string for display
const extensions = params.extensions;
return extensions.length > 0 ? extensions.join(", ") : "";
}
return undefined;
};
/**
* Helper functions to create params for different rule types
*/
const createStringValueParams = (value?: number | string) => ({
value: value === undefined || value === null ? "" : String(value),
});
const createMinParams = (value?: number | string, defaultValue = 0) => ({
min: Number(value) || defaultValue,
});
const createMaxParams = (value?: number | string, defaultValue = 100) => ({
max: Number(value) || defaultValue,
});
const createDateParams = (value?: number | string) => ({
date: value === undefined || value === null ? "" : String(value),
});
const createDateRangeParams = (value?: number | string) => {
if (typeof value === "string" && value.includes(",")) {
const [startDate, endDate] = value.split(",");
return {
startDate: startDate?.trim() || "",
endDate: endDate?.trim() || "",
};
}
return { startDate: "", endDate: "" };
};
const createFileExtensionParams = (value?: number | string) => {
if (Array.isArray(value)) {
return { extensions: value };
}
if (typeof value === "string" && value.includes(",")) {
return { extensions: value.split(",").map((ext) => ext.trim()) };
}
const extensionValue = value === undefined || value === null ? "" : String(value);
return { extensions: extensionValue ? [extensionValue] : [] };
};
/**
* Create params object from rule type and value (without type field)
*/
export const createRuleParams = (
ruleType: TValidationRuleType,
value?: number | string
): TValidationRule["params"] => {
// Rules that return empty params
if (
ruleType === "email" ||
ruleType === "url" ||
ruleType === "phone" ||
ruleType === "rankAll" ||
ruleType === "answerAllRows"
) {
return {};
}
// Rules that use string value params
if (
ruleType === "equals" ||
ruleType === "doesNotEqual" ||
ruleType === "contains" ||
ruleType === "doesNotContain"
) {
return createStringValueParams(value);
}
// Rules that use min params
if (
ruleType === "minLength" ||
ruleType === "minValue" ||
ruleType === "isGreaterThan" ||
ruleType === "minSelections" ||
ruleType === "minRanked" ||
ruleType === "minRowsAnswered"
) {
const defaultValue =
ruleType === "minSelections" || ruleType === "minRanked" || ruleType === "minRowsAnswered" ? 1 : 0;
return createMinParams(value, defaultValue);
}
// Rules that use max params
if (
ruleType === "maxLength" ||
ruleType === "maxValue" ||
ruleType === "isLessThan" ||
ruleType === "maxSelections"
) {
const defaultValue = ruleType === "maxSelections" ? 3 : 100;
return createMaxParams(value, defaultValue);
}
// Rules that use date params
if (ruleType === "isLaterThan" || ruleType === "isEarlierThan") {
return createDateParams(value);
}
// Rules that use date range params
if (ruleType === "isBetween" || ruleType === "isNotBetween") {
return createDateRangeParams(value);
}
// Rules that use file extension params
if (ruleType === "fileExtensionIs" || ruleType === "fileExtensionIsNot") {
return createFileExtensionParams(value);
}
// Pattern rule
if (ruleType === "pattern") {
return { pattern: value === undefined || value === null ? "" : String(value) };
}
return {};
};

View File

@@ -274,6 +274,24 @@ describe("validation.isEndingCardValid", () => {
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(false);
});
test("should return true for endScreen card with http:// URL", () => {
const card: TSurveyEndScreenCard = {
...baseEndScreenCard,
buttonLabel: { default: "Go", en: "Go", de: "Los" },
buttonLink: "http://example.com",
};
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(true);
});
test("should return true for endScreen card with dynamic URL containing recall", () => {
const card: TSurveyEndScreenCard = {
...baseEndScreenCard,
buttonLabel: { default: "Go", en: "Go", de: "Los" },
buttonLink: "https://#recall:test123/fallback:example.com",
};
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(true);
});
// RedirectURL Card tests
test("should return true for a valid redirectUrl card", () => {
expect(validation.isEndingCardValid(baseRedirectUrlCard, surveyLanguagesEnabled)).toBe(true);
@@ -284,6 +302,16 @@ describe("validation.isEndingCardValid", () => {
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(false);
});
test("should return true for redirectUrl card with http:// URL", () => {
const card = { ...baseRedirectUrlCard, url: "http://example.com" };
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(true);
});
test("should return true for redirectUrl card with dynamic URL containing recall", () => {
const card = { ...baseRedirectUrlCard, url: "https://#recall:test123/fallback:example.com" };
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(true);
});
test("should return false for redirectUrl card if label is empty", () => {
const card = { ...baseRedirectUrlCard, label: " " };
expect(validation.isEndingCardValid(card, surveyLanguagesEnabled)).toBe(false);

View File

@@ -1,7 +1,7 @@
// extend this object in order to add more validation rules
import { TFunction } from "i18next";
import { toast } from "react-hot-toast";
import { z } from "zod";
import { ZEndingCardUrl } from "@formbricks/types/common";
import { TI18nString } from "@formbricks/types/i18n";
import { ZSegmentFilters } from "@formbricks/types/segment";
import {
@@ -206,9 +206,15 @@ export const isEndingCardValid = (
surveyLanguages: TSurveyLanguage[]
) => {
if (card.type === "endScreen") {
const parseResult = z.string().url().safeParse(card.buttonLink);
if (card.buttonLabel !== undefined && !parseResult.success) {
return false;
// Use ZEndingCardUrl for consistent validation - allows dynamic URLs via hidden fields/recall values
if (card.buttonLabel !== undefined) {
if (!card.buttonLink) {
return false;
}
const parseResult = ZEndingCardUrl.safeParse(card.buttonLink.trim());
if (!parseResult.success) {
return false;
}
}
return (
@@ -217,12 +223,15 @@ export const isEndingCardValid = (
isContentValid(card.buttonLabel, surveyLanguages)
);
} else {
const parseResult = z.string().url().safeParse(card.url);
if (parseResult.success) {
return card.label?.trim() !== "";
} else {
// Use ZEndingCardUrl for consistent validation - allows dynamic URLs via hidden fields/recall values
if (!card.url || card.url.trim() === "") {
return false;
}
const parseResult = ZEndingCardUrl.safeParse(card.url.trim());
if (!parseResult.success) {
return false;
}
return card.label?.trim() !== "";
}
};

View File

@@ -77,7 +77,14 @@ describe("getWebAppLocale", () => {
test("returns en-US when default requested but no default language", () => {
const surveyNoDefault = createMockSurvey([
{
language: { id: "l1", code: "de", alias: null, createdAt: new Date(), updatedAt: new Date(), projectId: "p1" },
language: {
id: "l1",
code: "de",
alias: null,
createdAt: new Date(),
updatedAt: new Date(),
projectId: "p1",
},
default: false,
enabled: true,
},

View File

@@ -13,7 +13,7 @@ import { COLOR_DEFAULTS } from "@/lib/styling/constants";
import { CardArrangementTabs } from "@/modules/ui/components/card-arrangement-tabs";
import { ColorPicker } from "@/modules/ui/components/color-picker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { Slider } from "@/modules/ui/components/slider";
import { ColorField, DimensionInput } from "@/modules/ui/components/styling-fields";
import { Switch } from "@/modules/ui/components/switch";
type CardStylingSettingsProps = {
@@ -39,9 +39,10 @@ export const CardStylingSettings = ({
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "straight";
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "straight";
const roundness = form.watch("roundness") ?? 8;
const hideProgressBar = form.watch("hideProgressBar");
const [parent] = useAutoAnimate();
return (
<Collapsible.Root
open={open}
@@ -59,7 +60,7 @@ export const CardStylingSettings = ({
)}>
<div className="inline-flex px-4 py-4">
{!isSettingsPage && (
<div className="flex items-center pl-2 pr-5">
<div className="flex items-center pr-5 pl-2">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
@@ -81,47 +82,18 @@ export const CardStylingSettings = ({
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
<hr className="py-1 text-slate-600" />
<div className="flex flex-col gap-6 p-6 pt-2">
<div className="flex flex-col justify-center">
<FormField
control={form.control}
name="roundness"
render={() => (
<FormItem>
<div>
<FormLabel>{t("environments.surveys.edit.roundness")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_radius_of_the_card_and_the_inputs")}
</FormDescription>
</div>
<FormControl>
<div className="rounded-lg border bg-slate-50 p-6">
<Slider
value={[roundness]}
max={22}
onValueChange={(value) => {
form.setValue("roundness", value[0]);
}}
/>
</div>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4 p-6 pt-2">
{/* Roundness */}
<DimensionInput form={form} name="roundness" label={t("environments.surveys.edit.roundness")} />
<FormField
control={form.control}
name="cardBackgroundColor.light"
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.card_background_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_background_color_of_the_card")}
</FormDescription>
</div>
<FormItem className="space-y-1">
<FormLabel className="text-xs">
{t("environments.surveys.edit.card_background_color")}
</FormLabel>
<FormControl>
<ColorPicker
@@ -138,13 +110,8 @@ export const CardStylingSettings = ({
control={form.control}
name="cardBorderColor.light"
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.card_border_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_color_of_the_card")}
</FormDescription>
</div>
<FormItem className="space-y-1">
<FormLabel className="text-xs">{t("environments.surveys.edit.card_border_color")}</FormLabel>
<FormControl>
<ColorPicker
@@ -157,66 +124,6 @@ export const CardStylingSettings = ({
)}
/>
<FormField
control={form.control}
name={"cardArrangement"}
render={() => (
<FormItem>
<div>
<FormLabel>
{t("environments.surveys.edit.card_arrangement_for_survey_type_derived", {
surveyTypeDerived: surveyTypeDerived,
})}
</FormLabel>
<FormDescription>
{t(
"environments.surveys.edit.how_funky_do_you_want_your_cards_in_survey_type_derived_surveys",
{
surveyTypeDerived: surveyTypeDerived,
}
)}
</FormDescription>
</div>
<FormControl>
<CardArrangementTabs
key={isAppSurvey ? "app" : "link"}
surveyType={isAppSurvey ? "app" : "link"}
activeCardArrangement={isAppSurvey ? appCardArrangement : linkCardArrangement}
setActiveCardArrangement={(value, type) => {
type === "app"
? form.setValue("cardArrangement.appSurveys", value)
: form.setValue("cardArrangement.linkSurveys", value);
}}
/>
</FormControl>
</FormItem>
)}
/>
<div className="flex items-center space-x-1">
<FormField
control={form.control}
name="hideProgressBar"
render={({ field }) => (
<FormItem className="flex w-full items-center gap-2 space-y-0">
<FormControl>
<Switch
id="hideProgressBar"
checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)}
/>
</FormControl>
<div>
<FormLabel>{t("environments.surveys.edit.hide_progress_bar")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.disable_the_visibility_of_survey_progress")}
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
{(!surveyType || isAppSurvey) && (
<div className="flex max-w-xs flex-col gap-4">
<div className="flex items-center space-x-1">
@@ -245,9 +152,6 @@ export const CardStylingSettings = ({
<div>
<FormLabel>{t("environments.surveys.edit.add_highlight_border")}</FormLabel>
<FormDescription className="text-xs font-normal text-slate-500">
{t("environments.surveys.edit.add_highlight_border_description")}
</FormDescription>
</div>
</div>
@@ -271,6 +175,86 @@ export const CardStylingSettings = ({
</div>
</div>
)}
<FormField
control={form.control}
name={"cardArrangement"}
render={() => (
<FormItem className="col-span-2">
<div>
<FormLabel>
{t("environments.surveys.edit.card_arrangement_for_survey_type_derived", {
surveyTypeDerived: surveyTypeDerived,
})}
</FormLabel>
</div>
<FormControl>
<CardArrangementTabs
key={isAppSurvey ? "app" : "link"}
surveyType={isAppSurvey ? "app" : "link"}
activeCardArrangement={isAppSurvey ? appCardArrangement : linkCardArrangement}
setActiveCardArrangement={(value, type) => {
type === "app"
? form.setValue("cardArrangement.appSurveys", value)
: form.setValue("cardArrangement.linkSurveys", value);
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>
{/* Progress Bar Section (Moved from Advanced) */}
<div className="flex flex-col gap-6 p-6 pt-0">
<hr className="text-slate-600" />
<div className="flex flex-col gap-4">
<div className="my-2">
<FormField
control={form.control}
name="hideProgressBar"
render={({ field }) => (
<FormItem className="flex w-full items-center gap-2 space-y-0">
<FormControl>
<Switch
id="hideProgressBar"
checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)}
/>
</FormControl>
<div>
<FormLabel className="text-sm font-normal">
{t("environments.surveys.edit.hide_progress_bar")}
</FormLabel>
<FormDescription className="text-xs">
{t("environments.surveys.edit.disable_the_visibility_of_survey_progress")}
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
{!hideProgressBar && (
<div className="grid grid-cols-2 gap-4">
<ColorField
form={form}
name="progressTrackBgColor.light"
label={t("environments.workspace.look.advanced_styling_field_track_bg")}
/>
<ColorField
form={form}
name="progressIndicatorBgColor.light"
label={t("environments.workspace.look.advanced_styling_field_indicator_bg")}
/>
<DimensionInput
form={form}
name="progressTrackHeight"
label={t("environments.workspace.look.advanced_styling_field_track_height")}
/>
</div>
)}
</div>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>

View File

@@ -12,12 +12,12 @@ interface ColorPickerProps {
}
export const ColorPicker = ({ color, onChange, containerClass, disabled = false }: ColorPickerProps) => {
return (
<div className={cn("my-2", containerClass)}>
<div className="flex w-full items-center justify-between space-x-1 rounded-md border border-slate-300 bg-white px-2 text-sm text-slate-400">
<div className={cn(containerClass)}>
<div className="flex h-10 w-full items-center justify-between space-x-1 rounded-md border border-slate-300 bg-white px-2 text-sm text-slate-400">
<div className="flex w-full items-center">
#
<HexColorInput
className="ml-2 mr-2 h-10 w-32 flex-1 border-0 bg-transparent text-slate-500 outline-none focus:border-none"
className="ml-2 mr-2 w-32 flex-1 border-0 bg-transparent text-slate-500 outline-none focus:border-none"
color={color}
onChange={onChange}
id="color"

View File

@@ -115,7 +115,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
onLoadingComplete={() => setBackgroundLoaded(true)}
/>
{authorDetailsForUnsplash.authorName && (
<div className="absolute bottom-4 right-6 z-10 ml-auto hidden w-max text-xs text-slate-400 md:block">
<div className="absolute right-6 bottom-4 z-10 ml-auto hidden w-max text-xs text-slate-400 md:block">
<span>{t("common.photo_by")}</span>
<Link
href={authorDetailsForUnsplash.authorURL + "?utm_source=formbricks&utm_medium=referral"}
@@ -169,17 +169,17 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
data-testid="mobile-preview-container"
className={`relative h-[90%] w-full overflow-hidden rounded-[3rem] border-[6px] border-slate-400 lg:w-[75%] ${getFilterStyle()}`}>
{/* below element is use to create notch for the mobile device mockup */}
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
<div className="absolute top-2 right-1/2 left-1/2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
{surveyType === "link" && renderBackground()}
{renderContent()}
</div>
);
} else if (isEditorView) {
return (
<div ref={ContentRef} className="overflow-hiddem flex flex-grow flex-col rounded-b-lg">
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
<div ref={ContentRef} className="flex flex-col rounded-b-lg">
<div className="relative flex w-full flex-col items-center justify-center p-4 py-6">
{renderBackground()}
<div className="flex h-full w-full items-center justify-center">{children}</div>
<div className="flex w-full items-center justify-center">{children}</div>
</div>
</div>
);

View File

@@ -3,6 +3,7 @@
import { Command as CommandPrimitive } from "cmdk";
import { X } from "lucide-react";
import * as React from "react";
import { createPortal } from "react-dom";
import { Command, CommandGroup, CommandItem, CommandList } from "@/modules/ui/components/command";
import { Badge } from "@/modules/ui/components/multi-select/badge";
@@ -35,25 +36,62 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
const [position, setPosition] = React.useState<{ top: number; left: number; width: number } | null>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
// Track if changes are user-initiated (not from value prop)
const isUserInitiatedRef = React.useRef(false);
React.useEffect(() => {
if (value) {
setSelected(
value.map((val) => options.find((o) => o.value === val)).filter((o): o is TOption<T> => !!o)
);
const newSelected = value
.map((val) => options.find((o) => o.value === val))
.filter((o): o is TOption<T> => !!o);
// Only update if different (avoid unnecessary updates)
const currentValues = selected.map((s) => s.value);
const newValues = newSelected.map((s) => s.value);
if (
currentValues.length !== newValues.length ||
currentValues.some((val, idx) => val !== newValues[idx])
) {
isUserInitiatedRef.current = false; // Mark as prop-initiated
setSelected(newSelected);
}
}
}, [value, options]);
// Sync user-initiated selected changes to parent via onChange (deferred to avoid render issues)
const prevSelectedRef = React.useRef(selected);
React.useEffect(() => {
// Only call onChange if change was user-initiated and selected actually changed
if (isUserInitiatedRef.current && prevSelectedRef.current !== selected) {
const selectedValues = selected.map((s) => s.value) as K;
const prevValues = prevSelectedRef.current.map((s) => s.value) as K;
// Check if values actually changed
if (
selectedValues.length !== prevValues.length ||
selectedValues.some((val, idx) => val !== prevValues[idx])
) {
// Use queueMicrotask to defer the onChange call after render
queueMicrotask(() => {
onChange?.(selectedValues);
});
}
prevSelectedRef.current = selected;
isUserInitiatedRef.current = false; // Reset flag
} else if (!isUserInitiatedRef.current) {
// Update ref even if not user-initiated to track state
prevSelectedRef.current = selected;
}
}, [selected, onChange]);
const handleUnselect = React.useCallback(
(option: TOption<T>) => {
if (disabled) return;
setSelected((prev) => {
const newSelected = prev.filter((s) => s.value !== option.value);
onChange?.(newSelected.map((s) => s.value) as K);
return newSelected;
});
isUserInitiatedRef.current = true; // Mark as user-initiated
setSelected((prev) => prev.filter((s) => s.value !== option.value));
},
[onChange, disabled]
[disabled]
);
const handleKeyDown = React.useCallback(
@@ -62,10 +100,10 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
if (!input || disabled) return;
if ((e.key === "Delete" || e.key === "Backspace") && input.value === "") {
isUserInitiatedRef.current = true; // Mark as user-initiated
setSelected((prev) => {
const newSelected = [...prev];
newSelected.pop();
onChange?.(newSelected.map((s) => s.value) as K);
return newSelected;
});
}
@@ -86,11 +124,26 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
});
}, [options, selected, inputValue]);
// Calculate position for dropdown when opening
React.useEffect(() => {
if (open && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY + 6,
left: rect.left + window.scrollX,
width: rect.width,
});
} else {
setPosition(null);
}
}, [open]);
return (
<Command
onKeyDown={handleKeyDown}
className={`overflow-visible bg-white ${disabled ? "cursor-not-allowed opacity-50" : ""}`}>
className={`relative overflow-visible bg-white ${disabled ? "cursor-not-allowed opacity-50" : ""}`}>
<div
ref={containerRef}
className={`border-input ring-offset-background group rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-offset-2 ${
disabled ? "pointer-events-none" : "focus-within:ring-ring"
}`}>
@@ -126,34 +179,45 @@ export function MultiSelect<T extends string, K extends TOption<T>["value"][]>(
/>
</div>
</div>
{open && selectableOptions.length > 0 && !disabled && (
<div className="relative mt-2">
<CommandList className="border-0">
<div className="text-popover-foreground animate-in absolute top-0 z-10 max-h-32 w-full overflow-auto rounded-md bg-white shadow-md outline-none">
<CommandGroup className="h-full overflow-auto">
{selectableOptions.map((option) => (
<CommandItem
key={option.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
if (disabled) return;
const newSelected = [...selected, option];
setSelected(newSelected);
onChange?.(newSelected.map((o) => o.value) as K);
setInputValue("");
}}
className="cursor-pointer">
{option.label}
</CommandItem>
))}
</CommandGroup>
</div>
</CommandList>
</div>
)}
{open &&
selectableOptions.length > 0 &&
!disabled &&
position &&
globalThis.window !== undefined &&
createPortal(
<div
className="absolute z-[100]"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
width: `${position.width}px`,
}}>
<CommandList className="border-0">
<div className="text-popover-foreground animate-in max-h-32 w-full overflow-auto rounded-md border border-slate-300 bg-white shadow-md outline-none">
<CommandGroup className="h-full overflow-auto">
{selectableOptions.map((option) => (
<CommandItem
key={option.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
if (disabled) return;
isUserInitiatedRef.current = true; // Mark as user-initiated
setSelected((prev) => [...prev, option]);
setInputValue("");
}}
className="cursor-pointer">
{option.label}
</CommandItem>
))}
</CommandGroup>
</div>
</CommandList>
</div>,
document.body
)}
</Command>
);
}

View File

@@ -12,7 +12,7 @@ interface ModalProps {
previewMode: string;
clickOutsideClose: boolean;
darkOverlay: boolean;
borderRadius?: number;
borderRadius?: number | string;
background?: string;
}
@@ -142,8 +142,8 @@ export const Modal = ({
ref={modalRef}
style={{
...scalingClasses,
...(borderRadius && {
borderRadius: `${borderRadius}px`,
...(borderRadius !== undefined && {
borderRadius: typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
}),
...(background && {
background,

View File

@@ -0,0 +1,191 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { ChevronDown, ChevronRight } from "lucide-react";
import React from "react";
import { ColorPicker } from "@/modules/ui/components/color-picker";
import { FormControl, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
export const StylingSection = ({
title,
open,
setOpen,
children,
}: {
title: string;
open: boolean;
setOpen: (o: boolean) => void;
children: React.ReactNode;
}) => {
const [parent] = useAutoAnimate();
return (
<div ref={parent} className="rounded-md border">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between rounded-t-md bg-slate-50 p-3 text-sm font-medium text-slate-700 hover:bg-slate-100">
{title}
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
{open && <div className="rounded-b-md border-t bg-white p-4">{children}</div>}
</div>
);
};
export const ColorField = ({
form,
name,
label,
containerClass,
}: {
form: any;
name: string;
label: string;
containerClass?: string;
}) => (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel className="text-xs">{label}</FormLabel>
<FormControl>
<ColorPicker
color={field.value}
onChange={(color) => field.onChange(color)}
containerClass={containerClass || "w-full"}
/>
</FormControl>
</FormItem>
)}
/>
);
export const NumberField = ({
form,
name,
label,
step = 1,
max,
placeholder,
}: {
form: any;
name: string;
label: string;
step?: number;
max?: number;
placeholder?: string;
}) => (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel className="text-xs">{label}</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) => {
const val = e.target.valueAsNumber;
field.onChange(Number.isNaN(val) ? null : val);
}}
step={step}
max={max}
className="text-xs"
placeholder={placeholder}
/>
</FormControl>
</FormItem>
)}
/>
);
export const DimensionInput = ({
form,
name,
label,
placeholder,
}: {
form: any;
name: string;
label: string;
placeholder?: string;
}) => (
<FormField
control={form.control}
name={name}
render={({ field }) => {
const value = field.value;
let unit = "px";
if (typeof value === "string") {
if (value.endsWith("%")) unit = "%";
else if (value.endsWith("rem")) unit = "rem";
else if (value.endsWith("em")) unit = "em";
}
const numericValue = typeof value === "string" ? Number.parseFloat(value) : value;
return (
<FormItem className="space-y-1">
<FormLabel className="text-xs">{label}</FormLabel>
<FormControl>
<div className="flex rounded-md shadow-xs">
<Input
type="number"
value={numericValue ?? ""}
onChange={(e) => {
const valStr = e.target.value;
if (valStr === "") {
field.onChange(null);
return;
}
const newVal = Number.parseFloat(valStr);
if (Number.isNaN(newVal)) {
return;
}
field.onChange(unit === "px" ? newVal : `${newVal}${unit}`);
}}
className="flex-1 rounded-r-none border-r-0 text-xs focus-visible:ring-0"
placeholder={placeholder}
/>
<select
value={unit}
onChange={(e) => {
const newUnit = e.target.value;
const currentVal = numericValue ?? 0;
if (newUnit === "px") {
field.onChange(currentVal);
} else {
field.onChange(`${currentVal}${newUnit}`);
}
}}
className="ring-offset-background placeholder:text-muted-foreground focus:border-brand-dark h-10 items-center justify-between rounded-r-md border border-slate-300 bg-white pr-8 pl-3 text-xs font-medium focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50">
<option value="px">px</option>
<option value="%">%</option>
<option value="rem">rem</option>
<option value="em">em</option>
</select>
</div>
</FormControl>
</FormItem>
);
}}
/>
);
export const TextField = ({ form, name, label }: { form: any; name: string; label: string }) => (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel className="text-xs">{label}</FormLabel>
<FormControl>
<Input type="text" {...field} className="text-xs" />
</FormControl>
</FormItem>
)}
/>
);

View File

@@ -91,7 +91,7 @@ export const ThemeStylingPreviewSurvey = ({
shrink: {
display: "relative",
width: ["83.33%"],
height: ["95%"],
height: "auto",
},
};
@@ -140,9 +140,9 @@ export const ThemeStylingPreviewSurvey = ({
: "expanded_with_fixed_positioning"
: "shrink"
}
className="relative flex h-[95%] max-h-[95%] w-5/6 items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
<div className="flex h-full w-5/6 flex-1 flex-col">
<div className="flex h-8 w-full items-center rounded-t-lg bg-slate-100">
className="relative z-10 flex h-auto max-h-[85%] w-5/6 flex-col overflow-y-auto rounded-lg border border-slate-300 bg-white shadow-xl">
<div className="flex w-full flex-col rounded-lg">
<div className="flex h-auto w-full items-center rounded-t-lg bg-slate-100 py-2">
<div className="ml-6 flex space-x-2">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
@@ -193,7 +193,7 @@ export const ThemeStylingPreviewSurvey = ({
)}
<div
key={surveyKey}
className={`${project.logo?.url && !project.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
className={`${!project.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md overflow-hidden rounded-lg p-4`}>
<SurveyInline
appUrl={publicDomain}
isPreviewMode={true}

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -66,10 +66,7 @@ test.describe("API Tests for Responses", () => {
finished: true,
language: "en",
data: {
question1: "answer1",
question2: 2,
question3: ["answer3", "answer4"],
question4: { subquestion1: "answer5" },
jpvm9b73u06xdrhzi11k2h76: "answer1",
},
variables: {
variable1: "answer1",
@@ -114,10 +111,7 @@ test.describe("API Tests for Responses", () => {
finished: true,
language: "en",
data: {
question1: "answer2",
question2: 3,
question3: ["answer5", "answer6"],
question4: { subquestion1: "answer7" },
jpvm9b73u06xdrhzi11k2h76: "answer2",
},
variables: {
variable1: "answer2",
@@ -185,10 +179,7 @@ test.describe("API Tests for Responses", () => {
finished: true,
language: "en",
data: {
question1: "answer1",
question2: 2,
question3: ["answer3", "answer4"],
question4: { subquestion1: "answer5" },
jpvm9b73u06xdrhzi11k2h76: "answer1",
},
variables: {
variable1: "answer1",
@@ -218,10 +209,7 @@ test.describe("API Tests for Responses", () => {
finished: true,
language: "en",
data: {
question1: "answer2",
question2: 3,
question3: ["answer5", "answer6"],
question4: { subquestion1: "answer7" },
jpvm9b73u06xdrhzi11k2h76: "answer2",
},
variables: {
variable1: "answer2",
@@ -341,10 +329,7 @@ test.describe("API Tests for Responses", () => {
singleUseId: null,
displayId: null,
data: {
question1: "updatedAnswer1",
question2: 5,
question3: ["updatedAnswer3", "updatedAnswer4"],
question4: { subquestion1: "updatedAnswer5" },
jpvm9b73u06xdrhzi11k2h76: "updatedAnswer1",
},
variables: {
variable1: "updatedAnswer1",
@@ -400,10 +385,7 @@ test.describe("API Tests for Responses", () => {
singleUseId: null,
displayId: null,
data: {
question1: "updatedAnswer1",
question2: 5,
question3: ["updatedAnswer3", "updatedAnswer4"],
question4: { subquestion1: "updatedAnswer5" },
jpvm9b73u06xdrhzi11k2h76: "updatedAnswer1",
},
variables: {
variable1: "updatedAnswer1",

View File

@@ -85,6 +85,7 @@
"pnpm": {
"overrides": {
"axios": ">=1.12.2",
"uuid": "11.1.0",
"node-forge": ">=1.3.2",
"tar-fs": "2.1.4",
"typeorm": ">=0.3.26",
@@ -97,4 +98,4 @@
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
}
}
}
}

View File

@@ -0,0 +1,139 @@
import { Prisma } from "@prisma/client";
import { logger } from "@formbricks/logger";
import type { MigrationScript } from "../../src/scripts/migration-runner";
import type { MigrationStats, SurveyRecord } from "./types";
import { migrateSurveyBlocks } from "./utils";
export const migrateLegacyValidationToRules: MigrationScript = {
type: "data",
id: "clx8k9m2n0001l508xyz12345",
name: "20260119123112_migrate_legacy_validation_to_rules",
run: async ({ tx }) => {
// Initialize migration statistics
const stats: MigrationStats = {
totalSurveys: 0,
surveysProcessed: 0,
surveysSkipped: 0,
openTextElementsMigrated: 0,
fileUploadElementsMigrated: 0,
errors: 0,
};
// Query to find surveys with elements that have legacy validation fields
// This includes ALL elements with charLimit or allowedFileExtensions keys,
// regardless of enabled status or array length, to ensure complete cleanup
const surveysFindQuery = `
SELECT s.id, s.blocks
FROM "Survey" AS s
WHERE EXISTS (
SELECT 1
FROM unnest(s.blocks) AS block
CROSS JOIN jsonb_array_elements(block->'elements') AS element
WHERE (
-- Open Text elements with any charLimit field (enabled, disabled, or any value)
(element->>'type' = 'openText'
AND element ? 'charLimit')
OR
-- File Upload elements with any allowedFileExtensions field (even empty array)
(element->>'type' = 'fileUpload'
AND element ? 'allowedFileExtensions')
)
)
`;
const surveysNeedingMigration: SurveyRecord[] = await tx.$queryRaw`${Prisma.raw(surveysFindQuery)}`;
stats.totalSurveys = surveysNeedingMigration.length;
if (surveysNeedingMigration.length === 0) {
logger.info("No surveys found that need migration");
return;
}
logger.info(`Found ${surveysNeedingMigration.length.toString()} surveys to migrate`);
// Process surveys in batches
const SURVEY_BATCH_SIZE = 1000;
const updates: { id: string; blocks: SurveyRecord["blocks"] }[] = [];
for (const survey of surveysNeedingMigration) {
// Deep clone blocks to avoid mutating original
const blocksCopy = JSON.parse(JSON.stringify(survey.blocks)) as SurveyRecord["blocks"];
// Migrate blocks - if this fails, the entire transaction will roll back
try {
migrateSurveyBlocks(blocksCopy);
} catch (error) {
logger.error(error, `Failed to migrate survey ${survey.id}`);
throw new Error(
`Migration failed for survey ${survey.id}: ${error instanceof Error ? error.message : String(error)}`
);
}
updates.push({
id: survey.id,
blocks: blocksCopy,
});
stats.surveysProcessed++;
}
logger.info(
`Processed ${updates.length.toString()} surveys: ${stats.surveysProcessed.toString()} modified`
);
// Update surveys in batches using UNNEST for performance
if (updates.length === 0) {
logger.info("No surveys needed updating (all already migrated)");
return;
}
{
let updatedCount = 0;
for (let i = 0; i < updates.length; i += SURVEY_BATCH_SIZE) {
const batch = updates.slice(i, i + SURVEY_BATCH_SIZE);
try {
// Build arrays for batch update
const ids = batch.map((u) => u.id);
const blocksJsonStrings = batch.map((u) => JSON.stringify(u.blocks));
// Use UNNEST to update multiple surveys in a single query
await tx.$executeRawUnsafe(
`UPDATE "Survey" AS s
SET
blocks = (
SELECT array_agg(elem)
FROM jsonb_array_elements(data.blocks_json::jsonb) AS elem
)
FROM (
SELECT
unnest($1::text[]) AS id,
unnest($2::text[]) AS blocks_json
) AS data
WHERE s.id = data.id`,
ids,
blocksJsonStrings
);
updatedCount += batch.length;
// Log progress
logger.info(`Progress: ${updatedCount.toString()}/${updates.length.toString()} surveys updated`);
} catch (error) {
logger.error(error, `Failed to update survey batch starting at index ${i.toString()}`);
throw new Error(
`Database batch update failed at index ${i.toString()}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
logger.info(`Migration complete: ${updatedCount.toString()} surveys migrated`);
}
// Log final statistics
logger.info(
`Migration complete: ${stats.totalSurveys.toString()} total surveys, ${stats.surveysProcessed.toString()} processed`
);
},
};

View File

@@ -0,0 +1,46 @@
export interface SurveyElement {
id: string;
type: string;
charLimit?: {
enabled?: boolean;
min?: number;
max?: number;
};
allowedFileExtensions?: string[];
validation?: {
rules: ValidationRule[];
logic?: "and" | "or";
};
[key: string]: unknown;
}
export interface ValidationRule {
id: string;
type: string;
params: {
min?: number;
max?: number;
extensions?: string[];
[key: string]: unknown;
};
field?: string;
}
export interface Block {
id: string;
elements: SurveyElement[];
}
export interface SurveyRecord {
id: string;
blocks: Block[];
}
export interface MigrationStats {
totalSurveys: number;
surveysProcessed: number;
surveysSkipped: number;
openTextElementsMigrated: number;
fileUploadElementsMigrated: number;
errors: number;
}

View File

@@ -0,0 +1,175 @@
import { v7 as uuidv7 } from "uuid";
import type { SurveyElement, ValidationRule } from "./types";
/**
* Check if a validation rule with matching type and params already exists
*/
export function hasMatchingRule(
rules: ValidationRule[],
type: string,
params: Record<string, unknown>
): boolean {
return rules.some((rule) => rule.type === type && JSON.stringify(rule.params) === JSON.stringify(params));
}
/**
* Initialize validation object if it doesn't exist and return it
*/
export function ensureValidationObject(element: SurveyElement): {
rules: ValidationRule[];
logic: "and" | "or";
} {
element.validation ??= {
rules: [],
logic: "and",
};
if (!element.validation.rules) {
element.validation.rules = [];
}
if (!element.validation.logic) {
element.validation.logic = "and";
}
// After ensuring logic is set, we know it's defined
return element.validation as { rules: ValidationRule[]; logic: "and" | "or" };
}
/**
* Migrate Open Text element charLimit to validation rules
* Always removes legacy charLimit field after processing
* Throws error if transformation fails
*/
export function migrateOpenTextCharLimit(element: SurveyElement): void {
// Skip if charLimit is missing or not enabled (already migrated)
if (
element.charLimit?.enabled !== true ||
(element.charLimit?.min === undefined && element.charLimit?.max === undefined)
) {
// Still remove legacy field if it exists but is disabled
if (element.charLimit) {
delete element.charLimit;
}
return;
}
// Ensure validation object exists
const validation = ensureValidationObject(element);
const existingRules = validation.rules;
// Migrate minLength if min is defined and valid
if (
element.charLimit.min !== undefined &&
element.charLimit.min >= 0 &&
!hasMatchingRule(existingRules, "minLength", { min: element.charLimit.min })
) {
const newRule: ValidationRule = {
id: uuidv7(),
type: "minLength",
params: { min: element.charLimit.min },
};
existingRules.push(newRule);
}
// Migrate maxLength if max is defined and valid
if (
element.charLimit.max !== undefined &&
element.charLimit.max >= 0 &&
!hasMatchingRule(existingRules, "maxLength", {
max: element.charLimit.max,
})
) {
const newRule: ValidationRule = {
id: uuidv7(),
type: "maxLength",
params: { max: element.charLimit.max },
};
existingRules.push(newRule);
}
// Always remove legacy charLimit field after processing
delete element.charLimit;
}
/**
* Check if two arrays contain the same extensions (order-independent)
*/
function extensionsMatch(extensions1: string[], extensions2: string[]): boolean {
if (extensions1.length !== extensions2.length) {
return false;
}
const compareFn = (a: string, b: string) => a.localeCompare(b);
const sorted1 = [...extensions1].sort(compareFn);
const sorted2 = [...extensions2].sort(compareFn);
return JSON.stringify(sorted1) === JSON.stringify(sorted2);
}
/**
* Migrate File Upload element allowedFileExtensions to validation rules
* Always removes legacy allowedFileExtensions field after processing
* Throws error if transformation fails
*/
export function migrateFileUploadExtensions(element: SurveyElement): void {
// Skip if allowedFileExtensions is missing or empty (already migrated)
if (
!element.allowedFileExtensions ||
!Array.isArray(element.allowedFileExtensions) ||
element.allowedFileExtensions.length === 0
) {
// Still remove legacy field if it exists but is empty
if (element.allowedFileExtensions) {
delete element.allowedFileExtensions;
}
return;
}
// Ensure validation object exists
const validation = ensureValidationObject(element);
const existingRules = validation.rules;
const extensions = element.allowedFileExtensions;
// Check if a matching fileExtensionIs rule already exists
const hasMatchingExtensionRule = existingRules.some(
(rule) =>
rule.type === "fileExtensionIs" &&
rule.params.extensions &&
Array.isArray(rule.params.extensions) &&
extensionsMatch(rule.params.extensions, extensions)
);
if (!hasMatchingExtensionRule) {
// Create new fileExtensionIs rule
const newRule: ValidationRule = {
id: uuidv7(),
type: "fileExtensionIs",
params: { extensions: [...extensions] },
};
existingRules.push(newRule);
}
// Always remove legacy allowedFileExtensions field after processing
delete element.allowedFileExtensions;
}
/**
* Migrate a single survey's blocks
* Throws error if any element fails to transform
*/
export function migrateSurveyBlocks(blocks: { id: string; elements: SurveyElement[] }[]): void {
for (const block of blocks) {
for (const element of block.elements) {
// Skip if element type is not OpenText or FileUpload
if (element.type !== "openText" && element.type !== "fileUpload") {
continue;
}
// Migrate Open Text elements - throws if transformation fails
if (element.type === "openText") {
migrateOpenTextCharLimit(element);
}
// Migrate File Upload elements - throws if transformation fails
if (element.type === "fileUpload") {
migrateFileUploadExtensions(element);
}
}
}
}

View File

@@ -51,6 +51,7 @@
"@paralleldrive/cuid2": "2.2.2",
"@prisma/client": "6.14.0",
"bcryptjs": "2.4.3",
"uuid": "11.1.0",
"zod": "3.24.4",
"zod-openapi": "4.2.4"
},

View File

@@ -62,6 +62,7 @@ export default defineConfig(async (): Promise<UserConfig> => {
"zod",
"zod-openapi",
"@paralleldrive/cuid2",
"uuid",
],
},
emptyOutDir: true,

View File

@@ -13,14 +13,14 @@
"clean": "rimraf .turbo node_modules dist"
},
"dependencies": {
"@react-email/components": "1.0.1",
"react-email": "5.0.8"
"@react-email/components": "1.0.6",
"react-email": "5.2.5"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@formbricks/types": "workspace:*",
"@react-email/preview-server": "5.0.8",
"@react-email/preview-server": "5.2.5",
"autoprefixer": "10.4.21",
"clsx": "2.1.1",
"postcss": "8.5.3",

View File

@@ -112,41 +112,43 @@ function FormField({
/>
{/* Form Fields */}
<div className="relative space-y-3">
<div className="relative">
<ElementError errorMessage={errorMessage} dir={dir} />
{visibleFields.map((field) => {
const fieldRequired = isFieldRequired(field);
const fieldValue = currentValues[field.id] ?? "";
const fieldInputId = `${elementId}-${field.id}`;
<div className="space-y-3">
{visibleFields.map((field) => {
const fieldRequired = isFieldRequired(field);
const fieldValue = currentValues[field.id] ?? "";
const fieldInputId = `${elementId}-${field.id}`;
// Determine input type
let inputType: "text" | "email" | "tel" | "number" | "url" = field.type ?? "text";
if (field.id === "email" && !field.type) {
inputType = "email";
} else if (field.id === "phone" && !field.type) {
inputType = "tel";
}
// Determine input type
let inputType: "text" | "email" | "tel" | "number" | "url" = field.type ?? "text";
if (field.id === "email" && !field.type) {
inputType = "email";
} else if (field.id === "phone" && !field.type) {
inputType = "tel";
}
return (
<div key={field.id} className="space-y-2">
<Label htmlFor={fieldInputId} variant="default">
{fieldRequired ? `${field.label}*` : field.label}
</Label>
<Input
id={fieldInputId}
type={inputType}
value={fieldValue}
onChange={(e) => {
handleFieldChange(field.id, e.target.value);
}}
required={fieldRequired}
disabled={disabled}
dir={dir}
aria-invalid={Boolean(errorMessage) || undefined}
/>
</div>
);
})}
return (
<div key={field.id} className="space-y-2">
<Label htmlFor={fieldInputId} variant="default">
{fieldRequired ? `${field.label}*` : field.label}
</Label>
<Input
id={fieldInputId}
type={inputType}
value={fieldValue}
onChange={(e) => {
handleFieldChange(field.id, e.target.value);
}}
required={fieldRequired}
disabled={disabled}
dir={dir}
aria-invalid={Boolean(errorMessage) || undefined}
/>
</div>
);
})}
</div>
</div>
</div>
);

View File

@@ -24,7 +24,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(
// Layout and behavior
"flex min-w-0 border transition-[color,box-shadow] outline-none",
// Customizable styles via CSS variables (using Tailwind theme extensions)
"w-input h-input",
"w-input min-h-[var(--fb-input-height)]",
"bg-input-bg border-input-border rounded-input",
"font-input font-input-weight",
"text-input-text",

View File

@@ -51,7 +51,7 @@
--slate-50: rgb(248, 250, 252);
--slate-100: rgb(241 245 249);
--slate-200: rgb(226 232 240);
/* ---------------------------------------------------------------------------
Survey general Colors
The primary accent color used throughout the survey. Override this to
@@ -65,12 +65,12 @@
/* ---------------------------------------------------------------------------
Element Headline Tokens
Used for question headlines and main element titles.
Used for headline text in elements.
--------------------------------------------------------------------------- */
--fb-element-headline-font-family: inherit;
--fb-element-headline-font-weight: 400;
--fb-element-headline-font-size: 1rem;
--fb-element-headline-color: (--input);
--fb-element-headline-color: var(--input);
--fb-element-headline-opacity: 1;
/* ---------------------------------------------------------------------------
@@ -113,10 +113,10 @@
Used for text inputs, textareas, and other form controls.
--------------------------------------------------------------------------- */
--fb-input-bg-color: var(--slate-50);
--fb-input-border-color: var(--fb-survey-brand-color);
--fb-input-border-color: var(--border);
--fb-input-border-radius: var(--radius);
--fb-input-font-family: inherit;
--fb-input-font-size: 0.875rem !important;
--fb-input-font-size: 0.875rem;
--fb-input-font-weight: 400;
--fb-input-color: var(--foreground);
--fb-input-placeholder-color: var(--fb-input-color);
@@ -152,7 +152,7 @@
--fb-option-font-weight: var(--fb-input-font-weight);
}
.button-custom {
#fbjs .button-custom {
width: var(--fb-button-width);
height: var(--fb-button-height);
background-color: var(--fb-button-bg-color);
@@ -167,7 +167,7 @@
font-size: var(--fb-button-font-size);
}
.label-headline {
#fbjs .label-headline {
font-family: var(--fb-element-headline-font-family);
font-weight: var(--fb-element-headline-font-weight);
font-size: var(--fb-element-headline-font-size) !important;
@@ -175,15 +175,16 @@
opacity: var(--fb-element-headline-opacity);
}
.label-description {
#fbjs .label-description,
#fbjs .text-subheading {
font-family: var(--fb-element-description-font-family);
font-weight: var(--fb-element-description-font-weight);
font-size: var(--fb-element-description-font-size) !important;
color: var(--fb-element-description-color);
color: var(--fb-element-description-color) !important;
opacity: var(--fb-element-description-opacity);
}
.label-default {
#fbjs .label-default {
font-family: var(--fb-label-font-family);
font-weight: var(--fb-label-font-weight);
font-size: var(--fb-label-font-size) !important;
@@ -191,13 +192,13 @@
opacity: var(--fb-label-opacity);
}
.progress-track {
#fbjs .progress-track {
height: var(--fb-progress-track-height);
background-color: var(--fb-progress-track-bg-color);
border-radius: var(--fb-progress-track-border-radius);
}
.progress-indicator {
#fbjs .progress-indicator {
background-color: var(--fb-progress-indicator-bg-color);
border-radius: var(--fb-progress-indicator-border-radius);
}

View File

@@ -13,7 +13,6 @@ checksums:
common/open_in_new_tab: 6844e4922a7a40a7ee25c10ea109cdeb
common/people_responded: b685fb877090d8658db724ad07a0dbd8
common/please_retry_now_or_try_again_later: 949a3841e2eb01fa249790a42bf23aa5
common/please_specify: e1faa6cd085144f7339c7e74dc6fb366
common/powered_by: 6b6f88e2fa5a1ecec6cebf813abaeebb
common/privacy_policy: 7459744a63ef8af4e517a09024bd7c08
common/protected_by_reCAPTCHA_and_the_Google: 32de026bff5d52e9edf5410d7d7b835f
@@ -31,6 +30,10 @@ checksums:
common/the_servers_cannot_be_reached_at_the_moment: f8adbeccac69f9230a55b5b3af52b081
common/they_will_be_redirected_immediately: 936bc99cb575cba95ea8f04d82bb353b
common/your_feedback_is_stuck: db2b6aba26723b01aee0fc918d3ca052
errors/all_options_must_be_ranked: 360a2edff623496f7047907bad115ea1
errors/all_rows_must_be_answered: 295f41a0ef04cbb3491c798053c61abd
errors/file_extension_must_be: 3102dc81a482f1b05ee490767b1c3c97
errors/file_extension_must_not_be: bd21065a4201d9a29b126586aecd8f29
errors/file_input/duplicate_files: 198dd29e67beb6abc5b2534ede7d7f68
errors/file_input/file_size_exceeded: 072045b042a39fa1df76200f8fa36dd4
errors/file_input/file_size_exceeded_alert: d8e482a2ff05e78bbacaed9e9db9b5eb
@@ -40,14 +43,28 @@ checksums:
errors/file_input/you_can_only_upload_a_maximum_of_files: 72fe144f81075e5b06bae53b3a84d4db
errors/invalid_device_error/message: 8813dcd0e3e41934af18d7a15f8c83f4
errors/invalid_device_error/title: 20d261b478aaba161b0853a588926e23
errors/please_book_an_appointment: 9e8acea3721f660b6a988f79c4105ab8
errors/invalid_format: 66df570c79b420d66f3badaf5867e4ad
errors/is_between: 2e8e3d11ac315aed727fa3b9e0bc435b
errors/is_earlier_than: 377af5c84d09083f57580ddd8199cda8
errors/is_greater_than: ab6ff5c676478d66cd64930deb4f0fd1
errors/is_later_than: dc64e8fe79ade4b53843270e00957163
errors/is_less_than: e1d0b7e6292e8c775e9eee349fee6269
errors/is_not_between: 887b4ece0ad58c027fc35ef6b3f2d5e8
errors/max_length: cbaef9675c55b3229e6306eb2d78dbfd
errors/max_selections: b2682a6ce820681074d4a319b96f8dbd
errors/max_value: edfe2d387a283c2b2a48da251fcf6ade
errors/min_length: d08ea43d2cc9d6b2aee87ddba60dd2b3
errors/min_selections: 23a75f21b4cb0530e5cfd02650d3e3d7
errors/min_value: f52e2d54c7fc0e96d5e3f5d32b2618c7
errors/minimum_options_ranked: 7c06caceff53b590159d0e0a9a8bd812
errors/minimum_rows_answered: ca132ce930516cf5ce97c34c7a7ef3d3
errors/please_enter_a_valid_email_address: 8de4bc8832b11b380bc4cbcedc16e48b
errors/please_enter_a_valid_phone_number: 1530eb9ab7d6d190bddb37667c711631
errors/please_enter_a_valid_url: e3bcfb605be4ee32aa19d9ac32bb11a4
errors/please_fill_out_this_field: 88d4fd502ae8d423277aef723afcd1a7
errors/please_rank_all_items_before_submitting: 24fb14a2550bd7ec3e253dda0997cea8
errors/please_select_a_date: 1abdc8ffb887dbbdcc0d05486cd84de7
errors/please_select_an_option: 9fede3bb9ded29301e89b98616e3583a
errors/please_upload_a_file: 4356dfca88553acb377664c923c2d6b7
errors/recaptcha_error/message: b3f2c5950cbc0887f391f9e2bccb676e
errors/recaptcha_error/title: 8e923ec38a92041569879a39c6467131
errors/value_must_contain: cd555796372f06b42f42f6b304f98e92
errors/value_must_equal: 227b5c87bb7761714ab9bc5a8adc6607
errors/value_must_not_contain: 5349a544135c7d7fc6e01c4221004236
errors/value_must_not_equal: cb25ed72166baeeaaa263bd5cf1c0e1c

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "فتح في علامة تبويب جديدة",
"people_responded": "{count, plural, one {شخص واحد استجاب} two {شخصان استجابا} few {{count} أشخاص استجابوا} many {{count} شخصًا استجابوا} other {{count} شخص استجابوا}}",
"please_retry_now_or_try_again_later": "يرجى إعادة المحاولة الآن أو المحاولة مرة أخرى لاحقًا.",
"please_specify": "يرجى التحديد",
"powered_by": "مشغل بواسطة",
"privacy_policy": "سياسة الخصوصية",
"protected_by_reCAPTCHA_and_the_Google": "محمي بواسطة reCAPTCHA و Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "تعليقك عالق :("
},
"errors": {
"all_options_must_be_ranked": "يرجى ترتيب جميع الخيارات",
"all_rows_must_be_answered": "يرجى الإجابة على جميع الصفوف",
"file_extension_must_be": "يجب أن يكون امتداد الملف {extension}",
"file_extension_must_not_be": "يجب ألا يكون امتداد الملف {extension}",
"file_input": {
"duplicate_files": "الملفات التالية تم تحميلها بالفعل: {duplicateNames}. لا يُسمح بالملفات المكررة.",
"file_size_exceeded": "الملف (الملفات) التالية تتجاوز الحجم الأقصى البالغ {maxSizeInMB} ميجابايت وتم إزالتها: {fileNames}",
@@ -45,18 +48,32 @@
"message": "يرجى تعطيل حماية البريد المزعج في إعدادات الاستبيان للاستمرار في استخدام هذا الجهاز.",
"title": "هذا الجهاز لا يدعم حماية البريد المزعج."
},
"please_book_an_appointment": "يرجى حجز موعد",
"invalid_format": "يرجى إدخال تنسيق صالح",
"is_between": "يرجى اختيار تاريخ بين {startDate} و {endDate}",
"is_earlier_than": "يرجى اختيار تاريخ أقدم من {date}",
"is_greater_than": "يرجى إدخال قيمة أكبر من {min}",
"is_later_than": "يرجى اختيار تاريخ أحدث من {date}",
"is_less_than": "يرجى إدخال قيمة أقل من {max}",
"is_not_between": "يرجى اختيار تاريخ ليس بين {startDate} و {endDate}",
"max_length": "يرجى إدخال {max} حرفًا كحد أقصى",
"max_selections": "يرجى اختيار {max} خيارات كحد أقصى",
"max_value": "يرجى إدخال قيمة لا تزيد عن {max}",
"min_length": "يرجى إدخال {min} أحرف على الأقل",
"min_selections": "يرجى اختيار {min} خيارات على الأقل",
"min_value": "يرجى إدخال قيمة لا تقل عن {min}",
"minimum_options_ranked": "يرجى ترتيب {min} خيارات على الأقل",
"minimum_rows_answered": "يرجى الإجابة على {min} صفوف على الأقل",
"please_enter_a_valid_email_address": "الرجاء إدخال عنوان بريد إلكتروني صالح",
"please_enter_a_valid_phone_number": "يرجى إدخال رقم هاتف صحيح",
"please_enter_a_valid_url": "الرجاء إدخال عنوان URL صالح",
"please_fill_out_this_field": "يرجى ملء هذا الحقل",
"please_rank_all_items_before_submitting": "يرجى ترتيب جميع العناصر قبل الإرسال",
"please_select_a_date": "يرجى اختيار تاريخ",
"please_select_an_option": "يرجى اختيار خيار",
"please_upload_a_file": "يرجى تحميل ملف",
"recaptcha_error": {
"message": "تعذر إرسال ردك لأنه تم تصنيفه كنشاط آلي. إذا كنت تتنفس، يرجى المحاولة مرة أخرى.",
"title": "لم نتمكن من التحقق من أنك إنسان."
}
},
"value_must_contain": "يجب أن تحتوي القيمة على {value}",
"value_must_equal": "يجب أن تساوي القيمة {value}",
"value_must_not_contain": "يجب ألا تحتوي القيمة على {value}",
"value_must_not_equal": "يجب ألا تساوي القيمة {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "In neuem Tab öffnen",
"people_responded": "{count, plural, one {1 Person hat geantwortet} other {{count} Personen haben geantwortet}}",
"please_retry_now_or_try_again_later": "Bitte versuchen Sie es jetzt erneut oder später noch einmal.",
"please_specify": "Bitte angeben",
"powered_by": "Bereitgestellt von",
"privacy_policy": "Datenschutzrichtlinie",
"protected_by_reCAPTCHA_and_the_Google": "Geschützt durch reCAPTCHA und die Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Ihr Feedback steckt fest :("
},
"errors": {
"all_options_must_be_ranked": "Bitte ordnen Sie alle Optionen ein",
"all_rows_must_be_answered": "Bitte beantworten Sie alle Zeilen",
"file_extension_must_be": "Die Dateierweiterung muss {extension} sein",
"file_extension_must_not_be": "Die Dateierweiterung darf nicht {extension} sein",
"file_input": {
"duplicate_files": "Die folgenden Dateien sind bereits hochgeladen: {duplicateNames}. Doppelte Dateien sind nicht erlaubt.",
"file_size_exceeded": "Die folgenden Dateien überschreiten die maximale Größe von {maxSizeInMB} MB und wurden entfernt: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Bitte deaktivieren Sie den Spam-Schutz in den Umfrageeinstellungen, um dieses Gerät weiterhin zu verwenden.",
"title": "Dieses Gerät unterstützt keinen Spam-Schutz."
},
"please_book_an_appointment": "Bitte vereinbaren Sie einen Termin",
"invalid_format": "Bitte geben Sie ein gültiges Format ein",
"is_between": "Bitte wählen Sie ein Datum zwischen {startDate} und {endDate}",
"is_earlier_than": "Bitte wählen Sie ein Datum vor dem {date}",
"is_greater_than": "Bitte geben Sie einen Wert größer als {min} ein",
"is_later_than": "Bitte wählen Sie ein Datum nach dem {date}",
"is_less_than": "Bitte geben Sie einen Wert kleiner als {max} ein",
"is_not_between": "Bitte wählen Sie ein Datum, das nicht zwischen {startDate} und {endDate} liegt",
"max_length": "Bitte geben Sie nicht mehr als {max} Zeichen ein",
"max_selections": "Bitte wählen Sie nicht mehr als {max} Optionen aus",
"max_value": "Bitte geben Sie einen Wert ein, der nicht größer als {max} ist",
"min_length": "Bitte geben Sie mindestens {min} Zeichen ein",
"min_selections": "Bitte wählen Sie mindestens {min} Optionen aus",
"min_value": "Bitte geben Sie einen Wert von mindestens {min} ein",
"minimum_options_ranked": "Bitte ordnen Sie mindestens {min} Optionen",
"minimum_rows_answered": "Bitte beantworten Sie mindestens {min} Zeilen",
"please_enter_a_valid_email_address": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"please_enter_a_valid_phone_number": "Bitte geben Sie eine gültige Telefonnummer ein",
"please_enter_a_valid_url": "Bitte geben Sie eine gültige URL ein",
"please_fill_out_this_field": "Bitte füllen Sie dieses Feld aus",
"please_rank_all_items_before_submitting": "Bitte ordnen Sie alle Elemente vor dem Absenden",
"please_select_a_date": "Bitte wählen Sie ein Datum aus",
"please_select_an_option": "Bitte wählen Sie eine Option",
"please_upload_a_file": "Bitte laden Sie eine Datei hoch",
"recaptcha_error": {
"message": "Ihre Antwort konnte nicht übermittelt werden, da sie als automatisierte Aktivität eingestuft wurde. Wenn Sie atmen, versuchen Sie es bitte erneut.",
"title": "Wir konnten nicht verifizieren, dass Sie ein Mensch sind."
}
},
"value_must_contain": "Wert muss {value} enthalten",
"value_must_equal": "Wert muss {value} entsprechen",
"value_must_not_contain": "Wert darf {value} nicht enthalten",
"value_must_not_equal": "Wert darf nicht {value} entsprechen"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Open in new tab",
"people_responded": "{count, plural, one {1 person responded} other {{count} people responded}}",
"please_retry_now_or_try_again_later": "Please retry now or try again later.",
"please_specify": "Please specify",
"powered_by": "Powered by",
"privacy_policy": "Privacy Policy",
"protected_by_reCAPTCHA_and_the_Google": "Protected by reCAPTCHA and the Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Your feedback is stuck :("
},
"errors": {
"all_options_must_be_ranked": "Please rank all options",
"all_rows_must_be_answered": "Please answer all rows",
"file_extension_must_be": "File extension must be {extension}",
"file_extension_must_not_be": "File extension must not be {extension}",
"file_input": {
"duplicate_files": "The following files are already uploaded: {duplicateNames}. Duplicate files are not allowed.",
"file_size_exceeded": "The following file(s) exceed the maximum size of {maxSizeInMB} MB and were removed: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Please disable spam protection in the survey settings to continue using this device.",
"title": "This device doesnt support spam protection."
},
"please_book_an_appointment": "Please book an appointment",
"invalid_format": "Please enter a valid format",
"is_between": "Please select a date between {startDate} and {endDate}",
"is_earlier_than": "Please select a date earlier than {date}",
"is_greater_than": "Please enter a value greater than {min}",
"is_later_than": "Please select a date later than {date}",
"is_less_than": "Please enter a value less than {max}",
"is_not_between": "Please select a date not between {startDate} and {endDate}",
"max_length": "Please enter no more than {max} characters",
"max_selections": "Please select no more than {max} options",
"max_value": "Please enter a value no greater than {max}",
"min_length": "Please enter at least {min} characters",
"min_selections": "Please select at least {min} options",
"min_value": "Please enter a value of at least {min}",
"minimum_options_ranked": "Please rank at least {min} options",
"minimum_rows_answered": "Please answer at least {min} rows",
"please_enter_a_valid_email_address": "Please enter a valid email address",
"please_enter_a_valid_phone_number": "Please enter a valid phone number",
"please_enter_a_valid_url": "Please enter a valid URL",
"please_fill_out_this_field": "Please fill out this field",
"please_rank_all_items_before_submitting": "Please rank all items before submitting",
"please_select_a_date": "Please select a date",
"please_select_an_option": "Please select an option",
"please_upload_a_file": "Please upload a file",
"recaptcha_error": {
"message": "Your response could not be submitted because it was flagged as automated activity. If you breathe, please try again.",
"title": "We couldn't verify that you're human."
}
},
"value_must_contain": "Value must contain {value}",
"value_must_equal": "Value must equal {value}",
"value_must_not_contain": "Value must not contain {value}",
"value_must_not_equal": "Value must not equal {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Abrir en nueva pestaña",
"people_responded": "{count, plural, one {1 persona respondió} other {{count} personas respondieron}}",
"please_retry_now_or_try_again_later": "Por favor, inténtalo ahora o prueba más tarde.",
"please_specify": "Por favor especifique",
"powered_by": "Desarrollado por",
"privacy_policy": "Política de privacidad",
"protected_by_reCAPTCHA_and_the_Google": "Protegido por reCAPTCHA y Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Tu feedback está atascado :("
},
"errors": {
"all_options_must_be_ranked": "Por favor, clasifica todas las opciones",
"all_rows_must_be_answered": "Por favor, responde todas las filas",
"file_extension_must_be": "La extensión del archivo debe ser {extension}",
"file_extension_must_not_be": "La extensión del archivo no debe ser {extension}",
"file_input": {
"duplicate_files": "Los siguientes archivos ya están subidos: {duplicateNames}. No se permiten archivos duplicados.",
"file_size_exceeded": "Los siguientes archivos exceden el tamaño máximo de {maxSizeInMB} MB y fueron eliminados: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Por favor, desactive la protección contra spam en la configuración de la encuesta para continuar usando este dispositivo.",
"title": "Este dispositivo no es compatible con la protección contra spam."
},
"please_book_an_appointment": "Por favor, reserve una cita",
"invalid_format": "Por favor, introduce un formato válido",
"is_between": "Por favor, selecciona una fecha entre {startDate} y {endDate}",
"is_earlier_than": "Por favor, selecciona una fecha anterior a {date}",
"is_greater_than": "Por favor, introduce un valor mayor que {min}",
"is_later_than": "Por favor, selecciona una fecha posterior a {date}",
"is_less_than": "Por favor, introduce un valor menor que {max}",
"is_not_between": "Por favor, selecciona una fecha que no esté entre {startDate} y {endDate}",
"max_length": "Por favor, introduce no más de {max} caracteres",
"max_selections": "Por favor, selecciona no más de {max} opciones",
"max_value": "Por favor, introduce un valor no mayor que {max}",
"min_length": "Por favor, introduce al menos {min} caracteres",
"min_selections": "Por favor, selecciona al menos {min} opciones",
"min_value": "Por favor, introduce un valor de al menos {min}",
"minimum_options_ranked": "Por favor, clasifica al menos {min} opciones",
"minimum_rows_answered": "Por favor, responde al menos {min} filas",
"please_enter_a_valid_email_address": "Por favor, introduce una dirección de correo electrónico válida",
"please_enter_a_valid_phone_number": "Por favor, introduzca un número de teléfono válido",
"please_enter_a_valid_url": "Por favor, introduce una URL válida",
"please_fill_out_this_field": "Por favor, complete este campo",
"please_rank_all_items_before_submitting": "Por favor, clasifique todos los elementos antes de enviar",
"please_select_a_date": "Por favor, seleccione una fecha",
"please_select_an_option": "Por favor selecciona una opción",
"please_upload_a_file": "Por favor, suba un archivo",
"recaptcha_error": {
"message": "Su respuesta no pudo ser enviada porque fue marcada como actividad automatizada. Si respira, por favor inténtelo de nuevo.",
"title": "No pudimos verificar que usted es humano."
}
},
"value_must_contain": "El valor debe contener {value}",
"value_must_equal": "El valor debe ser igual a {value}",
"value_must_not_contain": "El valor no debe contener {value}",
"value_must_not_equal": "El valor no debe ser igual a {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
"people_responded": "{count, plural, one {1 personne a répondu} other {{count} personnes ont répondu}}",
"please_retry_now_or_try_again_later": "Veuillez réessayer maintenant ou réessayer plus tard.",
"please_specify": "Veuillez préciser",
"powered_by": "Propulsé par",
"privacy_policy": "Politique de confidentialité",
"protected_by_reCAPTCHA_and_the_Google": "Protégé par reCAPTCHA et Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Votre feedback est bloqué :("
},
"errors": {
"all_options_must_be_ranked": "Veuillez classer toutes les options",
"all_rows_must_be_answered": "Veuillez répondre à toutes les lignes",
"file_extension_must_be": "L'extension du fichier doit être {extension}",
"file_extension_must_not_be": "L'extension du fichier ne doit pas être {extension}",
"file_input": {
"duplicate_files": "Les fichiers suivants sont déjà téléchargés : {duplicateNames}. Les fichiers en double ne sont pas autorisés.",
"file_size_exceeded": "Les fichiers suivants dépassent la taille maximale de {maxSizeInMB} Mo et ont été supprimés : {fileNames}",
@@ -45,18 +48,32 @@
"message": "Veuillez désactiver la protection contre le spam dans les paramètres du sondage pour continuer à utiliser cet appareil.",
"title": "Cet appareil ne prend pas en charge la protection contre le spam."
},
"please_book_an_appointment": "Veuillez prendre rendez-vous",
"invalid_format": "Veuillez saisir un format valide",
"is_between": "Veuillez sélectionner une date entre le {startDate} et le {endDate}",
"is_earlier_than": "Veuillez sélectionner une date antérieure au {date}",
"is_greater_than": "Veuillez saisir une valeur supérieure à {min}",
"is_later_than": "Veuillez sélectionner une date postérieure au {date}",
"is_less_than": "Veuillez saisir une valeur inférieure à {max}",
"is_not_between": "Veuillez sélectionner une date qui ne soit pas entre le {startDate} et le {endDate}",
"max_length": "Veuillez saisir au maximum {max} caractères",
"max_selections": "Veuillez sélectionner au maximum {max} options",
"max_value": "Veuillez saisir une valeur ne dépassant pas {max}",
"min_length": "Veuillez saisir au moins {min} caractères",
"min_selections": "Veuillez sélectionner au moins {min} options",
"min_value": "Veuillez saisir une valeur d'au moins {min}",
"minimum_options_ranked": "Veuillez classer au moins {min} options",
"minimum_rows_answered": "Veuillez répondre à au moins {min} lignes",
"please_enter_a_valid_email_address": "Veuillez saisir une adresse e-mail valide",
"please_enter_a_valid_phone_number": "Veuillez saisir un numéro de téléphone valide",
"please_enter_a_valid_url": "Veuillez saisir une URL valide",
"please_fill_out_this_field": "Veuillez remplir ce champ",
"please_rank_all_items_before_submitting": "Veuillez classer tous les éléments avant de soumettre",
"please_select_a_date": "Veuillez sélectionner une date",
"please_select_an_option": "Veuillez sélectionner une option",
"please_upload_a_file": "Veuillez télécharger un fichier",
"recaptcha_error": {
"message": "Votre réponse n'a pas pu être soumise car elle a été signalée comme une activité automatisée. Si vous respirez, veuillez réessayer.",
"title": "Nous n'avons pas pu vérifier que vous êtes humain."
}
},
"value_must_contain": "La valeur doit contenir {value}",
"value_must_equal": "La valeur doit être égale à {value}",
"value_must_not_contain": "La valeur ne doit pas contenir {value}",
"value_must_not_equal": "La valeur ne doit pas être égale à {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "नए टैब में खोलें",
"people_responded": "{count, plural, one {1 व्यक्ति ने जवाब दिया} other {{count} लोगों ने जवाब दिया}}",
"please_retry_now_or_try_again_later": "कृपया अभी पुनः प्रयास करें या बाद में फिर से प्रयास करें।",
"please_specify": "कृपया निर्दिष्ट करें",
"powered_by": "द्वारा संचालित",
"privacy_policy": "गोपनीयता नीति",
"protected_by_reCAPTCHA_and_the_Google": "reCAPTCHA और Google द्वारा संरक्षित",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "आपकी प्रतिक्रिया अटक गई है :("
},
"errors": {
"all_options_must_be_ranked": "कृपया सभी विकल्पों को रैंक करें",
"all_rows_must_be_answered": "कृपया सभी पंक्तियों का उत्तर दें",
"file_extension_must_be": "फ़ाइल एक्सटेंशन {extension} होना चाहिए",
"file_extension_must_not_be": "फ़ाइल एक्सटेंशन {extension} नहीं होना चाहिए",
"file_input": {
"duplicate_files": "निम्नलिखित फ़ाइलें पहले से ही अपलोड की गई हैं: {duplicateNames}। डुप्लिकेट फ़ाइलों की अनुमति नहीं है।",
"file_size_exceeded": "निम्नलिखित फ़ाइल(ें) अधिकतम आकार {maxSizeInMB} MB से अधिक हैं और हटा दी गई हैं: {fileNames}",
@@ -45,18 +48,32 @@
"message": "इस डिवाइस का उपयोग जारी रखने के लिए कृपया सर्वेक्षण सेटिंग्स में स्पैम सुरक्षा को अक्षम करें।",
"title": "यह डिवाइस स्पैम सुरक्षा का समर्थन नहीं करता है।"
},
"please_book_an_appointment": "कृपया एक अपॉइंटमेंट बुक करें",
"invalid_format": "कृपया एक मान्य प्रारूप दर्ज करें",
"is_between": "कृपया {startDate} और {endDate} के बीच की तारीख चुनें",
"is_earlier_than": "कृपया {date} से पहले की तारीख चुनें",
"is_greater_than": "कृपया {min} से अधिक मान दर्ज करें",
"is_later_than": "कृपया {date} के बाद की तारीख चुनें",
"is_less_than": "कृपया {max} से कम मान दर्ज करें",
"is_not_between": "कृपया {startDate} और {endDate} के बीच की तारीख न चुनें",
"max_length": "कृपया {max} वर्णों से अधिक दर्ज न करें",
"max_selections": "कृपया {max} विकल्पों से अधिक का चयन न करें",
"max_value": "कृपया {max} से अधिक मान दर्ज न करें",
"min_length": "कृपया कम से कम {min} वर्ण दर्ज करें",
"min_selections": "कृपया कम से कम {min} विकल्पों का चयन करें",
"min_value": "कृपया कम से कम {min} का मान दर्ज करें",
"minimum_options_ranked": "कृपया कम से कम {min} विकल्पों को रैंक करें",
"minimum_rows_answered": "कृपया कम से कम {min} पंक्तियों का उत्तर दें",
"please_enter_a_valid_email_address": "कृपया एक वैध ईमेल पता दर्ज करें",
"please_enter_a_valid_phone_number": "कृपया एक वैध फोन नंबर दर्ज करें",
"please_enter_a_valid_url": "कृपया एक वैध URL दर्ज करें",
"please_fill_out_this_field": "कृपया इस फील्ड को भरें",
"please_rank_all_items_before_submitting": "जमा करने से पहले कृपया सभी आइटम्स को रैंक करें",
"please_select_a_date": "कृपया एक तारीख चुनें",
"please_select_an_option": "कृपया एक विकल्प चुनें",
"please_upload_a_file": "कृपया एक फाइल अपलोड करें",
"recaptcha_error": {
"message": "आपका प्रतिसाद जमा नहीं किया जा सका क्योंकि इसे स्वचालित गतिविधि के रूप में चिह्नित किया गया था। यदि आप सांस लेते हैं, तो कृपया पुनः प्रयास करें।",
"title": "हम यह सत्यापित नहीं कर सके कि आप मानव हैं।"
}
},
"value_must_contain": "मान में {value} होना चाहिए",
"value_must_equal": "मान {value} के बराबर होना चाहिए",
"value_must_not_contain": "मान में {value} नहीं होना चाहिए",
"value_must_not_equal": "मान {value} के बराबर नहीं होना चाहिए"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Apri in una nuova scheda",
"people_responded": "{count, plural, one {1 persona ha risposto} other {{count} persone hanno risposto}}",
"please_retry_now_or_try_again_later": "Riprova ora o più tardi.",
"please_specify": "Si prega di specificare",
"powered_by": "Offerto da",
"privacy_policy": "Informativa sulla privacy",
"protected_by_reCAPTCHA_and_the_Google": "Protetto da reCAPTCHA e da Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Il tuo feedback è bloccato :("
},
"errors": {
"all_options_must_be_ranked": "Classifica tutte le opzioni",
"all_rows_must_be_answered": "Rispondi a tutte le righe",
"file_extension_must_be": "L'estensione del file deve essere {extension}",
"file_extension_must_not_be": "L'estensione del file non deve essere {extension}",
"file_input": {
"duplicate_files": "I seguenti file sono già caricati: {duplicateNames}. I file duplicati non sono consentiti.",
"file_size_exceeded": "I seguenti file superano la dimensione massima di {maxSizeInMB} MB e sono stati rimossi: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Per continuare a utilizzare questo dispositivo, disabilita la protezione anti-spam nelle impostazioni del sondaggio.",
"title": "Questo dispositivo non supporta la protezione anti-spam."
},
"please_book_an_appointment": "Prenota un appuntamento",
"invalid_format": "Inserisci un formato valido",
"is_between": "Seleziona una data compresa tra {startDate} e {endDate}",
"is_earlier_than": "Seleziona una data precedente a {date}",
"is_greater_than": "Inserisci un valore maggiore di {min}",
"is_later_than": "Seleziona una data successiva a {date}",
"is_less_than": "Inserisci un valore minore di {max}",
"is_not_between": "Seleziona una data non compresa tra {startDate} e {endDate}",
"max_length": "Inserisci non più di {max} caratteri",
"max_selections": "Seleziona non più di {max} opzioni",
"max_value": "Inserisci un valore non superiore a {max}",
"min_length": "Inserisci almeno {min} caratteri",
"min_selections": "Seleziona almeno {min} opzioni",
"min_value": "Inserisci un valore di almeno {min}",
"minimum_options_ranked": "Classifica almeno {min} opzioni",
"minimum_rows_answered": "Rispondi ad almeno {min} righe",
"please_enter_a_valid_email_address": "Inserisci un indirizzo email valido",
"please_enter_a_valid_phone_number": "Inserisci un numero di telefono valido",
"please_enter_a_valid_url": "Inserisci un URL valido",
"please_fill_out_this_field": "Compila questo campo",
"please_rank_all_items_before_submitting": "Classifica tutti gli elementi prima di inviare",
"please_select_a_date": "Seleziona una data",
"please_select_an_option": "Seleziona un'opzione",
"please_upload_a_file": "Carica un file",
"recaptcha_error": {
"message": "La tua risposta non può essere inviata perché è stata segnalata come attività automatizzata. Se respiri, riprova.",
"title": "Non siamo riusciti a verificare che sei umano."
}
},
"value_must_contain": "Il valore deve contenere {value}",
"value_must_equal": "Il valore deve essere uguale a {value}",
"value_must_not_contain": "Il valore non deve contenere {value}",
"value_must_not_equal": "Il valore non deve essere uguale a {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "新しいタブで開く",
"people_responded": "{count, plural, other {{count}人が回答しました}}",
"please_retry_now_or_try_again_later": "今すぐ再試行するか、後でもう一度お試しください。",
"please_specify": "具体的に記入してください",
"powered_by": "提供:",
"privacy_policy": "プライバシーポリシー",
"protected_by_reCAPTCHA_and_the_Google": "reCAPTCHAとGoogleによって保護されています",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "フィードバックが送信できません :("
},
"errors": {
"all_options_must_be_ranked": "すべてのオプションをランク付けしてください",
"all_rows_must_be_answered": "すべての行に回答してください",
"file_extension_must_be": "ファイル拡張子は{extension}である必要があります",
"file_extension_must_not_be": "ファイル拡張子は{extension}であってはなりません",
"file_input": {
"duplicate_files": "以下のファイルはすでにアップロードされています:{duplicateNames}。重複ファイルは許可されていません。",
"file_size_exceeded": "以下のファイルは最大サイズ{maxSizeInMB}MBを超えているため削除されました{fileNames}",
@@ -45,18 +48,32 @@
"message": "このデバイスを引き続き使用するには、アンケート設定でスパム保護を無効にしてください。",
"title": "このデバイスはスパム保護に対応していません。"
},
"please_book_an_appointment": "予約をお取りください",
"invalid_format": "有効な形式を入力してください",
"is_between": "{startDate}から{endDate}の間の日付を選択してください",
"is_earlier_than": "{date}より前の日付を選択してください",
"is_greater_than": "{min}より大きい値を入力してください",
"is_later_than": "{date}より後の日付を選択してください",
"is_less_than": "{max}より小さい値を入力してください",
"is_not_between": "{startDate}から{endDate}の間以外の日付を選択してください",
"max_length": "{max}文字以内で入力してください",
"max_selections": "{max}個以内のオプションを選択してください",
"max_value": "{max}以下の値を入力してください",
"min_length": "{min}文字以上入力してください",
"min_selections": "{min}個以上のオプションを選択してください",
"min_value": "{min}以上の値を入力してください",
"minimum_options_ranked": "最低{min}個のオプションをランク付けしてください",
"minimum_rows_answered": "最低{min}行に回答してください",
"please_enter_a_valid_email_address": "有効なメールアドレスを入力してください",
"please_enter_a_valid_phone_number": "有効な電話番号を入力してください",
"please_enter_a_valid_url": "有効なURLを入力してください",
"please_fill_out_this_field": "このフィールドに入力してください",
"please_rank_all_items_before_submitting": "送信する前にすべての項目をランク付けしてください",
"please_select_a_date": "日付を選択してください",
"please_select_an_option": "オプションを選択してください",
"please_upload_a_file": "ファイルをアップロードしてください",
"recaptcha_error": {
"message": "自動化された活動としてフラグが立てられたため、回答を送信できませんでした。人間の方は、もう一度お試しください。",
"title": "あなたが人間であることを確認できませんでした。"
}
},
"value_must_contain": "値に{value}を含める必要があります",
"value_must_equal": "値は{value}と等しい必要があります",
"value_must_not_contain": "値に{value}を含めることはできません",
"value_must_not_equal": "値は{value}と等しくない必要があります"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Openen in nieuw tabblad",
"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.",
"please_specify": "Graag specificeren",
"powered_by": "Aangedreven door",
"privacy_policy": "Privacybeleid",
"protected_by_reCAPTCHA_and_the_Google": "Beschermd door reCAPTCHA en Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Je feedback blijft hangen :("
},
"errors": {
"all_options_must_be_ranked": "Rangschik alle opties",
"all_rows_must_be_answered": "Beantwoord alle rijen",
"file_extension_must_be": "Bestandsextensie moet {extension} zijn",
"file_extension_must_not_be": "Bestandsextensie mag niet {extension} zijn",
"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}",
@@ -45,18 +48,32 @@
"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",
"invalid_format": "Voer een geldig formaat in",
"is_between": "Selecteer een datum tussen {startDate} en {endDate}",
"is_earlier_than": "Selecteer een datum vóór {date}",
"is_greater_than": "Voer een waarde in die groter is dan {min}",
"is_later_than": "Selecteer een datum na {date}",
"is_less_than": "Voer een waarde in die kleiner is dan {max}",
"is_not_between": "Selecteer een datum die niet tussen {startDate} en {endDate} ligt",
"max_length": "Voer niet meer dan {max} tekens in",
"max_selections": "Selecteer niet meer dan {max} opties",
"max_value": "Voer een waarde in die niet groter is dan {max}",
"min_length": "Voer minimaal {min} tekens in",
"min_selections": "Selecteer minimaal {min} opties",
"min_value": "Voer een waarde in van minimaal {min}",
"minimum_options_ranked": "Rangschik minimaal {min} opties",
"minimum_rows_answered": "Beantwoord minimaal {min} rijen",
"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_select_an_option": "Selecteer een optie",
"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."
}
},
"value_must_contain": "Waarde moet {value} bevatten",
"value_must_equal": "Waarde moet gelijk zijn aan {value}",
"value_must_not_contain": "Waarde mag {value} niet bevatten",
"value_must_not_equal": "Waarde mag niet gelijk zijn aan {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Abrir em nova aba",
"people_responded": "{count, plural, one {1 pessoa respondeu} other {{count} pessoas responderam}}",
"please_retry_now_or_try_again_later": "Por favor, tente novamente agora ou mais tarde.",
"please_specify": "Por favor, especifique",
"powered_by": "Desenvolvido por",
"privacy_policy": "Política de privacidade",
"protected_by_reCAPTCHA_and_the_Google": "Protegido pelo reCAPTCHA e o Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Seu feedback está preso :("
},
"errors": {
"all_options_must_be_ranked": "Por favor, classifique todas as opções",
"all_rows_must_be_answered": "Por favor, responda todas as linhas",
"file_extension_must_be": "A extensão do arquivo deve ser {extension}",
"file_extension_must_not_be": "A extensão do arquivo não deve ser {extension}",
"file_input": {
"duplicate_files": "Os seguintes arquivos já foram carregados: {duplicateNames}. Arquivos duplicados não são permitidos.",
"file_size_exceeded": "Os seguintes arquivos excedem o tamanho máximo de {maxSizeInMB} MB e foram removidos: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Por favor, desative a proteção contra spam nas configurações da pesquisa para continuar usando este dispositivo.",
"title": "Este dispositivo não suporta proteção contra spam."
},
"please_book_an_appointment": "Por favor, marque uma consulta",
"invalid_format": "Por favor, insira um formato válido",
"is_between": "Por favor, selecione uma data entre {startDate} e {endDate}",
"is_earlier_than": "Por favor, selecione uma data anterior a {date}",
"is_greater_than": "Por favor, insira um valor superior a {min}",
"is_later_than": "Por favor, selecione uma data posterior a {date}",
"is_less_than": "Por favor, insira um valor inferior a {max}",
"is_not_between": "Por favor, selecione uma data que não esteja entre {startDate} e {endDate}",
"max_length": "Por favor, insira no máximo {max} caracteres",
"max_selections": "Por favor, selecione no máximo {max} opções",
"max_value": "Por favor, insira um valor não superior a {max}",
"min_length": "Por favor, insira pelo menos {min} caracteres",
"min_selections": "Por favor, selecione pelo menos {min} opções",
"min_value": "Por favor, insira um valor de pelo menos {min}",
"minimum_options_ranked": "Por favor, classifique pelo menos {min} opções",
"minimum_rows_answered": "Por favor, responda pelo menos {min} linhas",
"please_enter_a_valid_email_address": "Por favor, insira um endereço de email válido",
"please_enter_a_valid_phone_number": "Por favor, insira um número de telefone válido",
"please_enter_a_valid_url": "Por favor, insira uma URL válida",
"please_fill_out_this_field": "Por favor, preencha este campo",
"please_rank_all_items_before_submitting": "Por favor, classifique todos os itens antes de enviar",
"please_select_a_date": "Por favor, selecione uma data",
"please_select_an_option": "Por favor, selecione uma opção",
"please_upload_a_file": "Por favor, carregue um arquivo",
"recaptcha_error": {
"message": "Sua resposta não pôde ser enviada porque foi sinalizada como atividade automatizada. Se você respira, por favor tente novamente.",
"title": "Não conseguimos verificar que você é humano."
}
},
"value_must_contain": "O valor deve conter {value}",
"value_must_equal": "O valor deve ser igual a {value}",
"value_must_not_contain": "O valor não deve conter {value}",
"value_must_not_equal": "O valor não deve ser igual a {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Deschide într-o filă nouă",
"people_responded": "{count, plural, one {1 persoană a răspuns} other {{count} persoane au răspuns}}",
"please_retry_now_or_try_again_later": "Te rugăm să încerci din nou acum sau mai târziu.",
"please_specify": "Vă rugăm să specificați",
"powered_by": "Susținut de",
"privacy_policy": "Politica de confidențialitate",
"protected_by_reCAPTCHA_and_the_Google": "Protejat de reCAPTCHA și de Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Feedback-ul tău este blocat :("
},
"errors": {
"all_options_must_be_ranked": "Vă rugăm să ordonați toate opțiunile",
"all_rows_must_be_answered": "Vă rugăm să răspundeți la toate rândurile",
"file_extension_must_be": "Extensia fișierului trebuie să fie {extension}",
"file_extension_must_not_be": "Extensia fișierului nu trebuie să fie {extension}",
"file_input": {
"duplicate_files": "Următoarele fișiere sunt deja încărcate: {duplicateNames}. Fișierele duplicate nu sunt permise.",
"file_size_exceeded": "Următoarele fișiere depășesc dimensiunea maximă de {maxSizeInMB} MB și au fost eliminate: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Dezactivați protecția împotriva spamului în setările sondajului pentru a continua să utilizați acest dispozitiv.",
"title": "Acest dispozitiv nu acceptă protecția împotriva spamului."
},
"please_book_an_appointment": "Vă rugăm să faceți o programare",
"invalid_format": "Vă rugăm să introduceți un format valid",
"is_between": "Vă rugăm să selectați o dată între {startDate} și {endDate}",
"is_earlier_than": "Vă rugăm să selectați o dată anterioară datei {date}",
"is_greater_than": "Vă rugăm să introduceți o valoare mai mare decât {min}",
"is_later_than": "Vă rugăm să selectați o dată ulterioară datei {date}",
"is_less_than": "Vă rugăm să introduceți o valoare mai mică decât {max}",
"is_not_between": "Vă rugăm să selectați o dată care nu este între {startDate} și {endDate}",
"max_length": "Vă rugăm să nu introduceți mai mult de {max} caractere",
"max_selections": "Vă rugăm să selectați cel mult {max} opțiuni",
"max_value": "Vă rugăm să introduceți o valoare care să nu depășească {max}",
"min_length": "Vă rugăm să introduceți cel puțin {min} caractere",
"min_selections": "Vă rugăm să selectați cel puțin {min} opțiuni",
"min_value": "Vă rugăm să introduceți o valoare de cel puțin {min}",
"minimum_options_ranked": "Vă rugăm să ordonați cel puțin {min} opțiuni",
"minimum_rows_answered": "Vă rugăm să răspundeți la cel puțin {min} rânduri",
"please_enter_a_valid_email_address": "Vă rugăm să introduceți o adresă de email validă",
"please_enter_a_valid_phone_number": "Vă rugăm să introduceți un număr de telefon valid",
"please_enter_a_valid_url": "Vă rugăm să introduceți un URL valid",
"please_fill_out_this_field": "Vă rugăm să completați acest câmp",
"please_rank_all_items_before_submitting": "Vă rugăm să clasificați toate elementele înainte de a trimite",
"please_select_a_date": "Vă rugăm să selectați o dată",
"please_select_an_option": "Vă rugăm să selectați o opțiune",
"please_upload_a_file": "Vă rugăm să încărcați un fișier",
"recaptcha_error": {
"message": "Răspunsul dumneavoastră nu a putut fi trimis deoarece a fost marcat ca activitate automată. Dacă respirați, încercați din nou.",
"title": "Nu am putut verifica dacă sunteți uman."
}
},
"value_must_contain": "Valoarea trebuie să conțină {value}",
"value_must_equal": "Valoarea trebuie să fie egală cu {value}",
"value_must_not_contain": "Valoarea nu trebuie să conțină {value}",
"value_must_not_equal": "Valoarea nu trebuie să fie egală cu {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Открыть в новой вкладке",
"people_responded": "{count, plural, one {1 человек ответил} other {{count} человека ответили}}",
"please_retry_now_or_try_again_later": "Пожалуйста, повторите попытку сейчас или попробуйте позже.",
"please_specify": "Пожалуйста, уточните",
"powered_by": "Работает на основе",
"privacy_policy": "Политика конфиденциальности",
"protected_by_reCAPTCHA_and_the_Google": "Защищено reCAPTCHA и Google",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Ваш отзыв застрял :("
},
"errors": {
"all_options_must_be_ranked": "Пожалуйста, расставьте все варианты по порядку",
"all_rows_must_be_answered": "Пожалуйста, ответьте на все строки",
"file_extension_must_be": "Расширение файла должно быть {extension}",
"file_extension_must_not_be": "Расширение файла не должно быть {extension}",
"file_input": {
"duplicate_files": "Следующие файлы уже загружены: {duplicateNames}. Дублирующие файлы не допускаются.",
"file_size_exceeded": "Следующие файлы превышают максимальный размер {maxSizeInMB} МБ и были удалены: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Пожалуйста, отключите защиту от спама в настройках опроса, чтобы продолжить использование этого устройства.",
"title": "Это устройство не поддерживает защиту от спама."
},
"please_book_an_appointment": "Пожалуйста, запишитесь на приём",
"invalid_format": "Пожалуйста, введите корректный формат",
"is_between": "Пожалуйста, выберите дату в диапазоне от {startDate} до {endDate}",
"is_earlier_than": "Пожалуйста, выберите дату раньше {date}",
"is_greater_than": "Пожалуйста, введите значение больше {min}",
"is_later_than": "Пожалуйста, выберите дату позже {date}",
"is_less_than": "Пожалуйста, введите значение меньше {max}",
"is_not_between": "Пожалуйста, выберите дату вне диапазона от {startDate} до {endDate}",
"max_length": "Пожалуйста, введите не более {max} символов",
"max_selections": "Пожалуйста, выберите не более {max} вариантов",
"max_value": "Пожалуйста, введите значение не больше {max}",
"min_length": "Пожалуйста, введите не менее {min} символов",
"min_selections": "Пожалуйста, выберите не менее {min} вариантов",
"min_value": "Пожалуйста, введите значение не меньше {min}",
"minimum_options_ranked": "Пожалуйста, ранжируйте не менее {min} вариантов",
"minimum_rows_answered": "Пожалуйста, ответьте как минимум на {min} строк(и)",
"please_enter_a_valid_email_address": "Пожалуйста, введите действительный адрес электронной почты",
"please_enter_a_valid_phone_number": "Пожалуйста, введите действительный номер телефона",
"please_enter_a_valid_url": "Пожалуйста, введите действительный URL-адрес",
"please_fill_out_this_field": "Пожалуйста, заполните это поле",
"please_rank_all_items_before_submitting": "Пожалуйста, оцените все элементы перед отправкой",
"please_select_a_date": "Пожалуйста, выберите дату",
"please_select_an_option": "Пожалуйста, выберите вариант",
"please_upload_a_file": "Пожалуйста, загрузите файл",
"recaptcha_error": {
"message": "Ваш ответ не может быть отправлен, так как он был помечен как автоматическая активность. Если вы дышите, попробуйте ещё раз.",
"title": "Мы не смогли подтвердить, что вы человек."
}
},
"value_must_contain": "Значение должно содержать {value}",
"value_must_equal": "Значение должно быть равно {value}",
"value_must_not_contain": "Значение не должно содержать {value}",
"value_must_not_equal": "Значение не должно быть равно {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Öppna i ny flik",
"people_responded": "{count, plural, one {1 person har svarat} other {{count} personer har svarat}}",
"please_retry_now_or_try_again_later": "Försök igen nu eller försök igen senare.",
"please_specify": "Vänligen ange",
"powered_by": "Drivs av",
"privacy_policy": "Integritetspolicy",
"protected_by_reCAPTCHA_and_the_Google": "Skyddas av reCAPTCHA och Googles",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Din feedback fastnade :("
},
"errors": {
"all_options_must_be_ranked": "Vänligen rangordna alla alternativ",
"all_rows_must_be_answered": "Vänligen besvara alla rader",
"file_extension_must_be": "Filändelsen måste vara {extension}",
"file_extension_must_not_be": "Filändelsen får inte vara {extension}",
"file_input": {
"duplicate_files": "Följande filer är redan uppladdade: {duplicateNames}. Dubbletter av filer är inte tillåtna.",
"file_size_exceeded": "Följande filer överstiger maxstorleken på {maxSizeInMB} MB och togs bort: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Vänligen inaktivera skräppostskyddet i enkätinställningarna för att fortsätta använda denna enhet.",
"title": "Denna enhet stöder inte skräppostskydd."
},
"please_book_an_appointment": "Vänligen boka ett möte",
"invalid_format": "Ange ett giltigt format",
"is_between": "Välj ett datum mellan {startDate} och {endDate}",
"is_earlier_than": "Välj ett datum före {date}",
"is_greater_than": "Ange ett värde som är större än {min}",
"is_later_than": "Välj ett datum efter {date}",
"is_less_than": "Ange ett värde som är mindre än {max}",
"is_not_between": "Välj ett datum som inte är mellan {startDate} och {endDate}",
"max_length": "Ange högst {max} tecken",
"max_selections": "Välj högst {max} alternativ",
"max_value": "Ange ett värde som är högst {max}",
"min_length": "Ange minst {min} tecken",
"min_selections": "Välj minst {min} alternativ",
"min_value": "Ange ett värde som är minst {min}",
"minimum_options_ranked": "Rangordna minst {min} alternativ",
"minimum_rows_answered": "Besvara minst {min} rader",
"please_enter_a_valid_email_address": "Vänligen ange en giltig e-postadress",
"please_enter_a_valid_phone_number": "Vänligen ange ett giltigt telefonnummer",
"please_enter_a_valid_url": "Vänligen ange en giltig URL",
"please_fill_out_this_field": "Vänligen fyll i detta fält",
"please_rank_all_items_before_submitting": "Vänligen rangordna alla objekt innan du skickar",
"please_select_a_date": "Vänligen välj ett datum",
"please_select_an_option": "Vänligen välj ett alternativ",
"please_upload_a_file": "Vänligen ladda upp en fil",
"recaptcha_error": {
"message": "Ditt svar kunde inte skickas eftersom det flaggades som automatiserad aktivitet. Om du andas, försök igen.",
"title": "Vi kunde inte verifiera att du är människa."
}
},
"value_must_contain": "Värdet måste innehålla {value}",
"value_must_equal": "Värdet måste vara {value}",
"value_must_not_contain": "Värdet får inte innehålla {value}",
"value_must_not_equal": "Värdet får inte vara {value}"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "Yangi oynada ochish",
"people_responded": "{count, plural, one {1 kishi javob berdi} other {{count} kishi javob berdi}}",
"please_retry_now_or_try_again_later": "Iltimos, hozir qayta urinib koring yoki keyinroq urinib koring.",
"please_specify": "Iltimos, aniqlang",
"powered_by": "Quvvatlanadi",
"privacy_policy": "Maxfiylik siyosati",
"protected_by_reCAPTCHA_and_the_Google": "reCAPTCHA va Google tomonidan himoyalangan",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "Sizning fikr-mulohazangiz qotib qoldi :("
},
"errors": {
"all_options_must_be_ranked": "Iltimos, barcha variantlarni tartiblang",
"all_rows_must_be_answered": "Iltimos, barcha qatorlarga javob bering",
"file_extension_must_be": "Fayl kengaytmasi {extension} bolishi kerak",
"file_extension_must_not_be": "Fayl kengaytmasi {extension} bolmasligi kerak",
"file_input": {
"duplicate_files": "Quyidagi fayllar allaqachon yuklangan: {duplicateNames}. Takroriy fayllarga ruxsat berilmaydi.",
"file_size_exceeded": "Quyidagi fayl(lar) {maxSizeInMB} MB maksimal hajmdan oshib ketdi va olib tashlandi: {fileNames}",
@@ -45,18 +48,32 @@
"message": "Iltimos, ushbu qurilmadan foydalanishni davom ettirish uchun so'rov sozlamalarida spam himoyasini o'chiring.",
"title": "Ushbu qurilma spam himoyasini qo'llab-quvvatlamaydi."
},
"please_book_an_appointment": "Iltimos, uchrashuvni bron qiling",
"invalid_format": "Iltimos, togri formatda kiriting",
"is_between": "Iltimos, {startDate} va {endDate} oraligidagi sanani tanlang",
"is_earlier_than": "Iltimos, {date} dan oldingi sanani tanlang",
"is_greater_than": "Iltimos, {min} dan katta qiymat kiriting",
"is_later_than": "Iltimos, {date} dan keyingi sanani tanlang",
"is_less_than": "Iltimos, {max} dan kichik qiymat kiriting",
"is_not_between": "Iltimos, {startDate} va {endDate} oraligidan tashqaridagi sanani tanlang",
"max_length": "Iltimos, {max} ta belgidan kop bolmagan matn kiriting",
"max_selections": "Iltimos, {max} tadan ortiq variant tanlamang",
"max_value": "Iltimos, {max} dan katta bolmagan qiymat kiriting",
"min_length": "Iltimos, kamida {min} ta belgi kiriting",
"min_selections": "Iltimos, kamida {min} ta variant tanlang",
"min_value": "Iltimos, kamida {min} qiymat kiriting",
"minimum_options_ranked": "Iltimos, kamida {min} ta variantni tartiblang",
"minimum_rows_answered": "Iltimos, kamida {min} ta qatorga javob bering",
"please_enter_a_valid_email_address": "Iltimos, toʻgʻri elektron pochta manzilini kiriting",
"please_enter_a_valid_phone_number": "Iltimos, to'g'ri telefon raqamini kiriting",
"please_enter_a_valid_url": "Iltimos, toʻgʻri URL manzilini kiriting",
"please_fill_out_this_field": "Iltimos, ushbu maydonni to'ldiring",
"please_rank_all_items_before_submitting": "Iltimos, yuborishdan oldin barcha elementlarni baholang",
"please_select_a_date": "Iltimos, sanani tanlang",
"please_select_an_option": "Iltimos, biror bir variantni tanlang",
"please_upload_a_file": "Iltimos, faylni yuklang",
"recaptcha_error": {
"message": "Sizning javobingiz avtomatlashtirilgan faoliyat sifatida belgilanganligi sababli yuborilmadi. Agar siz nafas olayotgan bo'lsangiz, qayta urinib ko'ring.",
"title": "Biz sizning inson ekanligingizni tasdiqlay olmadik."
}
},
"value_must_contain": "Qiymatda {value} bolishi kerak",
"value_must_equal": "Qiymat {value} ga teng bolishi kerak",
"value_must_not_contain": "Qiymatda {value} bolmasligi kerak",
"value_must_not_equal": "Qiymat {value} ga teng bolmasligi kerak"
}
}

View File

@@ -12,7 +12,6 @@
"open_in_new_tab": "在新标签页中打开",
"people_responded": "{count, plural, one {1 人已回应} other {{count} 人已回应}}",
"please_retry_now_or_try_again_later": "请立即重试或稍后再试。",
"please_specify": "请具体说明",
"powered_by": "技术支持",
"privacy_policy": "隐私政策",
"protected_by_reCAPTCHA_and_the_Google": "受 reCAPTCHA 和 Google 保护",
@@ -32,6 +31,10 @@
"your_feedback_is_stuck": "您的反馈卡住了 :("
},
"errors": {
"all_options_must_be_ranked": "请对所有选项进行排序",
"all_rows_must_be_answered": "请回答所有行",
"file_extension_must_be": "文件扩展名必须为{extension}",
"file_extension_must_not_be": "文件扩展名不能为{extension}",
"file_input": {
"duplicate_files": "以下文件已上传:{duplicateNames}。不允许重复文件。",
"file_size_exceeded": "以下文件超过了最大大小 {maxSizeInMB} MB已被移除{fileNames}",
@@ -45,18 +48,32 @@
"message": "请在调查设置中禁用垃圾邮件保护以继续使用此设备。",
"title": "此设备不支持垃圾邮件保护。"
},
"please_book_an_appointment": "请预约",
"invalid_format": "请输入有效的格式",
"is_between": "请选择{startDate}到{endDate}之间的日期",
"is_earlier_than": "请选择早于{date}的日期",
"is_greater_than": "请输入大于{min}的值",
"is_later_than": "请选择晚于{date}的日期",
"is_less_than": "请输入小于{max}的值",
"is_not_between": "请选择不在{startDate}到{endDate}之间的日期",
"max_length": "请输入不超过{max}个字符",
"max_selections": "请选择不超过{max}个选项",
"max_value": "请输入不大于{max}的值",
"min_length": "请输入至少{min}个字符",
"min_selections": "请选择至少{min}个选项",
"min_value": "请输入不少于{min}的值",
"minimum_options_ranked": "请至少排序{min}个选项",
"minimum_rows_answered": "请至少回答{min}行",
"please_enter_a_valid_email_address": "请输入有效的电子邮件地址",
"please_enter_a_valid_phone_number": "请输入有效的电话号码",
"please_enter_a_valid_url": "请输入有效的URL",
"please_fill_out_this_field": "请填写此字段",
"please_rank_all_items_before_submitting": "请在提交之前对所有项目进行排名",
"please_select_a_date": "请选择一个日期",
"please_select_an_option": "请选择一个选项",
"please_upload_a_file": "请上传一个文件",
"recaptcha_error": {
"message": "您的响应未能提交,因为它被标记为自动活动。如果您是人类,请重试。",
"title": "我们无法验证您是人类。"
}
},
"value_must_contain": "值必须包含{value}",
"value_must_equal": "值必须等于{value}",
"value_must_not_contain": "值不能包含{value}",
"value_must_not_equal": "值不能等于{value}"
}
}

View File

@@ -23,11 +23,15 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.umd.cjs"
},
"./validation": {
"types": "./dist/validation.d.ts",
"import": "./dist/validation.js"
}
},
"scripts": {
"dev": "vite build --watch --mode dev",
"build": "tsc && vite build",
"build": "tsc && vite build && BUILD_UMD=true vite build",
"build:analyze": "tsc && ANALYZE=true vite build",
"build:dev": "tsc && vite build --mode dev",
"go": "vite build --watch --mode dev",

View File

@@ -1,6 +1,7 @@
import { ButtonHTMLAttributes, useRef } from "preact/compat";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
interface SubmitButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
buttonLabel?: string;
@@ -72,7 +73,11 @@ export function SubmitButton({
type={type}
tabIndex={tabIndex}
autoFocus={focus}
className="bg-brand border-submit-button-border text-on-brand focus:ring-focus rounded-custom mb-1 flex items-center border px-3 py-3 text-base leading-4 font-medium shadow-xs hover:opacity-90 focus:ring-2 focus:ring-offset-2 focus:outline-hidden"
className={cn(
"bg-brand border-submit-button-border text-on-brand focus:ring-focus rounded-custom mb-1 flex items-center border px-3 py-3 text-base leading-4 font-medium shadow-xs hover:opacity-90 focus:ring-2 focus:ring-offset-2 focus:outline-hidden",
"justify-center shadow-xs hover:opacity-90 focus:ring-2 focus:ring-offset-2 focus:outline-hidden",
"button-custom"
)}
onClick={onClick}
disabled={disabled}>
{buttonLabel || (isLastQuestion ? t("common.finish") : t("common.next"))}

Some files were not shown because too many files have changed in this diff Show More