From 40f6c2f75ebe36d9b83b70f7c5b139fcb8dc4cb1 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 09:35:41 +0530 Subject: [PATCH 01/58] feat: show Form Checks only on a real fault, and simplify its wording Three changes to the same panel. Drops the four passing rows: "Form submissions are completing normally", the notification and integration equivalents, and "No caching plugin that needs configuring was found". A panel that mostly confirms nothing is wrong is one people learn to skip, and it took permanent sidebar space from the cards that do have something to say. On a healthy site get_action_items() now returns an empty array and nothing renders. Gates the whole surface on the Enable Logs toggle. That toggle is what feeds the fault counters, so with it off whatever is still standing in them describes the site as it was, not as it is, and a site owner who turned logging off has opted out of this surface. Both gates sit in get_action_items() and has_action_item_warnings(), the only two entry points: the React panel, the wp-admin banners and the promo stand-down all route through one or the other. has_action_item_warnings() returning false also lets the promo cards come back rather than leaving the sidebar with a permanent hole. Simplifies the notice copy and drops the em dashes. The success branch in the classic notice and the green icon in the panel are kept deliberately: srfm_action_items is public, so a third party can still contribute an item with that status and both surfaces should handle it rather than render something broken. --- admin/admin.php | 53 +++++++++++++-------------------- tests/unit/admin/test-admin.php | 44 ++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 876595ecf..efeb3d470 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -3477,7 +3477,7 @@ public function handle_dismiss_action_item_link() { /** * Whether anything is currently wrong enough to warrant a notice. * - * Deliberately re-derives the two conditions rather than calling + * Deliberately re-derives its conditions rather than calling * get_action_items(), which records an impression as a side effect and must not * run from a show_if callback. * @@ -3485,6 +3485,10 @@ public function handle_dismiss_action_item_link() { * @return bool */ public function has_action_item_warnings() { + if ( ! Client_Logger::is_enabled() ) { + return false; + } + if ( Client_Logger::has_persistent_failures() ) { return true; } @@ -3517,9 +3521,15 @@ public function get_action_items() { return []; } + // Logging off means the site owner has opted out of this whole surface: the + // fault counters stop being fed, so anything still standing in them is + // stale and cannot be trusted to reflect the site now. + if ( ! Client_Logger::is_enabled() ) { + return []; + } + $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) ); $warnings = []; - $passing = []; $open = Client_Logger::get_open_failures(); @@ -3533,38 +3543,26 @@ public function get_action_items() { /* translators: %s: form title. */ 'title' => __( 'We noticed a form submission failure on %s.', 'sureforms' ), 'generic' => __( 'We noticed a form submission failure.', 'sureforms' ), - 'message' => __( 'Visitors may be unable to reach you, and those entries were not saved.', 'sureforms' ), - 'passing' => __( 'Form submissions are completing normally.', 'sureforms' ), + 'message' => __( 'Visitors may not be able to reach you, and their entries were not saved.', 'sureforms' ), ], 'notification' => [ 'id' => 'notification_error', /* translators: %s: form title. */ 'title' => __( 'We noticed a notification failure on %s.', 'sureforms' ), 'generic' => __( 'We noticed a notification failure.', 'sureforms' ), - 'message' => __( 'The entry was saved, but the email telling you about it could not be sent — so new entries may be arriving without you hearing about them.', 'sureforms' ), - 'passing' => __( 'Notification emails are sending normally.', 'sureforms' ), + 'message' => __( 'The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing.', 'sureforms' ), ], 'integration' => [ 'id' => 'integration_error', /* translators: %s: form title. */ 'title' => __( 'We noticed an integration failure on %s.', 'sureforms' ), 'generic' => __( 'We noticed an integration failure.', 'sureforms' ), - 'message' => __( 'The entry was saved, but it could not be passed on to a connected service.', 'sureforms' ), - 'passing' => __( 'Integrations are running normally.', 'sureforms' ), + 'message' => __( 'The entry was saved, but we could not send it to a connected service.', 'sureforms' ), ], ]; foreach ( $categories as $category => $copy ) { if ( ! isset( $open[ $category ] ) ) { - $passing[] = [ - 'id' => $copy['id'], - 'status' => 'success', - 'title' => $copy['passing'], - 'message' => '', - 'cta_label' => '', - 'cta_url' => '', - 'dismissible' => false, - ]; continue; } @@ -3588,17 +3586,7 @@ public function get_action_items() { $caching_plugin = Helper::get_active_caching_plugin(); - if ( '' === $caching_plugin ) { - $passing[] = [ - 'id' => 'caching_plugin', - 'status' => 'success', - 'title' => __( 'No caching plugin that needs configuring was found.', 'sureforms' ), - 'message' => '', - 'cta_label' => '', - 'cta_url' => '', - 'dismissible' => false, - ]; - } elseif ( ! in_array( 'caching_plugin', $dismissed, true ) ) { + if ( '' !== $caching_plugin && ! in_array( 'caching_plugin', $dismissed, true ) ) { $warnings[] = [ 'id' => 'caching_plugin', 'status' => 'warning', @@ -3607,7 +3595,7 @@ public function get_action_items() { __( '%s may interfere with your forms.', 'sureforms' ), $caching_plugin ), - 'message' => __( 'Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order.', 'sureforms' ), + 'message' => __( 'Caching can show visitors an old copy of your form, or load its scripts in the wrong order.', 'sureforms' ), 'cta_label' => __( 'Help Me Fix', 'sureforms' ), 'cta_url' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-caching-plugins/', 'cta_action' => 'help_me_fix', @@ -3615,9 +3603,10 @@ public function get_action_items() { ]; } - // Warnings first: the point of the panel is what needs attention, with the - // passing checks below as reassurance rather than as the headline. - $items = array_merge( $warnings, $passing ); + // Only what needs attention. A panel confirming that nothing is wrong is + // something people learn to skip, and it takes sidebar space from the cards + // that do have something to say. + $items = $warnings; $this->track_action_item_impressions( $warnings ); diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 582b521ca..6ebf1e3d6 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1074,11 +1074,12 @@ private function make_user( $role ) { // --------------------------------------------------------------- /** - * A healthy site still reports, but only as passing checks. Nothing may be a - * warning -- logging is on by default, so a false positive reaches every - * install. + * A healthy site produces nothing at all, so the panel does not render. + * + * A widget confirming that nothing is wrong is something people learn to skip, + * and it takes sidebar space from the cards that do have something to say. */ - public function test_get_action_items_reports_only_passing_checks_when_healthy() { + public function test_get_action_items_is_empty_when_healthy() { wp_set_current_user( $this->make_user( 'administrator' ) ); delete_option( Client_Logger::FAILURES_OPTION ); @@ -1090,11 +1091,38 @@ public function test_get_action_items_reports_only_passing_checks_when_healthy() $items = Admin::get_instance()->get_action_items(); remove_filter( 'pre_option_active_plugins', $filter ); - $this->assertNotEmpty( $items ); + $this->assertSame( [], $items ); + } - foreach ( $items as $item ) { - $this->assertSame( 'success', $item['status'], $item['id'] . ' must pass on a healthy site.' ); - } + /** + * Turning logging off takes the whole panel down with it. + * + * The toggle is what feeds the fault counters, so once it is off whatever is + * still standing in them describes the site as it was, not as it is. Asserted + * with a fault recorded and a caching plugin active -- both surfaces the panel + * has -- so this cannot pass just because there was nothing to show. + */ + public function test_get_action_items_shows_nothing_when_logging_is_off() { + wp_set_current_user( $this->make_user( 'administrator' ) ); + delete_option( Client_Logger::FAILURES_OPTION ); + Helper::update_srfm_option( 'dismissed_action_items', [] ); + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + + update_option( 'srfm_general_settings_options', [ 'srfm_enable_logs' => false ] ); + + $filter = static function () { + return [ 'wp-rocket/wp-rocket.php' ]; + }; + + add_filter( 'pre_option_active_plugins', $filter ); + $items = Admin::get_instance()->get_action_items(); + $warnings = Admin::get_instance()->has_action_item_warnings(); + remove_filter( 'pre_option_active_plugins', $filter ); + + delete_option( 'srfm_general_settings_options' ); + + $this->assertSame( [], $items ); + $this->assertFalse( $warnings ); } /** From 0d71d99a355e0209eba09152efec61a027ec668e Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 09:36:40 +0530 Subject: [PATCH 02/58] fix: leave submissions by site editors out of the conversion rate Views are not counted for anyone who can edit the site (Form_Views::should_track()), but their entries were, so the two halves of the rate described different populations. Testing your own form five times added five to the numerator and nothing to the denominator: a form with 3 admin tests and 10 real visits reported 40% instead of 10%, or tripped the entries-exceed-views guard and rendered a dash on a form that was working perfectly well. The numerator now excludes the same people, from a get_users( capability => edit_posts ) list cached for the request. A logged-in subscriber stays counted on both halves, because their view is counted too. The Entries column is untouched and remains a true all-time count of every entry received. Two things this needed. NOT IN was not in the query builder's operator allowlist, so the clause would have been dropped silently. Added as a fallthrough on the existing IN case; no caller passed it before, since it would have been rejected, so nothing else changes. An empty list now emits the constant the empty set means rather than "col IN ()", which is a syntax error that fails the whole query. calculate_form_metrics() no longer takes an all-time entry count to reuse for forms created inside the tracking window. That count is unfiltered, so on a newly built form -- the one an admin has just been testing, which is exactly the case that skews -- it would have handed back the unexcluded number and undone the exclusion. It now takes only a form id, which also makes it impossible for the column and its sort order to be computed from different arguments. Also corrects a claim in that docblock: more entries than views was described as not possible in reality. An admin testing their own form is how it happened. --- inc/database/base.php | 13 ++- inc/forms-data.php | 158 +++++++++++++++++++--------- src/admin/settings/pages/General.js | 2 +- tests/unit/inc/test-forms-data.php | 122 ++++++++++++++++++--- 4 files changed, 232 insertions(+), 63 deletions(-) diff --git a/inc/database/base.php b/inc/database/base.php index 6a9f728a4..ddeb9d219 100644 --- a/inc/database/base.php +++ b/inc/database/base.php @@ -89,7 +89,7 @@ abstract class Base { * @var array * @since 1.8.0 */ - private $allowed_where_operators = [ 'LIKE', 'IN', '=', '!=', '>', '<', '>=', '<=' ]; + private $allowed_where_operators = [ 'LIKE', 'IN', 'NOT IN', '=', '!=', '>', '<', '>=', '<=' ]; /** * Init class. @@ -1053,6 +1053,7 @@ protected function cache_reset() { * } * * @since 1.1.1 -- Added support for "IN" compare. + * @since 2.12.6 -- Added support for "NOT IN" compare. * @since 0.0.13 * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided. */ @@ -1100,6 +1101,16 @@ protected function prepare_where_clauses( $where_clauses = [] ) { break; case 'IN': + case 'NOT IN': + // An empty list cannot be interpolated: "col IN ()" is a syntax + // error that fails the whole query, listing and COUNT alike. + // Emit the constant the empty set actually means instead, so + // neither operator can silently widen to match everything. + if ( ! is_array( $_value['value'] ) || [] === $_value['value'] ) { + $clause_parts[] = 'IN' === $_value['compare'] ? '1 = 0' : '1 = 1'; + break; + } + // Based on the number of values and datatype, it will create WHERE clause for $wpdb::prepare method. Eg: for ID with three values column: ID IN (%d, %d, %d). $datatype = $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ')'; diff --git a/inc/forms-data.php b/inc/forms-data.php index 618c1b04a..7da222146 100644 --- a/inc/forms-data.php +++ b/inc/forms-data.php @@ -25,6 +25,22 @@ class Forms_Data { use Get_Instance; + /** + * Cached ids of users who can edit the site, or null before the first lookup. + * + * A class property rather than a `static` inside the method so it can be reset, + * which the tests need: they create a user and then ask for the metrics inside + * one process. + * + * Deliberately per-request and never written to the object cache. A persistent + * cache would keep counting a newly promoted editor as a visitor until + * something invalidated it, and there is no natural invalidation point. + * + * @var array|null + * @since 2.12.6 + */ + private static $editing_user_ids = null; + /** * Constructor * @@ -348,20 +364,12 @@ static function ( $post ) { foreach ( $id_query->posts as $post_id ) { $form_id = Helper::get_integer_value( $post_id ); - // Same three arguments the render path passes. Calling this with only the - // form ID left $post_date_gmt empty, so strtotime() returned false, the - // "form is younger than the window" shortcut could never be taken, and the - // windowed COUNT ran for every row — both a second query per form and, for - // any entry whose created_at predates the form's post_date (an import, a - // migration, a restored backup), a different number than the column shows. - // The comment below promises order and display can never disagree; passing - // different arguments here is what made them disagree. - $post = get_post( $form_id ); - $metrics = $this->calculate_form_metrics( - $form_id, - $post->post_date_gmt ?? '', - Helper::get_integer_value( Entries::get_total_entries_by_status( 'all', $form_id ) ) - ); + // The form id is the whole input, here and on the render path, so the two + // cannot be handed different arguments and compute different numbers. + // They once could: this took a creation date and an all-time count as + // well, the two callers passed them differently, and the column and its + // sort order disagreed. + $metrics = $this->calculate_form_metrics( $form_id ); // Same helper the column renders from, so the order always matches the // numbers on screen. An unmeasurable rate sorts as -1 rather than 0, so @@ -443,6 +451,47 @@ private static function window_boundary_sql( $window_start ) { return gmdate( 'Y-m-d H:i:s', $window_start + $offset_seconds ); } + /** + * Ids of every user who can edit the site, cached for the request. + * + * The mirror of Form_Views::should_track()'s `user_can( $user_id, 'edit_posts' )` + * check, so the same people are absent from both halves of the rate. + * + * Cached because the forms listing asks once per row, and the answer cannot + * change within a request. + * + * Approximates the view gate rather than matching it exactly: this reads roles + * and capability meta, while `user_can()` also honours a `user_has_cap` filter, + * so a capability granted at runtime by a membership plugin is excluded from + * views but still counted here. Close enough for a percentage, and the + * alternative is user_can() per entry. + * + * @since 2.12.6 + * @return array + */ + private static function get_editing_user_ids() { + if ( null !== self::$editing_user_ids ) { + return self::$editing_user_ids; + } + + // ponytail: NOT IN over this list is fine for the handful of editors a + // typical site has; a site with thousands of authors wants a user_id > 0 + // join instead. + self::$editing_user_ids = array_map( + 'intval', + Helper::get_array_value( + get_users( + [ + 'capability' => 'edit_posts', + 'fields' => 'ID', + ] + ) + ) + ); + + return self::$editing_user_ids; + } + /** * Views and conversion rate for one form. * @@ -452,20 +501,25 @@ private static function window_boundary_sql( $window_start ) { * rendering a dash sorted as though its rate were several hundred percent. * Anything needing these numbers must come through here. * + * Both halves exclude the same people. Views are not counted for anyone who + * can edit the site (Form_Views::should_track()), so their submissions must not + * be counted either: testing your own form five times would otherwise add five + * to the numerator and nothing to the denominator, and report a rate several + * times the real one. The Entries column is unaffected and stays a true + * all-time count of every entry received. + * * Returns `null` for the rate rather than a number whenever it cannot be - * measured — tracking off, window never opened, no views yet, or more entries - * than views. That last case is not possible in reality (every entry needs a - * view first), so it means the view count is incomplete and any percentage - * would be invented; the table renders the dash instead. `0.0` is reserved for - * a real measurement of zero. + * measured: tracking off, window never opened, no views yet, or more entries + * than views. That last case should not arise once both halves exclude the same + * people, so it means the view count is incomplete and any percentage would be + * invented; the table renders the dash instead. `0.0` is reserved for a real + * measurement of zero. * - * @param int $form_id Form post ID. - * @param string $post_date_gmt Form creation date, GMT. Used to skip a redundant count. - * @param int $entries_all_time All-time entry count, when the caller already has it. + * @param int $form_id Form post ID. * @return array{views:int,conversion_rate:float|null} * @since 2.12.6 */ - private function calculate_form_metrics( $form_id, $post_date_gmt = '', $entries_all_time = null ) { + private function calculate_form_metrics( $form_id ) { $none = [ 'views' => 0, 'conversion_rate' => null, @@ -491,33 +545,39 @@ private function calculate_form_metrics( $form_id, $post_date_gmt = '', $entries // Compare like with like. The Entries column is all-time, but views only start // accruing when tracking opens, so the rate counts entries from that same - // moment — otherwise a form that existed beforehand divides years of entries by - // days of views and reports a rate that is pure noise. - $form_created = strtotime( (string) $post_date_gmt ); - - if ( null !== $entries_all_time && $form_created && $form_created >= $window_start ) { - // The form is younger than the window, so every entry it has is already - // inside the window and the caller's all-time count is the same number. - // Skips a second COUNT per row on the listing. - $entries_since = Helper::get_integer_value( $entries_all_time ); - } else { - $entries_since = Helper::get_integer_value( - Entries::get_total_entries_by_status( - 'all', - $form_id, - [ - [ - [ - 'key' => 'created_at', - 'compare' => '>=', - 'value' => self::window_boundary_sql( $window_start ), - ], - ], - ] - ) - ); + // moment. Otherwise a form that existed beforehand divides years of entries + // by days of views and reports a rate that is pure noise. + $where = [ + [ + [ + 'key' => 'created_at', + 'compare' => '>=', + 'value' => self::window_boundary_sql( $window_start ), + ], + ], + ]; + + $editing_users = self::get_editing_user_ids(); + + if ( [] !== $editing_users ) { + $where[] = [ + [ + 'key' => 'user_id', + 'compare' => 'NOT IN', + 'value' => $editing_users, + ], + ]; } + // Always counted, never taken from the caller's all-time total. That total + // includes the entries this exclusion exists to drop, so reusing it for a + // form created inside the window -- the newly built form an admin has just + // been testing, which is exactly the case that skews -- would hand back the + // unfiltered number and quietly undo the exclusion. + $entries_since = Helper::get_integer_value( + Entries::get_total_entries_by_status( 'all', $form_id, $where ) + ); + if ( $entries_since > $views ) { return [ 'views' => $views, @@ -546,7 +606,7 @@ private function prepare_form_for_listing( $post ) { // Views and conversion rate come from the same helper the sort path uses, so the // column can never order by a different number than it displays. - $metrics = $this->calculate_form_metrics( $form_id, $post->post_date_gmt, $entries_count ); + $metrics = $this->calculate_form_metrics( $form_id ); $views = $metrics['views']; $conversion_rate = $metrics['conversion_rate']; diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index d333ea15c..a06027831 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -234,7 +234,7 @@ const FormViewsTrackingContent = ( { 'sureforms' ), description: __( - 'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns — counting continues, so the figures are up to date if you switch it back on.', + 'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Views and submissions by anyone who can edit the site, including you, are not counted, so testing your own forms does not change the figures. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns. Counting keeps running, so the figures are up to date if you switch it back on.', 'sureforms' ), } } diff --git a/tests/unit/inc/test-forms-data.php b/tests/unit/inc/test-forms-data.php index 8cf013b80..6bf9d288e 100644 --- a/tests/unit/inc/test-forms-data.php +++ b/tests/unit/inc/test-forms-data.php @@ -8,6 +8,7 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase; use SRFM\Inc\Forms_Data; use SRFM\Inc\Helper; +use SRFM\Inc\Database\Tables\Entries; class Test_Forms_Data extends TestCase { @@ -65,6 +66,19 @@ public function test_register_custom_endpoint() { /** * Helper: create an admin user, set as current, and return the user ID. */ + /** + * Create a user with the given role. Not set as the current user. + */ + private function make_user( string $role ): int { + return (int) wp_insert_user( + [ + 'user_login' => 'srfm_' . $role . '_' . wp_generate_password( 6, false ), + 'user_pass' => 'password', + 'role' => $role, + ] + ); + } + private function set_admin_user(): int { $user_id = wp_insert_user( [ @@ -244,37 +258,121 @@ public function test_calculate_form_metrics() { $form_id = $this->make_tracked_form( 10 ); // Tracking off → no numbers at all, and no entry query. - $off = $this->call_private( 'calculate_form_metrics', [ $form_id, '', null ] ); + $off = $this->call_private( 'calculate_form_metrics', [ $form_id ] ); $this->assertSame( 0, $off['views'], 'Views must not be reported while the feature is off.' ); $this->assertNull( $off['conversion_rate'], 'No rate while the feature is off.' ); // Enable → window opens, views are reported. update_option( 'srfm_general_settings_options', [ 'srfm_form_views_tracking' => true ] ); - $on = $this->call_private( 'calculate_form_metrics', [ $form_id, '', null ] ); + $on = $this->call_private( 'calculate_form_metrics', [ $form_id ] ); $this->assertSame( 10, $on['views'], 'Views should be reported once enabled.' ); $this->assertSame( 0.0, $on['conversion_rate'], 'No entries yet is a real 0%, not a dash.' ); // A form with no views cannot have a rate — 0 views is not 0%. $unseen = $this->make_tracked_form( 0 ); - $none = $this->call_private( 'calculate_form_metrics', [ $unseen, '', null ] ); + $none = $this->call_private( 'calculate_form_metrics', [ $unseen ] ); $this->assertSame( 0, $none['views'] ); $this->assertNull( $none['conversion_rate'], 'Zero views must give a dash, not 0%.' ); - // More entries than views is impossible, so the count is incomplete and any - // percentage would be invented. The caller's all-time count is used directly - // when the form is younger than the window, which is the cheap path. - $young = $this->make_tracked_form( 2, gmdate( 'Y-m-d H:i:s' ) ); - $over = $this->call_private( 'calculate_form_metrics', [ $young, gmdate( 'Y-m-d H:i:s' ), 50 ] ); + // More entries than views means the view count is incomplete, so any + // percentage would be invented. + $over_form = $this->make_tracked_form( 2 ); + for ( $i = 0; $i < 3; $i++ ) { + Entries::add( + [ + 'form_id' => $over_form, + 'form_data' => [], + ] + ); + } + $over = $this->call_private( 'calculate_form_metrics', [ $over_form ] ); $this->assertSame( 2, $over['views'] ); $this->assertNull( $over['conversion_rate'], 'Entries exceeding views must render as a dash, never >100%.' ); - // Normal case on the cheap path: 1 entry, 2 views → 50%. - $half = $this->call_private( 'calculate_form_metrics', [ $young, gmdate( 'Y-m-d H:i:s' ), 1 ] ); + // Normal case: 1 entry, 2 views → 50%. + $half_form = $this->make_tracked_form( 2 ); + Entries::add( + [ + 'form_id' => $half_form, + 'form_data' => [], + ] + ); + $half = $this->call_private( 'calculate_form_metrics', [ $half_form ] ); $this->assertSame( 50.0, $half['conversion_rate'] ); wp_delete_post( $form_id, true ); wp_delete_post( $unseen, true ); - wp_delete_post( $young, true ); + wp_delete_post( $over_form, true ); + wp_delete_post( $half_form, true ); + delete_option( 'srfm_general_settings_options' ); + delete_option( \SRFM\Inc\Form_Views::TRACKING_STARTED_OPTION ); + } + + /** + * Entries submitted by someone who can edit the site are left out of the rate. + * + * Views already exclude those people (Form_Views::should_track()), so counting + * their submissions adds to the numerator and nothing to the denominator. An + * admin testing a new form five times would report a rate several times the + * real one, or trip the entries-exceed-views guard and render a dash on a form + * that is working perfectly well. + */ + public function test_calculate_form_metrics_excludes_entries_from_users_who_can_edit() { + delete_option( \SRFM\Inc\Form_Views::TRACKING_STARTED_OPTION ); + update_option( 'srfm_general_settings_options', [ 'srfm_form_views_tracking' => true ] ); + + $editor = $this->make_user( 'editor' ); + $subscriber = $this->make_user( 'subscriber' ); + + // The list is cached per request, and these users were created after + // earlier tests in this class already warmed it. + $cache = new ReflectionProperty( Forms_Data::class, 'editing_user_ids' ); + $cache->setAccessible( true ); + $cache->setValue( null, null ); + + $form_id = $this->make_tracked_form( 10 ); + + // Four entries, three of which must not count: the admin and editor can edit + // the site, so their views were never recorded either. + foreach ( [ 1, $editor, $editor, 0 ] as $user_id ) { + Entries::add( + [ + 'form_id' => $form_id, + 'user_id' => $user_id, + 'form_data' => [], + ] + ); + } + + $metrics = $this->call_private( 'calculate_form_metrics', [ $form_id ] ); + + $this->assertSame( 10, $metrics['views'] ); + $this->assertSame( + 10.0, + $metrics['conversion_rate'], + 'Only the logged-out entry counts: 1 of 10 views is 10%, not 40%.' + ); + + // A logged-in visitor who cannot edit is a real conversion, and their view + // was counted, so their entry must be counted too. + Entries::add( + [ + 'form_id' => $form_id, + 'user_id' => $subscriber, + 'form_data' => [], + ] + ); + + $with_subscriber = $this->call_private( 'calculate_form_metrics', [ $form_id ] ); + $this->assertSame( + 20.0, + $with_subscriber['conversion_rate'], + 'A subscriber is a genuine visitor on both halves of the rate.' + ); + + wp_delete_post( $form_id, true ); + wp_delete_user( $editor ); + wp_delete_user( $subscriber ); delete_option( 'srfm_general_settings_options' ); delete_option( \SRFM\Inc\Form_Views::TRACKING_STARTED_OPTION ); } @@ -288,7 +386,7 @@ public function test_prepare_form_for_listing() { $form_id = $this->make_tracked_form( 7 ); $row = $this->call_private( 'prepare_form_for_listing', [ get_post( $form_id ) ] ); - $metrics = $this->call_private( 'calculate_form_metrics', [ $form_id, get_post( $form_id )->post_date_gmt, $row['entries_count'] ] ); + $metrics = $this->call_private( 'calculate_form_metrics', [ $form_id ] ); $this->assertSame( $metrics['views'], $row['views'], 'The row must show the calculated views.' ); $this->assertSame( $metrics['conversion_rate'], $row['conversion_rate'], 'The row must show the calculated rate.' ); From c799ec37f4e15d9283f5ee7ea52cbfa1cbe6c499 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 09:53:41 +0530 Subject: [PATCH 03/58] test: cover NOT IN, the empty-list guard, and cache_reset Direct coverage for the query-builder change: NOT IN survives the operator allowlist, and an empty list collapses to the constant the empty set means rather than emitting "col IN ()". The first of those is the one that matters. Without NOT IN on the allowlist the condition is dropped silently and the query matches every row, so an exclusion built on it fails open with no error anywhere. test_cache_reset() is what the coverage gate actually asked for. It attributes each changed line to the last public/protected function declared above it, and the @since tag added to prepare_where_clauses' docblock sits above that function's own declaration, so it was booked against cache_reset(). The method genuinely had no test, so it has one now rather than a bypass label. --- tests/unit/inc/database/test-base.php | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/unit/inc/database/test-base.php b/tests/unit/inc/database/test-base.php index 4cc5ab807..d95d01837 100644 --- a/tests/unit/inc/database/test-base.php +++ b/tests/unit/inc/database/test-base.php @@ -394,6 +394,95 @@ public function test_prepare_where_clauses_in_operator() { $this->assertStringContainsString( '3', $result ); } + public function test_prepare_where_clauses_not_in_operator() { + $result = $this->prepare( + [ + [ + [ + 'key' => 'user_id', + 'compare' => 'NOT IN', + 'value' => [ 4, 5 ], + ], + ], + ] + ); + + // Without 'NOT IN' on the operator allowlist the whole condition is dropped + // and the query silently matches every row, which is how an exclusion built + // on this would fail open. + $this->assertStringContainsString( 'NOT IN', $result ); + $this->assertStringContainsString( 'user_id', $result ); + $this->assertStringContainsString( '4', $result ); + $this->assertStringContainsString( '5', $result ); + } + + /** + * An empty list must not be interpolated into "col IN ()". + * + * That is a syntax error, and it fails the whole query rather than the one + * condition, taking out the listing and its COUNT together. Each operator + * collapses to the constant the empty set actually means, so neither can widen + * to match everything. + */ + public function test_prepare_where_clauses_empty_list_does_not_emit_invalid_sql() { + $in = $this->prepare( + [ + [ + [ + 'key' => 'ID', + 'compare' => 'IN', + 'value' => [], + ], + ], + ] + ); + + $this->assertStringNotContainsString( 'IN ()', $in ); + $this->assertStringContainsString( '1 = 0', $in, 'An empty IN matches nothing.' ); + + $not_in = $this->prepare( + [ + [ + [ + 'key' => 'ID', + 'compare' => 'NOT IN', + 'value' => [], + ], + ], + ] + ); + + $this->assertStringNotContainsString( 'IN ()', $not_in ); + $this->assertStringContainsString( '1 = 1', $not_in, 'An empty NOT IN excludes nothing.' ); + } + + // --------------------------------------------------------------- + // cache_reset + // --------------------------------------------------------------- + + /** + * cache_reset() empties the per-instance query cache. + */ + public function test_cache_reset() { + $set = new ReflectionMethod( $this->entries_table, 'cache_set' ); + $get = new ReflectionMethod( $this->entries_table, 'cache_get' ); + $reset = new ReflectionMethod( $this->entries_table, 'cache_reset' ); + + foreach ( [ $set, $get, $reset ] as $method ) { + $method->setAccessible( true ); + } + + $set->invoke( $this->entries_table, 'srfm_cache_reset_probe', 'stored' ); + $this->assertSame( 'stored', $get->invoke( $this->entries_table, 'srfm_cache_reset_probe' ) ); + + $reset->invoke( $this->entries_table ); + + $this->assertNull( + $get->invoke( $this->entries_table, 'srfm_cache_reset_probe' ), + 'The cached value must be gone after a reset.' + ); + } + // --------------------------------------------------------------- // prepare_where_clauses — date range // --------------------------------------------------------------- From 9d8335445ee355a0660025942bb56fc7d99a7608 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 09:58:44 +0530 Subject: [PATCH 04/58] docs: say which logged-in visitors the rate does count The help text only named who is left out, which reads as though being logged in is itself disqualifying. Subscribers and customers are counted on both halves, and on a membership or WooCommerce site that is most of the audience, so the omission was the misleading half. Verified against the gate rather than assumed: administrator, editor, author and contributor all have edit_posts and are excluded; subscriber and customer do not and are counted. --- src/admin/settings/pages/General.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index a06027831..7a7b0871a 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -234,7 +234,7 @@ const FormViewsTrackingContent = ( { 'sureforms' ), description: __( - 'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Views and submissions by anyone who can edit the site, including you, are not counted, so testing your own forms does not change the figures. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns. Counting keeps running, so the figures are up to date if you switch it back on.', + 'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Logged-out visitors are counted, and so are logged-in users who cannot edit the site, such as subscribers and customers. Views and submissions by anyone who can edit the site, including you, are left out, so testing your own forms does not change the figures. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns. Counting keeps running, so the figures are up to date if you switch it back on.', 'sureforms' ), } } From 97ab2fbb8fae20b2ac20aede0a0c5c095272a1cc Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 10:18:17 +0530 Subject: [PATCH 05/58] feat: set the analytics and logs settings apart with a rule Everything above the rule configures how forms behave. Anonymous Analytics and Logs are about SureForms itself -- what it may report back, and what it records for support -- so reading them as more form settings is the wrong frame. A single rule is enough to say that without adding a heading or a second panel. Uses the border utilities the rest of the admin uses rather than a new component: force-ui ships no separator, and Navigation.js and ImportResult.js already draw rules this way. The border-0 and border-solid resets are load-bearing in wp-admin, which zeroes border styles out from under Tailwind. --- src/admin/settings/pages/General.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index d333ea15c..da9929441 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -524,6 +524,11 @@ const GeneralPage = ( { /> } /> + { /* Everything above configures how forms behave. The two below are + about SureForms itself -- what it may report back, and what it + records for support -- so they are set apart rather than read as + more form settings. */ } +
Date: Thu, 3 Sep 2026 10:29:06 +0530 Subject: [PATCH 06/58] feat: put Anonymous Analytics last, below Logs Analytics is the only setting on the page that sends anything outward, and the least likely to be looked for, so it sits at the bottom. Logs moves up next to the rule, where someone sent here by support will find it without scrolling past an opt-in they did not come for. Order only. Both sections are unchanged. --- src/admin/settings/pages/General.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index da9929441..2aee04c3d 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -525,29 +525,30 @@ const GeneralPage = ( { } /> { /* Everything above configures how forms behave. The two below are - about SureForms itself -- what it may report back, and what it - records for support -- so they are set apart rather than read as - more form settings. */ } + about SureForms itself -- what it records for support, and what + it may report back -- so they are set apart rather than read as + more form settings. Analytics sits last: it is the only one that + sends anything outward, and the least likely to be looked for. */ }
} /> } /> From 253170d49ab811a037da3cf0df62d2d949723985 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 11:00:39 +0530 Subject: [PATCH 07/58] feat: move the rule down so it sets off Analytics alone The rule sat above Logs, which grouped Logs with Analytics and implied the two belong together. They do not: Logs is something SureForms does for this site, and Analytics is the one setting that sends anything outward. The rule now sits directly above Analytics and separates only that. --- src/admin/settings/pages/General.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index 2aee04c3d..e022e031b 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -524,12 +524,6 @@ const GeneralPage = ( { /> } /> - { /* Everything above configures how forms behave. The two below are - about SureForms itself -- what it records for support, and what - it may report back -- so they are set apart rather than read as - more form settings. Analytics sits last: it is the only one that - sends anything outward, and the least likely to be looked for. */ } -
} /> + { /* Everything above is something SureForms does for this site. + Analytics is the one setting that sends anything outward, so it + sits last, behind a rule, rather than reading as one more form + setting. */ } +
Date: Thu, 3 Sep 2026 17:27:03 +0530 Subject: [PATCH 08/58] Fix: stop silently dropping submitted fields when block_id drifts (#1517643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit strip_unknown_field_keys() (added 2.12.3) removes any submitted `-lbl-` key whose block_id is not in the allowlist walked from the form's CURRENT post_content. But the block_id in the HTML a visitor actually submitted can differ from the current one — full-page caching (LiteSpeed) serving a stale render, or an editor rebuild reassigning block_ids. The result was silent, unrecoverable loss of real submission data: the keys were absent from the entry, no error, success message still shown. 2.12.2 has no such step, which is why rolling back fixed it. Fix: match on block_id OR slug. The slug is the stable field identifier (unlike block_id it is not regenerated on rebuild and is unaffected by a stale cached render), so a legitimately-submitted field whose block_id drifted now survives on its slug. A key with neither a known block_id nor a known slug is still foreign and dropped, so the anti-injection guard the function exists for is preserved. - Collect field slugs alongside block_ids in one walk (walk_field_identifiers). - Add get_known_field_slugs() + srfm_known_field_slugs filter, mirroring the block-id getter. - Keep a submitted key when its block_id OR slug matches; only strip when both are foreign. Applies to repeater child keys too. Regression tests: a drifted-block_id-but-known-slug field survives (revert-tested: fails without the fix), and a fully-foreign key is still dropped. --- inc/field-validation.php | 162 +++++++++++++++++++---- tests/unit/inc/test-field-validation.php | 51 +++++++ 2 files changed, 190 insertions(+), 23 deletions(-) diff --git a/inc/field-validation.php b/inc/field-validation.php index e9d0729d7..b62986775 100644 --- a/inc/field-validation.php +++ b/inc/field-validation.php @@ -228,15 +228,7 @@ public static function get_known_field_block_ids( $form_id ) { // rest of the request, and because the lookup short-circuits on isset() the walk // would never be retried. if ( ! isset( $cache[ $form_id ] ) ) { - $ids = []; - $post = get_post( $form_id ); - - if ( $post instanceof \WP_Post && ! empty( $post->post_content ) && function_exists( 'parse_blocks' ) ) { - $visited = []; - self::collect_field_block_ids( parse_blocks( $post->post_content ), $ids, $visited ); - } - - $cache[ $form_id ] = $ids; + $cache[ $form_id ] = self::walk_field_identifiers( $form_id ); } /** @@ -253,19 +245,82 @@ public static function get_known_field_block_ids( $form_id ) { * @param array $ids Map of known block id => true. * @param int $form_id The form post id. */ - $ids = apply_filters( 'srfm_known_field_block_ids', $cache[ $form_id ], $form_id ); + $ids = apply_filters( 'srfm_known_field_block_ids', $cache[ $form_id ]['ids'], $form_id ); // Coerce defensively. A callback returning a list (`[ 'aaa', 'bbb' ]`) rather // than a map would otherwise make every real field look unknown, and a non-array // return would drop the whole allowlist — so fall back to the walked set instead // of silently turning the check off. if ( ! is_array( $ids ) ) { - return $cache[ $form_id ]; + return $cache[ $form_id ]['ids']; } return wp_is_numeric_array( $ids ) ? array_fill_keys( array_map( 'strval', $ids ), true ) : $ids; } + /** + * The slugs of every SureForms field the form defines, block_id => true style. + * + * A slug is stable across renders where a block_id is not, so it is the identifier + * used to keep a submitted field whose block_id drifted (full-page cache, an editor + * rebuild). Mirrors get_known_field_block_ids(): one memoised walk, an unmemoised + * filter pass. + * + * @param int|mixed $form_id The form post id. + * @since 2.12.7 + * @return array Map of known slug => true. + */ + public static function get_known_field_slugs( $form_id ) { + static $cache = []; + + $form_id = Helper::get_integer_value( $form_id ); + if ( $form_id <= 0 ) { + return []; + } + + if ( ! isset( $cache[ $form_id ] ) ) { + $cache[ $form_id ] = self::walk_field_identifiers( $form_id ); + } + + /** + * Filter the set of field slugs considered valid for a form during submission. + * + * @since 2.12.7 + * @param array $slugs Map of known slug => true. + * @param int $form_id The form post id. + */ + $slugs = apply_filters( 'srfm_known_field_slugs', $cache[ $form_id ]['slugs'], $form_id ); + + if ( ! is_array( $slugs ) ) { + return $cache[ $form_id ]['slugs']; + } + + return wp_is_numeric_array( $slugs ) ? array_fill_keys( array_map( 'strval', $slugs ), true ) : $slugs; + } + + /** + * Walk a form's blocks once, returning both the block-id and slug allowlists. + * + * @param int $form_id The form post id. + * @since 2.12.7 + * @return array{ids:array,slugs:array} + */ + private static function walk_field_identifiers( $form_id ) { + $ids = []; + $slugs = []; + $post = get_post( $form_id ); + + if ( $post instanceof \WP_Post && ! empty( $post->post_content ) && function_exists( 'parse_blocks' ) ) { + $visited = []; + self::collect_field_block_ids( parse_blocks( $post->post_content ), $ids, $slugs, $visited ); + } + + return [ + 'ids' => $ids, + 'slugs' => $slugs, + ]; + } + /** * Remove submitted field keys the form does not define. * @@ -297,12 +352,14 @@ public static function strip_unknown_field_keys( $form_data, $form_id ) { return []; } - $known_block_ids = self::get_known_field_block_ids( Helper::get_integer_value( $form_id ) ); + $form_id_int = Helper::get_integer_value( $form_id ); + $known_block_ids = self::get_known_field_block_ids( $form_id_int ); + $known_slugs = self::get_known_field_slugs( $form_id_int ); - // Fail open. The set is empty only when the form's blocks could not be derived + // Fail open. The sets are empty only when the form's blocks could not be derived // (no/empty post_content, a parse failure, or a structure this walk does not // recognise). Enforcing on an empty set would strip every field. - if ( empty( $known_block_ids ) ) { + if ( empty( $known_block_ids ) && empty( $known_slugs ) ) { return $form_data; } @@ -311,20 +368,68 @@ public static function strip_unknown_field_keys( $form_data, $form_id ) { continue; } - if ( ! isset( $known_block_ids[ Helper::get_block_id_from_key( $key ) ] ) ) { + // Keep the field when EITHER its block_id OR its slug matches the form. The + // block_id can drift between the (possibly full-page-cached) HTML the visitor + // submitted and the current post_content; the slug does not, so requiring only + // a block_id match silently deleted real submissions (#1517643). A key with + // neither a known block_id nor a known slug is genuinely foreign and dropped. + if ( ! self::field_key_belongs_to_form( $key, $known_block_ids, $known_slugs ) ) { unset( $form_data[ $key ] ); continue; } // A known key whose value is an array is a repeater: walk its rows. if ( is_array( $value ) ) { - $form_data[ $key ] = self::strip_unknown_repeater_keys( $value, $known_block_ids ); + $form_data[ $key ] = self::strip_unknown_repeater_keys( $value, $known_block_ids, $known_slugs ); } } return $form_data; } + /** + * Whether a submitted field key belongs to the form, by block_id or by slug. + * + * @param string $key Submitted field key. + * @param array $known_block_ids Allowlisted block ids. + * @param array $known_slugs Allowlisted field slugs. + * @since 2.12.7 + * @return bool + */ + private static function field_key_belongs_to_form( $key, $known_block_ids, $known_slugs ) { + if ( isset( $known_block_ids[ Helper::get_block_id_from_key( $key ) ] ) ) { + return true; + } + + $slug = self::get_slug_from_key( $key ); + + return '' !== $slug && isset( $known_slugs[ $slug ] ); + } + + /** + * Extract a field's slug from its submitted key. + * + * A key is `srfm---lbl--`. Helper::encode() is + * padding-stripped standard base64 and never contains a hyphen, so the slug is + * everything after the first hyphen that follows `-lbl-` (the slug itself may + * contain hyphens, which is why only the first is used as the boundary). + * + * @param string $key Submitted field key. + * @since 2.12.7 + * @return string The slug, or '' when it cannot be derived. + */ + private static function get_slug_from_key( $key ) { + if ( ! is_string( $key ) || false === strpos( $key, '-lbl-' ) ) { + return ''; + } + + $parts = explode( '-lbl-', $key ); + $after = isset( $parts[1] ) ? $parts[1] : ''; + $pos = strpos( $after, '-' ); + + return false === $pos ? '' : substr( $after, $pos + 1 ); + } + /** * Validate form data for a given form. * @@ -499,10 +604,11 @@ public static function get_email_char_limits() { * * @param array $rows The repeater's submitted rows. * @param array $known_block_ids Map of block ids belonging to the form. + * @param array $known_slugs Map of field slugs belonging to the form. * @since 2.12.3 * @return array The rows with unknown child keys removed. */ - private static function strip_unknown_repeater_keys( $rows, $known_block_ids ) { + private static function strip_unknown_repeater_keys( $rows, $known_block_ids, $known_slugs = [] ) { foreach ( $rows as $index => $row ) { if ( ! is_array( $row ) ) { continue; @@ -513,7 +619,7 @@ private static function strip_unknown_repeater_keys( $rows, $known_block_ids ) { continue; } - if ( ! isset( $known_block_ids[ Helper::get_block_id_from_key( $child_key ) ] ) ) { + if ( ! self::field_key_belongs_to_form( $child_key, $known_block_ids, $known_slugs ) ) { unset( $row[ $child_key ] ); } } @@ -529,12 +635,13 @@ private static function strip_unknown_repeater_keys( $rows, $known_block_ids ) { * * @param array $blocks Parsed blocks from parse_blocks(). * @param array $ids Accumulator of block id => true (by reference). + * @param array $slugs Accumulator of field slug => true (by reference). * @param array $visited Expanded reusable-block post ids, guards cycles. * @param int $depth Current recursion depth, guards pathological trees. * @since 2.12.3 * @return void */ - private static function collect_field_block_ids( $blocks, &$ids, &$visited, $depth = 0 ) { + private static function collect_field_block_ids( $blocks, &$ids, &$slugs, &$visited, $depth = 0 ) { if ( ! is_array( $blocks ) || $depth > 50 ) { return; } @@ -553,8 +660,17 @@ private static function collect_field_block_ids( $blocks, &$ids, &$visited, $dep // alters under a different string than the one looked up, making a legitimate // field permanently unsubmittable. These are map keys used for comparison // only; nothing is echoed from here. - if ( 0 === strpos( $block_name, 'srfm/' ) && ! empty( $attrs['block_id'] ) && is_string( $attrs['block_id'] ) ) { - $ids[ $attrs['block_id'] ] = true; + if ( 0 === strpos( $block_name, 'srfm/' ) ) { + if ( ! empty( $attrs['block_id'] ) && is_string( $attrs['block_id'] ) ) { + $ids[ $attrs['block_id'] ] = true; + } + // Also index by slug. The block_id is rebuilt when the editor recreates a + // field and can differ between the cached HTML a visitor submitted and the + // current post_content, but the slug is the stable field identifier — so a + // slug match keeps a legitimately-submitted field whose block_id drifted. + if ( ! empty( $attrs['slug'] ) && is_string( $attrs['slug'] ) ) { + $slugs[ $attrs['slug'] ] = true; + } } // Expand reusable/synced patterns so fields living inside a pattern count as @@ -565,13 +681,13 @@ private static function collect_field_block_ids( $blocks, &$ids, &$visited, $dep $visited[ $ref ] = true; $ref_post = get_post( $ref ); if ( $ref_post instanceof \WP_Post && 'wp_block' === $ref_post->post_type && '' !== $ref_post->post_content ) { - self::collect_field_block_ids( parse_blocks( $ref_post->post_content ), $ids, $visited, $depth + 1 ); + self::collect_field_block_ids( parse_blocks( $ref_post->post_content ), $ids, $slugs, $visited, $depth + 1 ); } } } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { - self::collect_field_block_ids( $block['innerBlocks'], $ids, $visited, $depth + 1 ); + self::collect_field_block_ids( $block['innerBlocks'], $ids, $slugs, $visited, $depth + 1 ); } } } diff --git a/tests/unit/inc/test-field-validation.php b/tests/unit/inc/test-field-validation.php index 3ff0aea00..62683ed20 100644 --- a/tests/unit/inc/test-field-validation.php +++ b/tests/unit/inc/test-field-validation.php @@ -579,6 +579,57 @@ public function test_strip_unknown_field_keys_drops_invented_keys() { wp_delete_post( $form_id, true ); } + /** + * REGRESSION (#1517643): a field whose block_id drifted between the rendered HTML + * the visitor submitted and the current post_content — the systematic case on a + * full-page-cached site, or after an editor rebuild — must survive on its slug. + * + * Matching on block_id alone silently deleted real submission data (keys absent, + * success message shown). The slug is the stable field identifier, so a slug match + * keeps the field. + */ + public function test_strip_unknown_field_keys_keeps_a_drifted_block_id_via_slug() { + $form_id = wp_insert_post( + [ + 'post_type' => 'sureforms_form', + 'post_status' => 'publish', + 'post_title' => 'Drift Form', + 'post_content' => '', + ] + ); + + // Different block_id (drift), same stable slug 'finish-time'. + $drifted = 'srfm-input-cachedid-lbl-RmluaXNo-finish-time'; + + $kept = Field_Validation::strip_unknown_field_keys( [ $drifted => 'value', 'form-id' => $form_id ], $form_id ); + + $this->assertArrayHasKey( $drifted, $kept, 'a field whose block_id drifted must survive on its slug' ); + + wp_delete_post( $form_id, true ); + } + + /** + * The slug fallback must not weaken the anti-injection guard: a key with neither a + * known block_id nor a known slug is still foreign and dropped. + */ + public function test_strip_unknown_field_keys_still_drops_foreign_block_id_and_slug() { + $form_id = wp_insert_post( + [ + 'post_type' => 'sureforms_form', + 'post_status' => 'publish', + 'post_title' => 'Drift Form 2', + 'post_content' => '', + ] + ); + + $foreign = 'srfm-input-deadbeef-lbl-R2hvc3Q-ghost'; + $stripped = Field_Validation::strip_unknown_field_keys( [ $foreign => 'x', 'form-id' => $form_id ], $form_id ); + + $this->assertArrayNotHasKey( $foreign, $stripped, 'a key with neither a known block_id nor a known slug is dropped' ); + + wp_delete_post( $form_id, true ); + } + /** * REGRESSION (#2993, review H1): repeater child keys must be validated too. * From f8a67ce641035334daf57610ffc3437094223f51 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 17:29:03 +0530 Subject: [PATCH 09/58] test: cover get_known_field_slugs() for the coverage gate --- tests/unit/inc/test-field-validation.php | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/inc/test-field-validation.php b/tests/unit/inc/test-field-validation.php index 62683ed20..747e4f062 100644 --- a/tests/unit/inc/test-field-validation.php +++ b/tests/unit/inc/test-field-validation.php @@ -535,6 +535,32 @@ public function test_strip_unknown_field_keys_keeps_non_alphanumeric_block_id() wp_delete_post( $form_id, true ); } + /** + * get_known_field_slugs() collects the slug of every SureForms field in the form, + * recursing into innerBlocks, so a submitted field can be matched by its stable + * slug when its block_id has drifted. + */ + public function test_get_known_field_slugs_collects_field_slugs() { + $form_id = wp_insert_post( + [ + 'post_type' => 'sureforms_form', + 'post_status' => 'publish', + 'post_title' => 'Slug Set Form', + 'post_content' => '' + . '', + ] + ); + + $slugs = Field_Validation::get_known_field_slugs( $form_id ); + + $this->assertArrayHasKey( 'finish-time', $slugs ); + $this->assertArrayHasKey( 'result-url', $slugs ); + $this->assertArrayNotHasKey( 'not-a-field', $slugs ); + $this->assertSame( [], Field_Validation::get_known_field_slugs( 0 ), 'invalid form id yields an empty set' ); + + wp_delete_post( $form_id, true ); + } + /** * Invalid form ids return an empty set (callers fail open). */ From 7ed0443172d05562bb78afa3e8993de65c2f065b Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 17:41:19 +0530 Subject: [PATCH 10/58] fix: send people to the right documentation, and support the right ticket Three related changes to what the failure notices point at. They share admin/admin.php and its test file, so they land together. Caching doc per plugin. Six of the twenty recognised caching plugins have a setup guide of their own, and the notice pointed everyone at the general page, so a site running WP Rocket got advice it had to translate first. Name and doc slug now live in one array rather than two keyed on each other, because kept apart they drift and the failure is silent: the link keeps working, it just degrades to the general page. Anything without its own guide, and a site where detection finds nothing, still lands on the general one rather than an empty href. A guide for notification failures. Email is the one failure here a site owner can usually fix without us, since it is almost always SMTP not being configured, so waiting on a support reply is the wrong default. The notice now offers the email troubleshooting guide next to Contact Support. It is optional keys rather than a new item shape: absent guide_label/guide_url render nothing, so the other categories need no branch in either renderer and neither does anything contributed through srfm_action_items. Contact Support keeps its acknowledgement behaviour -- reading a guide does not retire the notice, because nothing is fixed yet. The support email described the wrong problem. Subject and opening line were hardcoded to submissions, so a site whose notification email was broken raised a ticket titled "form submissions are failing" and got routed to the wrong queue. The count was wrong too: get_support_message() read get_fault_streak(), which returns the submission count only, so a notification failure quoted a number from a different counter, usually zero. Both now come from the category, out of one array so they cannot drift apart, and the form title goes in the body since support asks for it first. An unknown or absent category gets neutral wording rather than the submission copy, which would state something that may not be true. All eight documentation URLs verified live (HTTP 200) rather than inferred from the slug pattern. --- admin/admin.php | 171 ++++++++++++++++++++++----- inc/helper.php | 122 ++++++++++++++------ src/admin/dashboard/ActionItems.js | 64 +++++++---- tests/unit/admin/test-admin.php | 179 ++++++++++++++++++++++++++++- tests/unit/inc/test-helper.php | 66 +++++++++++ 5 files changed, 514 insertions(+), 88 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index efeb3d470..c39733775 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -2819,6 +2819,7 @@ public function handle_notice_response() { ], 'notification_error' => [ 'contact_support' => 'notification_failure_notice_cta', + 'help_me_fix' => 'notification_failure_notice_guide', 'dismissed' => 'notification_failure_notice_dismiss', ], 'integration_error' => [ @@ -3438,6 +3439,18 @@ class="button button-primary" > + + + + + @@ -3551,6 +3564,10 @@ public function get_action_items() { 'title' => __( 'We noticed a notification failure on %s.', 'sureforms' ), 'generic' => __( 'We noticed a notification failure.', 'sureforms' ), 'message' => __( 'The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing.', 'sureforms' ), + // Email is the one failure here a site owner can usually fix without + // us: it is almost always SMTP not being configured. Offer the guide + // alongside support rather than making them wait for a reply. + 'guide' => 'https://sureforms.com/docs/troubleshooting-email-sending-in-sureforms/', ], 'integration' => [ 'id' => 'integration_error', @@ -3570,7 +3587,7 @@ public function get_action_items() { // twenty of them, and the title is the first thing anyone asks for. $form_title = Helper::get_string_value( $open[ $category ]['form_title'] ?? '' ); - $warnings[] = [ + $warning = [ 'id' => $copy['id'], 'status' => 'error', 'title' => '' !== $form_title @@ -3578,10 +3595,21 @@ public function get_action_items() { : $copy['generic'], 'message' => $copy['message'], 'cta_label' => __( 'Contact Support', 'sureforms' ), - 'cta_url' => $this->get_support_mailto_url(), + 'cta_url' => $this->get_support_mailto_url( $category, $form_title ), 'cta_action' => 'contact_support', 'dismissible' => false, ]; + + // A second, optional action. Absent keys render nothing, so a category + // without a guide needs no branch in either renderer, and neither does + // an item contributed through srfm_action_items. + if ( ! empty( $copy['guide'] ) ) { + $warning['guide_label'] = __( 'Help Me Fix', 'sureforms' ); + $warning['guide_url'] = $copy['guide']; + $warning['guide_action'] = 'help_me_fix'; + } + + $warnings[] = $warning; } $caching_plugin = Helper::get_active_caching_plugin(); @@ -3597,7 +3625,7 @@ public function get_action_items() { ), 'message' => __( 'Caching can show visitors an old copy of your form, or load its scripts in the wrong order.', 'sureforms' ), 'cta_label' => __( 'Help Me Fix', 'sureforms' ), - 'cta_url' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-caching-plugins/', + 'cta_url' => Helper::get_caching_plugin_doc_url(), 'cta_action' => 'help_me_fix', 'dismissible' => true, ]; @@ -4289,29 +4317,39 @@ private function track_action_item_impressions( $warnings ) { } /** - * Pre-addressed support email for a run of failed submissions. + * Pre-addressed support email for one kind of failure. * * Carries the details support would otherwise have to ask for, so the first * reply can be an answer rather than a questionnaire, along with the recent log * entries inline. * + * The subject and the opening line both come from the category. They used to be + * hardcoded to submissions, so a site whose email was failing sent support a + * ticket titled "form submissions are failing" and a count that belonged to a + * different counter -- wrong at a glance and routed to the wrong place. + * * The log is pasted into the body rather than attached because mailto has no * attachment parameter -- browsers drop any attempt to add one -- and it is a * tail rather than the whole file because a megabyte of JSON would exceed the * URL length every mail client enforces. * + * @param string $category One of Client_Logger::CATEGORIES. Unknown or absent + * gets neutral wording rather than a specific claim. + * @param string $form_title Form the failure was recorded against, when known. * @since 2.12.6 * @return string */ - private function get_support_mailto_url() { + private function get_support_mailto_url( $category = '', $form_title = '' ) { + $copy = $this->get_support_copy( $category ); + $subject = sprintf( /* translators: %s: site host. */ - __( 'SureForms: form submissions are failing on %s', 'sureforms' ), + $copy['subject'], Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ) ); $log = Client_Logger::get_tail(); - $body = $this->get_support_message(); + $body = $this->get_support_message( $category, $form_title ); $body .= "\r\n\r\n" . '---' . "\r\n"; if ( '' === $log['text'] ) { @@ -4344,45 +4382,120 @@ private function get_support_mailto_url() { ); } + /** + * Subject and opening line for one kind of failure. + * + * Both come from here so they cannot drift apart: a subject naming one problem + * over a body describing another is worse than either alone. + * + * An unknown or absent category gets deliberately neutral wording. The + * alternative -- defaulting to the submission copy -- states something specific + * that may not be true, and an item contributed through srfm_action_items has no + * category at all. + * + * @param string $category One of Client_Logger::CATEGORIES. + * @since 2.12.6 + * @return array{subject:string,singular:string,plural:string} + */ + private function get_support_copy( $category ) { + $copy = [ + 'submission' => [ + /* translators: %s: site host. */ + 'subject' => __( 'SureForms: form submissions are failing on %s', 'sureforms' ), + /* translators: %d: number of failed submissions. */ + 'singular' => __( 'SureForms has recorded %d form submission that could not be completed.', 'sureforms' ), + /* translators: %d: number of failed submissions. */ + 'plural' => __( 'SureForms has recorded %d form submissions that could not be completed.', 'sureforms' ), + ], + 'notification' => [ + /* translators: %s: site host. */ + 'subject' => __( 'SureForms: notification emails are not being sent on %s', 'sureforms' ), + /* translators: %d: number of failed notifications. */ + 'singular' => __( 'SureForms saved %d entry but could not send the notification email for it.', 'sureforms' ), + /* translators: %d: number of failed notifications. */ + 'plural' => __( 'SureForms saved %d entries but could not send the notification emails for them.', 'sureforms' ), + ], + 'integration' => [ + /* translators: %s: site host. */ + 'subject' => __( 'SureForms: an integration is not receiving entries on %s', 'sureforms' ), + /* translators: %d: number of failed integration hand-offs. */ + 'singular' => __( 'SureForms saved %d entry but could not pass it to a connected service.', 'sureforms' ), + /* translators: %d: number of failed integration hand-offs. */ + 'plural' => __( 'SureForms saved %d entries but could not pass them to a connected service.', 'sureforms' ), + ], + ]; + + if ( isset( $copy[ $category ] ) ) { + return $copy[ $category ]; + } + + return [ + /* translators: %s: site host. */ + 'subject' => __( 'SureForms: a problem with the forms on %s', 'sureforms' ), + /* translators: %d: number of recorded problems. */ + 'singular' => __( 'SureForms has recorded %d problem with the forms on this site.', 'sureforms' ), + /* translators: %d: number of recorded problems. */ + 'plural' => __( 'SureForms has recorded %d problems with the forms on this site.', 'sureforms' ), + ]; + } + /** * Diagnostics block for the support email. * * Carries what support would otherwise have to ask for, so the first reply can * be an answer rather than a questionnaire. * + * The count is the one for this category, not get_fault_streak(), which reports + * submissions only -- so a notification failure used to quote a number from an + * unrelated counter, often zero. + * + * @param string $category One of Client_Logger::CATEGORIES. + * @param string $form_title Form the failure was recorded against, when known. * @since 2.12.6 * @return string */ - private function get_support_message() { + private function get_support_message( $category = '', $form_title = '' ) { global $wp_version; - $count = Client_Logger::get_fault_streak(); + $failures = Client_Logger::get_failures(); + $count = Helper::get_integer_value( $failures[ $category ]['count'] ?? 0 ); + $copy = $this->get_support_copy( $category ); + + // A category with nothing recorded still needs to read as a sentence, and + // "0 entries" reads as a bug in the email rather than a fault on the site. + $count = max( 1, $count ); $lines = [ __( 'Hello SureForms support,', 'sureforms' ), '', - sprintf( - /* translators: %d: number of consecutive failed submissions. */ - _n( - 'SureForms has recorded %d form submission in a row that could not be completed.', - 'SureForms has recorded %d form submissions in a row that could not be completed.', - $count, - 'sureforms' - ), - $count - ), - '', - '---', - __( 'Site details', 'sureforms' ), - 'Site: ' . home_url(), - 'SureForms: ' . SRFM_VER, - 'SureForms Pro: ' . ( Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' ) ), - 'WordPress: ' . Helper::get_string_value( $wp_version ), - 'PHP: ' . PHP_VERSION, - 'Caching: ' . ( '' !== Helper::get_active_caching_plugin() ? Helper::get_active_caching_plugin() : __( 'none detected', 'sureforms' ) ), - 'Consecutive failures: ' . $count, + sprintf( 1 === $count ? $copy['singular'] : $copy['plural'], $count ), ]; + if ( '' !== $form_title ) { + $lines[] = ''; + $lines[] = sprintf( + /* translators: %s: form title. */ + __( 'Form: %s', 'sureforms' ), + $form_title + ); + } + + $lines = array_merge( + $lines, + [ + '', + '---', + __( 'Site details', 'sureforms' ), + 'Site: ' . home_url(), + 'SureForms: ' . SRFM_VER, + 'SureForms Pro: ' . ( Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' ) ), + 'WordPress: ' . Helper::get_string_value( $wp_version ), + 'PHP: ' . PHP_VERSION, + 'Caching: ' . ( '' !== Helper::get_active_caching_plugin() ? Helper::get_active_caching_plugin() : __( 'none detected', 'sureforms' ) ), + 'Recorded failures: ' . $count, + ] + ); + return implode( "\r\n", $lines ); } diff --git a/inc/helper.php b/inc/helper.php index 06e75073e..640ae0f18 100644 --- a/inc/helper.php +++ b/inc/helper.php @@ -2218,51 +2218,34 @@ public static function get_block_name_from_field( $field_name ) { * the scripts a form depends on. This is what surfaces that to the site owner * before it turns into "my form stopped working". * - * Detection is by plugin path, mirroring is_any_smtp_plugin_active(), including - * the multisite network-active merge. - * * @since 2.12.6 * @return string Human-readable plugin name, or '' when none is active. */ public static function get_active_caching_plugin() { - $caching_plugins = [ - 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', - 'wp-rocket/wp-rocket.php' => 'WP Rocket', - 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', - 'wp-super-cache/wp-cache.php' => 'WP Super Cache', - 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', - 'autoptimize/autoptimize.php' => 'Autoptimize', - 'sg-cachepress/sg-cachepress.php' => 'SiteGround Optimizer', - 'wp-optimize/wp-optimize.php' => 'WP-Optimize', - 'cache-enabler/cache-enabler.php' => 'Cache Enabler', - 'comet-cache/comet-cache.php' => 'Comet Cache', - 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird', - 'breeze/breeze.php' => 'Breeze', - 'nitropack/main.php' => 'NitroPack', - 'swift-performance-lite/performance.php' => 'Swift Performance Lite', - 'wp-cloudflare-page-cache/wp-cloudflare-page-cache.php' => 'Super Page Cache', - 'flying-press/flying-press.php' => 'FlyingPress', - 'redis-cache/redis-cache.php' => 'Redis Object Cache', - 'powered-cache/powered-cache.php' => 'Powered Cache', - 'docket-cache/docket-cache.php' => 'Docket Cache', - 'seraphinite-accelerator/plugin_root.php' => 'Seraphinite Accelerator', - ]; + $entry = self::get_active_caching_plugin_entry(); - $active_plugins = (array) get_option( 'active_plugins', [] ); - - // For multisite, merge sitewide active plugins. - if ( is_multisite() ) { - $network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] ); - $active_plugins = array_merge( $active_plugins, array_keys( $network_plugins ) ); - } + return null === $entry ? '' : $entry[0]; + } - foreach ( $caching_plugins as $path => $name ) { - if ( in_array( $path, $active_plugins, true ) ) { - return $name; - } - } + /** + * Setup guide for the active caching plugin. + * + * Six of the recognised plugins have a guide of their own; the rest, and any + * site with none detected, get the general one. Sending someone to a page that + * names the plugin they actually run is the difference between advice they can + * follow and advice they have to translate. + * + * Falls back to the general guide rather than returning nothing, so the notice + * always has somewhere to send them. + * + * @since 2.12.6 + * @return string Absolute documentation URL. + */ + public static function get_caching_plugin_doc_url() { + $entry = self::get_active_caching_plugin_entry(); + $slug = null === $entry || '' === $entry[1] ? 'how-to-set-up-sureforms-with-caching-plugins' : $entry[1]; - return ''; + return 'https://sureforms.com/docs/' . $slug . '/'; } /** @@ -2796,4 +2779,67 @@ private static function get_geo_failure_ttl() { return self::get_integer_value( apply_filters( 'srfm_geo_failure_ttl', 5 * MINUTE_IN_SECONDS ) ); } + /** + * Caching plugins SureForms recognises, and the guide for each. + * + * `path => [ display name, doc slug ]`. An empty slug means there is no + * plugin-specific guide and the general one applies. Name and slug live in one + * array on purpose: keyed separately they drift, and a doc link that silently + * degrades to the generic page is the kind of regression nobody reports. + * + * @since 2.12.6 + * @return array + */ + private static function get_known_caching_plugins() { + return [ + 'litespeed-cache/litespeed-cache.php' => [ 'LiteSpeed Cache', 'how-to-set-up-sureforms-with-litespeed-cache' ], + 'wp-rocket/wp-rocket.php' => [ 'WP Rocket', 'how-to-set-up-sureforms-with-wp-rocket' ], + 'w3-total-cache/w3-total-cache.php' => [ 'W3 Total Cache', 'how-to-set-up-sureforms-with-w3-total-cache' ], + 'wp-fastest-cache/wpFastestCache.php' => [ 'WP Fastest Cache', 'how-to-set-up-sureforms-with-wp-fastest-cache' ], + 'sg-cachepress/sg-cachepress.php' => [ 'SiteGround Optimizer', 'how-to-set-up-sureforms-with-siteground-optimizer' ], + 'autoptimize/autoptimize.php' => [ 'Autoptimize', 'how-to-set-up-sureforms-with-autoptimize' ], + 'wp-super-cache/wp-cache.php' => [ 'WP Super Cache', '' ], + 'wp-optimize/wp-optimize.php' => [ 'WP-Optimize', '' ], + 'cache-enabler/cache-enabler.php' => [ 'Cache Enabler', '' ], + 'comet-cache/comet-cache.php' => [ 'Comet Cache', '' ], + 'hummingbird-performance/wp-hummingbird.php' => [ 'Hummingbird', '' ], + 'breeze/breeze.php' => [ 'Breeze', '' ], + 'nitropack/main.php' => [ 'NitroPack', '' ], + 'swift-performance-lite/performance.php' => [ 'Swift Performance Lite', '' ], + 'wp-cloudflare-page-cache/wp-cloudflare-page-cache.php' => [ 'Super Page Cache', '' ], + 'flying-press/flying-press.php' => [ 'FlyingPress', '' ], + 'redis-cache/redis-cache.php' => [ 'Redis Object Cache', '' ], + 'powered-cache/powered-cache.php' => [ 'Powered Cache', '' ], + 'docket-cache/docket-cache.php' => [ 'Docket Cache', '' ], + 'seraphinite-accelerator/plugin_root.php' => [ 'Seraphinite Accelerator', '' ], + ]; + } + + /** + * The active caching plugin's entry, if there is one. + * + * Detection is by plugin path, mirroring is_any_smtp_plugin_active(), including + * the multisite network-active merge. + * + * @since 2.12.6 + * @return array{0:string,1:string}|null Name and doc slug, or null when none is active. + */ + private static function get_active_caching_plugin_entry() { + $active_plugins = (array) get_option( 'active_plugins', [] ); + + // For multisite, merge sitewide active plugins. + if ( is_multisite() ) { + $network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] ); + $active_plugins = array_merge( $active_plugins, array_keys( $network_plugins ) ); + } + + foreach ( self::get_known_caching_plugins() as $path => $entry ) { + if ( in_array( $path, $active_plugins, true ) ) { + return $entry; + } + } + + return null; + } + } diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 20ce8c231..cbb295e30 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -72,12 +72,12 @@ export default () => { // The mailto now carries the log in its body, so the link just works -- no // download to trigger, nothing to intercept. - const handleFix = ( item ) => () => + const handleFix = ( item, action ) => () => post( 'srfm_notice_response', srfm_admin?.notice_response_nonce, { notice_id: item.id, // Named by the server, so the button key is not duplicated here and in // the allowlist that has to accept it. - button: item.cta_action, + button: action, } ); return ( @@ -141,24 +141,48 @@ export default () => { ) } - { !! item.cta_label && ( -
- + { ( !! item.cta_label || !! item.guide_label ) && ( +
+ { !! item.cta_label && ( + + ) } + { /* A second action, when the failure is one the + site owner can usually fix themselves. */ } + { !! item.guide_label && !! item.guide_url && ( + + ) }
) }
diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 6ebf1e3d6..762287cf6 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1146,6 +1146,177 @@ public function test_get_action_items() { } } + /** + * The notification failure offers the SMTP guide alongside Contact Support. + * + * Email is the one failure here a site owner can usually fix without us -- it is + * almost always SMTP not being configured -- so making them wait on a support + * reply for it is the wrong default. The other two categories have no guide, and + * must not grow an empty second button. + */ + public function test_get_action_items_offers_a_guide_for_notification_failures() { + wp_set_current_user( $this->make_user( 'administrator' ) ); + delete_option( Client_Logger::FAILURES_OPTION ); + + Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + + $items = []; + foreach ( Admin::get_instance()->get_action_items() as $item ) { + $items[ $item['id'] ] = $item; + } + + $this->assertArrayHasKey( 'notification_error', $items ); + $this->assertSame( + 'https://sureforms.com/docs/troubleshooting-email-sending-in-sureforms/', + $items['notification_error']['guide_url'] ?? '', + 'The notification failure must link to the email troubleshooting guide.' + ); + $this->assertSame( 'help_me_fix', $items['notification_error']['guide_action'] ?? '' ); + $this->assertNotEmpty( $items['notification_error']['guide_label'] ?? '' ); + + // Contact Support is still there; the guide is an addition, not a swap. + $this->assertSame( 'contact_support', $items['notification_error']['cta_action'] ?? '' ); + + // A category with no guide must not carry empty guide keys, which both + // renderers treat as "render a second button". + $this->assertArrayHasKey( 'form_submission_error', $items ); + $this->assertArrayNotHasKey( 'guide_url', $items['form_submission_error'] ); + $this->assertArrayNotHasKey( 'guide_label', $items['form_submission_error'] ); + } + + /** + * The guide click is a distinct analytics event, and is accepted by the + * allowlist that gates every notice response. + * + * A button the allowlist does not know about is rejected with a 400, so adding + * a CTA without its allowlist entry ships a link that reports nothing. + */ + public function test_handle_notice_response_accepts_the_notification_guide() { + wp_set_current_user( $this->make_user( 'administrator' ) ); + + // check_ajax_referer() reads $_REQUEST, so $_POST alone fails the nonce. + $nonce = wp_create_nonce( 'srfm_notice_response' ); + $_POST['nonce'] = $nonce; + $_REQUEST['nonce'] = $nonce; + $_POST['notice_id'] = 'notification_error'; + $_REQUEST['notice_id'] = 'notification_error'; + $_POST['button'] = 'help_me_fix'; + $_REQUEST['button'] = 'help_me_fix'; + + // wp_send_json_*() terminates via wp_die(); make the handlers throw so the + // runner survives and the emitted body can be asserted on. Asserting on the + // body rather than "did it die" is what separates accept from reject here -- + // both end the request. + $throw_handler = static function () { + return static function () { + throw new \WPDieException( 'srfm_test_die' ); + }; + }; + // wp_send_json() only routes through wp_die() when wp_doing_ajax() is true; + // otherwise it calls a bare die() that no filter can intercept, which takes + // the test runner with it. + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( 'wp_die_ajax_handler', $throw_handler ); + add_filter( 'wp_die_handler', $throw_handler ); + + ob_start(); + try { + Admin::get_instance()->handle_notice_response(); + } catch ( \WPDieException $e ) { + // Expected. + } finally { + $body = (string) ob_get_clean(); + remove_filter( 'wp_doing_ajax', '__return_true' ); + remove_filter( 'wp_die_ajax_handler', $throw_handler ); + remove_filter( 'wp_die_handler', $throw_handler ); + unset( + $_POST['nonce'], + $_POST['notice_id'], + $_POST['button'], + $_REQUEST['nonce'], + $_REQUEST['notice_id'], + $_REQUEST['button'] + ); + } + + $this->assertStringContainsString( '"success":true', $body, 'The guide button must be on the allowlist.' ); + } + + /** + * Each failure emails support about the failure that actually happened. + * + * Subject and opening line were both hardcoded to submissions, so a site whose + * email was broken sent support a ticket titled "form submissions are failing" + * carrying a count read from the submission counter -- often zero, and always + * about something else. Wrong at a glance, and routed to the wrong queue. + */ + public function test_get_support_mailto_url_describes_the_failure_that_happened() { + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); + Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); + Client_Logger::record_failure( 'integration', 43, 'Job Application' ); + + $method = new ReflectionMethod( Admin::class, 'get_support_mailto_url' ); + $method->setAccessible( true ); + + $parts = static function ( $url ) { + parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + return $query; + }; + + $submission = $parts( $method->invoke( Admin::get_instance(), 'submission', 'Contact Form' ) ); + $notification = $parts( $method->invoke( Admin::get_instance(), 'notification', 'Contact Form' ) ); + $integration = $parts( $method->invoke( Admin::get_instance(), 'integration', 'Job Application' ) ); + + // Three distinct subjects. Asserted against each other as well as their own + // text, because the failure mode here was one subject serving all three. + $this->assertStringContainsString( 'form submissions are failing', $submission['subject'] ); + $this->assertStringContainsString( 'notification emails are not being sent', $notification['subject'] ); + $this->assertStringContainsString( 'integration is not receiving entries', $integration['subject'] ); + $this->assertNotSame( $submission['subject'], $notification['subject'] ); + $this->assertNotSame( $notification['subject'], $integration['subject'] ); + + // The body must not claim submissions are failing when they are not. + $this->assertStringNotContainsString( + 'form submission', + $notification['body'], + 'A notification failure must not describe itself as a submission failure.' + ); + $this->assertStringContainsString( 'could not send the notification emails', $notification['body'] ); + + // The count is this category's, not the submission counter's. Notification + // has 2 recorded against submission's 1, so a leaked streak shows up here. + $this->assertStringContainsString( 'SureForms saved 2 entries', $notification['body'] ); + $this->assertStringContainsString( 'Recorded failures: 2', $notification['body'] ); + + // The form is named, which is the first thing support asks for. + $this->assertStringContainsString( 'Job Application', $integration['body'] ); + } + + /** + * An unknown category gets neutral wording rather than a specific claim. + * + * srfm_action_items is public, so an item can arrive with no category at all. + * Defaulting to the submission copy would state something that may not be true. + */ + public function test_get_support_mailto_url_stays_neutral_without_a_category() { + delete_option( Client_Logger::FAILURES_OPTION ); + + $method = new ReflectionMethod( Admin::class, 'get_support_mailto_url' ); + $method->setAccessible( true ); + + parse_str( + (string) wp_parse_url( $method->invoke( Admin::get_instance(), '', '' ), PHP_URL_QUERY ), + $query + ); + + $this->assertStringContainsString( 'a problem with the forms on', $query['subject'] ); + $this->assertStringNotContainsString( 'form submissions are failing', $query['subject'] ); + $this->assertStringNotContainsString( 'form submission that could not', $query['body'] ); + } + /** * An active caching plugin produces a dismissible advisory item pointing at the * setup guide. @@ -1174,7 +1345,13 @@ public function test_get_action_items_flags_an_active_caching_plugin() { $this->assertNotNull( $caching, 'An active caching plugin must be reported.' ); $this->assertStringContainsString( 'WP Rocket', $caching['title'] ); $this->assertTrue( $caching['dismissible'] ); - $this->assertStringContainsString( 'caching-plugins', $caching['cta_url'] ); + // The guide for the plugin actually running, not the general page. WP Rocket + // has one of its own, so linking to the general page here would be the + // regression this assertion exists to catch. + $this->assertSame( + 'https://sureforms.com/docs/how-to-set-up-sureforms-with-wp-rocket/', + $caching['cta_url'] + ); } /** diff --git a/tests/unit/inc/test-helper.php b/tests/unit/inc/test-helper.php index 9d63e4597..63858eb04 100644 --- a/tests/unit/inc/test-helper.php +++ b/tests/unit/inc/test-helper.php @@ -3231,6 +3231,72 @@ public function test_get_active_caching_plugin() { } } + /** + * Each plugin with a guide of its own must link to that guide. + * + * The point of the notice is advice the site owner can act on, and a page that + * names the plugin they actually run is the difference between following it and + * translating it. Asserted per plugin rather than as a count, so renaming a slug + * fails here instead of quietly serving the general page. + */ + public function test_get_caching_plugin_doc_url() { + $expected = [ + 'litespeed-cache/litespeed-cache.php' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-litespeed-cache/', + 'wp-rocket/wp-rocket.php' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-wp-rocket/', + 'w3-total-cache/w3-total-cache.php' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-w3-total-cache/', + 'wp-fastest-cache/wpFastestCache.php' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-wp-fastest-cache/', + 'sg-cachepress/sg-cachepress.php' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-siteground-optimizer/', + 'autoptimize/autoptimize.php' => 'https://sureforms.com/docs/how-to-set-up-sureforms-with-autoptimize/', + ]; + + foreach ( $expected as $path => $url ) { + $filter = static function () use ( $path ) { + return [ $path ]; + }; + + add_filter( 'pre_option_active_plugins', $filter ); + $detected = Helper::get_caching_plugin_doc_url(); + remove_filter( 'pre_option_active_plugins', $filter ); + + $this->assertSame( $url, $detected, $path . ' must link to its own guide.' ); + } + } + + /** + * A recognised plugin with no guide of its own, and a site with no caching + * plugin at all, both fall back to the general guide. + * + * Returning nothing would leave the notice with a Help Me Fix link pointing + * nowhere, which is worse than general advice. + */ + public function test_get_caching_plugin_doc_url_falls_back_to_the_general_guide() { + $general = 'https://sureforms.com/docs/how-to-set-up-sureforms-with-caching-plugins/'; + + // Recognised, but has no guide of its own. + $filter = static function () { + return [ 'breeze/breeze.php' ]; + }; + + add_filter( 'pre_option_active_plugins', $filter ); + $detected = Helper::get_caching_plugin_doc_url(); + $name = Helper::get_active_caching_plugin(); + remove_filter( 'pre_option_active_plugins', $filter ); + + $this->assertSame( $general, $detected, 'A plugin with no guide gets the general one.' ); + $this->assertSame( 'Breeze', $name, 'It is still detected by name.' ); + + // No caching plugin at all. + $none = static function () { + return [ 'akismet/akismet.php' ]; + }; + + add_filter( 'pre_option_active_plugins', $none ); + $detected = Helper::get_caching_plugin_doc_url(); + remove_filter( 'pre_option_active_plugins', $none ); + + $this->assertSame( $general, $detected, 'No caching plugin still yields a usable link.' ); + } + /** * A site with no caching plugin must produce nothing. Caching plugins are * common but far from universal, and a false positive puts a permanent From 201dd19cb2f92b49fcc724887965cd8bd1ffddea Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 18:32:27 +0530 Subject: [PATCH 11/58] style: satisfy PHP Insights (null coalescing + ordered class elements) --- inc/field-validation.php | 132 +++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/inc/field-validation.php b/inc/field-validation.php index b62986775..6a55d6f45 100644 --- a/inc/field-validation.php +++ b/inc/field-validation.php @@ -298,29 +298,6 @@ public static function get_known_field_slugs( $form_id ) { return wp_is_numeric_array( $slugs ) ? array_fill_keys( array_map( 'strval', $slugs ), true ) : $slugs; } - /** - * Walk a form's blocks once, returning both the block-id and slug allowlists. - * - * @param int $form_id The form post id. - * @since 2.12.7 - * @return array{ids:array,slugs:array} - */ - private static function walk_field_identifiers( $form_id ) { - $ids = []; - $slugs = []; - $post = get_post( $form_id ); - - if ( $post instanceof \WP_Post && ! empty( $post->post_content ) && function_exists( 'parse_blocks' ) ) { - $visited = []; - self::collect_field_block_ids( parse_blocks( $post->post_content ), $ids, $slugs, $visited ); - } - - return [ - 'ids' => $ids, - 'slugs' => $slugs, - ]; - } - /** * Remove submitted field keys the form does not define. * @@ -387,49 +364,6 @@ public static function strip_unknown_field_keys( $form_data, $form_id ) { return $form_data; } - /** - * Whether a submitted field key belongs to the form, by block_id or by slug. - * - * @param string $key Submitted field key. - * @param array $known_block_ids Allowlisted block ids. - * @param array $known_slugs Allowlisted field slugs. - * @since 2.12.7 - * @return bool - */ - private static function field_key_belongs_to_form( $key, $known_block_ids, $known_slugs ) { - if ( isset( $known_block_ids[ Helper::get_block_id_from_key( $key ) ] ) ) { - return true; - } - - $slug = self::get_slug_from_key( $key ); - - return '' !== $slug && isset( $known_slugs[ $slug ] ); - } - - /** - * Extract a field's slug from its submitted key. - * - * A key is `srfm---lbl--`. Helper::encode() is - * padding-stripped standard base64 and never contains a hyphen, so the slug is - * everything after the first hyphen that follows `-lbl-` (the slug itself may - * contain hyphens, which is why only the first is used as the boundary). - * - * @param string $key Submitted field key. - * @since 2.12.7 - * @return string The slug, or '' when it cannot be derived. - */ - private static function get_slug_from_key( $key ) { - if ( ! is_string( $key ) || false === strpos( $key, '-lbl-' ) ) { - return ''; - } - - $parts = explode( '-lbl-', $key ); - $after = isset( $parts[1] ) ? $parts[1] : ''; - $pos = strpos( $after, '-' ); - - return false === $pos ? '' : substr( $after, $pos + 1 ); - } - /** * Validate form data for a given form. * @@ -599,6 +533,72 @@ public static function get_email_char_limits() { ]; } + /** + * Walk a form's blocks once, returning both the block-id and slug allowlists. + * + * @param int $form_id The form post id. + * @since 2.12.7 + * @return array{ids:array,slugs:array} + */ + private static function walk_field_identifiers( $form_id ) { + $ids = []; + $slugs = []; + $post = get_post( $form_id ); + + if ( $post instanceof \WP_Post && ! empty( $post->post_content ) && function_exists( 'parse_blocks' ) ) { + $visited = []; + self::collect_field_block_ids( parse_blocks( $post->post_content ), $ids, $slugs, $visited ); + } + + return [ + 'ids' => $ids, + 'slugs' => $slugs, + ]; + } + + /** + * Whether a submitted field key belongs to the form, by block_id or by slug. + * + * @param string $key Submitted field key. + * @param array $known_block_ids Allowlisted block ids. + * @param array $known_slugs Allowlisted field slugs. + * @since 2.12.7 + * @return bool + */ + private static function field_key_belongs_to_form( $key, $known_block_ids, $known_slugs ) { + if ( isset( $known_block_ids[ Helper::get_block_id_from_key( $key ) ] ) ) { + return true; + } + + $slug = self::get_slug_from_key( $key ); + + return '' !== $slug && isset( $known_slugs[ $slug ] ); + } + + /** + * Extract a field's slug from its submitted key. + * + * A key is `srfm---lbl--`. Helper::encode() is + * padding-stripped standard base64 and never contains a hyphen, so the slug is + * everything after the first hyphen that follows `-lbl-` (the slug itself may + * contain hyphens, which is why only the first is used as the boundary). + * + * @param string $key Submitted field key. + * @since 2.12.7 + * @return string The slug, or '' when it cannot be derived. + */ + private static function get_slug_from_key( $key ) { + if ( ! is_string( $key ) || false === strpos( $key, '-lbl-' ) ) { + return ''; + } + + $parts = explode( '-lbl-', $key ); + $after = $parts[1] ?? ''; + $pos = strpos( $after, '-' ); + + return false === $pos ? '' : substr( $after, $pos + 1 ); + } + /** * Remove unknown child field keys from repeater rows. * From 2dcc598b09c1a9e08ff909e7454d42ac26d69bca Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 18:49:37 +0530 Subject: [PATCH 12/58] fix: form pages render unstyled when Breakdance is active A single form page served with none of its stylesheets while Breakdance was active. The form markup was correct, so it read as a conditional enqueue problem, but the assets were enqueued fine and simply never reached the browser. Breakdance filters template_include at priority 1000000, renders the theme template into an output buffer, and substitutes its own asset placeholders into the result. SureForms filters the same hook at PHP_INT_MAX, so on a single form it returns the Instant Form template and that buffered page is discarded. The work behind it is not: wp_head() has already run once inside the buffer, so every stylesheet is recorded in WP_Styles::$done, and the second wp_head() in the form template prints none of them again. Measured on a local reproduction: wp_head fired twice, 28 stylesheets recorded as printed, zero in the delivered page, and Breakdance's unreplaced left in the head. After the fix, wp_head fires once and 20 stylesheets arrive, SureForms' own form.css, single.css and common.css among them. Standing Breakdance's filter down on these pages costs nothing that works today, because its output was already being thrown away. It removes the wasted render, leaves wp_head firing once, and lets the styles print normally. Scoped tightly on purpose. Breakdance is how the rest of these sites are built, so the guard is is_singular( SRFM_FORMS_POST_TYPE ) and the hook is `wp` -- the last point before template_include is applied and the first at which the queried object is known. It is also guarded on that exact filter being registered rather than on a version constant, since the removal is only correct while that callback is in the chain. The priority is matched exactly because remove_filter() needs it to find the entry. A mismatch there is a silent no-op: the page renders unstyled with nothing in any log, which is covered by its own test. --- .../page-builders/breakdance.php | 89 +++++++++ plugin-loader.php | 2 + .../page-builders/test-breakdance.php | 183 ++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 inc/compatibility/page-builders/breakdance.php create mode 100644 tests/unit/inc/compatibility/page-builders/test-breakdance.php diff --git a/inc/compatibility/page-builders/breakdance.php b/inc/compatibility/page-builders/breakdance.php new file mode 100644 index 000000000..f0c949dc0 --- /dev/null +++ b/inc/compatibility/page-builders/breakdance.php @@ -0,0 +1,89 @@ + + */ + private $previous_query = []; + + /** + * Put the main query on a singular view of the given post. + * + * go_to() belongs to WP_UnitTestCase and this suite uses Yoast's TestCase, so + * the query globals are driven directly -- the same approach as + * Test_Generate_Form_Markup. + * + * @param int $post_id Post to be the queried object. + */ + private function make_singular( $post_id ) { + global $wp_query; + + $this->previous_query = [ + 'is_singular' => $wp_query->is_singular, + 'queried_object' => $wp_query->get_queried_object(), + 'queried_object_id' => $wp_query->get_queried_object_id(), + ]; + + $wp_query->is_singular = true; + $wp_query->queried_object = get_post( $post_id ); + $wp_query->queried_object_id = $post_id; + } + + /** + * Clean up whatever a test registered. + */ + protected function tear_down(): void { + remove_filter( 'template_include', self::CALLBACK, self::PRIORITY ); + + if ( ! empty( $this->previous_query ) ) { + global $wp_query; + $wp_query->is_singular = $this->previous_query['is_singular']; + $wp_query->queried_object = $this->previous_query['queried_object']; + $wp_query->queried_object_id = $this->previous_query['queried_object_id']; + $this->previous_query = []; + } + + parent::tear_down(); + } + + /** + * On a single form, Breakdance's filter is taken out of the chain. + */ + public function test_stand_down_on_instant_form() { + $form_id = wp_insert_post( + [ + 'post_title' => 'Breakdance Styling Form', + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + ] + ); + + $this->make_singular( $form_id ); + $this->assertTrue( is_singular( SRFM_FORMS_POST_TYPE ), 'Test setup: the query must be a single form.' ); + + $this->register_breakdance_filter(); + $this->assertNotFalse( + has_filter( 'template_include', self::CALLBACK ), + 'Test setup: the stand-in filter must be registered before standing it down.' + ); + + Breakdance::get_instance()->stand_down_on_instant_form(); + + $this->assertFalse( + has_filter( 'template_include', self::CALLBACK ), + 'Breakdance must not buffer a page SureForms is about to replace.' + ); + + wp_delete_post( $form_id, true ); + } + + /** + * Everywhere else, Breakdance is left completely alone. + * + * Breakdance is how the rest of the site is built. Removing its renderer on a + * page it owns would take the entire layout down, which is far worse than the + * bug being fixed -- so this asserts the negative as carefully as the positive. + */ + public function test_stand_down_on_instant_form_leaves_other_pages_alone() { + $page_id = wp_insert_post( + [ + 'post_title' => 'An Ordinary Page', + 'post_type' => 'page', + 'post_status' => 'publish', + ] + ); + + $this->make_singular( $page_id ); + $this->assertFalse( is_singular( SRFM_FORMS_POST_TYPE ), 'Test setup: this must not be a form page.' ); + + $this->register_breakdance_filter(); + + Breakdance::get_instance()->stand_down_on_instant_form(); + + $this->assertNotFalse( + has_filter( 'template_include', self::CALLBACK ), + 'Breakdance must keep rendering every page it owns.' + ); + + wp_delete_post( $page_id, true ); + } + + /** + * With Breakdance absent, the hook is left exactly as it was. + * + * The overwhelming majority of installs have no Breakdance at all, so the + * common path must touch nothing. + */ + public function test_stand_down_on_instant_form_does_nothing_without_breakdance() { + $form_id = wp_insert_post( + [ + 'post_title' => 'Form Without Breakdance', + 'post_type' => SRFM_FORMS_POST_TYPE, + 'post_status' => 'publish', + ] + ); + + $this->make_singular( $form_id ); + + $before = has_filter( 'template_include' ); + + Breakdance::get_instance()->stand_down_on_instant_form(); + + $this->assertSame( + $before, + has_filter( 'template_include' ), + 'Nothing on template_include may change when Breakdance is not installed.' + ); + + wp_delete_post( $form_id, true ); + } +} From ec604c63798c77e408ad76c41cb126c45649eb7e Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 3 Sep 2026 23:49:16 +0530 Subject: [PATCH 13/58] feat: lead the notification notice with Help Me Fix Someone who can fix a problem themselves should see that before they are pointed at a support queue. Email is almost always SMTP not being configured, so the guide is the better first move and Contact Support is the fallback. Emphasis follows position rather than identity. Reordering alone would have left the secondary styling on the leading action and the primary button on the one below it, so both renderers now key the emphasis off the order: whichever action leads is primary, and an item with no guide still leads with Contact Support looking exactly as it does today. The React panel builds an ordered list rather than two conditional blocks, which is what makes that rule expressible once instead of per button. --- admin/admin.php | 28 +++++---- src/admin/dashboard/ActionItems.js | 94 +++++++++++++++++------------- tests/unit/admin/test-admin.php | 63 ++++++++++++++++++++ 3 files changed, 134 insertions(+), 51 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index c39733775..091d053ce 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -3429,20 +3429,17 @@ public function render_action_item_notices() {
- { ( !! item.cta_label || !! item.guide_label ) && ( + { !! actionsFor( item ).length && (
- { !! item.cta_label && ( - - ) } - { /* A second action, when the failure is one the - site owner can usually fix themselves. */ } - { !! item.guide_label && !! item.guide_url && ( - + { actionsFor( item ).map( + ( action, index ) => ( + + ) ) }
) } diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 762287cf6..bf5ce2b3b 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1295,6 +1295,69 @@ public function test_get_support_mailto_url_describes_the_failure_that_happened( $this->assertStringContainsString( 'Job Application', $integration['body'] ); } + /** + * The guide leads and Contact Support follows, in both surfaces. + * + * Someone who can fix it themselves should see that before being pointed at a + * support queue. Emphasis follows position rather than identity, so an item with + * no guide still leads with Contact Support as the primary button -- asserted + * here, because a swap that only reorders would leave the secondary styling on + * the leading action. + */ + public function test_render_action_item_notices_puts_the_guide_before_contact_support() { + wp_set_current_user( $this->make_user( 'administrator' ) ); + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + + global $pagenow; + $previous_pagenow = $pagenow; + $pagenow = 'plugins.php'; + + ob_start(); + Admin::get_instance()->render_action_item_notices(); + $html = (string) ob_get_clean(); + + $pagenow = $previous_pagenow; + + $guide = strpos( $html, 'help_me_fix' ); + $this->assertNotFalse( $guide, 'The notification guide must render.' ); + + // The support link that follows it, not the one in the submission notice + // above, so this measures order within the same notice. + $support_after_guide = strpos( $html, 'contact_support', $guide ); + $this->assertNotFalse( $support_after_guide, 'Contact Support must follow the guide.' ); + $this->assertGreaterThan( $guide, $support_after_guide ); + + // Whichever action leads carries the primary button. + $this->assertMatchesRegularExpression( + '/class="button button-primary"[^>]*data-srfm-button="help_me_fix"/', + $html, + 'The leading action must be the primary button.' + ); + $this->assertMatchesRegularExpression( + '/class="button"[^>]*data-srfm-button="contact_support"/', + $html, + 'The following action must be secondary.' + ); + + // An item with no guide keeps Contact Support primary. + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + + $pagenow = 'plugins.php'; + ob_start(); + Admin::get_instance()->render_action_item_notices(); + $alone = (string) ob_get_clean(); + $pagenow = $previous_pagenow; + + $this->assertMatchesRegularExpression( + '/class="button button-primary"[^>]*data-srfm-button="contact_support"/', + $alone, + 'With no guide, Contact Support leads and stays primary.' + ); + } + /** * An unknown category gets neutral wording rather than a specific claim. * From 5b7dcf387141ef41679f629945f9dd3f5470cadd Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Fri, 4 Sep 2026 00:02:31 +0530 Subject: [PATCH 14/58] style: match Form Checks to the Quick Access panel The two sit in the same sidebar column and were built differently: Quick Access nests a grey well inside its white card and puts white rows in it, while Form Checks laid bordered rows straight onto the card. Side by side they read as two components from different plugins. Form Checks now uses the same nesting -- bg-background-secondary well, white rows with shadow-sm-blur-1 and no border -- so the column is one thing. Rows only. The outer card, the collapse control and the row contents are untouched. --- src/admin/dashboard/ActionItems.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 25d02995e..579a542a7 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -132,12 +132,15 @@ export default () => { ) } + { /* Same nesting as Quick Access below it: a grey well inside the white + card, holding white rows. The two sit in one column and were + reading as different components. */ } { open && ( -
+
{ items.map( ( item ) => (
{ ICONS[ item.status ] ?? ICONS.warning } From e17634ba01ea211f46a863dfedebb51433d83bfe Mon Sep 17 00:00:00 2001 From: Avinash Kumar Sharma Date: Fri, 4 Sep 2026 23:59:42 +0530 Subject: [PATCH 15/58] fix: add width/height to edit form SVG to prevent oversizing in Divi --- inc/generate-form-markup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/generate-form-markup.php b/inc/generate-form-markup.php index 32fa8aac2..87d4592a5 100644 --- a/inc/generate-form-markup.php +++ b/inc/generate-form-markup.php @@ -1483,7 +1483,7 @@ public static function render_edit_form_button( $form_id ) { ?>
- + From 61f007a6c063bb09d0bcb30977421dc94f7f36fc Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 14:24:58 +0530 Subject: [PATCH 16/58] feat: open Contact Support in Gmail instead of handing off to mailto A mailto goes to whatever the machine has registered as its mail handler. On a machine with none configured -- common enough for someone who works in webmail -- clicking it opens nothing at all, and the person is left with a button that appears broken at the moment they are trying to report something broken. The compose window always appears now. The trade is real and worth stating: someone who does not use Gmail on the web gets a compose window for an account they may not want. They can still copy the message out of it, which beats a link that does nothing. Gmail truncates a long body silently, and the overflow is the end of the log -- the newest entries, the ones describing the failure being reported. So the URL is held to a 2000 character budget and the log is trimmed here instead, oldest first, with a line in the body saying it was shortened and where to get the whole file. Measured: 662 characters with an empty log, 1258 with a full one. No /u/0/ in the endpoint. That pins the first signed-in account, which on a machine with several is often the wrong one; without it Gmail composes from whichever account is active. The support address goes through a new srfm_support_email_address filter, so a reseller can point it at their own inbox rather than ours. Renamed from get_support_mailto_url, which would now describe the wrong thing. The mailto check in both renderers stays: srfm_action_items is public, so a third party can still contribute one, and that must open the mail client rather than a browser tab. --- admin/admin.php | 96 ++++++++++++++++++++++++++---- src/admin/dashboard/ActionItems.js | 9 ++- tests/unit/admin/test-admin.php | 74 +++++++++++++++++++---- 3 files changed, 152 insertions(+), 27 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 091d053ce..4fdb10b91 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -88,6 +88,28 @@ class Admin { */ public const THANKYOU_PROMPT_NOTICE_ID = 'srfm-thankyou-prompt'; + /** + * Gmail's compose endpoint, used by the Contact Support action. + * + * No `/u/0/`: that pins the first signed-in account, which on a machine with + * several is often the wrong one. Without it Gmail composes from whichever + * account is active. + * + * @since 2.12.6 + */ + private const GMAIL_COMPOSE_URL = 'https://mail.google.com/mail/'; + + /** + * Ceiling for the generated support URL, in characters. + * + * Gmail truncates a long `body` without saying so, and browsers have their own + * limits. 2000 is the length every current browser handles, and it leaves the + * diagnostics block plus roughly the most recent few log entries. + * + * @since 2.12.6 + */ + private const SUPPORT_URL_BUDGET = 2000; + /** * Dashboard widget entries data. * @@ -2764,8 +2786,18 @@ public function enqueue_notice_response_script() { 'srfm-notice-response', 'srfmNoticeResponse', [ - 'ajaxurl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'srfm_notice_response' ), + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'srfm_notice_response' ), + // Carousel chrome. Built in the browser rather than printed here so + // that with JavaScript off every notice simply stays visible, which + // is the behaviour this replaced -- controls that cannot work must + // not be what hides a warning. + 'carousel' => [ + 'previous' => __( 'Previous notice', 'sureforms' ), + 'next' => __( 'Next notice', 'sureforms' ), + /* translators: 1: current position, 2: total notices. */ + 'counter' => __( '%1$d of %2$d', 'sureforms' ), + ], ] ); } @@ -3426,7 +3458,7 @@ public function render_action_item_notices() { // but they are not the same kind of message and should not look alike. $class = 'error' === $status ? 'notice-error' : 'notice-warning'; ?> -
+

$copy['message'], 'cta_label' => __( 'Contact Support', 'sureforms' ), - 'cta_url' => $this->get_support_mailto_url( $category, $form_title ), + 'cta_url' => $this->get_support_email_url( $category, $form_title ), 'cta_action' => 'contact_support', 'dismissible' => false, ]; @@ -4334,10 +4366,21 @@ private function track_action_item_impressions( $warnings ) { * ticket titled "form submissions are failing" and a count that belonged to a * different counter -- wrong at a glance and routed to the wrong place. * - * The log is pasted into the body rather than attached because mailto has no - * attachment parameter -- browsers drop any attempt to add one -- and it is a - * tail rather than the whole file because a megabyte of JSON would exceed the - * URL length every mail client enforces. + * Opens Gmail's compose window rather than handing off to `mailto:`. A mailto + * goes to whatever the machine has registered as its mail handler, which on a + * machine with none configured opens nothing at all; this always lands + * somewhere the person can see and send. + * + * The trade is that someone who does not use Gmail on the web gets a compose + * window for an account they may not want. They can still copy the message out + * of it, and it is a visible window rather than a link that appears to do + * nothing. + * + * The log is pasted into the body rather than attached because neither a mailto + * nor a compose URL can carry an attachment, and it is a tail rather than the + * whole file because a megabyte of JSON would exceed any URL length. Gmail + * truncates a long `body` silently, so the log is trimmed to a budget below -- + * losing the oldest entries visibly beats losing the newest without saying so. * * @param string $category One of Client_Logger::CATEGORIES. Unknown or absent * gets neutral wording rather than a specific claim. @@ -4345,7 +4388,7 @@ private function track_action_item_impressions( $warnings ) { * @since 2.12.6 * @return string */ - private function get_support_mailto_url( $category = '', $form_title = '' ) { + private function get_support_email_url( $category = '', $form_title = '' ) { $copy = $this->get_support_copy( $category ); $subject = sprintf( @@ -4377,15 +4420,42 @@ private function get_support_mailto_url( $category = '', $form_title = '' ) { } } - return 'mailto:support@sureforms.com?' . http_build_query( + /** + * Filter the support address the Contact Support action writes to. + * + * @since 2.12.6 + * + * @param string $address Support inbox. + */ + $address = Helper::get_string_value( apply_filters( 'srfm_support_email_address', 'support@sureforms.com' ) ); + + // Budget for the whole URL. Gmail drops the overflow of an over-long body + // without a word, and the overflow is the end of the log -- the newest + // entries, the ones describing the failure being reported. Trimming here + // instead means the oldest go first and the note below says so. + $base = self::GMAIL_COMPOSE_URL . '?' . http_build_query( [ - 'subject' => $subject, - 'body' => $body, + 'view' => 'cm', + 'fs' => '1', + 'to' => $address, + 'su' => $subject, ], '', '&', PHP_QUERY_RFC3986 - ); + ) . '&body='; + + $room = self::SUPPORT_URL_BUDGET - strlen( $base ); + + if ( strlen( rawurlencode( $body ) ) > $room ) { + // Cut on the raw string, then re-encode: slicing an encoded string can + // land inside a percent-escape and produce a malformed URL. + $budget = max( 0, (int) floor( $room / 3 ) ); + $body = mb_substr( $body, 0, $budget ); + $body .= "\r\n\r\n" . __( 'This log was shortened to fit. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); + } + + return $base . rawurlencode( $body ); } /** diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 579a542a7..548ca8132 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -91,15 +91,18 @@ export default () => { name: item.cta_action, label: item.cta_label, url: item.cta_url, - // A mailto: must open in the mail client, not a new tab. + // Contact Support is a Gmail compose URL, so it opens in a new + // tab like any other link. The mailto check stays because + // srfm_action_items is public and a third party can still + // contribute one, which must go to the mail client instead. external: ! item.cta_url?.startsWith( 'mailto:' ), }, ] : [] ), ]; - // The mailto now carries the log in its body, so the link just works -- no - // download to trigger, nothing to intercept. + // The support link carries the log in its body, so the click just opens a + // composed message -- no download to trigger, nothing to intercept. const handleFix = ( item, action ) => () => post( 'srfm_notice_response', srfm_admin?.notice_response_nonce, { notice_id: item.id, diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index bf5ce2b3b..ecaf20a18 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1251,14 +1251,14 @@ public function test_handle_notice_response_accepts_the_notification_guide() { * carrying a count read from the submission counter -- often zero, and always * about something else. Wrong at a glance, and routed to the wrong queue. */ - public function test_get_support_mailto_url_describes_the_failure_that_happened() { + public function test_get_support_email_url_describes_the_failure_that_happened() { delete_option( Client_Logger::FAILURES_OPTION ); Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); Client_Logger::record_failure( 'integration', 43, 'Job Application' ); - $method = new ReflectionMethod( Admin::class, 'get_support_mailto_url' ); + $method = new ReflectionMethod( Admin::class, 'get_support_email_url' ); $method->setAccessible( true ); $parts = static function ( $url ) { @@ -1272,11 +1272,11 @@ public function test_get_support_mailto_url_describes_the_failure_that_happened( // Three distinct subjects. Asserted against each other as well as their own // text, because the failure mode here was one subject serving all three. - $this->assertStringContainsString( 'form submissions are failing', $submission['subject'] ); - $this->assertStringContainsString( 'notification emails are not being sent', $notification['subject'] ); - $this->assertStringContainsString( 'integration is not receiving entries', $integration['subject'] ); - $this->assertNotSame( $submission['subject'], $notification['subject'] ); - $this->assertNotSame( $notification['subject'], $integration['subject'] ); + $this->assertStringContainsString( 'form submissions are failing', $submission['su'] ); + $this->assertStringContainsString( 'notification emails are not being sent', $notification['su'] ); + $this->assertStringContainsString( 'integration is not receiving entries', $integration['su'] ); + $this->assertNotSame( $submission['su'], $notification['su'] ); + $this->assertNotSame( $notification['su'], $integration['su'] ); // The body must not claim submissions are failing when they are not. $this->assertStringNotContainsString( @@ -1293,6 +1293,58 @@ public function test_get_support_mailto_url_describes_the_failure_that_happened( // The form is named, which is the first thing support asks for. $this->assertStringContainsString( 'Job Application', $integration['body'] ); + + // Gmail, not a mailto. A mailto goes to whatever the machine registered as + // its mail handler, and on one with none configured it opens nothing. + $url = $method->invoke( Admin::get_instance(), 'submission', 'Contact Form' ); + $this->assertStringStartsWith( 'https://mail.google.com/mail/', $url ); + $this->assertStringNotContainsString( 'mailto:', $url ); + $this->assertStringContainsString( 'view=cm', $url ); + $this->assertStringContainsString( 'to=support%40sureforms.com', $url ); + } + + /** + * The URL stays inside a length Gmail will not silently truncate. + * + * Gmail drops the overflow of a long body without a word, and the overflow is + * the end of the log -- the newest entries, the ones describing the failure + * being reported. Trimmed here instead, oldest first, with a line saying so. + */ + public function test_get_support_email_url_stays_within_the_length_gmail_accepts() { + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::clear(); + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + + // Far more log than can fit, so the trim has to engage. + for ( $i = 0; $i < 40; $i++ ) { + Client_Logger::append( + [ + 'type' => 'network', + 'status' => 500, + 'form_id' => 42, + 'form_title' => 'Contact Form', + 'message' => 'Submission responded 500 (text/html): a long server failure message repeated to fill the log ' . $i, + 'duration_ms' => 900, + ] + ); + } + + $method = new ReflectionMethod( Admin::class, 'get_support_email_url' ); + $method->setAccessible( true ); + $url = $method->invoke( Admin::get_instance(), 'submission', 'Contact Form' ); + + $this->assertLessThanOrEqual( + 2000, + strlen( $url ), + 'A longer URL is truncated by Gmail, and what it drops is the newest log entries.' + ); + + // Still a usable email, not a stub: the subject and the diagnostics survive. + parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + $this->assertStringContainsString( 'form submissions are failing', $query['su'] ); + $this->assertStringContainsString( 'Hello SureForms support', $query['body'] ); + + Client_Logger::clear(); } /** @@ -1364,10 +1416,10 @@ public function test_render_action_item_notices_puts_the_guide_before_contact_su * srfm_action_items is public, so an item can arrive with no category at all. * Defaulting to the submission copy would state something that may not be true. */ - public function test_get_support_mailto_url_stays_neutral_without_a_category() { + public function test_get_support_email_url_stays_neutral_without_a_category() { delete_option( Client_Logger::FAILURES_OPTION ); - $method = new ReflectionMethod( Admin::class, 'get_support_mailto_url' ); + $method = new ReflectionMethod( Admin::class, 'get_support_email_url' ); $method->setAccessible( true ); parse_str( @@ -1375,8 +1427,8 @@ public function test_get_support_mailto_url_stays_neutral_without_a_category() { $query ); - $this->assertStringContainsString( 'a problem with the forms on', $query['subject'] ); - $this->assertStringNotContainsString( 'form submissions are failing', $query['subject'] ); + $this->assertStringContainsString( 'a problem with the forms on', $query['su'] ); + $this->assertStringNotContainsString( 'form submissions are failing', $query['su'] ); $this->assertStringNotContainsString( 'form submission that could not', $query['body'] ); } From e7f3a9b8768a882348cb6b9ae8d63fe11b846461 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 14:25:14 +0530 Subject: [PATCH 17/58] feat: show the stacked admin notices one at a time Four faults at once pushed the actual page below the fold on every admin screen, so SureForms' notices became the page. They now step through one at a time, with the count and arrows pinned to the top right of whichever notice is being read. Built in the browser rather than printed by PHP, which is the part worth keeping if this is ever changed: with JavaScript off every notice stays visible exactly as before. A control that cannot run must not be the thing that hides a warning. The controls are moved between notices rather than duplicated, so the count always sits with the message it counts. Positioned absolutely instead of floated so they cannot reflow the text, with padding reserved on that side -- a long form title would otherwise run underneath them. The counter is aria-live, since stepping swaps the text above it with no other signal, and it wraps at both ends so a run can be read round without hunting for the end. Engages at two notices. One needs no chrome, and the unrelated notices other plugins print are left alone -- only the action-item notices carry the class the carousel looks for. --- admin/assets/js/notice-response.js | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js index 76bce2380..0f54db494 100644 --- a/admin/assets/js/notice-response.js +++ b/admin/assets/js/notice-response.js @@ -82,4 +82,115 @@ } } ); } ); + + // ------------------------------------------------------------------ + // Carousel for the action-item notices + // ------------------------------------------------------------------ + + /** + * Show SureForms' stacked admin notices one at a time. + * + * Four faults at once pushed the actual page below the fold on every admin + * screen, so the notices became the page. One at a time with a count keeps the + * warning visible without taking over. + * + * Built here rather than printed by PHP so that with JavaScript off every + * notice simply stays visible, exactly as before. Controls that cannot work + * must not be the thing that hides a warning. + */ + function buildNoticeCarousel() { + const cards = Array.prototype.slice.call( + document.querySelectorAll( '.srfm-action-item-notice' ) + ); + + // One notice needs no chrome, and none needs nothing. + if ( cards.length < 2 ) { + return; + } + + const labels = ( srfmNoticeResponse && srfmNoticeResponse.carousel ) || {}; + let index = 0; + + // Wrap in place, so the cards keep the position WordPress gave them + // rather than being moved to the end of the screen. + const wrap = document.createElement( 'div' ); + wrap.className = 'srfm-action-item-carousel'; + cards[ 0 ].parentNode.insertBefore( wrap, cards[ 0 ] ); + cards.forEach( function ( notice ) { + wrap.appendChild( notice ); + } ); + + // Pinned to the top right of the notice being read. Absolute rather than a + // float so it cannot reflow the message text, and the notice gets padding on + // that side to reserve the space -- a long form title would otherwise run + // underneath the controls. + const nav = document.createElement( 'p' ); + nav.className = 'srfm-action-item-carousel-nav'; + nav.style.display = 'flex'; + nav.style.alignItems = 'center'; + nav.style.gap = '8px'; + nav.style.position = 'absolute'; + nav.style.top = '8px'; + nav.style.right = '12px'; + nav.style.margin = '0'; + + cards.forEach( function ( notice ) { + notice.style.position = 'relative'; + notice.style.paddingRight = '130px'; + } ); + + const prev = document.createElement( 'button' ); + prev.type = 'button'; + prev.className = 'button button-small'; + prev.innerHTML = '‹'; + prev.setAttribute( 'aria-label', labels.previous || 'Previous' ); + + const next = document.createElement( 'button' ); + next.type = 'button'; + next.className = 'button button-small'; + next.innerHTML = '›'; + next.setAttribute( 'aria-label', labels.next || 'Next' ); + + const counter = document.createElement( 'span' ); + // Announced, because stepping swaps the text above with no other signal. + counter.setAttribute( 'aria-live', 'polite' ); + + function render() { + cards.forEach( function ( notice, i ) { + notice.style.display = i === index ? '' : 'none'; + } ); + + // Moved rather than duplicated: one set of controls, always inside the + // notice being read, so the count sits with the message it counts. + cards[ index ].appendChild( nav ); + + counter.textContent = ( labels.counter || '%1$d of %2$d' ) + .replace( '%1$d', index + 1 ) + .replace( '%2$d', cards.length ); + } + + function step( delta ) { + return function () { + // Wraps, so a run of cards can be read round without hunting for + // the end. + index = ( index + delta + cards.length ) % cards.length; + render(); + }; + } + + prev.addEventListener( 'click', step( -1 ) ); + next.addEventListener( 'click', step( 1 ) ); + + nav.appendChild( prev ); + nav.appendChild( counter ); + nav.appendChild( next ); + + render(); + } + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', buildNoticeCarousel ); + } else { + buildNoticeCarousel(); + } }() ); From 638111dc7e79f04cc922e2e3801bbfb18d0626cc Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 16:12:24 +0530 Subject: [PATCH 18/58] feat: replace Contact Support with View details and a diagnostics modal Contact Support sent people straight into a composed message without ever showing them what it contained. Someone reporting a fault on their own site is entitled to read the diagnostics and the log first, and a support agent gets a clean paste instead of a screenshot of a notice. The action is now View details. It opens the same text that used to be posted blind, with Copy details and a Contact Support button pointing at sureforms.com/contact -- a form, which collects the licence and site details support would otherwise have to ask for, by which time the diagnostics are already on the clipboard. get_support_log_block() is extracted so the modal and the support URL share one source. They were built separately, which is how a "details" view drifts from what it claims to show. The wp-admin notice carries the text in the page, hidden beside it, rather than fetching it: a modal that has to make a request can fail at the exact moment someone is trying to report a failure. Written with esc_html and read as textContent, never parsed as HTML -- a log holds whatever a server put in an error message. The href stays as the no-JS path. It goes to the dashboard, where the same details are readable, and the click is only swallowed when the modal actually opens. Analytics gained view_details and copy_details per category, so neither click reports nothing. INCOMPLETE: the modal does not open in either surface. The click handler fires and the navigation is suppressed, so the overlay is being built and then not displayed. Ruled out: force-ui Dialog (a plain overlay fails identically), a stale cached script (forced a refetch), and JS errors (console is clean). The next thing to check is whether the overlay is in the DOM after the click and what is hiding it -- most likely the notice carousel's wrapper or WordPress's own .notice styling. --- admin/admin.php | 129 +++++++++++++++++----- admin/assets/js/notice-response.js | 162 ++++++++++++++++++++++++++++ src/admin/dashboard/ActionItems.js | 168 ++++++++++++++++++++++++++--- tests/unit/admin/test-admin.php | 29 +++-- 4 files changed, 436 insertions(+), 52 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 4fdb10b91..cb3f935c6 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -97,6 +97,17 @@ class Admin { * * @since 2.12.6 */ + /** + * Where the dialog's Contact Support button goes. + * + * A form rather than an inbox: it collects the licence and site details support + * would otherwise have to ask for, and the diagnostics are already on the + * clipboard by the time someone gets here. + * + * @since 2.12.6 + */ + private const SUPPORT_CONTACT_URL = 'https://sureforms.com/contact/'; + private const GMAIL_COMPOSE_URL = 'https://mail.google.com/mail/'; /** @@ -2798,6 +2809,16 @@ public function enqueue_notice_response_script() { /* translators: 1: current position, 2: total notices. */ 'counter' => __( '%1$d of %2$d', 'sureforms' ), ], + // Details modal chrome, translated here so the script carries no + // user-facing English of its own. + 'details' => [ + 'title' => __( 'Details', 'sureforms' ), + 'description' => __( 'What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it.', 'sureforms' ), + 'copy' => __( 'Copy details', 'sureforms' ), + 'copied' => __( 'Copied', 'sureforms' ), + 'contact' => __( 'Contact Support', 'sureforms' ), + 'close' => __( 'Close', 'sureforms' ), + ], ] ); } @@ -2846,15 +2867,21 @@ public function handle_notice_response() { ], // The "Finish setting up" prompt (#3030): three CTAs, plus the ✕. 'form_submission_error' => [ + 'view_details' => 'submission_failure_notice_view', + 'copy_details' => 'submission_failure_notice_copy', 'contact_support' => 'submission_failure_notice_cta', 'dismissed' => 'submission_failure_notice_dismiss', ], 'notification_error' => [ + 'view_details' => 'notification_failure_notice_view', + 'copy_details' => 'notification_failure_notice_copy', 'contact_support' => 'notification_failure_notice_cta', 'help_me_fix' => 'notification_failure_notice_guide', 'dismissed' => 'notification_failure_notice_dismiss', ], 'integration_error' => [ + 'view_details' => 'integration_failure_notice_view', + 'copy_details' => 'integration_failure_notice_copy', 'contact_support' => 'integration_failure_notice_cta', 'dismissed' => 'integration_failure_notice_dismiss', ], @@ -3485,7 +3512,16 @@ class="button button-primary" class="" data-srfm-notice-id="" data-srfm-button="" - + > @@ -3495,6 +3531,20 @@ class=""

+ + + +
$copy['message'], - 'cta_label' => __( 'Contact Support', 'sureforms' ), - 'cta_url' => $this->get_support_email_url( $category, $form_title ), - 'cta_action' => 'contact_support', + // Shows what would be sent before anything is sent. Someone reporting + // a fault on their own site is entitled to read the diagnostics and + // the log first, and a support agent gets a cleaner paste than a + // screenshot of a notice. + 'cta_label' => __( 'View details', 'sureforms' ), + // Where the classic wp-admin notice sends people, since it cannot open + // the panel's dialog. The dashboard is where the details are readable. + 'cta_url' => admin_url( 'admin.php?page=sureforms_menu' ), + 'cta_action' => 'view_details', + // The dialog's own contents. Plain text: it is rendered inside a
+				// and copied verbatim, so any markup here would be read as characters.
+				'details'     => $this->get_support_message( $category, $form_title )
+					. "\r\n\r\n" . $this->get_support_log_block(),
+				'support_url' => self::SUPPORT_CONTACT_URL,
 				'dismissible' => false,
 			];
 
@@ -4354,6 +4415,42 @@ private function track_action_item_impressions( $warnings ) {
 		}
 	}
 
+	/**
+	 * The log tail, formatted for pasting.
+	 *
+	 * Shared by the support URL and the dialog, so the text someone reads before
+	 * sending is the text that gets sent. They used to be built separately, which
+	 * is how a "details" view drifts from what it claims to show.
+	 *
+	 * @since 2.12.6
+	 * @return string
+	 */
+	private function get_support_log_block() {
+		$log   = Client_Logger::get_tail();
+		$block = '---' . "\r\n";
+
+		if ( '' === $log['text'] ) {
+			return $block . __( 'Debug log: no entries recorded.', 'sureforms' );
+		}
+
+		$block .= sprintf(
+			/* translators: 1: entries shown, 2: entries recorded. */
+			__( 'Debug log (most recent %1$d of %2$d entries)', 'sureforms' ),
+			$log['shown'],
+			$log['total']
+		) . "\r\n";
+
+		// Fenced so it survives a reply and reads as data rather than prose wherever
+		// Markdown is rendered.
+		$block .= '```' . "\r\n" . str_replace( "\n", "\r\n", $log['text'] ) . "\r\n" . '```';
+
+		if ( $log['shown'] < $log['total'] ) {
+			$block .= "\r\n\r\n" . __( 'Older entries were left out to keep this within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' );
+		}
+
+		return $block;
+	}
+
 	/**
 	 * Pre-addressed support email for one kind of failure.
 	 *
@@ -4397,28 +4494,8 @@ private function get_support_email_url( $category = '', $form_title = '' ) {
 			Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) )
 		);
 
-		$log   = Client_Logger::get_tail();
-		$body  = $this->get_support_message( $category, $form_title );
-		$body .= "\r\n\r\n" . '---' . "\r\n";
-
-		if ( '' === $log['text'] ) {
-			$body .= __( 'Debug log: no entries recorded.', 'sureforms' );
-		} else {
-			$body .= sprintf(
-				/* translators: 1: entries shown, 2: entries recorded. */
-				__( 'Debug log (most recent %1$d of %2$d entries)', 'sureforms' ),
-				$log['shown'],
-				$log['total']
-			) . "\r\n";
-
-			// Fenced so it survives a reply and reads as data rather than prose in
-			// clients that render Markdown.
-			$body .= '```' . "\r\n" . str_replace( "\n", "\r\n", $log['text'] ) . "\r\n" . '```';
-
-			if ( $log['shown'] < $log['total'] ) {
-				$body .= "\r\n\r\n" . __( 'Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' );
-			}
-		}
+		$body = $this->get_support_message( $category, $form_title )
+			. "\r\n\r\n" . $this->get_support_log_block();
 
 		/**
 		 * Filter the support address the Contact Support action writes to.
diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js
index 0f54db494..72bdc085c 100644
--- a/admin/assets/js/notice-response.js
+++ b/admin/assets/js/notice-response.js
@@ -193,4 +193,166 @@
 	} else {
 		buildNoticeCarousel();
 	}
+
+	// ------------------------------------------------------------------
+	// Details modal
+	// ------------------------------------------------------------------
+
+	/**
+	 * Show what would be sent to support, before anything is sent.
+	 *
+	 * The text is already in the page, hidden next to its notice, so opening this
+	 * makes no request -- a modal that has to fetch can fail at the exact moment
+	 * someone is trying to report a failure.
+	 *
+	 * Read with textContent and written with textContent, never innerHTML: the log
+	 * contains whatever a server or a browser put in an error message, and that is
+	 * not markup to be trusted.
+	 *
+	 * @param {string} noticeId Item id, matched against the hidden payload's
+	 *                          data-srfm-details-id.
+	 * @return {boolean} Whether the payload was found and the modal opened.
+	 */
+	function openDetails( noticeId ) {
+		const source = document.querySelector(
+			'.srfm-notice-details[data-srfm-details-id="' + noticeId + '"]'
+		);
+
+		if ( ! source ) {
+			return false;
+		}
+
+		const labels = ( srfmNoticeResponse && srfmNoticeResponse.details ) || {};
+		const text = source.textContent || '';
+		const supportUrl = source.getAttribute( 'data-srfm-support-url' ) || '';
+
+		const overlay = document.createElement( 'div' );
+		overlay.style.cssText =
+			'position:fixed;inset:0;z-index:100000;display:flex;align-items:center;' +
+			'justify-content:center;background:rgba(0,0,0,.5);padding:16px;';
+
+		const panel = document.createElement( 'div' );
+		panel.setAttribute( 'role', 'dialog' );
+		panel.setAttribute( 'aria-modal', 'true' );
+		panel.setAttribute( 'aria-label', labels.title || 'Details' );
+		panel.style.cssText =
+			'background:#fff;border-radius:8px;padding:16px;width:100%;' +
+			'max-width:720px;box-shadow:0 10px 30px rgba(0,0,0,.2);';
+
+		const heading = document.createElement( 'h2' );
+		heading.textContent = labels.title || 'Details';
+		heading.style.cssText = 'margin:0 0 4px;font-size:14px;';
+
+		const description = document.createElement( 'p' );
+		description.textContent = labels.description || '';
+		description.style.cssText = 'margin:0 0 12px;color:#50575e;';
+
+		// Selectable and scrollable, because clipboard access can be refused and
+		// then selecting by hand is the only way through.
+		const pre = document.createElement( 'pre' );
+		pre.textContent = text;
+		pre.style.cssText =
+			'margin:0;max-height:320px;overflow:auto;white-space:pre-wrap;' +
+			'word-break:break-word;background:#f6f7f7;padding:12px;' +
+			'border-radius:6px;font-size:12px;';
+
+		const actions = document.createElement( 'p' );
+		actions.style.cssText =
+			'display:flex;gap:8px;justify-content:flex-end;margin:12px 0 0;';
+
+		const copy = document.createElement( 'button' );
+		copy.type = 'button';
+		copy.className = 'button';
+		copy.textContent = labels.copy || 'Copy details';
+
+		const contact = document.createElement( 'a' );
+		contact.className = 'button button-primary';
+		contact.href = supportUrl;
+		contact.target = '_blank';
+		contact.rel = 'noopener noreferrer';
+		contact.textContent = labels.contact || 'Contact Support';
+
+		const close = document.createElement( 'button' );
+		close.type = 'button';
+		close.className = 'button-link';
+		close.textContent = labels.close || 'Close';
+		close.setAttribute( 'aria-label', labels.close || 'Close' );
+
+		function dismiss() {
+			document.removeEventListener( 'keydown', onKey );
+			overlay.remove();
+		}
+
+		function onKey( e ) {
+			if ( e.key === 'Escape' ) {
+				dismiss();
+			}
+		}
+
+		copy.addEventListener( 'click', function () {
+			const done = function () {
+				copy.textContent = labels.copied || 'Copied';
+				// Reverts on its own: a button stuck on "Copied" says nothing about
+				// the next click.
+				window.setTimeout( function () {
+					copy.textContent = labels.copy || 'Copy details';
+				}, 2000 );
+				sendResponse( noticeId, 'copy_details' );
+			};
+
+			if ( navigator.clipboard && navigator.clipboard.writeText ) {
+				navigator.clipboard.writeText( text ).then( done, function () {
+					// Refused (insecure origin, permission policy). The text is on
+					// screen and selectable, so say nothing rather than claim it
+					// was copied.
+				} );
+				return;
+			}
+
+			pre.focus();
+		} );
+
+		contact.addEventListener( 'click', function () {
+			sendResponse( noticeId, 'contact_support' );
+		} );
+
+		close.addEventListener( 'click', dismiss );
+		overlay.addEventListener( 'click', function ( e ) {
+			// Backdrop only: a click inside the panel must not close it while
+			// someone is selecting the text.
+			if ( e.target === overlay ) {
+				dismiss();
+			}
+		} );
+		document.addEventListener( 'keydown', onKey );
+
+		actions.appendChild( close );
+		actions.appendChild( copy );
+		actions.appendChild( contact );
+		panel.appendChild( heading );
+		panel.appendChild( description );
+		panel.appendChild( pre );
+		panel.appendChild( actions );
+		overlay.appendChild( panel );
+		document.body.appendChild( overlay );
+
+		copy.focus();
+
+		return true;
+	}
+
+	document.addEventListener( 'click', function ( e ) {
+		const trigger = e.target.closest( '[data-srfm-details-for]' );
+
+		if ( ! trigger ) {
+			return;
+		}
+
+		// Only swallow the navigation if the modal actually opened. If the payload
+		// is missing the href still goes to the dashboard, which is where the same
+		// details are readable.
+		if ( openDetails( trigger.getAttribute( 'data-srfm-details-for' ) ) ) {
+			e.preventDefault();
+		}
+	} );
 }() );
diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js
index 548ca8132..073f38117 100644
--- a/src/admin/dashboard/ActionItems.js
+++ b/src/admin/dashboard/ActionItems.js
@@ -1,7 +1,15 @@
 import { __ } from '@wordpress/i18n';
 import { useState } from '@wordpress/element';
 import { Button, Label } from '@bsf/force-ui';
-import { CircleCheck, TriangleAlert, ChevronUp, ChevronDown } from 'lucide-react';
+import {
+	CircleCheck,
+	TriangleAlert,
+	ChevronUp,
+	ChevronDown,
+	Check,
+	Copy,
+	X,
+} from 'lucide-react';
 
 /**
  * Dashboard panel listing what SureForms has checked on this site.
@@ -31,6 +39,10 @@ const ICONS = {
 export default () => {
 	const [ dismissed, setDismissed ] = useState( [] );
 	const [ open, setOpen ] = useState( true );
+	// The item whose details are being read, or null. Holds the item rather than a
+	// boolean so the dialog keeps rendering the right one while it closes.
+	const [ details, setDetails ] = useState( null );
+	const [ copied, setCopied ] = useState( false );
 
 	const items = ( srfm_admin?.action_items || [] ).filter(
 		( item ) => ! dismissed.includes( item.id )
@@ -91,16 +103,39 @@ export default () => {
 					name: item.cta_action,
 					label: item.cta_label,
 					url: item.cta_url,
-					// Contact Support is a Gmail compose URL, so it opens in a new
-					// tab like any other link. The mailto check stays because
-					// srfm_action_items is public and a third party can still
-					// contribute one, which must go to the mail client instead.
+					// An item carrying details opens them here rather than
+					// navigating: the point is to read the diagnostics before
+					// sending them anywhere. cta_url stays as the fallback the
+					// classic wp-admin notice uses, since it cannot open a dialog.
+					dialog: !! item.details,
+					// A mailto must reach the mail client, not a browser tab.
+					// srfm_action_items is public, so one can still arrive that way.
 					external: ! item.cta_url?.startsWith( 'mailto:' ),
 				},
 			  ]
 			: [] ),
 	];
 
+	const handleCopy = ( item ) => async () => {
+		try {
+			await navigator.clipboard.writeText( item.details || '' );
+			setCopied( true );
+			// Reverts on its own: a button stuck on "Copied" says nothing about the
+			// next click.
+			setTimeout( () => setCopied( false ), 2000 );
+		} catch ( e ) {
+			// Clipboard access can be refused outright (an insecure origin, a
+			// permission policy). The text is on screen and selectable, so there is
+			// nothing to recover -- just do not claim it was copied.
+			setCopied( false );
+		}
+
+		post( 'srfm_notice_response', srfm_admin?.notice_response_nonce, {
+			notice_id: item.id,
+			button: 'copy_details',
+		} );
+	};
+
 	// The support link carries the log in its body, so the click just opens a
 	// composed message -- no download to trigger, nothing to intercept.
 	const handleFix = ( item, action ) => () =>
@@ -183,16 +218,34 @@ export default () => {
 												key={ action.name }
 												variant="link"
 												size="xs"
-												tag="a"
-												href={ action.url }
-												{ ...( action.external && {
-													target: '_blank',
-													rel: 'noopener noreferrer',
-												} ) }
-												onClick={ handleFix(
-													item,
-													action.name
-												) }
+												{ ...( action.dialog
+													? {
+														onClick: ( e ) => {
+															e.preventDefault();
+															setCopied(
+																false
+															);
+															setDetails(
+																item
+															);
+															handleFix(
+																item,
+																action.name
+															)();
+														},
+													  }
+													: {
+														tag: 'a',
+														href: action.url,
+														onClick: handleFix(
+															item,
+															action.name
+														),
+														...( action.external && {
+															target: '_blank',
+															rel: 'noopener noreferrer',
+														} ),
+													  } ) }
 												className={ `font-medium focus:outline-none focus:[box-shadow:none] [&>span]:px-0${
 													index > 0
 														? ' text-text-secondary'
@@ -209,6 +262,91 @@ export default () => {
 					) ) }
 				
) } + { /* Read before send: the same text a support request needs, so it can + be pasted rather than described. + + A plain overlay rather than force-ui's Dialog because the identical + modal has to exist for the classic wp-admin notices, which have no + React. One implementation, one behaviour, nothing to keep in step. */ } + { !! details && ( +
{ + // Backdrop only. A click inside the panel must not close it + // while someone is selecting the text. + if ( e.target === e.currentTarget ) { + setDetails( null ); + } + } } + > +
+
+ + +
+ + { /* Selectable and scrollable: clipboard access can be + refused, and then selecting by hand is the only way + through. */ } +
+							{ details.details || '' }
+						
+
+ + +
+
+
+ ) }
); }; diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index ecaf20a18..61d8e9e03 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1175,8 +1175,12 @@ public function test_get_action_items_offers_a_guide_for_notification_failures() $this->assertSame( 'help_me_fix', $items['notification_error']['guide_action'] ?? '' ); $this->assertNotEmpty( $items['notification_error']['guide_label'] ?? '' ); - // Contact Support is still there; the guide is an addition, not a swap. - $this->assertSame( 'contact_support', $items['notification_error']['cta_action'] ?? '' ); + // The details action is still there; the guide is an addition, not a swap. + $this->assertSame( 'view_details', $items['notification_error']['cta_action'] ?? '' ); + $this->assertNotEmpty( + $items['notification_error']['details'] ?? '', + 'The dialog has nothing to show without the details payload.' + ); // A category with no guide must not carry empty guide keys, which both // renderers treat as "render a second button". @@ -1356,7 +1360,7 @@ public function test_get_support_email_url_stays_within_the_length_gmail_accepts * here, because a swap that only reorders would leave the secondary styling on * the leading action. */ - public function test_render_action_item_notices_puts_the_guide_before_contact_support() { + public function test_render_action_item_notices_puts_the_guide_before_the_details_action() { wp_set_current_user( $this->make_user( 'administrator' ) ); delete_option( Client_Logger::FAILURES_OPTION ); Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); @@ -1375,11 +1379,11 @@ public function test_render_action_item_notices_puts_the_guide_before_contact_su $guide = strpos( $html, 'help_me_fix' ); $this->assertNotFalse( $guide, 'The notification guide must render.' ); - // The support link that follows it, not the one in the submission notice + // The details action that follows it, not the one in the submission notice // above, so this measures order within the same notice. - $support_after_guide = strpos( $html, 'contact_support', $guide ); - $this->assertNotFalse( $support_after_guide, 'Contact Support must follow the guide.' ); - $this->assertGreaterThan( $guide, $support_after_guide ); + $details_after_guide = strpos( $html, 'view_details', $guide ); + $this->assertNotFalse( $details_after_guide, 'View details must follow the guide.' ); + $this->assertGreaterThan( $guide, $details_after_guide ); // Whichever action leads carries the primary button. $this->assertMatchesRegularExpression( @@ -1388,7 +1392,7 @@ public function test_render_action_item_notices_puts_the_guide_before_contact_su 'The leading action must be the primary button.' ); $this->assertMatchesRegularExpression( - '/class="button"[^>]*data-srfm-button="contact_support"/', + '/class="button"[^>]*data-srfm-button="view_details"/', $html, 'The following action must be secondary.' ); @@ -1404,9 +1408,9 @@ public function test_render_action_item_notices_puts_the_guide_before_contact_su $pagenow = $previous_pagenow; $this->assertMatchesRegularExpression( - '/class="button button-primary"[^>]*data-srfm-button="contact_support"/', + '/class="button button-primary"[^>]*data-srfm-button="view_details"/', $alone, - 'With no guide, Contact Support leads and stays primary.' + 'With no guide, View details leads and stays primary.' ); } @@ -1651,7 +1655,10 @@ public function test_render_action_item_notices() { $output = ob_get_clean(); $this->assertStringContainsString( 'notice-error', $output, $screen . ' must show the notice.' ); - $this->assertStringContainsString( 'Contact Support', $output ); + // Contact Support moved into the details modal; the notice itself offers + // the diagnostics first. + $this->assertStringContainsString( 'View details', $output ); + $this->assertStringContainsString( 'srfm-notice-details', $output, 'The modal payload must ship with the notice.' ); } } From 2b3e865d3082354e0dc2c6b198fc69f406f6db1e Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 16:16:27 +0530 Subject: [PATCH 19/58] fix: keep the support email's line breaks in Gmail The body arrived in Gmail's compose window as one paragraph. The diagnostics and the log ran together, which is the opposite of the point: support has to be able to read it at a glance. Cause was CRLF. That is the mailto convention and correct when a mail client parses the URL, but Gmail's compose does not honour the carriage returns, so every break was dropped. Nothing parses this as a mail header any more -- the mailto path is gone -- so CRLF had no consumer left. Now bare LF throughout, which Gmail keeps: 22 encoded newlines in the URL and a 23-line body where there was previously one run of text. The log's own newlines were already LF, so the rewrite that normalised them is gone too. Covered by a test asserting the URL carries no %0D%0A and the body no carriage return, with a floor on the line count -- one long line is the failure being fixed, and it would otherwise look like a pass. --- admin/admin.php | 20 ++++++++++++------- tests/unit/admin/test-admin.php | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 4fdb10b91..00a0e0626 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -4397,9 +4397,14 @@ private function get_support_email_url( $category = '', $form_title = '' ) { Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ) ); + // Bare LF, not CRLF. CRLF is the mailto convention and correct when a mail + // client parses the URL, but Gmail's compose window does not honour the + // carriage returns and runs the whole body together as one paragraph -- the + // diagnostics and the log arrive as a wall of text. Nothing parses this as + // a mail header any more, so there is no reason left to send CRLF. $log = Client_Logger::get_tail(); $body = $this->get_support_message( $category, $form_title ); - $body .= "\r\n\r\n" . '---' . "\r\n"; + $body .= "\n\n" . '---' . "\n"; if ( '' === $log['text'] ) { $body .= __( 'Debug log: no entries recorded.', 'sureforms' ); @@ -4409,14 +4414,15 @@ private function get_support_email_url( $category = '', $form_title = '' ) { __( 'Debug log (most recent %1$d of %2$d entries)', 'sureforms' ), $log['shown'], $log['total'] - ) . "\r\n"; + ) . "\n"; // Fenced so it survives a reply and reads as data rather than prose in - // clients that render Markdown. - $body .= '```' . "\r\n" . str_replace( "\n", "\r\n", $log['text'] ) . "\r\n" . '```'; + // clients that render Markdown. The log's own newlines are already LF, + // so nothing needs rewriting. + $body .= '```' . "\n" . $log['text'] . "\n" . '```'; if ( $log['shown'] < $log['total'] ) { - $body .= "\r\n\r\n" . __( 'Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); + $body .= "\n\n" . __( 'Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); } } @@ -4452,7 +4458,7 @@ private function get_support_email_url( $category = '', $form_title = '' ) { // land inside a percent-escape and produce a malformed URL. $budget = max( 0, (int) floor( $room / 3 ) ); $body = mb_substr( $body, 0, $budget ); - $body .= "\r\n\r\n" . __( 'This log was shortened to fit. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); + $body .= "\n\n" . __( 'This log was shortened to fit. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); } return $base . rawurlencode( $body ); @@ -4572,7 +4578,7 @@ private function get_support_message( $category = '', $form_title = '' ) { ] ); - return implode( "\r\n", $lines ); + return implode( "\n", $lines ); } /** diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index ecaf20a18..614a8712c 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1303,6 +1303,41 @@ public function test_get_support_email_url_describes_the_failure_that_happened() $this->assertStringContainsString( 'to=support%40sureforms.com', $url ); } + /** + * The body uses bare LF, so Gmail keeps the line breaks. + * + * CRLF is the mailto convention and correct when a mail client parses the URL, + * but Gmail's compose window ignores the carriage returns and runs the whole + * body together as one paragraph -- the diagnostics and the log arrive as a wall + * of text. Nothing parses this as a mail header any more, so CRLF has no + * consumer left. + */ + public function test_get_support_email_url_keeps_line_breaks_gmail_will_honour() { + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::clear(); + Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); + + $method = new ReflectionMethod( Admin::class, 'get_support_email_url' ); + $method->setAccessible( true ); + $url = $method->invoke( Admin::get_instance(), 'notification', 'Contact Form' ); + + $this->assertStringNotContainsString( + '%0D%0A', + $url, + 'A carriage return collapses the body to one paragraph in Gmail.' + ); + $this->assertStringContainsString( '%0A', $url, 'The body must still be broken into lines.' ); + + parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + $this->assertStringNotContainsString( "\r", $query['body'] ); + $this->assertGreaterThan( + 5, + substr_count( $query['body'], "\n" ), + 'The diagnostics block is several lines; one long line means the breaks were lost.' + ); + } + /** * The URL stays inside a length Gmail will not silently truncate. * From cebd6235aa6882459151a5a4a1fa40caba2fdd08 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 16:22:33 +0530 Subject: [PATCH 20/58] fix: copy the details as HTML so a rich composer keeps the formatting The diagnostics arrived in Gmail as one paragraph. Every line break was gone -- the site details and the log ran together, which is the opposite of the point. The URL was not the problem to solve. Gmail's compose window is a rich-text field: it drops newlines out of plain text whatever the URL encodes, and it escapes any markup passed through the body parameter, so no encoding survives that route. Sending the diagnostics through it was always going to lose the formatting. Copy details now writes both clipboard flavours. A rich composer takes the text/html and keeps every break, with
 holding the log's columns
in line; a plain-text field takes the text/plain unchanged. Where
ClipboardItem is unavailable it falls back to writeText, which loses the
formatting but still beats copying nothing.

The HTML is escaped in the browser, once, in the same place the markup is
built. The source is a log holding whatever a server put into an error
message, so it is never trusted as markup.

Both surfaces share the behaviour: the wp-admin modal and the dashboard
panel escape and wrap identically.
---
 admin/assets/js/notice-response.js | 79 +++++++++++++++++++++++++-----
 src/admin/dashboard/ActionItems.js | 32 +++++++++++-
 2 files changed, 97 insertions(+), 14 deletions(-)

diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js
index 72bdc085c..49060817c 100644
--- a/admin/assets/js/notice-response.js
+++ b/admin/assets/js/notice-response.js
@@ -198,6 +198,70 @@
 	// Details modal
 	// ------------------------------------------------------------------
 
+	/**
+	 * The details as HTML, for the clipboard's text/html flavour.
+	 *
+	 * Gmail's composer is a rich-text field: it drops the newlines out of plain
+	 * text, which is what turned the diagnostics into one paragraph. Pasting HTML
+	 * instead keeps every break, and 
 keeps the log's columns lined up.
+	 *
+	 * Escaped here, not on the server, so the escaping happens once and in the same
+	 * place the markup is built. The source is a log holding whatever a server put
+	 * in an error message, so it is never trusted as markup.
+	 *
+	 * @param {string} text Plain-text details.
+	 * @return {string} Escaped HTML.
+	 */
+	function detailsAsHtml( text ) {
+		const escaped = text
+			.replace( /&/g, '&' )
+			.replace( //g, '>' );
+
+		return (
+			'
' +
+			escaped +
+			'
' + ); + } + + /** + * Put the details on the clipboard in both flavours. + * + * A rich composer takes the HTML and keeps the line breaks; a plain-text field + * takes the text. Falls back to writeText where ClipboardItem is unavailable -- + * that loses the formatting, but losing it is better than copying nothing. + * + * @param {string} text Plain-text details. + * @param {Function} done Called once the clipboard actually holds it. + */ + function copyDetails( text, done ) { + const supportsRich = + window.ClipboardItem && + navigator.clipboard && + navigator.clipboard.write; + + if ( supportsRich ) { + const item = new window.ClipboardItem( { + 'text/plain': new Blob( [ text ], { type: 'text/plain' } ), + 'text/html': new Blob( [ detailsAsHtml( text ) ], { + type: 'text/html', + } ), + } ); + + navigator.clipboard.write( [ item ] ).then( done, function () { + // Refused (insecure origin, permission policy). The text is on + // screen and selectable, so say nothing rather than claim success. + } ); + return; + } + + if ( navigator.clipboard && navigator.clipboard.writeText ) { + navigator.clipboard.writeText( text ).then( done, function () {} ); + } + } + /** * Show what would be sent to support, before anything is sent. * @@ -290,7 +354,7 @@ } copy.addEventListener( 'click', function () { - const done = function () { + copyDetails( text, function () { copy.textContent = labels.copied || 'Copied'; // Reverts on its own: a button stuck on "Copied" says nothing about // the next click. @@ -298,18 +362,7 @@ copy.textContent = labels.copy || 'Copy details'; }, 2000 ); sendResponse( noticeId, 'copy_details' ); - }; - - if ( navigator.clipboard && navigator.clipboard.writeText ) { - navigator.clipboard.writeText( text ).then( done, function () { - // Refused (insecure origin, permission policy). The text is on - // screen and selectable, so say nothing rather than claim it - // was copied. - } ); - return; - } - - pre.focus(); + } ); } ); contact.addEventListener( 'click', function () { diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 073f38117..4613bf7dc 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -117,8 +117,38 @@ export default () => { ]; const handleCopy = ( item ) => async () => { + const text = item.details || ''; + try { - await navigator.clipboard.writeText( item.details || '' ); + // Both flavours. A rich-text composer -- Gmail's, for one -- drops the + // newlines out of plain text, which is what turned the diagnostics into + // one paragraph; pasting HTML keeps every break, and
 keeps the
+			// log's columns lined up. A plain-text field still gets the text.
+			//
+			// Escaped here because the source is a log holding whatever a server put
+			// in an error message. It is never trusted as markup.
+			const html = `
${ text
+				.replace( /&/g, '&' )
+				.replace( //g, '>' ) }
`; + + if ( window.ClipboardItem && navigator.clipboard?.write ) { + await navigator.clipboard.write( [ + new window.ClipboardItem( { + 'text/plain': new Blob( [ text ], { + type: 'text/plain', + } ), + 'text/html': new Blob( [ html ], { + type: 'text/html', + } ), + } ), + ] ); + } else { + // No ClipboardItem: the formatting is lost, which still beats + // copying nothing. + await navigator.clipboard.writeText( text ); + } + setCopied( true ); // Reverts on its own: a button stuck on "Copied" says nothing about the // next click. From 8216d8b1240b8263d7a58ba45d8e25f33f42010a Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 16:34:41 +0530 Subject: [PATCH 21/58] style: drop the underlines from the Form Checks actions Three underlined links stacked in a narrow sidebar column read as a block of noise rather than as three actions. Underlined on hover only now, which is what Quick Access directly below already does -- the two panels were styling the same kind of control two different ways. Applies to Ignore as well: it is the same sort of control and was the odd one out either way. --- src/admin/dashboard/ActionItems.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 548ca8132..65f02b0e6 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -169,7 +169,7 @@ export default () => { variant="link" size="xs" onClick={ handleIgnore( item ) } - className="font-medium focus:outline-none focus:[box-shadow:none] [&>span]:px-0 text-text-secondary shrink-0" + className="font-medium no-underline hover:underline focus:outline-none focus:[box-shadow:none] [&>span]:px-0 text-text-secondary shrink-0" > { __( 'Ignore', 'sureforms' ) } @@ -193,7 +193,12 @@ export default () => { item, action.name ) } - className={ `font-medium focus:outline-none focus:[box-shadow:none] [&>span]:px-0${ + // Underlined on hover only, matching + // Quick Access below it. Three + // underlined links stacked in a narrow + // column read as a block of noise + // rather than as actions. + className={ `font-medium no-underline hover:underline focus:outline-none focus:[box-shadow:none] [&>span]:px-0${ index > 0 ? ' text-text-secondary' : '' From bd62408e3f6651dccfdad6fb35536b6c3c68a0ac Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 7 Sep 2026 16:43:36 +0530 Subject: [PATCH 22/58] fix: open the details with a button, not a link View details opened the dashboard in a new tab -- the same page the reader was already on. Passing onClick through force-ui's Button did not stop it applying tag and href, so the click navigated instead of opening anything. It is a native button now. A control that opens a panel in place should not be an anchor: there is nowhere to navigate to, so there is no href to follow and nothing to open in a new tab. The other actions are real links and keep the Button they had. Also drops the underline from the actions, hover only, matching Quick Access directly below -- three underlined links stacked in a narrow column read as a block of noise rather than as three actions. Still open: the dialog does not render on click. The navigation is gone, which was the reported bug, but the overlay does not appear. Next step is to establish whether it reaches the DOM at all. --- src/admin/dashboard/ActionItems.js | 73 ++++++++++++++++-------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index ea321a0be..799e729dd 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -242,45 +242,48 @@ export default () => {
{ !! actionsFor( item ).length && (
- { actionsFor( item ).map( - ( action, index ) => ( + { actionsFor( item ).map( ( action, index ) => + // A native button for the one that opens the + // dialog. Routed through force-ui's Button it + // followed the href instead -- the same page in + // a new tab -- and a control that navigates is + // the wrong element for something that opens a + // panel in place. + action.dialog ? ( + + ) : ( From f83c389c98f23a979455d14cdcf109adbef5472b Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 8 Sep 2026 15:13:36 +0530 Subject: [PATCH 26/58] feat: keep Contact Support locked until the details are copied The support form asks for the diagnostics. Someone who reaches it without having copied them describes the failure from memory, which is the situation this whole flow exists to avoid. Copy details unlocks it. The unlock is its own state rather than the `copied` flag, which reverts after a couple of seconds so the button stops claiming "Copied" -- but having the clipboard stays true after the label has gone back. Only a successful copy unlocks it. A refused clipboard leaves the form locked, because the alternative is arriving there with nothing to paste and no idea why. While locked it is not an anchor. `disabled` on an does nothing at all -- it still navigates -- so the href only exists once the copy has been made, and the locked state is a real disabled button with a title saying what to do. Opening a different failure resets it: each one is its own report, so the copy has to be made again for that one. --- src/admin/dashboard/ActionItems.js | 67 +++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 56e8e9bc8..cc81c30dc 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -43,6 +43,11 @@ export default () => { // boolean so the dialog keeps rendering the right one while it closes. const [ details, setDetails ] = useState( null ); const [ copied, setCopied ] = useState( false ); + // Separate from `copied`, which reverts after a couple of seconds so the button + // stops claiming "Copied". This one does not revert: the support form is only + // useful to someone who has the diagnostics on their clipboard, and that stays + // true after the label has gone back. + const [ copiedOnce, setCopiedOnce ] = useState( false ); const items = ( srfm_admin?.action_items || [] ).filter( ( item ) => ! dismissed.includes( item.id ) @@ -150,6 +155,9 @@ export default () => { } setCopied( true ); + // Only on success. A refused clipboard must not unlock the support form, + // or someone arrives at it with nothing to paste. + setCopiedOnce( true ); // Reverts on its own: a button stuck on "Copied" says nothing about the // next click. setTimeout( () => setCopied( false ), 2000 ); @@ -255,6 +263,10 @@ export default () => { type="button" onClick={ () => { setCopied( false ); + // Each failure is its own report, + // so the copy has to be made again + // for this one. + setCopiedOnce( false ); setDetails( item ); handleFix( item, @@ -370,21 +382,46 @@ export default () => { ? __( 'Copied', 'sureforms' ) : __( 'Copy details', 'sureforms' ) } - + { /* Locked until the details are on the clipboard. The + support form asks for them, and arriving with nothing + to paste means describing the failure from memory. + + Not an anchor while it is locked: `disabled` on an + does nothing at all -- it still navigates -- so + the href only exists once the copy has been made. */ } + { copiedOnce ? ( + + ) : ( + + ) }
From 5446a664f83d4b5fb02bb1b18bd932c8ba196f6d Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 8 Sep 2026 15:21:05 +0530 Subject: [PATCH 27/58] feat: close the dialog on Contact Support, and stamp when it was reported Clicking Contact Support opens the form in its own tab and closes the dialog. Leaving it up meant coming back to a panel still asking to be dealt with, for something already reported. The notice standing down until something new fails was already working -- acknowledge_category() sets acked to the current count, and get_open_failures() only returns a category whose count has overtaken it. Verified end to end: notice shown, Contact Support hides it, a new failure brings it back. What was missing is the timestamp. acked_at now records when the report was made, which is worth having when support reads the ticket. It is stored, not used as the gate. A timestamp cannot be the gate here: it is written to the second, so a failure recorded in the same second as the acknowledgement compares equal and gets swallowed -- at the one moment it matters most, because a fault arriving as someone reports the last one is a fault still happening. The count does that job. record_failure() carries acked_at forward. That array is rebuilt from a fixed set of keys, so anything not named there is dropped, and "when did I last report this" quietly disappearing on the next failure is the kind of loss nobody notices until support asks. --- inc/client-logger.php | 17 ++++++++++++++++- src/admin/dashboard/ActionItems.js | 18 ++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/inc/client-logger.php b/inc/client-logger.php index ff2c2029c..a66f3c462 100644 --- a/inc/client-logger.php +++ b/inc/client-logger.php @@ -83,7 +83,7 @@ class Client_Logger { * Option holding per-category failure state, keyed by category. * * Shape: [ category => [ 'count' => int, 'form_id' => int, 'form_title' => string, - * 'at' => int, 'acked' => int ] ]. + * 'at' => int, 'acked' => int, 'acked_at' => int ] ]. * * Kept per category because the three read completely differently to a site * owner: submissions failing means visitors cannot reach you, a notification @@ -196,6 +196,11 @@ public static function record_failure( $category, $form_id = 0, $form_title = '' // Preserved: a report already made still stands until this new count // overtakes it, which is what get_open_failures() compares. 'acked' => Helper::get_integer_value( $existing['acked'] ?? 0 ), + // Carried forward too. This array is rebuilt from a fixed set of keys, so + // anything not named here is dropped -- and "when did I last report + // this" quietly disappearing on the next failure is exactly the kind of + // loss nobody notices until support asks. + 'acked_at' => Helper::get_integer_value( $existing['acked_at'] ?? 0 ), ]; update_option( self::FAILURES_OPTION, $failures, false ); @@ -254,6 +259,16 @@ public static function acknowledge_category( $category ) { $failures[ $category ]['acked'] = Helper::get_integer_value( $failures[ $category ]['count'] ?? 0 ); + // Recorded for the report -- "you told us at 14:12" is worth having when + // support reads the ticket -- but deliberately not what decides whether the + // notice comes back. The count does that. + // + // A timestamp cannot: it is written to the second, so a failure recorded in + // the same second as the acknowledgement compares equal and gets swallowed. + // That is the one moment it matters most, because a fault arriving as + // someone reports the last one is a fault still happening. + $failures[ $category ]['acked_at'] = time(); + update_option( self::FAILURES_OPTION, $failures, false ); } diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index cc81c30dc..bda2d7744 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -397,10 +397,20 @@ export default () => { href={ details.support_url } target="_blank" rel="noopener noreferrer" - onClick={ handleFix( - details, - 'contact_support' - ) } + onClick={ () => { + // Records the click, which is also what + // stands the notice down until something + // new fails. + handleFix( + details, + 'contact_support' + )(); + // The form opens in its own tab, so the + // dialog has nothing left to show. Leaving + // it up means coming back to a panel still + // asking to be dealt with. + setDetails( null ); + } } // It is a button, not a link in prose. Rendering // it as an anchor is what brings the underline // with it, and wp-admin's own anchor styles reach From cebb52d5d074d0609a098df31b2409eccd9c6302 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 8 Sep 2026 15:26:21 +0530 Subject: [PATCH 28/58] feat: lock Contact Support in the wp-admin modal too, until details are copied Parity with the dashboard panel: the support form asks for the diagnostics, and reaching it without having copied them means describing the failure from memory. Locked by removing the destination, not by a disabled attribute. `disabled` on an does nothing at all -- it still navigates -- so the href is only set once the copy has been made, with pointer-events and aria-disabled behind it so the state is visible and announced rather than merely styled. Only a successful copy unlocks it. A refused clipboard leaves it locked, because the alternative is arriving at the form with nothing to paste and no idea why. The unlock does not revert when the "Copied" label does -- having the clipboard stays true after the label has gone back. Clicking it also closes the dialog, matching the panel: the form opens in its own tab, so leaving the dialog up means returning to something still asking to be dealt with. The reason it is locked is a real string from the server rather than English in the script, so it translates like the rest of the modal. --- admin/admin.php | 3 +++ admin/assets/js/notice-response.js | 28 ++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 31a5eb660..72fde0ab6 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -2818,6 +2818,9 @@ public function enqueue_notice_response_script() { 'copied' => __( 'Copied', 'sureforms' ), 'contact' => __( 'Contact Support', 'sureforms' ), 'close' => __( 'Close', 'sureforms' ), + // Shown on the locked Contact Support button, so the reason it + // cannot be clicked yet is readable rather than guessed at. + 'copyFirst' => __( 'Copy the details first, so you have them to paste.', 'sureforms' ), ], ] ); diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js index 49060817c..0ee18b49c 100644 --- a/admin/assets/js/notice-response.js +++ b/admin/assets/js/notice-response.js @@ -329,12 +329,30 @@ copy.className = 'button'; copy.textContent = labels.copy || 'Copy details'; + // Locked until the details are on the clipboard. The support form asks for + // them, and arriving with nothing to paste means describing the failure from + // memory. + // + // No href while it is locked, and a click guard behind that: `disabled` on + // an does nothing at all -- it still navigates -- so removing the + // destination is what actually locks it. const contact = document.createElement( 'a' ); contact.className = 'button button-primary'; - contact.href = supportUrl; contact.target = '_blank'; contact.rel = 'noopener noreferrer'; contact.textContent = labels.contact || 'Contact Support'; + contact.setAttribute( 'aria-disabled', 'true' ); + contact.title = labels.copyFirst || ''; + contact.style.opacity = '0.6'; + contact.style.pointerEvents = 'none'; + + function unlockContact() { + contact.href = supportUrl; + contact.removeAttribute( 'aria-disabled' ); + contact.removeAttribute( 'title' ); + contact.style.opacity = ''; + contact.style.pointerEvents = ''; + } const close = document.createElement( 'button' ); close.type = 'button'; @@ -357,16 +375,22 @@ copyDetails( text, function () { copy.textContent = labels.copied || 'Copied'; // Reverts on its own: a button stuck on "Copied" says nothing about - // the next click. + // the next click. The unlock does not revert with it -- having the + // clipboard stays true after the label has gone back. window.setTimeout( function () { copy.textContent = labels.copy || 'Copy details'; }, 2000 ); + // Only on success. A refused clipboard leaves the form locked rather + // than sending someone to it with nothing to paste. + unlockContact(); sendResponse( noticeId, 'copy_details' ); } ); } ); contact.addEventListener( 'click', function () { sendResponse( noticeId, 'contact_support' ); + // The form opens in its own tab, so the dialog has nothing left to show. + dismiss(); } ); close.addEventListener( 'click', dismiss ); From 6f756f1dd4ed19e6aa83e158f351c508f7665145 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 8 Sep 2026 16:05:18 +0530 Subject: [PATCH 29/58] feat: tag the Contact Support links so the source is identifiable One campaign, tagged per failure. utm_content carries the category, so the report answers which check actually sends people to support rather than only how many arrive -- a submission failure and a caching advisory are different problems, and it is worth knowing which one drives the tickets. Built with add_query_arg, and returned through esc_url_raw rather than esc_url. These rows reach the browser as JSON and React sets the href verbatim, so an HTML-escaped ampersand would arrive literally and the first parameter would be the only one the far end could read. Verified: four parameters parse back out, no & anywhere. Both surfaces get it from the same item field, so the panel and the wp-admin modal are tagged identically without repeating the URL. --- admin/admin.php | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/admin/admin.php b/admin/admin.php index 72fde0ab6..a7da75715 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -3698,7 +3698,7 @@ public function get_action_items() { // and copied verbatim, so any markup here would be read as characters. 'details' => $this->get_support_message( $category, $form_title ) . "\n\n" . $this->get_support_log_block(), - 'support_url' => self::SUPPORT_CONTACT_URL, + 'support_url' => $this->get_support_contact_url( $category ), 'dismissible' => false, ]; @@ -4418,6 +4418,38 @@ private function track_action_item_impressions( $warnings ) { } } + /** + * The contact form's address, tagged with where the click came from. + * + * One campaign, tagged per failure, so the report answers which check actually + * sends people to support rather than only how many arrive. A submission + * failure and a caching advisory are different problems and it is worth knowing + * which one drives the tickets. + * + * Built with add_query_arg rather than string concatenation, so it stays + * correct if the constant ever gains a query string of its own. + * + * @param string $category One of Client_Logger::CATEGORIES, naming the failure + * the visitor is reporting. + * @since 2.12.6 + * @return string + */ + private function get_support_contact_url( $category ) { + return esc_url_raw( + add_query_arg( + [ + 'utm_source' => 'sureforms', + 'utm_medium' => 'form_checks', + 'utm_campaign' => 'contact_support', + // Which check sent them. The one part that differs per button, + // and the reason for tagging at all. + 'utm_content' => $category, + ], + self::SUPPORT_CONTACT_URL + ) + ); + } + /** * The log tail, formatted for pasting. * From d4f03787df04efe66b89d4d6c58e18431ca0e849 Mon Sep 17 00:00:00 2001 From: Aditya Jain Date: Wed, 9 Sep 2026 09:42:19 +0530 Subject: [PATCH 30/58] fix: after-submission request cancelled by redirect confirmations The after-submission call was dispatched after the redirect branch had already invoked redirectToUrl(). window.location.assign() begins an unload that cancels the in-flight fetch, so on every "different page" and "custom url" submission the request aborted in 10-25ms. srfm_after_submission_process therefore never fired for those forms, silently skipping webhooks, native integrations, CPT creation and quiz scoring on Pro. On free it surfaced only as a false "submission failed" alert, since the entry itself had already been saved. Dispatch the request before any branch can navigate away, and send it with keepalive so it still completes once unload starts. The abort is prevented rather than suppressed, so a genuine after-submission failure is still reported. --- assets/js/unminified/form-submit.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/assets/js/unminified/form-submit.js b/assets/js/unminified/form-submit.js index 55ce53b4a..2b4e78a6f 100644 --- a/assets/js/unminified/form-submit.js +++ b/assets/js/unminified/form-submit.js @@ -800,7 +800,13 @@ async function afterSubmit( formStatus, form ) { const startedAt = performance.now(); try { - const response = await fetch( afterSubmitUrl, { method: 'GET' } ); + // keepalive lets the request complete after the document starts + // unloading, which is exactly what a redirect confirmation does. The + // request is a GET with no body, so the 64KB keepalive cap does not apply. + const response = await fetch( afterSubmitUrl, { + method: 'GET', + keepalive: true, + } ); const status = response.status; const durationMs = Math.round( performance.now() - startedAt ); @@ -1189,6 +1195,16 @@ async function handleFormSubmission( formStatus?.data?.submission_settings?.after_submission || oldAfterSubmission; + // Dispatched before the branches below, because the 'different page' + // and 'custom url' branches call redirectToUrl() and the resulting + // unload cancels an in-flight fetch. Issuing it afterwards meant the + // request was aborted on every redirect-mode submission, so the + // after-submission process never ran. Kept out of the branches so it + // still covers all submission modes. + if ( formStatus?.data?.after_submit ) { + afterSubmit( formStatus, form ); + } + if ( submitType === 'same page' ) { showSuccessMessage( successContainer, @@ -1228,10 +1244,6 @@ async function handleFormSubmission( // Re-enable submit button after redirect. enableSubmitButton( form ); } - // Moving afterSubmit action out of specific method so it should work for all submission mode - if ( formStatus?.data?.after_submit ) { - afterSubmit( formStatus, form ); - } } else { const errorData = formStatus?.data || {}; showErrorMessage( { form, ...errorData } ); From 48643dc15186bed2c8744b82bb496cb297b47481 Mon Sep 17 00:00:00 2001 From: Aditya Jain Date: Wed, 9 Sep 2026 14:35:54 +0530 Subject: [PATCH 31/58] fix: dispatch after-submission before the success event too Review of #3123 pointed out the dispatch still sat after emitFormSubmitSuccess(), so a third-party listener on srfm_form_submission_success that navigates -- a GTM or analytics snippet on a form page, for instance -- would cancel the request the same way the redirect branches did, reintroducing #3122 through an adjacent route. afterSubmit() reads only after_submit_url, so it depends on neither the event nor the submission-mode resolution below it. Moving the dispatch to the top of the success path closes that route at no cost. Also guard both redirect specs against the regression. They submit custom url and different page forms but assert only that the URL changed, so they passed throughout the bug. They now fail if the after-submission request is aborted. --- assets/js/unminified/form-submit.js | 22 ++++++++++-------- tests/play/specs/form-settings.spec.js | 32 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/assets/js/unminified/form-submit.js b/assets/js/unminified/form-submit.js index 2b4e78a6f..ce8cd6215 100644 --- a/assets/js/unminified/form-submit.js +++ b/assets/js/unminified/form-submit.js @@ -1176,6 +1176,18 @@ async function handleFormSubmission( const formStatus = await submitFormData( form ); if ( formStatus?.success ) { + // Dispatched first, before anything below can navigate away. The + // 'different page' and 'custom url' branches call redirectToUrl(), and a + // third-party listener on srfm_form_submission_success may navigate too; + // either unload cancels an in-flight fetch, and issuing this afterwards + // meant the request aborted so the after-submission process never ran. + // afterSubmit() reads only after_submit_url, so it depends on neither the + // event nor the submission-mode resolution below, and gating it here on + // after_submit alone keeps every submission mode covered. + if ( formStatus?.data?.after_submit ) { + afterSubmit( formStatus, form ); + } + /** * Emit a function to signal the successful submission of a form. */ @@ -1195,16 +1207,6 @@ async function handleFormSubmission( formStatus?.data?.submission_settings?.after_submission || oldAfterSubmission; - // Dispatched before the branches below, because the 'different page' - // and 'custom url' branches call redirectToUrl() and the resulting - // unload cancels an in-flight fetch. Issuing it afterwards meant the - // request was aborted on every redirect-mode submission, so the - // after-submission process never ran. Kept out of the branches so it - // still covers all submission modes. - if ( formStatus?.data?.after_submit ) { - afterSubmit( formStatus, form ); - } - if ( submitType === 'same page' ) { showSuccessMessage( successContainer, diff --git a/tests/play/specs/form-settings.spec.js b/tests/play/specs/form-settings.spec.js index 674ac4d0d..e5100fe86 100644 --- a/tests/play/specs/form-settings.spec.js +++ b/tests/play/specs/form-settings.spec.js @@ -86,6 +86,20 @@ test.describe( 'Form settings', () => { await page.goto( formURL ); await page.waitForLoadState( 'load' ); + // Guards #3122. The after-submission request is issued as the redirect + // begins; before the fix window.location.assign() cancelled it, so the + // after-submission process (webhooks, integrations, CPT creation) never + // ran. Asserting only on the URL cannot see that -- these specs passed + // throughout the regression. + const abortedAfterSubmission = []; + page.on( 'requestfailed', ( request ) => { + if ( request.url().includes( 'sureforms/v1/after-submission' ) ) { + abortedAfterSubmission.push( + request.failure()?.errorText ?? 'request failed' + ); + } + } ); + await page.locator( 'input.srfm-input-input' ).first().fill( 'Redirect test' ); await Promise.all( [ @@ -94,6 +108,8 @@ test.describe( 'Form settings', () => { ] ); expect( page.url() ).toContain( '/wp-admin/' ); + + expect( abortedAfterSubmission ).toEqual( [] ); } ); // ── 3.3 Store entries disabled ──────────────────────────────────────────── @@ -179,6 +195,20 @@ test.describe( 'Form settings', () => { await page.goto( formURL ); await page.waitForLoadState( 'load' ); + // Guards #3122. The after-submission request is issued as the redirect + // begins; before the fix window.location.assign() cancelled it, so the + // after-submission process (webhooks, integrations, CPT creation) never + // ran. Asserting only on the URL cannot see that -- these specs passed + // throughout the regression. + const abortedAfterSubmission = []; + page.on( 'requestfailed', ( request ) => { + if ( request.url().includes( 'sureforms/v1/after-submission' ) ) { + abortedAfterSubmission.push( + request.failure()?.errorText ?? 'request failed' + ); + } + } ); + await page.locator( 'input.srfm-input-input' ).first().fill( 'Page redirect test' ); await Promise.all( [ @@ -187,6 +217,8 @@ test.describe( 'Form settings', () => { ] ); expect( page.url() ).toContain( '/wp-admin/' ); + + expect( abortedAfterSubmission ).toEqual( [] ); } ); // ── 3.5 Store entries — entry appears in admin after submission ─────────── From c9eabb2ff62a44ebcc7ff95235025dbb4483b487 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Wed, 9 Sep 2026 14:49:02 +0530 Subject: [PATCH 32/58] fix: address review on the editor exclusion Empty NOT IN no longer emits a literal true. Under AND that was a no-op, but the same builder also assembles OR groups, where a literal true makes the whole group match every row and neutralises its siblings -- including the customer filter the payment history shortcode builds. The condition is dropped instead, which means the same thing under AND and narrows rather than widens under OR. Payments::build_clause_for_group() substituted [ 0 ] for the same case, so it is aligned to the identical handling. The editor lookup is inverted. Asking the user table for everyone with edit_posts ran an unindexable leading-wildcard scan of capability meta with no LIMIT, and the result was interpolated into one windowed COUNT per form. It now starts from the distinct in-window submitters, which is the only set the exclusion can affect, tests each with user_can() -- the same call the view gate makes, so super admins, runtime-granted capabilities and explicit denials resolve the same way on both halves -- and primes the user cache so the loop costs two queries rather than one per submitter. Past a filterable ceiling of 500 submitters the exclusion is skipped whole and announced, rather than silently truncated. Also: the settings copy keeps the shipped msgid verbatim and adds the new sentence as its own string, so the seven translated locales are not orphaned; total_form_views()'s docblock no longer invites division by the unfiltered all-time total_entries; the docblocks say the exclusion reflects current capability and name the promotion drift; the cached-value checks treat zero and the empty set as answers rather than misses; three @since tags move to x.x.x; and get_form_ids_by_entries() rejects an empty list with || rather than &&. --- admin/analytics.php | 13 +- inc/database/base.php | 33 +++- inc/database/tables/entries.php | 2 +- inc/database/tables/payments.php | 13 +- inc/forms-data.php | 172 ++++++++++++++---- src/admin/settings/pages/General.js | 22 ++- .../unit/inc/database/tables/test-entries.php | 53 +++++- tests/unit/inc/database/test-base.php | 95 +++++++--- tests/unit/inc/test-forms-data.php | 90 ++++++++- 9 files changed, 402 insertions(+), 91 deletions(-) diff --git a/admin/analytics.php b/admin/analytics.php index d8aa4a1a4..806aaaf79 100644 --- a/admin/analytics.php +++ b/admin/analytics.php @@ -513,9 +513,16 @@ public function global_settings_data() { /** * Total page views counted across all published forms. * - * Paired with `forms_with_views` and the existing `total_entries`, this is what - * makes the conversion figure interpretable in the warehouse: a site with views - * but no entries reads very differently from one with neither. + * Read alongside `forms_with_views`, which is what makes this figure legible: a + * site with views but no forms recording any reads very differently from one + * with neither. + * + * Not a denominator for `total_entries`, and the two must not be divided. This + * total starts at the moment tracking opened and leaves out anyone who can edit + * the site, while `total_entries` is an unfiltered all-time count of every + * entry ever received. A rate built from the pair answers neither question and + * will not match the Conversion Rate column, which measures both halves over + * the same window with the same exclusion. * * Counts only published forms, so views left behind by a trashed or draft form * do not inflate the total against a denominator that no longer includes them. diff --git a/inc/database/base.php b/inc/database/base.php index ddeb9d219..7ab63806d 100644 --- a/inc/database/base.php +++ b/inc/database/base.php @@ -829,8 +829,10 @@ public function get_results( $where_clauses = [], $columns = '*', $extra_queries $query = rtrim( trim( $query ), ';' ) . ';'; $cached_results = $this->cache_get( $query ); - if ( $cached_results ) { - // Return the cached data if exists. + if ( null !== $cached_results ) { + // Return the cached data if exists. Tested against null rather than + // truthiness: an empty result set is a real answer, and re-running the + // query for it means every no-match lookup runs once per caller. return Helper::get_array_value( $cached_results ); } @@ -918,8 +920,10 @@ public function get_total_count( $where_clauses = [] ) { $query = rtrim( trim( $query ), ';' ) . ';'; $cached_results = $this->cache_get( $query ); - if ( $cached_results ) { - // Return the cached data if exists. + if ( null !== $cached_results ) { + // Return the cached data if exists. Tested against null rather than + // truthiness: a count of zero is a real answer, and the editor exclusion + // makes zero the common case rather than the exception. return Helper::get_integer_value( $cached_results ); } @@ -1053,7 +1057,7 @@ protected function cache_reset() { * } * * @since 1.1.1 -- Added support for "IN" compare. - * @since 2.12.6 -- Added support for "NOT IN" compare. + * @since x.x.x -- Added support for "NOT IN" compare. * @since 0.0.13 * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided. */ @@ -1104,10 +1108,15 @@ protected function prepare_where_clauses( $where_clauses = [] ) { case 'NOT IN': // An empty list cannot be interpolated: "col IN ()" is a syntax // error that fails the whole query, listing and COUNT alike. - // Emit the constant the empty set actually means instead, so - // neither operator can silently widen to match everything. + // An empty IN matches nothing, so '1 = 0' says that in any + // relation. An empty NOT IN excludes nothing, but a literal + // would be '1 = 1', and that makes an enclosing OR group + // unconditionally true. Dropping the condition means the same + // thing under AND and stays fail-closed under OR. if ( ! is_array( $_value['value'] ) || [] === $_value['value'] ) { - $clause_parts[] = 'IN' === $_value['compare'] ? '1 = 0' : '1 = 1'; + if ( 'IN' === $_value['compare'] ) { + $clause_parts[] = '1 = 0'; + } break; } @@ -1146,6 +1155,14 @@ protected function prepare_where_clauses( $where_clauses = [] ) { $where = ' WHERE ' . implode( ' AND ', $groups ); + if ( [] === $values ) { + // Every branch that builds a placeholder also pushes a value, so an + // empty list here means the only conditions were constant ones. There + // is nothing for prepare() to fill, and calling it with no placeholder + // trips _doing_it_wrong. + return $where; + } + // Prepare the query with placeholders. // @phpstan-ignore-next-line -- We are already assigning non-literal string above using "get_format_by_datatype" methods. return $wpdb->prepare( $where, ...$values ); // phpcs:ignore -- We are returning prepared sql query here. We are already using necessary placeholders in $where variable. diff --git a/inc/database/tables/entries.php b/inc/database/tables/entries.php index 2de9cefd4..49d62a79e 100644 --- a/inc/database/tables/entries.php +++ b/inc/database/tables/entries.php @@ -543,7 +543,7 @@ public static function has_duplicate_field_value( $form_id, $field_key, $field_v * @return array An array of form IDs. */ public static function get_form_ids_by_entries( $entry_ids ) { - if ( empty( $entry_ids ) && ! is_array( $entry_ids ) ) { + if ( empty( $entry_ids ) || ! is_array( $entry_ids ) ) { return []; } diff --git a/inc/database/tables/payments.php b/inc/database/tables/payments.php index 0e5d47ad1..25ecdc964 100644 --- a/inc/database/tables/payments.php +++ b/inc/database/tables/payments.php @@ -1287,8 +1287,17 @@ private static function build_clause_for_group( array $where_group, array &$para if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) && is_array( $condition['value'] ) ) { $ids = array_map( 'absint', $condition['value'] ); - if ( empty( $ids ) ) { - $ids = [ 0 ]; + if ( [] === $ids ) { + // Same empty-list handling as Base::prepare_where_clauses(), so the + // two builders cannot disagree. An empty IN matches nothing. An + // empty NOT IN excludes nothing, and is dropped rather than written + // as a literal, because a literal true would make an enclosing OR + // group -- such as the customer filter this shortcode builds -- + // match every row. + if ( 'IN' === $operator ) { + $sub_clauses[] = '1 = 0'; + } + continue; } $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $sub_clauses[] = "{$column} {$operator} ({$placeholders})"; diff --git a/inc/forms-data.php b/inc/forms-data.php index 7da222146..f17c34bbc 100644 --- a/inc/forms-data.php +++ b/inc/forms-data.php @@ -26,7 +26,11 @@ class Forms_Data { use Get_Instance; /** - * Cached ids of users who can edit the site, or null before the first lookup. + * Ids of the in-window submitters who can edit the site, keyed by blog id. + * + * Keyed on the blog rather than held as a single list because capabilities are + * per-site: after a switch_to_blog() the previous site's answer is wrong, and a + * flat cache would hand it back. * * A class property rather than a `static` inside the method so it can be reset, * which the tests need: they create a user and then ask for the metrics inside @@ -36,8 +40,8 @@ class Forms_Data { * cache would keep counting a newly promoted editor as a visitor until * something invalidated it, and there is no natural invalidation point. * - * @var array|null - * @since 2.12.6 + * @var array>|null + * @since x.x.x */ private static $editing_user_ids = null; @@ -323,7 +327,12 @@ private function get_forms_sorted_by_metric( $args, $orderby, $order, $page, $pe */ $limit = Helper::get_integer_value( apply_filters( 'srfm_forms_metric_sort_limit', 500 ) ); - if ( $limit > 0 && count( $id_query->posts ) > $limit ) { + // A filter is allowed to tighten or loosen the ceiling, not to remove it. Zero + // or negative read as "no ceiling" to a `> $limit` test, which is the one + // outcome the ceiling exists to prevent, so fall back to the default. + $limit = $limit > 0 ? $limit : 500; + + if ( count( $id_query->posts ) > $limit ) { /** * Fires when the metric sort is skipped because the site has too many forms. * @@ -452,44 +461,124 @@ private static function window_boundary_sql( $window_start ) { } /** - * Ids of every user who can edit the site, cached for the request. + * Ids of the in-window submitters who can edit the site, cached for the request. + * + * Bounded by submitters, not by users. Asking the user table for everyone with + * `edit_posts` runs an unindexable leading-wildcard scan of capability meta with + * no LIMIT, and the answer is then interpolated into one windowed COUNT per form, + * up to the ceiling in get_forms_sorted_by_metric(). On a membership site that + * hands `contributor` to every member that is a five-figure placeholder list + * rebuilt for every row of a ten-row page. Only people who actually submitted + * inside the window can affect the rate, that set is small, and `idx_user_id` + * covers it. + * + * Decided with `user_can()`, the same call Form_Views::should_track() makes, so + * both halves of the rate answer one question rather than two similar ones. A + * capability granted at runtime through `user_has_cap`, a multisite super admin + * who is not a member of the subsite, and a role carrying `edit_posts` as an + * explicit denial all resolve the same way on both sides. * - * The mirror of Form_Views::should_track()'s `user_can( $user_id, 'edit_posts' )` - * check, so the same people are absent from both halves of the rate. + * Reflects capability as it stands now, not as it stood at submission time. On a + * site that promotes or demotes people the two halves still drift: a promoted + * subscriber's earlier views stay in the denominator while their earlier entries + * leave the numerator, and a demoted editor's earlier entries return without + * their views. Closing that means stamping the decision on the entry at submit + * time, which is a schema change this percentage does not justify. * * Cached because the forms listing asks once per row, and the answer cannot * change within a request. * - * Approximates the view gate rather than matching it exactly: this reads roles - * and capability meta, while `user_can()` also honours a `user_has_cap` filter, - * so a capability granted at runtime by a membership plugin is excluded from - * views but still counted here. Close enough for a percentage, and the - * alternative is user_can() per entry. - * - * @since 2.12.6 + * @param int $window_start Unix timestamp the view window opened at. + * @since x.x.x * @return array */ - private static function get_editing_user_ids() { - if ( null !== self::$editing_user_ids ) { - return self::$editing_user_ids; + private static function get_editing_submitter_ids( $window_start ) { + $blog_id = get_current_blog_id(); + + if ( null === self::$editing_user_ids ) { + self::$editing_user_ids = []; + } + + if ( isset( self::$editing_user_ids[ $blog_id ] ) ) { + return self::$editing_user_ids[ $blog_id ]; } - // ponytail: NOT IN over this list is fine for the handful of editors a - // typical site has; a site with thousands of authors wants a user_id > 0 - // join instead. - self::$editing_user_ids = array_map( - 'intval', - Helper::get_array_value( - get_users( + /** + * Largest number of distinct in-window submitters to test for edit access. + * + * The test is bounded work per submitter, so this is a guard against a site + * where a very large share of submissions are made while logged in. Past the + * ceiling the exclusion is skipped rather than truncated: a partial exclusion + * list reports a rate that is wrong in a way nobody can see, where no + * exclusion at least reproduces the pre-existing behaviour. + * + * @param int $limit Maximum submitters to test. Default 500. + * @since x.x.x + */ + $limit = Helper::get_integer_value( apply_filters( 'srfm_forms_metric_submitter_limit', 500 ) ); + + // Same reasoning as the sort ceiling: a filter may move it, not remove it. + $limit = $limit > 0 ? $limit : 500; + + // One row past the ceiling, so a full page is proof the ceiling was passed + // without counting the rest of the table to find out. + $rows = Entries::get_instance()->get_results( + [ + [ [ - 'capability' => 'edit_posts', - 'fields' => 'ID', - ] - ) + 'key' => 'created_at', + 'compare' => '>=', + 'value' => self::window_boundary_sql( $window_start ), + ], + [ + 'key' => 'user_id', + 'compare' => '>', + 'value' => 0, + ], + ], + ], + 'DISTINCT user_id', + [ 'LIMIT ' . ( $limit + 1 ) ] + ); + + $submitters = array_values( array_unique( array_map( 'absint', array_column( $rows, 'user_id' ) ) ) ); + + if ( count( $submitters ) > $limit ) { + /** + * Fires when the editor exclusion is skipped because too many submitters + * would have to be tested. + * + * Announced rather than skipped silently, for the same reason as + * `srfm_forms_metric_sort_skipped`: a rate that quietly stops excluding + * editors reads as a wrong number, not as a deliberate ceiling. + * + * @param int $count Number of distinct submitters found, capped at $limit + 1. + * @param int $limit The ceiling in force. + * @since x.x.x + */ + do_action( 'srfm_forms_metric_submitter_limit_exceeded', count( $submitters ), $limit ); + + self::$editing_user_ids[ $blog_id ] = []; + + return self::$editing_user_ids[ $blog_id ]; + } + + if ( [] !== $submitters ) { + // Two queries for the whole set. Without it user_can() resolves each user + // on its own and the loop becomes one query per submitter. + cache_users( $submitters ); + } + + self::$editing_user_ids[ $blog_id ] = array_values( + array_filter( + $submitters, + static function ( $user_id ) { + return user_can( $user_id, 'edit_posts' ); + } ) ); - return self::$editing_user_ids; + return self::$editing_user_ids[ $blog_id ]; } /** @@ -501,22 +590,25 @@ private static function get_editing_user_ids() { * rendering a dash sorted as though its rate were several hundred percent. * Anything needing these numbers must come through here. * - * Both halves exclude the same people. Views are not counted for anyone who - * can edit the site (Form_Views::should_track()), so their submissions must not - * be counted either: testing your own form five times would otherwise add five - * to the numerator and nothing to the denominator, and report a rate several - * times the real one. The Entries column is unaffected and stays a true - * all-time count of every entry received. + * Both halves apply the same test. Views are not counted for anyone who can + * edit the site (Form_Views::should_track()), so their submissions must not be + * counted either: testing your own form five times would otherwise add five to + * the numerator and nothing to the denominator, and report a rate several times + * the real one. The test is applied to capability as it stands now on both + * sides, so a site that promotes or demotes people still sees some drift -- + * get_editing_submitter_ids() has the detail. The Entries column is unaffected + * and stays a true all-time count of every entry received. * * Returns `null` for the rate rather than a number whenever it cannot be * measured: tracking off, window never opened, no views yet, or more entries - * than views. That last case should not arise once both halves exclude the same - * people, so it means the view count is incomplete and any percentage would be - * invented; the table renders the dash instead. `0.0` is reserved for a real - * measurement of zero. + * than views. That last case means the view count is incomplete, or that a + * submitter was promoted after submitting, and any percentage would be invented; + * the table renders the dash instead. `0.0` is reserved for a real measurement + * of zero. * * @param int $form_id Form post ID. * @return array{views:int,conversion_rate:float|null} + * @since x.x.x -- Signature reduced to $form_id. * @since 2.12.6 */ private function calculate_form_metrics( $form_id ) { @@ -557,7 +649,7 @@ private function calculate_form_metrics( $form_id ) { ], ]; - $editing_users = self::get_editing_user_ids(); + $editing_users = self::get_editing_submitter_ids( $window_start ); if ( [] !== $editing_users ) { $where[] = [ diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index 7a7b0871a..26927e1f0 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -233,9 +233,25 @@ const FormViewsTrackingContent = ( { 'Show views and conversion rate', 'sureforms' ), - description: __( - 'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Logged-out visitors are counted, and so are logged-in users who cannot edit the site, such as subscribers and customers. Views and submissions by anyone who can edit the site, including you, are left out, so testing your own forms does not change the figures. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns. Counting keeps running, so the figures are up to date if you switch it back on.', - 'sureforms' + description: ( + <> + { /* Kept verbatim from 2.12.6: all seven shipped locales + already translate this msgid, and re-punctuating it + to add the sentence below would orphan every one of + them. New copy goes in its own __() call. */ } + + { __( + 'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns — counting continues, so the figures are up to date if you switch it back on.', + 'sureforms' + ) } + + + { __( + 'Views and submissions by anyone who can edit the site are left out, so testing your own forms does not change these figures.', + 'sureforms' + ) } + + ), } } value={ generalTabOptions.srfm_form_views_tracking } diff --git a/tests/unit/inc/database/tables/test-entries.php b/tests/unit/inc/database/tables/test-entries.php index 551138b9d..4f2d958f7 100644 --- a/tests/unit/inc/database/tables/test-entries.php +++ b/tests/unit/inc/database/tables/test-entries.php @@ -267,7 +267,7 @@ public function test_get_form_data_nonexistent() { } /** - * Test get_form_ids_by_entries with empty array returns empty. + * Test get_form_ids_by_entries with a non-existent entry id returns empty. */ public function test_get_form_ids_by_entries_nonexistent() { $result = Entries::get_form_ids_by_entries( [ 999999 ] ); @@ -275,6 +275,57 @@ public function test_get_form_ids_by_entries_nonexistent() { $this->assertEmpty( $result ); } + /** + * Empty the entries table's per-instance query cache. + */ + private function reset_entries_cache(): void { + $reset = new ReflectionMethod( Entries::get_instance(), 'cache_reset' ); + $reset->setAccessible( true ); + $reset->invoke( Entries::get_instance() ); + } + + /** + * An empty list never reaches the database. + * + * The guard has to reject an empty array as well as a non-array. Asserted on + * the query count rather than the return value: an empty result comes back + * either way, so only "no query was run" distinguishes the guard from a query + * that happened to match nothing. + */ + public function test_get_form_ids_by_entries_empty_array() { + global $wpdb; + + // Both guards, once removed, build the same constant-only query, so one + // test's result would answer the other's from the per-instance cache. + $this->reset_entries_cache(); + + $before = $wpdb->num_queries; + $result = Entries::get_form_ids_by_entries( [] ); + + $this->assertIsArray( $result ); + $this->assertEmpty( $result ); + $this->assertSame( $before, $wpdb->num_queries, 'An empty list must not reach the database.' ); + } + + /** + * A non-array argument is rejected too, and also without a query. + */ + public function test_get_form_ids_by_entries_non_array() { + global $wpdb; + + // Both guards, once removed, build the same constant-only query, so one + // test's result would answer the other's from the per-instance cache. + $this->reset_entries_cache(); + + $before = $wpdb->num_queries; + // @phpstan-ignore-next-line -- Deliberately passing the wrong type to exercise the guard. + $result = Entries::get_form_ids_by_entries( 'not-an-array' ); + + $this->assertIsArray( $result ); + $this->assertEmpty( $result ); + $this->assertSame( $before, $wpdb->num_queries, 'A non-array argument must not reach the database.' ); + } + /** * Test get_entry_data returns empty array for non-existent entry. */ diff --git a/tests/unit/inc/database/test-base.php b/tests/unit/inc/database/test-base.php index d95d01837..736a831fb 100644 --- a/tests/unit/inc/database/test-base.php +++ b/tests/unit/inc/database/test-base.php @@ -420,11 +420,10 @@ public function test_prepare_where_clauses_not_in_operator() { * An empty list must not be interpolated into "col IN ()". * * That is a syntax error, and it fails the whole query rather than the one - * condition, taking out the listing and its COUNT together. Each operator - * collapses to the constant the empty set actually means, so neither can widen - * to match everything. + * condition, taking out the listing and its COUNT together. An empty IN matches + * nothing, so it collapses to a constant that says so. */ - public function test_prepare_where_clauses_empty_list_does_not_emit_invalid_sql() { + public function test_prepare_where_clauses_empty_in_matches_nothing() { $in = $this->prepare( [ [ @@ -439,7 +438,18 @@ public function test_prepare_where_clauses_empty_list_does_not_emit_invalid_sql( $this->assertStringNotContainsString( 'IN ()', $in ); $this->assertStringContainsString( '1 = 0', $in, 'An empty IN matches nothing.' ); + } + /** + * An empty NOT IN is dropped, never written as a constant. + * + * It excludes nothing, and the constant that says so is a literal true. Under + * AND that is a no-op, but the same clause list also builds OR groups, where a + * literal true makes the whole group match every row and neutralises the + * sibling conditions. Dropping the condition means the same thing under AND and + * narrows rather than widens under OR. + */ + public function test_prepare_where_clauses_empty_not_in_is_dropped() { $not_in = $this->prepare( [ [ @@ -453,34 +463,38 @@ public function test_prepare_where_clauses_empty_list_does_not_emit_invalid_sql( ); $this->assertStringNotContainsString( 'IN ()', $not_in ); - $this->assertStringContainsString( '1 = 1', $not_in, 'An empty NOT IN excludes nothing.' ); + $this->assertStringNotContainsString( '1 = 1', $not_in, 'An empty NOT IN must not emit a literal true.' ); + $this->assertStringNotContainsString( 'NOT IN', $not_in, 'The condition is dropped, not built.' ); } - // --------------------------------------------------------------- - // cache_reset - // --------------------------------------------------------------- - /** - * cache_reset() empties the per-instance query cache. + * An empty NOT IN beside an OR sibling must not widen the group. + * + * This is the shape that matters: a group whose siblings are the only thing + * keeping a query narrow. The surviving clause must still be the sibling alone. */ - public function test_cache_reset() { - $set = new ReflectionMethod( $this->entries_table, 'cache_set' ); - $get = new ReflectionMethod( $this->entries_table, 'cache_get' ); - $reset = new ReflectionMethod( $this->entries_table, 'cache_reset' ); - - foreach ( [ $set, $get, $reset ] as $method ) { - $method->setAccessible( true ); - } - - $set->invoke( $this->entries_table, 'srfm_cache_reset_probe', 'stored' ); - $this->assertSame( 'stored', $get->invoke( $this->entries_table, 'srfm_cache_reset_probe' ) ); - - $reset->invoke( $this->entries_table ); - - $this->assertNull( - $get->invoke( $this->entries_table, 'srfm_cache_reset_probe' ), - 'The cached value must be gone after a reset.' + public function test_prepare_where_clauses_empty_not_in_does_not_widen_an_or_group() { + $result = $this->prepare( + [ + [ + [ + 'key' => 'form_id', + 'compare' => '=', + 'value' => 7, + ], + [ + 'key' => 'user_id', + 'compare' => 'NOT IN', + 'value' => [], + ], + 'RELATION' => 'OR', + ], + ] ); + + $this->assertStringContainsString( 'form_id', $result, 'The sibling condition survives.' ); + $this->assertStringNotContainsString( '1 = 1', $result ); + $this->assertStringNotContainsString( ' OR ', $result, 'Nothing is left to OR the sibling against.' ); } // --------------------------------------------------------------- @@ -696,4 +710,31 @@ public function test_table_exists_reports_a_dropped_table_as_missing() { $this->assertFalse( $missing ); $this->assertTrue( $this->entries_table->table_exists() ); } + + // --------------------------------------------------------------- + // cache_reset + // --------------------------------------------------------------- + + /** + * cache_reset() empties the per-instance query cache. + */ + public function test_cache_reset() { + $set = new ReflectionMethod( $this->entries_table, 'cache_set' ); + $get = new ReflectionMethod( $this->entries_table, 'cache_get' ); + $reset = new ReflectionMethod( $this->entries_table, 'cache_reset' ); + + foreach ( [ $set, $get, $reset ] as $method ) { + $method->setAccessible( true ); + } + + $set->invoke( $this->entries_table, 'srfm_cache_reset_probe', 'stored' ); + $this->assertSame( 'stored', $get->invoke( $this->entries_table, 'srfm_cache_reset_probe' ) ); + + $reset->invoke( $this->entries_table ); + + $this->assertNull( + $get->invoke( $this->entries_table, 'srfm_cache_reset_probe' ), + 'The cached value must be gone after a reset.' + ); + } } diff --git a/tests/unit/inc/test-forms-data.php b/tests/unit/inc/test-forms-data.php index 6bf9d288e..de5b2979e 100644 --- a/tests/unit/inc/test-forms-data.php +++ b/tests/unit/inc/test-forms-data.php @@ -16,6 +16,22 @@ class Test_Forms_Data extends TestCase { protected function setUp(): void { $this->forms_data = new Forms_Data(); + $this->reset_editing_cache(); + } + + protected function tearDown(): void { + // The list is memoised for the request, and a test process is one request. + // Left behind it holds ids of users a later test has already deleted. + $this->reset_editing_cache(); + } + + /** + * Clear Forms_Data's per-request editor cache. + */ + private function reset_editing_cache(): void { + $cache = new ReflectionProperty( Forms_Data::class, 'editing_user_ids' ); + $cache->setAccessible( true ); + $cache->setValue( null, null ); } public function test_get_form_permissions_check_admin() { @@ -63,9 +79,6 @@ public function test_register_custom_endpoint() { // get_forms_list() // --------------------------------------------------------------- - /** - * Helper: create an admin user, set as current, and return the user ID. - */ /** * Create a user with the given role. Not set as the current user. */ @@ -79,6 +92,9 @@ private function make_user( string $role ): int { ); } + /** + * Helper: create an admin user, set as current, and return the user ID. + */ private function set_admin_user(): int { $user_id = wp_insert_user( [ @@ -326,9 +342,7 @@ public function test_calculate_form_metrics_excludes_entries_from_users_who_can_ // The list is cached per request, and these users were created after // earlier tests in this class already warmed it. - $cache = new ReflectionProperty( Forms_Data::class, 'editing_user_ids' ); - $cache->setAccessible( true ); - $cache->setValue( null, null ); + $this->reset_editing_cache(); $form_id = $this->make_tracked_form( 10 ); @@ -377,6 +391,70 @@ public function test_calculate_form_metrics_excludes_entries_from_users_who_can_ delete_option( \SRFM\Inc\Form_Views::TRACKING_STARTED_OPTION ); } + /** + * Past the submitter ceiling the exclusion is skipped, and says so. + * + * Testing every in-window submitter for edit access is bounded work per + * submitter, so a site where most submissions are made while logged in needs a + * stop. The exclusion is dropped whole rather than truncated: a partial list + * reports a rate that is wrong in a way nobody can see, while no list at least + * reproduces the behaviour that shipped before it existed. + */ + public function test_calculate_form_metrics_skips_the_exclusion_past_the_submitter_ceiling() { + delete_option( \SRFM\Inc\Form_Views::TRACKING_STARTED_OPTION ); + update_option( 'srfm_general_settings_options', [ 'srfm_form_views_tracking' => true ] ); + + $editor_one = $this->make_user( 'editor' ); + $editor_two = $this->make_user( 'editor' ); + + $this->reset_editing_cache(); + + $form_id = $this->make_tracked_form( 10 ); + + foreach ( [ $editor_one, $editor_two ] as $user_id ) { + Entries::add( + [ + 'form_id' => $form_id, + 'user_id' => $user_id, + 'form_data' => [], + ] + ); + } + + $announced = 0; + $reported = null; + $listener = static function ( $count, $limit ) use ( &$announced, &$reported ) { + $announced++; + $reported = [ $count, $limit ]; + }; + + $ceiling = static function () { + return 1; + }; + + add_filter( 'srfm_forms_metric_submitter_limit', $ceiling ); + add_action( 'srfm_forms_metric_submitter_limit_exceeded', $listener, 10, 2 ); + + $metrics = $this->call_private( 'calculate_form_metrics', [ $form_id ] ); + + remove_action( 'srfm_forms_metric_submitter_limit_exceeded', $listener, 10 ); + remove_filter( 'srfm_forms_metric_submitter_limit', $ceiling ); + + $this->assertSame( 1, $announced, 'Skipping the exclusion must be announced, not silent.' ); + $this->assertSame( [ 2, 1 ], $reported, 'The action reports the count found and the ceiling in force.' ); + $this->assertSame( + 20.0, + $metrics['conversion_rate'], + 'Past the ceiling both editor entries count, which is the pre-exclusion behaviour.' + ); + + wp_delete_post( $form_id, true ); + wp_delete_user( $editor_one ); + wp_delete_user( $editor_two ); + delete_option( 'srfm_general_settings_options' ); + delete_option( \SRFM\Inc\Form_Views::TRACKING_STARTED_OPTION ); + } + /** * The listing row must carry the same numbers calculate_form_metrics() produces. */ From 37db55dd8bc04c3a4d6496c24a98864e2b1694c5 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Wed, 9 Sep 2026 16:31:19 +0530 Subject: [PATCH 33/58] fix: address review on the Form Checks notices Reverts the Gmail transport back to a mailto. Four of the review's findings existed only because the support link had stopped being one, and PR #3118 removes the last caller of that machinery, so fixing them in place would have been work on code about to be deleted: - esc_url() strips %0a/%0d from every scheme except mailto:, so the classic renderer was deleting every line break from the composed body -- a regression against dev, measured at 16 breaks before and none after. The test that was meant to cover it asserted on the private method's return value, a layer above the escaping, so it could not see it. It now asserts through the renderer, and fails if the scheme changes again. - The body no longer leaves the machine. A compose URL put home_url(), the version set, the active caching plugin, the failing form's title and up to 1200 characters of debug log into a third party's request logs, the admin's synced browser history and any extension holding webRequest, with no preview and no consent step. - The trim that cut the assembled body cut from the end, which is the newest entries -- the ones describing the failure being reported. get_tail() already bounds the log by dropping whole lines from the oldest end, so the budget arithmetic is gone and the test now asserts the newest entry survives. - That arithmetic measured bytes and cut characters, so the ceiling was defeated by any non-Latin content: 2.9x over on CJK, 3.8x on emoji. Two payload-hygiene fixes stand regardless of transport: field_keys now goes through scrub_text() like every other free-text value, and scrub_text() masks credentials named in the text and everything past a URL's first path segment -- a Slack or Discord webhook token sits in the path, which the old rule kept verbatim. The notification fault now clears when notifications work again. clear_category() had one caller hardcoded to 'submission', and the notice is deliberately not dismissible, so a site that had fixed its SMTP kept an undismissable banner on every admin page until somebody opened a ticket. Held until every recipient for a submission is done, because one succeeding while another fails is still a failure. The Enable Logs gate moves below srfm_action_items. It was silencing a third party's advisory, which has nothing to do with SureForms' logging toggle. Its comment claimed the counters go stale with logging off; record_failure() has no enabled check and the notification and integration categories keep counting accurately, so the comment now says what the gate is. Both item URLs go through esc_url_raw() once in get_action_items() rather than relying on each renderer: the classic notice dropped a disallowed scheme, React assigned href directly and react-dom 18 leaves javascript: intact with its warning compiled out of production. The same pass decodes the & that get_sureforms_website_url() emits, which React would otherwise send to the server verbatim. The classic renderer also guards cta_label and cta_url together, so a filter item carrying only a guide no longer emits two undefined-key warnings and an empty href. The count sentence goes back behind _n() with literal calls, so a locale can supply the plural forms it actually uses -- the ternary on 1 === $count was correct only for two-form languages. With nothing recorded it describes the failure without a number instead of reporting a count nobody recorded. The carousel controls are created once and never moved. Moving them into the newly shown card unfocused their contents, so each activation dropped focus to , and it carried the aria-live counter out of and back into the document with its text already set, which is not announced. Positioning is in a stylesheet with logical properties, the arrows follow the reading direction, and the room reserved for the controls is measured rather than a number picked against the English counter. Also: seven @since tags to x.x.x; get_tail() memoised per request, which was six whole-file reads per admin page on a site with three open failures; the doc URLs through Helper::get_sureforms_website_url() with UTM attribution; focus-visible rings on the action links, which had none at all; the caching map's precedence documented; $dismissed read only when it is consulted; get_active_caching_plugin() called once; and four stale comments corrected. makepot regenerated -- the seven shipped locales need a translation pass for the reworded notice copy. --- admin/admin.php | 508 ++++++++++++++++++---------- admin/assets/js/notice-response.js | 57 ++-- inc/client-logger.php | 72 +++- inc/form-submit.php | 20 +- inc/helper.php | 30 +- languages/sureforms.pot | 361 +++++++++++++------- src/admin/dashboard/ActionItems.js | 41 ++- tests/unit/admin/test-admin.php | 256 +++++++++++--- tests/unit/inc/test-form-submit.php | 137 ++++++++ tests/unit/inc/test-helper.php | 9 +- 10 files changed, 1087 insertions(+), 404 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 00a0e0626..f2bf27add 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -88,28 +88,6 @@ class Admin { */ public const THANKYOU_PROMPT_NOTICE_ID = 'srfm-thankyou-prompt'; - /** - * Gmail's compose endpoint, used by the Contact Support action. - * - * No `/u/0/`: that pins the first signed-in account, which on a machine with - * several is often the wrong one. Without it Gmail composes from whichever - * account is active. - * - * @since 2.12.6 - */ - private const GMAIL_COMPOSE_URL = 'https://mail.google.com/mail/'; - - /** - * Ceiling for the generated support URL, in characters. - * - * Gmail truncates a long `body` without saying so, and browsers have their own - * limits. 2000 is the length every current browser handles, and it leaves the - * diagnostics block plus roughly the most recent few log entries. - * - * @since 2.12.6 - */ - private const SUPPORT_URL_BUDGET = 2000; - /** * Dashboard widget entries data. * @@ -3443,9 +3421,28 @@ public function render_action_item_notices() { return; } + $items = $this->get_action_items(); + + // Only the faults reach this surface, so count those before deciding + // whether the carousel stylesheet is worth printing. + $rendered = 0; + + foreach ( $items as $item ) { + $status = Helper::get_string_value( $item['status'] ?? '' ); + + if ( 'success' !== $status && '' !== $status ) { + $rendered++; + } + } + + if ( 0 === $rendered ) { + return; + } + $this->enqueue_notice_response_script(); + $this->print_action_item_carousel_styles(); - foreach ( $this->get_action_items() as $item ) { + foreach ( $items as $item ) { $status = Helper::get_string_value( $item['status'] ?? '' ); // Passing checks belong in the SureForms panel, not in wp-admin. A @@ -3466,6 +3463,13 @@ public function render_action_item_notices() { // identity: whichever action leads is the primary button, and an item // with no guide still leads with Contact Support. $has_guide = ! empty( $item['guide_label'] ) && ! empty( $item['guide_url'] ); + + // Both keys, not either. An item contributed through + // srfm_action_items may carry only guide_* keys -- reading cta_url + // unguarded emits two PHP 8 undefined-key warnings and renders + // href="" -- and a label without a URL renders an anchor that is not + // keyboard focusable. React gates on the same pair. + $has_cta = ! empty( $item['cta_label'] ) && ! empty( $item['cta_url'] ); ?>

@@ -3480,15 +3484,17 @@ class="button button-primary" - - > - - + + + > + + + @@ -3528,9 +3534,16 @@ public function handle_dismiss_action_item_link() { /** * Whether anything is currently wrong enough to warrant a notice. * - * Deliberately re-derives its conditions rather than calling - * get_action_items(), which records an impression as a side effect and must not - * run from a show_if callback. + * Deliberately re-derives rather than calling get_action_items(), which + * records an impression as a side effect and must not run from a show_if + * callback. It re-derives the two first-party conditions only -- a persistent + * failure and an active caching plugin -- so an item contributed through + * `srfm_action_items` does not suppress the notices this gates. + * + * Returns false with logging disabled, which is what makes the rating, + * getting-started and Thank-You notices eligible again on a site that has + * turned this surface off. Intended: those notices are gated on nothing being + * wrong, and with the surface off there is nothing being reported. * * @since 2.12.6 * @return bool @@ -3572,17 +3585,147 @@ public function get_action_items() { return []; } - // Logging off means the site owner has opted out of this whole surface: the - // fault counters stop being fed, so anything still standing in them is - // stale and cannot be trusted to reflect the site now. - if ( ! Client_Logger::is_enabled() ) { - return []; + // Logging off is the opt-out for this surface. Not because the counters go + // stale -- Client_Logger::record_failure() has no enabled check, and the + // notification and integration categories are written by direct calls in + // inc/form-submit.php that keep counting accurately with logging off. It is + // simply the switch a site owner has to turn these notices off, and it + // covers our own items only: the filter below still runs, because a third + // party's advisory has nothing to do with SureForms' logging toggle. + $warnings = []; + + if ( Client_Logger::is_enabled() ) { + $warnings = $this->get_first_party_action_items(); } - $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) ); - $warnings = []; + $this->track_action_item_impressions( $warnings ); + + /** + * Filter the dashboard action items. + * + * Each entry needs id, status ('warning' or 'success'), title, message, + * cta_label, cta_url and dismissible. Only ids in + * handle_dismiss_action_item()'s allowlist can actually be dismissed, so + * adding a dismissible item here also needs a line there. + * + * @since 2.12.6 + * + * @param array> $items Action items. + */ + $items = Helper::apply_filters_as_array( 'srfm_action_items', $warnings ); + + // Both URLs normalised once, here, rather than trusting each renderer to do + // it. Two things are being fixed at once. + // + // The scheme: the classic notice runs esc_url() and drops anything outside + // the allowlist, while React assigns href directly and react-dom 18 leaves + // a javascript: URL intact -- its sanitizeURL() only warns, and the warning + // is compiled out of the production build. esc_url_raw() with the same + // allowlist closes both. + // + // The ampersands: Helper::get_sureforms_website_url() returns an esc_url()'d + // string, so a URL with UTM parameters arrives with & in it. In an HTML + // href the browser decodes that; React sets the property directly, so the + // entity would be sent to the server verbatim. Decoded to one raw form here, + // and each renderer escapes it for its own context. + foreach ( $items as $index => $item ) { + foreach ( [ 'cta_url', 'guide_url' ] as $key ) { + if ( ! isset( $item[ $key ] ) ) { + continue; + } + + $items[ $index ][ $key ] = esc_url_raw( + wp_specialchars_decode( Helper::get_string_value( $item[ $key ] ), ENT_QUOTES ), + [ 'http', 'https', 'mailto' ] + ); + } + } + + return $items; + } + + /** + * Dismiss one action item. + * + * Hooked - wp_ajax_srfm_dismiss_action_item. + * + * Only items get_action_items() marks dismissible can be dismissed, so a + * crafted request cannot silence a genuine fault. + * + * @since 2.12.6 + * @return void + */ + public function handle_dismiss_action_item() { + if ( ! Helper::current_user_can() ) { + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); + return; + } + + if ( ! check_ajax_referer( 'srfm_dismiss_action_item', 'nonce', false ) ) { + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); + return; + } + + $item_id = isset( $_POST['item_id'] ) ? sanitize_key( wp_unslash( $_POST['item_id'] ) ) : ''; + + if ( ! $this->dismiss_action_item( $item_id ) ) { + wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 ); + return; + } + + wp_send_json_success(); + } + + /** + * Styles for the stacked-notice carousel. + * + * In a stylesheet rather than eight inline style assignments in + * notice-response.js, so the rules use logical properties and an RTL sheet can + * override them. The reserved room on the trailing edge is a custom property + * the script measures and sets, because a translated counter is wider than + * "1 of 4" and a fixed value lets a long form title run under the buttons. + * + * Printed only when at least one notice is about to render, and only from this + * renderer -- print_srfm_notice_styles() is hooked to + * astra_notice_before_markup_* and never fires for these. + * + * @since x.x.x + * @return void + */ + private function print_action_item_carousel_styles() { + ?> + + > + */ + private function get_first_party_action_items() { + $warnings = []; + $open = Client_Logger::get_open_failures(); // One item per category. They read differently to a site owner and must not // be collapsed: submissions failing means visitors cannot reach you, a @@ -3605,7 +3748,13 @@ public function get_action_items() { // Email is the one failure here a site owner can usually fix without // us: it is almost always SMTP not being configured. Offer the guide // alongside support rather than making them wait for a reply. - 'guide' => 'https://sureforms.com/docs/troubleshooting-email-sending-in-sureforms/', + 'guide' => Helper::get_sureforms_website_url( + 'docs/troubleshooting-email-sending-in-sureforms/', + [ + 'utm_medium' => 'form_checks_notice', + 'utm_content' => 'notification_error', + ] + ), ], 'integration' => [ 'id' => 'integration_error', @@ -3652,7 +3801,15 @@ public function get_action_items() { $caching_plugin = Helper::get_active_caching_plugin(); - if ( '' !== $caching_plugin && ! in_array( 'caching_plugin', $dismissed, true ) ) { + if ( '' === $caching_plugin ) { + return $warnings; + } + + // Read here rather than at the top: with no caching plugin active nothing + // consults it, and this is the only dismissible item. + $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) ); + + if ( ! in_array( 'caching_plugin', $dismissed, true ) ) { $warnings[] = [ 'id' => 'caching_plugin', 'status' => 'warning', @@ -3669,58 +3826,7 @@ public function get_action_items() { ]; } - // Only what needs attention. A panel confirming that nothing is wrong is - // something people learn to skip, and it takes sidebar space from the cards - // that do have something to say. - $items = $warnings; - - $this->track_action_item_impressions( $warnings ); - - /** - * Filter the dashboard action items. - * - * Each entry needs id, status ('warning' or 'success'), title, message, - * cta_label, cta_url and dismissible. Only ids in - * handle_dismiss_action_item()'s allowlist can actually be dismissed, so - * adding a dismissible item here also needs a line there. - * - * @since 2.12.6 - * - * @param array> $items Action items. - */ - return Helper::apply_filters_as_array( 'srfm_action_items', $items ); - } - - /** - * Dismiss one action item. - * - * Hooked - wp_ajax_srfm_dismiss_action_item. - * - * Only items get_action_items() marks dismissible can be dismissed, so a - * crafted request cannot silence a genuine fault. - * - * @since 2.12.6 - * @return void - */ - public function handle_dismiss_action_item() { - if ( ! Helper::current_user_can() ) { - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); - return; - } - - if ( ! check_ajax_referer( 'srfm_dismiss_action_item', 'nonce', false ) ) { - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); - return; - } - - $item_id = isset( $_POST['item_id'] ) ? sanitize_key( wp_unslash( $_POST['item_id'] ) ) : ''; - - if ( ! $this->dismiss_action_item( $item_id ) ) { - wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 ); - return; - } - - wp_send_json_success(); + return $warnings; } /** @@ -4301,9 +4407,11 @@ private function get_dismiss_action_item_url( $item_id ) { * sites are affected. A day per user answers the question that matters -- how * many people are seeing this -- for one option write. * - * Passing checks are not counted. "Nothing is wrong" is not an impression. + * Counts SureForms' own items only. It runs before `srfm_action_items`, so a + * third party's contribution is not counted here -- SureForms has no name for + * it and no analytics key that would mean anything. * - * @param array> $warnings Warning items only. + * @param array> $warnings SureForms' own items. * @since 2.12.6 * @return void */ @@ -4366,21 +4474,21 @@ private function track_action_item_impressions( $warnings ) { * ticket titled "form submissions are failing" and a count that belonged to a * different counter -- wrong at a glance and routed to the wrong place. * - * Opens Gmail's compose window rather than handing off to `mailto:`. A mailto - * goes to whatever the machine has registered as its mail handler, which on a - * machine with none configured opens nothing at all; this always lands - * somewhere the person can see and send. - * - * The trade is that someone who does not use Gmail on the web gets a compose - * window for an account they may not want. They can still copy the message out - * of it, and it is a visible window rather than a link that appears to do - * nothing. - * - * The log is pasted into the body rather than attached because neither a mailto - * nor a compose URL can carry an attachment, and it is a tail rather than the - * whole file because a megabyte of JSON would exceed any URL length. Gmail - * truncates a long `body` silently, so the log is trimmed to a budget below -- - * losing the oldest entries visibly beats losing the newest without saying so. + * A `mailto:`, handed to whatever the machine has registered as its mail + * handler. The body never leaves the machine on the way there, which a webmail + * compose URL cannot say: that would put the site host, the version set, the + * form title and the log excerpt into a third party's request logs, the + * admin's synced browser history and any extension holding `webRequest`. + * `esc_url()` also exempts `mailto:` from its `%0a`/`%0d` stripping + * (`wp-includes/formatting.php`), so the body keeps its line breaks through + * the classic renderer; every other scheme arrives as one paragraph. + * + * The log is pasted into the body rather than attached because a mailto has no + * attachment parameter -- browsers drop any attempt to add one -- and it is a + * tail rather than the whole file because a megabyte of JSON would exceed the + * URL length every mail client enforces. Client_Logger::get_tail() applies + * that bound itself, cutting whole lines from the oldest end, so the entries + * describing the failure being reported are the ones that survive. * * @param string $category One of Client_Logger::CATEGORIES. Unknown or absent * gets neutral wording rather than a specific claim. @@ -4397,11 +4505,10 @@ private function get_support_email_url( $category = '', $form_title = '' ) { Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ) ); - // Bare LF, not CRLF. CRLF is the mailto convention and correct when a mail - // client parses the URL, but Gmail's compose window does not honour the - // carriage returns and runs the whole body together as one paragraph -- the - // diagnostics and the log arrive as a wall of text. Nothing parses this as - // a mail header any more, so there is no reason left to send CRLF. + // Bare LF, not CRLF. RFC 6068 says a mailto body's line breaks are %0A, and + // the carriage return is what several webmail compose windows drop on the + // floor -- the diagnostics and the log then arrive as one paragraph. No + // consumer here parses this as a mail header, so CRLF buys nothing. $log = Client_Logger::get_tail(); $body = $this->get_support_message( $category, $form_title ); $body .= "\n\n" . '---' . "\n"; @@ -4426,49 +4533,24 @@ private function get_support_email_url( $category = '', $form_title = '' ) { } } - /** - * Filter the support address the Contact Support action writes to. - * - * @since 2.12.6 - * - * @param string $address Support inbox. - */ - $address = Helper::get_string_value( apply_filters( 'srfm_support_email_address', 'support@sureforms.com' ) ); - - // Budget for the whole URL. Gmail drops the overflow of an over-long body - // without a word, and the overflow is the end of the log -- the newest - // entries, the ones describing the failure being reported. Trimming here - // instead means the oldest go first and the note below says so. - $base = self::GMAIL_COMPOSE_URL . '?' . http_build_query( + return 'mailto:support@sureforms.com?' . http_build_query( [ - 'view' => 'cm', - 'fs' => '1', - 'to' => $address, - 'su' => $subject, + 'subject' => $subject, + 'body' => $body, ], '', '&', PHP_QUERY_RFC3986 - ) . '&body='; - - $room = self::SUPPORT_URL_BUDGET - strlen( $base ); - - if ( strlen( rawurlencode( $body ) ) > $room ) { - // Cut on the raw string, then re-encode: slicing an encoded string can - // land inside a percent-escape and produce a malformed URL. - $budget = max( 0, (int) floor( $room / 3 ) ); - $body = mb_substr( $body, 0, $budget ); - $body .= "\n\n" . __( 'This log was shortened to fit. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); - } - - return $base . rawurlencode( $body ); + ); } /** - * Subject and opening line for one kind of failure. + * Subject and countless opening line for one kind of failure. * * Both come from here so they cannot drift apart: a subject naming one problem - * over a body describing another is worse than either alone. + * over a body describing another is worse than either alone. The counted form + * of the opening line lives in get_support_count_sentence(), which needs + * `_n()`'s literals and so cannot be an array lookup. * * An unknown or absent category gets deliberately neutral wording. The * alternative -- defaulting to the submission copy -- states something specific @@ -4476,34 +4558,28 @@ private function get_support_email_url( $category = '', $form_title = '' ) { * category at all. * * @param string $category One of Client_Logger::CATEGORIES. - * @since 2.12.6 - * @return array{subject:string,singular:string,plural:string} + * @since x.x.x + * @return array{subject:string,anon:string} */ private function get_support_copy( $category ) { $copy = [ 'submission' => [ /* translators: %s: site host. */ - 'subject' => __( 'SureForms: form submissions are failing on %s', 'sureforms' ), - /* translators: %d: number of failed submissions. */ - 'singular' => __( 'SureForms has recorded %d form submission that could not be completed.', 'sureforms' ), - /* translators: %d: number of failed submissions. */ - 'plural' => __( 'SureForms has recorded %d form submissions that could not be completed.', 'sureforms' ), + 'subject' => __( 'SureForms: form submissions are failing on %s', 'sureforms' ), + /* translators: %s: site host. */ + 'anon' => __( 'SureForms has recorded form submissions on %s that could not be completed.', 'sureforms' ), ], 'notification' => [ /* translators: %s: site host. */ - 'subject' => __( 'SureForms: notification emails are not being sent on %s', 'sureforms' ), - /* translators: %d: number of failed notifications. */ - 'singular' => __( 'SureForms saved %d entry but could not send the notification email for it.', 'sureforms' ), - /* translators: %d: number of failed notifications. */ - 'plural' => __( 'SureForms saved %d entries but could not send the notification emails for them.', 'sureforms' ), + 'subject' => __( 'SureForms: notification emails are not being sent on %s', 'sureforms' ), + /* translators: %s: site host. */ + 'anon' => __( 'SureForms saved entries on %s but could not send the notification emails for them.', 'sureforms' ), ], 'integration' => [ /* translators: %s: site host. */ - 'subject' => __( 'SureForms: an integration is not receiving entries on %s', 'sureforms' ), - /* translators: %d: number of failed integration hand-offs. */ - 'singular' => __( 'SureForms saved %d entry but could not pass it to a connected service.', 'sureforms' ), - /* translators: %d: number of failed integration hand-offs. */ - 'plural' => __( 'SureForms saved %d entries but could not pass them to a connected service.', 'sureforms' ), + 'subject' => __( 'SureForms: an integration is not receiving entries on %s', 'sureforms' ), + /* translators: %s: site host. */ + 'anon' => __( 'SureForms saved entries on %s but could not pass them to a connected service.', 'sureforms' ), ], ]; @@ -4513,14 +4589,80 @@ private function get_support_copy( $category ) { return [ /* translators: %s: site host. */ - 'subject' => __( 'SureForms: a problem with the forms on %s', 'sureforms' ), - /* translators: %d: number of recorded problems. */ - 'singular' => __( 'SureForms has recorded %d problem with the forms on this site.', 'sureforms' ), - /* translators: %d: number of recorded problems. */ - 'plural' => __( 'SureForms has recorded %d problems with the forms on this site.', 'sureforms' ), + 'subject' => __( 'SureForms: a problem with the forms on %s', 'sureforms' ), + /* translators: %s: site host. */ + 'anon' => __( 'SureForms has recorded a problem with the forms on %s.', 'sureforms' ), ]; } + /** + * The sentence that opens the support email, with the failure count in it. + * + * A switch with literal `_n()` calls rather than a singular/plural pair looked + * up from an array. `_n()` has to see its two literals at extraction time to + * emit an `msgid_plural`, and only that lets a locale supply the number of + * forms it actually uses -- Polish and Russian need three, Arabic six, + * Japanese one. Choosing on `1 === $count` in PHP is correct for English and + * wrong everywhere with a different plural rule. + * + * @param string $category One of Client_Logger::CATEGORIES. Unknown or absent + * gets neutral wording rather than a specific claim. + * @param int $count Failures recorded for that category. + * @since x.x.x + * @return string + */ + private function get_support_count_sentence( $category, $count ) { + switch ( $category ) { + case 'submission': + return sprintf( + /* translators: %d: number of failed submissions. */ + _n( + 'SureForms has recorded %d form submission that could not be completed.', + 'SureForms has recorded %d form submissions that could not be completed.', + $count, + 'sureforms' + ), + $count + ); + + case 'notification': + return sprintf( + /* translators: %d: number of failed notifications. */ + _n( + 'SureForms saved %d entry but could not send the notification email for it.', + 'SureForms saved %d entries but could not send the notification emails for them.', + $count, + 'sureforms' + ), + $count + ); + + case 'integration': + return sprintf( + /* translators: %d: number of failed integration hand-offs. */ + _n( + 'SureForms saved %d entry but could not pass it to a connected service.', + 'SureForms saved %d entries but could not pass them to a connected service.', + $count, + 'sureforms' + ), + $count + ); + + default: + return sprintf( + /* translators: %d: number of recorded problems. */ + _n( + 'SureForms has recorded %d problem with the forms on this site.', + 'SureForms has recorded %d problems with the forms on this site.', + $count, + 'sureforms' + ), + $count + ); + } + } + /** * Diagnostics block for the support email. * @@ -4543,14 +4685,17 @@ private function get_support_message( $category = '', $form_title = '' ) { $count = Helper::get_integer_value( $failures[ $category ]['count'] ?? 0 ); $copy = $this->get_support_copy( $category ); - // A category with nothing recorded still needs to read as a sentence, and - // "0 entries" reads as a bug in the email rather than a fault on the site. - $count = max( 1, $count ); + // With nothing recorded, describe the failure without a number. The old + // max( 1, $count ) reported "recorded 1 problem" and "Recorded failures: 1" + // for a count nobody recorded -- a number support would then chase. + $host = Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ); $lines = [ __( 'Hello SureForms support,', 'sureforms' ), '', - sprintf( 1 === $count ? $copy['singular'] : $copy['plural'], $count ), + $count > 0 + ? $this->get_support_count_sentence( $category, $count ) + : sprintf( $copy['anon'], $host ), ]; if ( '' !== $form_title ) { @@ -4562,6 +4707,9 @@ private function get_support_message( $category = '', $form_title = '' ) { ); } + // Once: each call reads an option and a site option. + $caching = Helper::get_active_caching_plugin(); + $lines = array_merge( $lines, [ @@ -4573,8 +4721,8 @@ private function get_support_message( $category = '', $form_title = '' ) { 'SureForms Pro: ' . ( Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' ) ), 'WordPress: ' . Helper::get_string_value( $wp_version ), 'PHP: ' . PHP_VERSION, - 'Caching: ' . ( '' !== Helper::get_active_caching_plugin() ? Helper::get_active_caching_plugin() : __( 'none detected', 'sureforms' ) ), - 'Recorded failures: ' . $count, + 'Caching: ' . ( '' !== $caching ? $caching : __( 'none detected', 'sureforms' ) ), + 'Recorded failures: ' . ( $count > 0 ? $count : __( 'none recorded', 'sureforms' ) ), ] ); diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js index 0f54db494..84fb083e2 100644 --- a/admin/assets/js/notice-response.js +++ b/admin/assets/js/notice-response.js @@ -97,6 +97,15 @@ * Built here rather than printed by PHP so that with JavaScript off every * notice simply stays visible, exactly as before. Controls that cannot work * must not be the thing that hides a warning. + * + * The controls are created once, in the wrapper, and never moved. Moving them + * into the newly shown card ran the DOM remove steps first, which unfocuses + * whatever they contain -- so every activation dropped focus to and a + * keyboard user had to Tab in from the top of the page again, once per notice. + * It also carried the aria-live counter out of and back into the document with + * its text already set, which is generally not announced. Only one card is ever + * visible, so the wrapper's box is the visible card's box and pinning the + * controls to the wrapper looks identical. */ function buildNoticeCarousel() { const cards = Array.prototype.slice.call( @@ -108,7 +117,10 @@ return; } - const labels = ( srfmNoticeResponse && srfmNoticeResponse.carousel ) || {}; + const labels = + ( typeof srfmNoticeResponse !== 'undefined' && + srfmNoticeResponse.carousel ) || + {}; let index = 0; // Wrap in place, so the cards keep the position WordPress gave them @@ -120,50 +132,40 @@ wrap.appendChild( notice ); } ); - // Pinned to the top right of the notice being read. Absolute rather than a - // float so it cannot reflow the message text, and the notice gets padding on - // that side to reserve the space -- a long form title would otherwise run - // underneath the controls. + // Positioning and spacing live in the stylesheet the renderer prints, so + // an RTL sheet can override them and nothing here is a magic number. const nav = document.createElement( 'p' ); nav.className = 'srfm-action-item-carousel-nav'; - nav.style.display = 'flex'; - nav.style.alignItems = 'center'; - nav.style.gap = '8px'; - nav.style.position = 'absolute'; - nav.style.top = '8px'; - nav.style.right = '12px'; - nav.style.margin = '0'; - cards.forEach( function ( notice ) { - notice.style.position = 'relative'; - notice.style.paddingRight = '130px'; - } ); + const isRtl = + document.documentElement.getAttribute( 'dir' ) === 'rtl' || + document.body.classList.contains( 'rtl' ); const prev = document.createElement( 'button' ); prev.type = 'button'; prev.className = 'button button-small'; - prev.innerHTML = '‹'; + // Previous points at the start of the reading order, which is the right in + // an RTL locale. Hardcoding one direction contradicts the aria-label. + prev.textContent = isRtl ? '\u203A' : '\u2039'; prev.setAttribute( 'aria-label', labels.previous || 'Previous' ); const next = document.createElement( 'button' ); next.type = 'button'; next.className = 'button button-small'; - next.innerHTML = '›'; + next.textContent = isRtl ? '\u2039' : '\u203A'; next.setAttribute( 'aria-label', labels.next || 'Next' ); const counter = document.createElement( 'span' ); // Announced, because stepping swaps the text above with no other signal. + // The node stays put, so the region is in the document before its text + // changes -- which is what makes the change announce at all. counter.setAttribute( 'aria-live', 'polite' ); function render() { cards.forEach( function ( notice, i ) { - notice.style.display = i === index ? '' : 'none'; + notice.hidden = i !== index; } ); - // Moved rather than duplicated: one set of controls, always inside the - // notice being read, so the count sits with the message it counts. - cards[ index ].appendChild( nav ); - counter.textContent = ( labels.counter || '%1$d of %2$d' ) .replace( '%1$d', index + 1 ) .replace( '%2$d', cards.length ); @@ -184,8 +186,17 @@ nav.appendChild( prev ); nav.appendChild( counter ); nav.appendChild( next ); + wrap.appendChild( nav ); render(); + + // Reserve exactly the room the controls take, measured rather than assumed: + // a translated counter is wider than "1 of 4" and a fixed padding lets a + // long form title run underneath the buttons. + wrap.style.setProperty( + '--srfm-carousel-reserve', + Math.ceil( nav.getBoundingClientRect().width ) + 24 + 'px' + ); } if ( document.readyState === 'loading' ) { diff --git a/inc/client-logger.php b/inc/client-logger.php index ff2c2029c..51634d60a 100644 --- a/inc/client-logger.php +++ b/inc/client-logger.php @@ -102,6 +102,18 @@ class Client_Logger { */ public const CATEGORIES = [ 'submission', 'notification', 'integration' ]; + /** + * Memoised get_tail() results for this request, keyed by character budget. + * + * A class property rather than a static inside the method so append() and + * clear() can invalidate it: a request that writes to the log and then reads a + * tail must not be handed the tail from before the write. + * + * @var array + * @since x.x.x + */ + private static $tail_memo = []; + /** * Whether client error logging is currently switched on. * @@ -347,8 +359,13 @@ public static function acknowledge_failures() { * Hooked - srfm_form_submit, which fires only on the success path. * * Only the submission category is cleared. A submission getting through says - * nothing about whether its notification email sent or its integrations ran, - * so those clear when they next succeed or when the owner reports them. + * nothing about whether its notification email sent or its integrations ran. + * Notification clears on its own path, in Form_Submit::send_email(), once every + * recipient for a submission has sent. Integration has no success signal to + * clear on yet -- the failures are recorded by pro through + * Form_Submit::log_integration_failure() and there is no matching + * "it worked" call -- so that category still clears only when the owner + * reports it. * * @since 2.12.6 * @return void @@ -392,6 +409,9 @@ public static function get_log_path( $create = true ) { * @return bool True when the line was written. */ public static function append( array $entry ) { + // Any write invalidates a memoised tail, whether or not this one lands. + self::$tail_memo = []; + // Checked here as well as at the route, so the guard sits on the function // that writes rather than only on today's single caller. Without it any // future caller writes to disk on a site that never switched logging on. @@ -455,6 +475,17 @@ public static function append( array $entry ) { * @return array{text:string,shown:int,total:int} */ public static function get_tail( $max_chars = 1200 ) { + // Memoised per request and per budget. get_action_items() asks once per + // open failure category and runs twice per admin request -- building the + // localisation payload and again in the classic renderer -- so a site with + // three open failures was reading a file capped at 1 MB six times to render + // one page. + $max_chars = (int) $max_chars; + + if ( isset( self::$tail_memo[ $max_chars ] ) ) { + return self::$tail_memo[ $max_chars ]; + } + $empty = [ 'text' => '', 'shown' => 0, @@ -464,6 +495,8 @@ public static function get_tail( $max_chars = 1200 ) { $path = self::get_log_path( false ); if ( '' === $path || ! file_exists( $path ) ) { + self::$tail_memo[ $max_chars ] = $empty; + return $empty; } @@ -471,6 +504,8 @@ public static function get_tail( $max_chars = 1200 ) { $lines = file( $path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); if ( ! is_array( $lines ) || empty( $lines ) ) { + self::$tail_memo[ $max_chars ] = $empty; + return $empty; } @@ -491,11 +526,13 @@ public static function get_tail( $max_chars = 1200 ) { $used += $length; } - return [ + self::$tail_memo[ $max_chars ] = [ 'text' => implode( "\n", $kept ), 'shown' => count( $kept ), 'total' => $total, ]; + + return self::$tail_memo[ $max_chars ]; } /** @@ -533,6 +570,8 @@ public static function get_file_size() { * @return bool */ public static function clear() { + self::$tail_memo = []; + $path = self::get_log_path( false ); if ( '' === $path || ! file_exists( $path ) ) { @@ -594,7 +633,11 @@ public static function sanitize_entry( array $raw ) { // disabled the feature until an admin cleared it. A real key is // `srfm-input-lbl-`, far inside this bound. foreach ( array_slice( $raw['field_keys'], 0, 100 ) as $field_key ) { - $keys[] = mb_substr( sanitize_text_field( Helper::get_string_value( $field_key ) ), 0, self::MAX_KEY_LENGTH ); + // Through scrub_text() like every other free-text value. A key is + // supposed to be `srfm-input-lbl-`, but the array arrives + // from the browser and nothing server-side guarantees that, so a + // caller is free to put an address or a token in one. + $keys[] = mb_substr( self::scrub_text( Helper::get_string_value( $field_key ) ), 0, self::MAX_KEY_LENGTH ); } $entry['field_keys'] = $keys; @@ -615,6 +658,13 @@ public static function sanitize_entry( array $raw ) { * string. Whitespace is collapsed as a log-injection guard, matching * inc/ai-form-builder/ai-helper.php. * + * Removes, in order: URL query strings and fragments; everything past a URL's + * first path segment, because a webhook credential sits in the path as often + * as in the query; the value following a name that identifies a credential; + * email addresses; and long digit runs. What it cannot remove is a name, a + * street address or a free-text message body -- those have no shape to match, + * so the log excerpt this produces should still be treated as personal data. + * * @param string $text Raw text. * @since 2.12.6 * @return string @@ -628,6 +678,20 @@ public static function scrub_text( $text ) { // parameters. A token after # is just as sensitive as one after ?. $text = (string) preg_replace( '#(https?://[^\s?\#]+)[?\#]\S*#i', '$1', $text ); + // Keep the origin and the first path segment, drop the rest. A webhook + // credential is as often in the path as in the query -- Slack and Discord + // both put theirs there -- and the rule above keeps paths verbatim. + $text = (string) preg_replace( '#(https?://[^\s/]+(?:/[^\s/]*)?)/\S+#i', '$1/[path]', $text ); + + // Credentials named in the text itself, whether or not they sit in a URL: + // `api_key=…`, `Authorization: Bearer …`, `"token": "…"`. Matched on the + // name so the value's shape does not have to be guessed. + $text = (string) preg_replace( + '/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|secret|token|bearer|password|passwd|pwd|auth)["\']?\s*(?:[:=]|\s)\s*["\']?)[^\s"\',;&]{6,}/i', + '$1[redacted]', + $text + ); + // Email addresses. $text = (string) preg_replace( '/[\w.+-]+@[\w-]+\.[\w.-]+/', '[email]', $text ); diff --git a/inc/form-submit.php b/inc/form-submit.php index 008a22d04..dee4b706f 100644 --- a/inc/form-submit.php +++ b/inc/form-submit.php @@ -1030,7 +1030,10 @@ public static function parse_email_notification_template( $submission_data, $ite public static function send_email( $id, $submission_data, $form_data = [] ) { $email_notification = get_post_meta( intval( $id ), '_srfm_email_notification' ); $is_mail_sent = false; - $emails = []; + // Any recipient failing counts as a failure for the whole submission, so + // this is set inside the loop and only read after it. + $notification_failed = false; + $emails = []; // Filter to determine whether the email notification should be sent. $email_notification = apply_filters( 'srfm_email_notification_should_send', $email_notification, $submission_data, $form_data ); @@ -1103,6 +1106,10 @@ static function() { } $email_report = ob_get_clean(); // Catch any printed notice/errors/message for reports. + if ( true !== $sent ) { + $notification_failed = true; + } + if ( is_int( $log_key ) ) { if ( true === $sent ) { $entries_db_instance->update_log( @@ -1183,6 +1190,17 @@ static function() { $entries_db_instance->reset_logs(); $entries_db_instance->add_log( __( 'No emails were sent.', 'sureforms' ) ); } + + // The notification fault clears when notifications work again. Nothing + // else retired it: Client_Logger::clear_category() had a single caller + // hardcoded to 'submission', and the notice is deliberately not + // dismissible, so a site that had fixed its SMTP kept an undismissable + // banner on every admin page until somebody opened a support ticket. + // Held until the loop is done because one recipient succeeding while + // another fails is still a failure. + if ( ! empty( $emails ) && ! $notification_failed ) { + Client_Logger::clear_category( 'notification' ); + } } return [ diff --git a/inc/helper.php b/inc/helper.php index 640ae0f18..48da2867a 100644 --- a/inc/helper.php +++ b/inc/helper.php @@ -2238,14 +2238,24 @@ public static function get_active_caching_plugin() { * Falls back to the general guide rather than returning nothing, so the notice * always has somewhere to send them. * - * @since 2.12.6 + * @since x.x.x * @return string Absolute documentation URL. */ public static function get_caching_plugin_doc_url() { $entry = self::get_active_caching_plugin_entry(); $slug = null === $entry || '' === $entry[1] ? 'how-to-set-up-sureforms-with-caching-plugins' : $entry[1]; - return 'https://sureforms.com/docs/' . $slug . '/'; + // Through the central builder rather than hardcoding the domain, so the + // link carries the same UTM attribution as every other doc link and a + // domain change is one edit. utm_content is the slug, so the notice can be + // told which guide people actually open. + return self::get_sureforms_website_url( + 'docs/' . $slug . '/', + [ + 'utm_medium' => 'form_checks_notice', + 'utm_content' => $slug, + ] + ); } /** @@ -2787,7 +2797,16 @@ private static function get_geo_failure_ttl() { * array on purpose: keyed separately they drift, and a doc link that silently * degrades to the generic page is the kind of regression nobody reports. * - * @since 2.12.6 + * Order is precedence: the first active plugin in this list wins. The six with + * their own guide are listed first on purpose, so a site running two caching + * plugins is pointed at the specific guide rather than whichever plugin the + * old alphabetical order happened to reach first. That flips the winner on a + * few pairs -- WP Fastest Cache over WP Super Cache, SiteGround Optimizer and + * Autoptimize over their partners -- and in each case the new winner is the + * one that has something to say. Reordering this array changes which guide a + * two-plugin site sees. + * + * @since x.x.x * @return array */ private static function get_known_caching_plugins() { @@ -2819,9 +2838,10 @@ private static function get_known_caching_plugins() { * The active caching plugin's entry, if there is one. * * Detection is by plugin path, mirroring is_any_smtp_plugin_active(), including - * the multisite network-active merge. + * the multisite network-active merge. First match in + * get_known_caching_plugins() wins; that array's order is the precedence. * - * @since 2.12.6 + * @since x.x.x * @return array{0:string,1:string}|null Name and doc slug, or null when none is active. */ private static function get_active_caching_plugin_entry() { diff --git a/languages/sureforms.pot b/languages/sureforms.pot index 97fa5f208..c98ed9b7d 100644 --- a/languages/sureforms.pot +++ b/languages/sureforms.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-09-01T12:14:35+00:00\n" +"POT-Creation-Date: 2026-09-09T10:57:21+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.11.0\n" "X-Domain: sureforms\n" @@ -19,7 +19,7 @@ msgstr "" #: sureforms.php #: admin/admin.php:1152 #: admin/admin.php:1153 -#: admin/admin.php:3021 +#: admin/admin.php:3057 #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 @@ -59,7 +59,7 @@ msgid "Review or edit your form" msgstr "" #: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:4111 msgid "Edit form" msgstr "" @@ -139,7 +139,7 @@ msgstr "" #: admin/admin.php:1236 #: admin/admin.php:1237 -#: admin/admin.php:4129 +#: admin/admin.php:4333 #: assets/build/blocks.js:172 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 @@ -220,7 +220,7 @@ msgstr "" #: admin/admin.php:1461 #: admin/admin.php:1462 -#: admin/admin.php:3340 +#: admin/admin.php:3376 #: inc/generate-form-markup.php:211 #: inc/generate-form-markup.php:224 #: inc/global-settings/email-summary.php:225 @@ -381,12 +381,25 @@ msgstr "" msgid "I already know" msgstr "" -#: admin/admin.php:2784 -#: admin/admin.php:2912 -#: admin/admin.php:2950 -#: admin/admin.php:2971 -#: admin/admin.php:3294 -#: admin/admin.php:3601 +#: admin/admin.php:2774 +msgid "Previous notice" +msgstr "" + +#: admin/admin.php:2775 +msgid "Next notice" +msgstr "" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2777 +msgid "%1$d of %2$d" +msgstr "" + +#: admin/admin.php:2794 +#: admin/admin.php:2948 +#: admin/admin.php:2986 +#: admin/admin.php:3007 +#: admin/admin.php:3330 +#: admin/admin.php:3652 #: inc/admin-ajax.php:164 #: inc/payments/admin/admin-handler.php:638 #: inc/payments/stripe/admin-stripe-handler.php:80 @@ -394,230 +407,316 @@ msgstr "" msgid "Invalid nonce." msgstr "" -#: admin/admin.php:2789 -#: admin/admin.php:2908 -#: admin/admin.php:2946 -#: admin/admin.php:2967 -#: admin/admin.php:3298 -#: admin/admin.php:3596 +#: admin/admin.php:2799 +#: admin/admin.php:2944 +#: admin/admin.php:2982 +#: admin/admin.php:3003 +#: admin/admin.php:3334 +#: admin/admin.php:3647 msgid "Unauthorized user." msgstr "" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2852 +#: admin/admin.php:3659 msgid "Invalid parameters." msgstr "" #. translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong -#: admin/admin.php:2917 +#: admin/admin.php:2953 msgid "%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s" msgstr "" -#: admin/admin.php:2928 +#: admin/admin.php:2964 msgid "SureForms is waiting for you!" msgstr "" -#: admin/admin.php:2930 +#: admin/admin.php:2966 msgid "Build My First Form" msgstr "" -#: admin/admin.php:2931 -#: admin/admin.php:3418 +#: admin/admin.php:2967 +#: admin/admin.php:3500 msgid "Dismiss" msgstr "" -#: admin/admin.php:3039 +#: admin/admin.php:3075 msgid "SureForms AI Quick Draft" msgstr "" -#: admin/admin.php:3058 +#: admin/admin.php:3094 msgid "Describe the form and let SureForms AI generate it for you." msgstr "" -#: admin/admin.php:3061 +#: admin/admin.php:3097 msgid "Describe your form" msgstr "" -#: admin/admin.php:3068 +#: admin/admin.php:3104 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "" -#: admin/admin.php:3072 +#: admin/admin.php:3108 #: inc/page-builders/elementor/form-widget.php:346 #: assets/build/dashboard.js:2 msgid "Create New Form" msgstr "" -#: admin/admin.php:3109 +#: admin/admin.php:3145 msgid "Redirecting..." msgstr "" -#: admin/admin.php:3327 +#: admin/admin.php:3363 msgid "Recent Entries" msgstr "" -#: admin/admin.php:3328 +#: admin/admin.php:3364 msgid "( Last 7 days )" msgstr "" -#: admin/admin.php:3331 +#: admin/admin.php:3367 msgid "View" msgstr "" -#: admin/admin.php:3339 +#: admin/admin.php:3375 #: inc/global-settings/email-summary.php:224 #: assets/build/entries.js:172 #: assets/build/payments.js:172 msgid "Form Name" msgstr "" -#: admin/admin.php:3437 +#: admin/admin.php:3519 msgid "You do not have permission to do this." msgstr "" -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "" -msgstr[1] "" +#. translators: %s: form title. +#: admin/admin.php:3725 +msgid "We noticed a form submission failure on %s." +msgstr "" -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." +#: admin/admin.php:3726 +msgid "We noticed a form submission failure." msgstr "" -#: admin/admin.php:3516 -#: assets/build/templatePicker.js:172 -msgid "Contact Support" +#: admin/admin.php:3727 +msgid "Visitors may not be able to reach you, and their entries were not saved." msgstr "" -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." +#. translators: %s: form title. +#: admin/admin.php:3732 +msgid "We noticed a notification failure on %s." msgstr "" -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." +#: admin/admin.php:3733 +msgid "We noticed a notification failure." msgstr "" -#. translators: %s: caching plugin name. -#: admin/admin.php:3551 -msgid "%s may interfere with your forms." +#: admin/admin.php:3734 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "" + +#. translators: %s: form title. +#: admin/admin.php:3749 +msgid "We noticed an integration failure on %s." +msgstr "" + +#: admin/admin.php:3750 +msgid "We noticed an integration failure." msgstr "" -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." +#: admin/admin.php:3751 +msgid "The entry was saved, but we could not send it to a connected service." msgstr "" -#: admin/admin.php:3555 +#: admin/admin.php:3771 +#: assets/build/templatePicker.js:172 +msgid "Contact Support" +msgstr "" + +#: admin/admin.php:3781 +#: admin/admin.php:3809 msgid "Help Me Fix" msgstr "" -#: admin/admin.php:3650 +#. translators: %s: caching plugin name. +#: admin/admin.php:3805 +msgid "%s may interfere with your forms." +msgstr "" + +#: admin/admin.php:3808 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "" + +#: admin/admin.php:3854 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "" -#: admin/admin.php:3653 +#: admin/admin.php:3857 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "" -#: admin/admin.php:3896 +#: admin/admin.php:4100 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "" #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4105 msgid "Finish setting up “%s”" msgstr "" -#: admin/admin.php:3914 +#: admin/admin.php:4118 msgid "Edit the Thank You message" msgstr "" -#: admin/admin.php:3920 +#: admin/admin.php:4124 msgid "Set where replies go" msgstr "" -#: admin/admin.php:4070 +#: admin/admin.php:4274 msgid "Use Conditional Logic to show only what matters" msgstr "" -#: admin/admin.php:4071 +#: admin/admin.php:4275 msgid "Split your form into steps to keep it easy" msgstr "" -#: admin/admin.php:4072 +#: admin/admin.php:4276 msgid "Let people upload files directly to your form" msgstr "" -#: admin/admin.php:4073 +#: admin/admin.php:4277 msgid "Turn responses into downloadable PDFs automatically" msgstr "" -#: admin/admin.php:4074 +#: admin/admin.php:4278 msgid "Let users sign with a simple signature field" msgstr "" -#: admin/admin.php:4075 +#: admin/admin.php:4279 msgid "Connect your form to other tools using webhooks" msgstr "" -#: admin/admin.php:4076 +#: admin/admin.php:4280 msgid "Use Conversational Forms for a chat-like experience" msgstr "" -#: admin/admin.php:4077 +#: admin/admin.php:4281 msgid "Let users register or log in through your form" msgstr "" -#: admin/admin.php:4078 +#: admin/admin.php:4282 msgid "Build forms that create WordPress user accounts" msgstr "" -#: admin/admin.php:4079 +#: admin/admin.php:4283 msgid "Add calculations to auto-total scores or prices" msgstr "" -#. translators: %s: site host. -#: admin/admin.php:4265 -msgid "SureForms: form submissions are failing on %s" -msgstr "" - -#: admin/admin.php:4274 +#: admin/admin.php:4502 msgid "Debug log: no entries recorded." msgstr "" #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4506 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "" -#: admin/admin.php:4288 +#: admin/admin.php:4517 msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." msgstr "" -#: admin/admin.php:4318 -msgid "Hello SureForms support," +#. translators: %s: site host. +#: admin/admin.php:4553 +msgid "SureForms: form submissions are failing on %s" msgstr "" -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." +#. translators: %s: site host. +#: admin/admin.php:4555 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "" + +#. translators: %s: site host. +#: admin/admin.php:4559 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "" + +#. translators: %s: site host. +#: admin/admin.php:4561 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "" + +#. translators: %s: site host. +#: admin/admin.php:4565 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "" + +#. translators: %s: site host. +#: admin/admin.php:4567 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "" + +#. translators: %s: site host. +#: admin/admin.php:4577 +msgid "SureForms: a problem with the forms on %s" +msgstr "" + +#. translators: %s: site host. +#: admin/admin.php:4579 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "" + +#. translators: %d: number of failed submissions. +#: admin/admin.php:4604 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." msgstr[0] "" msgstr[1] "" -#: admin/admin.php:4332 +#. translators: %d: number of failed notifications. +#: admin/admin.php:4616 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "" +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:4628 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "" +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:4640 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "" +msgstr[1] "" + +#: admin/admin.php:4679 +msgid "Hello SureForms support," +msgstr "" + +#. translators: %s: form title. +#: admin/admin.php:4690 +msgid "Form: %s" +msgstr "" + +#: admin/admin.php:4703 msgid "Site details" msgstr "" -#: admin/admin.php:4335 +#: admin/admin.php:4706 msgid "not active" msgstr "" -#: admin/admin.php:4338 +#: admin/admin.php:4709 msgid "none detected" msgstr "" +#: admin/admin.php:4710 +msgid "none recorded" +msgstr "" + #: inc/abilities/abilities-registrar.php:107 msgid "SureForms MCP Server" msgstr "" @@ -1188,7 +1287,7 @@ msgid "OttoKit is not configured properly." msgstr "" #: inc/admin-ajax.php:184 -#: inc/form-submit.php:1153 +#: inc/form-submit.php:1231 msgid "Invalid form ID." msgstr "" @@ -1323,10 +1422,10 @@ msgid "You are not allowed to convert HTML forms." msgstr "" #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1227 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -2042,11 +2141,11 @@ msgstr "" msgid "This form is no longer accepting submissions. The submission period has ended." msgstr "" -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "" -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -2059,129 +2158,129 @@ msgstr "" msgid "Security verification failed." msgstr "" -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "" -#: inc/form-submit.php:231 +#: inc/form-submit.php:281 msgid "Cloudflare Turnstile secret key is invalid." msgstr "" -#: inc/form-submit.php:238 +#: inc/form-submit.php:288 msgid "Cloudflare Turnstile response is missing." msgstr "" -#: inc/form-submit.php:282 +#: inc/form-submit.php:332 msgid "hCaptcha secret key is invalid." msgstr "" -#: inc/form-submit.php:289 +#: inc/form-submit.php:339 msgid "hCaptcha response is missing." msgstr "" -#: inc/form-submit.php:330 -#: inc/form-submit.php:589 +#: inc/form-submit.php:380 +#: inc/form-submit.php:639 msgid "Form data is not found." msgstr "" -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "" -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "" -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "" -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "" -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "" -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "" -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "" -#: inc/form-submit.php:990 +#: inc/form-submit.php:1043 msgid "Email notification passed to the sending server" msgstr "" #. translators: Here, %s is the comma separated emails list. -#: inc/form-submit.php:1063 +#: inc/form-submit.php:1120 msgid "Email notification recipient: %s" msgstr "" -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1127 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "" -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1128 msgid "Email sending failed for an unknown reason." msgstr "" #. translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). -#: inc/form-submit.php:1080 +#: inc/form-submit.php:1137 msgid "Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s" msgstr "" -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1191 msgid "No emails were sent." msgstr "" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1236 msgid "Invalid form." msgstr "" -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1241 msgid "Too many requests. Please try again shortly." msgstr "" -#: inc/form-submit.php:1288 +#: inc/form-submit.php:1366 msgid "Captcha validation failed. No error code provided." msgstr "" -#: inc/form-submit.php:1289 +#: inc/form-submit.php:1367 msgid "Captcha validation failed." msgstr "" -#: inc/form-submit.php:1344 +#: inc/form-submit.php:1422 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Google reCAPTCHA" msgstr "" -#: inc/form-submit.php:1348 +#: inc/form-submit.php:1426 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "hCaptcha" msgstr "" -#: inc/form-submit.php:1352 +#: inc/form-submit.php:1430 #: assets/build/settings.js:172 msgid "Cloudflare Turnstile" msgstr "" -#: inc/form-submit.php:1356 +#: inc/form-submit.php:1434 msgid "Unknown Captcha" msgstr "" -#: inc/form-submit.php:1357 +#: inc/form-submit.php:1435 msgid "Invalid captcha type." msgstr "" #. translators: %s is the captcha title. -#: inc/form-submit.php:1370 +#: inc/form-submit.php:1448 msgid "%s verification failed. Please contact your site administrator." msgstr "" @@ -2596,7 +2695,7 @@ msgstr "" msgid "You do not have permission to perform this action." msgstr "" -#: inc/helper.php:2375 +#: inc/helper.php:2368 msgid "Blank Form" msgstr "" @@ -16502,7 +16601,7 @@ msgid "This form could not be submitted. Please refresh the page and try again." msgstr "" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:932 msgid "An error occurred while submitting your form. Please try again." msgstr "" @@ -17270,11 +17369,11 @@ msgid "Form Views & Conversion" msgstr "" #: assets/build/settings.js:172 -msgid "Anonymous Analytics" +msgid "Logs" msgstr "" #: assets/build/settings.js:172 -msgid "Logs" +msgid "Anonymous Analytics" msgstr "" #: assets/build/settings.js:172 diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 65f02b0e6..53dadf701 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -6,9 +6,10 @@ import { CircleCheck, TriangleAlert, ChevronUp, ChevronDown } from 'lucide-react /** * Dashboard panel listing what SureForms has checked on this site. * - * Follows SureRank's Page Checks: one row per check, warnings first with a fix - * link and an Ignore control, passing checks below as a green tick. Row markup - * mirrors SureRank's CheckCard so the two plugins look like siblings. + * Follows SureRank's Page Checks: one row per problem, with a fix link and, for + * advisory rows, an Ignore control. Row markup mirrors SureRank's CheckCard so + * the two plugins look like siblings. Passing checks are not listed -- a panel + * confirming nothing is wrong is something people learn to skip. * * Rows are supplied fully formed by the server (see Admin::get_action_items()), * so adding a check needs no change here. @@ -85,24 +86,21 @@ export default () => { }, ] : [] ), - ...( item.cta_label + ...( item.cta_label && item.cta_url ? [ { name: item.cta_action, label: item.cta_label, url: item.cta_url, - // Contact Support is a Gmail compose URL, so it opens in a new - // tab like any other link. The mailto check stays because - // srfm_action_items is public and a third party can still - // contribute one, which must go to the mail client instead. + // A mailto: must open in the mail client, not a new tab. external: ! item.cta_url?.startsWith( 'mailto:' ), }, ] : [] ), ]; - // The support link carries the log in its body, so the click just opens a - // composed message -- no download to trigger, nothing to intercept. + // The mailto carries the log in its body, so the link just works -- no + // download to trigger, nothing to intercept. const handleFix = ( item, action ) => () => post( 'srfm_notice_response', srfm_admin?.notice_response_nonce, { notice_id: item.id, @@ -169,7 +167,7 @@ export default () => { variant="link" size="xs" onClick={ handleIgnore( item ) } - className="font-medium no-underline hover:underline focus:outline-none focus:[box-shadow:none] [&>span]:px-0 text-text-secondary shrink-0" + className="font-medium no-underline hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-1 focus:[box-shadow:none] [&>span]:px-0 text-text-secondary shrink-0" > { __( 'Ignore', 'sureforms' ) } @@ -180,7 +178,10 @@ export default () => { { actionsFor( item ).map( ( action, index ) => (

) } + { /* Read before send: the same text a support request needs, so it can + be pasted rather than described. force-ui's Dialog portals out of + the panel, which is what an overlay rendered in place could not do, + and it carries the focus trap, scroll lock and return-focus. */ } + + + { /* Wider than force-ui's default w-120: this holds a diagnostics block + and a fenced log, and 480px wraps almost every line of it. */ } + + +
+ + { DIALOG.title || __( 'Details', 'sureforms' ) } + + +
+ + { DIALOG.description || '' } + +
+ + { /* Selectable and scrollable: clipboard access can be + refused, and then selecting by hand is the only way + through. tabIndex because Chromium and WebKit do not + make a scroll container focusable on their own, so + without it a keyboard user cannot reach the very thing + the dialog exists to show. */ } +
+							{ details?.details || '' }
+						
+
+ + { /* Visible text, not a title attribute: a title never fires + on keyboard focus and is commonly dropped by screen + readers on an unavailable control, so the sentence + saying why the button is inert could not be read by + anyone. role="status" so the unlock is announced. */ } + +
+ + { /* Not rendered at all without a destination. An empty + href resolves to the current document, so the click + would open a duplicate of this page and still + acknowledge the failure -- standing the notice down + without anything having been reported. Reachable + through srfm_action_items for an item carrying + details but no support_url. + + Not an anchor while it is locked either: `disabled` + on an does nothing at all, so the href only + exists once the copy has been made. */ } + { !! details?.support_url && + ( copiedOnce ? ( + + ) : ( + + ) ) } +
+ + + ); }; diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 2a98b8f18..5275c9131 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1635,15 +1635,68 @@ public function test_handle_notice_response_accepts_every_pair_the_items_emit() * pointed at an inbox and has no destination left to change. */ public function test_get_support_contact_url_is_tagged_and_filterable() { + // An address of its own: make_user() does not set one, and an admin with no + // email is exactly the case the empty fallback exists for. + $admin = $this->make_user( 'administrator' ); + wp_update_user( + [ + 'ID' => $admin, + 'user_email' => 'owner@example.org', + ] + ); + wp_set_current_user( $admin ); + $method = new ReflectionMethod( Admin::class, 'get_support_contact_url' ); $method->setAccessible( true ); $url = $method->invoke( Admin::get_instance(), 'notification' ); - $this->assertStringStartsWith( 'https://sureforms.com/contact/', $url ); + $this->assertStringStartsWith( 'https://sureforms.com/form/troubleshooting-form/', $url ); $this->assertStringContainsString( 'utm_content=notification', $url ); $this->assertStringContainsString( 'utm_campaign=contact_support', $url ); + parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + // Prefilled, so the person reporting a fault does not retype what SureForms + // already knows. + $this->assertSame( 'Email notification failure', $query['subject'] ?? '' ); + $this->assertSame( + Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ), + $query['site_url'] ?? '' + ); + $this->assertSame( 'owner@example.org', $query['mail'] ?? '', 'The admin address prefills the form.' ); + + // An admin with no usable address still gets a working link, with the field + // left empty rather than carrying a broken value into the form. + $no_email = $this->make_user( 'administrator' ); + wp_set_current_user( $no_email ); + + parse_str( + (string) wp_parse_url( $method->invoke( Admin::get_instance(), 'notification' ), PHP_URL_QUERY ), + $without + ); + + $this->assertSame( '', $without['mail'] ?? 'missing' ); + $this->assertSame( 'Email notification failure', $without['subject'] ?? '' ); + + wp_set_current_user( $admin ); + + // One subject per category, matched against the form's own options -- so + // these are machine values and must not be translated. + $subject = static function ( $category ) use ( $method ) { + parse_str( + (string) wp_parse_url( $method->invoke( Admin::get_instance(), $category ), PHP_URL_QUERY ), + $parsed + ); + return $parsed['subject'] ?? ''; + }; + + $this->assertSame( 'Form submission failure', $subject( 'submission' ) ); + $this->assertSame( 'Integration failure', $subject( 'integration' ) ); + // srfm_action_items is public, so an item can carry any category or none. + $this->assertSame( 'Other', $subject( 'something-else' ) ); + $this->assertSame( 'Other', $subject( '' ) ); + // Different check, different tag -- otherwise the parameter says nothing. $this->assertStringContainsString( 'utm_content=integration', From 5508eda6c896c8ad192c780632eea94823d5b1f9 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 10 Sep 2026 14:19:24 +0530 Subject: [PATCH 38/58] fix: address round-2 review on the details dialog force-ui stays on the dashboard, so this closes its gaps rather than swapping implementations again. support_url is added to the URL normalisation loop. It is the key this PR introduced and the one React assigns straight to href, so it was the single key missing from the loop that exists precisely because react-dom 18 leaves a javascript: URL intact with its warning compiled out of production. Reachable only through srfm_action_items from site-side PHP -- a pro integration or a third-party advisory whose URL comes from a remote feed -- but on the dashboard that is script executing in the wp-admin origin as manage_options. The same loop now skips a non-array item, which isset() would have waved past both the URL check and the sanitize_key() below it. The dialog is portalled. force-ui's Dialog.Panel renders FloatingOverlay in place -- FloatingPortal is used by Dialog.Portal alone, which Panel does not use -- so the overlay was still a descendant of the Form Checks card inside the dashboard's Container nesting, which is the position that caused #3121. It worked only because nothing above it happened to establish a containing block. It now goes through Dialog.Portal into #srfm-dialog-root rather than document.body, because tailwind.config.js scopes every utility behind an :is() allowlist that includes that id and body would render it unstyled. The stylesheet is registered on admin_enqueue_scripts. admin_notices fires after admin_print_styles has flushed the head, so enqueuing from the renderer reached the page only through core's late-styles pass in the footer -- and until that parsed, every stacked notice rendered expanded before collapsing to one, the carousel controls overlapped the notice text, and the defensive display:none on the hidden payload was inert, which is the exact window that rule exists for. Four divergences from the classic dialog closed: exitOnClickOutside passed, since force-ui defaults it off and a backdrop click did nothing; aria-labelledby and aria-describedby wired against ids on Title and Description, because Dialog.Panel forwards only className and the dialog had no accessible name at all; initial focus moved to Copy details rather than the close cross; and CloseButton's hardcoded, untranslated aria-label overridden. A synchronous clipboard throw no longer strands the classic notice. ClipboardItem's constructor and clipboard.write() both throw outside the promise, and with neither callback run the caller never unlocked -- leaving a non-dismissible notice with no working action, which is round-1 item 1 narrowed rather than closed. On a plain-HTTP dashboard Contact Support is pre-unlocked and the hint says why nothing can be copied, matching the classic surface. It was a real disabled button with its explanation blanked, recoverable only by clicking a Copy that cannot work. The dead window.srfmOpenDetails export is gone -- zero callers, and unreachable by design since the file is not loaded on the one screen it was written for -- along with the three comments asserting a single implementation that no longer exists. The comments now say what is true: two renderers by necessity, one source of strings. Also: the copy-label timer is held in a ref and cleared on unmount; the hint is a p rather than force-ui's default label and stays mounted, because Label returns null on falsy children and a live region inserted with its text already set is not announced; an empty aria-label no longer names the pre region; DIALOG and canCopy moved inside the component, so a missing localize object cannot take the whole dashboard down at module eval; setOpen honours its boolean; and the trigger got the key fallback and guarded onClick its sibling already had. Test fixes: the guide-order test resets the memo and now asserts the item count and the absence of the guide, so it fails on the stale two-item list instead of passing vacuously; the line-breaks test captures the payload div with a regex rather than measuring to end-of-output, where the template's own newlines were carrying it; and the trim-direction assertion drops a trailing space that made it unfalsifiable. Deferred, with reasons: the POT is left alone on Vansh's instruction; findings 7, 9 and 10 need decisions rather than edits and are answered on the PR. Co-Authored-By: Claude Opus 5 (1M context) --- admin/admin.php | 148 +++++++++----- admin/assets/js/notice-response.js | 64 ++++-- src/admin/dashboard/ActionItems.js | 313 ++++++++++++++++++++--------- tests/unit/admin/test-admin.php | 34 +++- 4 files changed, 392 insertions(+), 167 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 673aae3d8..17fcacefc 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -204,6 +204,9 @@ public function __construct() { // in place before admin_enqueue_scripts localizes it. add_action( 'admin_init', [ $this, 'register_database_repair_notice' ], 5 ); add_action( 'admin_notices', [ $this, 'render_action_item_notices' ] ); + // Late priority so the items are built after anything hooking + // srfm_action_items has had a chance to register. + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_action_item_styles' ], 20 ); add_action( 'admin_notices', [ $this, 'render_database_repair_notice' ] ); add_action( 'admin_post_srfm_repair_entries_table', [ $this, 'handle_database_repair' ] ); // Display notices on traditional WordPress admin pages. @@ -3491,7 +3494,6 @@ public function render_action_item_notices() { } $this->enqueue_notice_response_script(); - $this->enqueue_action_item_styles(); foreach ( $items as $item ) { $status = Helper::get_string_value( $item['status'] ?? '' ); @@ -3713,7 +3715,14 @@ public function get_action_items() { // entity would be sent to the server verbatim. Decoded to one raw form here, // and each renderer escapes it for its own context. foreach ( $items as $index => $item ) { - foreach ( [ 'cta_url', 'guide_url' ] as $key ) { + // A filter may hand back an object. isset() on it returns false, which + // would slip the item past both the URL normalisation and the + // sanitize_key() below without any sign that it had. + if ( ! is_array( $item ) ) { + continue; + } + + foreach ( [ 'cta_url', 'guide_url', 'support_url' ] as $key ) { if ( ! isset( $item[ $key ] ) ) { continue; } @@ -3771,43 +3780,6 @@ public function handle_dismiss_action_item() { wp_send_json_success(); } - /** - * The details dialog's strings. - * - * One array, two consumers: the classic wp-admin dialog in - * notice-response.js, and the dashboard's force-ui one. Declared here rather - * than inline in each, because the same sentence written as `__()` in PHP and - * again in JSX looks identical to translators until the first edit to either, - * after which one surface silently reverts to English. - * - * @since x.x.x - * @return array - */ - private function get_details_dialog_labels() { - return [ - 'title' => __( 'Details', 'sureforms' ), - 'description' => __( 'What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it.', 'sureforms' ), - 'copy' => __( 'Copy details', 'sureforms' ), - 'copied' => __( 'Copied', 'sureforms' ), - 'contact' => __( 'Contact Support', 'sureforms' ), - 'close' => __( 'Close', 'sureforms' ), - // Shown beside the buttons rather than as a title attribute: - // pointer-events:none suppresses the native tooltip, a title - // never fires on keyboard focus, and screen readers commonly - // drop it on an unavailable control -- so the sentence saying - // why the button is inert could not be read by anyone. - 'copyFirst' => __( 'Copy the details first, so you have them to paste.', 'sureforms' ), - // The unlock changes the label, the icon and whether Contact - // Support works, none of which was announced. This goes in a - // role="status" node so it is. - 'unlocked' => __( 'Copied. Contact Support is now available.', 'sureforms' ), - 'copyFailed' => __( 'Your browser would not let us copy. Select the text above and copy it by hand.', 'sureforms' ), - // The scrollable diagnostics block is focusable, so it needs a name of - // its own. - 'logRegion' => __( 'Recorded diagnostics', 'sureforms' ), - ]; - } - /** * The stylesheet for the notice carousel and the details dialog. * @@ -3815,10 +3787,20 @@ private function get_details_dialog_labels() { * notice-response.js, so the rules use logical properties, an RTL sheet can * override them, and a site can restyle the dialog without patching a script. * + * Only the classic wp-admin surface needs these. The SureForms dashboard's + * dialog is force-ui's, styled by the Tailwind build, so nothing here reaches + * it -- the two surfaces share their strings, not their markup. + * * Attached to a registered handle with no file of its own, which is the WP way - * to ship CSS tied to one script, and it means both surfaces get the same rules - * from one place: the classic notices, and the SureForms dashboard, where the - * React panel calls the same dialog. + * to ship CSS tied to one script. + * + * Hooked to admin_enqueue_scripts rather than called from the renderer. + * admin_notices fires from admin-header.php after admin_print_styles has + * flushed the head, so enqueuing there reached the page only through core's + * late-styles pass in the footer -- and until that parsed, every stacked notice + * rendered expanded before collapsing to one, the carousel controls overlapped + * the notice text, and the defensive `display: none` on the hidden payload was + * inert, which is the exact window that rule exists for. * * The buttons are painted explicitly. They carry core's `button` classes for * their shape and focus behaviour, and core paints those with @@ -3830,11 +3812,32 @@ private function get_details_dialog_labels() { * @since x.x.x * @return void */ - private function enqueue_action_item_styles() { + public function enqueue_action_item_styles() { if ( wp_style_is( 'srfm-action-items', 'enqueued' ) ) { return; } + if ( ! Helper::current_user_can() ) { + return; + } + + // Nothing to style unless a notice is actually going to render. Cheap to + // ask: get_action_items() is memoised for the request. + $has_notice = false; + + foreach ( $this->get_action_items() as $item ) { + $status = Helper::get_string_value( is_array( $item ) ? $item['status'] ?? '' : '' ); + + if ( 'success' !== $status && '' !== $status ) { + $has_notice = true; + break; + } + } + + if ( ! $has_notice ) { + return; + } + wp_register_style( 'srfm-action-items', false, [], SRFM_VER ); wp_enqueue_style( 'srfm-action-items' ); @@ -3958,6 +3961,43 @@ private function enqueue_action_item_styles() { wp_add_inline_style( 'srfm-action-items', $css ); } + /** + * The details dialog's strings. + * + * One array, two consumers: the classic wp-admin dialog in + * notice-response.js, and the dashboard's force-ui one. Declared here rather + * than inline in each, because the same sentence written as `__()` in PHP and + * again in JSX looks identical to translators until the first edit to either, + * after which one surface silently reverts to English. + * + * @since x.x.x + * @return array + */ + private function get_details_dialog_labels() { + return [ + 'title' => __( 'Details', 'sureforms' ), + 'description' => __( 'What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it.', 'sureforms' ), + 'copy' => __( 'Copy details', 'sureforms' ), + 'copied' => __( 'Copied', 'sureforms' ), + 'contact' => __( 'Contact Support', 'sureforms' ), + 'close' => __( 'Close', 'sureforms' ), + // Shown beside the buttons rather than as a title attribute: + // pointer-events:none suppresses the native tooltip, a title + // never fires on keyboard focus, and screen readers commonly + // drop it on an unavailable control -- so the sentence saying + // why the button is inert could not be read by anyone. + 'copyFirst' => __( 'Copy the details first, so you have them to paste.', 'sureforms' ), + // The unlock changes the label, the icon and whether Contact + // Support works, none of which was announced. This goes in a + // role="status" node so it is. + 'unlocked' => __( 'Copied. Contact Support is now available.', 'sureforms' ), + 'copyFailed' => __( 'Your browser would not let us copy. Select the text above and copy it by hand.', 'sureforms' ), + // The scrollable diagnostics block is focusable, so it needs a name of + // its own. + 'logRegion' => __( 'Recorded diagnostics', 'sureforms' ), + ]; + } + /** * SureForms' own action items, before the filter. * @@ -4987,7 +5027,7 @@ private function get_support_count_sentence( $category, $count ) { } /** - * Diagnostics block for the support email. + * Diagnostics block for the support report. * * Carries what support would otherwise have to ask for, so the first reply can * be an answer rather than a questionnaire. @@ -5049,12 +5089,20 @@ private function get_support_message( $category = '', $form_title = '' ) { ] ); - // Only when there is one. A repeat report is worth knowing about at the top - // of a ticket: the same category having been reported before means the last - // answer did not hold, which is a different conversation from a first - // report. Stored as time() -- a UTC epoch, comparable with the sibling - // 'at' -- and formatted here with wp_date() so it reads in the site's - // timezone rather than the server's. + // Only when there is one. A repeat report is worth knowing about: the same + // category having been reported before means the last answer did not hold, + // which is a different conversation from a first report. Appended with the + // rest of the site details rather than raised to the top, because it is + // context for them rather than a headline. + // + // Survives only until the next success in that category, because + // clear_category() unsets the whole record -- so in practice it is + // reachable for 'integration', which has no success signal, and transient + // for the other two. + // + // Stored as time(), a UTC epoch comparable with the sibling 'at', and + // formatted here with wp_date() so it reads in the site's timezone rather + // than the server's. $acked_at = Helper::get_integer_value( $failures[ $category ]['acked_at'] ?? 0 ); if ( $acked_at > 0 ) { diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js index a8c4aa7aa..bab903416 100644 --- a/admin/assets/js/notice-response.js +++ b/admin/assets/js/notice-response.js @@ -308,19 +308,35 @@ navigator.clipboard.write; if ( supportsRich ) { - const item = new window.ClipboardItem( { - 'text/plain': new Blob( [ text ], { type: 'text/plain' } ), - 'text/html': new Blob( [ detailsAsHtml( text ) ], { - type: 'text/html', - } ), - } ); + // Both of these throw synchronously, not through the promise: + // ClipboardItem's constructor on an unsupported MIME type, and + // clipboard.write() on a bad argument in Chromium or a stale user + // gesture in WebKit. Outside a try, the exception leaves copyDetails + // and the click handler with neither callback run -- so the caller + // never unlocks and the notice keeps no working action. + try { + const item = new window.ClipboardItem( { + 'text/plain': new Blob( [ text ], { type: 'text/plain' } ), + 'text/html': new Blob( [ detailsAsHtml( text ) ], { + type: 'text/html', + } ), + } ); + + navigator.clipboard.write( [ item ] ).then( done, nope ); + } catch ( e ) { + nope(); + } - navigator.clipboard.write( [ item ] ).then( done, nope ); return; } if ( navigator.clipboard && navigator.clipboard.writeText ) { - navigator.clipboard.writeText( text ).then( done, nope ); + try { + navigator.clipboard.writeText( text ).then( done, nope ); + } catch ( e ) { + nope(); + } + return; } @@ -333,15 +349,20 @@ /** * Show what would be sent to support, before anything is sent. * - * The one implementation of this dialog. It appends to document.body, so it is - * not subject to a containing block established by an ancestor transform or - * filter, and it is framework-free, so the React dashboard panel can call it - * through window.srfmOpenDetails rather than carrying a second copy. A second - * copy is how two surfaces end up with different keyboard behaviour, different - * contrast and two sets of the same strings. + * The dialog for the classic wp-admin notices. The SureForms dashboard has its + * own, built on force-ui's Dialog, because there is no React on these screens + * and no force-ui bundle either -- so the two surfaces are separate + * implementations by necessity. They read their strings from one PHP array + * (Admin::get_details_dialog_labels(), reaching here as + * srfmNoticeResponse.details and the dashboard as srfm_admin.details_dialog), + * so the copy cannot drift even though the markup does. + * + * This one appends to document.body, so it is not subject to a containing + * block established by an ancestor transform or filter. * - * Every string comes from srfmNoticeResponse.details, so the labels are - * translated in PHP once and neither caller carries user-facing English. + * The fallbacks below are English, because a plain admin script has no + * gettext runtime to fall back to; they are reached only if the localize data + * is missing entirely. * * Read with textContent and written with textContent, never innerHTML: the log * contains whatever a server or a browser put in an error message, and that is @@ -657,12 +678,13 @@ } ); } - // The dashboard panel is React and this file is not, so it calls in here rather - // than carrying its own copy of the dialog. One implementation, one set of - // strings, one keyboard behaviour. - window.srfmOpenDetails = showDetails; - document.addEventListener( 'click', function ( e ) { + // A click whose target is not an Element -- text nodes, some synthetic + // events -- has no closest() and would throw out of the handler. + if ( ! e.target || typeof e.target.closest !== 'function' ) { + return; + } + const trigger = e.target.closest( '[data-srfm-details-for]' ); if ( ! trigger ) { diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index e0b33645b..5fc4de31f 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -1,5 +1,11 @@ import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; +import { + useState, + useRef, + useMemo, + useEffect, + useLayoutEffect, +} from '@wordpress/element'; import { Button, Dialog, Label } from '@bsf/force-ui'; import { CircleCheck, @@ -10,20 +16,6 @@ import { Copy, } from 'lucide-react'; -// Translated in PHP and handed to both surfaces, so the same sentence is not -// written as __() here and again in admin/assets/js/notice-response.js -- which -// looks identical to translators until the first edit to either, after which one -// surface silently reverts to English. -const DIALOG = srfm_admin?.details_dialog || {}; - -// Whether copying is possible at all. On a plain-HTTP admin navigator.clipboard -// does not exist, so the copy step cannot be a precondition for anything -- see -// the unlock reasoning on handleCopy. -const canCopy = !! ( - navigator.clipboard && - ( navigator.clipboard.write || navigator.clipboard.writeText ) -); - /** * Dashboard panel listing what SureForms has checked on this site. * @@ -51,6 +43,27 @@ const ICONS = { }; export default () => { + // Read here rather than at module scope. Optional chaining does not protect + // against an undeclared identifier, so a bundle enqueued without its localize + // data would throw at module eval and take the whole dashboard down instead of + // just this panel. + const dialogLabels = useMemo( + () => ( typeof srfm_admin === 'undefined' ? {} : srfm_admin?.details_dialog || {} ), + [] + ); + + // Whether copying is possible at all. On a plain-HTTP admin + // navigator.clipboard does not exist, so the copy step cannot be a + // precondition for anything -- see the unlock reasoning in handleCopy. + const canCopy = useMemo( + () => + !! ( + navigator.clipboard && + ( navigator.clipboard.write || navigator.clipboard.writeText ) + ), + [] + ); + const [ dismissed, setDismissed ] = useState( [] ); const [ open, setOpen ] = useState( true ); // The item whose details are being read, or null. @@ -65,6 +78,72 @@ export default () => { // changes the label, the icon and whether Contact Support works at once. const [ hint, setHint ] = useState( '' ); + // force-ui's Dialog.Panel renders FloatingOverlay in place in the React tree + // -- FloatingPortal is used only by Dialog.Portal, which Panel does not use. + // Left there, the overlay is a descendant of the Form Checks card inside the + // dashboard's Container nesting, and position:fixed resolves against the + // nearest ancestor establishing a containing block, which is what kept an + // overlay rendered in here from ever appearing. Portalling to + // #srfm-dialog-root rather than document.body matters: tailwind.config.js + // scopes every utility behind an :is() allowlist that includes that id, so + // body would render the dialog with no styling at all. + const [ dialogRoot, setDialogRoot ] = useState( null ); + + useLayoutEffect( () => { + let root = document.getElementById( 'srfm-dialog-root' ); + + if ( ! root ) { + root = document.createElement( 'div' ); + root.id = 'srfm-dialog-root'; + document.body.appendChild( root ); + } + + setDialogRoot( root ); + }, [] ); + + // Dialog.Panel forwards only className, so aria-labelledby cannot be passed to + // the node it puts role="dialog" on. Wired here instead, against the ids set + // on Title and Description below -- without it the dialog has no accessible + // name at all. + const titleId = 'srfm-details-title'; + const descriptionId = 'srfm-details-description'; + + useEffect( () => { + if ( ! details || ! dialogRoot ) { + return; + } + + const node = dialogRoot.querySelector( '[role="dialog"]' ); + + if ( ! node ) { + return; + } + + node.setAttribute( 'aria-labelledby', titleId ); + node.setAttribute( 'aria-describedby', descriptionId ); + }, [ details, dialogRoot ] ); + + // Initial focus on Copy details rather than whatever FloatingFocusManager + // reaches first, which is the close cross. + const copyRef = useRef( null ); + + useEffect( () => { + if ( ! details ) { + return; + } + + const timer = window.setTimeout( () => copyRef.current?.focus(), 50 ); + + return () => window.clearTimeout( timer ); + }, [ details ] ); + + // Low: an uncleared label timer crosses itself on two copies inside 2s, and + // leaves a setState pending on a possibly-unmounted tree when the dialog is + // closed inside that window. + const revertRef = useRef( 0 ); + + useEffect( () => () => window.clearTimeout( revertRef.current ), [] ); + const items = ( srfm_admin?.action_items || [] ).filter( ( item ) => ! dismissed.includes( item.id ) ); @@ -174,10 +253,11 @@ export default () => { setCopied( true ); setCopiedOnce( true ); - setHint( DIALOG.unlocked || '' ); + setHint( dialogLabels.unlocked || '' ); // Reverts on its own: a button stuck on "Copied" says nothing about the // next click. - setTimeout( () => setCopied( false ), 2000 ); + window.clearTimeout( revertRef.current ); + revertRef.current = window.setTimeout( () => setCopied( false ), 2000 ); // Reported inside the try, so the event means a copy happened rather // than a copy was attempted. The classic notice records it the same way. @@ -194,7 +274,7 @@ export default () => { // working action. The text is on screen and selectable either way. setCopied( false ); setCopiedOnce( true ); - setHint( DIALOG.copyFailed || '' ); + setHint( dialogLabels.copyFailed || '' ); } }; @@ -298,23 +378,40 @@ export default () => { // strings from the same PHP array. action.dialog ? ( - { /* Not rendered at all without a destination. An empty + + { /* Not rendered at all without a destination. An empty href resolves to the current document, so the click would open a duplicate of this page and still acknowledge the failure -- standing the notice down @@ -459,7 +586,7 @@ export default () => { Not an anchor while it is locked either: `disabled` on an does nothing at all, so the href only exists once the copy has been made. */ } - { !! details?.support_url && + { !! details?.support_url && ( copiedOnce ? ( ) : ( ) ) } - - - + + + + + ) } ); diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 5275c9131..f4d5fd393 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1345,10 +1345,16 @@ public function test_render_action_item_notices_keeps_the_details_line_breaks() $pagenow = $previous_pagenow; - $start = strpos( $html, 'srfm-notice-details' ); - $this->assertNotFalse( $start, 'The hidden payload must render.' ); + // The div's own contents, not the rest of the output: the template emits + // newlines of its own around it, so measuring from here to end-of-output + // passed even when the payload itself was a single line. + $this->assertSame( + 1, + preg_match( '#]*class="srfm-notice-details"[^>]*>(.*?)#s', $html, $matches ), + 'The hidden payload must render.' + ); - $payload = substr( $html, $start ); + $payload = $matches[1]; $this->assertStringNotContainsString( "\r", $payload, 'Bare LF only.' ); $this->assertGreaterThan( @@ -1395,8 +1401,11 @@ public function test_get_support_log_block_keeps_the_newest_entries() { $block, 'The newest entry must survive the trim.' ); + // No trailing space: the fixture is '…fill the log ' . $i inside a JSON + // line, so the next character is always a quote and 'fill the log 0 ' + // could never appear -- the assertion passed whichever end was trimmed. $this->assertStringNotContainsString( - 'fill the log 0 ', + 'fill the log 0"', $block, 'The oldest entries are what the bound drops.' ); @@ -1468,6 +1477,10 @@ public function test_render_action_item_notices_puts_the_guide_before_the_detail // An item with no guide keeps Contact Support primary. delete_option( Client_Logger::FAILURES_OPTION ); Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + // Without this the render above has already populated the memo, and the + // assertion below is satisfied by the stale list rather than by the + // fixture it just set up. + Admin::reset_action_items_cache(); $pagenow = 'plugins.php'; ob_start(); @@ -1480,6 +1493,19 @@ public function test_render_action_item_notices_puts_the_guide_before_the_detail $alone, 'With no guide, View details leads and stays primary.' ); + + // Proof this rendered the fixture rather than the memo from the render + // above: that list held two items, one of them the notification guide. + $this->assertSame( + 1, + substr_count( $alone, 'srfm-action-item-notice' ), + 'Only the submission failure is recorded, so only it may render.' + ); + $this->assertStringNotContainsString( + 'help_me_fix', + $alone, + 'A stale list would still carry the notification guide.' + ); } /** From 3705ae50953f77a783ea7cb69cefa0d5cb380499 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 10 Sep 2026 14:26:40 +0530 Subject: [PATCH 39/58] test: cover enqueue_action_item_styles() Asserts the hook as well as the output. Once a page has finished loading the CSS looks identical whether it arrived in the head or through core's late-styles pass in the footer, which is how the wrong hook shipped -- so the hook itself is what this pins, along with the display:none rule whose absence during that window was the bug, the logical properties, and the brand colour rather than the admin scheme. Fails when the hook moves back to admin_notices. Co-Authored-By: Claude Opus 5 (1M context) --- tests/unit/admin/test-admin.php | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index f4d5fd393..e4adde127 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1548,6 +1548,70 @@ public function test_reset_action_items_cache() { Admin::reset_action_items_cache(); } + /** + * The carousel and dialog CSS reaches the head, and only when it is needed. + * + * The rules have to be in the head, not the footer. admin_notices fires from + * admin-header.php after admin_print_styles has flushed, so enqueuing from the + * renderer arrived only through core's late-styles pass -- and until that + * parsed, every stacked notice rendered expanded before collapsing to one and + * the defensive display:none on the hidden diagnostics was inert, which is the + * window that rule exists for. Asserted on the hook as well as the output, + * because the output looks identical either way once the page has finished + * loading, which is how this got shipped. + */ + public function test_enqueue_action_item_styles() { + $this->assertNotFalse( + has_action( 'admin_enqueue_scripts', [ Admin::get_instance(), 'enqueue_action_item_styles' ] ), + 'Registered on admin_enqueue_scripts, or the rules cannot reach the head.' + ); + $this->assertFalse( + has_action( 'admin_notices', [ Admin::get_instance(), 'enqueue_action_item_styles' ] ), + 'Not on admin_notices: by then the head has already been printed.' + ); + + wp_set_current_user( $this->make_user( 'administrator' ) ); + wp_dequeue_style( 'srfm-action-items' ); + wp_deregister_style( 'srfm-action-items' ); + + // Nothing wrong: nothing to style. + delete_option( Client_Logger::FAILURES_OPTION ); + Helper::update_srfm_option( 'dismissed_action_items', [ 'caching_plugin' ] ); + Admin::reset_action_items_cache(); + + Admin::get_instance()->enqueue_action_item_styles(); + + $this->assertFalse( + wp_style_is( 'srfm-action-items', 'enqueued' ), + 'A healthy site ships none of this.' + ); + + // A fault to report, so the notices render and need their rules. + Client_Logger::record_failure( 'submission', 42, 'Contact Form' ); + Admin::reset_action_items_cache(); + + Admin::get_instance()->enqueue_action_item_styles(); + + $this->assertTrue( wp_style_is( 'srfm-action-items', 'enqueued' ) ); + + $css = implode( "\n", (array) wp_styles()->get_data( 'srfm-action-items', 'after' ) ); + + // The rule whose absence during the head-to-footer window was the bug. + $this->assertStringContainsString( '.srfm-notice-details { display: none; }', $css ); + $this->assertStringContainsString( 'srfm-action-item-notice[hidden]', $css ); + // Logical properties, so an RTL sheet can override rather than fight it. + $this->assertStringContainsString( 'inset-inline-end', $css ); + $this->assertStringContainsString( 'padding-inline-end', $css ); + // Brand, not the admin colour scheme. + $this->assertStringContainsString( '#D54407', $css ); + + wp_dequeue_style( 'srfm-action-items' ); + wp_deregister_style( 'srfm-action-items' ); + delete_option( Client_Logger::FAILURES_OPTION ); + Helper::update_srfm_option( 'dismissed_action_items', [] ); + Admin::reset_action_items_cache(); + } + /** * Every (notice, button) pair either surface can emit is on the allowlist. * From b58b11bcbe3cb6b837941f4a1df0414fce44db8d Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 10 Sep 2026 15:15:26 +0530 Subject: [PATCH 40/58] fix: address round-2 review on the editor exclusion The settings copy shipped broken and that was the PR's whole visible deliverable. force-ui's Label carries `flex items-center gap-0.5` in its base classes, and the className Switch passes it has no display utility for twMerge to override that with -- so the two sibling spans became flex items side by side, two squeezed text columns two pixels apart, and `mt-1` only nudged one down under align-items: center. The existing JSX descriptions in this codebase never showed it because they pass a single child. Now one wrapper span is the flex item and its children stack as ordinary blocks. Verified in the browser: both at left 775, 612px each, the second at top 519 below the first's 415+100. The submitter lookup gets an index that leads on the columns it filters, `idx_user_id_created_at (user_id, created_at)`, through the same get_new_columns_definition() path idx_form_id_created_at_status used. idx_user_id could not serve it -- created_at is not in it and no other index leads on created_at -- so this was a range scan with a row read per row plus a temp table for the DISTINCT, reading every logged-in-submitted entry ever recorded to return a short list. The docblock said the index covered it, which was true of the result set and false of the scanned set. A non-array IN/NOT IN value is now reported rather than swallowed. Folded in with the empty-array case, 'NOT IN' with 5 -- a plausible typo for [ 5 ] -- dropped the condition and excluded nobody, silently, while the same typo on 'IN' failed closed. Round 1 asked for this and I skipped it citing no precedent in the repo; the asymmetry on a data-scoping primitive is the better argument. The empty-array case stays silent, because an empty set is a legitimate input. Operators are normalised before the allowlist test. Payments' builder upper-cases and trims while this one compared strictly, so 'not in' was honoured by one and silently dropped by the other -- and now that NOT IN is the exclusion primitive, a silent drop is a silently disabled exclusion. Payments' builder also reports a scalar rather than emitting `col IN %s`, which is a syntax error that fails the whole query. The ceiling's shape is documented rather than left to be discovered: it counts distinct submitters since tracking was first enabled and never resets, so a membership site reaches 500 in ordinary operation and stays past it, with the rate quietly counting editor submissions again. The settings copy now says those are "normally" left out rather than promising it outright. Trashed entries consume ceiling budget too, which is noted. Also: the payments comment named the wrong consumer -- that builder serves the admin listing, and the shortcode's OR group is compiled by Base, which is where the fail-open would actually have leaked; the user_id NOT NULL DEFAULT 0 invariant is recorded beside the column, because making it nullable would drop every anonymous entry from the numerator and report ~0% everywhere; LIMIT goes through sprintf like every sibling; tearDown() chains to parent; and the @since tags are ordered. Tests for both new guards, both revert-tested: the scalar case asserts the doing_it_wrong_run notice and that no condition was built from the bad input, and the case test asserts ' not in ' is honoured while an unknown operator is still dropped. Co-Authored-By: Claude Opus 5 (1M context) --- inc/database/base.php | 40 +++++++++--- inc/database/tables/entries.php | 13 ++++ inc/database/tables/payments.php | 35 ++++++++--- inc/forms-data.php | 23 ++++++- src/admin/settings/pages/General.js | 12 +++- tests/unit/inc/database/test-base.php | 88 +++++++++++++++++++++++++++ tests/unit/inc/test-forms-data.php | 2 + 7 files changed, 192 insertions(+), 21 deletions(-) diff --git a/inc/database/base.php b/inc/database/base.php index 7ab63806d..922ac33db 100644 --- a/inc/database/base.php +++ b/inc/database/base.php @@ -1056,8 +1056,8 @@ protected function cache_reset() { * } * } * - * @since 1.1.1 -- Added support for "IN" compare. * @since x.x.x -- Added support for "NOT IN" compare. + * @since 1.1.1 -- Added support for "IN" compare. * @since 0.0.13 * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided. */ @@ -1083,8 +1083,17 @@ protected function prepare_where_clauses( $where_clauses = [] ) { $clause_parts = []; foreach ( $value as $_key => $_value ) { if ( is_int( $_key ) ) { + // Normalised before the allowlist test. Payments' + // builder upper-cases and trims, this one compared + // strictly -- so a caller writing 'not in' was honoured + // by one and silently dropped by the other. A dropped + // condition used to be harmless; now that NOT IN is the + // exclusion primitive, dropping it disables the + // exclusion without a word. + $compare = strtoupper( trim( Helper::get_string_value( $_value['compare'] ) ) ); + // Check if the operator is allowed. - if ( ! in_array( $_value['compare'], $this->allowed_where_operators, true ) ) { + if ( ! in_array( $compare, $this->allowed_where_operators, true ) ) { continue; } @@ -1093,19 +1102,34 @@ protected function prepare_where_clauses( $where_clauses = [] ) { continue; } - switch ( $_value['compare'] ) { + switch ( $compare ) { case 'LIKE': // Single quotes to match WP core. Under a MySQL session with // ANSI_QUOTES set (not in WP's incompatible_modes list, which // only names the compound ANSI mode) a double-quoted pattern // parses as an identifier and the query hard-fails, taking out // both the listing and its COUNT(*). - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . " '%%" . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . "%%'"; + $clause_parts[] = $_value['key'] . ' ' . $compare . " '%%" . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . "%%'"; $values[] = $_value['value']; break; case 'IN': case 'NOT IN': + // A scalar is a caller bug, not an empty set, and it must + // surface. 'NOT IN' with value 5 -- a plausible typo for + // [ 5 ] -- would otherwise drop the condition and exclude + // nobody, with no error and a green test suite, while the + // same typo on 'IN' fails closed. On a primitive whose only + // job is scoping data, that asymmetry is a hazard. + if ( ! is_array( $_value['value'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html( "{$compare} requires an array value, received " . gettype( $_value['value'] ) . '.' ), + 'x.x.x' + ); + break; + } + // An empty list cannot be interpolated: "col IN ()" is a syntax // error that fails the whole query, listing and COUNT alike. // An empty IN matches nothing, so '1 = 0' says that in any @@ -1113,8 +1137,8 @@ protected function prepare_where_clauses( $where_clauses = [] ) { // would be '1 = 1', and that makes an enclosing OR group // unconditionally true. Dropping the condition means the same // thing under AND and stays fail-closed under OR. - if ( ! is_array( $_value['value'] ) || [] === $_value['value'] ) { - if ( 'IN' === $_value['compare'] ) { + if ( [] === $_value['value'] ) { + if ( 'IN' === $compare ) { $clause_parts[] = '1 = 0'; } break; @@ -1122,12 +1146,12 @@ protected function prepare_where_clauses( $where_clauses = [] ) { // Based on the number of values and datatype, it will create WHERE clause for $wpdb::prepare method. Eg: for ID with three values column: ID IN (%d, %d, %d). $datatype = $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ')'; + $clause_parts[] = $_value['key'] . ' ' . $compare . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ')'; $values = array_merge( $values, $_value['value'] ); break; default: - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); + $clause_parts[] = $_value['key'] . ' ' . $compare . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); $values[] = $_value['value']; break; } diff --git a/inc/database/tables/entries.php b/inc/database/tables/entries.php index 49d62a79e..0622b4cfc 100644 --- a/inc/database/tables/entries.php +++ b/inc/database/tables/entries.php @@ -120,6 +120,11 @@ public function get_columns_definition() { return [ 'ID BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY', 'form_id BIGINT(20) UNSIGNED', + // NOT NULL DEFAULT 0 is load-bearing, not tidiness. An anonymous + // submission stores 0, and Forms_Data::calculate_form_metrics() excludes + // site editors with `user_id NOT IN (…)`. NOT IN never matches NULL, so + // making this column nullable would drop every anonymous entry from the + // numerator and report a conversion rate near 0% on every form. 'user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0', 'form_data LONGTEXT', // Note: @since 0.0.13 -- We have renamed `user_data` column to `form_data`. 'logs LONGTEXT', @@ -133,6 +138,12 @@ public function get_columns_definition() { 'INDEX idx_form_id (form_id)', // Indexing for the performance improvements. 'INDEX idx_user_id (user_id)', 'INDEX idx_form_id_created_at_status (form_id, created_at, status)', // Composite index for performance improvements. + // Leads on the two columns Forms_Data::get_editing_submitter_ids() + // filters. idx_user_id alone cannot serve it -- created_at is not in it, + // and no other index leads on created_at -- so that lookup was a range + // scan with a row read per row plus a temp table for DISTINCT, on every + // Forms-list render. + 'INDEX idx_user_id_created_at (user_id, created_at)', ]; } @@ -146,6 +157,8 @@ public function get_new_columns_definition() { 'extras LONGTEXT AFTER status', 'user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER form_id', 'INDEX idx_user_id (user_id)', + // Note: @since x.x.x -- Covers the conversion rate's submitter lookup. + 'INDEX idx_user_id_created_at (user_id, created_at)', ]; } diff --git a/inc/database/tables/payments.php b/inc/database/tables/payments.php index 25ecdc964..73d13dd0d 100644 --- a/inc/database/tables/payments.php +++ b/inc/database/tables/payments.php @@ -1285,15 +1285,36 @@ private static function build_clause_for_group( array $where_group, array &$para $column = $condition['key']; - if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) && is_array( $condition['value'] ) ) { + if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) ) { + if ( ! is_array( $condition['value'] ) ) { + // A scalar used to fall through to the else branch and emit + // `col IN %s`, which is a syntax error that fails the whole + // query. Base::prepare_where_clauses() reports this and drops + // the condition; do the same rather than leave the two builders + // disagreeing on malformed input. + _doing_it_wrong( + __METHOD__, + esc_html( "{$operator} requires an array value, received " . gettype( $condition['value'] ) . '.' ), + 'x.x.x' + ); + continue; + } + $ids = array_map( 'absint', $condition['value'] ); if ( [] === $ids ) { - // Same empty-list handling as Base::prepare_where_clauses(), so the - // two builders cannot disagree. An empty IN matches nothing. An - // empty NOT IN excludes nothing, and is dropped rather than written - // as a literal, because a literal true would make an enclosing OR - // group -- such as the customer filter this shortcode builds -- - // match every row. + // Same empty-list handling as Base::prepare_where_clauses(), so + // the two builders cannot disagree. An empty IN matches nothing. + // An empty NOT IN excludes nothing, and is dropped rather than + // written as a literal, because a literal true would make an + // enclosing OR group match every row. + // + // This builder serves the manage_options-gated admin payments + // listing only, through get_all_main_payments() and + // get_total_main_payments_by_status(). The payment-history + // shortcode's customer filter is compiled by + // Base::prepare_where_clauses() via Payments::get_all() -- that + // group is where an OR fail-open would actually leak, and it is + // covered by the change in base.php. if ( 'IN' === $operator ) { $sub_clauses[] = '1 = 0'; } diff --git a/inc/forms-data.php b/inc/forms-data.php index f17c34bbc..958eaa5bf 100644 --- a/inc/forms-data.php +++ b/inc/forms-data.php @@ -469,8 +469,15 @@ private static function window_boundary_sql( $window_start ) { * up to the ceiling in get_forms_sorted_by_metric(). On a membership site that * hands `contributor` to every member that is a five-figure placeholder list * rebuilt for every row of a ten-row page. Only people who actually submitted - * inside the window can affect the rate, that set is small, and `idx_user_id` - * covers it. + * inside the window can affect the rate, and that set is small. + * + * Served by `idx_user_id_created_at (user_id, created_at)`, added for this + * lookup. `idx_user_id` alone cannot: `created_at` is not in it and no other + * index leads on `created_at`, so before that index this was a range scan with + * a row read per row plus a temp table for the DISTINCT -- on a site with years + * of logged-in submissions, every entry ever recorded, to return a short list. + * The LIMIT bounds what comes back, not what is read; the index bounds the + * read. * * Decided with `user_can()`, the same call Form_Views::should_track() makes, so * both halves of the rate answer one question rather than two similar ones. A @@ -512,6 +519,16 @@ private static function get_editing_submitter_ids( $window_start ) { * list reports a rate that is wrong in a way nobody can see, where no * exclusion at least reproduces the pre-existing behaviour. * + * Two things to know before raising or lowering it. The count is of + * distinct submitters across all forms since tracking was first enabled, + * and that window never resets -- so a membership site, a store or an LMS + * reaches 500 in ordinary operation, and once passed it stays passed, with + * the rate quietly counting editor submissions again. That is why the + * settings copy says those are "normally" left out rather than promising it + * outright. And the query does not filter on status, so a user whose only + * in-window entries were trashed still lands on the list and consumes + * budget -- harmless for the count, but it brings the ceiling closer. + * * @param int $limit Maximum submitters to test. Default 500. * @since x.x.x */ @@ -538,7 +555,7 @@ private static function get_editing_submitter_ids( $window_start ) { ], ], 'DISTINCT user_id', - [ 'LIMIT ' . ( $limit + 1 ) ] + [ sprintf( 'LIMIT %d', $limit + 1 ) ] ); $submitters = array_values( array_unique( array_map( 'absint', array_column( $rows, 'user_id' ) ) ) ); diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js index 26927e1f0..48baac125 100644 --- a/src/admin/settings/pages/General.js +++ b/src/admin/settings/pages/General.js @@ -234,7 +234,13 @@ const FormViewsTrackingContent = ( { 'sureforms' ), description: ( - <> + // One flex item, not two. force-ui's Label carries `flex + // items-center gap-0.5` in its base classes, and the className + // Switch passes it has no display utility to override that -- + // so two sibling spans become flex items side by side, not + // stacked, however they are displayed themselves. The wrapper + // is the single item; its children stack as ordinary blocks. + { /* Kept verbatim from 2.12.6: all seven shipped locales already translate this msgid, and re-punctuating it to add the sentence below would orphan every one of @@ -247,11 +253,11 @@ const FormViewsTrackingContent = ( { { __( - 'Views and submissions by anyone who can edit the site are left out, so testing your own forms does not change these figures.', + 'Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.', 'sureforms' ) } - + ), } } value={ generalTabOptions.srfm_form_views_tracking } diff --git a/tests/unit/inc/database/test-base.php b/tests/unit/inc/database/test-base.php index 9e3767272..22d252a19 100644 --- a/tests/unit/inc/database/test-base.php +++ b/tests/unit/inc/database/test-base.php @@ -467,6 +467,94 @@ public function test_prepare_where_clauses_empty_not_in_is_dropped() { $this->assertStringNotContainsString( 'NOT IN', $not_in, 'The condition is dropped, not built.' ); } + /** + * A scalar where an array belongs is a caller bug, and it must surface. + * + * `'NOT IN'` with `5` -- a plausible typo for `[ 5 ]` -- used to be folded in + * with the empty-array case and drop the condition, excluding nobody with no + * error and a green suite, while the same typo on `'IN'` failed closed. On a + * primitive whose only job is scoping data that asymmetry is the hazard. + */ + public function test_prepare_where_clauses_non_array_in_value_is_doing_it_wrong() { + $notices = []; + + $observe = static function ( $function_name, $message ) use ( &$notices ) { + $notices[] = $function_name . ': ' . $message; + }; + + add_action( 'doing_it_wrong_run', $observe, 10, 2 ); + // _doing_it_wrong() escalates to trigger_error() under WP_DEBUG, which + // would abort the test rather than let it assert. + add_filter( 'doing_it_wrong_trigger_error', '__return_false' ); + + $result = $this->prepare( + [ + [ + [ + 'key' => 'user_id', + 'compare' => 'NOT IN', + // @phpstan-ignore-next-line -- Deliberately the wrong type. + 'value' => 5, + ], + ], + ] + ); + + remove_filter( 'doing_it_wrong_trigger_error', '__return_false' ); + remove_action( 'doing_it_wrong_run', $observe, 10 ); + + $this->assertNotEmpty( $notices, 'A scalar value must be reported, not swallowed.' ); + $this->assertStringContainsString( 'prepare_where_clauses', $notices[0] ); + $this->assertStringContainsString( 'NOT IN requires an array value', $notices[0] ); + // The received type, so the caller does not have to guess what it sent. + $this->assertStringContainsString( 'integer', $notices[0] ); + + // And it must not have built a condition out of the bad input. + $this->assertStringNotContainsString( 'NOT IN', $result ); + $this->assertStringNotContainsString( '1 = 1', $result ); + } + + /** + * An operator is normalised before the allowlist test. + * + * Payments' builder upper-cases and trims; this one compared strictly. So a + * caller writing `'not in'` was honoured by one and silently dropped by the + * other -- and a dropped NOT IN is a silently disabled exclusion. + */ + public function test_prepare_where_clauses_normalises_operator_case() { + $result = $this->prepare( + [ + [ + [ + 'key' => 'user_id', + 'compare' => ' not in ', + 'value' => [ 4, 5 ], + ], + ], + ] + ); + + $this->assertStringContainsString( 'NOT IN', $result, 'A lower-case operator must still be honoured.' ); + $this->assertStringContainsString( '4', $result ); + $this->assertStringContainsString( '5', $result ); + + // Still an allowlist: an unknown operator is dropped however it is cased. + $this->assertSame( + '', + $this->prepare( + [ + [ + [ + 'key' => 'user_id', + 'compare' => 'drop table', + 'value' => [ 1 ], + ], + ], + ] + ) + ); + } + /** * An empty NOT IN beside an OR sibling must not widen the group. * diff --git a/tests/unit/inc/test-forms-data.php b/tests/unit/inc/test-forms-data.php index de5b2979e..0d4ab3411 100644 --- a/tests/unit/inc/test-forms-data.php +++ b/tests/unit/inc/test-forms-data.php @@ -23,6 +23,8 @@ protected function tearDown(): void { // The list is memoised for the request, and a test process is one request. // Left behind it holds ids of users a later test has already deleted. $this->reset_editing_cache(); + + parent::tearDown(); } /** From b90da4632ff573cda477beb00fa6872047f16c43 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 10 Sep 2026 15:42:27 +0530 Subject: [PATCH 41/58] fix: address round-2 review on the Form Checks notices The scrubber shipped as the fix for a High and failed in both directions. I reproduced all of it before rewriting. It missed the secret. A WP REST error body arrives slash-escaped -- wp_json_encode() escapes "/" and serve_request() does not pass JSON_UNESCAPED_SLASHES -- and two of the four body sinks log the raw response text, so both URL rules saw `https:\/\/` and matched nothing. The Slack token this round existed to mask survived verbatim in the largest sink. The credential rule only fired when the keyword was the last token of the name, so AWS_SECRET_ACCESS_KEY, stripe_secret_key, private_key, license_key, Authorization: Basic and X-Hub-Signature all passed through untouched, and a userinfo password survived because only the path was truncated. It also deleted the diagnosis. `source` is a stack frame, not a page address -- form-submit.js takes error.stack.split( "\n" )[1] -- so truncating every path removed the filename, the line and column, and which plugin threw, on every site. And the credential rule accepted a bare space as a separator against a list of unanchored English words, so "Invalid token provided", "password protected" and "captcha token verification failed" were all redacted. Those are the most common things support reads out of this log, and test_scrub_text_preserves_error_codes exists to prevent exactly that and still passed. Now: slash-escaping normalised first, userinfo masked before the host rules, same-origin paths exempt from truncation, credential names matched as whole identifiers with a separator required except after bearer/basic, tails bounded so a trailing paren or brace is not eaten, and the clamp applied first rather than last. Verified against 19 cases: 13 secrets redacted, 6 diagnostics intact including the full stack frame with line and column. Both rules now have tests, as two data providers -- the twelve secret shapes, including the slash-escaped Slack body as WP actually delivers it, and the over-reach guards that had no coverage at all. Both halves revert-tested. The carousel's adopt() is driven by a MutationObserver rather than from a click. Called only from render() it could not fire for a relocation after the first paint, because reaching it needed the arrow click that a relocation breaks -- cards move away carrying their `hidden` attribute while nav stays behind, leaving one notice visible, the rest permanently hidden, and nothing the user can reach. It also anchors on the first card still outside the wrapper, so a removed card or a partially-adopted state cannot throw out of render() before the visibility loop, and it saves and restores focus, because moving the wrapper blurs the arrow inside it. The notification clear is scoped to the form the fault was recorded against, skipped when the success came from the mail() fallback, and gated on is_int( $log_key ) so clearing and recording are symmetric. It runs on the public submit path and the counter is per category, so an anonymous submission of a working form was retiring a different form's standing fault -- once per admin page load, by anyone -- and mail() returning true means the local MTA accepted a message it will bounce, which is the broken case. Also: title and message guarded like every sibling key; a non-array filter item skipped rather than slipping past the URL pass; the carousel stylesheet gated on more than one notice, without touching the guard that decides whether notices render at all; field_keys keeps wp_check_invalid_utf8(), because invalid UTF-8 made wp_json_encode() drop the whole line after the counter had incremented; the tail memo keyed by blog; case-insensitive mailto; sendResponse() guards the global like its neighbour; the dead translators comment removed; and POT references refreshed. Tests added for the mixed success/failure loop -- the case the flag is hoisted out of the loop for and the one nothing covered -- and for the cross-form clear. Both revert-tested. Co-Authored-By: Claude Opus 5 (1M context) --- admin/admin.php | 29 ++++- admin/assets/js/notice-response.js | 70 +++++++++-- inc/client-logger.php | 95 ++++++++++++--- inc/form-submit.php | 36 +++++- languages/sureforms.pot | 154 ++++++++++++------------ src/admin/dashboard/ActionItems.js | 6 +- tests/unit/inc/test-client-logger.php | 85 ++++++++++++++ tests/unit/inc/test-form-submit.php | 163 ++++++++++++++++++++++++-- 8 files changed, 517 insertions(+), 121 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index f2bf27add..9c37bc16e 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -3440,7 +3440,12 @@ public function render_action_item_notices() { } $this->enqueue_notice_response_script(); - $this->print_action_item_carousel_styles(); + + // The carousel is the stylesheet's only consumer and notice-response.js + // bails below two cards, so a single open fault would ship inert CSS. + if ( $rendered > 1 ) { + $this->print_action_item_carousel_styles(); + } foreach ( $items as $item ) { $status = Helper::get_string_value( $item['status'] ?? '' ); @@ -3456,8 +3461,17 @@ public function render_action_item_notices() { $class = 'error' === $status ? 'notice-error' : 'notice-warning'; ?>
-

-

+ +

+

$item ) { + // A filter may hand back an object. isset() on it returns false, which + // would slip the item past this pass and hand React an unchecked URL. + if ( ! is_array( $item ) ) { + continue; + } + foreach ( [ 'cta_url', 'guide_url' ] as $key ) { if ( ! isset( $item[ $key ] ) ) { continue; @@ -4499,8 +4519,9 @@ private function track_action_item_impressions( $warnings ) { private function get_support_email_url( $category = '', $form_title = '' ) { $copy = $this->get_support_copy( $category ); + // No translators comment here: makepot cannot attach one to a variable, and + // the real ones are on the literals inside get_support_copy(). $subject = sprintf( - /* translators: %s: site host. */ $copy['subject'], Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ) ); diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js index 76eb3ffe8..4e15292f5 100644 --- a/admin/assets/js/notice-response.js +++ b/admin/assets/js/notice-response.js @@ -36,6 +36,13 @@ } function sendResponse( noticeId, button ) { + // Guarded like the carousel's read of the same global: this file is + // enqueued from several places and a missing localize object should not + // throw out of a click handler. + if ( typeof srfmNoticeResponse === 'undefined' ) { + return; + } + const body = new FormData(); body.append( 'action', 'srfm_notice_response' ); body.append( 'nonce', srfmNoticeResponse.nonce ); @@ -128,6 +135,13 @@ const wrap = document.createElement( 'div' ); wrap.className = 'srfm-action-item-carousel'; + // Positioning and spacing live in the stylesheet the renderer prints, so + // an RTL sheet can override them and nothing here is a magic number. + // Declared above adopt() because adopt() reads it and runs before the + // controls are assembled. + const nav = document.createElement( 'p' ); + nav.className = 'srfm-action-item-carousel-nav'; + /** * Put the cards inside the wrapper, wherever they currently are. * @@ -135,9 +149,17 @@ * and that runs after this file's DOMContentLoaded handler -- so wrapping * once at build time left the wrapper behind, empty and zero-height, with * the controls positioned against it off the side of the screen and the - * reserved padding matching nothing. Re-checking is cheap, only moves - * anything when something else has moved it, and also survives a plugin - * that relocates notices later. + * reserved padding matching nothing. + * + * Driven by a MutationObserver rather than from render(). Called only from + * render() it could not fire for a relocation that happens after the first + * paint -- reaching it needed an arrow click, and a relocation is exactly + * what strands the arrows: the cards move away carrying their `hidden` + * attribute while nav stays behind, leaving one notice visible, the rest + * permanently hidden, and no control the user can reach to recover. On a + * surface whose only job is showing faults that is worse than not having a + * carousel. The every() check makes this idempotent, so the observer + * seeing its own writes is harmless. */ function adopt() { if ( @@ -148,19 +170,38 @@ return; } - cards[ 0 ].parentNode.insertBefore( wrap, cards[ 0 ] ); + // Anchor on the first card still outside the wrapper. cards[0] may + // already be inside it, and wrap.insertBefore( wrap, … ) is a + // HierarchyRequestError; a card that was removed rather than moved has + // no parentNode at all, and dereferencing it would throw out of + // render() before the visibility loop ran. + const anchor = cards.filter( function ( notice ) { + return notice.parentNode && notice.parentNode !== wrap; + } )[ 0 ]; + + if ( ! anchor ) { + return; + } + + // Moving the wrapper runs the DOM remove steps over its subtree, which + // blurs whatever inside it had focus -- and nav is inside it, so the + // arrow the user just pressed loses focus in exactly the case adopt() + // exists for. + const active = nav.ownerDocument.activeElement; + const focused = nav.contains( active ) ? active : null; + + anchor.parentNode.insertBefore( wrap, anchor ); cards.forEach( function ( notice ) { wrap.appendChild( notice ); } ); + + if ( focused ) { + focused.focus(); + } } adopt(); - // Positioning and spacing live in the stylesheet the renderer prints, so - // an RTL sheet can override them and nothing here is a magic number. - const nav = document.createElement( 'p' ); - nav.className = 'srfm-action-item-carousel-nav'; - const isRtl = document.documentElement.getAttribute( 'dir' ) === 'rtl' || document.body.classList.contains( 'rtl' ); @@ -241,6 +282,17 @@ render(); measure(); }, 0 ); + + // And keep watching. A plugin acting on window.load, an admin-notice + // manager or a screen-options re-layout can move the notices at any point, + // and adopt() driven only from a click cannot recover from that -- the + // click it needs is the thing the move breaks. + if ( window.MutationObserver ) { + new window.MutationObserver( function () { + adopt(); + measure(); + } ).observe( document.body, { childList: true, subtree: true } ); + } } if ( document.readyState === 'loading' ) { diff --git a/inc/client-logger.php b/inc/client-logger.php index 51634d60a..56c38c55d 100644 --- a/inc/client-logger.php +++ b/inc/client-logger.php @@ -109,7 +109,7 @@ class Client_Logger { * clear() can invalidate it: a request that writes to the log and then reads a * tail must not be handed the tail from before the write. * - * @var array + * @var array * @since x.x.x */ private static $tail_memo = []; @@ -480,7 +480,10 @@ public static function get_tail( $max_chars = 1200 ) { // localisation payload and again in the classic renderer -- so a site with // three open failures was reading a file capped at 1 MB six times to render // one page. - $max_chars = (int) $max_chars; + // Keyed by blog as well as budget: get_log_path() hashes the blog id into + // the filename, so after a switch_to_blog() the same budget is a different + // file. Unreachable today; nothing switches blogs on this path. + $max_chars = get_current_blog_id() . ':' . (int) $max_chars; if ( isset( self::$tail_memo[ $max_chars ] ) ) { return self::$tail_memo[ $max_chars ]; @@ -637,7 +640,17 @@ public static function sanitize_entry( array $raw ) { // supposed to be `srfm-input-lbl-`, but the array arrives // from the browser and nothing server-side guarantees that, so a // caller is free to put an address or a token in one. - $keys[] = mb_substr( self::scrub_text( Helper::get_string_value( $field_key ) ), 0, self::MAX_KEY_LENGTH ); + // + // wp_check_invalid_utf8() is kept because scrub_text() is not a + // drop-in for sanitize_text_field(): invalid UTF-8 reaching + // wp_json_encode() in append() makes it return false and drop the + // whole line -- after record_failure() has already incremented the + // counter, leaving a banner with no log line behind it. + $keys[] = mb_substr( + self::scrub_text( wp_check_invalid_utf8( Helper::get_string_value( $field_key ) ) ), + 0, + self::MAX_KEY_LENGTH + ); } $entry['field_keys'] = $keys; @@ -658,12 +671,18 @@ public static function sanitize_entry( array $raw ) { * string. Whitespace is collapsed as a log-injection guard, matching * inc/ai-form-builder/ai-helper.php. * - * Removes, in order: URL query strings and fragments; everything past a URL's - * first path segment, because a webhook credential sits in the path as often - * as in the query; the value following a name that identifies a credential; - * email addresses; and long digit runs. What it cannot remove is a name, a - * street address or a free-text message body -- those have no shape to match, - * so the log excerpt this produces should still be treated as personal data. + * Removes, in order: JSON slash-escaping, so the rules below can see URLs at + * all; credentials in a URL's userinfo; query strings and fragments; a foreign + * URL's path past its first segment, keeping same-origin paths intact because + * those are stack frames and the path is the diagnosis; the value following a + * name that identifies a credential; email addresses; and long digit runs. + * + * A single-segment foreign path is truncated whole rather than kept, which is + * the safe direction. + * + * What it cannot remove is a name, a street address or a free-text message + * body -- those have no shape to match, so the log excerpt this produces + * should still be treated as personal data. * * @param string $text Raw text. * @since 2.12.6 @@ -674,20 +693,58 @@ public static function scrub_text( $text ) { return ''; } + // Clamped before the rules run, not after. Without it every pattern below + // is applied to whatever the caller sent, however long that is. + $text = mb_substr( $text, 0, self::MAX_TEXT_LENGTH * 4 ); + + // A WP REST error body arrives slash-escaped -- wp_json_encode() escapes + // "/" and WP_REST_Server::serve_request() does not pass + // JSON_UNESCAPED_SLASHES -- and two of the four body sinks log the raw + // response text rather than the decoded object. Without this every URL + // rule below misses every URL in the largest sink, including the webhook + // tokens they exist for. + $text = str_replace( '\\/', '/', $text ); + + // Credentials in the userinfo position, before the host rules see them. + $text = (string) preg_replace( '#(https?://)[^\s/@]+@#i', '$1[credentials]@', $text ); + // Drop query strings and fragments wholesale rather than allowlisting // parameters. A token after # is just as sensitive as one after ?. - $text = (string) preg_replace( '#(https?://[^\s?\#]+)[?\#]\S*#i', '$1', $text ); - - // Keep the origin and the first path segment, drop the rest. A webhook - // credential is as often in the path as in the query -- Slack and Discord - // both put theirs there -- and the rule above keeps paths verbatim. - $text = (string) preg_replace( '#(https?://[^\s/]+(?:/[^\s/]*)?)/\S+#i', '$1/[path]', $text ); + $text = (string) preg_replace( '#(https?://[^\s?\#]+)[?\#][^\s"\'<>,;)\]}]*#i', '$1', $text ); + + $site_host = Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ); + + // Keep the origin and the first path segment of a foreign URL, drop the + // rest: a webhook credential sits in the path as often as in the query, + // and Slack and Discord both put theirs there. + // + // Same-origin URLs are exempt. `source` is a stack frame, not a page + // address -- assets/js/unminified/form-submit.js takes + // error.stack.split( "\n" )[1] -- so truncating our own paths deletes the + // filename, the line and column, and which plugin threw, which is the + // whole diagnosis. A third-party credential is never same-origin. + $text = (string) preg_replace_callback( + '#(https?://)([^\s/]+)((?:/[^\s/]*)?)/[^\s"\'<>,;)\]}]+#i', + static function ( $matches ) use ( $site_host ) { + if ( '' !== $site_host && 0 === strcasecmp( $matches[2], $site_host ) ) { + return $matches[0]; + } + + return $matches[1] . $matches[2] . $matches[3] . '/[path]'; + }, + $text + ); - // Credentials named in the text itself, whether or not they sit in a URL: - // `api_key=…`, `Authorization: Bearer …`, `"token": "…"`. Matched on the - // name so the value's shape does not have to be guessed. + // Credentials named in the text itself. The name is matched as a whole + // identifier, so a keyword with a prefix or suffix is still caught -- + // AWS_SECRET_ACCESS_KEY, stripe_secret_key, X-Hub-Signature. And a + // separator is required, so ordinary prose survives: "Invalid token + // provided" and "password protected" are the most common things support + // reads out of this log, and an earlier version redacted both. `bearer` + // and `basic` are the exception, because those carry the value after a + // space with no separator at all. $text = (string) preg_replace( - '/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|secret|token|bearer|password|passwd|pwd|auth)["\']?\s*(?:[:=]|\s)\s*["\']?)[^\s"\',;&]{6,}/i', + '/(\b[\w.-]*(?:api[_-]?key|key|secret|token|password|passwd|pwd|auth|credential|signature)[\w.-]*["\']?\s*[:=]\s*["\']?|\b(?:bearer|basic)\s+)[^\s"\',;&]{8,}/i', '$1[redacted]', $text ); diff --git a/inc/form-submit.php b/inc/form-submit.php index dee4b706f..1b6994686 100644 --- a/inc/form-submit.php +++ b/inc/form-submit.php @@ -1031,9 +1031,12 @@ public static function send_email( $id, $submission_data, $form_data = [] ) { $email_notification = get_post_meta( intval( $id ), '_srfm_email_notification' ); $is_mail_sent = false; // Any recipient failing counts as a failure for the whole submission, so - // this is set inside the loop and only read after it. + // these are set inside the loop and only read after it. $notification_failed = false; - $emails = []; + // Whether any recipient's "success" came from the mail() fallback, which + // reports true for a message the local MTA accepted and will bounce. + $used_mail_fallback = false; + $emails = []; // Filter to determine whether the email notification should be sent. $email_notification = apply_filters( 'srfm_email_notification_should_send', $email_notification, $submission_data, $form_data ); @@ -1103,6 +1106,13 @@ static function() { if ( ! $sent ) { // Fallback to default PHP mail if for some reasons wp_mail fails. $sent = mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] ); + + if ( $sent ) { + // Accepted by the local MTA, not delivered. Good + // enough to avoid recording a fault, not good + // enough to retire one. + $used_mail_fallback = true; + } } $email_report = ob_get_clean(); // Catch any printed notice/errors/message for reports. @@ -1198,7 +1208,27 @@ static function() { // banner on every admin page until somebody opened a support ticket. // Held until the loop is done because one recipient succeeding while // another fails is still a failure. - if ( ! empty( $emails ) && ! $notification_failed ) { + // + // Scoped to the form the fault was recorded against. send_email() runs + // on the public submit path and the counter is per category, not per + // form, so without this an anonymous submission of a working form + // wipes a different form's standing fault -- once per admin page load, + // by anyone. The notice names a form, so the granularity is visible + // now that this clears as well as records. + // + // wp_mail() only. The mail() fallback above returns true when the local + // MTA merely accepts a message it will later bounce, which is the + // broken configuration rather than the fixed one. + // + // is_int( $log_key ) mirrors the recording guard: record_failure() sits + // inside it, so without it an install where add_log() returns a + // non-int would never record a notification fault but would still + // clear one. + $open_failures = Client_Logger::get_failures(); + + if ( ! empty( $emails ) && ! $notification_failed && is_int( $log_key ) + && ! $used_mail_fallback + && intval( $id ) === Helper::get_integer_value( $open_failures['notification']['form_id'] ?? 0 ) ) { Client_Logger::clear_category( 'notification' ); } } diff --git a/languages/sureforms.pot b/languages/sureforms.pot index c98ed9b7d..7c0228097 100644 --- a/languages/sureforms.pot +++ b/languages/sureforms.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-09-09T10:57:21+00:00\n" +"POT-Creation-Date: 2026-09-10T10:11:21+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.11.0\n" "X-Domain: sureforms\n" @@ -59,7 +59,7 @@ msgid "Review or edit your form" msgstr "" #: admin/admin.php:628 -#: admin/admin.php:4111 +#: admin/admin.php:4144 msgid "Edit form" msgstr "" @@ -139,7 +139,7 @@ msgstr "" #: admin/admin.php:1236 #: admin/admin.php:1237 -#: admin/admin.php:4333 +#: admin/admin.php:4366 #: assets/build/blocks.js:172 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 @@ -399,7 +399,7 @@ msgstr "" #: admin/admin.php:2986 #: admin/admin.php:3007 #: admin/admin.php:3330 -#: admin/admin.php:3652 +#: admin/admin.php:3685 #: inc/admin-ajax.php:164 #: inc/payments/admin/admin-handler.php:638 #: inc/payments/stripe/admin-stripe-handler.php:80 @@ -412,12 +412,12 @@ msgstr "" #: admin/admin.php:2982 #: admin/admin.php:3003 #: admin/admin.php:3334 -#: admin/admin.php:3647 +#: admin/admin.php:3680 msgid "Unauthorized user." msgstr "" #: admin/admin.php:2852 -#: admin/admin.php:3659 +#: admin/admin.php:3692 msgid "Invalid parameters." msgstr "" @@ -435,7 +435,7 @@ msgid "Build My First Form" msgstr "" #: admin/admin.php:2967 -#: admin/admin.php:3500 +#: admin/admin.php:3514 msgid "Dismiss" msgstr "" @@ -484,236 +484,236 @@ msgstr "" msgid "Form Name" msgstr "" -#: admin/admin.php:3519 +#: admin/admin.php:3533 msgid "You do not have permission to do this." msgstr "" #. translators: %s: form title. -#: admin/admin.php:3725 +#: admin/admin.php:3758 msgid "We noticed a form submission failure on %s." msgstr "" -#: admin/admin.php:3726 +#: admin/admin.php:3759 msgid "We noticed a form submission failure." msgstr "" -#: admin/admin.php:3727 +#: admin/admin.php:3760 msgid "Visitors may not be able to reach you, and their entries were not saved." msgstr "" #. translators: %s: form title. -#: admin/admin.php:3732 +#: admin/admin.php:3765 msgid "We noticed a notification failure on %s." msgstr "" -#: admin/admin.php:3733 +#: admin/admin.php:3766 msgid "We noticed a notification failure." msgstr "" -#: admin/admin.php:3734 +#: admin/admin.php:3767 msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." msgstr "" #. translators: %s: form title. -#: admin/admin.php:3749 +#: admin/admin.php:3782 msgid "We noticed an integration failure on %s." msgstr "" -#: admin/admin.php:3750 +#: admin/admin.php:3783 msgid "We noticed an integration failure." msgstr "" -#: admin/admin.php:3751 +#: admin/admin.php:3784 msgid "The entry was saved, but we could not send it to a connected service." msgstr "" -#: admin/admin.php:3771 +#: admin/admin.php:3804 #: assets/build/templatePicker.js:172 msgid "Contact Support" msgstr "" -#: admin/admin.php:3781 -#: admin/admin.php:3809 +#: admin/admin.php:3814 +#: admin/admin.php:3842 msgid "Help Me Fix" msgstr "" #. translators: %s: caching plugin name. -#: admin/admin.php:3805 +#: admin/admin.php:3838 msgid "%s may interfere with your forms." msgstr "" -#: admin/admin.php:3808 +#: admin/admin.php:3841 msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." msgstr "" -#: admin/admin.php:3854 +#: admin/admin.php:3887 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "" -#: admin/admin.php:3857 +#: admin/admin.php:3890 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "" -#: admin/admin.php:4100 +#: admin/admin.php:4133 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "" #. translators: %s: form name. -#: admin/admin.php:4105 +#: admin/admin.php:4138 msgid "Finish setting up “%s”" msgstr "" -#: admin/admin.php:4118 +#: admin/admin.php:4151 msgid "Edit the Thank You message" msgstr "" -#: admin/admin.php:4124 +#: admin/admin.php:4157 msgid "Set where replies go" msgstr "" -#: admin/admin.php:4274 +#: admin/admin.php:4307 msgid "Use Conditional Logic to show only what matters" msgstr "" -#: admin/admin.php:4275 +#: admin/admin.php:4308 msgid "Split your form into steps to keep it easy" msgstr "" -#: admin/admin.php:4276 +#: admin/admin.php:4309 msgid "Let people upload files directly to your form" msgstr "" -#: admin/admin.php:4277 +#: admin/admin.php:4310 msgid "Turn responses into downloadable PDFs automatically" msgstr "" -#: admin/admin.php:4278 +#: admin/admin.php:4311 msgid "Let users sign with a simple signature field" msgstr "" -#: admin/admin.php:4279 +#: admin/admin.php:4312 msgid "Connect your form to other tools using webhooks" msgstr "" -#: admin/admin.php:4280 +#: admin/admin.php:4313 msgid "Use Conversational Forms for a chat-like experience" msgstr "" -#: admin/admin.php:4281 +#: admin/admin.php:4314 msgid "Let users register or log in through your form" msgstr "" -#: admin/admin.php:4282 +#: admin/admin.php:4315 msgid "Build forms that create WordPress user accounts" msgstr "" -#: admin/admin.php:4283 +#: admin/admin.php:4316 msgid "Add calculations to auto-total scores or prices" msgstr "" -#: admin/admin.php:4502 +#: admin/admin.php:4538 msgid "Debug log: no entries recorded." msgstr "" #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4506 +#: admin/admin.php:4542 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "" -#: admin/admin.php:4517 +#: admin/admin.php:4553 msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." msgstr "" #. translators: %s: site host. -#: admin/admin.php:4553 +#: admin/admin.php:4589 msgid "SureForms: form submissions are failing on %s" msgstr "" #. translators: %s: site host. -#: admin/admin.php:4555 +#: admin/admin.php:4591 msgid "SureForms has recorded form submissions on %s that could not be completed." msgstr "" #. translators: %s: site host. -#: admin/admin.php:4559 +#: admin/admin.php:4595 msgid "SureForms: notification emails are not being sent on %s" msgstr "" #. translators: %s: site host. -#: admin/admin.php:4561 +#: admin/admin.php:4597 msgid "SureForms saved entries on %s but could not send the notification emails for them." msgstr "" #. translators: %s: site host. -#: admin/admin.php:4565 +#: admin/admin.php:4601 msgid "SureForms: an integration is not receiving entries on %s" msgstr "" #. translators: %s: site host. -#: admin/admin.php:4567 +#: admin/admin.php:4603 msgid "SureForms saved entries on %s but could not pass them to a connected service." msgstr "" #. translators: %s: site host. -#: admin/admin.php:4577 +#: admin/admin.php:4613 msgid "SureForms: a problem with the forms on %s" msgstr "" #. translators: %s: site host. -#: admin/admin.php:4579 +#: admin/admin.php:4615 msgid "SureForms has recorded a problem with the forms on %s." msgstr "" #. translators: %d: number of failed submissions. -#: admin/admin.php:4604 +#: admin/admin.php:4640 msgid "SureForms has recorded %d form submission that could not be completed." msgid_plural "SureForms has recorded %d form submissions that could not be completed." msgstr[0] "" msgstr[1] "" #. translators: %d: number of failed notifications. -#: admin/admin.php:4616 +#: admin/admin.php:4652 msgid "SureForms saved %d entry but could not send the notification email for it." msgid_plural "SureForms saved %d entries but could not send the notification emails for them." msgstr[0] "" msgstr[1] "" #. translators: %d: number of failed integration hand-offs. -#: admin/admin.php:4628 +#: admin/admin.php:4664 msgid "SureForms saved %d entry but could not pass it to a connected service." msgid_plural "SureForms saved %d entries but could not pass them to a connected service." msgstr[0] "" msgstr[1] "" #. translators: %d: number of recorded problems. -#: admin/admin.php:4640 +#: admin/admin.php:4676 msgid "SureForms has recorded %d problem with the forms on this site." msgid_plural "SureForms has recorded %d problems with the forms on this site." msgstr[0] "" msgstr[1] "" -#: admin/admin.php:4679 +#: admin/admin.php:4715 msgid "Hello SureForms support," msgstr "" #. translators: %s: form title. -#: admin/admin.php:4690 +#: admin/admin.php:4726 msgid "Form: %s" msgstr "" -#: admin/admin.php:4703 +#: admin/admin.php:4739 msgid "Site details" msgstr "" -#: admin/admin.php:4706 +#: admin/admin.php:4742 msgid "not active" msgstr "" -#: admin/admin.php:4709 +#: admin/admin.php:4745 msgid "none detected" msgstr "" -#: admin/admin.php:4710 +#: admin/admin.php:4746 msgid "none recorded" msgstr "" @@ -1287,7 +1287,7 @@ msgid "OttoKit is not configured properly." msgstr "" #: inc/admin-ajax.php:184 -#: inc/form-submit.php:1231 +#: inc/form-submit.php:1261 msgid "Invalid form ID." msgstr "" @@ -1425,7 +1425,7 @@ msgstr "" #: inc/form-submit.php:247 #: inc/form-submit.php:553 #: inc/form-submit.php:598 -#: inc/form-submit.php:1227 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -2212,75 +2212,75 @@ msgstr "" msgid "Unable to submit form. Please try again." msgstr "" -#: inc/form-submit.php:1043 +#: inc/form-submit.php:1046 msgid "Email notification passed to the sending server" msgstr "" #. translators: Here, %s is the comma separated emails list. -#: inc/form-submit.php:1120 +#: inc/form-submit.php:1130 msgid "Email notification recipient: %s" msgstr "" -#: inc/form-submit.php:1127 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "" -#: inc/form-submit.php:1128 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "" #. translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). -#: inc/form-submit.php:1137 +#: inc/form-submit.php:1147 msgid "Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s" msgstr "" -#: inc/form-submit.php:1191 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "" -#: inc/form-submit.php:1236 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "" -#: inc/form-submit.php:1241 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "" -#: inc/form-submit.php:1366 +#: inc/form-submit.php:1396 msgid "Captcha validation failed. No error code provided." msgstr "" -#: inc/form-submit.php:1367 +#: inc/form-submit.php:1397 msgid "Captcha validation failed." msgstr "" -#: inc/form-submit.php:1422 +#: inc/form-submit.php:1452 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Google reCAPTCHA" msgstr "" -#: inc/form-submit.php:1426 +#: inc/form-submit.php:1456 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "hCaptcha" msgstr "" -#: inc/form-submit.php:1430 +#: inc/form-submit.php:1460 #: assets/build/settings.js:172 msgid "Cloudflare Turnstile" msgstr "" -#: inc/form-submit.php:1434 +#: inc/form-submit.php:1464 msgid "Unknown Captcha" msgstr "" -#: inc/form-submit.php:1435 +#: inc/form-submit.php:1465 msgid "Invalid captcha type." msgstr "" #. translators: %s is the captcha title. -#: inc/form-submit.php:1448 +#: inc/form-submit.php:1478 msgid "%s verification failed. Please contact your site administrator." msgstr "" diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 53dadf701..d1116fc8d 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -92,8 +92,10 @@ export default () => { name: item.cta_action, label: item.cta_label, url: item.cta_url, - // A mailto: must open in the mail client, not a new tab. - external: ! item.cta_url?.startsWith( 'mailto:' ), + // A mailto: must open in the mail client, not a new tab. Matched + // case-insensitively, because a MAILTO: from the filter would + // otherwise get target="_blank" and open a blank tab. + external: ! /^mailto:/i.test( item.cta_url ), }, ] : [] ), diff --git a/tests/unit/inc/test-client-logger.php b/tests/unit/inc/test-client-logger.php index 975a09454..75b2cf4e5 100644 --- a/tests/unit/inc/test-client-logger.php +++ b/tests/unit/inc/test-client-logger.php @@ -256,6 +256,91 @@ public function test_sanitize_entry_clamps_each_field_key_length() { $this->assertFalse( Client_Logger::is_full() ); } + /** + * Webhook credentials are masked in every shape they actually arrive in. + * + * The round-1 finding this answers was about a Slack token surviving in a URL + * path. The first version of these rules missed it in the largest sink: a WP + * REST error body is slash-escaped, and two of the four body sinks log the raw + * response text, so every URL rule saw `https:\/\/` and matched nothing. It + * also only fired when the credential keyword was the last token of the name, + * so AWS_SECRET_ACCESS_KEY, stripe_secret_key and X-Hub-Signature all passed + * through untouched. + * + * @dataProvider provide_secrets + * @param string $input Log text as it would arrive. + * @param string $secret The substring that must not survive. + */ + public function test_scrub_text_masks_credentials( $input, $secret ) { + $this->assertStringNotContainsString( + $secret, + Client_Logger::scrub_text( $input ), + 'This is written to a downloadable log and pasted into a support email.' + ); + } + + /** + * Inputs whose secret must not survive scrub_text(). + * + * @return array + */ + public function provide_secrets() { + return [ + 'slack in a path' => [ 'POST https://hooks.slack.com/services/T0/B0/xoxb-secrettoken failed', 'xoxb-secrettoken' ], + 'slack slash-escaped' => [ '{"message":"post to https:\\/\\/hooks.slack.com\\/services\\/T1\\/B2\\/xoxb-escapedtoken failed"}', 'xoxb-escapedtoken' ], + 'discord webhook' => [ 'Discord https://discord.com/api/webhooks/123/AbCdEfGhIjKlMnOp rejected', 'AbCdEfGhIjKlMnOp' ], + 'aws prefixed name' => [ 'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMIK7MDENG', 'wJalrXUtnFEMIK7MDENG' ], + 'stripe suffixed name' => [ 'stripe_secret_key: sk_live_51H8xyzABCDEF', 'sk_live_51H8xyzABCDEF' ], + 'private key' => [ 'private_key=MIIEvQIBADANBgkq', 'MIIEvQIBADANBgkq' ], + 'basic auth' => [ 'Authorization: Basic dXNlcjpwYXNzd29yZA==', 'dXNlcjpwYXNzd29yZA' ], + 'bearer token' => [ 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig', 'eyJhbGciOiJIUzI1NiJ9' ], + 'signature header' => [ 'X-Hub-Signature: sha256=9f86d081884c7d659a2f1234567890ab', '9f86d081884c7d659a2f' ], + 'url userinfo' => [ 'https://admin:hunter2pass@api.example.com/v1/x', 'hunter2pass' ], + 'json token value' => [ '{"token": "abc123def456", "ok": false}', 'abc123def456' ], + 'query string' => [ 'GET https://example.com/reset?key=supersecretvalue failed', 'supersecretvalue' ], + ]; + } + + /** + * The diagnosis survives, which is the half with no coverage before now. + * + * An earlier version of these rules truncated every URL path, including + * same-origin ones -- but `source` is a stack frame, not a page address, so + * that deleted the filename, the line and column, and which plugin threw. It + * also redacted ordinary prose, because the credential rule accepted a bare + * space as a separator: "Invalid token provided" and "password protected" are + * among the most common things support reads out of this log. + * + * @dataProvider provide_diagnostics + * @param string $input Log text as it would arrive. + * @param string $needle The substring support needs to still be there. + */ + public function test_scrub_text_keeps_the_diagnosis( $input, $needle ) { + $this->assertStringContainsString( + $needle, + Client_Logger::scrub_text( $input ), + 'Redaction must not cost the reason the log is read.' + ); + } + + /** + * Inputs whose diagnostic content must survive scrub_text(). + * + * @return array + */ + public function provide_diagnostics() { + return [ + 'own stack frame' => [ 'at handleSubmit (' . home_url( '/wp-content/plugins/sureforms/assets/js/form-submit.min.js' ) . ':12:3456)', 'form-submit.min.js' ], + 'own line number' => [ 'at handleSubmit (' . home_url( '/wp-content/plugins/sureforms/assets/js/form-submit.min.js' ) . ':12:3456)', ':12:3456' ], + 'prose token' => [ 'Invalid token provided', 'token provided' ], + 'prose password' => [ 'This form is password protected', 'password protected' ], + 'captcha message' => [ 'captcha token verification failed', 'token verification failed' ], + 'prose secret' => [ 'The secret provided was rejected', 'secret provided' ], + 'http status' => [ 'Submission responded 500 (text/html)', '500' ], + 'foreign host kept' => [ 'POST https://hooks.slack.com/services/T0/B0/tok failed', 'hooks.slack.com' ], + ]; + } + /** * A phone number is written 555-123-4567, not 5551234567, so a * contiguous-digits rule never sees a real one. The earlier test only used an diff --git a/tests/unit/inc/test-form-submit.php b/tests/unit/inc/test-form-submit.php index 306d9217e..5afdcb95b 100644 --- a/tests/unit/inc/test-form-submit.php +++ b/tests/unit/inc/test-form-submit.php @@ -601,13 +601,6 @@ public function test_send_email() { */ public function test_send_email_clears_the_notification_fault_on_success() { delete_option( Client_Logger::FAILURES_OPTION ); - Client_Logger::record_failure( 'notification', 0, 'Contact Form' ); - - $this->assertArrayHasKey( - 'notification', - Client_Logger::get_open_failures(), - 'Precondition: the fault is standing before the send.' - ); $form_id = wp_insert_post( [ @@ -617,6 +610,17 @@ public function test_send_email_clears_the_notification_fault_on_success() { ] ); + // Recorded against this form: clearing is scoped to the form the fault + // belongs to, because the counter is per category and the notice names a + // form. + Client_Logger::record_failure( 'notification', $form_id, 'Notification Recovery' ); + + $this->assertArrayHasKey( + 'notification', + Client_Logger::get_open_failures(), + 'Precondition: the fault is standing before the send.' + ); + update_post_meta( $form_id, '_srfm_email_notification', @@ -660,6 +664,151 @@ public function test_send_email_clears_the_notification_fault_on_success() { delete_option( Client_Logger::FAILURES_OPTION ); } + /** + * A different form's notification succeeding must not clear the fault. + * + * send_email() runs on the public submit path and the counter is per category + * rather than per form, so without the scoping an anonymous submission of a + * working form retires a standing fault on a broken one -- repeatable once per + * admin page load, by anyone. The notice names a form, so this is visible. + */ + public function test_send_email_does_not_clear_another_forms_notification_fault() { + delete_option( Client_Logger::FAILURES_OPTION ); + + $broken = wp_insert_post( + [ + 'post_type' => 'sureforms_form', + 'post_title' => 'Broken Form', + 'post_status' => 'publish', + ] + ); + $working = wp_insert_post( + [ + 'post_type' => 'sureforms_form', + 'post_title' => 'Working Form', + 'post_status' => 'publish', + ] + ); + + Client_Logger::record_failure( 'notification', $broken, 'Broken Form' ); + + update_post_meta( + $working, + '_srfm_email_notification', + [ + [ + 'id' => 1, + 'status' => true, + ], + ] + ); + + $parsed = static function () { + return [ + 'to' => 'owner@example.com', + 'subject' => 'New entry', + 'message' => 'An entry arrived.', + 'headers' => [], + ]; + }; + $sent = static function () { + return true; + }; + + add_filter( 'srfm_email_notification', $parsed, 99 ); + add_filter( 'pre_wp_mail', $sent, 99 ); + + Form_Submit::send_email( $working, [] ); + + remove_filter( 'pre_wp_mail', $sent, 99 ); + remove_filter( 'srfm_email_notification', $parsed, 99 ); + + $this->assertArrayHasKey( + 'notification', + Client_Logger::get_open_failures(), + "A working form's success must not retire another form's fault." + ); + + wp_delete_post( $broken, true ); + wp_delete_post( $working, true ); + delete_option( Client_Logger::FAILURES_OPTION ); + } + + /** + * One recipient succeeding while another fails is still a failure. + * + * This is the case the flag is hoisted out of the loop for, and the one + * nothing covered: with the clear inside the loop, the succeeding recipient + * would retire a fault the failing one had just recorded. Two notifications on + * one form, the first delivering and the second not. + */ + public function test_send_email_keeps_the_notification_fault_on_a_mixed_loop() { + delete_option( Client_Logger::FAILURES_OPTION ); + + $form_id = wp_insert_post( + [ + 'post_type' => 'sureforms_form', + 'post_title' => 'Mixed Notifications', + 'post_status' => 'publish', + ] + ); + + Client_Logger::record_failure( 'notification', $form_id, 'Mixed Notifications' ); + + update_post_meta( + $form_id, + '_srfm_email_notification', + [ + [ + 'id' => 1, + 'status' => true, + ], + [ + 'id' => 2, + 'status' => true, + ], + ] + ); + + // First recipient valid, second empty -- and an empty destination fails in + // every environment, where a failing transport does not, because + // send_email() falls back to PHP's mail(). + $calls = 0; + $parsed = static function () use ( &$calls ) { + $calls++; + + return [ + 'to' => 1 === $calls ? 'owner@example.com' : '', + 'subject' => 'New entry', + 'message' => 'An entry arrived.', + 'headers' => [], + ]; + }; + + // Only the first send succeeds; the second falls through to mail( '' ). + $sent = static function () use ( &$calls ) { + return 1 === $calls; + }; + + add_filter( 'srfm_email_notification', $parsed, 99 ); + add_filter( 'pre_wp_mail', $sent, 99 ); + + Form_Submit::send_email( $form_id, [] ); + + remove_filter( 'pre_wp_mail', $sent, 99 ); + remove_filter( 'srfm_email_notification', $parsed, 99 ); + + $this->assertSame( 2, $calls, 'Both notifications must have been attempted.' ); + $this->assertArrayHasKey( + 'notification', + Client_Logger::get_open_failures(), + 'One recipient failing leaves the fault standing, however many succeeded.' + ); + + wp_delete_post( $form_id, true ); + delete_option( Client_Logger::FAILURES_OPTION ); + } + /** * A notification still failing leaves the fault standing. * From b7ffbd7a530749fc7887bb8e7d8f01171c3a89d9 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Thu, 10 Sep 2026 16:26:55 +0530 Subject: [PATCH 42/58] fix: restore focus by hand, and attach the ref that was never attached Two things I stated fixed in the round-2 response and had not verified. Return focus was not missing, it was defeated. force-ui passes returnFocus to FloatingFocusManager as refs.reference -- and its `c?.reference` guard is always truthy, because useFloating() creates that ref object unconditionally -- whose .current is null since no trigger prop is used. Passing an object makes getReturnElement() skip the boolean branch that would have called getPreviouslyFocusedElement(), so the library's own correct default is overridden and every close path lands on . Captured on the trigger's click and restored on a macrotask after unmount instead. The initial-focus fix was dead code: copyRef was declared and read, but never attached to anything, so copyRef.current?.focus() was a permanent silent no-op that the optional chaining hid. force-ui's Button is forwardRef, so it is one attribute. The accessible name landed on the wrong node. There are two role=dialog elements in the portal -- force-ui's outer motion div hardcodes it and floating-ui's useRole() adds it to the inner floating element -- and querySelector returned the outer scroll wrapper, leaving the panel focus is trapped inside unnamed. Both are named now, through a query scoped to dialogRoot, since that id is shared with other dialogs in this codebase. URLs are normalised at the sink as well as the source. srfm_admin_filter runs after get_action_items(), on the already-built localisation payload, so the server-side esc_url_raw() pass is not unconditionally the last word before a value reaches href. safeUrl() lives in its own module so it is testable at all -- importing the component pulls in force-ui's TS source, which Jest cannot transform -- and the Contact Support guard now tests the normalised value, so a rejected scheme drops the anchor rather than rendering one with no href. 22 Jest cases on safeUrl, which is the part of this that can be pinned. The focus behaviour is DOM work and still wants the Playwright case this PR has been carrying as an outstanding item. Co-Authored-By: Claude Opus 5 (1M context) --- src/admin/dashboard/ActionItems.js | 76 +++++++++++++++++++----- src/admin/dashboard/test/safeUrl.test.js | 61 +++++++++++++++++++ src/admin/dashboard/utils/safeUrl.js | 30 ++++++++++ 3 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 src/admin/dashboard/test/safeUrl.test.js create mode 100644 src/admin/dashboard/utils/safeUrl.js diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 5fc4de31f..d28b9cfc3 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -15,6 +15,7 @@ import { Check, Copy, } from 'lucide-react'; +import safeUrl from './utils/safeUrl'; /** * Dashboard panel listing what SureForms has checked on this site. @@ -113,18 +114,25 @@ export default () => { return; } - const node = dialogRoot.querySelector( '[role="dialog"]' ); - - if ( ! node ) { - return; - } - - node.setAttribute( 'aria-labelledby', titleId ); - node.setAttribute( 'aria-describedby', descriptionId ); + // Both of them. There are two role="dialog" nodes in the portal, not one: + // force-ui's outer motion div hardcodes it, and floating-ui's useRole() + // puts it on the inner floating element too. querySelector returns the + // outer scroll wrapper, so naming only that left the panel focus is + // actually trapped inside unnamed. Scoped to dialogRoot because that id is + // shared with other dialogs in this codebase. + dialogRoot + .querySelectorAll( '[role="dialog"]' ) + .forEach( ( node ) => { + node.setAttribute( 'aria-labelledby', titleId ); + node.setAttribute( 'aria-describedby', descriptionId ); + } ); }, [ details, dialogRoot ] ); // Initial focus on Copy details rather than whatever FloatingFocusManager - // reaches first, which is the close cross. + // reaches first, which is the close cross. The ref has to be attached to the + // Button as well as declared -- force-ui's Button is forwardRef and applies it + // to the rendered tag, and without the attribute this was a permanent no-op + // that the optional chaining hid. const copyRef = useRef( null ); useEffect( () => { @@ -137,6 +145,35 @@ export default () => { return () => window.clearTimeout( timer ); }, [ details ] ); + // Finding 1: force-ui always passes returnFocus to FloatingFocusManager as + // `refs.reference` -- a ref object that always exists, so its `c?.reference` + // guard is always truthy -- whose `.current` is null because no trigger prop + // is used. FloatingFocusManager's getReturnElement() only takes its correct + // `getPreviouslyFocusedElement()` path when returnFocus is a boolean, so + // passing the object defeats the library's own default and every close path + // drops focus to . Restored by hand instead. + const openerRef = useRef( null ); + + useEffect( () => { + if ( details ) { + return; + } + + const opener = openerRef.current; + + if ( ! opener || typeof opener.focus !== 'function' ) { + return; + } + + openerRef.current = null; + + // After the library has finished its own restore attempt on unmount, + // otherwise it lands on the detached fallback span afterwards. + const timer = window.setTimeout( () => opener.focus(), 0 ); + + return () => window.clearTimeout( timer ); + }, [ details ] ); + // Low: an uncleared label timer crosses itself on two copies inside 2s, and // leaves a setState pending on a possibly-unmounted tree when the dialog is // closed inside that window. @@ -192,7 +229,7 @@ export default () => { { name: item.guide_action, label: item.guide_label, - url: item.guide_url, + url: safeUrl( item.guide_url ), external: true, }, ] @@ -202,7 +239,7 @@ export default () => { { name: item.cta_action, label: item.cta_label, - url: item.cta_url, + url: safeUrl( item.cta_url ), // An item carrying details opens them here rather than // navigating: the point is to read the diagnostics before // sending them anywhere. cta_url stays as the fallback the @@ -383,7 +420,15 @@ export default () => { `${ item.id }-${ index }` } type="button" - onClick={ () => { + onClick={ ( event ) => { + // Captured before the dialog + // mounts: force-ui defeats + // FloatingFocusManager's own + // return-focus, so nothing + // else remembers this. + openerRef.current = + event.currentTarget; + // Only when the server named // one: an unnamed action posts // "undefined" and 400s. @@ -556,6 +601,7 @@ export default () => {
> $items Action items. @@ -3731,7 +3800,7 @@ public function get_action_items() { continue; } - foreach ( [ 'cta_url', 'guide_url', 'support_url' ] as $key ) { + foreach ( [ 'cta_url', 'guide_url' ] as $key ) { if ( ! isset( $item[ $key ] ) ) { continue; } @@ -3855,11 +3924,10 @@ public function enqueue_action_item_styles() { $css = <<<'CSS' .srfm-action-item-carousel { position: relative; } .srfm-action-item-carousel .srfm-action-item-notice { padding-inline-end: var(--srfm-carousel-reserve, 130px); } -/* [hidden] is only a UA rule. WordPress sets display on .notice, and a third-party - admin sheet doing something like `.notice div { display: block }` would put the - raw diagnostics on screen inside the notice. */ +/* [hidden] is only a UA rule, and WordPress sets display on .notice, so a + third-party admin sheet can otherwise put a notice the carousel has hidden back + on screen. */ .srfm-action-item-carousel .srfm-action-item-notice[hidden] { display: none; } -.srfm-notice-details { display: none; } .srfm-action-item-carousel-nav { position: absolute; top: 8px; @@ -4006,6 +4074,11 @@ private function get_details_dialog_labels() { // The scrollable diagnostics block is focusable, so it needs a name of // its own. 'logRegion' => __( 'Recorded diagnostics', 'sureforms' ), + // The dialog opens before its payload arrives -- see + // handle_action_item_details() for why the report is not shipped with + // the page. + 'loading' => __( 'Collecting the details…', 'sureforms' ), + 'unavailable' => __( 'We could not collect the details. Contact Support and describe what happened, and we will take it from there.', 'sureforms' ), ]; } @@ -4087,14 +4160,21 @@ private function get_first_party_action_items() { // the panel's dialog. The dashboard is where the details are readable. 'cta_url' => admin_url( 'admin.php?page=sureforms_menu' ), 'cta_action' => 'view_details', - // The dialog's own contents. Plain text: it is rendered inside a
-				// and copied verbatim, so any markup here would be read as characters.
-				// A wider excerpt than a URL could ever have carried: this is read on
-				// screen and copied to a clipboard, neither of which has a length
-				// limit worth designing around.
-				'details'     => $this->get_support_message( $category, $form_title )
-					. "\n\n" . $this->get_support_log_block( 8000 ),
-				'support_url' => $this->get_support_contact_url( $category ),
+				// Not the payload itself, only that one exists. The diagnostics are
+				// fetched when the dialog opens -- see handle_action_item_details().
+				//
+				// They used to ride along in the localisation JSON and in a hidden
+				// div on every admin page. The content is authored by whoever
+				// triggered the failure, and the client-error-log route is a public
+				// endpoint gated on a submit token rather than a capability, so an
+				// anonymous visitor can fill that excerpt. Broadcasting it to every
+				// admin screen -- read or not -- put attacker-authored text in page
+				// source site-wide and made any future escaping slip a
+				// manage_options-context problem. On demand, it reaches only the
+				// admin who asked for it.
+				'has_details' => true,
+				// Which record to fetch. Not the payload, just the key.
+				'category'    => $category,
 				'dismissible' => false,
 			];
 
diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js
index a0e66ba7e..a9078131d 100644
--- a/admin/assets/js/notice-response.js
+++ b/admin/assets/js/notice-response.js
@@ -420,18 +420,23 @@
 	 * contains whatever a server or a browser put in an error message, and that is
 	 * not markup to be trusted.
 	 *
-	 * @param {Object} config            Dialog contents.
-	 * @param {string} config.noticeId   Item id, used as the analytics key.
-	 * @param {string} config.text       Plain-text details to show and copy.
-	 * @param {string} config.supportUrl Where Contact Support goes. Empty means
-	 *                                   there is nowhere to send them, so the
-	 *                                   button is not rendered at all.
-	 * @return {boolean} Whether the dialog opened.
+	 * @param {Object}  config            Dialog contents.
+	 * @param {string}  config.noticeId   Item id, used as the analytics key.
+	 * @param {string}  config.text       Plain-text details to show and copy.
+	 * @param {string}  config.supportUrl Where Contact Support goes. Empty means
+	 *                                    there is nowhere to send them, so the
+	 *                                    button is not reachable at all.
+	 * @param {boolean} config.pending    Whether `text` is a placeholder that a
+	 *                                    later setText() will replace.
+	 * @return {Object|false} Handle with setText()/setSupportUrl(), or false if
+	 *                        there was nothing to open.
 	 */
 	function showDetails( config ) {
 		const noticeId = ( config && config.noticeId ) || '';
-		const text = ( config && config.text ) || '';
-		const supportUrl = ( config && config.supportUrl ) || '';
+		const pending = !! ( config && config.pending );
+
+		let text = ( config && config.text ) || '';
+		let supportUrl = ( config && config.supportUrl ) || '';
 
 		if ( ! text ) {
 			return false;
@@ -489,6 +494,13 @@
 		pre.setAttribute( 'role', 'region' );
 		pre.setAttribute( 'aria-label', labels.logRegion || 'Diagnostics' );
 
+		if ( pending ) {
+			// Announced as busy rather than silently showing a placeholder, so a
+			// screen reader says the region is still filling instead of reading
+			// "Loading" as if it were the report.
+			pre.setAttribute( 'aria-busy', 'true' );
+		}
+
 		const actions = document.createElement( 'p' );
 		actions.className = 'srfm-details-actions';
 
@@ -518,25 +530,20 @@
 		// an  does nothing at all -- it still navigates -- so removing the
 		// destination is what actually locks it.
 		//
-		// Not rendered at all without a destination. An empty href resolves to the
-		// current document, so the click used to open a duplicate of the admin page
-		// and still acknowledge the failure -- standing the notice down without
-		// anything having been reported. Reachable through srfm_action_items for an
-		// item carrying details but no support_url.
-		const contact = supportUrl ? document.createElement( 'a' ) : null;
-
-		if ( contact ) {
-			contact.className = 'button button-primary srfm-details-contact';
-			contact.target = '_blank';
-			contact.rel = 'noopener noreferrer';
-			contact.textContent = labels.contact || 'Contact Support';
-		}
+		// Hidden, not merely href-less, until there is a destination. An empty href
+		// resolves to the current document, so the click used to open a duplicate
+		// of the admin page and still acknowledge the failure -- standing the
+		// notice down without anything having been reported. The destination now
+		// arrives with the fetched payload, so the element is built up front and
+		// revealed by setSupportUrl().
+		const contact = document.createElement( 'a' );
+		contact.className = 'button button-primary srfm-details-contact';
+		contact.target = '_blank';
+		contact.rel = 'noopener noreferrer';
+		contact.textContent = labels.contact || 'Contact Support';
+		contact.hidden = ! supportUrl;
 
 		function lockContact() {
-			if ( ! contact ) {
-				return;
-			}
-
 			// The dimming and the pointer-events block hang off aria-disabled in the
 			// stylesheet, so the state is declared once rather than in two places.
 			contact.removeAttribute( 'href' );
@@ -544,7 +551,7 @@
 		}
 
 		function unlockContact() {
-			if ( ! contact ) {
+			if ( ! supportUrl ) {
 				return;
 			}
 
@@ -562,6 +569,10 @@
 			lockContact();
 		}
 
+		// Nothing to copy yet. Left focusable-but-disabled rather than hidden, so
+		// the control does not appear from nowhere once the payload lands.
+		copy.disabled = pending;
+
 		const close = document.createElement( 'button' );
 		close.type = 'button';
 		close.className = 'button-link srfm-details-close';
@@ -586,7 +597,12 @@
 		// walks into the admin bar, the admin menu and the links behind the overlay.
 		function focusables() {
 			return [ pre, close, copy, contact ].filter( function ( el ) {
-				return el && ! el.hasAttribute( 'aria-disabled' );
+				return (
+					el &&
+					! el.hidden &&
+					! el.disabled &&
+					! el.hasAttribute( 'aria-disabled' )
+				);
 			} );
 		}
 
@@ -647,14 +663,12 @@
 			);
 		} );
 
-		if ( contact ) {
-			contact.addEventListener( 'click', function () {
-				sendResponse( noticeId, 'contact_support' );
-				// The form opens in its own tab, so the dialog has nothing left to
-				// show.
-				dismiss();
-			} );
-		}
+		contact.addEventListener( 'click', function () {
+			sendResponse( noticeId, 'contact_support' );
+			// The form opens in its own tab, so the dialog has nothing left to
+			// show.
+			dismiss();
+		} );
 
 		close.addEventListener( 'click', dismiss );
 		overlay.addEventListener( 'click', function ( e ) {
@@ -670,9 +684,7 @@
 		actions.appendChild( close );
 		actions.appendChild( copy );
 
-		if ( contact ) {
-			actions.appendChild( contact );
-		}
+		actions.appendChild( contact );
 
 		panel.appendChild( heading );
 		panel.appendChild( description );
@@ -684,50 +696,127 @@
 		// The page behind must not scroll under the backdrop.
 		document.body.style.overflow = 'hidden';
 
-		copy.focus();
+		// The diagnostics region while the payload is still coming: it carries the
+		// placeholder, so focusing it says what the dialog is doing. Once there is
+		// something to copy, the copy button is the first thing anyone wants.
+		( copy.disabled ? pre : copy ).focus();
+
+		return {
+			/**
+			 * Replace the placeholder once the payload arrives.
+			 *
+			 * @param {string}  next     The text to show and copy.
+			 * @param {boolean} copyable Whether it is worth copying. False for an
+			 *                           error message, which would otherwise unlock
+			 *                           Contact Support on the strength of having
+			 *                           copied the error.
+			 * @return {void}
+			 */
+			setText( next, copyable ) {
+				text = next || '';
+				pre.textContent = text;
+				pre.removeAttribute( 'aria-busy' );
+
+				if ( false === copyable ) {
+					// No report to paste, so the copy-first gate has nothing to gate
+					// on. Contact Support is the only route that retires the notice,
+					// and these items are not dismissible -- leaving it locked would
+					// be an undismissable notice with no working action on it.
+					copy.hidden = true;
+					hint.textContent = '';
+					unlockContact();
+					return;
+				}
 
-		return true;
+				copy.disabled = ! text;
+			},
+
+			/**
+			 * Point Contact Support somewhere, once the payload names it.
+			 *
+			 * @param {string} next Destination. Empty keeps the button out of the
+			 *                      dialog entirely.
+			 * @return {void}
+			 */
+			setSupportUrl( next ) {
+				supportUrl = next || '';
+				contact.hidden = ! supportUrl;
+
+				if ( supportUrl && ! contact.hasAttribute( 'aria-disabled' ) ) {
+					contact.href = supportUrl;
+				}
+			},
+
+			dismiss,
+		};
 	}
 
 	/**
-	 * Open the dialog for one classic notice, from the payload beside it.
+	 * Open the dialog for one classic notice and fetch its payload.
+	 *
+	 * Fetched on open rather than printed beside every notice. The diagnostics are
+	 * written by a public REST route, so shipping them with the page put
+	 * attacker-authored text on every admin screen whether or not anyone opened
+	 * the dialog.
 	 *
-	 * The text is already in the page, hidden next to its notice, so opening this
-	 * makes no request -- a modal that has to fetch can fail at the exact moment
-	 * someone is trying to report a failure.
+	 * The dialog opens first, with a placeholder, and fills in when the response
+	 * lands -- a surface whose whole job is reporting a failure must not be a
+	 * button that does nothing until the network answers. A failed fetch says so
+	 * in place and still offers Contact Support, which is the only action that
+	 * retires the notice.
 	 *
-	 * @param {string} noticeId Item id, matched against the hidden payload's
-	 *                          data-srfm-details-id.
+	 * @param {string} noticeId Item id, used as the analytics key.
+	 * @param {string} category Failure category, sent to the handler.
 	 * @return {boolean} Whether the payload was found and the dialog opened.
 	 */
-	function openDetails( noticeId ) {
-		// Escaped: the id comes from the items array, which srfm_action_items can
-		// contribute to, and an unescaped quote here throws a SyntaxError out of
-		// the click handler.
-		const selector =
-			'.srfm-notice-details[data-srfm-details-id="' +
-			( window.CSS && window.CSS.escape
-				? window.CSS.escape( noticeId )
-				: noticeId ) +
-			'"]';
-
-		let source = null;
-
-		try {
-			source = document.querySelector( selector );
-		} catch ( e ) {
-			source = null;
-		}
-
-		if ( ! source ) {
+	function openDetails( noticeId, category ) {
+		if ( typeof srfmNoticeResponse === 'undefined' || ! category ) {
 			return false;
 		}
 
-		return showDetails( {
+		const labels = srfmNoticeResponse.details || {};
+
+		const dialog = showDetails( {
 			noticeId,
-			text: source.textContent || '',
-			supportUrl: source.getAttribute( 'data-srfm-support-url' ) || '',
+			text: labels.loading || 'Loading…',
+			supportUrl: '',
+			pending: true,
 		} );
+
+		if ( ! dialog ) {
+			return false;
+		}
+
+		const body = new FormData();
+		body.append( 'action', 'srfm_action_item_details' );
+		body.append( 'nonce', srfmNoticeResponse.detailsNonce );
+		body.append( 'category', category );
+
+		fetch( srfmNoticeResponse.ajaxurl, { method: 'POST', body } )
+			.then( function ( response ) {
+				return response.json();
+			} )
+			.then( function ( json ) {
+				if ( ! json || ! json.success || ! json.data ) {
+					throw new Error( 'unavailable' );
+				}
+
+				dialog.setText( json.data.details || '' );
+				dialog.setSupportUrl( json.data.support_url || '' );
+			} )
+			.catch( function () {
+				// The report is built server-side, so there is nothing to show in
+				// its place. Say so, and fall back to the untagged support form:
+				// Contact Support is the only action that retires these notices, so
+				// a failed fetch must not also take the way out with it.
+				//
+				// Destination set before the message, because the message releases
+				// the copy-first lock and that release needs somewhere to point.
+				dialog.setSupportUrl( srfmNoticeResponse.supportUrl || '' );
+				dialog.setText( labels.unavailable || '', false );
+			} );
+
+		return true;
 	}
 
 	document.addEventListener( 'click', function ( e ) {
@@ -743,10 +832,15 @@
 			return;
 		}
 
-		// Only swallow the navigation if the dialog actually opened. If the payload
-		// is missing the href still goes to the dashboard, which is where the same
-		// details are readable.
-		if ( openDetails( trigger.getAttribute( 'data-srfm-details-for' ) ) ) {
+		// Only swallow the navigation if the dialog actually opened. If it cannot,
+		// the href still goes to the dashboard, which is where the same details are
+		// readable.
+		if (
+			openDetails(
+				trigger.getAttribute( 'data-srfm-details-for' ),
+				trigger.getAttribute( 'data-srfm-category' )
+			)
+		) {
 			e.preventDefault();
 		}
 	} );
diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js
index b873a13e2..7d7f54900 100644
--- a/src/admin/dashboard/ActionItems.js
+++ b/src/admin/dashboard/ActionItems.js
@@ -179,6 +179,9 @@ export default () => {
 	// closed inside that window.
 	const revertRef = useRef( 0 );
 
+	// Bumped per open and per close; a response carrying a stale token is dropped.
+	const requestRef = useRef( 0 );
+
 	useEffect( () => () => window.clearTimeout( revertRef.current ), [] );
 
 	const items = ( srfm_admin?.action_items || [] ).filter(
@@ -242,9 +245,9 @@ export default () => {
 					url: safeUrl( item.cta_url ),
 					// An item carrying details opens them here rather than
 					// navigating: the point is to read the diagnostics before
-					// sending them anywhere. cta_url stays as the fallback the
-					// classic wp-admin notice uses, since it cannot open a dialog.
-					dialog: !! item.details,
+					// sending them anywhere. cta_url stays as the fallback for a
+					// browser with no JavaScript, which cannot open a dialog.
+					dialog: !! item.has_details,
 					// A mailto must reach the mail client, not a browser tab.
 					// srfm_action_items is public, so one can still arrive that way.
 					// Matched case-insensitively: a MAILTO: from the filter would
@@ -317,7 +320,100 @@ export default () => {
 		}
 	};
 
-	const closeDetails = () => setDetails( null );
+	const closeDetails = () => {
+		// Invalidates any fetch still in flight, so a slow response cannot refill a
+		// dialog the user has already closed.
+		requestRef.current += 1;
+		setDetails( null );
+	};
+
+	// Fetched on open rather than localised with the page. The diagnostics are
+	// written through a public REST route, so shipping them in srfm_admin put
+	// attacker-authored text into every admin screen's HTML whether or not anyone
+	// opened this dialog. See Admin::handle_action_item_details().
+	//
+	// The dialog opens immediately with a placeholder and fills in when the
+	// response lands. A surface whose whole job is reporting a failure must not be
+	// a button that does nothing until the network answers.
+	const openDetails = ( item ) => {
+		// Guards against interleaving: open A, close it, open B, and A's response
+		// would otherwise land in B's dialog.
+		const token = ++requestRef.current;
+
+		setDetails( {
+			...item,
+			details: dialogLabels.loading || '',
+			support_url: '',
+			pending: true,
+		} );
+
+		const fail = () => {
+			// No report to paste, so the copy-first gate has nothing to gate on.
+			// Contact Support is the only action that retires these notices, and
+			// they are not dismissible -- leaving it locked would be an
+			// undismissable notice with no working action on it.
+			setCopiedOnce( true );
+			setHint( '' );
+
+			setDetails( ( prev ) =>
+				prev
+					? {
+						...prev,
+						details: dialogLabels.unavailable || '',
+						// Untagged fallback: Contact Support is the only action
+						// that retires these notices, so a failed fetch must not
+						// take the way out with it.
+						support_url: srfm_admin?.support_url || '',
+						pending: false,
+						failed: true,
+					  }
+					: prev
+			);
+		};
+
+		const ajaxUrl = srfm_admin?.ajax_url;
+		const nonce = srfm_admin?.action_item_details_nonce;
+
+		if ( ! ajaxUrl || ! nonce ) {
+			fail();
+			return;
+		}
+
+		const body = new FormData();
+		body.append( 'action', 'srfm_action_item_details' );
+		body.append( 'nonce', nonce );
+		body.append( 'category', item.category || '' );
+
+		fetch( ajaxUrl, { method: 'POST', credentials: 'same-origin', body } )
+			.then( ( response ) => response.json() )
+			.then( ( json ) => {
+				if ( ! json?.success || ! json?.data ) {
+					throw new Error( 'unavailable' );
+				}
+
+				if ( token !== requestRef.current ) {
+					return;
+				}
+
+				setDetails( ( prev ) =>
+					prev
+						? {
+							...prev,
+							details: json.data.details || '',
+							support_url: json.data.support_url || '',
+							pending: false,
+						  }
+						: prev
+				);
+			} )
+			.catch( () => {
+				if ( token !== requestRef.current ) {
+					return;
+				}
+
+				fail();
+			} );
+	};
 
 	// Opens the dialog rather than navigating: the details are read here before
 	// anything is sent. Nothing to download, nothing to intercept.
@@ -460,7 +556,7 @@ export default () => {
 															: dialogLabels.copyFailed ||
 																	''
 													);
-													setDetails( item );
+													openDetails( item );
 												} }
 												className={ `bg-transparent border-0 p-0 cursor-pointer text-xs font-medium no-underline hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-1${
 													index > 0
@@ -545,7 +641,8 @@ export default () => {
 							
 								
- { dialogLabels.title || __( 'Details', 'sureforms' ) } + { dialogLabels.title || + __( 'Details', 'sureforms' ) } { /* force-ui hardcodes an untranslated aria-label="Close dialog"; it sits before the prop @@ -554,7 +651,7 @@ export default () => { onClick={ closeDetails } aria-label={ dialogLabels.close || - __( 'Close', 'sureforms' ) + __( 'Close', 'sureforms' ) } />
@@ -602,27 +699,39 @@ export default () => { { hint || ' ' }
- + { /* Nothing to copy before the payload lands, and + nothing worth copying if it never did -- an + error message on the clipboard is not a + report. */ } + { ! details?.failed && ( + + ) } { /* Not rendered at all without a destination. An empty href resolves to the current document, so the click would open a duplicate of this page and still @@ -635,40 +744,50 @@ export default () => { on an does nothing at all, so the href only exists once the copy has been made. */ } { !! safeUrl( details?.support_url ) && - ( copiedOnce ? ( - - ) : ( - - ) ) } + ( copiedOnce ? ( + + ) : ( + + ) ) }
diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 2c260a3fd..ac27af50a 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -1189,9 +1189,19 @@ public function test_get_action_items_offers_a_guide_for_notification_failures() // The details action is still there; the guide is an addition, not a swap. $this->assertSame( 'view_details', $items['notification_error']['cta_action'] ?? '' ); - $this->assertNotEmpty( - $items['notification_error']['details'] ?? '', - 'The dialog has nothing to show without the details payload.' + $this->assertTrue( + $items['notification_error']['has_details'] ?? false, + 'Without this flag neither renderer offers the dialog at all.' + ); + $this->assertSame( + 'notification', + $items['notification_error']['category'] ?? '', + 'The category is what the dialog sends to fetch the report.' + ); + $this->assertArrayNotHasKey( + 'details', + $items['notification_error'], + 'The diagnostics are attacker-authored; they must not ride along with the page.' ); // A category with no guide must not carry empty guide keys, which both @@ -1320,50 +1330,184 @@ public function test_support_copy_describes_the_failure_that_happened() { } /** - * The rendered details payload keeps its line breaks. + * The served details payload keeps its line breaks. * * The text is several lines of diagnostics and a fenced log; one long line is - * unreadable and useless to paste. It reaches the page through esc_html(), - * which preserves \n verbatim -- unlike esc_url(), which strips %0a from every - * scheme but mailto: and is what flattened the body when this was carried in a - * compose URL. + * unreadable and useless to paste. Nothing in the JSON path should touch them, + * but the earlier mailto: version was flattened by esc_url() stripping %0a from + * every scheme but mailto:, so the breaks are worth asserting on wherever the + * text is assembled. */ - public function test_render_action_item_notices_keeps_the_details_line_breaks() { + public function test_handle_action_item_details_keeps_the_line_breaks() { wp_set_current_user( $this->make_user( 'administrator' ) ); delete_option( Client_Logger::FAILURES_OPTION ); Client_Logger::clear(); Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); Admin::reset_action_items_cache(); - global $pagenow; - $previous_pagenow = $pagenow; - $pagenow = 'plugins.php'; - - ob_start(); - Admin::get_instance()->render_action_item_notices(); - $html = (string) ob_get_clean(); - - $pagenow = $previous_pagenow; - - // The div's own contents, not the rest of the output: the template emits - // newlines of its own around it, so measuring from here to end-of-output - // passed even when the payload itself was a single line. - $this->assertSame( - 1, - preg_match( '#]*class="srfm-notice-details"[^>]*>(.*?)
#s', $html, $matches ), - 'The hidden payload must render.' + $payload = $this->request_action_item_details( + [ + 'category' => 'notification', + 'nonce' => wp_create_nonce( 'srfm_action_item_details' ), + ] ); - $payload = $matches[1]; + $this->assertTrue( $payload['success'] ?? false ); + + $details = (string) ( $payload['data']['details'] ?? '' ); - $this->assertStringNotContainsString( "\r", $payload, 'Bare LF only.' ); + $this->assertStringNotContainsString( "\r", $details, 'Bare LF only.' ); $this->assertGreaterThan( 5, - substr_count( $payload, "\n" ), + substr_count( $details, "\n" ), 'The diagnostics block is several lines; one long line means the breaks were lost.' ); } + /** + * The details endpoint is gated, in the order the sibling handlers use. + * + * It hands back the client error log, which is filled through a public REST + * route gated on a submit token any visitor can obtain -- so this is the boundary + * between attacker-authored text and an administrator's screen. + */ + public function test_handle_action_item_details_refuses_everything_but_a_real_request() { + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::clear(); + Client_Logger::record_failure( 'notification', 42, 'Contact Form' ); + Admin::reset_action_items_cache(); + + // No capability. The nonce is valid, so only the capability check stands + // between a subscriber and the log. + wp_set_current_user( $this->make_user( 'subscriber' ) ); + $payload = $this->request_action_item_details( + [ + 'category' => 'notification', + 'nonce' => wp_create_nonce( 'srfm_action_item_details' ), + ] + ); + $this->assertFalse( $payload['success'] ?? true, 'A subscriber must not read the log.' ); + + wp_set_current_user( $this->make_user( 'administrator' ) ); + + // Minted after the switch: a nonce is tied to the user who created it, so + // one made earlier in this test would fail for the wrong reason. + $valid_nonce = wp_create_nonce( 'srfm_action_item_details' ); + + // Absent nonce, which is the shape a guard that only checks a supplied + // value would wave through. + $payload = $this->request_action_item_details( [ 'category' => 'notification' ] ); + $this->assertFalse( $payload['success'] ?? true, 'A missing nonce must not pass.' ); + + // Wrong action's nonce: this endpoint has its own, and the notice-response + // one is already in the page for every admin. + $payload = $this->request_action_item_details( + [ + 'category' => 'notification', + 'nonce' => wp_create_nonce( 'srfm_notice_response' ), + ] + ); + $this->assertFalse( $payload['success'] ?? true, 'Another action\'s nonce must not pass.' ); + + // Absent, empty, unrecognised, and recognised-but-nothing-wrong. All four + // are refused by the same check -- get_open_failures() only ever holds + // categories that are both known and currently failing. + foreach ( [ null, '', 'anything_else', 'submission' ] as $category ) { + $request = [ 'nonce' => $valid_nonce ]; + + if ( null !== $category ) { + $request['category'] = $category; + } + + $payload = $this->request_action_item_details( $request ); + $this->assertFalse( + $payload['success'] ?? true, + 'Nothing open in "' . (string) $category . '" means there is no report to serve.' + ); + } + + // An array where a string belongs, which arrives simply by writing + // category[]= in the body. Pinned because the refusal depends on + // sanitize_key() flattening a non-scalar to '' rather than on any branch + // here, and a future rewrite reaching for $_POST directly would lose it. + $payload = $this->request_action_item_details( + [ + 'category' => [ 'notification' ], + 'nonce' => $valid_nonce, + ] + ); + $this->assertFalse( $payload['success'] ?? true, 'A non-string category must be refused, not fatal.' ); + + // The happy path, so the refusals above are not passing because the + // endpoint refuses everything. + $payload = $this->request_action_item_details( + [ + 'category' => 'notification', + 'nonce' => $valid_nonce, + ] + ); + $this->assertTrue( $payload['success'] ?? false ); + $this->assertNotEmpty( $payload['data']['details'] ?? '' ); + $this->assertStringStartsWith( + 'https://sureforms.com/form/troubleshooting-form/', + $payload['data']['support_url'] ?? '', + 'The dialog gets its Contact Support destination from here too.' + ); + } + + /** + * Call the details endpoint and decode what it sent. + * + * wp_send_json_* ends in a bare die() unless wp_doing_ajax() is true, which no + * filter can intercept -- so without this the runner dies mid-class and every + * later test in the file silently never runs. + * + * @param array $post Request body. + * @return array Decoded response. + */ + private function request_action_item_details( $post ) { + $previous_post = $_POST; + $previous_request = $_REQUEST; + + // Both: check_ajax_referer() reads the nonce out of $_REQUEST, not $_POST, + // so setting only one silently tests the missing-nonce path. + $_POST = $post; + $_REQUEST = $post; + + add_filter( 'wp_doing_ajax', '__return_true' ); + + $handler = static function () { + return static function () { + throw new \WPDieException( 'srfm-json-sent' ); + }; + }; + + add_filter( 'wp_die_ajax_handler', $handler ); + add_filter( 'wp_die_handler', $handler ); + + ob_start(); + + try { + Admin::get_instance()->handle_action_item_details(); + } catch ( \WPDieException $e ) { + // Expected: this is how wp_send_json_* returns. + unset( $e ); + } + + $json = (string) ob_get_clean(); + + remove_filter( 'wp_die_ajax_handler', $handler ); + remove_filter( 'wp_die_handler', $handler ); + remove_filter( 'wp_doing_ajax', '__return_true' ); + + $_POST = $previous_post; + $_REQUEST = $previous_request; + + $decoded = json_decode( $json, true ); + + return is_array( $decoded ) ? $decoded : []; + } + /** * A long log is cut from the oldest end, so the newest entries survive. * @@ -1609,7 +1753,6 @@ public function test_enqueue_action_item_styles() { $css = implode( "\n", (array) wp_styles()->get_data( 'srfm-action-items', 'after' ) ); // The rule whose absence during the head-to-footer window was the bug. - $this->assertStringContainsString( '.srfm-notice-details { display: none; }', $css ); $this->assertStringContainsString( 'srfm-action-item-notice[hidden]', $css ); // Logical properties, so an RTL sheet can override rather than fight it. $this->assertStringContainsString( 'inset-inline-end', $css ); @@ -2258,7 +2401,16 @@ public function test_render_action_item_notices() { // Contact Support moved into the details modal; the notice itself offers // the diagnostics first. $this->assertStringContainsString( 'View details', $output ); - $this->assertStringContainsString( 'srfm-notice-details', $output, 'The modal payload must ship with the notice.' ); + $this->assertStringContainsString( + 'data-srfm-category="submission"', + $output, + 'The trigger carries the category the dialog fetches with.' + ); + $this->assertStringNotContainsString( + 'srfm-notice-details', + $output, + 'The diagnostics are fetched on open, never printed beside the notice.' + ); } } From 7f8a37cf859b79f44585a18ac3d84756e8d18d94 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Fri, 11 Sep 2026 15:23:08 +0530 Subject: [PATCH 45/58] =?UTF-8?q?fix:=20address=20round=205=20=E2=80=94=20?= =?UTF-8?q?support=5Furl=20fallback,=20safeUrl=20bypasses,=20aria-busy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **The success path could hand back less than the failure path.** `.catch()` fell back to the localised support URL; the success path was `json.data.support_url || ''`. Fixed at the source rather than at the two call sites: `get_support_contact_url()` now never returns ''. Two things were wrong there. `esc_url_raw( $url, [ 'http', 'https' ] )` dropped `mailto:`, which is a legitimate destination for a white-label support contact and which `get_action_items()` already allows on the sibling item URLs -- so `mailto` is now in the allowlist. And any filter value that still cannot survive escaping falls back to the unfiltered SureForms URL, which is built here rather than supplied and so always escapes. These notices are `dismissible => false` and Contact Support is the only action that retires them, so '' there is an undismissable notice with nothing on it that works. Both clients also fall back to the localised URL on success, so neither depends on a server invariant it cannot see, and both drop `target="_blank"` for a `mailto:` -- it has no document to open and would leave a blank tab behind. **safeUrl() was checking a spelling the DOM never sees.** A browser trims C0 controls and spaces off both ends and strips tab, LF and CR from anywhere in the string before parsing the scheme, so " javascript:" and "java\tscript:" both failed to match `^[a-z]` and were returned untouched. It now applies that normalisation first and returns the normalised string rather than the input -- validating one spelling and handing back another is how a scheme check gets walked past. Protocol-relative "//" and its "\\" spelling are rejected too: they carry no scheme to check and resolve to another origin, so the schemeless-is-relative branch must not claim them. 12 new Jest cases, all failing on revert. **Smaller:** the vanilla dialog's setters now no-op once their overlay is detached; `aria-busy` is on the dashboard's diagnostics region as well as the classic one; `Previously reported:` is wrapped in __() -- its value already goes through wp_date(), so the line was mixed-locale either way. POT regeneration deliberately excluded, per Vansh. Co-Authored-By: Claude Opus 5 (1M context) --- admin/admin.php | 22 ++++++++++--- admin/assets/js/notice-response.js | 34 +++++++++++++++++-- src/admin/dashboard/ActionItems.js | 32 ++++++++++++++++-- src/admin/dashboard/test/safeUrl.test.js | 42 ++++++++++++++++++++++++ src/admin/dashboard/utils/safeUrl.js | 37 ++++++++++++++++++--- tests/unit/admin/test-admin.php | 37 +++++++++++++++++---- 6 files changed, 182 insertions(+), 22 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 2280e8883..8cfc49825 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -4948,11 +4948,21 @@ private function get_support_contact_url( $category ) { * @param string $url Contact form URL, already UTM-tagged. * @param string $category The failure being reported. */ - $url = Helper::get_string_value( apply_filters( 'srfm_support_contact_url', $url, $category ) ); + $filtered = Helper::get_string_value( apply_filters( 'srfm_support_contact_url', $url, $category ) ); // Escaped after the filter, not before: the point of escaping here is that - // neither renderer has to trust what comes back. - return esc_url_raw( $url, [ 'http', 'https' ] ); + // neither renderer has to trust what comes back. mailto: is allowed because + // an inbox is a legitimate destination for a white-label support contact, + // and get_action_items() already allows it on the sibling item URLs. + $safe = esc_url_raw( $filtered, [ 'http', 'https', 'mailto' ] ); + + // Never empty. Contact Support is the only action that retires these + // notices and they are dismissible => false, so returning '' for a filter + // value that cannot survive escaping leaves an undismissable notice with + // nothing on it that works. Falling back to SureForms' own form is worse + // for a white-label than their own URL and better than a dead end, and the + // unfiltered URL is built here rather than supplied, so it always escapes. + return '' !== $safe ? $safe : esc_url_raw( $url, [ 'http', 'https' ] ); } /** @@ -5197,7 +5207,11 @@ private function get_support_message( $category = '', $form_title = '' ) { $acked_at = Helper::get_integer_value( $failures[ $category ]['acked_at'] ?? 0 ); if ( $acked_at > 0 ) { - $lines[] = 'Previously reported: ' . Helper::get_string_value( wp_date( 'Y-m-d H:i T', $acked_at ) ); + $lines[] = sprintf( + /* translators: %s: date and time of the previous report, in the site's timezone. */ + __( 'Previously reported: %s', 'sureforms' ), + Helper::get_string_value( wp_date( 'Y-m-d H:i T', $acked_at ) ) + ); } return implode( "\n", $lines ); diff --git a/admin/assets/js/notice-response.js b/admin/assets/js/notice-response.js index a9078131d..88c5fb900 100644 --- a/admin/assets/js/notice-response.js +++ b/admin/assets/js/notice-response.js @@ -538,8 +538,6 @@ // revealed by setSupportUrl(). const contact = document.createElement( 'a' ); contact.className = 'button button-primary srfm-details-contact'; - contact.target = '_blank'; - contact.rel = 'noopener noreferrer'; contact.textContent = labels.contact || 'Contact Support'; contact.hidden = ! supportUrl; @@ -713,6 +711,13 @@ * @return {void} */ setText( next, copyable ) { + // Dropped once this dialog is gone. Open, close, open again inside + // one round trip and the first response would otherwise write into + // the second dialog -- or into a detached node, silently. + if ( ! overlay.isConnected ) { + return; + } + text = next || ''; pre.textContent = text; pre.removeAttribute( 'aria-busy' ); @@ -739,9 +744,24 @@ * @return {void} */ setSupportUrl( next ) { + if ( ! overlay.isConnected ) { + return; + } + supportUrl = next || ''; contact.hidden = ! supportUrl; + // A mailto: has no document to open, so _blank would leave a blank + // tab behind. srfm_support_contact_url can return one, and the + // notice's own links already match on this. + if ( /^mailto:/i.test( supportUrl ) ) { + contact.removeAttribute( 'target' ); + contact.removeAttribute( 'rel' ); + } else { + contact.target = '_blank'; + contact.rel = 'noopener noreferrer'; + } + if ( supportUrl && ! contact.hasAttribute( 'aria-disabled' ) ) { contact.href = supportUrl; } @@ -801,8 +821,16 @@ throw new Error( 'unavailable' ); } + // Same fallback as the failure path below. A successful fetch must + // not end up with less to act on than a failed one: these notices + // are not dismissible and Contact Support is the only thing that + // retires them, so an empty destination here is a dead end. + dialog.setSupportUrl( + json.data.support_url || + srfmNoticeResponse.supportUrl || + '' + ); dialog.setText( json.data.details || '' ); - dialog.setSupportUrl( json.data.support_url || '' ); } ) .catch( function () { // The report is built server-side, so there is nothing to show in diff --git a/src/admin/dashboard/ActionItems.js b/src/admin/dashboard/ActionItems.js index 7d7f54900..f4c1fb1b0 100644 --- a/src/admin/dashboard/ActionItems.js +++ b/src/admin/dashboard/ActionItems.js @@ -400,7 +400,15 @@ export default () => { ? { ...prev, details: json.data.details || '', - support_url: json.data.support_url || '', + // Same fallback as the failure path. A successful + // fetch must not end up with less to act on than a + // failed one: these notices are not dismissible and + // Contact Support is the only thing that retires + // them, so an empty destination here is a dead end. + support_url: + json.data.support_url || + srfm_admin?.support_url || + '', pending: false, } : prev @@ -668,6 +676,13 @@ export default () => { the dialog exists to show. */ }
 {
 												href={ safeUrl(
 													details.support_url
 												) }
-												target="_blank"
-												rel="noopener noreferrer"
+												// Not on a mailto: -- it has no
+												// document to open, so _blank
+												// leaves a blank tab behind.
+												// srfm_support_contact_url can
+												// return one, and the sibling
+												// item links already match on
+												// this.
+												{ ...( ! /^mailto:/i.test(
+													details.support_url || ''
+												) && {
+													target: '_blank',
+													rel: 'noopener noreferrer',
+												} ) }
 												onClick={ () => {
 													// Records the click, which is also
 													// what stands the notice down until
diff --git a/src/admin/dashboard/test/safeUrl.test.js b/src/admin/dashboard/test/safeUrl.test.js
index a2692d087..4c020ebe5 100644
--- a/src/admin/dashboard/test/safeUrl.test.js
+++ b/src/admin/dashboard/test/safeUrl.test.js
@@ -52,6 +52,48 @@ describe( 'safeUrl', () => {
 		}
 	);
 
+	// A browser trims C0 controls and spaces off both ends and strips tab, LF and
+	// CR from anywhere in the string before it parses the scheme. A check anchored
+	// on the raw string therefore reads a spelling the DOM never sees: every one
+	// of these was returned untouched and ran on click.
+	it.each( [
+		[ 'leading space', ' javascript:alert(1)' ],
+		[ 'leading newline', '\njavascript:alert(1)' ],
+		[ 'leading NUL', '\u0000javascript:alert(1)' ],
+		[ 'tab inside the scheme', 'java\tscript:alert(1)' ],
+		[ 'newline inside the scheme', 'java\nscript:alert(1)' ],
+		[ 'CR inside the scheme', 'java\rscript:alert(1)' ],
+		[ 'both at once', ' java\tscript:alert(1)' ],
+	] )( 'rejects javascript: hidden by a %s', ( _label, url ) => {
+		expect( safeUrl( url ) ).toBeUndefined();
+	} );
+
+	// No scheme to check and a different origin on the other end, so the
+	// schemeless-is-relative branch above must not claim these.
+	it.each( [
+		[ '//evil.test/x' ],
+		[ '\\\\evil.test/x' ],
+		[ ' //evil.test/x' ],
+		[ '/\\evil.test/x' ],
+	] )( 'rejects the protocol-relative %s', ( url ) => {
+		expect( safeUrl( url ) ).toBeUndefined();
+	} );
+
+	it( 'hands back what it checked, not what it was given', () => {
+		// Validating one spelling and returning another is how a scheme check gets
+		// walked past: the caller would bind the untrimmed string.
+		expect( safeUrl( '  https://example.test/x  ' ) ).toBe(
+			'https://example.test/x'
+		);
+	} );
+
+	it( 'still allows a single leading slash', () => {
+		// One slash is a same-origin path. Only two leave the site.
+		expect( safeUrl( '/wp-admin/admin.php?page=sureforms_menu' ) ).toBe(
+			'/wp-admin/admin.php?page=sureforms_menu'
+		);
+	} );
+
 	it( 'returns undefined rather than an empty string, so the caller drops the anchor', () => {
 		// An anchor with href="" is not keyboard focusable and resolves to the
 		// current document, so the guards have to see a falsy value they can act
diff --git a/src/admin/dashboard/utils/safeUrl.js b/src/admin/dashboard/utils/safeUrl.js
index e93d08cb9..9355fc41f 100644
--- a/src/admin/dashboard/utils/safeUrl.js
+++ b/src/admin/dashboard/utils/safeUrl.js
@@ -8,23 +8,50 @@
  * compiled out of production, so the server pass alone is not the last word
  * before the value reaches the DOM.
  *
+ * Returns the normalised string rather than the input. What is checked has to be
+ * what is used: validating one spelling and handing the caller another is how a
+ * scheme check gets walked past.
+ *
  * Returns undefined rather than '' so the caller's `&&` guards drop the anchor
  * entirely instead of rendering one that is not keyboard focusable.
  *
  * @param {string} url Candidate URL.
- * @return {string|undefined} The URL, or undefined when its scheme is not allowed.
+ * @return {string|undefined} The normalised URL, or undefined when it is not
+ *                            safe to bind.
  */
 const safeUrl = ( url ) => {
 	if ( typeof url !== 'string' || '' === url ) {
 		return undefined;
 	}
 
-	// Relative and same-document URLs carry no scheme and are safe.
-	if ( ! /^[a-z][a-z0-9+.-]*:/i.test( url ) ) {
-		return url;
+	// The same normalisation a browser applies before it parses the scheme: C0
+	// controls and spaces are trimmed from both ends, and tab, LF and CR are
+	// removed from anywhere in the string. Without it the scheme test reads a
+	// spelling the DOM never sees -- a leading space, or a tab inside the word
+	// "javascript", both fail to match `^[a-z]` and were returned untouched.
+	const normalised = url
+		// eslint-disable-next-line no-control-regex
+		.replace( /^[\u0000-\u0020\u007f]+|[\u0000-\u0020\u007f]+$/g, '' )
+		.replace( /[\t\n\r]/g, '' );
+
+	if ( '' === normalised ) {
+		return undefined;
+	}
+
+	// Protocol-relative, and the backslash spelling of it that the URL standard
+	// folds to the same thing under an http(s) base. It carries no scheme to
+	// check and resolves to a different origin, so "safe because it is relative"
+	// is exactly wrong here.
+	if ( /^[/\\]{2}/.test( normalised ) ) {
+		return undefined;
+	}
+
+	// Genuinely relative: a path, a query or a fragment, all same-origin.
+	if ( ! /^[a-z][a-z0-9+.-]*:/i.test( normalised ) ) {
+		return normalised;
 	}
 
-	return /^(?:https?|mailto):/i.test( url ) ? url : undefined;
+	return /^(?:https?|mailto):/i.test( normalised ) ? normalised : undefined;
 };
 
 export default safeUrl;
diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php
index ac27af50a..6790b6c11 100644
--- a/tests/unit/admin/test-admin.php
+++ b/tests/unit/admin/test-admin.php
@@ -1958,16 +1958,39 @@ public function test_get_support_contact_url_is_tagged_and_filterable() {
 
 		$this->assertSame( 'https://reseller.example.com/help/', $filtered );
 
-		// Escaped after the filter, so neither renderer has to trust what came back.
-		$bad = static function () {
-			return 'javascript:alert(1)';
+		// An inbox is a legitimate destination for a white-label support contact,
+		// and the sibling item URLs already allow it.
+		$inbox = static function () {
+			return 'mailto:help@reseller.example.com';
 		};
 
-		add_filter( 'srfm_support_contact_url', $bad );
-		$blocked = $method->invoke( Admin::get_instance(), 'notification' );
-		remove_filter( 'srfm_support_contact_url', $bad );
+		add_filter( 'srfm_support_contact_url', $inbox );
+		$mailto = $method->invoke( Admin::get_instance(), 'notification' );
+		remove_filter( 'srfm_support_contact_url', $inbox );
+
+		$this->assertSame( 'mailto:help@reseller.example.com', $mailto );
+
+		// Escaped after the filter, so neither renderer has to trust what came
+		// back -- but never down to ''. Contact Support is the only action that
+		// retires these notices and they are dismissible => false, so an empty
+		// destination is an undismissable notice with nothing on it that works.
+		// The unfiltered URL is built here rather than supplied, so it always
+		// escapes.
+		foreach ( [ 'javascript:alert(1)', 'data:text/html,x', '', '   ' ] as $rejected ) {
+			$bad = static function () use ( $rejected ) {
+				return $rejected;
+			};
+
+			add_filter( 'srfm_support_contact_url', $bad );
+			$blocked = $method->invoke( Admin::get_instance(), 'notification' );
+			remove_filter( 'srfm_support_contact_url', $bad );
 
-		$this->assertSame( '', $blocked, 'A scheme outside the allowlist must not survive the filter.' );
+			$this->assertStringStartsWith(
+				'https://sureforms.com/form/troubleshooting-form/',
+				$blocked,
+				'A filter value that cannot survive escaping must fall back, not blank the only working action.'
+			);
+		}
 	}
 
 	/**

From fa9222f7a9a22ebe23f38b0df201b0b56f625163 Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Fri, 11 Sep 2026 15:32:43 +0530
Subject: [PATCH 46/58] chore: drop this PR's POT change, restoring dev's

The committed sureforms.pot arrived here through the #3102 merges and is the
output of a `wp i18n make-pot` run that died part-way:

    Fatal error: Allowed memory size of 134217728 bytes exhausted
    in .../vendor/mck89/peast/lib/Peast/Syntax/Scanner.php on line 604

Peast is the JS parser make-pot uses. The process is killed mid-scan, so the
file is written short with no error in it -- it stopped referencing
admin/admin.php at line 3430 of 5249, dropping 74 translatable calls. dev's
copy reaches 4746 of 4782 and is essentially whole, so merging this would have
lost the existing translations for every dropped msgid on the next import.

Restored to dev's version, so this PR touches no translations at all.
Regeneration belongs in its own change, and needs the memory limit raised:

    php -d memory_limit=2G "$(command -v wp)" i18n make-pot . --skip-audit \
      --exclude='...' languages/sureforms.pot

That completes, reaches line 5212, and picks up all the new strings. Without
it the next run produces another silently short file.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 languages/sureforms.pot | 475 +++++++++++++++++++++++++++++-----------
 1 file changed, 350 insertions(+), 125 deletions(-)

diff --git a/languages/sureforms.pot b/languages/sureforms.pot
index e7b53db18..7c0228097 100644
--- a/languages/sureforms.pot
+++ b/languages/sureforms.pot
@@ -9,7 +9,7 @@ msgstr ""
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-09-10T10:58:46+00:00\n"
+"POT-Creation-Date: 2026-09-10T10:11:21+00:00\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "X-Generator: WP-CLI 2.11.0\n"
 "X-Domain: sureforms\n"
@@ -17,9 +17,9 @@ msgstr ""
 #. Plugin Name of the plugin
 #. Author of the plugin
 #: sureforms.php
-#: admin/admin.php:1197
-#: admin/admin.php:1198
-#: admin/admin.php:3111
+#: admin/admin.php:1152
+#: admin/admin.php:1153
+#: admin/admin.php:3057
 #: inc/abilities/abilities-registrar.php:139
 #: inc/gutenberg-hooks.php:109
 #: inc/page-builders/bricks/elements/form-widget.php:67
@@ -46,62 +46,63 @@ msgstr ""
 msgid "https://sureforms.com/"
 msgstr ""
 
-#: admin/admin.php:597
+#: admin/admin.php:552
 msgid "You are not allowed to update this prompt."
 msgstr ""
 
-#: admin/admin.php:640
+#: admin/admin.php:595
 msgid "Finish setting up your form"
 msgstr ""
 
-#: admin/admin.php:672
+#: admin/admin.php:627
 msgid "Review or edit your form"
 msgstr ""
 
-#: admin/admin.php:673
+#: admin/admin.php:628
+#: admin/admin.php:4144
 msgid "Edit form"
 msgstr ""
 
-#: admin/admin.php:678
+#: admin/admin.php:633
 msgid "Personalize the Thank You message"
 msgstr ""
 
-#: admin/admin.php:679
+#: admin/admin.php:634
 msgid "Edit message"
 msgstr ""
 
-#: admin/admin.php:684
+#: admin/admin.php:639
 msgid "Choose who gets notified of new replies"
 msgstr ""
 
-#: admin/admin.php:685
+#: admin/admin.php:640
 msgid "Set up email"
 msgstr ""
 
-#: admin/admin.php:693
+#: admin/admin.php:648
 msgid "your form"
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:696
+#: admin/admin.php:651
 msgid "Finish setting up %s"
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:704
+#: admin/admin.php:659
 msgid "View %s (opens in a new tab)"
 msgstr ""
 
-#: admin/admin.php:709
+#: admin/admin.php:664
 msgid "Customize your form to get it ready for real submissions:"
 msgstr ""
 
-#: admin/admin.php:1120
+#: admin/admin.php:1075
 msgid "Get SureForms Pro"
 msgstr ""
 
-#: admin/admin.php:1210
-#: admin/admin.php:1211
+#: admin/admin.php:1165
+#: admin/admin.php:1166
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -116,9 +117,9 @@ msgstr ""
 msgid "Dashboard"
 msgstr ""
 
-#: admin/admin.php:1228
-#: admin/admin.php:1229
-#: admin/admin.php:1661
+#: admin/admin.php:1183
+#: admin/admin.php:1184
+#: admin/admin.php:1616
 #: inc/compatibility/multilingual/string-collector.php:236
 #: inc/compatibility/multilingual/string-collector.php:245
 #: inc/compatibility/multilingual/string-collector.php:313
@@ -136,8 +137,9 @@ msgstr ""
 msgid "Settings"
 msgstr ""
 
-#: admin/admin.php:1281
-#: admin/admin.php:1282
+#: admin/admin.php:1236
+#: admin/admin.php:1237
+#: admin/admin.php:4366
 #: assets/build/blocks.js:172
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
@@ -145,42 +147,42 @@ msgstr ""
 msgid "Upgrade"
 msgstr ""
 
-#: admin/admin.php:1297
+#: admin/admin.php:1252
 msgid "Quiz Entries"
 msgstr ""
 
-#: admin/admin.php:1298
-#: admin/admin.php:1897
+#: admin/admin.php:1253
+#: admin/admin.php:1851
 #: assets/build/formEditor.js:172
 msgid "Quizzes"
 msgstr ""
 
-#: admin/admin.php:1300
-#: admin/admin.php:1333
-#: admin/admin.php:1366
+#: admin/admin.php:1255
+#: admin/admin.php:1288
+#: admin/admin.php:1321
 #: assets/build/settings.js:172
 msgid "New"
 msgstr ""
 
-#: admin/admin.php:1330
-#: admin/admin.php:1331
-#: admin/admin.php:1902
+#: admin/admin.php:1285
+#: admin/admin.php:1286
+#: admin/admin.php:1856
 msgid "Survey Reports"
 msgstr ""
 
-#: admin/admin.php:1363
-#: admin/admin.php:1364
-#: admin/admin.php:1907
+#: admin/admin.php:1318
+#: admin/admin.php:1319
+#: admin/admin.php:1861
 msgid "Partial Entries"
 msgstr ""
 
-#: admin/admin.php:1396
-#: admin/admin.php:1397
+#: admin/admin.php:1351
+#: admin/admin.php:1352
 msgid "SMTP"
 msgstr ""
 
-#: admin/admin.php:1459
-#: admin/admin.php:1460
+#: admin/admin.php:1414
+#: admin/admin.php:1415
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -194,8 +196,8 @@ msgstr ""
 msgid "Learn"
 msgstr ""
 
-#: admin/admin.php:1488
-#: admin/admin.php:1489
+#: admin/admin.php:1443
+#: admin/admin.php:1444
 #: inc/post-types.php:274
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
@@ -210,15 +212,15 @@ msgstr ""
 msgid "Forms"
 msgstr ""
 
-#: admin/admin.php:1497
-#: admin/admin.php:1498
+#: admin/admin.php:1452
+#: admin/admin.php:1453
 #: inc/post-types.php:270
 msgid "New Form"
 msgstr ""
 
-#: admin/admin.php:1506
-#: admin/admin.php:1507
-#: admin/admin.php:3430
+#: admin/admin.php:1461
+#: admin/admin.php:1462
+#: admin/admin.php:3376
 #: inc/generate-form-markup.php:211
 #: inc/generate-form-markup.php:224
 #: inc/global-settings/email-summary.php:225
@@ -235,8 +237,8 @@ msgstr ""
 msgid "Entries"
 msgstr ""
 
-#: admin/admin.php:1516
-#: admin/admin.php:1517
+#: admin/admin.php:1471
+#: admin/admin.php:1472
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -251,15 +253,15 @@ msgid "Payments"
 msgstr ""
 
 #. Translators: Post Title.
-#: admin/admin.php:1749
+#: admin/admin.php:1704
 msgid "Edit %1$s"
 msgstr ""
 
-#: admin/admin.php:1826
+#: admin/admin.php:1780
 msgid "Activating..."
 msgstr ""
 
-#: admin/admin.php:1827
+#: admin/admin.php:1781
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/formEditor.js:172
@@ -274,129 +276,130 @@ msgstr ""
 msgid "Activated"
 msgstr ""
 
-#: admin/admin.php:1828
+#: admin/admin.php:1782
 #: assets/build/formEditor.js:172
 #: assets/build/settings.js:172
 msgid "Activate"
 msgstr ""
 
-#: admin/admin.php:1829
+#: admin/admin.php:1783
 msgid "Installing..."
 msgstr ""
 
-#: admin/admin.php:1830
+#: admin/admin.php:1784
 msgid "Installed"
 msgstr ""
 
 #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version.
-#: admin/admin.php:2327
+#: admin/admin.php:2281
 msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version."
 msgstr ""
 
-#: admin/admin.php:2340
+#: admin/admin.php:2294
 msgid "Update Now"
 msgstr ""
 
-#: admin/admin.php:2387
-#: admin/admin.php:2476
+#: admin/admin.php:2341
+#: admin/admin.php:2430
 msgid "Your SureForms database is up to date. New form entries will be saved as usual."
 msgstr ""
 
-#: admin/admin.php:2401
-#: admin/admin.php:2485
+#: admin/admin.php:2355
+#: admin/admin.php:2439
 #: inc/rest-api.php:1447
 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support."
 msgstr ""
 
-#: admin/admin.php:2404
+#: admin/admin.php:2358
 msgid "Contact support"
 msgstr ""
 
-#: admin/admin.php:2424
+#: admin/admin.php:2378
 msgid "Database update needed"
 msgstr ""
 
-#: admin/admin.php:2430
+#: admin/admin.php:2384
 msgid "Fix now"
 msgstr ""
 
-#: admin/admin.php:2499
+#: admin/admin.php:2453
 msgid "SureForms — database update needed"
 msgstr ""
 
-#: admin/admin.php:2504
+#: admin/admin.php:2458
 msgid "Update database"
 msgstr ""
 
-#: admin/admin.php:2524
+#: admin/admin.php:2478
 msgid "You do not have permission to update the database."
 msgstr ""
 
 #. translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
-#: admin/admin.php:2618
+#: admin/admin.php:2572
 msgid "Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more."
 msgstr ""
 
 #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
-#: admin/admin.php:2636
+#: admin/admin.php:2590
 msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s."
 msgstr ""
 
-#: admin/admin.php:2681
+#: admin/admin.php:2635
 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!"
 msgstr ""
 
-#: admin/admin.php:2682
+#: admin/admin.php:2636
 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?"
 msgstr ""
 
-#: admin/admin.php:2685
+#: admin/admin.php:2639
 msgid "Rate SureForms"
 msgstr ""
 
-#: admin/admin.php:2693
-#: admin/admin.php:2757
+#: admin/admin.php:2647
+#: admin/admin.php:2711
 msgid "Maybe later"
 msgstr ""
 
-#: admin/admin.php:2699
+#: admin/admin.php:2653
 msgid "I already did"
 msgstr ""
 
-#: admin/admin.php:2748
+#: admin/admin.php:2702
 msgid "SureForms is ready to power your forms — explore what's possible!"
 msgstr ""
 
-#: admin/admin.php:2749
+#: admin/admin.php:2703
 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard."
 msgstr ""
 
-#: admin/admin.php:2752
+#: admin/admin.php:2706
 msgid "Go to Dashboard"
 msgstr ""
 
-#: admin/admin.php:2763
+#: admin/admin.php:2717
 msgid "I already know"
 msgstr ""
 
-#: admin/admin.php:2820
+#: admin/admin.php:2774
 msgid "Previous notice"
 msgstr ""
 
-#: admin/admin.php:2821
+#: admin/admin.php:2775
 msgid "Next notice"
 msgstr ""
 
 #. translators: 1: current position, 2: total notices.
-#: admin/admin.php:2823
+#: admin/admin.php:2777
 msgid "%1$d of %2$d"
 msgstr ""
 
-#: admin/admin.php:2843
-#: admin/admin.php:3002
-#: admin/admin.php:3040
-#: admin/admin.php:3061
-#: admin/admin.php:3384
+#: admin/admin.php:2794
+#: admin/admin.php:2948
+#: admin/admin.php:2986
+#: admin/admin.php:3007
+#: admin/admin.php:3330
+#: admin/admin.php:3685
 #: inc/admin-ajax.php:164
 #: inc/payments/admin/admin-handler.php:638
 #: inc/payments/stripe/admin-stripe-handler.php:80
@@ -404,80 +407,316 @@ msgstr ""
 msgid "Invalid nonce."
 msgstr ""
 
-#: admin/admin.php:2848
-#: admin/admin.php:2998
-#: admin/admin.php:3036
-#: admin/admin.php:3057
-#: admin/admin.php:3388
+#: admin/admin.php:2799
+#: admin/admin.php:2944
+#: admin/admin.php:2982
+#: admin/admin.php:3003
+#: admin/admin.php:3334
+#: admin/admin.php:3680
 msgid "Unauthorized user."
 msgstr ""
 
-#: admin/admin.php:2907
+#: admin/admin.php:2852
+#: admin/admin.php:3692
 msgid "Invalid parameters."
 msgstr ""
 
 #. translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong
-#: admin/admin.php:3007
+#: admin/admin.php:2953
 msgid "%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s"
 msgstr ""
 
-#: admin/admin.php:3018
+#: admin/admin.php:2964
 msgid "SureForms is waiting for you!"
 msgstr ""
 
-#: admin/admin.php:3020
+#: admin/admin.php:2966
 msgid "Build My First Form"
 msgstr ""
 
-#: admin/admin.php:3021
+#: admin/admin.php:2967
+#: admin/admin.php:3514
 msgid "Dismiss"
 msgstr ""
 
-#: admin/admin.php:3129
+#: admin/admin.php:3075
 msgid "SureForms AI Quick Draft"
 msgstr ""
 
-#: admin/admin.php:3148
+#: admin/admin.php:3094
 msgid "Describe the form and let SureForms AI generate it for you."
 msgstr ""
 
-#: admin/admin.php:3151
+#: admin/admin.php:3097
 msgid "Describe your form"
 msgstr ""
 
-#: admin/admin.php:3158
+#: admin/admin.php:3104
 msgid "Example: Create a contact form with name, email, phone, and message fields."
 msgstr ""
 
-#: admin/admin.php:3162
+#: admin/admin.php:3108
 #: inc/page-builders/elementor/form-widget.php:346
 #: assets/build/dashboard.js:2
 msgid "Create New Form"
 msgstr ""
 
-#: admin/admin.php:3199
+#: admin/admin.php:3145
 msgid "Redirecting..."
 msgstr ""
 
-#: admin/admin.php:3417
+#: admin/admin.php:3363
 msgid "Recent Entries"
 msgstr ""
 
-#: admin/admin.php:3418
+#: admin/admin.php:3364
 msgid "( Last 7 days )"
 msgstr ""
 
-#: admin/admin.php:3421
+#: admin/admin.php:3367
 msgid "View"
 msgstr ""
 
-#: admin/admin.php:3429
+#: admin/admin.php:3375
 #: inc/global-settings/email-summary.php:224
 #: assets/build/entries.js:172
 #: assets/build/payments.js:172
 msgid "Form Name"
 msgstr ""
 
+#: admin/admin.php:3533
+msgid "You do not have permission to do this."
+msgstr ""
+
+#. translators: %s: form title.
+#: admin/admin.php:3758
+msgid "We noticed a form submission failure on %s."
+msgstr ""
+
+#: admin/admin.php:3759
+msgid "We noticed a form submission failure."
+msgstr ""
+
+#: admin/admin.php:3760
+msgid "Visitors may not be able to reach you, and their entries were not saved."
+msgstr ""
+
+#. translators: %s: form title.
+#: admin/admin.php:3765
+msgid "We noticed a notification failure on %s."
+msgstr ""
+
+#: admin/admin.php:3766
+msgid "We noticed a notification failure."
+msgstr ""
+
+#: admin/admin.php:3767
+msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing."
+msgstr ""
+
+#. translators: %s: form title.
+#: admin/admin.php:3782
+msgid "We noticed an integration failure on %s."
+msgstr ""
+
+#: admin/admin.php:3783
+msgid "We noticed an integration failure."
+msgstr ""
+
+#: admin/admin.php:3784
+msgid "The entry was saved, but we could not send it to a connected service."
+msgstr ""
+
+#: admin/admin.php:3804
+#: assets/build/templatePicker.js:172
+msgid "Contact Support"
+msgstr ""
+
+#: admin/admin.php:3814
+#: admin/admin.php:3842
+msgid "Help Me Fix"
+msgstr ""
+
+#. translators: %s: caching plugin name.
+#: admin/admin.php:3838
+msgid "%s may interfere with your forms."
+msgstr ""
+
+#: admin/admin.php:3841
+msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order."
+msgstr ""
+
+#: admin/admin.php:3887
+msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept."
+msgstr ""
+
+#: admin/admin.php:3890
+msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here."
+msgstr ""
+
+#: admin/admin.php:4133
+msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions."
+msgstr ""
+
+#. translators: %s: form name.
+#: admin/admin.php:4138
+msgid "Finish setting up “%s”"
+msgstr ""
+
+#: admin/admin.php:4151
+msgid "Edit the Thank You message"
+msgstr ""
+
+#: admin/admin.php:4157
+msgid "Set where replies go"
+msgstr ""
+
+#: admin/admin.php:4307
+msgid "Use Conditional Logic to show only what matters"
+msgstr ""
+
+#: admin/admin.php:4308
+msgid "Split your form into steps to keep it easy"
+msgstr ""
+
+#: admin/admin.php:4309
+msgid "Let people upload files directly to your form"
+msgstr ""
+
+#: admin/admin.php:4310
+msgid "Turn responses into downloadable PDFs automatically"
+msgstr ""
+
+#: admin/admin.php:4311
+msgid "Let users sign with a simple signature field"
+msgstr ""
+
+#: admin/admin.php:4312
+msgid "Connect your form to other tools using webhooks"
+msgstr ""
+
+#: admin/admin.php:4313
+msgid "Use Conversational Forms for a chat-like experience"
+msgstr ""
+
+#: admin/admin.php:4314
+msgid "Let users register or log in through your form"
+msgstr ""
+
+#: admin/admin.php:4315
+msgid "Build forms that create WordPress user accounts"
+msgstr ""
+
+#: admin/admin.php:4316
+msgid "Add calculations to auto-total scores or prices"
+msgstr ""
+
+#: admin/admin.php:4538
+msgid "Debug log: no entries recorded."
+msgstr ""
+
+#. translators: 1: entries shown, 2: entries recorded.
+#: admin/admin.php:4542
+msgid "Debug log (most recent %1$d of %2$d entries)"
+msgstr ""
+
+#: admin/admin.php:4553
+msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General."
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4589
+msgid "SureForms: form submissions are failing on %s"
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4591
+msgid "SureForms has recorded form submissions on %s that could not be completed."
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4595
+msgid "SureForms: notification emails are not being sent on %s"
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4597
+msgid "SureForms saved entries on %s but could not send the notification emails for them."
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4601
+msgid "SureForms: an integration is not receiving entries on %s"
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4603
+msgid "SureForms saved entries on %s but could not pass them to a connected service."
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4613
+msgid "SureForms: a problem with the forms on %s"
+msgstr ""
+
+#. translators: %s: site host.
+#: admin/admin.php:4615
+msgid "SureForms has recorded a problem with the forms on %s."
+msgstr ""
+
+#. translators: %d: number of failed submissions.
+#: admin/admin.php:4640
+msgid "SureForms has recorded %d form submission that could not be completed."
+msgid_plural "SureForms has recorded %d form submissions that could not be completed."
+msgstr[0] ""
+msgstr[1] ""
+
+#. translators: %d: number of failed notifications.
+#: admin/admin.php:4652
+msgid "SureForms saved %d entry but could not send the notification email for it."
+msgid_plural "SureForms saved %d entries but could not send the notification emails for them."
+msgstr[0] ""
+msgstr[1] ""
+
+#. translators: %d: number of failed integration hand-offs.
+#: admin/admin.php:4664
+msgid "SureForms saved %d entry but could not pass it to a connected service."
+msgid_plural "SureForms saved %d entries but could not pass them to a connected service."
+msgstr[0] ""
+msgstr[1] ""
+
+#. translators: %d: number of recorded problems.
+#: admin/admin.php:4676
+msgid "SureForms has recorded %d problem with the forms on this site."
+msgid_plural "SureForms has recorded %d problems with the forms on this site."
+msgstr[0] ""
+msgstr[1] ""
+
+#: admin/admin.php:4715
+msgid "Hello SureForms support,"
+msgstr ""
+
+#. translators: %s: form title.
+#: admin/admin.php:4726
+msgid "Form: %s"
+msgstr ""
+
+#: admin/admin.php:4739
+msgid "Site details"
+msgstr ""
+
+#: admin/admin.php:4742
+msgid "not active"
+msgstr ""
+
+#: admin/admin.php:4745
+msgid "none detected"
+msgstr ""
+
+#: admin/admin.php:4746
+msgid "none recorded"
+msgstr ""
+
 #: inc/abilities/abilities-registrar.php:107
 msgid "SureForms MCP Server"
 msgstr ""
@@ -12957,7 +13196,6 @@ msgid "Upgrade to Unlock"
 msgstr ""
 
 #: assets/build/blocks.js:172
-#: assets/build/dashboard.js:172
 #: modules/gutenberg/build/blocks.js:4
 msgid "Close"
 msgstr ""
@@ -13849,23 +14087,6 @@ msgstr ""
 msgid "Ignore"
 msgstr ""
 
-#: assets/build/dashboard.js:172
-msgid "Details"
-msgstr ""
-
-#: assets/build/dashboard.js:172
-msgid "Copied"
-msgstr ""
-
-#: assets/build/dashboard.js:172
-msgid "Copy details"
-msgstr ""
-
-#: assets/build/dashboard.js:172
-#: assets/build/templatePicker.js:172
-msgid "Contact Support"
-msgstr ""
-
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -14335,6 +14556,10 @@ msgstr ""
 msgid "SureForms %s"
 msgstr ""
 
+#: assets/build/dashboard.js:172
+msgid "Copied"
+msgstr ""
+
 #: assets/build/dashboard.js:172
 msgid "First name is required."
 msgstr ""

From 512872a990b1637fed388d5438b54d5cd376b353 Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Fri, 11 Sep 2026 16:22:15 +0530
Subject: [PATCH 47/58] Version Bump 2.12.7
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Also fixes the dead `replace:plugin_const` grunt task, whose regex still
matched `UAGB_VER` from Ultimate Addons. `SRFM_VER` was never bumped by
`grunt version-bump` — it has been hand-edited every release.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 Gruntfile.js                     |   2 +-
 README.md                        |  13 +-
 admin/admin.php                  |  26 +-
 inc/client-logger.php            |   2 +-
 inc/database/base.php            |   4 +-
 inc/database/tables/entries.php  |   2 +-
 inc/database/tables/payments.php |   2 +-
 inc/forms-data.php               |  10 +-
 inc/frontend-assets.php          |   2 +-
 inc/helper.php                   |   6 +-
 languages/sureforms.pot          | 433 ++++++++++++++++++-------------
 package-lock.json                |   4 +-
 package.json                     |   2 +-
 readme.txt                       |  13 +-
 sureforms.php                    |   6 +-
 15 files changed, 299 insertions(+), 228 deletions(-)

diff --git a/Gruntfile.js b/Gruntfile.js
index 0a279f749..a7bd4fe62 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -268,7 +268,7 @@ module.exports = function ( grunt ) {
 				overwrite: true,
 				replacements: [
 					{
-						from: /UAGB_VER', '.*?'/g,
+						from: /SRFM_VER', '.*?'/g,
 						to: "SRFM_VER', '<%= pkg.version %>'",
 					},
 				],
diff --git a/README.md b/README.md
index f501560fd..53084c108 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 **Requires at least:** 6.4  
 **Tested up to:** 7.1  
 **Requires PHP:** 7.4  
-**Stable tag:** 2.12.6  
+**Stable tag:** 2.12.7  
 **License:** GPLv2 or later  
 **License URI:** http://www.gnu.org/licenses/gpl-2.0.html  
 
@@ -426,6 +426,14 @@ Visit the [SureForms features page](https://sureforms.com/features/?utm_source=w
 You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to validate, triage, and handle security reports.
 
 ## Changelog ##
+### 2.12.7 - 11th September 2026 ###
+* Improvement: Submission failure notices now offer View details, so you can read and copy the full diagnostics before contacting support.
+* Improvement: The Form Checks panel now appears only when something genuinely needs attention, with clearer wording, keeping the sidebar focused.
+* Fix: After-submission actions now complete reliably when a form redirects on success.
+* Fix: Form pages now keep their styles and submit script on sites using page builders that buffer page output, such as Breakdance.
+* Fix: Form submissions no longer drop fields when a caching plugin serves an older copy of the page, so every submitted field is saved to the entry.
+* Fix: The conversion rate no longer counts submissions made by site editors while previewing a form.
+* Fix: The Edit Form icon is no longer oversized in Divi.
 ### 2.12.6 - 2nd September 2026 ###
 * New: Added an Enable Logs option that records form submission failures to a downloadable file, making it easier to diagnose issues on a live site.
 * New: Added Views and Conversion Rate columns to the Forms list, so you can see how each form is performing at a glance.
@@ -434,9 +442,6 @@ You can report security issues through our [Bug Bounty Program](https://brainsto
 * Fix: This update addressed a security bug. Props to Patchstack for reporting it responsibly to our team.
 ### 2.12.5 - 25th August 2026 ###
 * Fix: Form submissions now go through reliably, even on sites where a performance plugin combines or defers JavaScript.
-### 2.12.4 - 19th August 2026 ###
-* Improvement: Email notification images are now saved to the Media Library instead of being embedded as base64, improving email deliverability and reducing message size.
-* Fix: Compatibility with WordPress 7.1.
 The full changelog is available [here](https://sureforms.com/whats-new/?utm_source=wordpress.org&utm_medium=whats_new).
 
 ## Upgrade Notice ##
\ No newline at end of file
diff --git a/admin/admin.php b/admin/admin.php
index 8cfc49825..5633911f6 100644
--- a/admin/admin.php
+++ b/admin/admin.php
@@ -95,7 +95,7 @@ class Admin {
 	 * would otherwise have to ask for, and the diagnostics are already on the
 	 * clipboard by the time someone gets here.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 */
 	private const SUPPORT_CONTACT_URL = 'https://sureforms.com/form/troubleshooting-form/';
 
@@ -162,7 +162,7 @@ class Admin {
 	 * records a failure and then asks again is testing the memo.
 	 *
 	 * @var array>|null
-	 * @since x.x.x
+	 * @since 2.12.7
 	 */
 	private static $action_items_cache = null;
 
@@ -486,7 +486,7 @@ public static function get_thankyou_prompt_forms() {
 	 * Anything that records or clears a failure inside one process has to call
 	 * this, or it reads the answer from before the change.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return void
 	 */
 	public static function reset_action_items_cache() {
@@ -2861,7 +2861,7 @@ public function enqueue_notice_response_script() {
 	 * Capability first, then nonce, then the category, matching the ordering of
 	 * the sibling handlers in this class.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return void
 	 */
 	public function handle_action_item_details() {
@@ -3887,7 +3887,7 @@ public function handle_dismiss_action_item() {
 	 * blue, on a SureForms panel that is otherwise entirely brand orange. Same
 	 * approach and same values as print_srfm_notice_styles().
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return void
 	 */
 	public function enqueue_action_item_styles() {
@@ -4049,7 +4049,7 @@ public function enqueue_action_item_styles() {
 	 * again in JSX looks identical to translators until the first edit to either,
 	 * after which one surface silently reverts to English.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return array
 	 */
 	private function get_details_dialog_labels() {
@@ -4090,7 +4090,7 @@ private function get_details_dialog_labels() {
 	 * filter has nothing to do with SureForms' logging toggle, and was being
 	 * silenced by it.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return array>
 	 */
 	private function get_first_party_action_items() {
@@ -4866,7 +4866,7 @@ private function track_action_item_impressions( $warnings ) {
 	 * track_action_item_impressions().
 	 *
 	 * @param string $event_name Analytics key from the allowlist.
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return void
 	 */
 	private function track_notice_event( $event_name ) {
@@ -4899,7 +4899,7 @@ private function track_notice_event( $event_name ) {
 	 *
 	 * @param string $category One of Client_Logger::CATEGORIES, naming the failure
 	 *                         the visitor is reporting.
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return string
 	 */
 	private function get_support_contact_url( $category ) {
@@ -4943,7 +4943,7 @@ private function get_support_contact_url( $category ) {
 		 * inbox and has no destination left to change now that the action opens a
 		 * form. A white-label install wants to point this at its own support page.
 		 *
-		 * @since x.x.x
+		 * @since 2.12.7
 		 *
 		 * @param string $url      Contact form URL, already UTM-tagged.
 		 * @param string $category The failure being reported.
@@ -4979,7 +4979,7 @@ private function get_support_contact_url( $category ) {
 	 * that is not in this flow.
 	 *
 	 * @param int $max_chars Characters of log to include.
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return string
 	 */
 	private function get_support_log_block( $max_chars = 1200 ) {
@@ -5022,7 +5022,7 @@ private function get_support_log_block( $max_chars = 1200 ) {
 	 * category at all.
 	 *
 	 * @param string $category One of Client_Logger::CATEGORIES.
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return array{subject:string,anon:string}
 	 */
 	private function get_support_copy( $category ) {
@@ -5072,7 +5072,7 @@ private function get_support_copy( $category ) {
 	 * @param string $category One of Client_Logger::CATEGORIES. Unknown or absent
 	 *                         gets neutral wording rather than a specific claim.
 	 * @param int    $count    Failures recorded for that category.
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return string
 	 */
 	private function get_support_count_sentence( $category, $count ) {
diff --git a/inc/client-logger.php b/inc/client-logger.php
index 919b8232a..57b7109e8 100644
--- a/inc/client-logger.php
+++ b/inc/client-logger.php
@@ -110,7 +110,7 @@ class Client_Logger {
 	 * tail must not be handed the tail from before the write.
 	 *
 	 * @var array
-	 * @since x.x.x
+	 * @since 2.12.7
 	 */
 	private static $tail_memo = [];
 
diff --git a/inc/database/base.php b/inc/database/base.php
index 922ac33db..93ad5ea39 100644
--- a/inc/database/base.php
+++ b/inc/database/base.php
@@ -1056,7 +1056,7 @@ protected function cache_reset() {
 	 *     }
 	 * }
 	 *
-	 * @since x.x.x -- Added support for "NOT IN" compare.
+	 * @since 2.12.7 -- Added support for "NOT IN" compare.
 	 * @since 1.1.1 -- Added support for "IN" compare.
 	 * @since 0.0.13
 	 * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided.
@@ -1125,7 +1125,7 @@ protected function prepare_where_clauses( $where_clauses = [] ) {
 										_doing_it_wrong(
 											__METHOD__,
 											esc_html( "{$compare} requires an array value, received " . gettype( $_value['value'] ) . '.' ),
-											'x.x.x'
+											'2.12.7'
 										);
 										break;
 									}
diff --git a/inc/database/tables/entries.php b/inc/database/tables/entries.php
index 0622b4cfc..d3ec611d3 100644
--- a/inc/database/tables/entries.php
+++ b/inc/database/tables/entries.php
@@ -157,7 +157,7 @@ public function get_new_columns_definition() {
 			'extras LONGTEXT AFTER status',
 			'user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER form_id',
 			'INDEX idx_user_id (user_id)',
-			// Note: @since x.x.x -- Covers the conversion rate's submitter lookup.
+			// Note: @since 2.12.7 -- Covers the conversion rate's submitter lookup.
 			'INDEX idx_user_id_created_at (user_id, created_at)',
 		];
 	}
diff --git a/inc/database/tables/payments.php b/inc/database/tables/payments.php
index 73d13dd0d..1b11ca293 100644
--- a/inc/database/tables/payments.php
+++ b/inc/database/tables/payments.php
@@ -1295,7 +1295,7 @@ private static function build_clause_for_group( array $where_group, array &$para
 					_doing_it_wrong(
 						__METHOD__,
 						esc_html( "{$operator} requires an array value, received " . gettype( $condition['value'] ) . '.' ),
-						'x.x.x'
+						'2.12.7'
 					);
 					continue;
 				}
diff --git a/inc/forms-data.php b/inc/forms-data.php
index 958eaa5bf..a4782a619 100644
--- a/inc/forms-data.php
+++ b/inc/forms-data.php
@@ -41,7 +41,7 @@ class Forms_Data {
 	 * something invalidated it, and there is no natural invalidation point.
 	 *
 	 * @var array>|null
-	 * @since x.x.x
+	 * @since 2.12.7
 	 */
 	private static $editing_user_ids = null;
 
@@ -496,7 +496,7 @@ private static function window_boundary_sql( $window_start ) {
 	 * change within a request.
 	 *
 	 * @param int $window_start Unix timestamp the view window opened at.
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return array
 	 */
 	private static function get_editing_submitter_ids( $window_start ) {
@@ -530,7 +530,7 @@ private static function get_editing_submitter_ids( $window_start ) {
 		 * budget -- harmless for the count, but it brings the ceiling closer.
 		 *
 		 * @param int $limit Maximum submitters to test. Default 500.
-		 * @since x.x.x
+		 * @since 2.12.7
 		 */
 		$limit = Helper::get_integer_value( apply_filters( 'srfm_forms_metric_submitter_limit', 500 ) );
 
@@ -571,7 +571,7 @@ private static function get_editing_submitter_ids( $window_start ) {
 			 *
 			 * @param int $count Number of distinct submitters found, capped at $limit + 1.
 			 * @param int $limit The ceiling in force.
-			 * @since x.x.x
+			 * @since 2.12.7
 			 */
 			do_action( 'srfm_forms_metric_submitter_limit_exceeded', count( $submitters ), $limit );
 
@@ -625,7 +625,7 @@ static function ( $user_id ) {
 	 *
 	 * @param int $form_id Form post ID.
 	 * @return array{views:int,conversion_rate:float|null}
-	 * @since x.x.x -- Signature reduced to $form_id.
+	 * @since 2.12.7 -- Signature reduced to $form_id.
 	 * @since 2.12.6
 	 */
 	private function calculate_form_metrics( $form_id ) {
diff --git a/inc/frontend-assets.php b/inc/frontend-assets.php
index 3ee4772e4..682ff74c0 100644
--- a/inc/frontend-assets.php
+++ b/inc/frontend-assets.php
@@ -585,7 +585,7 @@ public function page_template( $template ) {
 	 * Guarded on `wp_head` having already fired, so this is inert on the ordinary
 	 * path where no builder rendered first and nothing has been printed yet.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return void
 	 */
 	private static function reset_printed_assets() {
diff --git a/inc/helper.php b/inc/helper.php
index 48da2867a..3c22319ec 100644
--- a/inc/helper.php
+++ b/inc/helper.php
@@ -2238,7 +2238,7 @@ public static function get_active_caching_plugin() {
 	 * Falls back to the general guide rather than returning nothing, so the notice
 	 * always has somewhere to send them.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return string Absolute documentation URL.
 	 */
 	public static function get_caching_plugin_doc_url() {
@@ -2806,7 +2806,7 @@ private static function get_geo_failure_ttl() {
 	 * one that has something to say. Reordering this array changes which guide a
 	 * two-plugin site sees.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return array
 	 */
 	private static function get_known_caching_plugins() {
@@ -2841,7 +2841,7 @@ private static function get_known_caching_plugins() {
 	 * the multisite network-active merge. First match in
 	 * get_known_caching_plugins() wins; that array's order is the precedence.
 	 *
-	 * @since x.x.x
+	 * @since 2.12.7
 	 * @return array{0:string,1:string}|null Name and doc slug, or null when none is active.
 	 */
 	private static function get_active_caching_plugin_entry() {
diff --git a/languages/sureforms.pot b/languages/sureforms.pot
index 7c0228097..fee8658ee 100644
--- a/languages/sureforms.pot
+++ b/languages/sureforms.pot
@@ -2,14 +2,14 @@
 # This file is distributed under the GPLv2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: SureForms 2.12.6\n"
+"Project-Id-Version: SureForms 2.12.7\n"
 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/sureforms\n"
 "Last-Translator: FULL NAME \n"
 "Language-Team: LANGUAGE \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-09-10T10:11:21+00:00\n"
+"POT-Creation-Date: 2026-09-11T10:50:43+00:00\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "X-Generator: WP-CLI 2.11.0\n"
 "X-Domain: sureforms\n"
@@ -17,9 +17,9 @@ msgstr ""
 #. Plugin Name of the plugin
 #. Author of the plugin
 #: sureforms.php
-#: admin/admin.php:1152
-#: admin/admin.php:1153
-#: admin/admin.php:3057
+#: admin/admin.php:1198
+#: admin/admin.php:1199
+#: admin/admin.php:3185
 #: inc/abilities/abilities-registrar.php:139
 #: inc/gutenberg-hooks.php:109
 #: inc/page-builders/bricks/elements/form-widget.php:67
@@ -46,63 +46,63 @@ msgstr ""
 msgid "https://sureforms.com/"
 msgstr ""
 
-#: admin/admin.php:552
+#: admin/admin.php:598
 msgid "You are not allowed to update this prompt."
 msgstr ""
 
-#: admin/admin.php:595
+#: admin/admin.php:641
 msgid "Finish setting up your form"
 msgstr ""
 
-#: admin/admin.php:627
+#: admin/admin.php:673
 msgid "Review or edit your form"
 msgstr ""
 
-#: admin/admin.php:628
-#: admin/admin.php:4144
+#: admin/admin.php:674
+#: admin/admin.php:4515
 msgid "Edit form"
 msgstr ""
 
-#: admin/admin.php:633
+#: admin/admin.php:679
 msgid "Personalize the Thank You message"
 msgstr ""
 
-#: admin/admin.php:634
+#: admin/admin.php:680
 msgid "Edit message"
 msgstr ""
 
-#: admin/admin.php:639
+#: admin/admin.php:685
 msgid "Choose who gets notified of new replies"
 msgstr ""
 
-#: admin/admin.php:640
+#: admin/admin.php:686
 msgid "Set up email"
 msgstr ""
 
-#: admin/admin.php:648
+#: admin/admin.php:694
 msgid "your form"
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:651
+#: admin/admin.php:697
 msgid "Finish setting up %s"
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:659
+#: admin/admin.php:705
 msgid "View %s (opens in a new tab)"
 msgstr ""
 
-#: admin/admin.php:664
+#: admin/admin.php:710
 msgid "Customize your form to get it ready for real submissions:"
 msgstr ""
 
-#: admin/admin.php:1075
+#: admin/admin.php:1121
 msgid "Get SureForms Pro"
 msgstr ""
 
-#: admin/admin.php:1165
-#: admin/admin.php:1166
+#: admin/admin.php:1211
+#: admin/admin.php:1212
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -117,9 +117,9 @@ msgstr ""
 msgid "Dashboard"
 msgstr ""
 
-#: admin/admin.php:1183
-#: admin/admin.php:1184
-#: admin/admin.php:1616
+#: admin/admin.php:1229
+#: admin/admin.php:1230
+#: admin/admin.php:1662
 #: inc/compatibility/multilingual/string-collector.php:236
 #: inc/compatibility/multilingual/string-collector.php:245
 #: inc/compatibility/multilingual/string-collector.php:313
@@ -137,9 +137,9 @@ msgstr ""
 msgid "Settings"
 msgstr ""
 
-#: admin/admin.php:1236
-#: admin/admin.php:1237
-#: admin/admin.php:4366
+#: admin/admin.php:1282
+#: admin/admin.php:1283
+#: admin/admin.php:4737
 #: assets/build/blocks.js:172
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
@@ -147,42 +147,42 @@ msgstr ""
 msgid "Upgrade"
 msgstr ""
 
-#: admin/admin.php:1252
+#: admin/admin.php:1298
 msgid "Quiz Entries"
 msgstr ""
 
-#: admin/admin.php:1253
-#: admin/admin.php:1851
+#: admin/admin.php:1299
+#: admin/admin.php:1905
 #: assets/build/formEditor.js:172
 msgid "Quizzes"
 msgstr ""
 
-#: admin/admin.php:1255
-#: admin/admin.php:1288
-#: admin/admin.php:1321
+#: admin/admin.php:1301
+#: admin/admin.php:1334
+#: admin/admin.php:1367
 #: assets/build/settings.js:172
 msgid "New"
 msgstr ""
 
-#: admin/admin.php:1285
-#: admin/admin.php:1286
-#: admin/admin.php:1856
+#: admin/admin.php:1331
+#: admin/admin.php:1332
+#: admin/admin.php:1910
 msgid "Survey Reports"
 msgstr ""
 
-#: admin/admin.php:1318
-#: admin/admin.php:1319
-#: admin/admin.php:1861
+#: admin/admin.php:1364
+#: admin/admin.php:1365
+#: admin/admin.php:1915
 msgid "Partial Entries"
 msgstr ""
 
-#: admin/admin.php:1351
-#: admin/admin.php:1352
+#: admin/admin.php:1397
+#: admin/admin.php:1398
 msgid "SMTP"
 msgstr ""
 
-#: admin/admin.php:1414
-#: admin/admin.php:1415
+#: admin/admin.php:1460
+#: admin/admin.php:1461
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -196,8 +196,8 @@ msgstr ""
 msgid "Learn"
 msgstr ""
 
-#: admin/admin.php:1443
-#: admin/admin.php:1444
+#: admin/admin.php:1489
+#: admin/admin.php:1490
 #: inc/post-types.php:274
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
@@ -212,15 +212,15 @@ msgstr ""
 msgid "Forms"
 msgstr ""
 
-#: admin/admin.php:1452
-#: admin/admin.php:1453
+#: admin/admin.php:1498
+#: admin/admin.php:1499
 #: inc/post-types.php:270
 msgid "New Form"
 msgstr ""
 
-#: admin/admin.php:1461
-#: admin/admin.php:1462
-#: admin/admin.php:3376
+#: admin/admin.php:1507
+#: admin/admin.php:1508
+#: admin/admin.php:3504
 #: inc/generate-form-markup.php:211
 #: inc/generate-form-markup.php:224
 #: inc/global-settings/email-summary.php:225
@@ -237,8 +237,8 @@ msgstr ""
 msgid "Entries"
 msgstr ""
 
-#: admin/admin.php:1471
-#: admin/admin.php:1472
+#: admin/admin.php:1517
+#: admin/admin.php:1518
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -253,15 +253,15 @@ msgid "Payments"
 msgstr ""
 
 #. Translators: Post Title.
-#: admin/admin.php:1704
+#: admin/admin.php:1750
 msgid "Edit %1$s"
 msgstr ""
 
-#: admin/admin.php:1780
+#: admin/admin.php:1834
 msgid "Activating..."
 msgstr ""
 
-#: admin/admin.php:1781
+#: admin/admin.php:1835
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/formEditor.js:172
@@ -276,130 +276,141 @@ msgstr ""
 msgid "Activated"
 msgstr ""
 
-#: admin/admin.php:1782
+#: admin/admin.php:1836
 #: assets/build/formEditor.js:172
 #: assets/build/settings.js:172
 msgid "Activate"
 msgstr ""
 
-#: admin/admin.php:1783
+#: admin/admin.php:1837
 msgid "Installing..."
 msgstr ""
 
-#: admin/admin.php:1784
+#: admin/admin.php:1838
 msgid "Installed"
 msgstr ""
 
 #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version.
-#: admin/admin.php:2281
+#: admin/admin.php:2335
 msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version."
 msgstr ""
 
-#: admin/admin.php:2294
+#: admin/admin.php:2348
 msgid "Update Now"
 msgstr ""
 
-#: admin/admin.php:2341
-#: admin/admin.php:2430
+#: admin/admin.php:2395
+#: admin/admin.php:2484
 msgid "Your SureForms database is up to date. New form entries will be saved as usual."
 msgstr ""
 
-#: admin/admin.php:2355
-#: admin/admin.php:2439
+#: admin/admin.php:2409
+#: admin/admin.php:2493
 #: inc/rest-api.php:1447
 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support."
 msgstr ""
 
-#: admin/admin.php:2358
+#: admin/admin.php:2412
 msgid "Contact support"
 msgstr ""
 
-#: admin/admin.php:2378
+#: admin/admin.php:2432
 msgid "Database update needed"
 msgstr ""
 
-#: admin/admin.php:2384
+#: admin/admin.php:2438
 msgid "Fix now"
 msgstr ""
 
-#: admin/admin.php:2453
+#: admin/admin.php:2507
 msgid "SureForms — database update needed"
 msgstr ""
 
-#: admin/admin.php:2458
+#: admin/admin.php:2512
 msgid "Update database"
 msgstr ""
 
-#: admin/admin.php:2478
+#: admin/admin.php:2532
 msgid "You do not have permission to update the database."
 msgstr ""
 
 #. translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
-#: admin/admin.php:2572
+#: admin/admin.php:2626
 msgid "Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more."
 msgstr ""
 
 #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
-#: admin/admin.php:2590
+#: admin/admin.php:2644
 msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s."
 msgstr ""
 
-#: admin/admin.php:2635
+#: admin/admin.php:2689
 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!"
 msgstr ""
 
-#: admin/admin.php:2636
+#: admin/admin.php:2690
 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?"
 msgstr ""
 
-#: admin/admin.php:2639
+#: admin/admin.php:2693
 msgid "Rate SureForms"
 msgstr ""
 
-#: admin/admin.php:2647
-#: admin/admin.php:2711
+#: admin/admin.php:2701
+#: admin/admin.php:2765
 msgid "Maybe later"
 msgstr ""
 
-#: admin/admin.php:2653
+#: admin/admin.php:2707
 msgid "I already did"
 msgstr ""
 
-#: admin/admin.php:2702
+#: admin/admin.php:2756
 msgid "SureForms is ready to power your forms — explore what's possible!"
 msgstr ""
 
-#: admin/admin.php:2703
+#: admin/admin.php:2757
 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard."
 msgstr ""
 
-#: admin/admin.php:2706
+#: admin/admin.php:2760
 msgid "Go to Dashboard"
 msgstr ""
 
-#: admin/admin.php:2717
+#: admin/admin.php:2771
 msgid "I already know"
 msgstr ""
 
-#: admin/admin.php:2774
+#: admin/admin.php:2831
 msgid "Previous notice"
 msgstr ""
 
-#: admin/admin.php:2775
+#: admin/admin.php:2832
 msgid "Next notice"
 msgstr ""
 
 #. translators: 1: current position, 2: total notices.
-#: admin/admin.php:2777
+#: admin/admin.php:2834
 msgid "%1$d of %2$d"
 msgstr ""
 
-#: admin/admin.php:2794
-#: admin/admin.php:2948
-#: admin/admin.php:2986
-#: admin/admin.php:3007
-#: admin/admin.php:3330
-#: admin/admin.php:3685
+#: admin/admin.php:2869
+#: admin/admin.php:2922
+#: admin/admin.php:3072
+#: admin/admin.php:3110
+#: admin/admin.php:3131
+#: admin/admin.php:3462
+#: admin/admin.php:3842
+msgid "Unauthorized user."
+msgstr ""
+
+#: admin/admin.php:2874
+#: admin/admin.php:2917
+#: admin/admin.php:3076
+#: admin/admin.php:3114
+#: admin/admin.php:3135
+#: admin/admin.php:3458
+#: admin/admin.php:3847
 #: inc/admin-ajax.php:164
 #: inc/payments/admin/admin-handler.php:638
 #: inc/payments/stripe/admin-stripe-handler.php:80
@@ -407,316 +418,371 @@ msgstr ""
 msgid "Invalid nonce."
 msgstr ""
 
-#: admin/admin.php:2799
-#: admin/admin.php:2944
-#: admin/admin.php:2982
-#: admin/admin.php:3003
-#: admin/admin.php:3334
-#: admin/admin.php:3680
-msgid "Unauthorized user."
+#: admin/admin.php:2891
+msgid "Nothing to report."
 msgstr ""
 
-#: admin/admin.php:2852
-#: admin/admin.php:3692
+#: admin/admin.php:2981
+#: admin/admin.php:3854
 msgid "Invalid parameters."
 msgstr ""
 
 #. translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong
-#: admin/admin.php:2953
+#: admin/admin.php:3081
 msgid "%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s"
 msgstr ""
 
-#: admin/admin.php:2964
+#: admin/admin.php:3092
 msgid "SureForms is waiting for you!"
 msgstr ""
 
-#: admin/admin.php:2966
+#: admin/admin.php:3094
 msgid "Build My First Form"
 msgstr ""
 
-#: admin/admin.php:2967
-#: admin/admin.php:3514
+#: admin/admin.php:3095
+#: admin/admin.php:3650
 msgid "Dismiss"
 msgstr ""
 
-#: admin/admin.php:3075
+#: admin/admin.php:3203
 msgid "SureForms AI Quick Draft"
 msgstr ""
 
-#: admin/admin.php:3094
+#: admin/admin.php:3222
 msgid "Describe the form and let SureForms AI generate it for you."
 msgstr ""
 
-#: admin/admin.php:3097
+#: admin/admin.php:3225
 msgid "Describe your form"
 msgstr ""
 
-#: admin/admin.php:3104
+#: admin/admin.php:3232
 msgid "Example: Create a contact form with name, email, phone, and message fields."
 msgstr ""
 
-#: admin/admin.php:3108
+#: admin/admin.php:3236
 #: inc/page-builders/elementor/form-widget.php:346
 #: assets/build/dashboard.js:2
 msgid "Create New Form"
 msgstr ""
 
-#: admin/admin.php:3145
+#: admin/admin.php:3273
 msgid "Redirecting..."
 msgstr ""
 
-#: admin/admin.php:3363
+#: admin/admin.php:3491
 msgid "Recent Entries"
 msgstr ""
 
-#: admin/admin.php:3364
+#: admin/admin.php:3492
 msgid "( Last 7 days )"
 msgstr ""
 
-#: admin/admin.php:3367
+#: admin/admin.php:3495
 msgid "View"
 msgstr ""
 
-#: admin/admin.php:3375
+#: admin/admin.php:3503
 #: inc/global-settings/email-summary.php:224
 #: assets/build/entries.js:172
 #: assets/build/payments.js:172
 msgid "Form Name"
 msgstr ""
 
-#: admin/admin.php:3533
+#: admin/admin.php:3669
 msgid "You do not have permission to do this."
 msgstr ""
 
+#: admin/admin.php:4057
+#: assets/build/dashboard.js:172
+msgid "Details"
+msgstr ""
+
+#: admin/admin.php:4058
+msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it."
+msgstr ""
+
+#: admin/admin.php:4059
+#: assets/build/dashboard.js:172
+msgid "Copy details"
+msgstr ""
+
+#: admin/admin.php:4060
+#: assets/build/dashboard.js:172
+msgid "Copied"
+msgstr ""
+
+#: admin/admin.php:4061
+#: assets/build/dashboard.js:172
+#: assets/build/templatePicker.js:172
+msgid "Contact Support"
+msgstr ""
+
+#: admin/admin.php:4062
+#: assets/build/blocks.js:172
+#: assets/build/dashboard.js:172
+#: modules/gutenberg/build/blocks.js:4
+msgid "Close"
+msgstr ""
+
+#: admin/admin.php:4068
+msgid "Copy the details first, so you have them to paste."
+msgstr ""
+
+#: admin/admin.php:4072
+msgid "Copied. Contact Support is now available."
+msgstr ""
+
+#: admin/admin.php:4073
+msgid "Your browser would not let us copy. Select the text above and copy it by hand."
+msgstr ""
+
+#: admin/admin.php:4076
+msgid "Recorded diagnostics"
+msgstr ""
+
+#: admin/admin.php:4080
+msgid "Collecting the details…"
+msgstr ""
+
+#: admin/admin.php:4081
+msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there."
+msgstr ""
+
 #. translators: %s: form title.
-#: admin/admin.php:3758
+#: admin/admin.php:4108
 msgid "We noticed a form submission failure on %s."
 msgstr ""
 
-#: admin/admin.php:3759
+#: admin/admin.php:4109
 msgid "We noticed a form submission failure."
 msgstr ""
 
-#: admin/admin.php:3760
+#: admin/admin.php:4110
 msgid "Visitors may not be able to reach you, and their entries were not saved."
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:3765
+#: admin/admin.php:4115
 msgid "We noticed a notification failure on %s."
 msgstr ""
 
-#: admin/admin.php:3766
+#: admin/admin.php:4116
 msgid "We noticed a notification failure."
 msgstr ""
 
-#: admin/admin.php:3767
+#: admin/admin.php:4117
 msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing."
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:3782
+#: admin/admin.php:4132
 msgid "We noticed an integration failure on %s."
 msgstr ""
 
-#: admin/admin.php:3783
+#: admin/admin.php:4133
 msgid "We noticed an integration failure."
 msgstr ""
 
-#: admin/admin.php:3784
+#: admin/admin.php:4134
 msgid "The entry was saved, but we could not send it to a connected service."
 msgstr ""
 
-#: admin/admin.php:3804
-#: assets/build/templatePicker.js:172
-msgid "Contact Support"
+#: admin/admin.php:4158
+msgid "View details"
 msgstr ""
 
-#: admin/admin.php:3814
-#: admin/admin.php:3842
+#: admin/admin.php:4185
+#: admin/admin.php:4213
 msgid "Help Me Fix"
 msgstr ""
 
 #. translators: %s: caching plugin name.
-#: admin/admin.php:3838
+#: admin/admin.php:4209
 msgid "%s may interfere with your forms."
 msgstr ""
 
-#: admin/admin.php:3841
+#: admin/admin.php:4212
 msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order."
 msgstr ""
 
-#: admin/admin.php:3887
+#: admin/admin.php:4258
 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept."
 msgstr ""
 
-#: admin/admin.php:3890
+#: admin/admin.php:4261
 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here."
 msgstr ""
 
-#: admin/admin.php:4133
+#: admin/admin.php:4504
 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions."
 msgstr ""
 
 #. translators: %s: form name.
-#: admin/admin.php:4138
+#: admin/admin.php:4509
 msgid "Finish setting up “%s”"
 msgstr ""
 
-#: admin/admin.php:4151
+#: admin/admin.php:4522
 msgid "Edit the Thank You message"
 msgstr ""
 
-#: admin/admin.php:4157
+#: admin/admin.php:4528
 msgid "Set where replies go"
 msgstr ""
 
-#: admin/admin.php:4307
+#: admin/admin.php:4678
 msgid "Use Conditional Logic to show only what matters"
 msgstr ""
 
-#: admin/admin.php:4308
+#: admin/admin.php:4679
 msgid "Split your form into steps to keep it easy"
 msgstr ""
 
-#: admin/admin.php:4309
+#: admin/admin.php:4680
 msgid "Let people upload files directly to your form"
 msgstr ""
 
-#: admin/admin.php:4310
+#: admin/admin.php:4681
 msgid "Turn responses into downloadable PDFs automatically"
 msgstr ""
 
-#: admin/admin.php:4311
+#: admin/admin.php:4682
 msgid "Let users sign with a simple signature field"
 msgstr ""
 
-#: admin/admin.php:4312
+#: admin/admin.php:4683
 msgid "Connect your form to other tools using webhooks"
 msgstr ""
 
-#: admin/admin.php:4313
+#: admin/admin.php:4684
 msgid "Use Conversational Forms for a chat-like experience"
 msgstr ""
 
-#: admin/admin.php:4314
+#: admin/admin.php:4685
 msgid "Let users register or log in through your form"
 msgstr ""
 
-#: admin/admin.php:4315
+#: admin/admin.php:4686
 msgid "Build forms that create WordPress user accounts"
 msgstr ""
 
-#: admin/admin.php:4316
+#: admin/admin.php:4687
 msgid "Add calculations to auto-total scores or prices"
 msgstr ""
 
-#: admin/admin.php:4538
+#: admin/admin.php:4990
 msgid "Debug log: no entries recorded."
 msgstr ""
 
 #. translators: 1: entries shown, 2: entries recorded.
-#: admin/admin.php:4542
+#: admin/admin.php:4995
 msgid "Debug log (most recent %1$d of %2$d entries)"
 msgstr ""
 
-#: admin/admin.php:4553
-msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General."
+#: admin/admin.php:5005
+msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General."
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4589
+#: admin/admin.php:5032
 msgid "SureForms: form submissions are failing on %s"
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4591
+#: admin/admin.php:5034
 msgid "SureForms has recorded form submissions on %s that could not be completed."
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4595
+#: admin/admin.php:5038
 msgid "SureForms: notification emails are not being sent on %s"
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4597
+#: admin/admin.php:5040
 msgid "SureForms saved entries on %s but could not send the notification emails for them."
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4601
+#: admin/admin.php:5044
 msgid "SureForms: an integration is not receiving entries on %s"
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4603
+#: admin/admin.php:5046
 msgid "SureForms saved entries on %s but could not pass them to a connected service."
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4613
+#: admin/admin.php:5056
 msgid "SureForms: a problem with the forms on %s"
 msgstr ""
 
 #. translators: %s: site host.
-#: admin/admin.php:4615
+#: admin/admin.php:5058
 msgid "SureForms has recorded a problem with the forms on %s."
 msgstr ""
 
 #. translators: %d: number of failed submissions.
-#: admin/admin.php:4640
+#: admin/admin.php:5083
 msgid "SureForms has recorded %d form submission that could not be completed."
 msgid_plural "SureForms has recorded %d form submissions that could not be completed."
 msgstr[0] ""
 msgstr[1] ""
 
 #. translators: %d: number of failed notifications.
-#: admin/admin.php:4652
+#: admin/admin.php:5095
 msgid "SureForms saved %d entry but could not send the notification email for it."
 msgid_plural "SureForms saved %d entries but could not send the notification emails for them."
 msgstr[0] ""
 msgstr[1] ""
 
 #. translators: %d: number of failed integration hand-offs.
-#: admin/admin.php:4664
+#: admin/admin.php:5107
 msgid "SureForms saved %d entry but could not pass it to a connected service."
 msgid_plural "SureForms saved %d entries but could not pass them to a connected service."
 msgstr[0] ""
 msgstr[1] ""
 
 #. translators: %d: number of recorded problems.
-#: admin/admin.php:4676
+#: admin/admin.php:5119
 msgid "SureForms has recorded %d problem with the forms on this site."
 msgid_plural "SureForms has recorded %d problems with the forms on this site."
 msgstr[0] ""
 msgstr[1] ""
 
-#: admin/admin.php:4715
+#: admin/admin.php:5158
 msgid "Hello SureForms support,"
 msgstr ""
 
 #. translators: %s: form title.
-#: admin/admin.php:4726
+#: admin/admin.php:5169
 msgid "Form: %s"
 msgstr ""
 
-#: admin/admin.php:4739
+#: admin/admin.php:5182
 msgid "Site details"
 msgstr ""
 
-#: admin/admin.php:4742
+#: admin/admin.php:5185
 msgid "not active"
 msgstr ""
 
-#: admin/admin.php:4745
+#: admin/admin.php:5188
 msgid "none detected"
 msgstr ""
 
-#: admin/admin.php:4746
+#: admin/admin.php:5189
 msgid "none recorded"
 msgstr ""
 
+#. translators: %s: date and time of the previous report, in the site's timezone.
+#: admin/admin.php:5212
+msgid "Previously reported: %s"
+msgstr ""
+
 #: inc/abilities/abilities-registrar.php:107
 msgid "SureForms MCP Server"
 msgstr ""
@@ -1478,7 +1544,7 @@ msgstr ""
 #: inc/duplicate-form.php:191
 #: inc/export.php:126
 #: inc/export.php:184
-#: inc/forms-data.php:88
+#: inc/forms-data.php:108
 #: inc/global-settings/global-settings.php:89
 #: inc/global-settings/global-settings.php:639
 #: inc/rest-api.php:203
@@ -1985,24 +2051,24 @@ msgstr ""
 msgid "Unable to import form."
 msgstr ""
 
-#: inc/field-validation.php:413
+#: inc/field-validation.php:452
 msgid "Field is not valid."
 msgstr ""
 
 #. translators: %s represents the minimum number of characters required
-#: inc/field-validation.php:426
+#: inc/field-validation.php:465
 #: inc/translatable.php:94
 msgid "Please enter at least %s characters."
 msgstr ""
 
 #. translators: %s: maximum characters allowed before the @ symbol.
-#: inc/field-validation.php:448
+#: inc/field-validation.php:487
 #: inc/translatable.php:97
 msgid "The part before @ may not exceed %s characters."
 msgstr ""
 
 #. translators: %s: maximum characters allowed after the @ symbol.
-#: inc/field-validation.php:454
+#: inc/field-validation.php:493
 #: inc/translatable.php:100
 msgid "The part after @ may not exceed %s characters."
 msgstr ""
@@ -2284,7 +2350,7 @@ msgstr ""
 msgid "%s verification failed. Please contact your site administrator."
 msgstr ""
 
-#: inc/forms-data.php:68
+#: inc/forms-data.php:88
 msgid "Sorry, you are not allowed to view the form."
 msgstr ""
 
@@ -13195,11 +13261,6 @@ msgstr ""
 msgid "Upgrade to Unlock"
 msgstr ""
 
-#: assets/build/blocks.js:172
-#: modules/gutenberg/build/blocks.js:4
-msgid "Close"
-msgstr ""
-
 #: assets/build/blocks.js:172
 #: assets/build/formEditor.js:172
 #: modules/gutenberg/build/blocks.js:6
@@ -14556,10 +14617,6 @@ msgstr ""
 msgid "SureForms %s"
 msgstr ""
 
-#: assets/build/dashboard.js:172
-msgid "Copied"
-msgstr ""
-
 #: assets/build/dashboard.js:172
 msgid "First name is required."
 msgstr ""
@@ -16601,7 +16658,7 @@ msgid "This form could not be submitted. Please refresh the page and try again."
 msgstr ""
 
 #: assets/build/formSubmit.js:2
-#: assets/js/unminified/form-submit.js:932
+#: assets/js/unminified/form-submit.js:938
 msgid "An error occurred while submitting your form. Please try again."
 msgstr ""
 
@@ -17316,6 +17373,10 @@ msgstr ""
 msgid "Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns — counting continues, so the figures are up to date if you switch it back on."
 msgstr ""
 
+#: assets/build/settings.js:172
+msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures."
+msgstr ""
+
 #: assets/build/settings.js:172
 msgid "Help shape the future of SureForms"
 msgstr ""
diff --git a/package-lock.json b/package-lock.json
index a34dc2ae0..9fa5924cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
 	"name": "sureforms",
-	"version": "2.12.6",
+	"version": "2.12.7",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "sureforms",
-			"version": "2.12.6",
+			"version": "2.12.7",
 			"license": "GPL",
 			"dependencies": {
 				"@bsf/force-ui": "git+https://github.com/brainstormforce/bsf-admin-ui#v1.7.7",
diff --git a/package.json b/package.json
index d57cecb9e..59193eff2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "sureforms",
-	"version": "2.12.6",
+	"version": "2.12.7",
 	"description": "A simple yet powerful way to add forms to your website.",
 	"main": "index.js",
 	"scripts": {
diff --git a/readme.txt b/readme.txt
index e05aac30a..76d18d5a7 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: forms, ai forms, contact form, form builder, payment form
 Requires at least: 6.4
 Tested up to: 7.1
 Requires PHP: 7.4
-Stable tag: 2.12.6
+Stable tag: 2.12.7
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -426,6 +426,14 @@ Visit the [SureForms features page](https://sureforms.com/features/?utm_source=w
 You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to validate, triage, and handle security reports.
 
 == Changelog ==
+= 2.12.7 - 11th September 2026 =
+* Improvement: Submission failure notices now offer View details, so you can read and copy the full diagnostics before contacting support.
+* Improvement: The Form Checks panel now appears only when something genuinely needs attention, with clearer wording, keeping the sidebar focused.
+* Fix: After-submission actions now complete reliably when a form redirects on success.
+* Fix: Form pages now keep their styles and submit script on sites using page builders that buffer page output, such as Breakdance.
+* Fix: Form submissions no longer drop fields when a caching plugin serves an older copy of the page, so every submitted field is saved to the entry.
+* Fix: The conversion rate no longer counts submissions made by site editors while previewing a form.
+* Fix: The Edit Form icon is no longer oversized in Divi.
 = 2.12.6 - 2nd September 2026 =
 * New: Added an Enable Logs option that records form submission failures to a downloadable file, making it easier to diagnose issues on a live site.
 * New: Added Views and Conversion Rate columns to the Forms list, so you can see how each form is performing at a glance.
@@ -434,9 +442,6 @@ You can report security issues through our [Bug Bounty Program](https://brainsto
 * Fix: This update addressed a security bug. Props to Patchstack for reporting it responsibly to our team.
 = 2.12.5 - 25th August 2026 =
 * Fix: Form submissions now go through reliably, even on sites where a performance plugin combines or defers JavaScript.
-= 2.12.4 - 19th August 2026 =
-* Improvement: Email notification images are now saved to the Media Library instead of being embedded as base64, improving email deliverability and reducing message size.
-* Fix: Compatibility with WordPress 7.1.
 The full changelog is available [here](https://sureforms.com/whats-new/?utm_source=wordpress.org&utm_medium=whats_new).
 
 == Upgrade Notice ==
\ No newline at end of file
diff --git a/sureforms.php b/sureforms.php
index d1d3b78a5..0ffea7f66 100644
--- a/sureforms.php
+++ b/sureforms.php
@@ -7,7 +7,7 @@
  * Requires PHP: 7.4
  * Author: SureForms
  * Author URI: https://sureforms.com/
- * Version: 2.12.6
+ * Version: 2.12.7
  * License: GPLv2 or later
  * Text Domain: sureforms
  *
@@ -25,7 +25,7 @@
 define( 'SRFM_BASENAME', plugin_basename( SRFM_FILE ) );
 define( 'SRFM_DIR', plugin_dir_path( SRFM_FILE ) );
 define( 'SRFM_URL', plugins_url( '/', SRFM_FILE ) );
-define( 'SRFM_VER', '2.12.6' );
+define( 'SRFM_VER', '2.12.7' );
 define( 'SRFM_SLUG', 'srfm' );
 // ------ ADDITIONAL CONSTANTS ------- //
 define( 'SRFM_FORMS_POST_TYPE', 'sureforms_form' );
@@ -35,7 +35,7 @@
 define( 'SRFM_AI_MIDDLEWARE', 'https://credits.startertemplates.com/sureforms/' );
 define( 'SRFM_MIDDLEWARE_BASE_URL', 'https://api.sureforms.com/' );
 define( 'SRFM_BILLING_PORTAL', 'https://billing.sureforms.com/' );
-define( 'SRFM_PRO_RECOMMENDED_VER', '2.12.6' );
+define( 'SRFM_PRO_RECOMMENDED_VER', '2.12.7' );
 
 define( 'SRFM_SURETRIGGERS_INTEGRATION_BASE_URL', 'https://app.ottokit.com/' );
 

From ed71dffcda5002c42a4ff328716df5a63eef4f15 Mon Sep 17 00:00:00 2001
From: Avinash Kumar Sharma 
Date: Fri, 11 Sep 2026 19:46:23 +0530
Subject: [PATCH 48/58] feat: add and improve descriptions in General Settings

- Add a short description to the Enable email summaries toggle
- Shorten the Show views and conversion rate description
- Remove em dash from Show views and conversion rate description

Co-Authored-By: Claude Sonnet 4.6 
---
 src/admin/settings/pages/General.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js
index d333ea15c..84b75e7b5 100644
--- a/src/admin/settings/pages/General.js
+++ b/src/admin/settings/pages/General.js
@@ -35,6 +35,10 @@ const EmailSummariesContent = ( { emailTabOptions, updateGlobalSettings } ) => {
 			
@@ -234,7 +238,7 @@ const FormViewsTrackingContent = ( {
 					'sureforms'
 				),
 				description: __(
-					'Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns — counting continues, so the figures are up to date if you switch it back on.',
+					'Adds Views and Conversion Rate columns to the Forms list. Disabling hides the columns but does not stop tracking.',
 					'sureforms'
 				),
 			} }

From 3476c2319ee3834e6be8c5a7b14c4a11b1ea90b3 Mon Sep 17 00:00:00 2001
From: Avinash Kumar Sharma 
Date: Fri, 11 Sep 2026 21:26:37 +0530
Subject: [PATCH 49/58] fix: remove em dash from partial entries empty state
 text

Replace em dash with a comma in the feature bullet point copy for
consistency with the rest of the UI.

Co-Authored-By: Claude Sonnet 4.6 
---
 src/admin/PartialEntriesEmptyState.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/admin/PartialEntriesEmptyState.js b/src/admin/PartialEntriesEmptyState.js
index 8254b8475..3558e1b04 100644
--- a/src/admin/PartialEntriesEmptyState.js
+++ b/src/admin/PartialEntriesEmptyState.js
@@ -18,7 +18,7 @@ import './tw-base.scss';
 
 const features = [
 	__(
-		'Capture in-progress form data the moment visitors stop typing — no submit required',
+		'Capture in-progress form data the moment visitors stop typing, no submit required',
 		'sureforms'
 	),
 	__(

From ad52087f347389ddaf87125c49b320319ce3b21a Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Sat, 12 Sep 2026 09:06:07 +0530
Subject: [PATCH 50/58] fix: one block walk per submission, and keep own frames
 on a non-default port

Both from adi3890's review of #3129, where he scoped them to 2.12.8 rather than
blocking the release.

**walk_field_identifiers() ran twice per submission.** It returns ids and slugs
together precisely so a form is parsed once, but get_known_field_block_ids()
and get_known_field_slugs() each held their own `static $cache`, so
parse_blocks() plus the recursive collect ran once for each -- on every
submission, on markup that can be large. One shared memo in
get_field_identifiers() now. Only the walk is memoised, unchanged: the filtered
results stay uncached so a third party returning a malformed value cannot
poison the set for the rest of the request.

**The log scrubber truncated our own stack frames on any non-default port.**
The URL pattern captures the whole authority, so a frame from a site on :8443
reads `example.test:8443`, while $site_host comes from PHP_URL_HOST and never
carries a port. Every own frame failed the same-origin check and was cut to
/[path] -- deleting the filename, the line and the column, which is the exact
loss that exemption exists to prevent. Local installs, staging behind a proxy
and anything off 80/443 were all affected.

The port is stripped before comparing, trailing :digits only so an IPv6 literal
keeps its brackets and its own colons. Same host on a different port is treated
as ours: on a WordPress install that is the same site behind a dev server or a
proxy, and the alternative is deleting the diagnosis.

Both tests revert-tested. The scrubber one needed a home_url filter -- the
existing provider builds its frames from home_url(), which has no port on the
test site, so both sides of the comparison matched by accident and it could
never have caught this. The walk one observes rather than counts: the content
is removed between the two calls, so a second walk has nothing to find.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 inc/client-logger.php                    | 15 ++++++-
 inc/field-validation.php                 | 51 +++++++++++++++---------
 tests/unit/inc/test-client-logger.php    | 41 +++++++++++++++++++
 tests/unit/inc/test-field-validation.php | 42 +++++++++++++++++++
 4 files changed, 130 insertions(+), 19 deletions(-)

diff --git a/inc/client-logger.php b/inc/client-logger.php
index 57b7109e8..725574c62 100644
--- a/inc/client-logger.php
+++ b/inc/client-logger.php
@@ -752,10 +752,23 @@ public static function scrub_text( $text ) {
 		// error.stack.split( "\n" )[1] -- so truncating our own paths deletes the
 		// filename, the line and column, and which plugin threw, which is the
 		// whole diagnosis. A third-party credential is never same-origin.
+		//
+		// The port is stripped before comparing. The pattern captures the whole
+		// authority, so a site served on a non-default port produced frames reading
+		// `example.test:8443`, while $site_host is PHP_URL_HOST and never carries a
+		// port -- every own frame failed the check and was truncated to /[path],
+		// which is the case this exemption exists for. Same host, different port is
+		// treated as ours: on a WordPress install that is the same site behind a dev
+		// server or a proxy, and the alternative is deleting the diagnosis.
 		$text = (string) preg_replace_callback(
 			'#(https?://)([^\s/]+)((?:/[^\s/]*)?)/[^\s"\'<>,;)\]}]+#i',
 			static function ( $matches ) use ( $site_host ) {
-				if ( '' !== $site_host && 0 === strcasecmp( $matches[2], $site_host ) ) {
+				// Trailing :digits only, so an IPv6 literal keeps its brackets and
+				// its own colons -- [::1]:8080 becomes [::1], which is the form
+				// wp_parse_url() returns for one.
+				$host = (string) preg_replace( '/:\d+$/', '', $matches[2] );
+
+				if ( '' !== $site_host && 0 === strcasecmp( $host, $site_host ) ) {
 					return $matches[0];
 				}
 
diff --git a/inc/field-validation.php b/inc/field-validation.php
index c6b4de93c..1de07896d 100644
--- a/inc/field-validation.php
+++ b/inc/field-validation.php
@@ -219,20 +219,12 @@ public static function prepared_validation_data( $current_form_id ) {
 	 *                            blocks could not be derived (callers should fail open).
 	 */
 	public static function get_known_field_block_ids( $form_id ) {
-		static $cache = [];
-
 		$form_id = Helper::get_integer_value( $form_id );
 		if ( $form_id <= 0 ) {
 			return [];
 		}
 
-		// Only the block walk is memoised. The filtered result deliberately is not: a
-		// third party returning a malformed value would otherwise poison the set for the
-		// rest of the request, and because the lookup short-circuits on isset() the walk
-		// would never be retried.
-		if ( ! isset( $cache[ $form_id ] ) ) {
-			$cache[ $form_id ] = self::walk_field_identifiers( $form_id );
-		}
+		$walked = self::get_field_identifiers( $form_id );
 
 		/**
 		 * Filter the set of block ids considered valid for a form during submission.
@@ -248,14 +240,14 @@ public static function get_known_field_block_ids( $form_id ) {
 		 * @param array $ids     Map of known block id => true.
 		 * @param int                $form_id The form post id.
 		 */
-		$ids = apply_filters( 'srfm_known_field_block_ids', $cache[ $form_id ]['ids'], $form_id );
+		$ids = apply_filters( 'srfm_known_field_block_ids', $walked['ids'], $form_id );
 
 		// Coerce defensively. A callback returning a list (`[ 'aaa', 'bbb' ]`) rather
 		// than a map would otherwise make every real field look unknown, and a non-array
 		// return would drop the whole allowlist — so fall back to the walked set instead
 		// of silently turning the check off.
 		if ( ! is_array( $ids ) ) {
-			return $cache[ $form_id ]['ids'];
+			return $walked['ids'];
 		}
 
 		return wp_is_numeric_array( $ids ) ? array_fill_keys( array_map( 'strval', $ids ), true ) : $ids;
@@ -274,16 +266,12 @@ public static function get_known_field_block_ids( $form_id ) {
 	 * @return array Map of known slug => true.
 	 */
 	public static function get_known_field_slugs( $form_id ) {
-		static $cache = [];
-
 		$form_id = Helper::get_integer_value( $form_id );
 		if ( $form_id <= 0 ) {
 			return [];
 		}
 
-		if ( ! isset( $cache[ $form_id ] ) ) {
-			$cache[ $form_id ] = self::walk_field_identifiers( $form_id );
-		}
+		$walked = self::get_field_identifiers( $form_id );
 
 		/**
 		 * Filter the set of field slugs considered valid for a form during submission.
@@ -292,10 +280,10 @@ public static function get_known_field_slugs( $form_id ) {
 		 * @param array $slugs   Map of known slug => true.
 		 * @param int                $form_id The form post id.
 		 */
-		$slugs = apply_filters( 'srfm_known_field_slugs', $cache[ $form_id ]['slugs'], $form_id );
+		$slugs = apply_filters( 'srfm_known_field_slugs', $walked['slugs'], $form_id );
 
 		if ( ! is_array( $slugs ) ) {
-			return $cache[ $form_id ]['slugs'];
+			return $walked['slugs'];
 		}
 
 		return wp_is_numeric_array( $slugs ) ? array_fill_keys( array_map( 'strval', $slugs ), true ) : $slugs;
@@ -536,6 +524,33 @@ public static function get_email_char_limits() {
 		];
 	}
 
+	/**
+	 * The walked allowlists for one form, memoised for the request.
+	 *
+	 * One cache, not one per getter. Both public getters need the same walk, and each
+	 * holding its own `static $cache` meant parse_blocks() plus the recursive collect
+	 * ran twice for every submission -- once for the ids and again for the slugs --
+	 * on a form whose markup can be large.
+	 *
+	 * Only the walk is memoised. The filtered results deliberately are not: a third
+	 * party returning a malformed value would otherwise poison the set for the rest of
+	 * the request, and because the lookup short-circuits on isset() the walk would
+	 * never be retried.
+	 *
+	 * @param int $form_id The form post id, already normalised by the callers.
+	 * @since 2.12.8
+	 * @return array{ids:array,slugs:array}
+	 */
+	private static function get_field_identifiers( $form_id ) {
+		static $cache = [];
+
+		if ( ! isset( $cache[ $form_id ] ) ) {
+			$cache[ $form_id ] = self::walk_field_identifiers( $form_id );
+		}
+
+		return $cache[ $form_id ];
+	}
+
 	/**
 	 * Walk a form's blocks once, returning both the block-id and slug allowlists.
 	 *
diff --git a/tests/unit/inc/test-client-logger.php b/tests/unit/inc/test-client-logger.php
index eaf8821d8..cac7a1317 100644
--- a/tests/unit/inc/test-client-logger.php
+++ b/tests/unit/inc/test-client-logger.php
@@ -387,6 +387,47 @@ public function provide_diagnostics() {
 		];
 	}
 
+	/**
+	 * A site on a non-default port keeps its own stack frames.
+	 *
+	 * The URL pattern captures the whole authority, so a frame from a site served on
+	 * :8443 arrives as `example.test:8443`, while $site_host is PHP_URL_HOST and never
+	 * carries a port. Every own frame therefore failed the same-origin check and was
+	 * truncated to /[path] -- deleting the filename, the line and the column, which is
+	 * the exact loss this exemption exists to prevent. Local installs, staging behind a
+	 * proxy and anything not on 80/443 were all affected.
+	 *
+	 * The existing provider cannot catch this: it builds its frames from home_url(),
+	 * which has no port on the test site, so both sides of the comparison match by
+	 * accident.
+	 */
+	public function test_scrub_text_keeps_own_frames_on_a_non_default_port() {
+		$home = static function () {
+			return 'https://example.test:8443';
+		};
+
+		add_filter( 'home_url', $home, 99 );
+
+		$own = Client_Logger::scrub_text(
+			'at handleSubmit (https://example.test:8443/wp-content/plugins/sureforms/assets/js/form-submit.min.js:12:3456)'
+		);
+
+		// A foreign host, same port, so the port strip cannot have turned the
+		// exemption into "anything carrying a port is ours".
+		$foreign = Client_Logger::scrub_text(
+			'POST https://hooks.slack.com:8443/services/T0/B0/tok failed'
+		);
+
+		remove_filter( 'home_url', $home, 99 );
+
+		$this->assertStringContainsString( 'form-submit.min.js', $own, 'The filename is the diagnosis.' );
+		$this->assertStringContainsString( ':12:3456', $own, 'So are the line and column.' );
+		$this->assertStringNotContainsString( '[path]', $own, 'Our own frame must not be truncated.' );
+
+		$this->assertStringContainsString( 'hooks.slack.com', $foreign, 'The origin still identifies the third party.' );
+		$this->assertStringNotContainsString( 'tok', $foreign, 'A foreign path may still hold a credential.' );
+	}
+
 	/**
 	 * A phone number is written 555-123-4567, not 5551234567, so a
 	 * contiguous-digits rule never sees a real one. The earlier test only used an
diff --git a/tests/unit/inc/test-field-validation.php b/tests/unit/inc/test-field-validation.php
index 747e4f062..d0267f595 100644
--- a/tests/unit/inc/test-field-validation.php
+++ b/tests/unit/inc/test-field-validation.php
@@ -535,6 +535,48 @@ public function test_strip_unknown_field_keys_keeps_non_alphanumeric_block_id()
 		wp_delete_post( $form_id, true );
 	}
 
+	/**
+	 * Both allowlists come from one walk, not one each.
+	 *
+	 * walk_field_identifiers() returns ids and slugs together precisely so the form is
+	 * parsed once, but each public getter used to hold its own `static $cache`, so
+	 * parse_blocks() plus the recursive collect ran twice on every submission.
+	 *
+	 * Observed rather than counted: the content is removed between the two calls, so a
+	 * second walk has nothing to find. With one shared memo the slugs are already
+	 * cached and survive; with a cache per getter the second call re-walks an empty
+	 * form and returns nothing.
+	 */
+	public function test_both_allowlists_come_from_a_single_walk() {
+		$form_id = wp_insert_post(
+			[
+				'post_type'    => 'sureforms_form',
+				'post_status'  => 'publish',
+				'post_title'   => 'One Walk',
+				'post_content' => '',
+			]
+		);
+
+		$ids = Field_Validation::get_known_field_block_ids( $form_id );
+		$this->assertArrayHasKey( 'aaa111', $ids, 'Fixture must produce a block id to begin with.' );
+
+		// Anything a second walk would read is now gone.
+		wp_update_post(
+			[
+				'ID'           => $form_id,
+				'post_content' => '',
+			]
+		);
+
+		$this->assertArrayHasKey(
+			'finish-time',
+			Field_Validation::get_known_field_slugs( $form_id ),
+			'The slugs must come from the walk the block-id call already did, not from a second one.'
+		);
+
+		wp_delete_post( $form_id, true );
+	}
+
 	/**
 	 * get_known_field_slugs() collects the slug of every SureForms field in the form,
 	 * recursing into innerBlocks, so a submitted field can be matched by its stable

From a3dda7bc5e45097e4fe6c338e75df51bcac7ee8f Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Sat, 12 Sep 2026 09:55:33 +0530
Subject: [PATCH 51/58] fix: only alarm on after-submission failures the server
 confirmed

From Vansh, reading a real debug log: a form with nothing wrong was raising
"We noticed a form submission failure", and the log behind it was full of
`After-submission step failed: TypeError: Load failed`.

That step runs *after* the entry is saved -- the comment directly above the
logging call already said "The submission itself succeeded" -- but it logged
type 'error', which is_fault() counts unconditionally, and append() files every
fault it is handed under 'submission'. So the notice read "Visitors may not be
able to reach you, and their entries were not saved" about entries that were
saved, to visitors who saw the success message, on the one notice that cannot
be dismissed.

"Load failed" is Safari's message for a fetch the browser abandoned, which is
exactly what a redirect confirmation does to an in-flight request and exactly
what the keepalive above it exists to survive. The browser noticing it lost the
response says nothing about whether the server ran the work -- the endpoint is
guarded by is_after_submission_process_triggered and usually has.

Both after-submission paths now log type 'after_submission':

- Without a status -- the browser-side abort -- that is never a fault. Still
  logged, so the diagnosis survives, the way 'blocked' already is.
- With a 5xx or 403, the server said it failed, and that is a fault. Recorded
  against 'integration', not 'submission': srfm_after_submission_process is
  where integrations and webhooks hook in, so "a third party is not receiving
  them" is the true sentence. The category is derived server-side from the
  type -- the client names what happened, the server decides what it means.

Three tests, mutation-tested rather than just revert-tested: treating
after_submission as an unconditional fault fails the first, and filing it under
'submission' fails the second. The third pins 'blocked' staying silent, which
is the precedent this follows and which nothing covered.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 assets/js/unminified/form-submit.js   | 17 +++++-
 inc/client-logger.php                 | 24 ++++++--
 tests/unit/inc/test-client-logger.php | 88 +++++++++++++++++++++++++++
 3 files changed, 123 insertions(+), 6 deletions(-)

diff --git a/assets/js/unminified/form-submit.js b/assets/js/unminified/form-submit.js
index ce8cd6215..8135ce5c0 100644
--- a/assets/js/unminified/form-submit.js
+++ b/assets/js/unminified/form-submit.js
@@ -814,8 +814,12 @@ async function afterSubmit( formStatus, form ) {
 		await parseRestResponse( response );
 
 		if ( ! response.ok ) {
+			// Its own type, not 'network'. The entry is already saved by the time
+			// this runs, so a failure here is the after-submission process --
+			// integrations and webhooks -- not the submission. Recorded server-side
+			// against the integration category for that reason.
 			srfmLog.add( {
-				type: 'network',
+				type: 'after_submission',
 				status,
 				duration_ms: durationMs,
 				message: `After-submission step responded ${ status }`,
@@ -832,8 +836,17 @@ async function afterSubmit( formStatus, form ) {
 
 		// The submission itself succeeded, so nothing is shown to the visitor and
 		// this would otherwise be invisible outside the console.
+		//
+		// Logged without a status, which is what keeps it out of is_fault(). We
+		// caught the browser abandoning the request -- the page unloading behind a
+		// redirect confirmation is the usual cause, and it is what keepalive above
+		// exists to survive -- and that says nothing about whether the server ran
+		// the work. It is guarded by is_after_submission_process_triggered and
+		// usually has. Safari spells this abort "TypeError: Load failed", and
+		// raising a non-dismissible "their entries were not saved" notice off it
+		// reported healthy sites as broken.
 		srfmLog.add( {
-			type: 'error',
+			type: 'after_submission',
 			duration_ms: Math.round( performance.now() - startedAt ),
 			message: `After-submission step failed: ${
 				error?.name ?? 'Error'
diff --git a/inc/client-logger.php b/inc/client-logger.php
index 725574c62..098a76fb8 100644
--- a/inc/client-logger.php
+++ b/inc/client-logger.php
@@ -169,7 +169,14 @@ public static function is_fault( array $entry ) {
 			return true;
 		}
 
-		if ( 'network' !== $type ) {
+		// 'after_submission' runs after the entry is already saved, so it is only a
+		// fault when the server said so. A browser-side failure there -- an aborted
+		// fetch as the page unloads, which is what a redirect confirmation does and
+		// what `keepalive` exists to survive -- tells us nothing about whether the
+		// work ran: the endpoint is guarded by is_after_submission_process_triggered
+		// and usually has. Safari spells that abort "TypeError: Load failed", and
+		// alarming on it reported healthy sites as broken.
+		if ( ! in_array( $type, [ 'network', 'after_submission' ], true ) ) {
 			return false;
 		}
 
@@ -449,9 +456,18 @@ public static function append( array $entry ) {
 		// on a badly broken site those are exactly the conditions that occur.
 		// A notification or integration failure records its own category at the call
 		// site; everything else reaching here is the submission itself.
-		if ( self::is_fault( $entry ) && 'message' !== ( $entry['type'] ?? '' ) ) {
+		$type = Helper::get_string_value( $entry['type'] ?? '' );
+
+		if ( self::is_fault( $entry ) && 'message' !== $type ) {
+			// The after-submission step runs on an entry that is already saved and
+			// fires srfm_after_submission_process, which is where integrations and
+			// webhooks hook in. Calling that a submission failure told the site owner
+			// "their entries were not saved" about entries that were -- the wrong
+			// message on the one notice that cannot be dismissed. The category is
+			// derived here rather than taken from the entry: the client names what
+			// happened, the server decides what it means.
 			self::record_failure(
-				'submission',
+				'after_submission' === $type ? 'integration' : 'submission',
 				Helper::get_integer_value( $entry['form_id'] ?? 0 ),
 				Helper::get_string_value( $entry['form_title'] ?? '' )
 			);
@@ -626,7 +642,7 @@ public static function clear() {
 	 * @return array Empty when nothing usable survived.
 	 */
 	public static function sanitize_entry( array $raw ) {
-		$allowed_types = [ 'network', 'response', 'error', 'message', 'blocked' ];
+		$allowed_types = [ 'network', 'response', 'error', 'message', 'blocked', 'after_submission' ];
 		$type          = isset( $raw['type'] ) ? sanitize_key( Helper::get_string_value( $raw['type'] ) ) : '';
 
 		if ( ! in_array( $type, $allowed_types, true ) ) {
diff --git a/tests/unit/inc/test-client-logger.php b/tests/unit/inc/test-client-logger.php
index cac7a1317..cfc0de514 100644
--- a/tests/unit/inc/test-client-logger.php
+++ b/tests/unit/inc/test-client-logger.php
@@ -387,6 +387,94 @@ public function provide_diagnostics() {
 		];
 	}
 
+	/**
+	 * An after-submission failure the browser only guessed at raises no notice.
+	 *
+	 * The step runs after the entry is saved. When the client reports it without a
+	 * status it is reporting an abandoned fetch -- the page unloading behind a
+	 * redirect confirmation, which is what keepalive exists to survive -- and that
+	 * says nothing about whether the server ran srfm_after_submission_process. It is
+	 * guarded by is_after_submission_process_triggered and usually has.
+	 *
+	 * This shipped as type 'error', which is_fault() counts unconditionally, so every
+	 * Safari "TypeError: Load failed" raised a non-dismissible notice reading
+	 * "Visitors may not be able to reach you, and their entries were not saved" about
+	 * entries that were saved and visitors who saw the success message.
+	 */
+	public function test_a_browser_side_after_submission_failure_is_not_a_fault() {
+		$entry = Client_Logger::sanitize_entry(
+			[
+				'type'    => 'after_submission',
+				'form_id' => 4355,
+				'message' => 'After-submission step failed: TypeError: Load failed',
+			]
+		);
+
+		$this->assertNotEmpty( $entry, 'The type must survive sanitisation, or it is never logged at all.' );
+		$this->assertFalse(
+			Client_Logger::is_fault( $entry ),
+			'A fetch the browser abandoned is not evidence the server did not do the work.'
+		);
+	}
+
+	/**
+	 * A server-confirmed after-submission failure is a fault, and an integration one.
+	 *
+	 * srfm_after_submission_process is where integrations and webhooks hook in, so a
+	 * 5xx there means a third party did not receive the entry -- which is what the
+	 * integration notice says. The submission notice says entries were not saved, and
+	 * by this point they have been.
+	 */
+	public function test_a_server_confirmed_after_submission_failure_is_an_integration_fault() {
+		delete_option( Client_Logger::FAILURES_OPTION );
+		Client_Logger::clear();
+
+		$entry = Client_Logger::sanitize_entry(
+			[
+				'type'    => 'after_submission',
+				'status'  => 500,
+				'form_id' => 4355,
+				'message' => 'After-submission step responded 500',
+			]
+		);
+
+		$this->assertTrue( Client_Logger::is_fault( $entry ), 'The server said it failed.' );
+
+		Client_Logger::append( $entry );
+
+		$open = Client_Logger::get_open_failures();
+
+		$this->assertArrayHasKey( 'integration', $open, 'A failed after-submission step is an integration failure.' );
+		$this->assertArrayNotHasKey(
+			'submission',
+			$open,
+			'It must not claim the entry was never saved -- it was saved before this step ran.'
+		);
+
+		delete_option( Client_Logger::FAILURES_OPTION );
+		Client_Logger::clear();
+	}
+
+	/**
+	 * A captcha stop is the visitor's to clear, and raises nothing.
+	 *
+	 * Pinned because it is the precedent the after-submission rule above follows, and
+	 * because "Please verify that you are not a robot" is among the most common lines
+	 * in a real log -- counting it would tell healthy sites to contact support.
+	 */
+	public function test_a_blocked_entry_is_not_a_fault() {
+		$entry = Client_Logger::sanitize_entry(
+			[
+				'type'    => 'blocked',
+				'form_id' => 4355,
+				'message' => 'Blocked before submit: Please verify that you are not a robot.',
+			]
+		);
+
+		$this->assertNotEmpty( $entry );
+		$this->assertFalse( Client_Logger::is_fault( $entry ) );
+	}
+
 	/**
 	 * A site on a non-default port keeps its own stack frames.
 	 *

From 28acd8842f4c8e924446372efe2c91296ce6fda0 Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Sat, 12 Sep 2026 10:22:41 +0530
Subject: [PATCH 52/58] i18n: translate the support report's labels, leave the
 debug log verbatim

Vansh, reading a copied report: the prose translates but the diagnostic block's
labels did not, so a German site produced a half-translated document -- and
oddly, the *values* beside those labels were already translatable while the
labels themselves were bare English:

    'Caching: ' . ( '' !== $caching ? $caching : __( 'none detected' ) )

Seven labels wrapped: Site, SureForms, SureForms Pro, WordPress, PHP, Caching,
Recorded failures. The line the site owner reads is copy and belongs in the
catalogue.

What deliberately does not move:

- The values beside them. A version, a URL, a plugin name are machine data.
- The debug log block. Its JSON is the raw record support greps, and rewriting
  any of it would defeat the reason it is pasted.

Tested through a gettext filter rather than a real locale: the suite loads no
translations, so an untranslated string is indistinguishable from a translated
one that happens to match. The filter marks anything that reached the
catalogue, so a label that was never wrapped simply lacks the marker. Asserted
in both directions -- every label carries it, the JSON entry lines carry it
nowhere. Fails on revert.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 admin/admin.php                 | 34 +++++++++++---
 tests/unit/admin/test-admin.php | 82 +++++++++++++++++++++++++++++++++
 2 files changed, 109 insertions(+), 7 deletions(-)

diff --git a/admin/admin.php b/admin/admin.php
index 5633911f6..96ab0cf4d 100644
--- a/admin/admin.php
+++ b/admin/admin.php
@@ -5180,13 +5180,33 @@ private function get_support_message( $category = '', $form_title = '' ) {
 				'',
 				'---',
 				__( 'Site details', 'sureforms' ),
-				'Site: ' . home_url(),
-				'SureForms: ' . SRFM_VER,
-				'SureForms Pro: ' . ( Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' ) ),
-				'WordPress: ' . Helper::get_string_value( $wp_version ),
-				'PHP: ' . PHP_VERSION,
-				'Caching: ' . ( '' !== $caching ? $caching : __( 'none detected', 'sureforms' ) ),
-				'Recorded failures: ' . ( $count > 0 ? $count : __( 'none recorded', 'sureforms' ) ),
+				// Labels translated, values not. The site owner reads this on screen
+				// before sending it, so the labels are copy; the values are machine
+				// data -- a version, a URL, a plugin name -- and stay verbatim. The
+				// debug log below is left alone entirely for the same reason.
+				/* translators: %s: site address. */
+				sprintf( __( 'Site: %s', 'sureforms' ), home_url() ),
+				/* translators: %s: SureForms version. */
+				sprintf( __( 'SureForms: %s', 'sureforms' ), SRFM_VER ),
+				sprintf(
+					/* translators: %s: SureForms Pro version, or a note that it is not active. */
+					__( 'SureForms Pro: %s', 'sureforms' ),
+					Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' )
+				),
+				/* translators: %s: WordPress version. */
+				sprintf( __( 'WordPress: %s', 'sureforms' ), Helper::get_string_value( $wp_version ) ),
+				/* translators: %s: PHP version. */
+				sprintf( __( 'PHP: %s', 'sureforms' ), PHP_VERSION ),
+				sprintf(
+					/* translators: %s: caching plugin name, or a note that none was detected. */
+					__( 'Caching: %s', 'sureforms' ),
+					'' !== $caching ? $caching : __( 'none detected', 'sureforms' )
+				),
+				sprintf(
+					/* translators: %s: number of recorded failures, or a note that none were. */
+					__( 'Recorded failures: %s', 'sureforms' ),
+					$count > 0 ? Helper::get_string_value( $count ) : __( 'none recorded', 'sureforms' )
+				),
 			]
 		);
 
diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php
index 6790b6c11..04fa40c9f 100644
--- a/tests/unit/admin/test-admin.php
+++ b/tests/unit/admin/test-admin.php
@@ -1329,6 +1329,88 @@ public function test_support_copy_describes_the_failure_that_happened() {
 		$this->assertStringContainsString( 'Job Application', $integration );
 	}
 
+	/**
+	 * The report's prose translates; the debug log does not.
+	 *
+	 * The site owner reads this on screen before sending it, so every label in it is
+	 * copy and belongs in the catalogue. The values beside those labels are machine
+	 * data -- a version, a URL, a plugin name -- and the JSON log below them is the
+	 * raw record support greps, so neither is touched.
+	 *
+	 * Driven through a gettext filter rather than a real locale, because the suite
+	 * has no translations loaded and an untranslated string is indistinguishable
+	 * from a translated one that happens to match.
+	 */
+	public function test_support_message_translates_its_labels_but_not_the_log() {
+		wp_set_current_user( $this->make_user( 'administrator' ) );
+		delete_option( Client_Logger::FAILURES_OPTION );
+		Client_Logger::clear();
+		Client_Logger::record_failure( 'notification', 42, 'Contact Form' );
+		Client_Logger::append(
+			Client_Logger::sanitize_entry(
+				[
+					'type'    => 'network',
+					'status'  => 500,
+					'form_id' => 42,
+					'message' => 'Submission responded 500',
+				]
+			)
+		);
+		Admin::reset_action_items_cache();
+
+		// Marks anything that reached the catalogue, so a label that was never
+		// wrapped simply will not carry the marker.
+		$translate = static function ( $translated ) {
+			return '[t]' . $translated;
+		};
+
+		add_filter( 'gettext', $translate, 99 );
+
+		$method = new ReflectionMethod( Admin::class, 'get_support_message' );
+		$method->setAccessible( true );
+		$message = Helper::get_string_value( $method->invoke( Admin::get_instance(), 'notification', 'Contact Form' ) );
+
+		$log = Helper::get_string_value(
+			( new ReflectionMethod( Admin::class, 'get_support_log_block' ) )->getClosure( Admin::get_instance() )( 8000 )
+		);
+
+		remove_filter( 'gettext', $translate, 99 );
+
+		// Every label a reader sees, including the six that shipped as bare English
+		// while the values beside them were already translated.
+		foreach (
+			[
+				'Hello SureForms support,',
+				'Site details',
+				'Site: ',
+				'SureForms: ',
+				'SureForms Pro: ',
+				'WordPress: ',
+				'PHP: ',
+				'Caching: ',
+				'Recorded failures: ',
+			] as $label
+		) {
+			$this->assertStringContainsString(
+				'[t]' . $label,
+				$message,
+				sprintf( '"%s" is copy the site owner reads, so it must go through the catalogue.', trim( $label ) )
+			);
+		}
+
+		// The values are not copy, so they must arrive verbatim.
+		$this->assertStringContainsString( '[t]SureForms: ' . SRFM_VER, $message, 'The version itself is machine data.' );
+		$this->assertStringContainsString( '[t]Site: ' . home_url(), $message, 'So is the site address.' );
+
+		// And the log is the raw record support greps. Nothing in it may be rewritten.
+		$this->assertStringContainsString( '"type":"network"', $log, 'The JSON must survive verbatim.' );
+		$this->assertStringNotContainsString( '[t]{', $log, 'No entry line may be translated.' );
+		$this->assertStringNotContainsString( '[t]"type"', $log );
+
+		delete_option( Client_Logger::FAILURES_OPTION );
+		Client_Logger::clear();
+	}
+
 	/**
 	 * The served details payload keeps its line breaks.
 	 *

From 1294bcd958f461b0e363d1d6a1cf7753cd8f8ecb Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Sat, 12 Sep 2026 10:42:52 +0530
Subject: [PATCH 53/58] feat: add a Learn More link to the views and conversion
 rate setting

Points at https://sureforms.com/docs/show-views-and-conversion-rate/ for
the detail the inline copy deliberately does not carry.

Added as a third span inside the existing wrapper, in its own __() call,
so no shipped msgid changes and no locale loses a translation. Matches
the anchor already used by the usage tracking setting below it.
---
 src/admin/settings/pages/General.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/admin/settings/pages/General.js b/src/admin/settings/pages/General.js
index 9f767c3b8..6bbbae1e5 100644
--- a/src/admin/settings/pages/General.js
+++ b/src/admin/settings/pages/General.js
@@ -261,6 +261,16 @@ const FormViewsTrackingContent = ( {
 								'sureforms'
 							) }
 						
+						
+							
+								{ __( 'Learn More', 'sureforms' ) }
+							
+						
 					
 				),
 			} }

From 31ad60a9a96aed75deb7ab39192f661d4088ff07 Mon Sep 17 00:00:00 2001
From: Vansh Kapoor 
Date: Sat, 12 Sep 2026 10:48:24 +0530
Subject: [PATCH 54/58] fix: logging off records nothing, instead of only
 displaying nothing

From Vansh: with logging disabled nothing should be logged, and no Form Checks
or notices should appear.

Display was already right -- get_action_items() skips the first-party items and
has_action_item_warnings() returns false. The counter was not. record_failure()
had no enabled check, and the two call sites in form-submit.php sit directly
beside an append() that the check does stop, so on a site with logging switched
off the log file stayed empty while failure state kept accumulating in the
options table.

Measured before, logging off:

    log_file_bytes  0
    failures_option {"notification":{"count":1,"form_id":42,...}}
    open_failures   ["notification"]

The display gate hid that, which is what let it go unnoticed -- until logging
was switched back on, at which point every fault recorded during the quiet
period appeared as a notice, behind a View details report whose debug log is
empty because nothing had been written. A failure claim with no evidence
underneath it.

After: nothing written, nothing counted, and switching logging on surfaces no
backlog.

Gated inside record_failure() rather than at the three call sites, for the
reason append() already gives: the guard belongs on the function that writes,
not on today's callers. clear_category() and the acknowledge helpers stay
ungated -- they only remove state, and must keep working so the toggle strands
nothing.

The test asserts across the toggle rather than in one state. "No notice right
now" was already true, and testing only that is what would have missed this.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 inc/client-logger.php                 | 19 +++++++++
 tests/unit/inc/test-client-logger.php | 60 +++++++++++++++++++++++++++
 2 files changed, 79 insertions(+)

diff --git a/inc/client-logger.php b/inc/client-logger.php
index 098a76fb8..6d2353c28 100644
--- a/inc/client-logger.php
+++ b/inc/client-logger.php
@@ -200,6 +200,25 @@ public static function is_fault( array $entry ) {
 	 * @return void
 	 */
 	public static function record_failure( $category, $form_id = 0, $form_title = '' ) {
+		// Logging off means nothing is recorded, not merely nothing displayed.
+		//
+		// The display side was already gated -- get_action_items() skips the
+		// first-party items and has_action_item_warnings() returns false -- but the
+		// counter kept being written, from the two call sites in form-submit.php
+		// that sit beside an append() the enabled check does stop. So a site with
+		// logging switched off still accumulated failure state, and switching
+		// logging on surfaced every fault recorded while it was off, behind a View
+		// details report whose debug log is empty because nothing was written.
+		//
+		// Gated here rather than at the call sites, for the reason append() states:
+		// the guard belongs on the function that writes, not on today's callers.
+		// clear_category() and the acknowledge helpers are deliberately left
+		// ungated -- they only remove state, and must keep working so nothing is
+		// stranded by the toggle.
+		if ( ! self::is_enabled() ) {
+			return;
+		}
+
 		if ( ! in_array( $category, self::CATEGORIES, true ) ) {
 			return;
 		}
diff --git a/tests/unit/inc/test-client-logger.php b/tests/unit/inc/test-client-logger.php
index cfc0de514..b90cebc7c 100644
--- a/tests/unit/inc/test-client-logger.php
+++ b/tests/unit/inc/test-client-logger.php
@@ -387,6 +387,66 @@ public function provide_diagnostics() {
 		];
 	}
 
+	/**
+	 * Logging off records nothing, not merely displays nothing.
+	 *
+	 * The display side was already gated. The counter was not: the two record_failure()
+	 * calls in form-submit.php sit beside an append() that the enabled check does stop,
+	 * so a site with logging switched off kept accumulating failure state invisibly --
+	 * and switching logging back on surfaced every fault from the quiet period, behind a
+	 * View details report whose debug log is empty because nothing was ever written.
+	 *
+	 * Asserted across the toggle rather than in one state, because "no notice right now"
+	 * was already true and is what hid this.
+	 */
+	public function test_nothing_is_recorded_while_logging_is_disabled() {
+		$general = get_option( 'srfm_general_settings_options', [] );
+		$general = is_array( $general ) ? $general : [];
+
+		delete_option( Client_Logger::FAILURES_OPTION );
+		Client_Logger::clear();
+
+		update_option( 'srfm_general_settings_options', array_merge( $general, [ 'srfm_enable_logs' => false ] ) );
+		$this->assertFalse( Client_Logger::is_enabled(), 'Fixture must actually switch logging off.' );
+
+		// Every path that reports a fault: the server-side categories called directly
+		// from form-submit.php, and the client entries that route through append().
+		Client_Logger::record_failure( 'notification', 42, 'Contact Form' );
+		Client_Logger::record_failure( 'integration', 42, 'Contact Form' );
+		Client_Logger::append(
+			Client_Logger::sanitize_entry(
+				[
+					'type'    => 'network',
+					'status'  => 500,
+					'form_id' => 42,
+					'message' => 'Submission responded 500',
+				]
+			)
+		);
+
+		$this->assertSame( 0, Client_Logger::get_file_size(), 'Nothing may be written to the log.' );
+		$this->assertSame( [], Client_Logger::get_failures(), 'Nothing may be counted either.' );
+		$this->assertSame( [], Client_Logger::get_open_failures(), 'So there is nothing to raise a notice about.' );
+
+		// Switching logging on must not surface a backlog from the quiet period.
+		update_option( 'srfm_general_settings_options', array_merge( $general, [ 'srfm_enable_logs' => true ] ) );
+
+		$this->assertTrue( Client_Logger::is_enabled() );
+		$this->assertSame(
+			[],
+			Client_Logger::get_open_failures(),
+			'Turning logging on must not reveal faults from while it was off -- there is no log behind them.'
+		);
+
+		// And recording works again, so the guard is not simply always denying.
+		Client_Logger::record_failure( 'notification', 42, 'Contact Form' );
+		$this->assertArrayHasKey( 'notification', Client_Logger::get_open_failures() );
+
+		update_option( 'srfm_general_settings_options', $general );
+		delete_option( Client_Logger::FAILURES_OPTION );
+		Client_Logger::clear();
+	}
+
 	/**
 	 * An after-submission failure the browser only guessed at raises no notice.
 	 *

From 49f522d1955eededb4a0e145fe27ac1bcb84636e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 12 Sep 2026 05:28:28 +0000
Subject: [PATCH 55/58] chore: update i18n translations

Auto-generated by /i18n command on PR #3129
---
 ...e_DE-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...e_DE-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-de_DE.mo                  | Bin 333881 -> 338116 bytes
 languages/sureforms-de_DE.po                  | 592 +++++++++++-------
 ...s_ES-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...s_ES-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-es_ES.mo                  | Bin 335500 -> 339524 bytes
 languages/sureforms-es_ES.po                  | 592 +++++++++++-------
 ...r_FR-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...r_FR-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-fr_FR.mo                  | Bin 340160 -> 344348 bytes
 languages/sureforms-fr_FR.po                  | 592 +++++++++++-------
 ...t_IT-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...t_IT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-it_IT.mo                  | Bin 328981 -> 332935 bytes
 languages/sureforms-it_IT.po                  | 592 +++++++++++-------
 ...l_NL-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...l_NL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-nl_NL.mo                  | Bin 321995 -> 326131 bytes
 languages/sureforms-nl_NL.po                  | 592 +++++++++++-------
 ...l_PL-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...l_PL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-pl_PL.mo                  | Bin 330188 -> 334151 bytes
 languages/sureforms-pl_PL.po                  | 592 +++++++++++-------
 ...t_PT-51635fe6489fc8288d603fe596c755ca.json |   2 +-
 ...t_PT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json |   2 +-
 languages/sureforms-pt_PT.mo                  | Bin 332550 -> 336483 bytes
 languages/sureforms-pt_PT.po                  | 592 +++++++++++-------
 28 files changed, 2646 insertions(+), 1526 deletions(-)

diff --git a/languages/sureforms-de_DE-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-de_DE-51635fe6489fc8288d603fe596c755ca.json
index cc1c8e3dd..912e97d69 100644
--- a/languages/sureforms-de_DE-51635fe6489fc8288d603fe596c755ca.json
+++ b/languages/sureforms-de_DE-51635fe6489fc8288d603fe596c755ca.json
@@ -1 +1 @@
-{"translation-revision-date":"2024-12-13T16:14:52+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Armaturenbrett"],"Settings":["Einstellungen"],"Entries":["Eintr\u00e4ge"],"Status":["Status"],"Form":["Formular"],"Activated":["Aktiviert"],"Activate":["Aktivieren"],"Address":["Adresse"],"Checkbox":["Kontrollk\u00e4stchen"],"Dropdown":["Dropdown-Men\u00fc"],"Email":["E-Mail"],"Number":["Zahl"],"Phone":["Telefon"],"Textarea":["Textbereich"],"Monday":["Montag"],"Forms":["Formulare"],"New Form Submission - %s":["Neue Formulareinreichung - %s"],"GitHub":["GitHub"],"(no title)":["(kein Titel)"],"General":["Allgemein"],"No tags available":["Keine Tags verf\u00fcgbar"],"Back":["Zur\u00fcck"],"Generic tags":["Allgemeine Tags"],"Other":["Andere"],"Confirm":["Best\u00e4tigen"],"Cancel":["Abbrechen"],"Install":["Installieren"],"Plugin Installation failed, Please try again later.":["Plugin-Installation fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"Plugin activation failed, Please try again later.":["Plugin-Aktivierung fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"Integrations":["Integrationen"],"What's New?":["Was gibt's Neues?"],"Core":["Kern"],"Unlicensed":["Ohne Lizenz"],"Connecting\u2026":["Verbinden\u2026"],"Install & Activate":["Installieren & Aktivieren"],"Compliance Settings":["Compliance-Einstellungen"],"Enable GDPR Compliance":["Aktivieren Sie die DSGVO-Konformit\u00e4t"],"Never store entry data after form submission":["Speichern Sie niemals Eingabedaten nach dem Absenden des Formulars"],"When enabled this form will never store Entries.":["Wenn aktiviert, speichert dieses Formular niemals Eintr\u00e4ge."],"Automatically delete entries":["Eintr\u00e4ge automatisch l\u00f6schen"],"When enabled this form will automatically delete entries after a certain period of time.":["Wenn aktiviert, wird dieses Formular Eintr\u00e4ge nach einer bestimmten Zeitspanne automatisch l\u00f6schen."],"Entries older than the days set will be deleted automatically.":["Eintr\u00e4ge, die \u00e4lter sind als die festgelegten Tage, werden automatisch gel\u00f6scht."],"Visual":["Visuell"],"HTML":["HTML"],"All Data":["Alle Daten"],"Add Shortcode":["Shortcode hinzuf\u00fcgen"],"Form input tags":["Formulareingabetags"],"Comma separated values are also accepted.":["Kommagetrennte Werte werden ebenfalls akzeptiert."],"Email Notification":["E-Mail-Benachrichtigung"],"Name":["Name"],"Send Email To":["E-Mail senden an"],"Subject":["Betreff"],"CC":["CC"],"BCC":["BCC"],"Reply To":["Antworten an"],"Add Key":["Schl\u00fcssel hinzuf\u00fcgen"],"Add Value":["Wert hinzuf\u00fcgen"],"Add":["Hinzuf\u00fcgen"],"Confirmation Message":["Best\u00e4tigungsnachricht"],"After Form Submission":["Nach dem Absenden des Formulars"],"Hide Form":["Formular ausblenden"],"Reset Form":["Formular zur\u00fccksetzen"],"Custom URL":["Benutzerdefinierte URL"],"Add Query Parameters":["Abfrageparameter hinzuf\u00fcgen"],"Select if you want to add key-value pairs for form fields to include in query parameters":["W\u00e4hlen Sie, ob Sie Schl\u00fcssel-Wert-Paare f\u00fcr Formularfelder hinzuf\u00fcgen m\u00f6chten, die in Abfrageparametern enthalten sein sollen"],"Query Parameters":["Abfrageparameter"],"Success Message":["Erfolgsmeldung"],"Redirect":["Weiterleiten"],"Redirect to":["Weiterleiten zu"],"Page":["Seite"],"Form Confirmation":["Formularbest\u00e4tigung"],"Confirmation Type":["Best\u00e4tigungstyp"],"Clear":["Klar"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Unsichtbar"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validierungen"],"Spam Protection":["Spam-Schutz"],"Email Summaries":["E-Mail-Zusammenfassungen"],"Tuesday":["Dienstag"],"Wednesday":["Mittwoch"],"Thursday":["Donnerstag"],"Friday":["Freitag"],"Saturday":["Samstag"],"Sunday":["Sonntag"],"Test Email":["Test-E-Mail"],"Schedule Reports":["Berichte planen"],"IP Logging":["IP-Protokollierung"],"If this option is turned on, the user's IP address will be saved with the form data":["Wenn diese Option aktiviert ist, wird die IP-Adresse des Benutzers zusammen mit den Formulardaten gespeichert."],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s repr\u00e4sentiert die minimal erforderlichen Auswahlen. Zum Beispiel: \u201eMindestens 2 Auswahlen sind erforderlich.\u201c"],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s repr\u00e4sentiert die maximal erlaubten Auswahlen. Zum Beispiel: \u201eMaximal 4 Auswahlen sind erlaubt.\u201c"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s repr\u00e4sentiert die minimal erforderlichen Auswahlm\u00f6glichkeiten. Zum Beispiel: \u201eMindestens 1 Auswahl ist erforderlich.\u201c"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s repr\u00e4sentiert die maximal erlaubten Auswahlm\u00f6glichkeiten. Zum Beispiel: \u201eMaximal 3 Auswahlen sind erlaubt.\u201c"]," Error Message":["Fehlermeldung"],"Auto":["Auto"],"Light":["Licht"],"Dark":["Dunkel"],"Turnstile":["Drehkreuz"],"Honeypot":["Honigtopf"],"Get Keys":["Schl\u00fcssel holen"],"Documentation":["Dokumentation"],"Site Key":["Standortschl\u00fcssel"],"Secret Key":["Geheimer Schl\u00fcssel"],"Cloudflare Turnstile":["Cloudflare-Drehkreuz"],"Appearance Mode":["Erscheinungsmodus"],"Enable Honeypot Security":["Honeypot-Sicherheit aktivieren"],"Enable Honeypot Security for better spam protection":["Aktivieren Sie Honeypot-Sicherheit f\u00fcr besseren Spamschutz"],"Text":["Text"],"Confirmation Email Mismatch Message":["Best\u00e4tigungs-E-Mail-Unstimmigkeitsnachricht"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s repr\u00e4sentiert den minimalen Eingabewert. Zum Beispiel: \"Der Mindestwert ist 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s repr\u00e4sentiert den maximalen Eingabewert. Zum Beispiel: \"Der Maximalwert ist 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Mit OttoKit verbinden"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Die aktuelle 'Von-E-Mail'-Adresse stimmt nicht mit Ihrem Website-Domainnamen (%1$s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden. Alternativ versuchen Sie, eine Von-Adresse zu verwenden, die mit Ihrem Website-Domainnamen \u00fcbereinstimmt (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Die aktuelle 'Von-E-Mail'-Adresse stimmt nicht mit Ihrem Website-Domainnamen (%s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden."],"We strongly recommend that you install the free ":["Wir empfehlen Ihnen dringend, die kostenlose"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["Plugin! Der Einrichtungsassistent macht es einfach, Ihre E-Mails zu reparieren."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativ k\u00f6nnen Sie versuchen, eine Absenderadresse zu verwenden, die mit Ihrer Website-Domain \u00fcbereinstimmt (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Bitte geben Sie eine g\u00fcltige E-Mail-Adresse ein. Ihre Benachrichtigungen werden nicht gesendet, wenn das Feld nicht korrekt ausgef\u00fcllt ist."],"From Name":["Von Name"],"From Email":["Von E-Mail"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Die aktuelle 'Von-E-Mail'-Adresse stimmt m\u00f6glicherweise nicht mit Ihrem Website-Domainnamen (%1$s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden. Alternativ versuchen Sie, eine Von-Adresse zu verwenden, die mit Ihrem Website-Domainnamen \u00fcbereinstimmt (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Die aktuelle 'Von-E-Mail'-Adresse stimmt m\u00f6glicherweise nicht mit Ihrem Website-Domainnamen (%s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Bereit, \u00fcber den kostenlosen Plan hinauszugehen?"],"Upgrade now":["Jetzt upgraden"],"and unlock the full power of SureForms!":["und die volle Kraft von SureForms freischalten!"],"Upgrade SureForms":["SureForms aktualisieren"],"Upgrade Now":["Jetzt upgraden"],"Entries older than the selected days will be deleted.":["Eintr\u00e4ge, die \u00e4lter als die ausgew\u00e4hlten Tage sind, werden gel\u00f6scht."],"Entries Time Period":["Eintr\u00e4ge Zeitraum"],"Notifications can use only one From Email so please enter a single address.":["Benachrichtigungen k\u00f6nnen nur eine Absender-E-Mail verwenden, daher geben Sie bitte eine einzelne Adresse ein."],"Select Page to redirect":["Seite zum Weiterleiten ausw\u00e4hlen"],"Search for a page":["Suche nach einer Seite"],"Select a page":["W\u00e4hlen Sie eine Seite aus"],"Form Validation":["Formularvalidierung"],"Required Error Messages":["Erforderliche Fehlermeldungen"],"Other Error Messages":["Andere Fehlermeldungen"],"Input Field Unique":["Eingabefeld eindeutig"],"Email Field Unique":["E-Mail-Feld eindeutig"],"Invalid URL":["Ung\u00fcltige URL"],"Phone Field Unique":["Telefonfeld eindeutig"],"Invalid Field Number Block":["Ung\u00fcltiger Feldnummernblock"],"Invalid Email":["Ung\u00fcltige E-Mail"],"Number Minimum Value":["Zahlen-Mindestwert"],"Number Maximum Value":["Zahlen Maximalwert"],"Dropdown Minimum Selections":["Dropdown Minimale Auswahlen"],"Dropdown Maximum Selections":["Dropdown Maximale Auswahlen"],"Multiple Choice Minimum Selections":["Mehrfachauswahl Mindestanzahl der Auswahlen"],"Multiple Choice Maximum Selections":["Mehrfachauswahl Maximale Auswahlm\u00f6glichkeiten"],"Input Field":["Eingabefeld"],"Email Field":["E-Mail-Feld"],"URL Field":["URL-Feld"],"Phone Field":["Telefonfeld"],"Textarea Field":["Textbereichsfeld"],"Checkbox Field":["Kontrollk\u00e4stchenfeld"],"Dropdown Field":["Dropdown-Feld"],"Multiple Choice Field":["Mehrfachauswahlfeld"],"Address Field":["Adressfeld"],"Number Field":["Zahlenfeld"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Um die reCAPTCHA-Funktion in Ihren SureForms zu aktivieren, aktivieren Sie bitte die reCAPTCHA-Option in Ihren Blockeinstellungen und w\u00e4hlen Sie die Version aus. F\u00fcgen Sie hier den Google reCAPTCHA Secret und den Site Key hinzu. reCAPTCHA wird auf der Vorderseite Ihrer Seite hinzugef\u00fcgt."],"Enter your %s here":["Geben Sie hier Ihr %s ein"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Um hCAPTCHA zu aktivieren, f\u00fcgen Sie bitte Ihren Site-Schl\u00fcssel und Geheimschl\u00fcssel hinzu. Konfigurieren Sie diese Einstellungen innerhalb des einzelnen Formulars."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Um Cloudflare Turnstile zu aktivieren, f\u00fcgen Sie bitte Ihren Site-Schl\u00fcssel und Geheimschl\u00fcssel hinzu. Konfigurieren Sie diese Einstellungen innerhalb des einzelnen Formulars."],"Save":["Speichern"],"Anonymous Analytics":["Anonyme Analysen"],"Learn More":["Mehr erfahren"],"Admin Notification":["Administratorbenachrichtigung"],"Enable Admin Notification":["Admin-Benachrichtigung aktivieren"],"Admin notifications keep you informed about new form entries since your last visit.":["Admin-Benachrichtigungen halten Sie \u00fcber neue Formulareintr\u00e4ge seit Ihrem letzten Besuch auf dem Laufenden."],"Skip":["\u00dcberspringen"],"Continue":["Fortfahren"],"Maximum Number of Entries":["Maximale Anzahl von Eintr\u00e4gen"],"Maximum Entries":["Maximale Eintr\u00e4ge"],"Response Description After Maximum Entries":["Antwortbeschreibung nach maximalen Eintr\u00e4gen"],"Get Started":["Loslegen"],"Integration":["Integration"],"Connect Native Integrations with SureForms":["Native Integrationen mit SureForms verbinden"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Schalten Sie leistungsstarke Integrationen im Premium-Plan frei, um Ihre Arbeitsabl\u00e4ufe zu automatisieren und SureForms direkt mit Ihren Lieblingstools zu verbinden."],"Send form submissions straight to CRMs, email, and marketing platforms":["Senden Sie Formulareinsendungen direkt an CRMs, E-Mail- und Marketingplattformen"],"Automate repetitive tasks with seamless data syncing":["Automatisieren Sie repetitive Aufgaben mit nahtloser Datensynchronisierung"],"Access exclusive native integrations for faster workflows":["Zugriff auf exklusive native Integrationen f\u00fcr schnellere Arbeitsabl\u00e4ufe"],"Expected format for emails - email@sureforms.com or John Doe ":["Erwartetes Format f\u00fcr E-Mails - email@sureforms.com oder John Doe "],"Payments":["Zahlungen"],"Stripe account disconnected successfully.":["Stripe-Konto erfolgreich getrennt."],"Failed to create webhook.":["Fehler beim Erstellen des Webhooks."],"Failed to connect to Stripe.":["Verbindung zu Stripe fehlgeschlagen."],"Webhook":["Webhook"],"Knowledge Base":["Wissensdatenbank"],"What\u2019s New":["Was gibt's Neues"],"out of":["aus"],"No entries found":["Keine Eintr\u00e4ge gefunden"],"delete":["l\u00f6schen"],"Please type \"%s\" in the input box":["Bitte geben Sie \"%s\" in das Eingabefeld ein"],"To confirm, type \"%s\" in the box below:":["Um zu best\u00e4tigen, geben Sie \"%s\" in das Feld unten ein:"],"Type \"%s\"":["Geben Sie \"%s\" ein"],"Go to OttoKit Settings":["Gehe zu den OttoKit-Einstellungen"],"USD - US Dollar":["USD - US-Dollar"],"Payment Mode":["Zahlungsart"],"Test Mode":["Testmodus"],"Live Mode":["Live-Modus"],"Action":["Aktion"],"General Settings":["Allgemeine Einstellungen"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Richten Sie E-Mail-Zusammenfassungen, Admin-Benachrichtigungen und Datenpr\u00e4ferenzen ein, um Ihre Formulare m\u00fchelos zu verwalten."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Passen Sie die Standardfehlermeldungen an, die angezeigt werden, wenn Benutzer ung\u00fcltige oder unvollst\u00e4ndige Formulareintr\u00e4ge \u00fcbermitteln."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Aktivieren Sie den Spam-Schutz f\u00fcr Ihre Formulare mithilfe von CAPTCHA-Diensten oder Honeypot-Sicherheit."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Verbinden und verwalten Sie Ihre Zahlungsgateways, um Transaktionen sicher \u00fcber Ihre Formulare zu akzeptieren."],"1% transaction and payment gateway fees apply.":["1% Transaktions- und Zahlungsgateway-Geb\u00fchren fallen an."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["2,9 % Transaktions- und Zahlungsgateway-Geb\u00fchren fallen an. Aktivieren Sie die Lizenz, um die Transaktionsgeb\u00fchren zu reduzieren."],"2.9% transaction and payment gateway fees apply.":["Es fallen 2,9 % Transaktions- und Zahlungs-Gateway-Geb\u00fchren an."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Bitte besuchen Sie %1$s, l\u00f6schen Sie einen ungenutzten Webhook und klicken Sie dann unten, um es erneut zu versuchen."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms konnte keinen Webhook erstellen, da Ihr Stripe-Konto keine freien Slots mehr hat. Webhooks sind erforderlich, um Updates zu Zahlungen zu erhalten."],"Stripe Dashboard":["Stripe-Dashboard"],"Creating\u2026":["Erstellen\u2026"],"Create Webhook":["Webhook erstellen"],"Successfully connected to Stripe!":["Erfolgreich mit Stripe verbunden!"],"Invalid response from server. Please try again.":["Ung\u00fcltige Antwort vom Server. Bitte versuchen Sie es erneut."],"Failed to disconnect Stripe account.":["Trennen des Stripe-Kontos fehlgeschlagen."],"Webhook created successfully!":["Webhook erfolgreich erstellt!"],"Select Currency":["W\u00e4hrung ausw\u00e4hlen"],"Select the default currency for payment forms.":["W\u00e4hlen Sie die Standardw\u00e4hrung f\u00fcr Zahlungsformulare aus."],"Connection Status":["Verbindungsstatus"],"Disconnect Stripe Account":["Stripe-Konto trennen"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Sind Sie sicher, dass Sie Ihr Stripe-Konto trennen m\u00f6chten? Dadurch werden alle aktiven Zahlungen, Abonnements und Formulartransaktionen, die mit diesem Konto verbunden sind, gestoppt."],"Disconnect":["Trennen"],"Disconnecting\u2026":["Trennen\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook erfolgreich verbunden, alle Stripe-Ereignisse werden verfolgt."],"Connect your Stripe account to start accepting payments through your forms.":["Verbinden Sie Ihr Stripe-Konto, um Zahlungen \u00fcber Ihre Formulare zu akzeptieren."],"Connect to Stripe":["Mit Stripe verbinden"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Verbinden Sie sich sicher mit Stripe, um mit nur wenigen Klicks Zahlungen zu akzeptieren!"],"Set the total number of submissions allowed for this form.":["Legen Sie die Gesamtanzahl der zul\u00e4ssigen Einreichungen f\u00fcr dieses Formular fest."],"Payment Methods":["Zahlungsmethoden"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Der Testmodus erm\u00f6glicht es Ihnen, Zahlungen ohne echte Belastungen zu verarbeiten. Wechseln Sie in den Live-Modus f\u00fcr tats\u00e4chliche Transaktionen."],"General Payment Settings":["Allgemeine Zahlungseinstellungen"],"These settings apply to all payment gateways.":["Diese Einstellungen gelten f\u00fcr alle Zahlungs-Gateways."],"Stripe Settings":["Stripe-Einstellungen"],"Left ($100)":["Links ($100)"],"Right (100$)":["Richtig (100$)"],"Left Space ($ 100)":["Verbleibender Platz ($ 100)"],"Right Space (100 $)":["Rechter Raum (100 $)"],"Currency Sign Position":["Position des W\u00e4hrungszeichens"],"Select the position of the currency symbol relative to the amount.":["W\u00e4hlen Sie die Position des W\u00e4hrungssymbols relativ zum Betrag aus."],"Learn":["Lernen"],"Enable email summaries":["E-Mail-Zusammenfassungen aktivieren"],"Enable IP logging":["IP-Protokollierung aktivieren"],"Turn on Admin Notification from here.":["Aktivieren Sie hier die Admin-Benachrichtigung."],"New":["Neu"],"Send entries to 100+ popular apps.":["Eintr\u00e4ge an \u00fcber 100 beliebte Apps senden."],"Build automated workflows that run instantly.":["Erstellen Sie automatisierte Workflows, die sofort ausgef\u00fchrt werden."],"Create custom app integrations using our Custom App feature.":["Erstellen Sie benutzerdefinierte App-Integrationen mit unserer benutzerdefinierten App-Funktion."],"Keep your tools in sync automatically.":["Halten Sie Ihre Werkzeuge automatisch synchron."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Dies wird OttoKit auf Ihrer WordPress-Seite installieren und aktivieren, um Automatisierungsfunktionen zu erm\u00f6glichen."],"Automate Your Forms with OttoKit":["Automatisieren Sie Ihre Formulare mit OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Jede Formular\u00fcbermittlung sollte etwas ausl\u00f6sen \u2014 eine Slack-Benachrichtigung, einen CRM-Lead, eine Follow-up-E-Mail oder eine neue Zeile in Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Konfigurieren Sie die Berechtigungen des KI-Clients und die Einstellungen des MCP-Servers."],"View documentation":["Dokumentation anzeigen"],"Copy to clipboard":["In die Zwischenablage kopieren"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) oder %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Claude-Code"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (Projekt) oder ~\/.claude.json (global)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (Projekt) oder settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml oder config.json"],"Your client's MCP configuration file":["Die MCP-Konfigurationsdatei Ihres Kunden"],"Connect Your AI Client":["Verbinden Sie Ihren KI-Client"],"AI Client":["KI-Client"],"Create an Application Password \u2014 ":["Erstellen Sie ein Anwendungskennwort \u2014"],"Open Application Passwords":["Anwendungspassw\u00f6rter \u00f6ffnen"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Oder verwenden Sie diesen CLI-Befehl, um den Server schnell hinzuzuf\u00fcgen (Sie m\u00fcssen dennoch die Umgebungsvariablen festlegen):"],"Copy the JSON config below into: ":["Kopiere die JSON-Konfiguration unten in:"],"Replace \"your-application-password\" with the password from Step 1.":["Ersetzen Sie \"your-application-password\" durch das Passwort aus Schritt 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 der MCP-Endpunkt Ihrer Website. WP_API_USERNAME \u2014 Ihr WordPress-Benutzername. WP_API_PASSWORD \u2014 das Anwendungskennwort, das Sie erstellt haben."],"View setup docs":["Einrichtungsdokumente anzeigen"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Das MCP Adapter-Plugin ist installiert, aber nicht aktiv. Aktivieren Sie es, um die MCP-Einstellungen zu konfigurieren."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Das MCP Adapter-Plugin ist erforderlich, um AI-Clients mit Ihren Formularen zu verbinden. Laden Sie es von GitHub herunter, installieren Sie es und aktivieren Sie es dann."],"Download the latest release from":["Laden Sie die neueste Version herunter von"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installieren Sie das Plugin \u00fcber Plugins > Neues Plugin hinzuf\u00fcgen > Plugin hochladen."],"Activate the MCP Adapter plugin.":["Aktivieren Sie das MCP Adapter-Plugin."],"Activating\u2026":["Aktivierung\u2026"],"Activate MCP Adapter":["MCP-Adapter aktivieren"],"Download MCP Adapter":["MCP-Adapter herunterladen"],"Experimental":["Experimentell"],"Enable Abilities":["F\u00e4higkeiten aktivieren"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registriere die F\u00e4higkeiten von SureForms mit der WordPress Abilities API. Wenn aktiviert, k\u00f6nnen KI-Clients Ihre Formulare und Eintr\u00e4ge auflisten, lesen, erstellen, bearbeiten und l\u00f6schen. Wenn deaktiviert, werden keine F\u00e4higkeiten registriert und KI-Clients k\u00f6nnen keine Aktionen an Ihren Formularen durchf\u00fchren."],"Abilities API \u2014 Edit":["F\u00e4higkeiten-API \u2014 Bearbeiten"],"Enable Edit Abilities":["Bearbeitungsf\u00e4higkeiten aktivieren"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Wenn aktiviert, k\u00f6nnen KI-Clients neue Formulare erstellen, Formulartitel, Felder und Einstellungen aktualisieren, Formulare duplizieren und Eintragsstatus \u00e4ndern. Wenn deaktiviert, werden diese F\u00e4higkeiten abgemeldet und KI-Clients k\u00f6nnen nur Ihre Daten lesen."],"Abilities API \u2014 Delete":["F\u00e4higkeiten-API \u2014 L\u00f6schen"],"Enable Delete Abilities":["L\u00f6schf\u00e4higkeiten aktivieren"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Wenn aktiviert, k\u00f6nnen KI-Clients Formulare und Eintr\u00e4ge dauerhaft l\u00f6schen. Gel\u00f6schte Daten k\u00f6nnen nicht wiederhergestellt werden. Wenn deaktiviert, werden L\u00f6schf\u00e4higkeiten abgemeldet und KI-Clients k\u00f6nnen keine Daten entfernen."],"MCP Server":["MCP-Server"],"Enable MCP Server":["MCP-Server aktivieren"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Erstellt einen dedizierten SureForms MCP-Endpunkt, mit dem sich KI-Clients wie Claude verbinden k\u00f6nnen. Wenn er deaktiviert ist, wird der Endpunkt entfernt und externe KI-Clients k\u00f6nnen keine SureForms-Funktionen entdecken oder aufrufen."],"Learn more":["Mehr erfahren"],"MCP Adapter Required":["MCP-Adapter erforderlich"],"Heading 1":["\u00dcberschrift 1"],"Heading 2":["\u00dcberschrift 2"],"Heading 3":["\u00dcberschrift 3"],"Heading 4":["\u00dcberschrift 4"],"Heading 5":["\u00dcberschrift 5"],"Heading 6":["\u00dcberschrift 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Konfigurieren Sie den Google Maps API-Schl\u00fcssel f\u00fcr die Adressvervollst\u00e4ndigung und die Kartenansichtsvorschau."],"Help shape the future of SureForms":["Helfen Sie mit, die Zukunft von SureForms zu gestalten"],"Enable Google Address Autocomplete":["Google-Adress-Autovervollst\u00e4ndigung aktivieren"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Upgrade auf den SureForms Business-Plan, um Google-gest\u00fctzte Adressvervollst\u00e4ndigung mit interaktiver Kartenansicht zu Ihren Formularen hinzuzuf\u00fcgen."],"Auto-suggest addresses as users type for faster, error-free submissions":["Adressen automatisch vorschlagen, w\u00e4hrend Benutzer tippen, f\u00fcr schnellere und fehlerfreie Eingaben"],"Show an interactive map preview with draggable pin for precise locations":["Zeige eine interaktive Kartenansicht mit verschiebbarem Pin f\u00fcr pr\u00e4zise Standorte"],"Automatically populate address fields like city, state, and postal code":["Adressfelder wie Stadt, Bundesland und Postleitzahl automatisch ausf\u00fcllen"],"This form is now closed as we've received all the entries.":["Dieses Formular ist jetzt geschlossen, da wir alle Einsendungen erhalten haben."],"Thank you for contacting us! We will be in touch with you shortly.":["Vielen Dank, dass Sie uns kontaktiert haben! Wir werden uns in K\u00fcrze mit Ihnen in Verbindung setzen."],"Saving\u2026":["Speichern\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Wenn aktiviert, speichert dieses Formular die IP-Adresse des Benutzers, den Browsernamen und den Ger\u00e4tenamen nicht in den Eintr\u00e4gen."],"Form data":["Formulardaten"],"Unsaved changes":["Nicht gespeicherte \u00c4nderungen"],"Keep editing":["Weiter bearbeiten"],"Global Defaults":["Globale Standardeinstellungen"],"Form Restrictions":["Formulareinschr\u00e4nkungen"],"Configure default settings that apply to newly created forms.":["Standardm\u00e4\u00dfige Einstellungen konfigurieren, die f\u00fcr neu erstellte Formulare gelten."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Sammeln Sie nicht sensible Informationen von Ihrer Website, wie die verwendete PHP-Version und Funktionen, um uns zu helfen, Fehler schneller zu beheben, kl\u00fcgere Entscheidungen zu treffen und Funktionen zu entwickeln, die f\u00fcr Sie tats\u00e4chlich von Bedeutung sind."],"Failed to load pages. Please refresh and try again.":["Fehler beim Laden der Seiten. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut."],"Failed to load settings. Please refresh and try again.":["Fehler beim Laden der Einstellungen. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut."],"Form Validation fields cannot be left blank.":["Felder zur Formularvalidierung d\u00fcrfen nicht leer gelassen werden."],"Recipient email is required when email summaries are enabled.":["Die Empf\u00e4nger-E-Mail ist erforderlich, wenn E-Mail-Zusammenfassungen aktiviert sind."],"Please enter a valid recipient email.":["Bitte geben Sie eine g\u00fcltige Empf\u00e4nger-E-Mail-Adresse ein."],"Settings saved.":["Einstellungen gespeichert."],"Failed to save settings.":["Speichern der Einstellungen fehlgeschlagen."],"Some settings failed to save. Please retry.":["Einige Einstellungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Einige Felder enthalten ungespeicherte \u00c4nderungen. Verwerfen Sie diese, um fortzufahren, oder bleiben Sie, um Ihre \u00c4nderungen zu speichern."],"Discard & switch":["Verwerfen & wechseln"],"Import":["Importieren"],"Migration":["Migration"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Formulare von Contact Form 7, WPForms, Gravity Forms und anderen Plugins in SureForms importieren."],"Could not generate the import preview.":["Konnte die Importvorschau nicht erstellen."],"Import failed. Please try again or check your error logs.":["Import fehlgeschlagen. Bitte versuchen Sie es erneut oder \u00fcberpr\u00fcfen Sie Ihre Fehlerprotokolle."],"Go back":["Geh zur\u00fcck"],"Update & import":["Aktualisieren & importieren"],"Confirm & import":["Best\u00e4tigen & importieren"],"Form #%s":["Formular #%s"],"%d field will import":["%d Feld wird importiert"],"Some fields can't be migrated yet":["Einige Felder k\u00f6nnen noch nicht migriert werden"],"These fields will be skipped - add them manually after the form is created:":["Diese Felder werden \u00fcbersprungen - f\u00fcgen Sie sie manuell hinzu, nachdem das Formular erstellt wurde:"],"Some forms could not be parsed":["Einige Formulare konnten nicht analysiert werden"],"Update existing":["Vorhandenes aktualisieren"],"Create a new copy":["Erstellen Sie eine neue Kopie"],"Could not load forms for this source.":["Konnte die Formulare f\u00fcr diese Quelle nicht laden."],"(untitled form)":["(unbetiteltes Formular)"],"Previously imported":["Zuvor importiert"],"Open existing SureForms form in a new tab":["Vorhandenes SureForms-Formular in einem neuen Tab \u00f6ffnen"],"On re-import":["Beim erneuten Import"],"No forms found in this plugin.":["Keine Formulare in diesem Plugin gefunden."],"%1$d of %2$d form selected":["%1$d von %2$d Formular ausgew\u00e4hlt"],"Preview update":["Vorschau-Update"],"Preview import":["Importvorschau"],"Migration complete":["Migration abgeschlossen"],"Nothing was imported":["Nichts wurde importiert"],"%d form was imported into SureForms.":["%d Formular wurde in SureForms importiert."],"None of the selected forms could be migrated. See the warnings below for details.":["Keine der ausgew\u00e4hlten Formulare konnte migriert werden. Siehe die Warnungen unten f\u00fcr Details."],"Imported forms":["Importierte Formulare"],"Edit in SureForms":["In SureForms bearbeiten"],"Some fields were skipped during import":["Einige Felder wurden beim Import \u00fcbersprungen"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["F\u00fcgen Sie diese manuell im Formular-Editor hinzu - sie haben noch kein \u00c4quivalent in SureForms:"],"Some forms failed to import":["Einige Formulare konnten nicht importiert werden"],"Import more forms":["Mehr Formulare importieren"],"Could not load the list of importable plugins.":["Konnte die Liste der importierbaren Plugins nicht laden."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Auf dieser Website sind keine unterst\u00fctzten Formular-Plugins aktiv. Aktivieren Sie eines (wie Contact Form 7) mit mindestens einem Formular, um es in SureForms zu importieren."],"Plugin":["Plugin"],"%d form":["%d Formular"],"No forms":["Keine Formulare"],"Multiple choice":["Mehrfachauswahl"],"Consent":["Zustimmung"],"The image uploaded but could not be inserted. Please try again.":["Das Bild wurde hochgeladen, konnte aber nicht eingef\u00fcgt werden. Bitte versuchen Sie es erneut."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["Das Bild konnte nicht hochgeladen werden. Es k\u00f6nnte zu gro\u00df sein, oder Sie haben m\u00f6glicherweise keine Berechtigung, Dateien hochzuladen."],"Please choose an image file.":["Bitte w\u00e4hlen Sie eine Bilddatei aus."],"Show views and conversion rate":["Ansichten und Konversionsrate anzeigen"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["F\u00fcgt die Spalten Ansichten und Konversionsrate zur Formularliste hinzu. Eine Ansicht wird einmal pro Seitenbesuch gez\u00e4hlt, wenn das Formular auf dem Bildschirm erscheint, und die Konversionsrate ist der Anteil dieser Ansichten, die in einer Einreichung endeten. Die Z\u00e4hlung beginnt, sobald Sie dies aktivieren, sodass Einreichungen, die zuvor eingegangen sind, nicht zur Rate gez\u00e4hlt werden. Wenn Sie es danach deaktivieren, werden nur die Spalten ausgeblendet \u2014 die Z\u00e4hlung geht weiter, sodass die Zahlen aktuell sind, wenn Sie es wieder einschalten."],"Log cleared.":["Protokoll gel\u00f6scht."],"Could not clear the log.":["Protokoll konnte nicht gel\u00f6scht werden."],"Enable logs":["Protokolle aktivieren"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Erfasst Formular\u00fcbermittlungsfehler, die vom Browser gemeldet werden, damit Sie das Protokoll an den Support senden k\u00f6nnen, anstatt die Konsole zu lesen. Solange Ihre Formulare funktionieren, wird nichts geschrieben, und \u00fcbermittelte Werte werden niemals gespeichert."],"Download log":["Protokoll herunterladen"],"Confirm delete":["L\u00f6schen best\u00e4tigen"],"Log is full \u2014 download and clear it to keep recording.":["Protokoll ist voll \u2014 laden Sie es herunter und l\u00f6schen Sie es, um weiter aufzuzeichnen."],"Form Views & Conversion":["Formularansichten & Konversion"],"Logs":["Protokolle"],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}}
\ No newline at end of file
+{"translation-revision-date":"2024-12-13T16:14:52+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Armaturenbrett"],"Settings":["Einstellungen"],"Entries":["Eintr\u00e4ge"],"Status":["Status"],"Form":["Formular"],"Activated":["Aktiviert"],"Activate":["Aktivieren"],"Address":["Adresse"],"Checkbox":["Kontrollk\u00e4stchen"],"Dropdown":["Dropdown-Men\u00fc"],"Email":["E-Mail"],"Number":["Zahl"],"Phone":["Telefon"],"Textarea":["Textbereich"],"Monday":["Montag"],"Forms":["Formulare"],"New Form Submission - %s":["Neue Formulareinreichung - %s"],"GitHub":["GitHub"],"(no title)":["(kein Titel)"],"General":["Allgemein"],"No tags available":["Keine Tags verf\u00fcgbar"],"Back":["Zur\u00fcck"],"Generic tags":["Allgemeine Tags"],"Other":["Andere"],"Confirm":["Best\u00e4tigen"],"Cancel":["Abbrechen"],"Install":["Installieren"],"Plugin Installation failed, Please try again later.":["Plugin-Installation fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"Plugin activation failed, Please try again later.":["Plugin-Aktivierung fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"Integrations":["Integrationen"],"What's New?":["Was gibt's Neues?"],"Core":["Kern"],"Unlicensed":["Ohne Lizenz"],"Connecting\u2026":["Verbinden\u2026"],"Install & Activate":["Installieren & Aktivieren"],"Compliance Settings":["Compliance-Einstellungen"],"Enable GDPR Compliance":["Aktivieren Sie die DSGVO-Konformit\u00e4t"],"Never store entry data after form submission":["Speichern Sie niemals Eingabedaten nach dem Absenden des Formulars"],"When enabled this form will never store Entries.":["Wenn aktiviert, speichert dieses Formular niemals Eintr\u00e4ge."],"Automatically delete entries":["Eintr\u00e4ge automatisch l\u00f6schen"],"When enabled this form will automatically delete entries after a certain period of time.":["Wenn aktiviert, wird dieses Formular Eintr\u00e4ge nach einer bestimmten Zeitspanne automatisch l\u00f6schen."],"Entries older than the days set will be deleted automatically.":["Eintr\u00e4ge, die \u00e4lter sind als die festgelegten Tage, werden automatisch gel\u00f6scht."],"Visual":["Visuell"],"HTML":["HTML"],"All Data":["Alle Daten"],"Add Shortcode":["Shortcode hinzuf\u00fcgen"],"Form input tags":["Formulareingabetags"],"Comma separated values are also accepted.":["Kommagetrennte Werte werden ebenfalls akzeptiert."],"Email Notification":["E-Mail-Benachrichtigung"],"Name":["Name"],"Send Email To":["E-Mail senden an"],"Subject":["Betreff"],"CC":["CC"],"BCC":["BCC"],"Reply To":["Antworten an"],"Add Key":["Schl\u00fcssel hinzuf\u00fcgen"],"Add Value":["Wert hinzuf\u00fcgen"],"Add":["Hinzuf\u00fcgen"],"Confirmation Message":["Best\u00e4tigungsnachricht"],"After Form Submission":["Nach dem Absenden des Formulars"],"Hide Form":["Formular ausblenden"],"Reset Form":["Formular zur\u00fccksetzen"],"Custom URL":["Benutzerdefinierte URL"],"Add Query Parameters":["Abfrageparameter hinzuf\u00fcgen"],"Select if you want to add key-value pairs for form fields to include in query parameters":["W\u00e4hlen Sie, ob Sie Schl\u00fcssel-Wert-Paare f\u00fcr Formularfelder hinzuf\u00fcgen m\u00f6chten, die in Abfrageparametern enthalten sein sollen"],"Query Parameters":["Abfrageparameter"],"Success Message":["Erfolgsmeldung"],"Redirect":["Weiterleiten"],"Redirect to":["Weiterleiten zu"],"Page":["Seite"],"Form Confirmation":["Formularbest\u00e4tigung"],"Confirmation Type":["Best\u00e4tigungstyp"],"Clear":["Klar"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Unsichtbar"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validierungen"],"Spam Protection":["Spam-Schutz"],"Email Summaries":["E-Mail-Zusammenfassungen"],"Tuesday":["Dienstag"],"Wednesday":["Mittwoch"],"Thursday":["Donnerstag"],"Friday":["Freitag"],"Saturday":["Samstag"],"Sunday":["Sonntag"],"Test Email":["Test-E-Mail"],"Schedule Reports":["Berichte planen"],"IP Logging":["IP-Protokollierung"],"If this option is turned on, the user's IP address will be saved with the form data":["Wenn diese Option aktiviert ist, wird die IP-Adresse des Benutzers zusammen mit den Formulardaten gespeichert."],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s repr\u00e4sentiert die minimal erforderlichen Auswahlen. Zum Beispiel: \u201eMindestens 2 Auswahlen sind erforderlich.\u201c"],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s repr\u00e4sentiert die maximal erlaubten Auswahlen. Zum Beispiel: \u201eMaximal 4 Auswahlen sind erlaubt.\u201c"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s repr\u00e4sentiert die minimal erforderlichen Auswahlm\u00f6glichkeiten. Zum Beispiel: \u201eMindestens 1 Auswahl ist erforderlich.\u201c"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s repr\u00e4sentiert die maximal erlaubten Auswahlm\u00f6glichkeiten. Zum Beispiel: \u201eMaximal 3 Auswahlen sind erlaubt.\u201c"]," Error Message":["Fehlermeldung"],"Auto":["Auto"],"Light":["Licht"],"Dark":["Dunkel"],"Turnstile":["Drehkreuz"],"Honeypot":["Honigtopf"],"Get Keys":["Schl\u00fcssel holen"],"Documentation":["Dokumentation"],"Site Key":["Standortschl\u00fcssel"],"Secret Key":["Geheimer Schl\u00fcssel"],"Cloudflare Turnstile":["Cloudflare-Drehkreuz"],"Appearance Mode":["Erscheinungsmodus"],"Enable Honeypot Security":["Honeypot-Sicherheit aktivieren"],"Enable Honeypot Security for better spam protection":["Aktivieren Sie Honeypot-Sicherheit f\u00fcr besseren Spamschutz"],"Text":["Text"],"Confirmation Email Mismatch Message":["Best\u00e4tigungs-E-Mail-Unstimmigkeitsnachricht"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s repr\u00e4sentiert den minimalen Eingabewert. Zum Beispiel: \"Der Mindestwert ist 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s repr\u00e4sentiert den maximalen Eingabewert. Zum Beispiel: \"Der Maximalwert ist 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Mit OttoKit verbinden"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Die aktuelle 'Von-E-Mail'-Adresse stimmt nicht mit Ihrem Website-Domainnamen (%1$s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden. Alternativ versuchen Sie, eine Von-Adresse zu verwenden, die mit Ihrem Website-Domainnamen \u00fcbereinstimmt (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Die aktuelle 'Von-E-Mail'-Adresse stimmt nicht mit Ihrem Website-Domainnamen (%s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden."],"We strongly recommend that you install the free ":["Wir empfehlen Ihnen dringend, die kostenlose"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["Plugin! Der Einrichtungsassistent macht es einfach, Ihre E-Mails zu reparieren."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativ k\u00f6nnen Sie versuchen, eine Absenderadresse zu verwenden, die mit Ihrer Website-Domain \u00fcbereinstimmt (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Bitte geben Sie eine g\u00fcltige E-Mail-Adresse ein. Ihre Benachrichtigungen werden nicht gesendet, wenn das Feld nicht korrekt ausgef\u00fcllt ist."],"From Name":["Von Name"],"From Email":["Von E-Mail"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Die aktuelle 'Von-E-Mail'-Adresse stimmt m\u00f6glicherweise nicht mit Ihrem Website-Domainnamen (%1$s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden. Alternativ versuchen Sie, eine Von-Adresse zu verwenden, die mit Ihrem Website-Domainnamen \u00fcbereinstimmt (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Die aktuelle 'Von-E-Mail'-Adresse stimmt m\u00f6glicherweise nicht mit Ihrem Website-Domainnamen (%s) \u00fcberein. Dies kann dazu f\u00fchren, dass Ihre Benachrichtigungs-E-Mails blockiert oder als Spam markiert werden."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Bereit, \u00fcber den kostenlosen Plan hinauszugehen?"],"Upgrade now":["Jetzt upgraden"],"and unlock the full power of SureForms!":["und die volle Kraft von SureForms freischalten!"],"Upgrade SureForms":["SureForms aktualisieren"],"Upgrade Now":["Jetzt upgraden"],"Entries older than the selected days will be deleted.":["Eintr\u00e4ge, die \u00e4lter als die ausgew\u00e4hlten Tage sind, werden gel\u00f6scht."],"Entries Time Period":["Eintr\u00e4ge Zeitraum"],"Notifications can use only one From Email so please enter a single address.":["Benachrichtigungen k\u00f6nnen nur eine Absender-E-Mail verwenden, daher geben Sie bitte eine einzelne Adresse ein."],"Select Page to redirect":["Seite zum Weiterleiten ausw\u00e4hlen"],"Search for a page":["Suche nach einer Seite"],"Select a page":["W\u00e4hlen Sie eine Seite aus"],"Form Validation":["Formularvalidierung"],"Required Error Messages":["Erforderliche Fehlermeldungen"],"Other Error Messages":["Andere Fehlermeldungen"],"Input Field Unique":["Eingabefeld eindeutig"],"Email Field Unique":["E-Mail-Feld eindeutig"],"Invalid URL":["Ung\u00fcltige URL"],"Phone Field Unique":["Telefonfeld eindeutig"],"Invalid Field Number Block":["Ung\u00fcltiger Feldnummernblock"],"Invalid Email":["Ung\u00fcltige E-Mail"],"Number Minimum Value":["Zahlen-Mindestwert"],"Number Maximum Value":["Zahlen Maximalwert"],"Dropdown Minimum Selections":["Dropdown Minimale Auswahlen"],"Dropdown Maximum Selections":["Dropdown Maximale Auswahlen"],"Multiple Choice Minimum Selections":["Mehrfachauswahl Mindestanzahl der Auswahlen"],"Multiple Choice Maximum Selections":["Mehrfachauswahl Maximale Auswahlm\u00f6glichkeiten"],"Input Field":["Eingabefeld"],"Email Field":["E-Mail-Feld"],"URL Field":["URL-Feld"],"Phone Field":["Telefonfeld"],"Textarea Field":["Textbereichsfeld"],"Checkbox Field":["Kontrollk\u00e4stchenfeld"],"Dropdown Field":["Dropdown-Feld"],"Multiple Choice Field":["Mehrfachauswahlfeld"],"Address Field":["Adressfeld"],"Number Field":["Zahlenfeld"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Um die reCAPTCHA-Funktion in Ihren SureForms zu aktivieren, aktivieren Sie bitte die reCAPTCHA-Option in Ihren Blockeinstellungen und w\u00e4hlen Sie die Version aus. F\u00fcgen Sie hier den Google reCAPTCHA Secret und den Site Key hinzu. reCAPTCHA wird auf der Vorderseite Ihrer Seite hinzugef\u00fcgt."],"Enter your %s here":["Geben Sie hier Ihr %s ein"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Um hCAPTCHA zu aktivieren, f\u00fcgen Sie bitte Ihren Site-Schl\u00fcssel und Geheimschl\u00fcssel hinzu. Konfigurieren Sie diese Einstellungen innerhalb des einzelnen Formulars."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Um Cloudflare Turnstile zu aktivieren, f\u00fcgen Sie bitte Ihren Site-Schl\u00fcssel und Geheimschl\u00fcssel hinzu. Konfigurieren Sie diese Einstellungen innerhalb des einzelnen Formulars."],"Save":["Speichern"],"Anonymous Analytics":["Anonyme Analysen"],"Learn More":["Mehr erfahren"],"Admin Notification":["Administratorbenachrichtigung"],"Enable Admin Notification":["Admin-Benachrichtigung aktivieren"],"Admin notifications keep you informed about new form entries since your last visit.":["Admin-Benachrichtigungen halten Sie \u00fcber neue Formulareintr\u00e4ge seit Ihrem letzten Besuch auf dem Laufenden."],"Skip":["\u00dcberspringen"],"Continue":["Fortfahren"],"Maximum Number of Entries":["Maximale Anzahl von Eintr\u00e4gen"],"Maximum Entries":["Maximale Eintr\u00e4ge"],"Response Description After Maximum Entries":["Antwortbeschreibung nach maximalen Eintr\u00e4gen"],"Get Started":["Loslegen"],"Integration":["Integration"],"Connect Native Integrations with SureForms":["Native Integrationen mit SureForms verbinden"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Schalten Sie leistungsstarke Integrationen im Premium-Plan frei, um Ihre Arbeitsabl\u00e4ufe zu automatisieren und SureForms direkt mit Ihren Lieblingstools zu verbinden."],"Send form submissions straight to CRMs, email, and marketing platforms":["Senden Sie Formulareinsendungen direkt an CRMs, E-Mail- und Marketingplattformen"],"Automate repetitive tasks with seamless data syncing":["Automatisieren Sie repetitive Aufgaben mit nahtloser Datensynchronisierung"],"Access exclusive native integrations for faster workflows":["Zugriff auf exklusive native Integrationen f\u00fcr schnellere Arbeitsabl\u00e4ufe"],"Expected format for emails - email@sureforms.com or John Doe ":["Erwartetes Format f\u00fcr E-Mails - email@sureforms.com oder John Doe "],"Payments":["Zahlungen"],"Stripe account disconnected successfully.":["Stripe-Konto erfolgreich getrennt."],"Failed to create webhook.":["Fehler beim Erstellen des Webhooks."],"Failed to connect to Stripe.":["Verbindung zu Stripe fehlgeschlagen."],"Webhook":["Webhook"],"Knowledge Base":["Wissensdatenbank"],"What\u2019s New":["Was gibt's Neues"],"out of":["aus"],"No entries found":["Keine Eintr\u00e4ge gefunden"],"delete":["l\u00f6schen"],"Please type \"%s\" in the input box":["Bitte geben Sie \"%s\" in das Eingabefeld ein"],"To confirm, type \"%s\" in the box below:":["Um zu best\u00e4tigen, geben Sie \"%s\" in das Feld unten ein:"],"Type \"%s\"":["Geben Sie \"%s\" ein"],"Go to OttoKit Settings":["Gehe zu den OttoKit-Einstellungen"],"USD - US Dollar":["USD - US-Dollar"],"Payment Mode":["Zahlungsart"],"Test Mode":["Testmodus"],"Live Mode":["Live-Modus"],"Action":["Aktion"],"General Settings":["Allgemeine Einstellungen"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Richten Sie E-Mail-Zusammenfassungen, Admin-Benachrichtigungen und Datenpr\u00e4ferenzen ein, um Ihre Formulare m\u00fchelos zu verwalten."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Passen Sie die Standardfehlermeldungen an, die angezeigt werden, wenn Benutzer ung\u00fcltige oder unvollst\u00e4ndige Formulareintr\u00e4ge \u00fcbermitteln."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Aktivieren Sie den Spam-Schutz f\u00fcr Ihre Formulare mithilfe von CAPTCHA-Diensten oder Honeypot-Sicherheit."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Verbinden und verwalten Sie Ihre Zahlungsgateways, um Transaktionen sicher \u00fcber Ihre Formulare zu akzeptieren."],"1% transaction and payment gateway fees apply.":["1% Transaktions- und Zahlungsgateway-Geb\u00fchren fallen an."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["2,9 % Transaktions- und Zahlungsgateway-Geb\u00fchren fallen an. Aktivieren Sie die Lizenz, um die Transaktionsgeb\u00fchren zu reduzieren."],"2.9% transaction and payment gateway fees apply.":["Es fallen 2,9 % Transaktions- und Zahlungs-Gateway-Geb\u00fchren an."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Bitte besuchen Sie %1$s, l\u00f6schen Sie einen ungenutzten Webhook und klicken Sie dann unten, um es erneut zu versuchen."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms konnte keinen Webhook erstellen, da Ihr Stripe-Konto keine freien Slots mehr hat. Webhooks sind erforderlich, um Updates zu Zahlungen zu erhalten."],"Stripe Dashboard":["Stripe-Dashboard"],"Creating\u2026":["Erstellen\u2026"],"Create Webhook":["Webhook erstellen"],"Successfully connected to Stripe!":["Erfolgreich mit Stripe verbunden!"],"Invalid response from server. Please try again.":["Ung\u00fcltige Antwort vom Server. Bitte versuchen Sie es erneut."],"Failed to disconnect Stripe account.":["Trennen des Stripe-Kontos fehlgeschlagen."],"Webhook created successfully!":["Webhook erfolgreich erstellt!"],"Select Currency":["W\u00e4hrung ausw\u00e4hlen"],"Select the default currency for payment forms.":["W\u00e4hlen Sie die Standardw\u00e4hrung f\u00fcr Zahlungsformulare aus."],"Connection Status":["Verbindungsstatus"],"Disconnect Stripe Account":["Stripe-Konto trennen"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Sind Sie sicher, dass Sie Ihr Stripe-Konto trennen m\u00f6chten? Dadurch werden alle aktiven Zahlungen, Abonnements und Formulartransaktionen, die mit diesem Konto verbunden sind, gestoppt."],"Disconnect":["Trennen"],"Disconnecting\u2026":["Trennen\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook erfolgreich verbunden, alle Stripe-Ereignisse werden verfolgt."],"Connect your Stripe account to start accepting payments through your forms.":["Verbinden Sie Ihr Stripe-Konto, um Zahlungen \u00fcber Ihre Formulare zu akzeptieren."],"Connect to Stripe":["Mit Stripe verbinden"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Verbinden Sie sich sicher mit Stripe, um mit nur wenigen Klicks Zahlungen zu akzeptieren!"],"Set the total number of submissions allowed for this form.":["Legen Sie die Gesamtanzahl der zul\u00e4ssigen Einreichungen f\u00fcr dieses Formular fest."],"Payment Methods":["Zahlungsmethoden"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Der Testmodus erm\u00f6glicht es Ihnen, Zahlungen ohne echte Belastungen zu verarbeiten. Wechseln Sie in den Live-Modus f\u00fcr tats\u00e4chliche Transaktionen."],"General Payment Settings":["Allgemeine Zahlungseinstellungen"],"These settings apply to all payment gateways.":["Diese Einstellungen gelten f\u00fcr alle Zahlungs-Gateways."],"Stripe Settings":["Stripe-Einstellungen"],"Left ($100)":["Links ($100)"],"Right (100$)":["Richtig (100$)"],"Left Space ($ 100)":["Verbleibender Platz ($ 100)"],"Right Space (100 $)":["Rechter Raum (100 $)"],"Currency Sign Position":["Position des W\u00e4hrungszeichens"],"Select the position of the currency symbol relative to the amount.":["W\u00e4hlen Sie die Position des W\u00e4hrungssymbols relativ zum Betrag aus."],"Learn":["Lernen"],"Enable email summaries":["E-Mail-Zusammenfassungen aktivieren"],"Enable IP logging":["IP-Protokollierung aktivieren"],"Turn on Admin Notification from here.":["Aktivieren Sie hier die Admin-Benachrichtigung."],"New":["Neu"],"Send entries to 100+ popular apps.":["Eintr\u00e4ge an \u00fcber 100 beliebte Apps senden."],"Build automated workflows that run instantly.":["Erstellen Sie automatisierte Workflows, die sofort ausgef\u00fchrt werden."],"Create custom app integrations using our Custom App feature.":["Erstellen Sie benutzerdefinierte App-Integrationen mit unserer benutzerdefinierten App-Funktion."],"Keep your tools in sync automatically.":["Halten Sie Ihre Werkzeuge automatisch synchron."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Dies wird OttoKit auf Ihrer WordPress-Seite installieren und aktivieren, um Automatisierungsfunktionen zu erm\u00f6glichen."],"Automate Your Forms with OttoKit":["Automatisieren Sie Ihre Formulare mit OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Jede Formular\u00fcbermittlung sollte etwas ausl\u00f6sen \u2014 eine Slack-Benachrichtigung, einen CRM-Lead, eine Follow-up-E-Mail oder eine neue Zeile in Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Konfigurieren Sie die Berechtigungen des KI-Clients und die Einstellungen des MCP-Servers."],"View documentation":["Dokumentation anzeigen"],"Copy to clipboard":["In die Zwischenablage kopieren"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) oder %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Claude-Code"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (Projekt) oder ~\/.claude.json (global)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (Projekt) oder settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml oder config.json"],"Your client's MCP configuration file":["Die MCP-Konfigurationsdatei Ihres Kunden"],"Connect Your AI Client":["Verbinden Sie Ihren KI-Client"],"AI Client":["KI-Client"],"Create an Application Password \u2014 ":["Erstellen Sie ein Anwendungskennwort \u2014"],"Open Application Passwords":["Anwendungspassw\u00f6rter \u00f6ffnen"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Oder verwenden Sie diesen CLI-Befehl, um den Server schnell hinzuzuf\u00fcgen (Sie m\u00fcssen dennoch die Umgebungsvariablen festlegen):"],"Copy the JSON config below into: ":["Kopiere die JSON-Konfiguration unten in:"],"Replace \"your-application-password\" with the password from Step 1.":["Ersetzen Sie \"your-application-password\" durch das Passwort aus Schritt 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 der MCP-Endpunkt Ihrer Website. WP_API_USERNAME \u2014 Ihr WordPress-Benutzername. WP_API_PASSWORD \u2014 das Anwendungskennwort, das Sie erstellt haben."],"View setup docs":["Einrichtungsdokumente anzeigen"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Das MCP Adapter-Plugin ist installiert, aber nicht aktiv. Aktivieren Sie es, um die MCP-Einstellungen zu konfigurieren."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Das MCP Adapter-Plugin ist erforderlich, um AI-Clients mit Ihren Formularen zu verbinden. Laden Sie es von GitHub herunter, installieren Sie es und aktivieren Sie es dann."],"Download the latest release from":["Laden Sie die neueste Version herunter von"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installieren Sie das Plugin \u00fcber Plugins > Neues Plugin hinzuf\u00fcgen > Plugin hochladen."],"Activate the MCP Adapter plugin.":["Aktivieren Sie das MCP Adapter-Plugin."],"Activating\u2026":["Aktivierung\u2026"],"Activate MCP Adapter":["MCP-Adapter aktivieren"],"Download MCP Adapter":["MCP-Adapter herunterladen"],"Experimental":["Experimentell"],"Enable Abilities":["F\u00e4higkeiten aktivieren"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registriere die F\u00e4higkeiten von SureForms mit der WordPress Abilities API. Wenn aktiviert, k\u00f6nnen KI-Clients Ihre Formulare und Eintr\u00e4ge auflisten, lesen, erstellen, bearbeiten und l\u00f6schen. Wenn deaktiviert, werden keine F\u00e4higkeiten registriert und KI-Clients k\u00f6nnen keine Aktionen an Ihren Formularen durchf\u00fchren."],"Abilities API \u2014 Edit":["F\u00e4higkeiten-API \u2014 Bearbeiten"],"Enable Edit Abilities":["Bearbeitungsf\u00e4higkeiten aktivieren"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Wenn aktiviert, k\u00f6nnen KI-Clients neue Formulare erstellen, Formulartitel, Felder und Einstellungen aktualisieren, Formulare duplizieren und Eintragsstatus \u00e4ndern. Wenn deaktiviert, werden diese F\u00e4higkeiten abgemeldet und KI-Clients k\u00f6nnen nur Ihre Daten lesen."],"Abilities API \u2014 Delete":["F\u00e4higkeiten-API \u2014 L\u00f6schen"],"Enable Delete Abilities":["L\u00f6schf\u00e4higkeiten aktivieren"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Wenn aktiviert, k\u00f6nnen KI-Clients Formulare und Eintr\u00e4ge dauerhaft l\u00f6schen. Gel\u00f6schte Daten k\u00f6nnen nicht wiederhergestellt werden. Wenn deaktiviert, werden L\u00f6schf\u00e4higkeiten abgemeldet und KI-Clients k\u00f6nnen keine Daten entfernen."],"MCP Server":["MCP-Server"],"Enable MCP Server":["MCP-Server aktivieren"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Erstellt einen dedizierten SureForms MCP-Endpunkt, mit dem sich KI-Clients wie Claude verbinden k\u00f6nnen. Wenn er deaktiviert ist, wird der Endpunkt entfernt und externe KI-Clients k\u00f6nnen keine SureForms-Funktionen entdecken oder aufrufen."],"Learn more":["Mehr erfahren"],"MCP Adapter Required":["MCP-Adapter erforderlich"],"Heading 1":["\u00dcberschrift 1"],"Heading 2":["\u00dcberschrift 2"],"Heading 3":["\u00dcberschrift 3"],"Heading 4":["\u00dcberschrift 4"],"Heading 5":["\u00dcberschrift 5"],"Heading 6":["\u00dcberschrift 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Konfigurieren Sie den Google Maps API-Schl\u00fcssel f\u00fcr die Adressvervollst\u00e4ndigung und die Kartenansichtsvorschau."],"Help shape the future of SureForms":["Helfen Sie mit, die Zukunft von SureForms zu gestalten"],"Enable Google Address Autocomplete":["Google-Adress-Autovervollst\u00e4ndigung aktivieren"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Upgrade auf den SureForms Business-Plan, um Google-gest\u00fctzte Adressvervollst\u00e4ndigung mit interaktiver Kartenansicht zu Ihren Formularen hinzuzuf\u00fcgen."],"Auto-suggest addresses as users type for faster, error-free submissions":["Adressen automatisch vorschlagen, w\u00e4hrend Benutzer tippen, f\u00fcr schnellere und fehlerfreie Eingaben"],"Show an interactive map preview with draggable pin for precise locations":["Zeige eine interaktive Kartenansicht mit verschiebbarem Pin f\u00fcr pr\u00e4zise Standorte"],"Automatically populate address fields like city, state, and postal code":["Adressfelder wie Stadt, Bundesland und Postleitzahl automatisch ausf\u00fcllen"],"This form is now closed as we've received all the entries.":["Dieses Formular ist jetzt geschlossen, da wir alle Einsendungen erhalten haben."],"Thank you for contacting us! We will be in touch with you shortly.":["Vielen Dank, dass Sie uns kontaktiert haben! Wir werden uns in K\u00fcrze mit Ihnen in Verbindung setzen."],"Saving\u2026":["Speichern\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Wenn aktiviert, speichert dieses Formular die IP-Adresse des Benutzers, den Browsernamen und den Ger\u00e4tenamen nicht in den Eintr\u00e4gen."],"Form data":["Formulardaten"],"Unsaved changes":["Nicht gespeicherte \u00c4nderungen"],"Keep editing":["Weiter bearbeiten"],"Global Defaults":["Globale Standardeinstellungen"],"Form Restrictions":["Formulareinschr\u00e4nkungen"],"Configure default settings that apply to newly created forms.":["Standardm\u00e4\u00dfige Einstellungen konfigurieren, die f\u00fcr neu erstellte Formulare gelten."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Sammeln Sie nicht sensible Informationen von Ihrer Website, wie die verwendete PHP-Version und Funktionen, um uns zu helfen, Fehler schneller zu beheben, kl\u00fcgere Entscheidungen zu treffen und Funktionen zu entwickeln, die f\u00fcr Sie tats\u00e4chlich von Bedeutung sind."],"Failed to load pages. Please refresh and try again.":["Fehler beim Laden der Seiten. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut."],"Failed to load settings. Please refresh and try again.":["Fehler beim Laden der Einstellungen. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut."],"Form Validation fields cannot be left blank.":["Felder zur Formularvalidierung d\u00fcrfen nicht leer gelassen werden."],"Recipient email is required when email summaries are enabled.":["Die Empf\u00e4nger-E-Mail ist erforderlich, wenn E-Mail-Zusammenfassungen aktiviert sind."],"Please enter a valid recipient email.":["Bitte geben Sie eine g\u00fcltige Empf\u00e4nger-E-Mail-Adresse ein."],"Settings saved.":["Einstellungen gespeichert."],"Failed to save settings.":["Speichern der Einstellungen fehlgeschlagen."],"Some settings failed to save. Please retry.":["Einige Einstellungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Einige Felder enthalten ungespeicherte \u00c4nderungen. Verwerfen Sie diese, um fortzufahren, oder bleiben Sie, um Ihre \u00c4nderungen zu speichern."],"Discard & switch":["Verwerfen & wechseln"],"Import":["Importieren"],"Migration":["Migration"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Formulare von Contact Form 7, WPForms, Gravity Forms und anderen Plugins in SureForms importieren."],"Could not generate the import preview.":["Konnte die Importvorschau nicht erstellen."],"Import failed. Please try again or check your error logs.":["Import fehlgeschlagen. Bitte versuchen Sie es erneut oder \u00fcberpr\u00fcfen Sie Ihre Fehlerprotokolle."],"Go back":["Geh zur\u00fcck"],"Update & import":["Aktualisieren & importieren"],"Confirm & import":["Best\u00e4tigen & importieren"],"Form #%s":["Formular #%s"],"%d field will import":["%d Feld wird importiert"],"Some fields can't be migrated yet":["Einige Felder k\u00f6nnen noch nicht migriert werden"],"These fields will be skipped - add them manually after the form is created:":["Diese Felder werden \u00fcbersprungen - f\u00fcgen Sie sie manuell hinzu, nachdem das Formular erstellt wurde:"],"Some forms could not be parsed":["Einige Formulare konnten nicht analysiert werden"],"Update existing":["Vorhandenes aktualisieren"],"Create a new copy":["Erstellen Sie eine neue Kopie"],"Could not load forms for this source.":["Konnte die Formulare f\u00fcr diese Quelle nicht laden."],"(untitled form)":["(unbetiteltes Formular)"],"Previously imported":["Zuvor importiert"],"Open existing SureForms form in a new tab":["Vorhandenes SureForms-Formular in einem neuen Tab \u00f6ffnen"],"On re-import":["Beim erneuten Import"],"No forms found in this plugin.":["Keine Formulare in diesem Plugin gefunden."],"%1$d of %2$d form selected":["%1$d von %2$d Formular ausgew\u00e4hlt"],"Preview update":["Vorschau-Update"],"Preview import":["Importvorschau"],"Migration complete":["Migration abgeschlossen"],"Nothing was imported":["Nichts wurde importiert"],"%d form was imported into SureForms.":["%d Formular wurde in SureForms importiert."],"None of the selected forms could be migrated. See the warnings below for details.":["Keine der ausgew\u00e4hlten Formulare konnte migriert werden. Siehe die Warnungen unten f\u00fcr Details."],"Imported forms":["Importierte Formulare"],"Edit in SureForms":["In SureForms bearbeiten"],"Some fields were skipped during import":["Einige Felder wurden beim Import \u00fcbersprungen"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["F\u00fcgen Sie diese manuell im Formular-Editor hinzu - sie haben noch kein \u00c4quivalent in SureForms:"],"Some forms failed to import":["Einige Formulare konnten nicht importiert werden"],"Import more forms":["Mehr Formulare importieren"],"Could not load the list of importable plugins.":["Konnte die Liste der importierbaren Plugins nicht laden."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Auf dieser Website sind keine unterst\u00fctzten Formular-Plugins aktiv. Aktivieren Sie eines (wie Contact Form 7) mit mindestens einem Formular, um es in SureForms zu importieren."],"Plugin":["Plugin"],"%d form":["%d Formular"],"No forms":["Keine Formulare"],"Multiple choice":["Mehrfachauswahl"],"Consent":["Zustimmung"],"The image uploaded but could not be inserted. Please try again.":["Das Bild wurde hochgeladen, konnte aber nicht eingef\u00fcgt werden. Bitte versuchen Sie es erneut."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["Das Bild konnte nicht hochgeladen werden. Es k\u00f6nnte zu gro\u00df sein, oder Sie haben m\u00f6glicherweise keine Berechtigung, Dateien hochzuladen."],"Please choose an image file.":["Bitte w\u00e4hlen Sie eine Bilddatei aus."],"Show views and conversion rate":["Ansichten und Konversionsrate anzeigen"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["F\u00fcgt die Spalten Ansichten und Konversionsrate zur Formularliste hinzu. Eine Ansicht wird einmal pro Seitenbesuch gez\u00e4hlt, wenn das Formular auf dem Bildschirm erscheint, und die Konversionsrate ist der Anteil dieser Ansichten, die in einer Einreichung endeten. Die Z\u00e4hlung beginnt, sobald Sie dies aktivieren, sodass Einreichungen, die zuvor eingegangen sind, nicht zur Rate gez\u00e4hlt werden. Wenn Sie es danach deaktivieren, werden nur die Spalten ausgeblendet \u2014 die Z\u00e4hlung geht weiter, sodass die Zahlen aktuell sind, wenn Sie es wieder einschalten."],"Log cleared.":["Protokoll gel\u00f6scht."],"Could not clear the log.":["Protokoll konnte nicht gel\u00f6scht werden."],"Enable logs":["Protokolle aktivieren"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Erfasst Formular\u00fcbermittlungsfehler, die vom Browser gemeldet werden, damit Sie das Protokoll an den Support senden k\u00f6nnen, anstatt die Konsole zu lesen. Solange Ihre Formulare funktionieren, wird nichts geschrieben, und \u00fcbermittelte Werte werden niemals gespeichert."],"Download log":["Protokoll herunterladen"],"Confirm delete":["L\u00f6schen best\u00e4tigen"],"Log is full \u2014 download and clear it to keep recording.":["Protokoll ist voll \u2014 laden Sie es herunter und l\u00f6schen Sie es, um weiter aufzuzeichnen."],"Form Views & Conversion":["Formularansichten & Konversion"],"Logs":["Protokolle"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Ansichten und \u00dcbermittlungen von Personen, die die Seite bearbeiten k\u00f6nnen, werden normalerweise ausgelassen, damit das Testen Ihrer eigenen Formulare diese Zahlen nicht ver\u00e4ndert."],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}}
\ No newline at end of file
diff --git a/languages/sureforms-de_DE-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-de_DE-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json
index 600e100ff..cf2f49417 100644
--- a/languages/sureforms-de_DE-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json
+++ b/languages/sureforms-de_DE-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json
@@ -1 +1 @@
-{"translation-revision-date":"2024-12-13T16:14:52+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Armaturenbrett"],"Settings":["Einstellungen"],"Entries":["Eintr\u00e4ge"],"Activated":["Aktiviert"],"Advanced Fields":["Erweiterte Felder"],"Select Form":["Formular ausw\u00e4hlen"],"Create New Form":["Neues Formular erstellen"],"Forms":["Formulare"],"Copy":["Kopieren"],"Business":["Gesch\u00e4ft"],"Next":["Weiter"],"Back":["Zur\u00fcck"],"Cancel":["Abbrechen"],"This is where your form views will appear":["Hier werden Ihre Formularansichten angezeigt"],"Install":["Installieren"],"Plugin Installation failed, Please try again later.":["Plugin-Installation fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"Plugin activation failed, Please try again later.":["Plugin-Aktivierung fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"What's New?":["Was gibt's Neues?"],"Core":["Kern"],"Unlicensed":["Ohne Lizenz"],"Upgrade":["Aktualisieren"],"Webhooks":["Webhooks"],"Install & Activate":["Installieren & Aktivieren"],"Conditional Logic":["Bedingte Logik"],"Premium":["Premium"],"Welcome to SureForms!":["Willkommen bei SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms ist ein WordPress-Plugin, das es Benutzern erm\u00f6glicht, \u00fcber eine Drag-and-Drop-Oberfl\u00e4che ansprechend aussehende Formulare zu erstellen, ohne programmieren zu m\u00fcssen. Es integriert sich in den WordPress-Blockeditor."],"Read Full Guide":["Vollst\u00e4ndige Anleitung lesen"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Benutzerdefinierte WordPress-Formulare EINFACH GEMACHT"],"No Date":["Kein Datum"],"Invalid Date":["Ung\u00fcltiges Datum"],"Ready to go beyond free plan?":["Bereit, \u00fcber den kostenlosen Plan hinauszugehen?"],"Upgrade now":["Jetzt upgraden"],"and unlock the full power of SureForms!":["und die volle Kraft von SureForms freischalten!"],"Upgrade SureForms":["SureForms aktualisieren"],"Open Support Ticket":["Support-Ticket \u00f6ffnen"],"Help Center":["Hilfezentrum"],"Join our Community on Facebook":["Trete unserer Community auf Facebook bei"],"Leave Us a Review":["Hinterlassen Sie uns eine Bewertung"],"Quick Access":["Schneller Zugriff"],"Upgrade Now":["Jetzt upgraden"],"Clear Filters":["Filter l\u00f6schen"],"Unnamed Form":["Unbenanntes Formular"],"Forms Overview":["Formular\u00fcbersicht"],"Clear Form Filters":["Formularfilter l\u00f6schen"],"Clear Date Filters":["Datumsfilter l\u00f6schen"],"Select Date Range":["Datumsbereich ausw\u00e4hlen"],"Apply":["Anwenden"],"Please wait for the data to load":["Bitte warten Sie, bis die Daten geladen sind."],"No entries to display":["Keine Eintr\u00e4ge zum Anzeigen"],"Once you create a form and start receiving submissions, the data will appear here.":["Sobald Sie ein Formular erstellen und Einsendungen erhalten, werden die Daten hier angezeigt."],"Free":["Kostenlos"],"All Forms":["Alle Formulare"],"Guided Setup":["Gef\u00fchrte Einrichtung"],"Exit Guided Setup":["Gef\u00fchrte Einrichtung beenden"],"Skip":["\u00dcberspringen"],"Build beautiful forms visually":["Erstellen Sie sch\u00f6ne Formulare visuell"],"Works perfectly on mobile":["Funktioniert perfekt auf dem Handy"],"Easy to connect with automation tools":["Einfach mit Automatisierungstools zu verbinden"],"Welcome to SureForms":["Willkommen bei SureForms"],"Smart, Quick and Powerful Forms.":["Intelligente, schnelle und leistungsstarke Formulare."],"Let's Get Started":["Lass uns anfangen"],"Build up to 10 forms using AI":["Erstellen Sie bis zu 10 Formulare mit KI"],"A secure and private connection":["Eine sichere und private Verbindung"],"Smart form drafts based on your input":["Intelligente Formularentw\u00fcrfe basierend auf Ihren Eingaben"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Von einem leeren Formular aus zu beginnen, ist nicht immer einfach. Unsere KI kann helfen, indem sie ein Entwurfsformular basierend auf dem erstellt, was Sie tun m\u00f6chten \u2013 das spart Ihnen Zeit und gibt Ihnen eine klare Richtung."],"To do this, you'll need to connect your account.":["Um dies zu tun, m\u00fcssen Sie Ihr Konto verbinden."],"Let AI Help You Build Smarter, Faster Forms":["Lassen Sie KI Ihnen helfen, intelligentere und schnellere Formulare zu erstellen"],"Here's what that gives you:":["Hier ist, was Ihnen das gibt:"],"Continue":["Fortfahren"],"Connect":["Verbinden"],"Works smoothly with forms made using SureForms":["Funktioniert reibungslos mit Formularen, die mit SureForms erstellt wurden"],"Helps your emails reach the inbox instead of spam":["Hilft Ihren E-Mails, den Posteingang statt den Spam-Ordner zu erreichen"],"Setup is straightforward, even if you're not technical":["Die Einrichtung ist einfach, auch wenn Sie nicht technisch versiert sind."],"Lightweight and easy to use without adding clutter":["Leicht und einfach zu bedienen, ohne Unordnung hinzuzuf\u00fcgen"],"Make Sure Your Emails Get Delivered":["Stellen Sie sicher, dass Ihre E-Mails zugestellt werden"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["Die meisten WordPress-Seiten haben Schwierigkeiten, E-Mails zuverl\u00e4ssig zu versenden, was bedeutet, dass Formular\u00fcbermittlungen von Ihrer Seite m\u00f6glicherweise nicht in Ihrem Posteingang ankommen oder im Spam landen."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail ist ein einfaches SMTP-Plugin, das sicherstellt, dass Ihre E-Mails tats\u00e4chlich zugestellt werden."],"What you will get:":["Was Sie erhalten werden:"],"Install SureMail":["SureMail installieren"],"AI Form Generation":["KI-Formularerstellung"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["M\u00fcde vom manuellen Erstellen von Formularen? Lassen Sie die KI die Arbeit f\u00fcr Sie erledigen. Beschreiben Sie einfach, und unsere KI erstellt Ihr perfektes Formular in Sekundenschnelle."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Teilen Sie komplexe Formulare in einfache Schritte auf, um \u00dcberforderung zu reduzieren und die Abschlussraten zu erh\u00f6hen. F\u00fchren Sie die Benutzer reibungslos durch den Prozess."],"Conditional Fields":["Bedingte Felder"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Felder basierend auf den Antworten der Benutzer ein- oder ausblenden. Stellen Sie die richtigen Fragen und zeigen Sie nur das an, was ben\u00f6tigt wird, um Formulare sauber und relevant zu halten."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Verbessern Sie Ihre Formulare mit erweiterten Feldern wie Mehrfach-Datei-Upload, Bewertungsfeldern und Datums- & Uhrzeit-Auswahl, um reichhaltigere, flexiblere Daten zu sammeln."],"Conversational Forms":["Gespr\u00e4chsformulare"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Erstellen Sie Formulare, die sich wie ein Gespr\u00e4ch anf\u00fchlen. Eine Frage nach der anderen h\u00e4lt die Benutzer engagiert und erleichtert das Ausf\u00fcllen des Formulars."],"Digital Signatures":["Digitale Signaturen"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Sammeln Sie rechtsverbindliche digitale Unterschriften direkt in Ihren Formularen f\u00fcr Vereinbarungen, Genehmigungen und Vertr\u00e4ge."],"Calculators":["Taschenrechner"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["F\u00fcgen Sie interaktive Rechner zu Ihren Formularen hinzu, um Ihren Nutzern sofortige Sch\u00e4tzungen, Angebote und Berechnungen zu erm\u00f6glichen."],"User Registration and Login":["Benutzerregistrierung und Anmeldung"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Erm\u00f6glichen Sie Besuchern, sich auf Ihrer Website zu registrieren und anzumelden. N\u00fctzlich f\u00fcr Mitgliedschaften, Gemeinschaften oder jede Website, die Benutzerzugang ben\u00f6tigt."],"PDF Generation Made Simple":["PDF-Erstellung leicht gemacht"],"Custom App":["Benutzerdefinierte App"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Erfassen Sie Daten, senden Sie sie zur Verarbeitung an externe Anwendungen und zeigen Sie die Ergebnisse sofort an \u2013 alles nahtlos integriert, um dynamische, interaktive Benutzererlebnisse zu schaffen."],"Select Your Features":["W\u00e4hlen Sie Ihre Funktionen aus"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Erhalten Sie mehr Kontrolle, schnellere Arbeitsabl\u00e4ufe und tiefere Anpassungsm\u00f6glichkeiten \u2013 alles darauf ausgelegt, Ihnen zu helfen, bessere Websites mit weniger Aufwand zu erstellen."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Ausgew\u00e4hlte Funktionen erfordern %1$s - verwenden Sie den Code %2$s, um 10 % Rabatt auf jeden Plan zu erhalten."],"Copied":["Kopiert"],"Style your form to better match your site's design":["Gestalten Sie Ihr Formular so, dass es besser zum Design Ihrer Website passt"],"Add spam protection to block common bot submissions":["F\u00fcgen Sie einen Spam-Schutz hinzu, um h\u00e4ufige Bot-Einsendungen zu blockieren"],"Get weekly email reports with a summary of form activity":["Erhalten Sie w\u00f6chentliche E-Mail-Berichte mit einer Zusammenfassung der Formularaktivit\u00e4ten"],"You're All Set! \ud83d\ude80":["Alles ist bereit! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Verwenden Sie unseren KI-Formular-Builder, um schnell loszulegen, oder erstellen Sie Ihr Formular von Grund auf neu, wenn Sie bereits wissen, was Sie ben\u00f6tigen. Ihre Formulare sind bereit, erstellt, geteilt und mit Ihren Website-Besuchern verbunden zu werden."],"Final Touches That Make a Difference:":["Letzte Schliffe, die den Unterschied machen:"],"Build Your First Form":["Erstellen Sie Ihr erstes Formular"],"File Uploads":["Datei-Uploads"],"Signature & Rating":["Unterschrift & Bewertung"],"Calculation Forms":["Berechnungsformulare"],"And Much More\u2026":["Und vieles mehr\u2026"],"Upgrade to Pro":["Auf Pro upgraden"],"Unlock Premium Features":["Premium-Funktionen freischalten"],"Build Better Forms with SureForms":["Bauen Sie bessere Formulare mit SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["F\u00fcgen Sie erweiterte Felder, konversationelle Layouts und intelligente Logik hinzu, um Formulare zu erstellen, die Benutzer ansprechen und bessere Daten erfassen."],"SureForms Video Thumbnail":["SureForms Video-Miniaturansicht"],"Payments":["Zahlungen"],"Knowledge Base":["Wissensdatenbank"],"What\u2019s New":["Was gibt's Neues"],"Importing\u2026":["Importieren\u2026"],"Learn":["Lernen"],"Unable to complete action. Please try again.":["Aktion konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut."],"Supercharge Your Workflow":["Optimieren Sie Ihren Arbeitsablauf"],"Spam protection included":["Spam-Schutz inbegriffen"],"Multistep Forms":["Mehrstufige Formulare"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Senden Sie Formulareintr\u00e4ge sofort an jedes externe System oder Endpunkt, um erweiterte Workflows zu erm\u00f6glichen."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Formulareintr\u00e4ge automatisch in saubere, downloadbereite PDFs umwandeln. Perfekt f\u00fcr Aufzeichnungen, zum Teilen, Archivieren oder um Ordnung zu halten."],"Set up confirmation messages and email notifications for each entry":["Richten Sie Best\u00e4tigungsnachrichten und E-Mail-Benachrichtigungen f\u00fcr jeden Eintrag ein"],"Something went wrong. Please try again.":["Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut."],"Payment Forms":["Zahlungsformulare"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Erhalten Sie Zahlungen direkt \u00fcber Ihre Formulare. Akzeptieren Sie einmalige und wiederkehrende Zahlungen nahtlos."],"SureForms %s":["SureForms %s"],"First name is required.":["Vorname ist erforderlich."],"Please enter a valid email address.":["Bitte geben Sie eine g\u00fcltige E-Mail-Adresse ein."],"Email address is required.":["E-Mail-Adresse ist erforderlich."],"This is required.":["Dies ist erforderlich."],"Okay, just one last step\u2026":["Okay, nur noch ein letzter Schritt\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Helfen Sie uns, Ihre SureForms-Erfahrung zu personalisieren, indem Sie ein wenig \u00fcber sich selbst erz\u00e4hlen."],"First Name":["Vorname"],"Enter your first name":["Geben Sie Ihren Vornamen ein"],"Last Name":["Nachname"],"Enter your last name":["Geben Sie Ihren Nachnamen ein"],"Email Address":["E-Mail-Adresse"],"Enter your email address":["Geben Sie Ihre E-Mail-Adresse ein"],"Privacy Policy":["Datenschutzrichtlinie"],"Finish":["Fertig"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Bleiben Sie auf dem Laufenden und helfen Sie, SureForms mitzugestalten! Erhalten Sie Funktionsaktualisierungen und unterst\u00fctzen Sie uns bei der Verbesserung von SureForms, indem Sie mitteilen, wie Sie das Plugin verwenden. Datenschutzrichtlinie<\/a>."],"%d form ready":["%d Formular bereit"],"%d already imported":["%d bereits importiert"],"(unnamed source)":["(unbenannte Quelle)"],"All forms already imported":["Alle Formulare bereits importiert"],"Import forms":["Formulare importieren"],"Import %d form":["%d Formular importieren"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Beim Importieren ist ein Fehler aufgetreten. Sie k\u00f6nnen es erneut versuchen, \u00fcberspringen oder Einstellungen \u2192 Migration \u00f6ffnen."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Keine anderen Formular-Plugins erkannt. Sie k\u00f6nnen jederzeit \u00fcber Einstellungen \u2192 Migration importieren."],"Forms imported":["Formulare importiert"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d Formular von %2$s ist jetzt in SureForms, bereit zur Ver\u00f6ffentlichung, Gestaltung und Verbindung."],"%d form imported":["%d Formular importiert"],"%d form could not be imported":["%d Formular konnte nicht importiert werden"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d Feldtyp wurde nicht unterst\u00fctzt. Sie k\u00f6nnen es manuell in SureForms neu erstellen."],"Bring your existing forms with you":["Bringen Sie Ihre vorhandenen Formulare mit"],"We detected forms in another plugin. Pick one to import into SureForms.":["Wir haben Formulare in einem anderen Plugin erkannt. W\u00e4hlen Sie eines aus, um es in SureForms zu importieren."],"Choose a form plugin to import from":["W\u00e4hlen Sie ein Formular-Plugin zum Importieren aus"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importieren Sie mehr als ein Plugin? Sie k\u00f6nnen sp\u00e4ter zus\u00e4tzliche Quellen \u00fcber Einstellungen \u2192 Migration importieren."],"Import did not complete":["Import wurde nicht abgeschlossen"],"I'll do this later":["Ich mache das sp\u00e4ter"],"Retry":["Erneut versuchen"],"Form Checks":["Formular\u00fcberpr\u00fcfungen"],"Collapse form checks":["Formular\u00fcberpr\u00fcfungen einklappen"],"Expand form checks":["Formular\u00fcberpr\u00fcfungen ausklappen"],"Ignore":["Ignorieren"],"Working\u2026":["Arbeitet\u2026"]}}}
\ No newline at end of file
+{"translation-revision-date":"2024-12-13T16:14:52+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Armaturenbrett"],"Settings":["Einstellungen"],"Entries":["Eintr\u00e4ge"],"Activated":["Aktiviert"],"Advanced Fields":["Erweiterte Felder"],"Select Form":["Formular ausw\u00e4hlen"],"Create New Form":["Neues Formular erstellen"],"Forms":["Formulare"],"Copy":["Kopieren"],"Business":["Gesch\u00e4ft"],"Next":["Weiter"],"Back":["Zur\u00fcck"],"Close":["Schlie\u00dfen"],"Cancel":["Abbrechen"],"This is where your form views will appear":["Hier werden Ihre Formularansichten angezeigt"],"Install":["Installieren"],"Plugin Installation failed, Please try again later.":["Plugin-Installation fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"Plugin activation failed, Please try again later.":["Plugin-Aktivierung fehlgeschlagen, bitte versuchen Sie es sp\u00e4ter erneut."],"What's New?":["Was gibt's Neues?"],"Core":["Kern"],"Unlicensed":["Ohne Lizenz"],"Upgrade":["Aktualisieren"],"Webhooks":["Webhooks"],"Install & Activate":["Installieren & Aktivieren"],"Contact Support":["Support kontaktieren"],"Conditional Logic":["Bedingte Logik"],"Premium":["Premium"],"Welcome to SureForms!":["Willkommen bei SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms ist ein WordPress-Plugin, das es Benutzern erm\u00f6glicht, \u00fcber eine Drag-and-Drop-Oberfl\u00e4che ansprechend aussehende Formulare zu erstellen, ohne programmieren zu m\u00fcssen. Es integriert sich in den WordPress-Blockeditor."],"Read Full Guide":["Vollst\u00e4ndige Anleitung lesen"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Benutzerdefinierte WordPress-Formulare EINFACH GEMACHT"],"No Date":["Kein Datum"],"Invalid Date":["Ung\u00fcltiges Datum"],"Ready to go beyond free plan?":["Bereit, \u00fcber den kostenlosen Plan hinauszugehen?"],"Upgrade now":["Jetzt upgraden"],"and unlock the full power of SureForms!":["und die volle Kraft von SureForms freischalten!"],"Upgrade SureForms":["SureForms aktualisieren"],"Open Support Ticket":["Support-Ticket \u00f6ffnen"],"Help Center":["Hilfezentrum"],"Join our Community on Facebook":["Trete unserer Community auf Facebook bei"],"Leave Us a Review":["Hinterlassen Sie uns eine Bewertung"],"Quick Access":["Schneller Zugriff"],"Upgrade Now":["Jetzt upgraden"],"Clear Filters":["Filter l\u00f6schen"],"Unnamed Form":["Unbenanntes Formular"],"Forms Overview":["Formular\u00fcbersicht"],"Clear Form Filters":["Formularfilter l\u00f6schen"],"Clear Date Filters":["Datumsfilter l\u00f6schen"],"Select Date Range":["Datumsbereich ausw\u00e4hlen"],"Apply":["Anwenden"],"Please wait for the data to load":["Bitte warten Sie, bis die Daten geladen sind."],"No entries to display":["Keine Eintr\u00e4ge zum Anzeigen"],"Once you create a form and start receiving submissions, the data will appear here.":["Sobald Sie ein Formular erstellen und Einsendungen erhalten, werden die Daten hier angezeigt."],"Free":["Kostenlos"],"All Forms":["Alle Formulare"],"Guided Setup":["Gef\u00fchrte Einrichtung"],"Exit Guided Setup":["Gef\u00fchrte Einrichtung beenden"],"Skip":["\u00dcberspringen"],"Build beautiful forms visually":["Erstellen Sie sch\u00f6ne Formulare visuell"],"Works perfectly on mobile":["Funktioniert perfekt auf dem Handy"],"Easy to connect with automation tools":["Einfach mit Automatisierungstools zu verbinden"],"Welcome to SureForms":["Willkommen bei SureForms"],"Smart, Quick and Powerful Forms.":["Intelligente, schnelle und leistungsstarke Formulare."],"Let's Get Started":["Lass uns anfangen"],"Build up to 10 forms using AI":["Erstellen Sie bis zu 10 Formulare mit KI"],"A secure and private connection":["Eine sichere und private Verbindung"],"Smart form drafts based on your input":["Intelligente Formularentw\u00fcrfe basierend auf Ihren Eingaben"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Von einem leeren Formular aus zu beginnen, ist nicht immer einfach. Unsere KI kann helfen, indem sie ein Entwurfsformular basierend auf dem erstellt, was Sie tun m\u00f6chten \u2013 das spart Ihnen Zeit und gibt Ihnen eine klare Richtung."],"To do this, you'll need to connect your account.":["Um dies zu tun, m\u00fcssen Sie Ihr Konto verbinden."],"Let AI Help You Build Smarter, Faster Forms":["Lassen Sie KI Ihnen helfen, intelligentere und schnellere Formulare zu erstellen"],"Here's what that gives you:":["Hier ist, was Ihnen das gibt:"],"Continue":["Fortfahren"],"Connect":["Verbinden"],"Works smoothly with forms made using SureForms":["Funktioniert reibungslos mit Formularen, die mit SureForms erstellt wurden"],"Helps your emails reach the inbox instead of spam":["Hilft Ihren E-Mails, den Posteingang statt den Spam-Ordner zu erreichen"],"Setup is straightforward, even if you're not technical":["Die Einrichtung ist einfach, auch wenn Sie nicht technisch versiert sind."],"Lightweight and easy to use without adding clutter":["Leicht und einfach zu bedienen, ohne Unordnung hinzuzuf\u00fcgen"],"Make Sure Your Emails Get Delivered":["Stellen Sie sicher, dass Ihre E-Mails zugestellt werden"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["Die meisten WordPress-Seiten haben Schwierigkeiten, E-Mails zuverl\u00e4ssig zu versenden, was bedeutet, dass Formular\u00fcbermittlungen von Ihrer Seite m\u00f6glicherweise nicht in Ihrem Posteingang ankommen oder im Spam landen."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail ist ein einfaches SMTP-Plugin, das sicherstellt, dass Ihre E-Mails tats\u00e4chlich zugestellt werden."],"What you will get:":["Was Sie erhalten werden:"],"Install SureMail":["SureMail installieren"],"AI Form Generation":["KI-Formularerstellung"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["M\u00fcde vom manuellen Erstellen von Formularen? Lassen Sie die KI die Arbeit f\u00fcr Sie erledigen. Beschreiben Sie einfach, und unsere KI erstellt Ihr perfektes Formular in Sekundenschnelle."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Teilen Sie komplexe Formulare in einfache Schritte auf, um \u00dcberforderung zu reduzieren und die Abschlussraten zu erh\u00f6hen. F\u00fchren Sie die Benutzer reibungslos durch den Prozess."],"Conditional Fields":["Bedingte Felder"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Felder basierend auf den Antworten der Benutzer ein- oder ausblenden. Stellen Sie die richtigen Fragen und zeigen Sie nur das an, was ben\u00f6tigt wird, um Formulare sauber und relevant zu halten."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Verbessern Sie Ihre Formulare mit erweiterten Feldern wie Mehrfach-Datei-Upload, Bewertungsfeldern und Datums- & Uhrzeit-Auswahl, um reichhaltigere, flexiblere Daten zu sammeln."],"Conversational Forms":["Gespr\u00e4chsformulare"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Erstellen Sie Formulare, die sich wie ein Gespr\u00e4ch anf\u00fchlen. Eine Frage nach der anderen h\u00e4lt die Benutzer engagiert und erleichtert das Ausf\u00fcllen des Formulars."],"Digital Signatures":["Digitale Signaturen"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Sammeln Sie rechtsverbindliche digitale Unterschriften direkt in Ihren Formularen f\u00fcr Vereinbarungen, Genehmigungen und Vertr\u00e4ge."],"Calculators":["Taschenrechner"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["F\u00fcgen Sie interaktive Rechner zu Ihren Formularen hinzu, um Ihren Nutzern sofortige Sch\u00e4tzungen, Angebote und Berechnungen zu erm\u00f6glichen."],"User Registration and Login":["Benutzerregistrierung und Anmeldung"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Erm\u00f6glichen Sie Besuchern, sich auf Ihrer Website zu registrieren und anzumelden. N\u00fctzlich f\u00fcr Mitgliedschaften, Gemeinschaften oder jede Website, die Benutzerzugang ben\u00f6tigt."],"PDF Generation Made Simple":["PDF-Erstellung leicht gemacht"],"Custom App":["Benutzerdefinierte App"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Erfassen Sie Daten, senden Sie sie zur Verarbeitung an externe Anwendungen und zeigen Sie die Ergebnisse sofort an \u2013 alles nahtlos integriert, um dynamische, interaktive Benutzererlebnisse zu schaffen."],"Select Your Features":["W\u00e4hlen Sie Ihre Funktionen aus"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Erhalten Sie mehr Kontrolle, schnellere Arbeitsabl\u00e4ufe und tiefere Anpassungsm\u00f6glichkeiten \u2013 alles darauf ausgelegt, Ihnen zu helfen, bessere Websites mit weniger Aufwand zu erstellen."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Ausgew\u00e4hlte Funktionen erfordern %1$s - verwenden Sie den Code %2$s, um 10 % Rabatt auf jeden Plan zu erhalten."],"Copied":["Kopiert"],"Style your form to better match your site's design":["Gestalten Sie Ihr Formular so, dass es besser zum Design Ihrer Website passt"],"Add spam protection to block common bot submissions":["F\u00fcgen Sie einen Spam-Schutz hinzu, um h\u00e4ufige Bot-Einsendungen zu blockieren"],"Get weekly email reports with a summary of form activity":["Erhalten Sie w\u00f6chentliche E-Mail-Berichte mit einer Zusammenfassung der Formularaktivit\u00e4ten"],"You're All Set! \ud83d\ude80":["Alles ist bereit! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Verwenden Sie unseren KI-Formular-Builder, um schnell loszulegen, oder erstellen Sie Ihr Formular von Grund auf neu, wenn Sie bereits wissen, was Sie ben\u00f6tigen. Ihre Formulare sind bereit, erstellt, geteilt und mit Ihren Website-Besuchern verbunden zu werden."],"Final Touches That Make a Difference:":["Letzte Schliffe, die den Unterschied machen:"],"Build Your First Form":["Erstellen Sie Ihr erstes Formular"],"File Uploads":["Datei-Uploads"],"Signature & Rating":["Unterschrift & Bewertung"],"Calculation Forms":["Berechnungsformulare"],"And Much More\u2026":["Und vieles mehr\u2026"],"Upgrade to Pro":["Auf Pro upgraden"],"Unlock Premium Features":["Premium-Funktionen freischalten"],"Build Better Forms with SureForms":["Bauen Sie bessere Formulare mit SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["F\u00fcgen Sie erweiterte Felder, konversationelle Layouts und intelligente Logik hinzu, um Formulare zu erstellen, die Benutzer ansprechen und bessere Daten erfassen."],"SureForms Video Thumbnail":["SureForms Video-Miniaturansicht"],"Payments":["Zahlungen"],"Knowledge Base":["Wissensdatenbank"],"What\u2019s New":["Was gibt's Neues"],"Importing\u2026":["Importieren\u2026"],"Learn":["Lernen"],"Unable to complete action. Please try again.":["Aktion konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut."],"Supercharge Your Workflow":["Optimieren Sie Ihren Arbeitsablauf"],"Spam protection included":["Spam-Schutz inbegriffen"],"Multistep Forms":["Mehrstufige Formulare"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Senden Sie Formulareintr\u00e4ge sofort an jedes externe System oder Endpunkt, um erweiterte Workflows zu erm\u00f6glichen."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Formulareintr\u00e4ge automatisch in saubere, downloadbereite PDFs umwandeln. Perfekt f\u00fcr Aufzeichnungen, zum Teilen, Archivieren oder um Ordnung zu halten."],"Set up confirmation messages and email notifications for each entry":["Richten Sie Best\u00e4tigungsnachrichten und E-Mail-Benachrichtigungen f\u00fcr jeden Eintrag ein"],"Something went wrong. Please try again.":["Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut."],"Payment Forms":["Zahlungsformulare"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Erhalten Sie Zahlungen direkt \u00fcber Ihre Formulare. Akzeptieren Sie einmalige und wiederkehrende Zahlungen nahtlos."],"SureForms %s":["SureForms %s"],"First name is required.":["Vorname ist erforderlich."],"Please enter a valid email address.":["Bitte geben Sie eine g\u00fcltige E-Mail-Adresse ein."],"Email address is required.":["E-Mail-Adresse ist erforderlich."],"This is required.":["Dies ist erforderlich."],"Okay, just one last step\u2026":["Okay, nur noch ein letzter Schritt\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Helfen Sie uns, Ihre SureForms-Erfahrung zu personalisieren, indem Sie ein wenig \u00fcber sich selbst erz\u00e4hlen."],"First Name":["Vorname"],"Enter your first name":["Geben Sie Ihren Vornamen ein"],"Last Name":["Nachname"],"Enter your last name":["Geben Sie Ihren Nachnamen ein"],"Email Address":["E-Mail-Adresse"],"Enter your email address":["Geben Sie Ihre E-Mail-Adresse ein"],"Privacy Policy":["Datenschutzrichtlinie"],"Finish":["Fertig"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Bleiben Sie auf dem Laufenden und helfen Sie, SureForms mitzugestalten! Erhalten Sie Funktionsaktualisierungen und unterst\u00fctzen Sie uns bei der Verbesserung von SureForms, indem Sie mitteilen, wie Sie das Plugin verwenden. Datenschutzrichtlinie<\/a>."],"%d form ready":["%d Formular bereit"],"%d already imported":["%d bereits importiert"],"(unnamed source)":["(unbenannte Quelle)"],"All forms already imported":["Alle Formulare bereits importiert"],"Import forms":["Formulare importieren"],"Import %d form":["%d Formular importieren"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Beim Importieren ist ein Fehler aufgetreten. Sie k\u00f6nnen es erneut versuchen, \u00fcberspringen oder Einstellungen \u2192 Migration \u00f6ffnen."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Keine anderen Formular-Plugins erkannt. Sie k\u00f6nnen jederzeit \u00fcber Einstellungen \u2192 Migration importieren."],"Forms imported":["Formulare importiert"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d Formular von %2$s ist jetzt in SureForms, bereit zur Ver\u00f6ffentlichung, Gestaltung und Verbindung."],"%d form imported":["%d Formular importiert"],"%d form could not be imported":["%d Formular konnte nicht importiert werden"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d Feldtyp wurde nicht unterst\u00fctzt. Sie k\u00f6nnen es manuell in SureForms neu erstellen."],"Bring your existing forms with you":["Bringen Sie Ihre vorhandenen Formulare mit"],"We detected forms in another plugin. Pick one to import into SureForms.":["Wir haben Formulare in einem anderen Plugin erkannt. W\u00e4hlen Sie eines aus, um es in SureForms zu importieren."],"Choose a form plugin to import from":["W\u00e4hlen Sie ein Formular-Plugin zum Importieren aus"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importieren Sie mehr als ein Plugin? Sie k\u00f6nnen sp\u00e4ter zus\u00e4tzliche Quellen \u00fcber Einstellungen \u2192 Migration importieren."],"Import did not complete":["Import wurde nicht abgeschlossen"],"I'll do this later":["Ich mache das sp\u00e4ter"],"Retry":["Erneut versuchen"],"Form Checks":["Formular\u00fcberpr\u00fcfungen"],"Collapse form checks":["Formular\u00fcberpr\u00fcfungen einklappen"],"Expand form checks":["Formular\u00fcberpr\u00fcfungen ausklappen"],"Ignore":["Ignorieren"],"Working\u2026":["Arbeitet\u2026"],"Details":["Details"],"Copy details":["Details kopieren"]}}}
\ No newline at end of file
diff --git a/languages/sureforms-de_DE.mo b/languages/sureforms-de_DE.mo
index 3e09681508f0a2130c538086d4d09e33208086c2..a7e76d0c3c3df5c56fa19d38b7ad8db2b6fc3e60 100644
GIT binary patch
delta 76809
zcmX`!2i(rp|M>CizHbd>luBe}2ySd_L#A&-;w)x^MD*eSMxC@8?N=l`G5q1pjZ^)rmww
zyy;;}iNt;R(*EDexSBw@6Hdh(_!-v2lXxYTxGpW>8&$9!R>N#~Kjz2TX!~ohD(=Rt
zi9|B-PkbOt{_tR7EW!hIu{HKW8(fXq@f;SyECtdMg|R4(#>RLfZo(?~CsxNY1yda*
zZpGY`??3~)4{xIX#0)O-P;nR?>5pg!zvESSb)mGx^;i_I#YX5zx}yP&L6Vo4f{yg@
zSl)oeDZhht@MJ6(z21%!i57Sn{U;vd;tE`XnYbEF@w;ehKS3k^26N$`Xkb?q4gnNE
zpDTj~SOXnETQso2XhtWX16huj<2FqC;BGGJ;t4E)nVI2*=Gcew9oPon#oI7Xk+ei_
z9E(oT$7p?K(J%!MpdGx84R9ak#Vd=YC30dBbbwWgr6p4r4XH5o?eS9Vie}_ibZQ2n
zFHS^9{s=nK
z8l$Oi8_WHoccCL38_SdM63R2tT`ⅈXEviRZE6II-+wwADiIuBo}?TC|N2kk%7~Z
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

diff --git a/languages/sureforms-de_DE.po b/languages/sureforms-de_DE.po
index 14eb4d524..d918b6498 100644
--- a/languages/sureforms-de_DE.po
+++ b/languages/sureforms-de_DE.po
@@ -14,9 +14,9 @@ msgstr ""
 #. Plugin Name of the plugin
 #. Author of the plugin
 #: sureforms.php
-#: admin/admin.php:1152
-#: admin/admin.php:1153
-#: admin/admin.php:3021
+#: admin/admin.php:1198
+#: admin/admin.php:1199
+#: admin/admin.php:3185
 #: inc/abilities/abilities-registrar.php:139
 #: inc/gutenberg-hooks.php:109
 #: inc/page-builders/bricks/elements/form-widget.php:67
@@ -43,8 +43,8 @@ msgstr "Eine einfache, aber leistungsstarke Möglichkeit, moderne Formulare für
 msgid "https://sureforms.com/"
 msgstr "https://sureforms.com/"
 
-#: admin/admin.php:1165
-#: admin/admin.php:1166
+#: admin/admin.php:1211
+#: admin/admin.php:1212
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -59,9 +59,9 @@ msgstr "https://sureforms.com/"
 msgid "Dashboard"
 msgstr "Armaturenbrett"
 
-#: admin/admin.php:1183
-#: admin/admin.php:1184
-#: admin/admin.php:1616
+#: admin/admin.php:1229
+#: admin/admin.php:1230
+#: admin/admin.php:1662
 #: inc/compatibility/multilingual/string-collector.php:236
 #: inc/compatibility/multilingual/string-collector.php:245
 #: inc/compatibility/multilingual/string-collector.php:313
@@ -79,15 +79,15 @@ msgstr "Armaturenbrett"
 msgid "Settings"
 msgstr "Einstellungen"
 
-#: admin/admin.php:1452
-#: admin/admin.php:1453
+#: admin/admin.php:1498
+#: admin/admin.php:1499
 #: inc/post-types.php:270
 msgid "New Form"
 msgstr "Neues Formular"
 
-#: admin/admin.php:1461
-#: admin/admin.php:1462
-#: admin/admin.php:3340
+#: admin/admin.php:1507
+#: admin/admin.php:1508
+#: admin/admin.php:3504
 #: inc/generate-form-markup.php:211
 #: inc/generate-form-markup.php:224
 #: inc/global-settings/email-summary.php:225
@@ -105,7 +105,7 @@ msgid "Entries"
 msgstr "Einträge"
 
 #. Translators: Post Title.
-#: admin/admin.php:1704
+#: admin/admin.php:1750
 #, php-format
 msgid "Edit %1$s"
 msgstr "%1$s bearbeiten"
@@ -118,7 +118,7 @@ msgstr "%1$s bearbeiten"
 #: inc/duplicate-form.php:191
 #: inc/export.php:126
 #: inc/export.php:184
-#: inc/forms-data.php:88
+#: inc/forms-data.php:108
 #: inc/global-settings/global-settings.php:89
 #: inc/global-settings/global-settings.php:639
 #: inc/rest-api.php:203
@@ -126,18 +126,18 @@ msgid "Nonce verification failed."
 msgstr "Überprüfung des Nonce ist fehlgeschlagen."
 
 #. translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
-#: admin/admin.php:2572
+#: admin/admin.php:2626
 #, php-format
 msgid "Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more."
 msgstr "Bitte %1$saktivieren%2$s Sie Ihre Kopie von %3$s, um neue Funktionen zu erhalten, Support zu nutzen, Update-Benachrichtigungen zu erhalten und mehr."
 
 #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
-#: admin/admin.php:2590
+#: admin/admin.php:2644
 #, php-format
 msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s."
 msgstr "SureForms %1$s erfordert mindestens %2$s %3$s, um ordnungsgemäß zu funktionieren. Bitte aktualisieren Sie auf die neueste Version von %4$shier%5$s."
 
-#: admin/admin.php:3339
+#: admin/admin.php:3503
 #: inc/global-settings/email-summary.php:224
 #: assets/build/entries.js:172
 #: assets/build/payments.js:172
@@ -207,7 +207,7 @@ msgstr "Müll"
 msgid "Published"
 msgstr "Veröffentlicht"
 
-#: admin/admin.php:3331
+#: admin/admin.php:3495
 msgid "View"
 msgstr "Ansehen"
 
@@ -299,11 +299,11 @@ msgstr "Kein Plugin angegeben"
 msgid "Plugin Successfully Activated"
 msgstr "Plugin erfolgreich aktiviert"
 
-#: admin/admin.php:1780
+#: admin/admin.php:1834
 msgid "Activating..."
 msgstr "Aktivieren..."
 
-#: admin/admin.php:1781
+#: admin/admin.php:1835
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/formEditor.js:172
@@ -318,17 +318,17 @@ msgstr "Aktivieren..."
 msgid "Activated"
 msgstr "Aktiviert"
 
-#: admin/admin.php:1782
+#: admin/admin.php:1836
 #: assets/build/formEditor.js:172
 #: assets/build/settings.js:172
 msgid "Activate"
 msgstr "Aktivieren"
 
-#: admin/admin.php:1783
+#: admin/admin.php:1837
 msgid "Installing..."
 msgstr "Installation läuft..."
 
-#: admin/admin.php:1784
+#: admin/admin.php:1838
 msgid "Installed"
 msgstr "Installiert"
 
@@ -336,12 +336,13 @@ msgstr "Installiert"
 msgid "You do not have permission to access this page."
 msgstr "Sie haben keine Berechtigung, auf diese Seite zuzugreifen."
 
-#: admin/admin.php:2784
-#: admin/admin.php:2912
-#: admin/admin.php:2950
-#: admin/admin.php:2971
-#: admin/admin.php:3294
-#: admin/admin.php:3601
+#: admin/admin.php:2874
+#: admin/admin.php:2917
+#: admin/admin.php:3076
+#: admin/admin.php:3114
+#: admin/admin.php:3135
+#: admin/admin.php:3458
+#: admin/admin.php:3847
 #: inc/admin-ajax.php:164
 #: inc/payments/admin/admin-handler.php:638
 #: inc/payments/stripe/admin-stripe-handler.php:80
@@ -354,7 +355,7 @@ msgid "Form ID is required."
 msgstr "Formular-ID ist erforderlich."
 
 #: inc/admin-ajax.php:184
-#: inc/form-submit.php:1153
+#: inc/form-submit.php:1261
 msgid "Invalid form ID."
 msgstr "Ungültige Formular-ID."
 
@@ -593,28 +594,28 @@ msgstr "Telefon"
 msgid "Textarea"
 msgstr "Textbereich"
 
-#: inc/form-submit.php:231
+#: inc/form-submit.php:281
 msgid "Cloudflare Turnstile secret key is invalid."
 msgstr "Der geheime Schlüssel von Cloudflare Turnstile ist ungültig."
 
-#: inc/form-submit.php:238
+#: inc/form-submit.php:288
 msgid "Cloudflare Turnstile response is missing."
 msgstr "Cloudflare Turnstile-Antwort fehlt."
 
-#: inc/form-submit.php:282
+#: inc/form-submit.php:332
 msgid "hCaptcha secret key is invalid."
 msgstr "Der hCaptcha-Geheimschlüssel ist ungültig."
 
-#: inc/form-submit.php:289
+#: inc/form-submit.php:339
 msgid "hCaptcha response is missing."
 msgstr "hCaptcha-Antwort fehlt."
 
-#: inc/form-submit.php:330
-#: inc/form-submit.php:589
+#: inc/form-submit.php:380
+#: inc/form-submit.php:639
 msgid "Form data is not found."
 msgstr "Formulardaten wurden nicht gefunden."
 
-#: inc/forms-data.php:68
+#: inc/forms-data.php:88
 msgid "Sorry, you are not allowed to view the form."
 msgstr "Entschuldigung, Sie dürfen das Formular nicht ansehen."
 
@@ -724,7 +725,7 @@ msgstr "Formular bearbeiten"
 msgid "Edit"
 msgstr "Bearbeiten"
 
-#: admin/admin.php:3072
+#: admin/admin.php:3236
 #: inc/page-builders/elementor/form-widget.php:346
 #: assets/build/dashboard.js:2
 msgid "Create New Form"
@@ -761,8 +762,8 @@ msgstr "Formular anzeigen"
 msgid "View Forms"
 msgstr "Formulare anzeigen"
 
-#: admin/admin.php:1443
-#: admin/admin.php:1444
+#: admin/admin.php:1489
+#: admin/admin.php:1490
 #: inc/post-types.php:274
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
@@ -8276,7 +8277,7 @@ msgstr "Sozial"
 msgid "Travel"
 msgstr "Reise"
 
-#: inc/helper.php:2375
+#: inc/helper.php:2368
 msgid "Blank Form"
 msgstr "Leeres Formular"
 
@@ -8595,7 +8596,9 @@ msgstr "Bitte fügen Sie eine Option props zu MultiButtonsControl hinzu"
 msgid "Icon Library"
 msgstr "Icon-Bibliothek"
 
+#: admin/admin.php:4062
 #: assets/build/blocks.js:172
+#: assets/build/dashboard.js:172
 #: modules/gutenberg/build/blocks.js:4
 msgid "Close"
 msgstr "Schließen"
@@ -8884,9 +8887,9 @@ msgstr "Blöcke zur Schnellzugriffsleiste hinzufügen"
 msgid "Re-arrange block inside Quick Action Bar"
 msgstr "Block im Schnellaktionsbereich neu anordnen"
 
-#: admin/admin.php:1236
-#: admin/admin.php:1237
-#: admin/admin.php:4129
+#: admin/admin.php:1282
+#: admin/admin.php:1283
+#: admin/admin.php:4737
 #: assets/build/blocks.js:172
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
@@ -9259,7 +9262,7 @@ msgstr "Zentrum"
 msgid "Right"
 msgstr "Richtig"
 
-#: inc/form-submit.php:1344
+#: inc/form-submit.php:1452
 #: assets/build/formEditor.js:172
 #: assets/build/settings.js:172
 msgid "Google reCAPTCHA"
@@ -9269,7 +9272,7 @@ msgstr "Google reCAPTCHA"
 msgid "CloudFlare Turnstile"
 msgstr "CloudFlare Turnstile"
 
-#: inc/form-submit.php:1348
+#: inc/form-submit.php:1456
 #: assets/build/formEditor.js:172
 #: assets/build/settings.js:172
 msgid "hCaptcha"
@@ -9551,7 +9554,7 @@ msgstr "Standortschlüssel"
 msgid "Secret Key"
 msgstr "Geheimer Schlüssel"
 
-#: inc/form-submit.php:1352
+#: inc/form-submit.php:1460
 #: assets/build/settings.js:172
 msgid "Cloudflare Turnstile"
 msgstr "Cloudflare-Drehkreuz"
@@ -9576,7 +9579,8 @@ msgstr "Jetzt verbinden"
 msgid "Authentication Failed"
 msgstr "Authentifizierung fehlgeschlagen"
 
-#: admin/admin.php:3516
+#: admin/admin.php:4061
+#: assets/build/dashboard.js:172
 #: assets/build/templatePicker.js:172
 msgid "Contact Support"
 msgstr "Support kontaktieren"
@@ -10510,7 +10514,7 @@ msgstr "Fügen Sie Ihrer Website atemberaubende anpassbare Symbole hinzu."
 msgid "icon"
 msgstr "Symbol"
 
-#: admin/admin.php:2639
+#: admin/admin.php:2693
 msgid "Rate SureForms"
 msgstr "Bewerten Sie SureForms"
 
@@ -10566,18 +10570,18 @@ msgstr "Notiz hinzufügen entsperren"
 msgid "With the SureForms Starter plan, enhance your submitted form entries by adding personalized notes for better clarity and tracking."
 msgstr "Mit dem SureForms Starter-Plan verbessern Sie Ihre eingereichten Formulareinträge, indem Sie personalisierte Notizen für bessere Klarheit und Nachverfolgung hinzufügen."
 
-#: inc/form-submit.php:990
+#: inc/form-submit.php:1046
 msgid "Email notification passed to the sending server"
 msgstr "E-Mail-Benachrichtigung an den sendenden Server übermittelt"
 
 #. translators: Here, %s is the comma separated emails list.
-#: inc/form-submit.php:1063
+#: inc/form-submit.php:1130
 #, php-format
 msgid "Email notification recipient: %s"
 msgstr "E-Mail-Benachrichtigungsempfänger: %s"
 
 #. translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ).
-#: inc/form-submit.php:1080
+#: inc/form-submit.php:1147
 #, php-format
 msgid "Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s"
 msgstr "E-Mail-Server konnte die E-Mail-Benachrichtigung nicht senden. Empfänger: %1$s. Grund: %2$s"
@@ -10620,7 +10624,7 @@ msgstr "Konversationelle Formulare freischalten"
 msgid "With the SureForms Pro Plan, you can transform your forms into engaging conversational layouts for a seamless user experience."
 msgstr "Mit dem SureForms Pro Plan können Sie Ihre Formulare in ansprechende, dialogorientierte Layouts verwandeln, um ein nahtloses Benutzererlebnis zu schaffen."
 
-#: admin/admin.php:1075
+#: admin/admin.php:1121
 msgid "Get SureForms Pro"
 msgstr "Holen Sie sich SureForms Pro"
 
@@ -10832,24 +10836,24 @@ msgstr "Gradient auswählen"
 msgid "reCAPTCHA"
 msgstr "reCAPTCHA"
 
-#: inc/form-submit.php:1288
+#: inc/form-submit.php:1396
 msgid "Captcha validation failed. No error code provided."
 msgstr "Captcha-Überprüfung fehlgeschlagen. Kein Fehlercode angegeben."
 
-#: inc/form-submit.php:1289
+#: inc/form-submit.php:1397
 msgid "Captcha validation failed."
 msgstr "Captcha-Überprüfung fehlgeschlagen."
 
-#: inc/form-submit.php:1356
+#: inc/form-submit.php:1464
 msgid "Unknown Captcha"
 msgstr "Unbekanntes Captcha"
 
-#: inc/form-submit.php:1357
+#: inc/form-submit.php:1465
 msgid "Invalid captcha type."
 msgstr "Ungültiger Captcha-Typ."
 
 #. translators: %s is the captcha title.
-#: inc/form-submit.php:1370
+#: inc/form-submit.php:1478
 #, php-format
 msgid "%s verification failed. Please contact your site administrator."
 msgstr "%s-Verifizierung fehlgeschlagen. Bitte wenden Sie sich an Ihren Website-Administrator."
@@ -11246,8 +11250,8 @@ msgstr "Beschreiben Sie das Formular, das Sie erstellen möchten"
 msgid "We are building your form…"
 msgstr "Wir erstellen Ihr Formular…"
 
-#: admin/admin.php:1351
-#: admin/admin.php:1352
+#: admin/admin.php:1397
+#: admin/admin.php:1398
 msgid "SMTP"
 msgstr "SMTP"
 
@@ -11379,31 +11383,32 @@ msgstr "Admin-Benachrichtigung aktivieren"
 msgid "Admin notifications keep you informed about new form entries since your last visit."
 msgstr "Admin-Benachrichtigungen halten Sie über neue Formulareinträge seit Ihrem letzten Besuch auf dem Laufenden."
 
-#: admin/admin.php:2789
-#: admin/admin.php:2908
-#: admin/admin.php:2946
-#: admin/admin.php:2967
-#: admin/admin.php:3298
-#: admin/admin.php:3596
+#: admin/admin.php:2869
+#: admin/admin.php:2922
+#: admin/admin.php:3072
+#: admin/admin.php:3110
+#: admin/admin.php:3131
+#: admin/admin.php:3462
+#: admin/admin.php:3842
 msgid "Unauthorized user."
 msgstr "Unbefugter Benutzer."
 
 #. translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong
-#: admin/admin.php:2917
+#: admin/admin.php:3081
 #, php-format
 msgid "%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s"
 msgstr "%1$sBeginnen Sie damit, %2$sIhr erstes Formular zu erstellen%3$s.%4$s%5$sErleben Sie die Leistungsfähigkeit unseres intuitiven KI-Formularerstellers%6$s"
 
-#: admin/admin.php:2928
+#: admin/admin.php:3092
 msgid "SureForms is waiting for you!"
 msgstr "SureForms wartet auf Sie!"
 
-#: admin/admin.php:2930
+#: admin/admin.php:3094
 msgid "Build My First Form"
 msgstr "Erstelle mein erstes Formular"
 
-#: admin/admin.php:2931
-#: admin/admin.php:3418
+#: admin/admin.php:3095
+#: admin/admin.php:3650
 msgid "Dismiss"
 msgstr "Verwerfen"
 
@@ -11423,51 +11428,51 @@ msgstr "Anmelden"
 msgid "Register"
 msgstr "Registrieren"
 
-#: admin/admin.php:3327
+#: admin/admin.php:3491
 msgid "Recent Entries"
 msgstr "Neueste Einträge"
 
-#: admin/admin.php:3328
+#: admin/admin.php:3492
 msgid "( Last 7 days )"
 msgstr "( Letzte 7 Tage )"
 
-#: admin/admin.php:4070
+#: admin/admin.php:4678
 msgid "Use Conditional Logic to show only what matters"
 msgstr "Verwenden Sie bedingte Logik, um nur das Wichtige anzuzeigen"
 
-#: admin/admin.php:4071
+#: admin/admin.php:4679
 msgid "Split your form into steps to keep it easy"
 msgstr "Teilen Sie Ihr Formular in Schritte auf, um es einfach zu halten."
 
-#: admin/admin.php:4072
+#: admin/admin.php:4680
 msgid "Let people upload files directly to your form"
 msgstr "Lassen Sie Personen Dateien direkt in Ihr Formular hochladen"
 
-#: admin/admin.php:4073
+#: admin/admin.php:4681
 msgid "Turn responses into downloadable PDFs automatically"
 msgstr "Antworten automatisch in herunterladbare PDFs umwandeln"
 
-#: admin/admin.php:4074
+#: admin/admin.php:4682
 msgid "Let users sign with a simple signature field"
 msgstr "Lassen Sie Benutzer mit einem einfachen Unterschriftsfeld unterschreiben"
 
-#: admin/admin.php:4075
+#: admin/admin.php:4683
 msgid "Connect your form to other tools using webhooks"
 msgstr "Verbinden Sie Ihr Formular mit anderen Tools über Webhooks"
 
-#: admin/admin.php:4076
+#: admin/admin.php:4684
 msgid "Use Conversational Forms for a chat-like experience"
 msgstr "Verwenden Sie Konversationsformulare für ein chatähnliches Erlebnis"
 
-#: admin/admin.php:4077
+#: admin/admin.php:4685
 msgid "Let users register or log in through your form"
 msgstr "Lassen Sie Benutzer sich über Ihr Formular registrieren oder anmelden"
 
-#: admin/admin.php:4078
+#: admin/admin.php:4686
 msgid "Build forms that create WordPress user accounts"
 msgstr "Erstellen Sie Formulare, die WordPress-Benutzerkonten erstellen"
 
-#: admin/admin.php:4079
+#: admin/admin.php:4687
 msgid "Add calculations to auto-total scores or prices"
 msgstr "Berechnungen hinzufügen, um die Punktzahlen oder Preise automatisch zu summieren"
 
@@ -11666,6 +11671,7 @@ msgstr "Erhalten Sie mehr Kontrolle, schnellere Arbeitsabläufe und tiefere Anpa
 msgid "Selected features require %1$s - use code %2$s to get 10% off on any plan."
 msgstr "Ausgewählte Funktionen erfordern %1$s - verwenden Sie den Code %2$s, um 10 % Rabatt auf jeden Plan zu erhalten."
 
+#: admin/admin.php:4060
 #: assets/build/dashboard.js:172
 msgid "Copied"
 msgstr "Kopiert"
@@ -11889,7 +11895,7 @@ msgstr "Erkunde SureDash"
 msgid "Something went wrong. We have logged the error for further investigation"
 msgstr "Etwas ist schiefgelaufen. Wir haben den Fehler zur weiteren Untersuchung protokolliert."
 
-#: inc/field-validation.php:413
+#: inc/field-validation.php:452
 msgid "Field is not valid."
 msgstr "Feld ist nicht gültig."
 
@@ -11957,8 +11963,8 @@ msgstr "SureForms Video-Miniaturansicht"
 msgid "Expected format for emails - email@sureforms.com or John Doe "
 msgstr "Erwartetes Format für E-Mails - email@sureforms.com oder John Doe "
 
-#: admin/admin.php:1471
-#: admin/admin.php:1472
+#: admin/admin.php:1517
+#: admin/admin.php:1518
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -12173,7 +12179,7 @@ msgstr "Verbessern Sie Ihr SEO und steigen Sie mühelos in den Suchergebnissen a
 msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress."
 msgstr "Steigern Sie die Sichtbarkeit Ihrer Website mit intelligenter SEO-Automatisierung. Optimieren Sie Inhalte, verfolgen Sie die Leistung von Schlüsselwörtern und erhalten Sie umsetzbare Einblicke, alles innerhalb von WordPress."
 
-#: inc/form-submit.php:129
+#: inc/form-submit.php:174
 #: inc/form-views.php:205
 #: inc/payments/admin/admin-handler.php:99
 #: inc/payments/admin/admin-handler.php:183
@@ -15372,12 +15378,12 @@ msgid "Stripe Settings"
 msgstr "Stripe-Einstellungen"
 
 #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version.
-#: admin/admin.php:2281
+#: admin/admin.php:2335
 #, php-format
 msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version."
 msgstr "SureForms %1$s erfordert mindestens %2$s %3$s, um ordnungsgemäß zu funktionieren. Bitte aktualisieren Sie auf die neueste Version."
 
-#: admin/admin.php:2294
+#: admin/admin.php:2348
 msgid "Update Now"
 msgstr "Jetzt aktualisieren"
 
@@ -15425,8 +15431,8 @@ msgstr "Position des Währungszeichens"
 msgid "Select the position of the currency symbol relative to the amount."
 msgstr "Wählen Sie die Position des Währungssymbols relativ zum Betrag aus."
 
-#: admin/admin.php:1414
-#: admin/admin.php:1415
+#: admin/admin.php:1460
+#: admin/admin.php:1461
 #: assets/build/dashboard.js:172
 #: assets/build/entries.js:172
 #: assets/build/forms.js:172
@@ -15440,41 +15446,41 @@ msgstr "Wählen Sie die Position des Währungssymbols relativ zum Betrag aus."
 msgid "Learn"
 msgstr "Lernen"
 
-#: admin/admin.php:2635
+#: admin/admin.php:2689
 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!"
 msgstr "Erstaunlich! SureForms treibt Ihre Formulare und Einsendungen an - lassen Sie uns gemeinsam weiter wachsen!"
 
-#: admin/admin.php:2636
+#: admin/admin.php:2690
 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?"
 msgstr "Wenn SureForms hilfreich war, würden Sie sich einen Moment Zeit nehmen, um eine 5-Sterne-Bewertung auf WordPress.org zu hinterlassen?"
 
-#: admin/admin.php:2647
-#: admin/admin.php:2711
+#: admin/admin.php:2701
+#: admin/admin.php:2765
 msgid "Maybe later"
 msgstr "Vielleicht später"
 
-#: admin/admin.php:2653
+#: admin/admin.php:2707
 msgid "I already did"
 msgstr "Ich habe es bereits getan"
 
-#: admin/admin.php:2702
+#: admin/admin.php:2756
 msgid "SureForms is ready to power your forms — explore what's possible!"
 msgstr "SureForms ist bereit, Ihre Formulare zu unterstützen — entdecken Sie, was möglich ist!"
 
-#: admin/admin.php:2703
+#: admin/admin.php:2757
 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard."
 msgstr "Verwalten Sie Ihre Formulare, verfolgen Sie Einsendungen und entdecken Sie Funktionen wie den AI Form Builder, Zahlungsintegrationen und mehr im SureForms-Dashboard."
 
-#: admin/admin.php:2706
+#: admin/admin.php:2760
 msgid "Go to Dashboard"
 msgstr "Zum Dashboard gehen"
 
-#: admin/admin.php:2717
+#: admin/admin.php:2771
 msgid "I already know"
 msgstr "Ich weiß schon"
 
-#: admin/admin.php:2833
-#: admin/admin.php:3608
+#: admin/admin.php:2981
+#: admin/admin.php:3854
 msgid "Invalid parameters."
 msgstr "Ungültige Parameter."
 
@@ -15858,10 +15864,10 @@ msgid "This form is not yet available. Check back after the scheduled start time
 msgstr "Dieses Formular ist noch nicht verfügbar. Schauen Sie nach der geplanten Startzeit wieder vorbei."
 
 #: inc/admin/html-form-detector.php:268
-#: inc/form-submit.php:197
-#: inc/form-submit.php:503
-#: inc/form-submit.php:548
-#: inc/form-submit.php:1149
+#: inc/form-submit.php:247
+#: inc/form-submit.php:553
+#: inc/form-submit.php:598
+#: inc/form-submit.php:1257
 #: inc/migrator/bootstrap.php:357
 #: inc/payments/front-end.php:79
 #: inc/payments/front-end.php:265
@@ -15884,48 +15890,48 @@ msgstr "Dieses Formular ist noch nicht verfügbar. Schauen Sie nach der geplante
 msgid "Security verification failed. Please refresh the page and try again."
 msgstr "Sicherheitsüberprüfung fehlgeschlagen. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut."
 
-#: inc/form-submit.php:582
+#: inc/form-submit.php:632
 msgid "Form data was not found."
 msgstr "Formulardaten wurden nicht gefunden."
 
-#: inc/form-submit.php:336
+#: inc/form-submit.php:386
 msgid "Form ID is missing."
 msgstr "Formular-ID fehlt."
 
-#: inc/form-submit.php:213
+#: inc/form-submit.php:263
 msgid "Sorry, you do not have permission to access this resource."
 msgstr "Entschuldigung, Sie haben keine Berechtigung, auf diese Ressource zuzugreifen."
 
-#: inc/form-submit.php:368
+#: inc/form-submit.php:418
 msgid "You do not have permission to submit this form."
 msgstr "Sie haben keine Berechtigung, dieses Formular einzureichen."
 
-#: inc/form-submit.php:378
+#: inc/form-submit.php:428
 msgid "This form is no longer available."
 msgstr "Dieses Formular ist nicht mehr verfügbar."
 
-#: inc/form-submit.php:396
+#: inc/form-submit.php:446
 msgid "Please check the form for errors."
 msgstr "Bitte überprüfen Sie das Formular auf Fehler."
 
-#: inc/form-submit.php:523
-#: inc/form-submit.php:564
+#: inc/form-submit.php:573
+#: inc/form-submit.php:614
 msgid "Your submission was flagged as spam. Please try again."
 msgstr "Ihre Einsendung wurde als Spam markiert. Bitte versuchen Sie es erneut."
 
-#: inc/form-submit.php:818
+#: inc/form-submit.php:868
 msgid "Unable to submit form. Please try again."
 msgstr "Formular kann nicht übermittelt werden. Bitte versuchen Sie es erneut."
 
-#: inc/form-submit.php:1070
+#: inc/form-submit.php:1137
 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending."
 msgstr "Kein SMTP-Plugin erkannt. Bitte konfigurieren Sie ein SMTP-Plugin, um das Senden von E-Mails zu ermöglichen."
 
-#: inc/form-submit.php:1071
+#: inc/form-submit.php:1138
 msgid "Email sending failed for an unknown reason."
 msgstr "Das Senden der E-Mail ist aus einem unbekannten Grund fehlgeschlagen."
 
-#: inc/form-submit.php:1124
+#: inc/form-submit.php:1201
 msgid "No emails were sent."
 msgstr "Es wurden keine E-Mails gesendet."
 
@@ -16438,7 +16444,7 @@ msgid "Duplicate form?"
 msgstr "Formular duplizieren?"
 
 #: assets/build/formSubmit.js:2
-#: assets/js/unminified/form-submit.js:917
+#: assets/js/unminified/form-submit.js:938
 msgid "An error occurred while submitting your form. Please try again."
 msgstr "Beim Absenden Ihres Formulars ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut."
 
@@ -16815,28 +16821,28 @@ msgstr "Webhook-Geheimnis nicht konfiguriert."
 msgid "Invalid webhook signature."
 msgstr "Ungültige Webhook-Signatur."
 
-#: admin/admin.php:1253
-#: admin/admin.php:1851
+#: admin/admin.php:1299
+#: admin/admin.php:1905
 #: assets/build/formEditor.js:172
 msgid "Quizzes"
 msgstr "Quizze"
 
-#: admin/admin.php:1252
+#: admin/admin.php:1298
 msgid "Quiz Entries"
 msgstr "Quiz-Einträge"
 
-#: admin/admin.php:1255
-#: admin/admin.php:1288
-#: admin/admin.php:1321
+#: admin/admin.php:1301
+#: admin/admin.php:1334
+#: admin/admin.php:1367
 #: assets/build/settings.js:172
 msgid "New"
 msgstr "Neu"
 
-#: inc/form-submit.php:1158
+#: inc/form-submit.php:1266
 msgid "Invalid form."
 msgstr "Ungültiges Formular."
 
-#: inc/form-submit.php:1163
+#: inc/form-submit.php:1271
 msgid "Too many requests. Please try again shortly."
 msgstr "Zu viele Anfragen. Bitte versuchen Sie es in Kürze erneut."
 
@@ -17345,9 +17351,9 @@ msgstr "Perfekt für Bildung, Schulungen und unterhaltsame Quizfragen"
 msgid "Select this to create a quiz with scored questions and graded results."
 msgstr "Wählen Sie dies, um ein Quiz mit bewerteten Fragen und benoteten Ergebnissen zu erstellen."
 
-#: admin/admin.php:1285
-#: admin/admin.php:1286
-#: admin/admin.php:1856
+#: admin/admin.php:1331
+#: admin/admin.php:1332
+#: admin/admin.php:1910
 msgid "Survey Reports"
 msgstr "Umfrageberichte"
 
@@ -17753,7 +17759,7 @@ msgid "Error creating SureForms Form."
 msgstr "Fehler beim Erstellen des SureForms-Formulars."
 
 #. translators: %s represents the minimum number of characters required
-#: inc/field-validation.php:426
+#: inc/field-validation.php:465
 #: inc/translatable.php:94
 #, php-format
 msgid "Please enter at least %s characters."
@@ -17911,9 +17917,9 @@ msgstr "Die KI-Antwort war leer. Bitte verfeinern Sie Ihre Eingabeaufforderung u
 msgid "Unable to build form fields from the AI response."
 msgstr "Formularfelder können nicht aus der KI-Antwort erstellt werden."
 
-#: admin/admin.php:1318
-#: admin/admin.php:1319
-#: admin/admin.php:1861
+#: admin/admin.php:1364
+#: admin/admin.php:1365
+#: admin/admin.php:1915
 msgid "Partial Entries"
 msgstr "Teilweise Einträge"
 
@@ -18784,23 +18790,23 @@ msgstr "Der Zahlungsbetrag konnte für dieses Formular nicht verifiziert werden.
 msgid "Cancellation is not supported for this payment gateway."
 msgstr "Eine Stornierung wird für dieses Zahlungs-Gateway nicht unterstützt."
 
-#: admin/admin.php:3039
+#: admin/admin.php:3203
 msgid "SureForms AI Quick Draft"
 msgstr "SureForms AI Schnellentwurf"
 
-#: admin/admin.php:3058
+#: admin/admin.php:3222
 msgid "Describe the form and let SureForms AI generate it for you."
 msgstr "Beschreiben Sie das Formular und lassen Sie SureForms AI es für Sie erstellen."
 
-#: admin/admin.php:3061
+#: admin/admin.php:3225
 msgid "Describe your form"
 msgstr "Beschreiben Sie Ihr Formular"
 
-#: admin/admin.php:3068
+#: admin/admin.php:3232
 msgid "Example: Create a contact form with name, email, phone, and message fields."
 msgstr "Beispiel: Erstellen Sie ein Kontaktformular mit Feldern für Name, E-Mail, Telefon und Nachricht."
 
-#: admin/admin.php:3109
+#: admin/admin.php:3273
 msgid "Redirecting..."
 msgstr "Weiterleitung..."
 
@@ -18813,14 +18819,14 @@ msgid "The prompt is too long. Please shorten it and try again."
 msgstr "Die Eingabe ist zu lang. Bitte kürzen Sie sie und versuchen Sie es erneut."
 
 #. translators: %s: maximum characters allowed before the @ symbol.
-#: inc/field-validation.php:448
+#: inc/field-validation.php:487
 #: inc/translatable.php:97
 #, php-format
 msgid "The part before @ may not exceed %s characters."
 msgstr "Der Teil vor dem @ darf %s Zeichen nicht überschreiten."
 
 #. translators: %s: maximum characters allowed after the @ symbol.
-#: inc/field-validation.php:454
+#: inc/field-validation.php:493
 #: inc/translatable.php:100
 #, php-format
 msgid "The part after @ may not exceed %s characters."
@@ -18907,74 +18913,74 @@ msgstr "Entschuldigung, Sie dürfen dieses Formular nicht rendern."
 msgid "This form requires a payment. Please complete the payment and submit again."
 msgstr "Dieses Formular erfordert eine Zahlung. Bitte führen Sie die Zahlung durch und senden Sie es erneut."
 
-#: admin/admin.php:552
+#: admin/admin.php:598
 msgid "You are not allowed to update this prompt."
 msgstr "Sie dürfen diesen Hinweis nicht aktualisieren."
 
-#: admin/admin.php:595
+#: admin/admin.php:641
 msgid "Finish setting up your form"
 msgstr "Richten Sie Ihr Formular ein"
 
-#: admin/admin.php:627
+#: admin/admin.php:673
 msgid "Review or edit your form"
 msgstr "Überprüfen oder bearbeiten Sie Ihr Formular"
 
-#: admin/admin.php:628
-#: admin/admin.php:3907
+#: admin/admin.php:674
+#: admin/admin.php:4515
 msgid "Edit form"
 msgstr "Formular bearbeiten"
 
-#: admin/admin.php:633
+#: admin/admin.php:679
 msgid "Personalize the Thank You message"
 msgstr "Personalisieren Sie die Dankesnachricht"
 
-#: admin/admin.php:634
+#: admin/admin.php:680
 msgid "Edit message"
 msgstr "Nachricht bearbeiten"
 
-#: admin/admin.php:639
+#: admin/admin.php:685
 msgid "Choose who gets notified of new replies"
 msgstr "Wählen Sie aus, wer über neue Antworten benachrichtigt wird"
 
-#: admin/admin.php:640
+#: admin/admin.php:686
 msgid "Set up email"
 msgstr "E-Mail einrichten"
 
-#: admin/admin.php:648
+#: admin/admin.php:694
 msgid "your form"
 msgstr "Ihr Formular"
 
 #. translators: %s: form title.
-#: admin/admin.php:651
+#: admin/admin.php:697
 #, php-format
 msgid "Finish setting up %s"
 msgstr "Richten Sie %s ein"
 
 #. translators: %s: form title.
-#: admin/admin.php:659
+#: admin/admin.php:705
 #, php-format
 msgid "View %s (opens in a new tab)"
 msgstr "%s anzeigen (öffnet in einem neuen Tab)"
 
-#: admin/admin.php:664
+#: admin/admin.php:710
 msgid "Customize your form to get it ready for real submissions:"
 msgstr "Passen Sie Ihr Formular an, um es für echte Einreichungen vorzubereiten:"
 
-#: admin/admin.php:3896
+#: admin/admin.php:4504
 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions."
 msgstr "Wir haben dieses Formular bereits für Sie erstellt. Passen Sie es an, damit es bereit ist, echte Einreichungen zu sammeln."
 
 #. translators: %s: form name.
-#: admin/admin.php:3901
+#: admin/admin.php:4509
 #, php-format
 msgid "Finish setting up “%s”"
 msgstr "Richten Sie „%s“ ein"
 
-#: admin/admin.php:3914
+#: admin/admin.php:4522
 msgid "Edit the Thank You message"
 msgstr "Bearbeiten Sie die Dankesnachricht"
 
-#: admin/admin.php:3920
+#: admin/admin.php:4528
 msgid "Set where replies go"
 msgstr "Legen Sie fest, wohin die Antworten gehen"
 
@@ -19002,123 +19008,90 @@ msgstr "Bitte wählen Sie eine Bilddatei aus."
 msgid "This form could not be submitted. Please refresh the page and try again."
 msgstr "Dieses Formular konnte nicht übermittelt werden. Bitte aktualisieren Sie die Seite und versuchen Sie es erneut."
 
-#: admin/admin.php:2341
-#: admin/admin.php:2430
+#: admin/admin.php:2395
+#: admin/admin.php:2484
 msgid "Your SureForms database is up to date. New form entries will be saved as usual."
 msgstr "Ihre SureForms-Datenbank ist auf dem neuesten Stand. Neue Formulareinträge werden wie gewohnt gespeichert."
 
-#: admin/admin.php:2355
-#: admin/admin.php:2439
+#: admin/admin.php:2409
+#: admin/admin.php:2493
 #: inc/rest-api.php:1447
 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support."
 msgstr "SureForms konnte die Datenbankaktualisierung nicht abschließen. Ihr Hosting erlaubt möglicherweise nicht, dass SureForms Datenbanktabellen erstellt — bitte kontaktieren Sie Ihren Hosting-Anbieter oder den SureForms-Support."
 
-#: admin/admin.php:2358
+#: admin/admin.php:2412
 msgid "Contact support"
 msgstr "Support kontaktieren"
 
-#: admin/admin.php:2378
+#: admin/admin.php:2432
 msgid "Database update needed"
 msgstr "Datenbankaktualisierung erforderlich"
 
-#: admin/admin.php:2384
+#: admin/admin.php:2438
 msgid "Fix now"
 msgstr "Jetzt beheben"
 
-#: admin/admin.php:2453
+#: admin/admin.php:2507
 msgid "SureForms — database update needed"
 msgstr "SureForms — Datenbankaktualisierung erforderlich"
 
-#: admin/admin.php:2458
+#: admin/admin.php:2512
 msgid "Update database"
 msgstr "Datenbank aktualisieren"
 
-#: admin/admin.php:2478
+#: admin/admin.php:2532
 msgid "You do not have permission to update the database."
 msgstr "Sie haben keine Berechtigung, die Datenbank zu aktualisieren."
 
-#: admin/admin.php:3437
+#: admin/admin.php:3669
 msgid "You do not have permission to do this."
 msgstr "Sie haben keine Berechtigung, dies zu tun."
 
-#. translators: %d: number of consecutive failed submissions.
-#: admin/admin.php:3507
-msgid "%d form submission in a row could not be completed."
-msgid_plural "%d form submissions in a row could not be completed."
-msgstr[0] "%d aufeinanderfolgende Formulareinreichung konnte nicht abgeschlossen werden."
-msgstr[1] ""
-
-#: admin/admin.php:3515
-msgid "Visitors may be unable to reach you, and those entries were not saved."
-msgstr "Besucher können Sie möglicherweise nicht erreichen, und diese Einträge wurden nicht gespeichert."
-
-#: admin/admin.php:3525
-msgid "Form submissions are completing normally."
-msgstr "Formulareinreichungen werden normal abgeschlossen."
-
-#: admin/admin.php:3539
-msgid "No caching plugin that needs configuring was found."
-msgstr "Es wurde kein zu konfigurierendes Caching-Plugin gefunden."
-
 #. translators: %s: caching plugin name.
-#: admin/admin.php:3551
+#: admin/admin.php:4209
 msgid "%s may interfere with your forms."
 msgstr "%s kann Ihre Formulare beeinträchtigen."
 
-#: admin/admin.php:3554
-msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order."
-msgstr "Caching und JavaScript-Optimierung können eine veraltete Kopie Ihres Formulars bereitstellen oder dessen Skripte in falscher Reihenfolge laden."
-
-#: admin/admin.php:3555
+#: admin/admin.php:4185
+#: admin/admin.php:4213
 msgid "Help Me Fix"
 msgstr "Hilfe beim Beheben"
 
-#: admin/admin.php:3650
+#: admin/admin.php:4258
 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept."
 msgstr "SureForms hat Ihre Formulareinträge unter einem anderen Datenbanktabellen-Präfix gefunden. Das Wiederverbinden dauert einen Moment, und Ihre vorhandenen Einträge bleiben erhalten."
 
-#: admin/admin.php:3653
+#: admin/admin.php:4261
 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here."
 msgstr "SureForms muss Ihre Datenbank aktualisieren, bevor es neue Formulareinträge speichern kann. Dies dauert nur einen Moment und ändert weder Ihre Formulare noch vorhandene Inhalte. Vorher eingereichte Einträge können von hier aus nicht wiederhergestellt werden."
 
 #. translators: %s: site host.
-#: admin/admin.php:4265
+#: admin/admin.php:5032
 msgid "SureForms: form submissions are failing on %s"
 msgstr "SureForms: Formulareinreichungen schlagen auf %s fehl"
 
-#: admin/admin.php:4274
+#: admin/admin.php:4990
 msgid "Debug log: no entries recorded."
 msgstr "Debug-Log: Keine Einträge aufgezeichnet."
 
 #. translators: 1: entries shown, 2: entries recorded.
-#: admin/admin.php:4278
+#: admin/admin.php:4995
 msgid "Debug log (most recent %1$d of %2$d entries)"
 msgstr "Debug-Log (die neuesten %1$d von %2$d Einträgen)"
 
-#: admin/admin.php:4288
-msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General."
-msgstr "Ältere Einträge wurden ausgelassen, um diese E-Mail innerhalb der Länge zu halten, die ein E-Mail-Client akzeptiert. Das vollständige Log kann unter SureForms → Einstellungen → Allgemein heruntergeladen werden."
-
-#: admin/admin.php:4318
+#: admin/admin.php:5158
 msgid "Hello SureForms support,"
 msgstr "Hallo SureForms-Support,"
 
-#. translators: %d: number of consecutive failed submissions.
-#: admin/admin.php:4322
-msgid "SureForms has recorded %d form submission in a row that could not be completed."
-msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed."
-msgstr[0] "SureForms hat %d aufeinanderfolgende Formulareinreichung aufgezeichnet, die nicht abgeschlossen werden konnte."
-msgstr[1] ""
-
-#: admin/admin.php:4332
+#: admin/admin.php:5182
 msgid "Site details"
 msgstr "Seiteninformationen"
 
-#: admin/admin.php:4335
+#: admin/admin.php:5185
 msgid "not active"
 msgstr "nicht aktiv"
 
-#: admin/admin.php:4338
+#: admin/admin.php:5188
 msgid "none detected"
 msgstr "keine erkannt"
 
@@ -19126,7 +19099,7 @@ msgstr "keine erkannt"
 msgid "No form submission failures have been recorded."
 msgstr "Es wurden keine Formulareinreichungsfehler aufgezeichnet."
 
-#: inc/form-submit.php:118
+#: inc/form-submit.php:163
 msgid "Not found."
 msgstr "Nicht gefunden."
 
@@ -19211,6 +19184,193 @@ msgstr "Formularansichten & Konversion"
 msgid "Logs"
 msgstr "Protokolle"
 
+#: admin/admin.php:2831
+msgid "Previous notice"
+msgstr "Vorherige Mitteilung"
+
+#: admin/admin.php:2832
+msgid "Next notice"
+msgstr "Nächste Mitteilung"
+
+#. translators: 1: current position, 2: total notices.
+#: admin/admin.php:2834
+msgid "%1$d of %2$d"
+msgstr "%1$d von %2$d"
+
+#: admin/admin.php:2891
+msgid "Nothing to report."
+msgstr "Nichts zu berichten."
+
+#: admin/admin.php:4057
+#: assets/build/dashboard.js:172
+msgid "Details"
+msgstr "Details"
+
+#: admin/admin.php:4058
+msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it."
+msgstr "Was wir über dieses Problem aufgezeichnet haben. Kopieren Sie es in Ihre Supportanfrage, damit wir von der Ursache ausgehen können, anstatt von einer Beschreibung."
+
+#: admin/admin.php:4059
+#: assets/build/dashboard.js:172
+msgid "Copy details"
+msgstr "Details kopieren"
+
+#: admin/admin.php:4068
+msgid "Copy the details first, so you have them to paste."
+msgstr "Kopieren Sie zuerst die Details, damit Sie sie einfügen können."
+
+#: admin/admin.php:4072
+msgid "Copied. Contact Support is now available."
+msgstr "Kopiert. Kontakt zum Support ist jetzt verfügbar."
+
+#: admin/admin.php:4073
+msgid "Your browser would not let us copy. Select the text above and copy it by hand."
+msgstr "Ihr Browser hat uns nicht kopieren lassen. Wählen Sie den obigen Text aus und kopieren Sie ihn manuell."
+
+#: admin/admin.php:4076
+msgid "Recorded diagnostics"
+msgstr "Aufgezeichnete Diagnosen"
+
+#: admin/admin.php:4080
+msgid "Collecting the details…"
+msgstr "Details werden gesammelt…"
+
+#: admin/admin.php:4081
+msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there."
+msgstr "Wir konnten die Details nicht sammeln. Kontaktieren Sie den Support und beschreiben Sie, was passiert ist, und wir kümmern uns darum."
+
+#. translators: %s: form title.
+#: admin/admin.php:4108
+msgid "We noticed a form submission failure on %s."
+msgstr "Wir haben einen Fehler bei der Formularübermittlung auf %s festgestellt."
+
+#: admin/admin.php:4109
+msgid "We noticed a form submission failure."
+msgstr "Wir haben einen Fehler bei der Formularübermittlung festgestellt."
+
+#: admin/admin.php:4110
+msgid "Visitors may not be able to reach you, and their entries were not saved."
+msgstr "Besucher können Sie möglicherweise nicht erreichen, und ihre Eingaben wurden nicht gespeichert."
+
+#. translators: %s: form title.
+#: admin/admin.php:4115
+msgid "We noticed a notification failure on %s."
+msgstr "Wir haben einen Benachrichtigungsfehler auf %s festgestellt."
+
+#: admin/admin.php:4116
+msgid "We noticed a notification failure."
+msgstr "Wir haben einen Benachrichtigungsfehler festgestellt."
+
+#: admin/admin.php:4117
+msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing."
+msgstr "Der Eintrag wurde gespeichert, aber wir konnten die E-Mail darüber nicht senden. Neue Einträge könnten eingehen, ohne dass Sie es wissen."
+
+#. translators: %s: form title.
+#: admin/admin.php:4132
+msgid "We noticed an integration failure on %s."
+msgstr "Wir haben einen Integrationsfehler auf %s festgestellt."
+
+#: admin/admin.php:4133
+msgid "We noticed an integration failure."
+msgstr "Wir haben einen Integrationsfehler festgestellt."
+
+#: admin/admin.php:4134
+msgid "The entry was saved, but we could not send it to a connected service."
+msgstr "Der Eintrag wurde gespeichert, aber wir konnten ihn nicht an einen verbundenen Dienst senden."
+
+#: admin/admin.php:4158
+msgid "View details"
+msgstr "Details anzeigen"
+
+#: admin/admin.php:4212
+msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order."
+msgstr "Caching kann Besuchern eine alte Kopie Ihres Formulars anzeigen oder seine Skripte in der falschen Reihenfolge laden."
+
+#: admin/admin.php:5005
+msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General."
+msgstr "Ältere Einträge wurden ausgelassen, um diesen Auszug lesbar zu halten. Das vollständige Protokoll kann von SureForms → Einstellungen → Allgemein heruntergeladen werden."
+
+#. translators: %s: site host.
+#: admin/admin.php:5034
+msgid "SureForms has recorded form submissions on %s that could not be completed."
+msgstr "SureForms hat Formularübermittlungen auf %s aufgezeichnet, die nicht abgeschlossen werden konnten."
+
+#. translators: %s: site host.
+#: admin/admin.php:5038
+msgid "SureForms: notification emails are not being sent on %s"
+msgstr "SureForms: Benachrichtigungs-E-Mails werden auf %s nicht gesendet"
+
+#. translators: %s: site host.
+#: admin/admin.php:5040
+msgid "SureForms saved entries on %s but could not send the notification emails for them."
+msgstr "SureForms hat Einträge auf %s gespeichert, konnte jedoch die Benachrichtigungs-E-Mails dafür nicht senden."
+
+#. translators: %s: site host.
+#: admin/admin.php:5044
+msgid "SureForms: an integration is not receiving entries on %s"
+msgstr "SureForms: eine Integration empfängt keine Einträge auf %s"
+
+#. translators: %s: site host.
+#: admin/admin.php:5046
+msgid "SureForms saved entries on %s but could not pass them to a connected service."
+msgstr "SureForms hat Einträge auf %s gespeichert, konnte sie jedoch nicht an einen verbundenen Dienst übermitteln."
+
+#. translators: %s: site host.
+#: admin/admin.php:5056
+msgid "SureForms: a problem with the forms on %s"
+msgstr "SureForms: ein Problem mit den Formularen auf %s"
+
+#. translators: %s: site host.
+#: admin/admin.php:5058
+msgid "SureForms has recorded a problem with the forms on %s."
+msgstr "SureForms hat ein Problem mit den Formularen auf %s aufgezeichnet."
+
+#. translators: %d: number of failed submissions.
+#: admin/admin.php:5083
+msgid "SureForms has recorded %d form submission that could not be completed."
+msgid_plural "SureForms has recorded %d form submissions that could not be completed."
+msgstr[0] "SureForms hat %d Formularübermittlung aufgezeichnet, die nicht abgeschlossen werden konnte."
+msgstr[1] ""
+
+#. translators: %d: number of failed notifications.
+#: admin/admin.php:5095
+msgid "SureForms saved %d entry but could not send the notification email for it."
+msgid_plural "SureForms saved %d entries but could not send the notification emails for them."
+msgstr[0] "SureForms hat %d Eintrag gespeichert, konnte jedoch die Benachrichtigungs-E-Mail dafür nicht senden."
+msgstr[1] ""
+
+#. translators: %d: number of failed integration hand-offs.
+#: admin/admin.php:5107
+msgid "SureForms saved %d entry but could not pass it to a connected service."
+msgid_plural "SureForms saved %d entries but could not pass them to a connected service."
+msgstr[0] "SureForms hat %d Eintrag gespeichert, konnte ihn jedoch nicht an einen verbundenen Dienst übermitteln."
+msgstr[1] ""
+
+#. translators: %d: number of recorded problems.
+#: admin/admin.php:5119
+msgid "SureForms has recorded %d problem with the forms on this site."
+msgid_plural "SureForms has recorded %d problems with the forms on this site."
+msgstr[0] "SureForms hat %d Problem mit den Formularen auf dieser Seite aufgezeichnet."
+msgstr[1] ""
+
+#. translators: %s: form title.
+#: admin/admin.php:5169
+msgid "Form: %s"
+msgstr "Formular: %s"
+
+#: admin/admin.php:5189
+msgid "none recorded"
+msgstr "keine aufgezeichnet"
+
+#. translators: %s: date and time of the previous report, in the site's timezone.
+#: admin/admin.php:5212
+msgid "Previously reported: %s"
+msgstr "Zuvor gemeldet: %s"
+
+#: assets/build/settings.js:172
+msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures."
+msgstr "Ansichten und Übermittlungen von Personen, die die Seite bearbeiten können, werden normalerweise ausgelassen, damit das Testen Ihrer eigenen Formulare diese Zahlen nicht verändert."
+
 #: inc/post-types.php:265
 msgctxt "post type general name"
 msgid "Forms"
diff --git a/languages/sureforms-es_ES-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-es_ES-51635fe6489fc8288d603fe596c755ca.json
index 6b8761b34..fce740b2b 100644
--- a/languages/sureforms-es_ES-51635fe6489fc8288d603fe596c755ca.json
+++ b/languages/sureforms-es_ES-51635fe6489fc8288d603fe596c755ca.json
@@ -1 +1 @@
-{"translation-revision-date":"2024-12-13T16:34:28+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tablero"],"Settings":["Configuraciones"],"Entries":["Entradas"],"Status":["Estado"],"Form":["Formulario"],"Activated":["Activado"],"Activate":["Activar"],"Address":["Direcci\u00f3n"],"Checkbox":["Casilla de verificaci\u00f3n"],"Dropdown":["Desplegable"],"Email":["Correo electr\u00f3nico"],"Number":["N\u00famero"],"Phone":["Tel\u00e9fono"],"Textarea":["\u00c1rea de texto"],"Monday":["Lunes"],"Forms":["Formularios"],"New Form Submission - %s":["Nueva presentaci\u00f3n de formulario - %s"],"GitHub":["GitHub"],"(no title)":["(sin t\u00edtulo)"],"General":["General"],"No tags available":["No hay etiquetas disponibles"],"Back":["Atr\u00e1s"],"Generic tags":["Etiquetas gen\u00e9ricas"],"Other":["Otro"],"Confirm":["Confirmar"],"Cancel":["Cancelar"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["La instalaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"Plugin activation failed, Please try again later.":["La activaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"Integrations":["Integraciones"],"What's New?":["\u00bfQu\u00e9 hay de nuevo?"],"Core":["N\u00facleo"],"Unlicensed":["Sin licencia"],"Connecting\u2026":["Conectando\u2026"],"Install & Activate":["Instalar y activar"],"Compliance Settings":["Configuraci\u00f3n de Cumplimiento"],"Enable GDPR Compliance":["Habilitar el cumplimiento del RGPD"],"Never store entry data after form submission":["Nunca almacenes los datos de entrada despu\u00e9s de enviar el formulario"],"When enabled this form will never store Entries.":["Cuando est\u00e9 habilitado, este formulario nunca almacenar\u00e1 entradas."],"Automatically delete entries":["Eliminar autom\u00e1ticamente las entradas"],"When enabled this form will automatically delete entries after a certain period of time.":["Cuando est\u00e9 habilitado, este formulario eliminar\u00e1 autom\u00e1ticamente las entradas despu\u00e9s de un cierto per\u00edodo de tiempo."],"Entries older than the days set will be deleted automatically.":["Las entradas m\u00e1s antiguas que los d\u00edas establecidos se eliminar\u00e1n autom\u00e1ticamente."],"Visual":["Visual"],"HTML":["HTML"],"All Data":["Todos los datos"],"Add Shortcode":["Agregar c\u00f3digo corto"],"Form input tags":["Etiquetas de entrada de formulario"],"Comma separated values are also accepted.":["Tambi\u00e9n se aceptan valores separados por comas."],"Email Notification":["Notificaci\u00f3n de correo electr\u00f3nico"],"Name":["Nombre"],"Send Email To":["Enviar correo electr\u00f3nico a"],"Subject":["Asunto"],"CC":["CC"],"BCC":["BCC"],"Reply To":["Responder a"],"Add Key":["Agregar clave"],"Add Value":["A\u00f1adir valor"],"Add":["A\u00f1adir"],"Confirmation Message":["Mensaje de confirmaci\u00f3n"],"After Form Submission":["Despu\u00e9s del env\u00edo del formulario"],"Hide Form":["Ocultar formulario"],"Reset Form":["Restablecer formulario"],"Custom URL":["URL personalizada"],"Add Query Parameters":["Agregar par\u00e1metros de consulta"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Seleccione si desea agregar pares clave-valor para los campos del formulario que se incluir\u00e1n en los par\u00e1metros de consulta"],"Query Parameters":["Par\u00e1metros de consulta"],"Success Message":["Mensaje de \u00e9xito"],"Redirect":["Redirigir"],"Redirect to":["Redirigir a"],"Page":["P\u00e1gina"],"Form Confirmation":["Confirmaci\u00f3n de formulario"],"Confirmation Type":["Tipo de confirmaci\u00f3n"],"Clear":["Claro"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invisible"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validaciones"],"Spam Protection":["Protecci\u00f3n contra el spam"],"Email Summaries":["Res\u00famenes de correo electr\u00f3nico"],"Tuesday":["Martes"],"Wednesday":["Mi\u00e9rcoles"],"Thursday":["Jueves"],"Friday":["Viernes"],"Saturday":["S\u00e1bado"],"Sunday":["Domingo"],"Test Email":["Correo de prueba"],"Schedule Reports":["Programar informes"],"IP Logging":["Registro de IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Si esta opci\u00f3n est\u00e1 activada, la direcci\u00f3n IP del usuario se guardar\u00e1 con los datos del formulario"],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s representa las selecciones m\u00ednimas necesarias. Por ejemplo: \u201cSe requieren un m\u00ednimo de 2 selecciones.\u201d"],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s representa el m\u00e1ximo de selecciones permitidas. Por ejemplo: \u201cSe permiten un m\u00e1ximo de 4 selecciones.\u201d"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s representa las opciones m\u00ednimas necesarias. Por ejemplo: \"Se requiere un m\u00ednimo de 1 selecci\u00f3n.\""],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s representa las opciones m\u00e1ximas permitidas. Por ejemplo: \"Se permiten un m\u00e1ximo de 3 selecciones.\""]," Error Message":["Mensaje de error"],"Auto":["Auto"],"Light":["Luz"],"Dark":["Oscuro"],"Turnstile":["Torniquete"],"Honeypot":["Honeypot"],"Get Keys":["Obtener llaves"],"Documentation":["Documentaci\u00f3n"],"Site Key":["Clave del sitio"],"Secret Key":["Clave secreta"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Modo de apariencia"],"Enable Honeypot Security":["Habilitar la seguridad Honeypot"],"Enable Honeypot Security for better spam protection":["Habilitar la seguridad Honeypot para una mejor protecci\u00f3n contra el spam"],"Text":["Texto"],"Confirmation Email Mismatch Message":["Mensaje de discrepancia de correo electr\u00f3nico de confirmaci\u00f3n"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s representa el valor m\u00ednimo de entrada. Por ejemplo: \"El valor m\u00ednimo es 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s representa el valor m\u00e1ximo de entrada. Por ejemplo: \"El valor m\u00e1ximo es 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Con\u00e9ctate con OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual no coincide con el nombre de dominio de su sitio web (%1$s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam. Alternativamente, intente usar una direcci\u00f3n de remitente que coincida con el dominio de su sitio web (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual no coincide con el nombre de dominio de su sitio web (%s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam."],"We strongly recommend that you install the free ":["Recomendamos encarecidamente que instale el gratuito"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["\u00a1Complemento! El asistente de configuraci\u00f3n facilita la correcci\u00f3n de tus correos electr\u00f3nicos."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativamente, intenta usar una direcci\u00f3n de remitente que coincida con el dominio de tu sitio web (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Por favor, introduce una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida. Tus notificaciones no se enviar\u00e1n si el campo no est\u00e1 rellenado correctamente."],"From Name":["De Nombre"],"From Email":["De correo electr\u00f3nico"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual puede no coincidir con el nombre de dominio de su sitio web (%1$s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam. Alternativamente, intente usar una Direcci\u00f3n del Remitente que coincida con el dominio de su sitio web (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual puede no coincidir con el nombre de dominio de su sitio web (%s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["\u00bfListo para ir m\u00e1s all\u00e1 del plan gratuito?"],"Upgrade now":["Actualiza ahora"],"and unlock the full power of SureForms!":["y desbloquea todo el poder de SureForms!"],"Upgrade SureForms":["Actualizar SureForms"],"Upgrade Now":["Actualiza ahora"],"Entries older than the selected days will be deleted.":["Las entradas anteriores a los d\u00edas seleccionados ser\u00e1n eliminadas."],"Entries Time Period":["Per\u00edodo de tiempo de las entradas"],"Notifications can use only one From Email so please enter a single address.":["Las notificaciones solo pueden usar un \u00fanico correo electr\u00f3nico de origen, as\u00ed que por favor ingrese una sola direcci\u00f3n."],"Select Page to redirect":["Seleccionar p\u00e1gina para redirigir"],"Search for a page":["Buscar una p\u00e1gina"],"Select a page":["Selecciona una p\u00e1gina"],"Form Validation":["Validaci\u00f3n de formulario"],"Required Error Messages":["Mensajes de error requeridos"],"Other Error Messages":["Otros mensajes de error"],"Input Field Unique":["Campo de entrada \u00fanico"],"Email Field Unique":["Campo de correo electr\u00f3nico \u00fanico"],"Invalid URL":["URL no v\u00e1lida"],"Phone Field Unique":["Campo de Tel\u00e9fono \u00danico"],"Invalid Field Number Block":["Bloque de N\u00famero de Campo Inv\u00e1lido"],"Invalid Email":["Correo electr\u00f3nico no v\u00e1lido"],"Number Minimum Value":["Valor M\u00ednimo del N\u00famero"],"Number Maximum Value":["Valor M\u00e1ximo del N\u00famero"],"Dropdown Minimum Selections":["M\u00ednimo de selecciones en el men\u00fa desplegable"],"Dropdown Maximum Selections":["Selecciones M\u00e1ximas del Desplegable"],"Multiple Choice Minimum Selections":["M\u00faltiples opciones de selecci\u00f3n m\u00ednima"],"Multiple Choice Maximum Selections":["Selecciones M\u00faltiples de Opci\u00f3n M\u00faltiple"],"Input Field":["Campo de entrada"],"Email Field":["Campo de correo electr\u00f3nico"],"URL Field":["Campo de URL"],"Phone Field":["Campo de Tel\u00e9fono"],"Textarea Field":["Campo de \u00e1rea de texto"],"Checkbox Field":["Campo de casilla de verificaci\u00f3n"],"Dropdown Field":["Campo desplegable"],"Multiple Choice Field":["Campo de opci\u00f3n m\u00faltiple"],"Address Field":["Campo de direcci\u00f3n"],"Number Field":["Campo num\u00e9rico"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Para habilitar la funci\u00f3n reCAPTCHA en sus SureForms, por favor habilite la opci\u00f3n reCAPTCHA en la configuraci\u00f3n de sus bloques y seleccione la versi\u00f3n. Agregue aqu\u00ed la clave secreta y la clave del sitio de Google reCAPTCHA. reCAPTCHA se a\u00f1adir\u00e1 a su p\u00e1gina en el front-end."],"Enter your %s here":["Introduce tu %s aqu\u00ed"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Para habilitar hCAPTCHA, por favor a\u00f1ade tu clave del sitio y clave secreta. Configura estos ajustes dentro del formulario individual."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Para habilitar Cloudflare Turnstile, por favor a\u00f1ade tu clave de sitio y clave secreta. Configura estos ajustes dentro del formulario individual."],"Save":["Guardar"],"Anonymous Analytics":["Anal\u00edtica An\u00f3nima"],"Learn More":["Aprende m\u00e1s"],"Admin Notification":["Notificaci\u00f3n de administrador"],"Enable Admin Notification":["Habilitar notificaci\u00f3n de administrador"],"Admin notifications keep you informed about new form entries since your last visit.":["Las notificaciones de administrador te mantienen informado sobre nuevas entradas de formularios desde tu \u00faltima visita."],"Skip":["Omitir"],"Continue":["Continuar"],"Maximum Number of Entries":["N\u00famero m\u00e1ximo de entradas"],"Maximum Entries":["Entradas M\u00e1ximas"],"Response Description After Maximum Entries":["Descripci\u00f3n de la respuesta despu\u00e9s del m\u00e1ximo de entradas"],"Get Started":["Comenzar"],"Integration":["Integraci\u00f3n"],"Connect Native Integrations with SureForms":["Conecta integraciones nativas con SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Desbloquea potentes integraciones en el plan Premium para automatizar tus flujos de trabajo y conectar SureForms directamente con tus herramientas favoritas."],"Send form submissions straight to CRMs, email, and marketing platforms":["Env\u00eda las presentaciones de formularios directamente a los CRM, correo electr\u00f3nico y plataformas de marketing"],"Automate repetitive tasks with seamless data syncing":["Automatiza tareas repetitivas con una sincronizaci\u00f3n de datos sin problemas"],"Access exclusive native integrations for faster workflows":["Accede a integraciones nativas exclusivas para flujos de trabajo m\u00e1s r\u00e1pidos"],"Expected format for emails - email@sureforms.com or John Doe ":["Formato esperado para correos electr\u00f3nicos: email@sureforms.com o John Doe "],"Payments":["Pagos"],"Stripe account disconnected successfully.":["Cuenta de Stripe desconectada con \u00e9xito."],"Failed to create webhook.":["Error al crear el webhook."],"Failed to connect to Stripe.":["Error al conectar con Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Base de Conocimientos"],"What\u2019s New":["Qu\u00e9 hay de nuevo"],"out of":["fuera de"],"No entries found":["No se encontraron entradas"],"delete":["eliminar"],"Please type \"%s\" in the input box":["Por favor, escribe \"%s\" en el cuadro de entrada"],"To confirm, type \"%s\" in the box below:":["Para confirmar, escriba \"%s\" en el cuadro de abajo:"],"Type \"%s\"":["Escribe \"%s\""],"Go to OttoKit Settings":["Ve a la configuraci\u00f3n de OttoKit"],"USD - US Dollar":["USD - D\u00f3lar estadounidense"],"Payment Mode":["Modo de pago"],"Test Mode":["Modo de prueba"],"Live Mode":["Modo en vivo"],"Action":["Acci\u00f3n"],"General Settings":["Configuraci\u00f3n general"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Configure res\u00famenes de correo electr\u00f3nico, alertas de administrador y preferencias de datos para gestionar tus formularios con facilidad."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personaliza los mensajes de error predeterminados que se muestran cuando los usuarios env\u00edan entradas de formulario inv\u00e1lidas o incompletas."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Habilita la protecci\u00f3n contra spam para tus formularios utilizando servicios CAPTCHA o seguridad honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Conecta y gestiona tus pasarelas de pago para aceptar transacciones de forma segura a trav\u00e9s de tus formularios."],"1% transaction and payment gateway fees apply.":["Se aplican tarifas del 1% por transacciones y pasarelas de pago."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Se aplican tarifas de transacci\u00f3n y de pasarela de pago del 2.9%. Activa la licencia para reducir las tarifas de transacci\u00f3n."],"2.9% transaction and payment gateway fees apply.":["Se aplican tarifas del 2.9% por transacciones y pasarelas de pago."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Por favor, visita %1$s, elimina un webhook no utilizado, luego haz clic abajo para reintentar."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms no pudo crear un webhook porque tu cuenta de Stripe se ha quedado sin espacios libres. Los webhooks son necesarios para recibir actualizaciones sobre los pagos."],"Stripe Dashboard":["Panel de control de Stripe"],"Creating\u2026":["Creando\u2026"],"Create Webhook":["Crear Webhook"],"Successfully connected to Stripe!":["\u00a1Conectado exitosamente a Stripe!"],"Invalid response from server. Please try again.":["Respuesta inv\u00e1lida del servidor. Por favor, int\u00e9ntelo de nuevo."],"Failed to disconnect Stripe account.":["Error al desconectar la cuenta de Stripe."],"Webhook created successfully!":["\u00a1Webhook creado con \u00e9xito!"],"Select Currency":["Seleccionar moneda"],"Select the default currency for payment forms.":["Seleccione la moneda predeterminada para los formularios de pago."],"Connection Status":["Estado de la conexi\u00f3n"],"Disconnect Stripe Account":["Desconectar cuenta de Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["\u00bfEst\u00e1 seguro de que desea desconectar su cuenta de Stripe? Esto detendr\u00e1 todos los pagos activos, suscripciones y transacciones de formularios conectados a esta cuenta."],"Disconnect":["Desconectar"],"Disconnecting\u2026":["Desconectando\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook conectado con \u00e9xito, todos los eventos de Stripe est\u00e1n siendo rastreados."],"Connect your Stripe account to start accepting payments through your forms.":["Conecta tu cuenta de Stripe para comenzar a aceptar pagos a trav\u00e9s de tus formularios."],"Connect to Stripe":["Conectar a Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Con\u00e9ctate de forma segura a Stripe con solo unos clics para comenzar a aceptar pagos."],"Set the total number of submissions allowed for this form.":["Establezca el n\u00famero total de env\u00edos permitidos para este formulario."],"Payment Methods":["M\u00e9todos de pago"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["El modo de prueba te permite procesar pagos sin cargos reales. Cambia al modo en vivo para transacciones reales."],"General Payment Settings":["Configuraci\u00f3n General de Pagos"],"These settings apply to all payment gateways.":["Estos ajustes se aplican a todas las pasarelas de pago."],"Stripe Settings":["Configuraci\u00f3n de Stripe"],"Left ($100)":["Izquierda ($100)"],"Right (100$)":["Correcto (100$)"],"Left Space ($ 100)":["Espacio Izquierdo ($ 100)"],"Right Space (100 $)":["Espacio Derecho (100 $)"],"Currency Sign Position":["Posici\u00f3n del signo de moneda"],"Select the position of the currency symbol relative to the amount.":["Seleccione la posici\u00f3n del s\u00edmbolo de moneda en relaci\u00f3n con la cantidad."],"Learn":["Aprender"],"Enable email summaries":["Habilitar res\u00famenes de correo electr\u00f3nico"],"Enable IP logging":["Habilitar el registro de IP"],"Turn on Admin Notification from here.":["Activa la notificaci\u00f3n de administrador desde aqu\u00ed."],"New":["Nuevo"],"Send entries to 100+ popular apps.":["Env\u00eda entradas a m\u00e1s de 100 aplicaciones populares."],"Build automated workflows that run instantly.":["Crea flujos de trabajo automatizados que se ejecutan al instante."],"Create custom app integrations using our Custom App feature.":["Crea integraciones de aplicaciones personalizadas utilizando nuestra funci\u00f3n de Aplicaci\u00f3n Personalizada."],"Keep your tools in sync automatically.":["Mant\u00e9n tus herramientas sincronizadas autom\u00e1ticamente."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Esto instalar\u00e1 y activar\u00e1 OttoKit en su sitio de WordPress para habilitar funciones de automatizaci\u00f3n."],"Automate Your Forms with OttoKit":["Automatiza tus formularios con OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Cada env\u00edo de formulario deber\u00eda activar algo: una alerta de Slack, un cliente potencial en el CRM, un correo electr\u00f3nico de seguimiento o una nueva fila en Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configura los permisos del cliente de IA y la configuraci\u00f3n del servidor MCP."],"View documentation":["Ver documentaci\u00f3n"],"Copy to clipboard":["Copiar al portapapeles"],"Claude Desktop":["Escritorio Claude"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) o %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["C\u00f3digo Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (proyecto) o ~\/.claude.json (global)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (proyecto) o settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml o config.json"],"Your client's MCP configuration file":["El archivo de configuraci\u00f3n MCP de su cliente"],"Connect Your AI Client":["Conecta tu cliente de IA"],"AI Client":["Cliente de IA"],"Create an Application Password \u2014 ":["Crear una Contrase\u00f1a de Aplicaci\u00f3n \u2014"],"Open Application Passwords":["Abrir contrase\u00f1as de aplicaciones"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["O utiliza este comando CLI para a\u00f1adir el servidor r\u00e1pidamente (a\u00fan necesitar\u00e1s configurar las variables de entorno):"],"Copy the JSON config below into: ":["Copie la configuraci\u00f3n JSON a continuaci\u00f3n en:"],"Replace \"your-application-password\" with the password from Step 1.":["Reemplace \"your-application-password\" con la contrase\u00f1a del Paso 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 el punto final MCP de tu sitio. WP_API_USERNAME \u2014 tu nombre de usuario de WordPress. WP_API_PASSWORD \u2014 la contrase\u00f1a de la aplicaci\u00f3n que generaste."],"View setup docs":["Ver documentos de configuraci\u00f3n"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["El complemento MCP Adapter est\u00e1 instalado pero no activo. Act\u00edvalo para configurar los ajustes de MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["El complemento MCP Adapter es necesario para conectar clientes de IA a tus formularios. Desc\u00e1rgalo e inst\u00e1lalo desde GitHub, luego act\u00edvalo."],"Download the latest release from":["Descarga la \u00faltima versi\u00f3n desde"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Instala el complemento a trav\u00e9s de Complementos > A\u00f1adir nuevo complemento > Subir complemento."],"Activate the MCP Adapter plugin.":["Activa el complemento del adaptador MCP."],"Activating\u2026":["Activando\u2026"],"Activate MCP Adapter":["Activar adaptador MCP"],"Download MCP Adapter":["Descargar el adaptador MCP"],"Experimental":["Experimental"],"Enable Abilities":["Habilitar habilidades"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registra las habilidades de SureForms con la API de Habilidades de WordPress. Cuando est\u00e1 habilitado, los clientes de IA pueden listar, leer, crear, editar y eliminar tus formularios y entradas. Cuando est\u00e1 deshabilitado, no se registran habilidades y los clientes de IA no pueden realizar ninguna acci\u00f3n en tus formularios."],"Abilities API \u2014 Edit":["API de habilidades \u2014 Editar"],"Enable Edit Abilities":["Habilitar habilidades de edici\u00f3n"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Cuando est\u00e1 habilitado, los clientes de IA pueden crear nuevos formularios, actualizar t\u00edtulos de formularios, campos y configuraciones, duplicar formularios y modificar los estados de las entradas. Cuando est\u00e1 deshabilitado, estas habilidades se desregistran y los clientes de IA solo pueden leer sus datos."],"Abilities API \u2014 Delete":["API de habilidades \u2014 Eliminar"],"Enable Delete Abilities":["Habilitar habilidades de eliminaci\u00f3n"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Cuando est\u00e1 habilitado, los clientes de IA pueden eliminar permanentemente formularios y entradas. Los datos eliminados no se pueden recuperar. Cuando est\u00e1 deshabilitado, las capacidades de eliminaci\u00f3n se desregistran y los clientes de IA no pueden eliminar ning\u00fan dato."],"MCP Server":["Servidor MCP"],"Enable MCP Server":["Habilitar servidor MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Crea un punto final dedicado de SureForms MCP al que los clientes de IA como Claude pueden conectarse. Cuando est\u00e1 deshabilitado, el punto final se elimina y los clientes de IA externos no pueden descubrir ni llamar a ninguna capacidad de SureForms."],"Learn more":["Aprende m\u00e1s"],"MCP Adapter Required":["Adaptador MCP Requerido"],"Heading 1":["Encabezado 1"],"Heading 2":["Encabezado 2"],"Heading 3":["Encabezado 3"],"Heading 4":["Encabezado 4"],"Heading 5":["Encabezado 5"],"Heading 6":["Encabezado 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configura la clave de la API de Google Maps para la autocompletaci\u00f3n de direcciones y la vista previa del mapa."],"Help shape the future of SureForms":["Ayuda a dar forma al futuro de SureForms"],"Enable Google Address Autocomplete":["Habilitar la autocompletar de direcciones de Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Actualiza al Plan de Negocios de SureForms para agregar la autocompletaci\u00f3n de direcciones impulsada por Google con vista previa de mapa interactivo a tus formularios."],"Auto-suggest addresses as users type for faster, error-free submissions":["Sugerir direcciones autom\u00e1ticamente mientras los usuarios escriben para env\u00edos m\u00e1s r\u00e1pidos y sin errores"],"Show an interactive map preview with draggable pin for precise locations":["Muestra una vista previa de mapa interactivo con un marcador arrastrable para ubicaciones precisas"],"Automatically populate address fields like city, state, and postal code":["Rellenar autom\u00e1ticamente campos de direcci\u00f3n como ciudad, estado y c\u00f3digo postal"],"This form is now closed as we've received all the entries.":["Este formulario est\u00e1 ahora cerrado ya que hemos recibido todas las inscripciones."],"Thank you for contacting us! We will be in touch with you shortly.":["\u00a1Gracias por contactarnos! Nos pondremos en contacto contigo en breve."],"Saving\u2026":["Guardando\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Cuando est\u00e9 habilitado, este formulario no almacenar\u00e1 la IP del usuario, el nombre del navegador ni el nombre del dispositivo en las entradas."],"Form data":["Datos del formulario"],"Unsaved changes":["Cambios no guardados"],"Keep editing":["Sigue editando"],"Global Defaults":["Valores predeterminados globales"],"Form Restrictions":["Restricciones del formulario"],"Configure default settings that apply to newly created forms.":["Configurar los ajustes predeterminados que se aplican a los formularios reci\u00e9n creados."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Recopila informaci\u00f3n no sensible de tu sitio web, como la versi\u00f3n de PHP y las caracter\u00edsticas utilizadas, para ayudarnos a corregir errores m\u00e1s r\u00e1pido, tomar decisiones m\u00e1s inteligentes y desarrollar funciones que realmente te importen."],"Failed to load pages. Please refresh and try again.":["Error al cargar las p\u00e1ginas. Por favor, actualice e intente de nuevo."],"Failed to load settings. Please refresh and try again.":["Error al cargar la configuraci\u00f3n. Por favor, actualice e intente de nuevo."],"Form Validation fields cannot be left blank.":["Los campos de validaci\u00f3n de formularios no pueden dejarse en blanco."],"Recipient email is required when email summaries are enabled.":["Se requiere el correo electr\u00f3nico del destinatario cuando los res\u00famenes de correo electr\u00f3nico est\u00e1n habilitados."],"Please enter a valid recipient email.":["Por favor, introduce un correo electr\u00f3nico de destinatario v\u00e1lido."],"Settings saved.":["Configuraciones guardadas."],"Failed to save settings.":["Error al guardar la configuraci\u00f3n."],"Some settings failed to save. Please retry.":["Algunas configuraciones no se pudieron guardar. Por favor, int\u00e9ntalo de nuevo."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Algunos campos tienen cambios no guardados. Des\u00e9chelos para continuar, o permanezca para guardar sus ediciones."],"Discard & switch":["Descartar y cambiar"],"Import":["Importar"],"Migration":["Migraci\u00f3n"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importa formularios de Contact Form 7, WPForms, Gravity Forms y otros plugins en SureForms."],"Could not generate the import preview.":["No se pudo generar la vista previa de importaci\u00f3n."],"Import failed. Please try again or check your error logs.":["La importaci\u00f3n fall\u00f3. Por favor, int\u00e9ntelo de nuevo o revise sus registros de errores."],"Go back":["Regresar"],"Update & import":["Actualizar e importar"],"Confirm & import":["Confirmar e importar"],"Form #%s":["Formulario n.\u00ba %s"],"%d field will import":["Se importar\u00e1 %d campo"],"Some fields can't be migrated yet":["Algunos campos no se pueden migrar todav\u00eda"],"These fields will be skipped - add them manually after the form is created:":["Estos campos se omitir\u00e1n; agr\u00e9guelos manualmente despu\u00e9s de crear el formulario:"],"Some forms could not be parsed":["Algunos formularios no se pudieron analizar"],"Update existing":["Actualizar existente"],"Create a new copy":["Crear una nueva copia"],"Could not load forms for this source.":["No se pudieron cargar los formularios para esta fuente."],"(untitled form)":["(formulario sin t\u00edtulo)"],"Previously imported":["Importado previamente"],"Open existing SureForms form in a new tab":["Abrir el formulario SureForms existente en una nueva pesta\u00f1a"],"On re-import":["En la reimportaci\u00f3n"],"No forms found in this plugin.":["No se encontraron formularios en este complemento."],"%1$d of %2$d form selected":["%1$d de %2$d formulario seleccionado"],"Preview update":["Vista previa de la actualizaci\u00f3n"],"Preview import":["Vista previa de la importaci\u00f3n"],"Migration complete":["Migraci\u00f3n completa"],"Nothing was imported":["No se import\u00f3 nada"],"%d form was imported into SureForms.":["%d formulario fue importado a SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Ninguno de los formularios seleccionados pudo ser migrado. Consulte las advertencias a continuaci\u00f3n para obtener m\u00e1s detalles."],"Imported forms":["Formularios importados"],"Edit in SureForms":["Editar en SureForms"],"Some fields were skipped during import":["Algunos campos se omitieron durante la importaci\u00f3n"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Agrega estos manualmente en el editor de formularios - a\u00fan no tienen un equivalente en SureForms:"],"Some forms failed to import":["Algunos formularios no se pudieron importar"],"Import more forms":["Importar m\u00e1s formularios"],"Could not load the list of importable plugins.":["No se pudo cargar la lista de complementos importables."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["No hay complementos de formularios compatibles activos en este sitio. Activa uno (como Contact Form 7) con al menos un formulario para importarlo en SureForms."],"Plugin":["Complemento"],"%d form":["%d formulario"],"No forms":["No hay formularios"],"Multiple choice":["Opci\u00f3n m\u00faltiple"],"Consent":["Consentimiento"],"The image uploaded but could not be inserted. Please try again.":["La imagen se carg\u00f3 pero no pudo ser insertada. Por favor, int\u00e9ntalo de nuevo."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["La imagen no pudo ser cargada. Puede ser demasiado grande o es posible que no tengas permiso para subir archivos."],"Please choose an image file.":["Por favor, elige un archivo de imagen."],"Show views and conversion rate":["Mostrar vistas y tasa de conversi\u00f3n"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Agrega las columnas de Vistas y Tasa de Conversi\u00f3n a la lista de Formularios. Una vista se cuenta una vez por visita a la p\u00e1gina cuando el formulario aparece en pantalla, y la tasa de conversi\u00f3n es la proporci\u00f3n de esas vistas que terminaron en un env\u00edo. El conteo comienza la primera vez que lo enciende, por lo que los env\u00edos recibidos antes de entonces no se cuentan para la tasa. Apagarlo despu\u00e9s solo oculta las columnas \u2014 el conteo contin\u00faa, por lo que las cifras est\u00e1n actualizadas si lo vuelve a encender."],"Log cleared.":["Registro borrado."],"Could not clear the log.":["No se pudo borrar el registro."],"Enable logs":["Habilitar registros"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registra los fallos de env\u00edo de formularios reportados por el navegador, para que puedas enviar el registro al soporte en lugar de leer la consola. No se escribe nada mientras tus formularios funcionan, y los valores enviados nunca se almacenan."],"Download log":["Descargar registro"],"Confirm delete":["Confirmar eliminaci\u00f3n"],"Log is full \u2014 download and clear it to keep recording.":["El registro est\u00e1 lleno \u2014 desc\u00e1rgalo y b\u00f3rralo para seguir grabando."],"Form Views & Conversion":["Vistas y Conversi\u00f3n de Formularios"],"Logs":["Registros"],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}}
\ No newline at end of file
+{"translation-revision-date":"2024-12-13T16:34:28+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tablero"],"Settings":["Configuraciones"],"Entries":["Entradas"],"Status":["Estado"],"Form":["Formulario"],"Activated":["Activado"],"Activate":["Activar"],"Address":["Direcci\u00f3n"],"Checkbox":["Casilla de verificaci\u00f3n"],"Dropdown":["Desplegable"],"Email":["Correo electr\u00f3nico"],"Number":["N\u00famero"],"Phone":["Tel\u00e9fono"],"Textarea":["\u00c1rea de texto"],"Monday":["Lunes"],"Forms":["Formularios"],"New Form Submission - %s":["Nueva presentaci\u00f3n de formulario - %s"],"GitHub":["GitHub"],"(no title)":["(sin t\u00edtulo)"],"General":["General"],"No tags available":["No hay etiquetas disponibles"],"Back":["Atr\u00e1s"],"Generic tags":["Etiquetas gen\u00e9ricas"],"Other":["Otro"],"Confirm":["Confirmar"],"Cancel":["Cancelar"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["La instalaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"Plugin activation failed, Please try again later.":["La activaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"Integrations":["Integraciones"],"What's New?":["\u00bfQu\u00e9 hay de nuevo?"],"Core":["N\u00facleo"],"Unlicensed":["Sin licencia"],"Connecting\u2026":["Conectando\u2026"],"Install & Activate":["Instalar y activar"],"Compliance Settings":["Configuraci\u00f3n de Cumplimiento"],"Enable GDPR Compliance":["Habilitar el cumplimiento del RGPD"],"Never store entry data after form submission":["Nunca almacenes los datos de entrada despu\u00e9s de enviar el formulario"],"When enabled this form will never store Entries.":["Cuando est\u00e9 habilitado, este formulario nunca almacenar\u00e1 entradas."],"Automatically delete entries":["Eliminar autom\u00e1ticamente las entradas"],"When enabled this form will automatically delete entries after a certain period of time.":["Cuando est\u00e9 habilitado, este formulario eliminar\u00e1 autom\u00e1ticamente las entradas despu\u00e9s de un cierto per\u00edodo de tiempo."],"Entries older than the days set will be deleted automatically.":["Las entradas m\u00e1s antiguas que los d\u00edas establecidos se eliminar\u00e1n autom\u00e1ticamente."],"Visual":["Visual"],"HTML":["HTML"],"All Data":["Todos los datos"],"Add Shortcode":["Agregar c\u00f3digo corto"],"Form input tags":["Etiquetas de entrada de formulario"],"Comma separated values are also accepted.":["Tambi\u00e9n se aceptan valores separados por comas."],"Email Notification":["Notificaci\u00f3n de correo electr\u00f3nico"],"Name":["Nombre"],"Send Email To":["Enviar correo electr\u00f3nico a"],"Subject":["Asunto"],"CC":["CC"],"BCC":["BCC"],"Reply To":["Responder a"],"Add Key":["Agregar clave"],"Add Value":["A\u00f1adir valor"],"Add":["A\u00f1adir"],"Confirmation Message":["Mensaje de confirmaci\u00f3n"],"After Form Submission":["Despu\u00e9s del env\u00edo del formulario"],"Hide Form":["Ocultar formulario"],"Reset Form":["Restablecer formulario"],"Custom URL":["URL personalizada"],"Add Query Parameters":["Agregar par\u00e1metros de consulta"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Seleccione si desea agregar pares clave-valor para los campos del formulario que se incluir\u00e1n en los par\u00e1metros de consulta"],"Query Parameters":["Par\u00e1metros de consulta"],"Success Message":["Mensaje de \u00e9xito"],"Redirect":["Redirigir"],"Redirect to":["Redirigir a"],"Page":["P\u00e1gina"],"Form Confirmation":["Confirmaci\u00f3n de formulario"],"Confirmation Type":["Tipo de confirmaci\u00f3n"],"Clear":["Claro"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invisible"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validaciones"],"Spam Protection":["Protecci\u00f3n contra el spam"],"Email Summaries":["Res\u00famenes de correo electr\u00f3nico"],"Tuesday":["Martes"],"Wednesday":["Mi\u00e9rcoles"],"Thursday":["Jueves"],"Friday":["Viernes"],"Saturday":["S\u00e1bado"],"Sunday":["Domingo"],"Test Email":["Correo de prueba"],"Schedule Reports":["Programar informes"],"IP Logging":["Registro de IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Si esta opci\u00f3n est\u00e1 activada, la direcci\u00f3n IP del usuario se guardar\u00e1 con los datos del formulario"],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s representa las selecciones m\u00ednimas necesarias. Por ejemplo: \u201cSe requieren un m\u00ednimo de 2 selecciones.\u201d"],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s representa el m\u00e1ximo de selecciones permitidas. Por ejemplo: \u201cSe permiten un m\u00e1ximo de 4 selecciones.\u201d"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s representa las opciones m\u00ednimas necesarias. Por ejemplo: \"Se requiere un m\u00ednimo de 1 selecci\u00f3n.\""],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s representa las opciones m\u00e1ximas permitidas. Por ejemplo: \"Se permiten un m\u00e1ximo de 3 selecciones.\""]," Error Message":["Mensaje de error"],"Auto":["Auto"],"Light":["Luz"],"Dark":["Oscuro"],"Turnstile":["Torniquete"],"Honeypot":["Honeypot"],"Get Keys":["Obtener llaves"],"Documentation":["Documentaci\u00f3n"],"Site Key":["Clave del sitio"],"Secret Key":["Clave secreta"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Modo de apariencia"],"Enable Honeypot Security":["Habilitar la seguridad Honeypot"],"Enable Honeypot Security for better spam protection":["Habilitar la seguridad Honeypot para una mejor protecci\u00f3n contra el spam"],"Text":["Texto"],"Confirmation Email Mismatch Message":["Mensaje de discrepancia de correo electr\u00f3nico de confirmaci\u00f3n"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s representa el valor m\u00ednimo de entrada. Por ejemplo: \"El valor m\u00ednimo es 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s representa el valor m\u00e1ximo de entrada. Por ejemplo: \"El valor m\u00e1ximo es 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Con\u00e9ctate con OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual no coincide con el nombre de dominio de su sitio web (%1$s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam. Alternativamente, intente usar una direcci\u00f3n de remitente que coincida con el dominio de su sitio web (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual no coincide con el nombre de dominio de su sitio web (%s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam."],"We strongly recommend that you install the free ":["Recomendamos encarecidamente que instale el gratuito"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["\u00a1Complemento! El asistente de configuraci\u00f3n facilita la correcci\u00f3n de tus correos electr\u00f3nicos."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativamente, intenta usar una direcci\u00f3n de remitente que coincida con el dominio de tu sitio web (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Por favor, introduce una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida. Tus notificaciones no se enviar\u00e1n si el campo no est\u00e1 rellenado correctamente."],"From Name":["De Nombre"],"From Email":["De correo electr\u00f3nico"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual puede no coincidir con el nombre de dominio de su sitio web (%1$s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam. Alternativamente, intente usar una Direcci\u00f3n del Remitente que coincida con el dominio de su sitio web (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["La direcci\u00f3n de 'Correo Electr\u00f3nico del Remitente' actual puede no coincidir con el nombre de dominio de su sitio web (%s). Esto puede hacer que sus correos electr\u00f3nicos de notificaci\u00f3n sean bloqueados o marcados como spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["\u00bfListo para ir m\u00e1s all\u00e1 del plan gratuito?"],"Upgrade now":["Actualiza ahora"],"and unlock the full power of SureForms!":["y desbloquea todo el poder de SureForms!"],"Upgrade SureForms":["Actualizar SureForms"],"Upgrade Now":["Actualiza ahora"],"Entries older than the selected days will be deleted.":["Las entradas anteriores a los d\u00edas seleccionados ser\u00e1n eliminadas."],"Entries Time Period":["Per\u00edodo de tiempo de las entradas"],"Notifications can use only one From Email so please enter a single address.":["Las notificaciones solo pueden usar un \u00fanico correo electr\u00f3nico de origen, as\u00ed que por favor ingrese una sola direcci\u00f3n."],"Select Page to redirect":["Seleccionar p\u00e1gina para redirigir"],"Search for a page":["Buscar una p\u00e1gina"],"Select a page":["Selecciona una p\u00e1gina"],"Form Validation":["Validaci\u00f3n de formulario"],"Required Error Messages":["Mensajes de error requeridos"],"Other Error Messages":["Otros mensajes de error"],"Input Field Unique":["Campo de entrada \u00fanico"],"Email Field Unique":["Campo de correo electr\u00f3nico \u00fanico"],"Invalid URL":["URL no v\u00e1lida"],"Phone Field Unique":["Campo de Tel\u00e9fono \u00danico"],"Invalid Field Number Block":["Bloque de N\u00famero de Campo Inv\u00e1lido"],"Invalid Email":["Correo electr\u00f3nico no v\u00e1lido"],"Number Minimum Value":["Valor M\u00ednimo del N\u00famero"],"Number Maximum Value":["Valor M\u00e1ximo del N\u00famero"],"Dropdown Minimum Selections":["M\u00ednimo de selecciones en el men\u00fa desplegable"],"Dropdown Maximum Selections":["Selecciones M\u00e1ximas del Desplegable"],"Multiple Choice Minimum Selections":["M\u00faltiples opciones de selecci\u00f3n m\u00ednima"],"Multiple Choice Maximum Selections":["Selecciones M\u00faltiples de Opci\u00f3n M\u00faltiple"],"Input Field":["Campo de entrada"],"Email Field":["Campo de correo electr\u00f3nico"],"URL Field":["Campo de URL"],"Phone Field":["Campo de Tel\u00e9fono"],"Textarea Field":["Campo de \u00e1rea de texto"],"Checkbox Field":["Campo de casilla de verificaci\u00f3n"],"Dropdown Field":["Campo desplegable"],"Multiple Choice Field":["Campo de opci\u00f3n m\u00faltiple"],"Address Field":["Campo de direcci\u00f3n"],"Number Field":["Campo num\u00e9rico"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Para habilitar la funci\u00f3n reCAPTCHA en sus SureForms, por favor habilite la opci\u00f3n reCAPTCHA en la configuraci\u00f3n de sus bloques y seleccione la versi\u00f3n. Agregue aqu\u00ed la clave secreta y la clave del sitio de Google reCAPTCHA. reCAPTCHA se a\u00f1adir\u00e1 a su p\u00e1gina en el front-end."],"Enter your %s here":["Introduce tu %s aqu\u00ed"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Para habilitar hCAPTCHA, por favor a\u00f1ade tu clave del sitio y clave secreta. Configura estos ajustes dentro del formulario individual."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Para habilitar Cloudflare Turnstile, por favor a\u00f1ade tu clave de sitio y clave secreta. Configura estos ajustes dentro del formulario individual."],"Save":["Guardar"],"Anonymous Analytics":["Anal\u00edtica An\u00f3nima"],"Learn More":["Aprende m\u00e1s"],"Admin Notification":["Notificaci\u00f3n de administrador"],"Enable Admin Notification":["Habilitar notificaci\u00f3n de administrador"],"Admin notifications keep you informed about new form entries since your last visit.":["Las notificaciones de administrador te mantienen informado sobre nuevas entradas de formularios desde tu \u00faltima visita."],"Skip":["Omitir"],"Continue":["Continuar"],"Maximum Number of Entries":["N\u00famero m\u00e1ximo de entradas"],"Maximum Entries":["Entradas M\u00e1ximas"],"Response Description After Maximum Entries":["Descripci\u00f3n de la respuesta despu\u00e9s del m\u00e1ximo de entradas"],"Get Started":["Comenzar"],"Integration":["Integraci\u00f3n"],"Connect Native Integrations with SureForms":["Conecta integraciones nativas con SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Desbloquea potentes integraciones en el plan Premium para automatizar tus flujos de trabajo y conectar SureForms directamente con tus herramientas favoritas."],"Send form submissions straight to CRMs, email, and marketing platforms":["Env\u00eda las presentaciones de formularios directamente a los CRM, correo electr\u00f3nico y plataformas de marketing"],"Automate repetitive tasks with seamless data syncing":["Automatiza tareas repetitivas con una sincronizaci\u00f3n de datos sin problemas"],"Access exclusive native integrations for faster workflows":["Accede a integraciones nativas exclusivas para flujos de trabajo m\u00e1s r\u00e1pidos"],"Expected format for emails - email@sureforms.com or John Doe ":["Formato esperado para correos electr\u00f3nicos: email@sureforms.com o John Doe "],"Payments":["Pagos"],"Stripe account disconnected successfully.":["Cuenta de Stripe desconectada con \u00e9xito."],"Failed to create webhook.":["Error al crear el webhook."],"Failed to connect to Stripe.":["Error al conectar con Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Base de Conocimientos"],"What\u2019s New":["Qu\u00e9 hay de nuevo"],"out of":["fuera de"],"No entries found":["No se encontraron entradas"],"delete":["eliminar"],"Please type \"%s\" in the input box":["Por favor, escribe \"%s\" en el cuadro de entrada"],"To confirm, type \"%s\" in the box below:":["Para confirmar, escriba \"%s\" en el cuadro de abajo:"],"Type \"%s\"":["Escribe \"%s\""],"Go to OttoKit Settings":["Ve a la configuraci\u00f3n de OttoKit"],"USD - US Dollar":["USD - D\u00f3lar estadounidense"],"Payment Mode":["Modo de pago"],"Test Mode":["Modo de prueba"],"Live Mode":["Modo en vivo"],"Action":["Acci\u00f3n"],"General Settings":["Configuraci\u00f3n general"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Configure res\u00famenes de correo electr\u00f3nico, alertas de administrador y preferencias de datos para gestionar tus formularios con facilidad."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personaliza los mensajes de error predeterminados que se muestran cuando los usuarios env\u00edan entradas de formulario inv\u00e1lidas o incompletas."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Habilita la protecci\u00f3n contra spam para tus formularios utilizando servicios CAPTCHA o seguridad honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Conecta y gestiona tus pasarelas de pago para aceptar transacciones de forma segura a trav\u00e9s de tus formularios."],"1% transaction and payment gateway fees apply.":["Se aplican tarifas del 1% por transacciones y pasarelas de pago."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Se aplican tarifas de transacci\u00f3n y de pasarela de pago del 2.9%. Activa la licencia para reducir las tarifas de transacci\u00f3n."],"2.9% transaction and payment gateway fees apply.":["Se aplican tarifas del 2.9% por transacciones y pasarelas de pago."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Por favor, visita %1$s, elimina un webhook no utilizado, luego haz clic abajo para reintentar."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms no pudo crear un webhook porque tu cuenta de Stripe se ha quedado sin espacios libres. Los webhooks son necesarios para recibir actualizaciones sobre los pagos."],"Stripe Dashboard":["Panel de control de Stripe"],"Creating\u2026":["Creando\u2026"],"Create Webhook":["Crear Webhook"],"Successfully connected to Stripe!":["\u00a1Conectado exitosamente a Stripe!"],"Invalid response from server. Please try again.":["Respuesta inv\u00e1lida del servidor. Por favor, int\u00e9ntelo de nuevo."],"Failed to disconnect Stripe account.":["Error al desconectar la cuenta de Stripe."],"Webhook created successfully!":["\u00a1Webhook creado con \u00e9xito!"],"Select Currency":["Seleccionar moneda"],"Select the default currency for payment forms.":["Seleccione la moneda predeterminada para los formularios de pago."],"Connection Status":["Estado de la conexi\u00f3n"],"Disconnect Stripe Account":["Desconectar cuenta de Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["\u00bfEst\u00e1 seguro de que desea desconectar su cuenta de Stripe? Esto detendr\u00e1 todos los pagos activos, suscripciones y transacciones de formularios conectados a esta cuenta."],"Disconnect":["Desconectar"],"Disconnecting\u2026":["Desconectando\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook conectado con \u00e9xito, todos los eventos de Stripe est\u00e1n siendo rastreados."],"Connect your Stripe account to start accepting payments through your forms.":["Conecta tu cuenta de Stripe para comenzar a aceptar pagos a trav\u00e9s de tus formularios."],"Connect to Stripe":["Conectar a Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Con\u00e9ctate de forma segura a Stripe con solo unos clics para comenzar a aceptar pagos."],"Set the total number of submissions allowed for this form.":["Establezca el n\u00famero total de env\u00edos permitidos para este formulario."],"Payment Methods":["M\u00e9todos de pago"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["El modo de prueba te permite procesar pagos sin cargos reales. Cambia al modo en vivo para transacciones reales."],"General Payment Settings":["Configuraci\u00f3n General de Pagos"],"These settings apply to all payment gateways.":["Estos ajustes se aplican a todas las pasarelas de pago."],"Stripe Settings":["Configuraci\u00f3n de Stripe"],"Left ($100)":["Izquierda ($100)"],"Right (100$)":["Correcto (100$)"],"Left Space ($ 100)":["Espacio Izquierdo ($ 100)"],"Right Space (100 $)":["Espacio Derecho (100 $)"],"Currency Sign Position":["Posici\u00f3n del signo de moneda"],"Select the position of the currency symbol relative to the amount.":["Seleccione la posici\u00f3n del s\u00edmbolo de moneda en relaci\u00f3n con la cantidad."],"Learn":["Aprender"],"Enable email summaries":["Habilitar res\u00famenes de correo electr\u00f3nico"],"Enable IP logging":["Habilitar el registro de IP"],"Turn on Admin Notification from here.":["Activa la notificaci\u00f3n de administrador desde aqu\u00ed."],"New":["Nuevo"],"Send entries to 100+ popular apps.":["Env\u00eda entradas a m\u00e1s de 100 aplicaciones populares."],"Build automated workflows that run instantly.":["Crea flujos de trabajo automatizados que se ejecutan al instante."],"Create custom app integrations using our Custom App feature.":["Crea integraciones de aplicaciones personalizadas utilizando nuestra funci\u00f3n de Aplicaci\u00f3n Personalizada."],"Keep your tools in sync automatically.":["Mant\u00e9n tus herramientas sincronizadas autom\u00e1ticamente."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Esto instalar\u00e1 y activar\u00e1 OttoKit en su sitio de WordPress para habilitar funciones de automatizaci\u00f3n."],"Automate Your Forms with OttoKit":["Automatiza tus formularios con OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Cada env\u00edo de formulario deber\u00eda activar algo: una alerta de Slack, un cliente potencial en el CRM, un correo electr\u00f3nico de seguimiento o una nueva fila en Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configura los permisos del cliente de IA y la configuraci\u00f3n del servidor MCP."],"View documentation":["Ver documentaci\u00f3n"],"Copy to clipboard":["Copiar al portapapeles"],"Claude Desktop":["Escritorio Claude"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) o %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["C\u00f3digo Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (proyecto) o ~\/.claude.json (global)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (proyecto) o settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml o config.json"],"Your client's MCP configuration file":["El archivo de configuraci\u00f3n MCP de su cliente"],"Connect Your AI Client":["Conecta tu cliente de IA"],"AI Client":["Cliente de IA"],"Create an Application Password \u2014 ":["Crear una Contrase\u00f1a de Aplicaci\u00f3n \u2014"],"Open Application Passwords":["Abrir contrase\u00f1as de aplicaciones"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["O utiliza este comando CLI para a\u00f1adir el servidor r\u00e1pidamente (a\u00fan necesitar\u00e1s configurar las variables de entorno):"],"Copy the JSON config below into: ":["Copie la configuraci\u00f3n JSON a continuaci\u00f3n en:"],"Replace \"your-application-password\" with the password from Step 1.":["Reemplace \"your-application-password\" con la contrase\u00f1a del Paso 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 el punto final MCP de tu sitio. WP_API_USERNAME \u2014 tu nombre de usuario de WordPress. WP_API_PASSWORD \u2014 la contrase\u00f1a de la aplicaci\u00f3n que generaste."],"View setup docs":["Ver documentos de configuraci\u00f3n"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["El complemento MCP Adapter est\u00e1 instalado pero no activo. Act\u00edvalo para configurar los ajustes de MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["El complemento MCP Adapter es necesario para conectar clientes de IA a tus formularios. Desc\u00e1rgalo e inst\u00e1lalo desde GitHub, luego act\u00edvalo."],"Download the latest release from":["Descarga la \u00faltima versi\u00f3n desde"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Instala el complemento a trav\u00e9s de Complementos > A\u00f1adir nuevo complemento > Subir complemento."],"Activate the MCP Adapter plugin.":["Activa el complemento del adaptador MCP."],"Activating\u2026":["Activando\u2026"],"Activate MCP Adapter":["Activar adaptador MCP"],"Download MCP Adapter":["Descargar el adaptador MCP"],"Experimental":["Experimental"],"Enable Abilities":["Habilitar habilidades"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registra las habilidades de SureForms con la API de Habilidades de WordPress. Cuando est\u00e1 habilitado, los clientes de IA pueden listar, leer, crear, editar y eliminar tus formularios y entradas. Cuando est\u00e1 deshabilitado, no se registran habilidades y los clientes de IA no pueden realizar ninguna acci\u00f3n en tus formularios."],"Abilities API \u2014 Edit":["API de habilidades \u2014 Editar"],"Enable Edit Abilities":["Habilitar habilidades de edici\u00f3n"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Cuando est\u00e1 habilitado, los clientes de IA pueden crear nuevos formularios, actualizar t\u00edtulos de formularios, campos y configuraciones, duplicar formularios y modificar los estados de las entradas. Cuando est\u00e1 deshabilitado, estas habilidades se desregistran y los clientes de IA solo pueden leer sus datos."],"Abilities API \u2014 Delete":["API de habilidades \u2014 Eliminar"],"Enable Delete Abilities":["Habilitar habilidades de eliminaci\u00f3n"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Cuando est\u00e1 habilitado, los clientes de IA pueden eliminar permanentemente formularios y entradas. Los datos eliminados no se pueden recuperar. Cuando est\u00e1 deshabilitado, las capacidades de eliminaci\u00f3n se desregistran y los clientes de IA no pueden eliminar ning\u00fan dato."],"MCP Server":["Servidor MCP"],"Enable MCP Server":["Habilitar servidor MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Crea un punto final dedicado de SureForms MCP al que los clientes de IA como Claude pueden conectarse. Cuando est\u00e1 deshabilitado, el punto final se elimina y los clientes de IA externos no pueden descubrir ni llamar a ninguna capacidad de SureForms."],"Learn more":["Aprende m\u00e1s"],"MCP Adapter Required":["Adaptador MCP Requerido"],"Heading 1":["Encabezado 1"],"Heading 2":["Encabezado 2"],"Heading 3":["Encabezado 3"],"Heading 4":["Encabezado 4"],"Heading 5":["Encabezado 5"],"Heading 6":["Encabezado 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configura la clave de la API de Google Maps para la autocompletaci\u00f3n de direcciones y la vista previa del mapa."],"Help shape the future of SureForms":["Ayuda a dar forma al futuro de SureForms"],"Enable Google Address Autocomplete":["Habilitar la autocompletar de direcciones de Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Actualiza al Plan de Negocios de SureForms para agregar la autocompletaci\u00f3n de direcciones impulsada por Google con vista previa de mapa interactivo a tus formularios."],"Auto-suggest addresses as users type for faster, error-free submissions":["Sugerir direcciones autom\u00e1ticamente mientras los usuarios escriben para env\u00edos m\u00e1s r\u00e1pidos y sin errores"],"Show an interactive map preview with draggable pin for precise locations":["Muestra una vista previa de mapa interactivo con un marcador arrastrable para ubicaciones precisas"],"Automatically populate address fields like city, state, and postal code":["Rellenar autom\u00e1ticamente campos de direcci\u00f3n como ciudad, estado y c\u00f3digo postal"],"This form is now closed as we've received all the entries.":["Este formulario est\u00e1 ahora cerrado ya que hemos recibido todas las inscripciones."],"Thank you for contacting us! We will be in touch with you shortly.":["\u00a1Gracias por contactarnos! Nos pondremos en contacto contigo en breve."],"Saving\u2026":["Guardando\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Cuando est\u00e9 habilitado, este formulario no almacenar\u00e1 la IP del usuario, el nombre del navegador ni el nombre del dispositivo en las entradas."],"Form data":["Datos del formulario"],"Unsaved changes":["Cambios no guardados"],"Keep editing":["Sigue editando"],"Global Defaults":["Valores predeterminados globales"],"Form Restrictions":["Restricciones del formulario"],"Configure default settings that apply to newly created forms.":["Configurar los ajustes predeterminados que se aplican a los formularios reci\u00e9n creados."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Recopila informaci\u00f3n no sensible de tu sitio web, como la versi\u00f3n de PHP y las caracter\u00edsticas utilizadas, para ayudarnos a corregir errores m\u00e1s r\u00e1pido, tomar decisiones m\u00e1s inteligentes y desarrollar funciones que realmente te importen."],"Failed to load pages. Please refresh and try again.":["Error al cargar las p\u00e1ginas. Por favor, actualice e intente de nuevo."],"Failed to load settings. Please refresh and try again.":["Error al cargar la configuraci\u00f3n. Por favor, actualice e intente de nuevo."],"Form Validation fields cannot be left blank.":["Los campos de validaci\u00f3n de formularios no pueden dejarse en blanco."],"Recipient email is required when email summaries are enabled.":["Se requiere el correo electr\u00f3nico del destinatario cuando los res\u00famenes de correo electr\u00f3nico est\u00e1n habilitados."],"Please enter a valid recipient email.":["Por favor, introduce un correo electr\u00f3nico de destinatario v\u00e1lido."],"Settings saved.":["Configuraciones guardadas."],"Failed to save settings.":["Error al guardar la configuraci\u00f3n."],"Some settings failed to save. Please retry.":["Algunas configuraciones no se pudieron guardar. Por favor, int\u00e9ntalo de nuevo."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Algunos campos tienen cambios no guardados. Des\u00e9chelos para continuar, o permanezca para guardar sus ediciones."],"Discard & switch":["Descartar y cambiar"],"Import":["Importar"],"Migration":["Migraci\u00f3n"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importa formularios de Contact Form 7, WPForms, Gravity Forms y otros plugins en SureForms."],"Could not generate the import preview.":["No se pudo generar la vista previa de importaci\u00f3n."],"Import failed. Please try again or check your error logs.":["La importaci\u00f3n fall\u00f3. Por favor, int\u00e9ntelo de nuevo o revise sus registros de errores."],"Go back":["Regresar"],"Update & import":["Actualizar e importar"],"Confirm & import":["Confirmar e importar"],"Form #%s":["Formulario n.\u00ba %s"],"%d field will import":["Se importar\u00e1 %d campo"],"Some fields can't be migrated yet":["Algunos campos no se pueden migrar todav\u00eda"],"These fields will be skipped - add them manually after the form is created:":["Estos campos se omitir\u00e1n; agr\u00e9guelos manualmente despu\u00e9s de crear el formulario:"],"Some forms could not be parsed":["Algunos formularios no se pudieron analizar"],"Update existing":["Actualizar existente"],"Create a new copy":["Crear una nueva copia"],"Could not load forms for this source.":["No se pudieron cargar los formularios para esta fuente."],"(untitled form)":["(formulario sin t\u00edtulo)"],"Previously imported":["Importado previamente"],"Open existing SureForms form in a new tab":["Abrir el formulario SureForms existente en una nueva pesta\u00f1a"],"On re-import":["En la reimportaci\u00f3n"],"No forms found in this plugin.":["No se encontraron formularios en este complemento."],"%1$d of %2$d form selected":["%1$d de %2$d formulario seleccionado"],"Preview update":["Vista previa de la actualizaci\u00f3n"],"Preview import":["Vista previa de la importaci\u00f3n"],"Migration complete":["Migraci\u00f3n completa"],"Nothing was imported":["No se import\u00f3 nada"],"%d form was imported into SureForms.":["%d formulario fue importado a SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Ninguno de los formularios seleccionados pudo ser migrado. Consulte las advertencias a continuaci\u00f3n para obtener m\u00e1s detalles."],"Imported forms":["Formularios importados"],"Edit in SureForms":["Editar en SureForms"],"Some fields were skipped during import":["Algunos campos se omitieron durante la importaci\u00f3n"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Agrega estos manualmente en el editor de formularios - a\u00fan no tienen un equivalente en SureForms:"],"Some forms failed to import":["Algunos formularios no se pudieron importar"],"Import more forms":["Importar m\u00e1s formularios"],"Could not load the list of importable plugins.":["No se pudo cargar la lista de complementos importables."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["No hay complementos de formularios compatibles activos en este sitio. Activa uno (como Contact Form 7) con al menos un formulario para importarlo en SureForms."],"Plugin":["Complemento"],"%d form":["%d formulario"],"No forms":["No hay formularios"],"Multiple choice":["Opci\u00f3n m\u00faltiple"],"Consent":["Consentimiento"],"The image uploaded but could not be inserted. Please try again.":["La imagen se carg\u00f3 pero no pudo ser insertada. Por favor, int\u00e9ntalo de nuevo."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["La imagen no pudo ser cargada. Puede ser demasiado grande o es posible que no tengas permiso para subir archivos."],"Please choose an image file.":["Por favor, elige un archivo de imagen."],"Show views and conversion rate":["Mostrar vistas y tasa de conversi\u00f3n"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Agrega las columnas de Vistas y Tasa de Conversi\u00f3n a la lista de Formularios. Una vista se cuenta una vez por visita a la p\u00e1gina cuando el formulario aparece en pantalla, y la tasa de conversi\u00f3n es la proporci\u00f3n de esas vistas que terminaron en un env\u00edo. El conteo comienza la primera vez que lo enciende, por lo que los env\u00edos recibidos antes de entonces no se cuentan para la tasa. Apagarlo despu\u00e9s solo oculta las columnas \u2014 el conteo contin\u00faa, por lo que las cifras est\u00e1n actualizadas si lo vuelve a encender."],"Log cleared.":["Registro borrado."],"Could not clear the log.":["No se pudo borrar el registro."],"Enable logs":["Habilitar registros"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registra los fallos de env\u00edo de formularios reportados por el navegador, para que puedas enviar el registro al soporte en lugar de leer la consola. No se escribe nada mientras tus formularios funcionan, y los valores enviados nunca se almacenan."],"Download log":["Descargar registro"],"Confirm delete":["Confirmar eliminaci\u00f3n"],"Log is full \u2014 download and clear it to keep recording.":["El registro est\u00e1 lleno \u2014 desc\u00e1rgalo y b\u00f3rralo para seguir grabando."],"Form Views & Conversion":["Vistas y Conversi\u00f3n de Formularios"],"Logs":["Registros"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Las vistas y env\u00edos por parte de cualquier persona que pueda editar el sitio normalmente se dejan fuera, por lo que probar sus propios formularios no cambia estas cifras."],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}}
\ No newline at end of file
diff --git a/languages/sureforms-es_ES-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-es_ES-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json
index 712166914..c63d9b0d4 100644
--- a/languages/sureforms-es_ES-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json
+++ b/languages/sureforms-es_ES-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json
@@ -1 +1 @@
-{"translation-revision-date":"2024-12-13T16:34:28+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tablero"],"Settings":["Configuraciones"],"Entries":["Entradas"],"Activated":["Activado"],"Advanced Fields":["Campos avanzados"],"Select Form":["Seleccionar formulario"],"Create New Form":["Crear nuevo formulario"],"Forms":["Formularios"],"Copy":["Copiar"],"Business":["Negocios"],"Next":["Siguiente"],"Back":["Atr\u00e1s"],"Cancel":["Cancelar"],"This is where your form views will appear":["Aqu\u00ed es donde aparecer\u00e1n las vistas de su formulario"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["La instalaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"Plugin activation failed, Please try again later.":["La activaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"What's New?":["\u00bfQu\u00e9 hay de nuevo?"],"Core":["N\u00facleo"],"Unlicensed":["Sin licencia"],"Upgrade":["Actualizar"],"Webhooks":["Webhooks"],"Install & Activate":["Instalar y activar"],"Conditional Logic":["L\u00f3gica condicional"],"Premium":["Premium"],"Welcome to SureForms!":["\u00a1Bienvenido a SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms es un complemento de WordPress que permite a los usuarios crear formularios de aspecto atractivo a trav\u00e9s de una interfaz de arrastrar y soltar, sin necesidad de programar. Se integra con el editor de bloques de WordPress."],"Read Full Guide":["Leer la gu\u00eda completa"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Formularios personalizados de WordPress HECHOS SIMPLES"],"No Date":["Sin fecha"],"Invalid Date":["Fecha no v\u00e1lida"],"Ready to go beyond free plan?":["\u00bfListo para ir m\u00e1s all\u00e1 del plan gratuito?"],"Upgrade now":["Actualiza ahora"],"and unlock the full power of SureForms!":["y desbloquea todo el poder de SureForms!"],"Upgrade SureForms":["Actualizar SureForms"],"Open Support Ticket":["Abrir Ticket de Soporte"],"Help Center":["Centro de ayuda"],"Join our Community on Facebook":["\u00danete a nuestra comunidad en Facebook"],"Leave Us a Review":["D\u00e9janos una rese\u00f1a"],"Quick Access":["Acceso R\u00e1pido"],"Upgrade Now":["Actualiza ahora"],"Clear Filters":["Limpiar filtros"],"Unnamed Form":["Formulario sin nombre"],"Forms Overview":["Descripci\u00f3n general de los formularios"],"Clear Form Filters":["Limpiar filtros del formulario"],"Clear Date Filters":["Borrar filtros de fecha"],"Select Date Range":["Seleccionar rango de fechas"],"Apply":["Aplicar"],"Please wait for the data to load":["Por favor, espera a que los datos se carguen"],"No entries to display":["No hay entradas para mostrar"],"Once you create a form and start receiving submissions, the data will appear here.":["Una vez que crees un formulario y comiences a recibir env\u00edos, los datos aparecer\u00e1n aqu\u00ed."],"Free":["Gratis"],"All Forms":["Todas las formas"],"Guided Setup":["Configuraci\u00f3n Guiada"],"Exit Guided Setup":["Salir de la Configuraci\u00f3n Guiada"],"Skip":["Omitir"],"Build beautiful forms visually":["Construye formularios hermosos visualmente"],"Works perfectly on mobile":["Funciona perfectamente en dispositivos m\u00f3viles"],"Easy to connect with automation tools":["F\u00e1cil de conectar con herramientas de automatizaci\u00f3n"],"Welcome to SureForms":["Bienvenido a SureForms"],"Smart, Quick and Powerful Forms.":["Formularios inteligentes, r\u00e1pidos y potentes."],"Let's Get Started":["Empecemos"],"Build up to 10 forms using AI":["Construye hasta 10 formularios usando IA"],"A secure and private connection":["Una conexi\u00f3n segura y privada"],"Smart form drafts based on your input":["Borradores de formularios inteligentes basados en tu entrada"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Comenzar desde un formulario en blanco no siempre es f\u00e1cil. Nuestra IA puede ayudar creando un borrador del formulario basado en lo que intentas hacer, ahorr\u00e1ndote tiempo y d\u00e1ndote una direcci\u00f3n clara."],"To do this, you'll need to connect your account.":["Para hacer esto, necesitar\u00e1s conectar tu cuenta."],"Let AI Help You Build Smarter, Faster Forms":["Deja que la IA te ayude a crear formularios m\u00e1s inteligentes y r\u00e1pidos"],"Here's what that gives you:":["Aqu\u00ed tienes lo que eso te da:"],"Continue":["Continuar"],"Connect":["Conectar"],"Works smoothly with forms made using SureForms":["Funciona sin problemas con formularios creados usando SureForms"],"Helps your emails reach the inbox instead of spam":["Ayuda a que tus correos electr\u00f3nicos lleguen a la bandeja de entrada en lugar de a la carpeta de spam"],"Setup is straightforward, even if you're not technical":["La configuraci\u00f3n es sencilla, incluso si no eres t\u00e9cnico."],"Lightweight and easy to use without adding clutter":["Ligero y f\u00e1cil de usar sin a\u00f1adir desorden"],"Make Sure Your Emails Get Delivered":["Aseg\u00farate de que tus correos electr\u00f3nicos se entreguen"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["La mayor\u00eda de los sitios de WordPress tienen dificultades para enviar correos electr\u00f3nicos de manera confiable, lo que significa que los env\u00edos de formularios desde su sitio podr\u00edan no llegar a su bandeja de entrada o terminar en spam."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail es un sencillo complemento SMTP que ayuda a asegurar que tus correos electr\u00f3nicos se entreguen realmente."],"What you will get:":["Lo que obtendr\u00e1s:"],"Install SureMail":["Instalar SureMail"],"AI Form Generation":["Generaci\u00f3n de Formularios de IA"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["\u00bfCansado de crear formularios manualmente? Deja que la IA haga el trabajo por ti. Solo describe y nuestra IA crear\u00e1 tu formulario perfecto en segundos."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Descompone formularios complejos en pasos simples, reduciendo la sensaci\u00f3n de agobio y aumentando las tasas de finalizaci\u00f3n. Gu\u00eda a los usuarios de manera fluida a trav\u00e9s del proceso"],"Conditional Fields":["Campos Condicionales"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Mostrar u ocultar campos seg\u00fan las respuestas del usuario. Haz las preguntas correctas y muestra solo lo necesario para mantener los formularios limpios y relevantes."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Mejora tus formularios con campos avanzados como carga de m\u00faltiples archivos, campos de calificaci\u00f3n y selectores de fecha y hora para recopilar datos m\u00e1s ricos y flexibles."],"Conversational Forms":["Formas conversacionales"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Crea formularios que se sientan como una conversaci\u00f3n. Una pregunta a la vez mantiene a los usuarios comprometidos y facilita la finalizaci\u00f3n del formulario."],"Digital Signatures":["Firmas Digitales"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Recoge firmas digitales legalmente vinculantes directamente en tus formularios para acuerdos, aprobaciones y contratos."],"Calculators":["Calculadoras"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Agrega calculadoras interactivas a tus formularios para obtener estimaciones, cotizaciones y c\u00e1lculos instant\u00e1neos para tus usuarios."],"User Registration and Login":["Registro y acceso de usuario"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Permite a los visitantes registrarse e iniciar sesi\u00f3n en tu sitio. \u00datil para membres\u00edas, comunidades o cualquier sitio que necesite acceso de usuarios."],"PDF Generation Made Simple":["Generaci\u00f3n de PDF hecha simple"],"Custom App":["Aplicaci\u00f3n Personalizada"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Recopila datos, env\u00edalos a aplicaciones externas para su procesamiento y muestra los resultados al instante, todo integrado de manera fluida para crear experiencias de usuario din\u00e1micas e interactivas."],"Select Your Features":["Selecciona tus caracter\u00edsticas"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Obt\u00e9n m\u00e1s control, flujos de trabajo m\u00e1s r\u00e1pidos y una personalizaci\u00f3n m\u00e1s profunda, todo dise\u00f1ado para ayudarte a crear mejores sitios web con menos esfuerzo."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Las funciones seleccionadas requieren %1$s - usa el c\u00f3digo %2$s para obtener un 10% de descuento en cualquier plan."],"Copied":["Copiado"],"Style your form to better match your site's design":["Estiliza tu formulario para que se adapte mejor al dise\u00f1o de tu sitio"],"Add spam protection to block common bot submissions":["Agrega protecci\u00f3n contra spam para bloquear env\u00edos comunes de bots"],"Get weekly email reports with a summary of form activity":["Recibe informes semanales por correo electr\u00f3nico con un resumen de la actividad del formulario"],"You're All Set! \ud83d\ude80":["\u00a1Todo listo! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Utiliza nuestro creador de formularios con IA para comenzar r\u00e1pidamente, o construye tu formulario desde cero si ya sabes lo que necesitas. Tus formularios est\u00e1n listos para crear, compartir y conectar con los visitantes de tu sitio."],"Final Touches That Make a Difference:":["Toques finales que marcan la diferencia:"],"Build Your First Form":["Construye tu primer formulario"],"File Uploads":["Subidas de archivos"],"Signature & Rating":["Firma y Calificaci\u00f3n"],"Calculation Forms":["Formularios de C\u00e1lculo"],"And Much More\u2026":["Y mucho m\u00e1s\u2026"],"Upgrade to Pro":["Actualiza a Pro"],"Unlock Premium Features":["Desbloquear funciones premium"],"Build Better Forms with SureForms":["Construye mejores formularios con SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Agrega campos avanzados, dise\u00f1os conversacionales y l\u00f3gica inteligente para crear formularios que involucren a los usuarios y capturen mejores datos."],"SureForms Video Thumbnail":["Miniatura de video de SureForms"],"Payments":["Pagos"],"Knowledge Base":["Base de Conocimientos"],"What\u2019s New":["Qu\u00e9 hay de nuevo"],"Importing\u2026":["Importando\u2026"],"Learn":["Aprender"],"Unable to complete action. Please try again.":["No se puede completar la acci\u00f3n. Por favor, int\u00e9ntelo de nuevo."],"Supercharge Your Workflow":["Potencia tu flujo de trabajo"],"Spam protection included":["Protecci\u00f3n contra spam incluida"],"Multistep Forms":["Formularios de varios pasos"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Env\u00eda las entradas de formularios instant\u00e1neamente a cualquier sistema o punto final externo para potenciar flujos de trabajo avanzados."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Convierte autom\u00e1ticamente las entradas de formularios en PDFs limpios y listos para descargar. Perfecto para registros, compartir, archivar o mantener las cosas organizadas."],"Set up confirmation messages and email notifications for each entry":["Configura mensajes de confirmaci\u00f3n y notificaciones por correo electr\u00f3nico para cada entrada"],"Something went wrong. Please try again.":["Algo sali\u00f3 mal. Por favor, int\u00e9ntalo de nuevo."],"Payment Forms":["Formularios de Pago"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Cobra pagos directamente a trav\u00e9s de tus formularios. Acepta pagos \u00fanicos y recurrentes sin problemas."],"SureForms %s":["SureForms %s"],"First name is required.":["Se requiere el nombre."],"Please enter a valid email address.":["Por favor, introduce una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida."],"Email address is required.":["Se requiere la direcci\u00f3n de correo electr\u00f3nico."],"This is required.":["Esto es necesario."],"Okay, just one last step\u2026":["Est\u00e1 bien, solo un \u00faltimo paso\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Ay\u00fadanos a personalizar tu experiencia con SureForms compartiendo un poco sobre ti."],"First Name":["Nombre"],"Enter your first name":["Ingrese su nombre"],"Last Name":["Apellido"],"Enter your last name":["Ingrese su apellido"],"Email Address":["Direcci\u00f3n de correo electr\u00f3nico"],"Enter your email address":["Introduce tu direcci\u00f3n de correo electr\u00f3nico"],"Privacy Policy":["Pol\u00edtica de Privacidad"],"Finish":["Terminar"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Mantente al tanto y ayuda a dar forma a SureForms. Recibe actualizaciones de funciones y ay\u00fadanos a mejorar SureForms compartiendo c\u00f3mo utilizas el complemento. Pol\u00edtica de Privacidad<\/a>."],"%d form ready":["%d formulario listo"],"%d already imported":["%d ya importados"],"(unnamed source)":["(fuente sin nombre)"],"All forms already imported":["Todos los formularios ya importados"],"Import forms":["Importar formularios"],"Import %d form":["Importar %d formulario"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Algo sali\u00f3 mal durante la importaci\u00f3n. Puedes reintentar, omitir o abrir Configuraci\u00f3n \u2192 Migraci\u00f3n."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["No se detectaron otros complementos de formularios. Puedes importar en cualquier momento desde Configuraci\u00f3n \u2192 Migraci\u00f3n."],"Forms imported":["Formularios importados"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["El formulario %1$d de %2$s est\u00e1 ahora en SureForms, listo para publicar, estilizar y conectar."],"%d form imported":["%d formulario importado"],"%d form could not be imported":["No se pudo importar %d formulario"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d tipo de campo no era compatible. Puedes reconstruirlo manualmente dentro de SureForms."],"Bring your existing forms with you":["Trae tus formularios existentes contigo"],"We detected forms in another plugin. Pick one to import into SureForms.":["Detectamos formularios en otro complemento. Elige uno para importar a SureForms."],"Choose a form plugin to import from":["Elige un complemento de formulario para importar desde"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["\u00bfImportando m\u00e1s de un complemento? Puedes importar fuentes adicionales m\u00e1s tarde desde Configuraci\u00f3n \u2192 Migraci\u00f3n."],"Import did not complete":["La importaci\u00f3n no se complet\u00f3"],"I'll do this later":["Lo har\u00e9 m\u00e1s tarde"],"Retry":["Reintentar"],"Form Checks":["Verificaciones de formulario"],"Collapse form checks":["Colapsar verificaciones de formulario"],"Expand form checks":["Expandir verificaciones de formulario"],"Ignore":["Ignorar"],"Working\u2026":["Trabajando\u2026"]}}}
\ No newline at end of file
+{"translation-revision-date":"2024-12-13T16:34:28+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tablero"],"Settings":["Configuraciones"],"Entries":["Entradas"],"Activated":["Activado"],"Advanced Fields":["Campos avanzados"],"Select Form":["Seleccionar formulario"],"Create New Form":["Crear nuevo formulario"],"Forms":["Formularios"],"Copy":["Copiar"],"Business":["Negocios"],"Next":["Siguiente"],"Back":["Atr\u00e1s"],"Close":["Cerrar"],"Cancel":["Cancelar"],"This is where your form views will appear":["Aqu\u00ed es donde aparecer\u00e1n las vistas de su formulario"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["La instalaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"Plugin activation failed, Please try again later.":["La activaci\u00f3n del complemento fall\u00f3, por favor intente de nuevo m\u00e1s tarde."],"What's New?":["\u00bfQu\u00e9 hay de nuevo?"],"Core":["N\u00facleo"],"Unlicensed":["Sin licencia"],"Upgrade":["Actualizar"],"Webhooks":["Webhooks"],"Install & Activate":["Instalar y activar"],"Contact Support":["Contactar con el soporte"],"Conditional Logic":["L\u00f3gica condicional"],"Premium":["Premium"],"Welcome to SureForms!":["\u00a1Bienvenido a SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms es un complemento de WordPress que permite a los usuarios crear formularios de aspecto atractivo a trav\u00e9s de una interfaz de arrastrar y soltar, sin necesidad de programar. Se integra con el editor de bloques de WordPress."],"Read Full Guide":["Leer la gu\u00eda completa"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Formularios personalizados de WordPress HECHOS SIMPLES"],"No Date":["Sin fecha"],"Invalid Date":["Fecha no v\u00e1lida"],"Ready to go beyond free plan?":["\u00bfListo para ir m\u00e1s all\u00e1 del plan gratuito?"],"Upgrade now":["Actualiza ahora"],"and unlock the full power of SureForms!":["y desbloquea todo el poder de SureForms!"],"Upgrade SureForms":["Actualizar SureForms"],"Open Support Ticket":["Abrir Ticket de Soporte"],"Help Center":["Centro de ayuda"],"Join our Community on Facebook":["\u00danete a nuestra comunidad en Facebook"],"Leave Us a Review":["D\u00e9janos una rese\u00f1a"],"Quick Access":["Acceso R\u00e1pido"],"Upgrade Now":["Actualiza ahora"],"Clear Filters":["Limpiar filtros"],"Unnamed Form":["Formulario sin nombre"],"Forms Overview":["Descripci\u00f3n general de los formularios"],"Clear Form Filters":["Limpiar filtros del formulario"],"Clear Date Filters":["Borrar filtros de fecha"],"Select Date Range":["Seleccionar rango de fechas"],"Apply":["Aplicar"],"Please wait for the data to load":["Por favor, espera a que los datos se carguen"],"No entries to display":["No hay entradas para mostrar"],"Once you create a form and start receiving submissions, the data will appear here.":["Una vez que crees un formulario y comiences a recibir env\u00edos, los datos aparecer\u00e1n aqu\u00ed."],"Free":["Gratis"],"All Forms":["Todas las formas"],"Guided Setup":["Configuraci\u00f3n Guiada"],"Exit Guided Setup":["Salir de la Configuraci\u00f3n Guiada"],"Skip":["Omitir"],"Build beautiful forms visually":["Construye formularios hermosos visualmente"],"Works perfectly on mobile":["Funciona perfectamente en dispositivos m\u00f3viles"],"Easy to connect with automation tools":["F\u00e1cil de conectar con herramientas de automatizaci\u00f3n"],"Welcome to SureForms":["Bienvenido a SureForms"],"Smart, Quick and Powerful Forms.":["Formularios inteligentes, r\u00e1pidos y potentes."],"Let's Get Started":["Empecemos"],"Build up to 10 forms using AI":["Construye hasta 10 formularios usando IA"],"A secure and private connection":["Una conexi\u00f3n segura y privada"],"Smart form drafts based on your input":["Borradores de formularios inteligentes basados en tu entrada"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Comenzar desde un formulario en blanco no siempre es f\u00e1cil. Nuestra IA puede ayudar creando un borrador del formulario basado en lo que intentas hacer, ahorr\u00e1ndote tiempo y d\u00e1ndote una direcci\u00f3n clara."],"To do this, you'll need to connect your account.":["Para hacer esto, necesitar\u00e1s conectar tu cuenta."],"Let AI Help You Build Smarter, Faster Forms":["Deja que la IA te ayude a crear formularios m\u00e1s inteligentes y r\u00e1pidos"],"Here's what that gives you:":["Aqu\u00ed tienes lo que eso te da:"],"Continue":["Continuar"],"Connect":["Conectar"],"Works smoothly with forms made using SureForms":["Funciona sin problemas con formularios creados usando SureForms"],"Helps your emails reach the inbox instead of spam":["Ayuda a que tus correos electr\u00f3nicos lleguen a la bandeja de entrada en lugar de a la carpeta de spam"],"Setup is straightforward, even if you're not technical":["La configuraci\u00f3n es sencilla, incluso si no eres t\u00e9cnico."],"Lightweight and easy to use without adding clutter":["Ligero y f\u00e1cil de usar sin a\u00f1adir desorden"],"Make Sure Your Emails Get Delivered":["Aseg\u00farate de que tus correos electr\u00f3nicos se entreguen"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["La mayor\u00eda de los sitios de WordPress tienen dificultades para enviar correos electr\u00f3nicos de manera confiable, lo que significa que los env\u00edos de formularios desde su sitio podr\u00edan no llegar a su bandeja de entrada o terminar en spam."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail es un sencillo complemento SMTP que ayuda a asegurar que tus correos electr\u00f3nicos se entreguen realmente."],"What you will get:":["Lo que obtendr\u00e1s:"],"Install SureMail":["Instalar SureMail"],"AI Form Generation":["Generaci\u00f3n de Formularios de IA"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["\u00bfCansado de crear formularios manualmente? Deja que la IA haga el trabajo por ti. Solo describe y nuestra IA crear\u00e1 tu formulario perfecto en segundos."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Descompone formularios complejos en pasos simples, reduciendo la sensaci\u00f3n de agobio y aumentando las tasas de finalizaci\u00f3n. Gu\u00eda a los usuarios de manera fluida a trav\u00e9s del proceso"],"Conditional Fields":["Campos Condicionales"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Mostrar u ocultar campos seg\u00fan las respuestas del usuario. Haz las preguntas correctas y muestra solo lo necesario para mantener los formularios limpios y relevantes."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Mejora tus formularios con campos avanzados como carga de m\u00faltiples archivos, campos de calificaci\u00f3n y selectores de fecha y hora para recopilar datos m\u00e1s ricos y flexibles."],"Conversational Forms":["Formas conversacionales"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Crea formularios que se sientan como una conversaci\u00f3n. Una pregunta a la vez mantiene a los usuarios comprometidos y facilita la finalizaci\u00f3n del formulario."],"Digital Signatures":["Firmas Digitales"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Recoge firmas digitales legalmente vinculantes directamente en tus formularios para acuerdos, aprobaciones y contratos."],"Calculators":["Calculadoras"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Agrega calculadoras interactivas a tus formularios para obtener estimaciones, cotizaciones y c\u00e1lculos instant\u00e1neos para tus usuarios."],"User Registration and Login":["Registro y acceso de usuario"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Permite a los visitantes registrarse e iniciar sesi\u00f3n en tu sitio. \u00datil para membres\u00edas, comunidades o cualquier sitio que necesite acceso de usuarios."],"PDF Generation Made Simple":["Generaci\u00f3n de PDF hecha simple"],"Custom App":["Aplicaci\u00f3n Personalizada"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Recopila datos, env\u00edalos a aplicaciones externas para su procesamiento y muestra los resultados al instante, todo integrado de manera fluida para crear experiencias de usuario din\u00e1micas e interactivas."],"Select Your Features":["Selecciona tus caracter\u00edsticas"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Obt\u00e9n m\u00e1s control, flujos de trabajo m\u00e1s r\u00e1pidos y una personalizaci\u00f3n m\u00e1s profunda, todo dise\u00f1ado para ayudarte a crear mejores sitios web con menos esfuerzo."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Las funciones seleccionadas requieren %1$s - usa el c\u00f3digo %2$s para obtener un 10% de descuento en cualquier plan."],"Copied":["Copiado"],"Style your form to better match your site's design":["Estiliza tu formulario para que se adapte mejor al dise\u00f1o de tu sitio"],"Add spam protection to block common bot submissions":["Agrega protecci\u00f3n contra spam para bloquear env\u00edos comunes de bots"],"Get weekly email reports with a summary of form activity":["Recibe informes semanales por correo electr\u00f3nico con un resumen de la actividad del formulario"],"You're All Set! \ud83d\ude80":["\u00a1Todo listo! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Utiliza nuestro creador de formularios con IA para comenzar r\u00e1pidamente, o construye tu formulario desde cero si ya sabes lo que necesitas. Tus formularios est\u00e1n listos para crear, compartir y conectar con los visitantes de tu sitio."],"Final Touches That Make a Difference:":["Toques finales que marcan la diferencia:"],"Build Your First Form":["Construye tu primer formulario"],"File Uploads":["Subidas de archivos"],"Signature & Rating":["Firma y Calificaci\u00f3n"],"Calculation Forms":["Formularios de C\u00e1lculo"],"And Much More\u2026":["Y mucho m\u00e1s\u2026"],"Upgrade to Pro":["Actualiza a Pro"],"Unlock Premium Features":["Desbloquear funciones premium"],"Build Better Forms with SureForms":["Construye mejores formularios con SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Agrega campos avanzados, dise\u00f1os conversacionales y l\u00f3gica inteligente para crear formularios que involucren a los usuarios y capturen mejores datos."],"SureForms Video Thumbnail":["Miniatura de video de SureForms"],"Payments":["Pagos"],"Knowledge Base":["Base de Conocimientos"],"What\u2019s New":["Qu\u00e9 hay de nuevo"],"Importing\u2026":["Importando\u2026"],"Learn":["Aprender"],"Unable to complete action. Please try again.":["No se puede completar la acci\u00f3n. Por favor, int\u00e9ntelo de nuevo."],"Supercharge Your Workflow":["Potencia tu flujo de trabajo"],"Spam protection included":["Protecci\u00f3n contra spam incluida"],"Multistep Forms":["Formularios de varios pasos"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Env\u00eda las entradas de formularios instant\u00e1neamente a cualquier sistema o punto final externo para potenciar flujos de trabajo avanzados."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Convierte autom\u00e1ticamente las entradas de formularios en PDFs limpios y listos para descargar. Perfecto para registros, compartir, archivar o mantener las cosas organizadas."],"Set up confirmation messages and email notifications for each entry":["Configura mensajes de confirmaci\u00f3n y notificaciones por correo electr\u00f3nico para cada entrada"],"Something went wrong. Please try again.":["Algo sali\u00f3 mal. Por favor, int\u00e9ntalo de nuevo."],"Payment Forms":["Formularios de Pago"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Cobra pagos directamente a trav\u00e9s de tus formularios. Acepta pagos \u00fanicos y recurrentes sin problemas."],"SureForms %s":["SureForms %s"],"First name is required.":["Se requiere el nombre."],"Please enter a valid email address.":["Por favor, introduce una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida."],"Email address is required.":["Se requiere la direcci\u00f3n de correo electr\u00f3nico."],"This is required.":["Esto es necesario."],"Okay, just one last step\u2026":["Est\u00e1 bien, solo un \u00faltimo paso\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Ay\u00fadanos a personalizar tu experiencia con SureForms compartiendo un poco sobre ti."],"First Name":["Nombre"],"Enter your first name":["Ingrese su nombre"],"Last Name":["Apellido"],"Enter your last name":["Ingrese su apellido"],"Email Address":["Direcci\u00f3n de correo electr\u00f3nico"],"Enter your email address":["Introduce tu direcci\u00f3n de correo electr\u00f3nico"],"Privacy Policy":["Pol\u00edtica de Privacidad"],"Finish":["Terminar"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Mantente al tanto y ayuda a dar forma a SureForms. Recibe actualizaciones de funciones y ay\u00fadanos a mejorar SureForms compartiendo c\u00f3mo utilizas el complemento. Pol\u00edtica de Privacidad<\/a>."],"%d form ready":["%d formulario listo"],"%d already imported":["%d ya importados"],"(unnamed source)":["(fuente sin nombre)"],"All forms already imported":["Todos los formularios ya importados"],"Import forms":["Importar formularios"],"Import %d form":["Importar %d formulario"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Algo sali\u00f3 mal durante la importaci\u00f3n. Puedes reintentar, omitir o abrir Configuraci\u00f3n \u2192 Migraci\u00f3n."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["No se detectaron otros complementos de formularios. Puedes importar en cualquier momento desde Configuraci\u00f3n \u2192 Migraci\u00f3n."],"Forms imported":["Formularios importados"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["El formulario %1$d de %2$s est\u00e1 ahora en SureForms, listo para publicar, estilizar y conectar."],"%d form imported":["%d formulario importado"],"%d form could not be imported":["No se pudo importar %d formulario"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d tipo de campo no era compatible. Puedes reconstruirlo manualmente dentro de SureForms."],"Bring your existing forms with you":["Trae tus formularios existentes contigo"],"We detected forms in another plugin. Pick one to import into SureForms.":["Detectamos formularios en otro complemento. Elige uno para importar a SureForms."],"Choose a form plugin to import from":["Elige un complemento de formulario para importar desde"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["\u00bfImportando m\u00e1s de un complemento? Puedes importar fuentes adicionales m\u00e1s tarde desde Configuraci\u00f3n \u2192 Migraci\u00f3n."],"Import did not complete":["La importaci\u00f3n no se complet\u00f3"],"I'll do this later":["Lo har\u00e9 m\u00e1s tarde"],"Retry":["Reintentar"],"Form Checks":["Verificaciones de formulario"],"Collapse form checks":["Colapsar verificaciones de formulario"],"Expand form checks":["Expandir verificaciones de formulario"],"Ignore":["Ignorar"],"Working\u2026":["Trabajando\u2026"],"Details":["Detalles"],"Copy details":["Copiar detalles"]}}}
\ No newline at end of file
diff --git a/languages/sureforms-es_ES.mo b/languages/sureforms-es_ES.mo
index 9c23727d5e33f67696807207fdf75531ee20284b..0072c6ef4bbdc23604233ddad3b26290f8b60952 100644
GIT binary patch
delta 76373
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

Q6XD}+M_>3Wq00` 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 diff --git a/languages/sureforms-it_IT.po b/languages/sureforms-it_IT.po index 88645b0c2..fd054940a 100644 --- a/languages/sureforms-it_IT.po +++ b/languages/sureforms-it_IT.po @@ -14,9 +14,9 @@ msgstr "" #. Plugin Name of the plugin #. Author of the plugin #: sureforms.php -#: admin/admin.php:1152 -#: admin/admin.php:1153 -#: admin/admin.php:3021 +#: admin/admin.php:1198 +#: admin/admin.php:1199 +#: admin/admin.php:3185 #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 @@ -43,8 +43,8 @@ msgstr "Un modo semplice ma potente per creare moduli moderni per il tuo sito we msgid "https://sureforms.com/" msgstr "https://sureforms.com/" -#: admin/admin.php:1165 -#: admin/admin.php:1166 +#: admin/admin.php:1211 +#: admin/admin.php:1212 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -59,9 +59,9 @@ msgstr "https://sureforms.com/" msgid "Dashboard" msgstr "Cruscotto" -#: admin/admin.php:1183 -#: admin/admin.php:1184 -#: admin/admin.php:1616 +#: admin/admin.php:1229 +#: admin/admin.php:1230 +#: admin/admin.php:1662 #: inc/compatibility/multilingual/string-collector.php:236 #: inc/compatibility/multilingual/string-collector.php:245 #: inc/compatibility/multilingual/string-collector.php:313 @@ -79,15 +79,15 @@ msgstr "Cruscotto" msgid "Settings" msgstr "Impostazioni" -#: admin/admin.php:1452 -#: admin/admin.php:1453 +#: admin/admin.php:1498 +#: admin/admin.php:1499 #: inc/post-types.php:270 msgid "New Form" msgstr "Nuovo modulo" -#: admin/admin.php:1461 -#: admin/admin.php:1462 -#: admin/admin.php:3340 +#: admin/admin.php:1507 +#: admin/admin.php:1508 +#: admin/admin.php:3504 #: inc/generate-form-markup.php:211 #: inc/generate-form-markup.php:224 #: inc/global-settings/email-summary.php:225 @@ -105,7 +105,7 @@ msgid "Entries" msgstr "Voci" #. Translators: Post Title. -#: admin/admin.php:1704 +#: admin/admin.php:1750 #, php-format msgid "Edit %1$s" msgstr "Modifica %1$s" @@ -118,7 +118,7 @@ msgstr "Modifica %1$s" #: inc/duplicate-form.php:191 #: inc/export.php:126 #: inc/export.php:184 -#: inc/forms-data.php:88 +#: inc/forms-data.php:108 #: inc/global-settings/global-settings.php:89 #: inc/global-settings/global-settings.php:639 #: inc/rest-api.php:203 @@ -126,18 +126,18 @@ msgid "Nonce verification failed." msgstr "Verifica del nonce fallita." #. translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name. -#: admin/admin.php:2572 +#: admin/admin.php:2626 #, php-format msgid "Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more." msgstr "Per favore %1$sattiva%2$s la tua copia di %3$s per ottenere nuove funzionalità, accedere al supporto, ricevere notifiche di aggiornamento e altro ancora." #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag. -#: admin/admin.php:2590 +#: admin/admin.php:2644 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s." msgstr "SureForms %1$s richiede almeno %2$s %3$s per funzionare correttamente. Si prega di aggiornare all'ultima versione da %4$squi%5$s." -#: admin/admin.php:3339 +#: admin/admin.php:3503 #: inc/global-settings/email-summary.php:224 #: assets/build/entries.js:172 #: assets/build/payments.js:172 @@ -207,7 +207,7 @@ msgstr "Spazzatura" msgid "Published" msgstr "Pubblicato" -#: admin/admin.php:3331 +#: admin/admin.php:3495 msgid "View" msgstr "Visualizza" @@ -299,11 +299,11 @@ msgstr "Nessun plugin specificato" msgid "Plugin Successfully Activated" msgstr "Plugin attivato con successo" -#: admin/admin.php:1780 +#: admin/admin.php:1834 msgid "Activating..." msgstr "Attivazione in corso..." -#: admin/admin.php:1781 +#: admin/admin.php:1835 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/formEditor.js:172 @@ -318,17 +318,17 @@ msgstr "Attivazione in corso..." msgid "Activated" msgstr "Attivato" -#: admin/admin.php:1782 +#: admin/admin.php:1836 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Activate" msgstr "Attiva" -#: admin/admin.php:1783 +#: admin/admin.php:1837 msgid "Installing..." msgstr "Installazione in corso..." -#: admin/admin.php:1784 +#: admin/admin.php:1838 msgid "Installed" msgstr "Installato" @@ -336,12 +336,13 @@ msgstr "Installato" msgid "You do not have permission to access this page." msgstr "Non hai il permesso di accedere a questa pagina." -#: admin/admin.php:2784 -#: admin/admin.php:2912 -#: admin/admin.php:2950 -#: admin/admin.php:2971 -#: admin/admin.php:3294 -#: admin/admin.php:3601 +#: admin/admin.php:2874 +#: admin/admin.php:2917 +#: admin/admin.php:3076 +#: admin/admin.php:3114 +#: admin/admin.php:3135 +#: admin/admin.php:3458 +#: admin/admin.php:3847 #: inc/admin-ajax.php:164 #: inc/payments/admin/admin-handler.php:638 #: inc/payments/stripe/admin-stripe-handler.php:80 @@ -354,7 +355,7 @@ msgid "Form ID is required." msgstr "L'ID del modulo è obbligatorio." #: inc/admin-ajax.php:184 -#: inc/form-submit.php:1153 +#: inc/form-submit.php:1261 msgid "Invalid form ID." msgstr "ID modulo non valido." @@ -593,28 +594,28 @@ msgstr "Telefono" msgid "Textarea" msgstr "Area di testo" -#: inc/form-submit.php:231 +#: inc/form-submit.php:281 msgid "Cloudflare Turnstile secret key is invalid." msgstr "La chiave segreta di Cloudflare Turnstile non è valida." -#: inc/form-submit.php:238 +#: inc/form-submit.php:288 msgid "Cloudflare Turnstile response is missing." msgstr "Manca la risposta di Cloudflare Turnstile." -#: inc/form-submit.php:282 +#: inc/form-submit.php:332 msgid "hCaptcha secret key is invalid." msgstr "La chiave segreta di hCaptcha non è valida." -#: inc/form-submit.php:289 +#: inc/form-submit.php:339 msgid "hCaptcha response is missing." msgstr "Manca la risposta di hCaptcha." -#: inc/form-submit.php:330 -#: inc/form-submit.php:589 +#: inc/form-submit.php:380 +#: inc/form-submit.php:639 msgid "Form data is not found." msgstr "I dati del modulo non sono stati trovati." -#: inc/forms-data.php:68 +#: inc/forms-data.php:88 msgid "Sorry, you are not allowed to view the form." msgstr "Spiacente, non sei autorizzato a visualizzare il modulo." @@ -667,7 +668,7 @@ msgstr "Il valore deve essere unico." msgid "Sorry, you are not allowed to perform this action." msgstr "Spiacente, non ti è permesso eseguire questa azione." -#: admin/admin.php:2639 +#: admin/admin.php:2693 msgid "Rate SureForms" msgstr "Valuta SureForms" @@ -728,7 +729,7 @@ msgstr "Modifica modulo" msgid "Edit" msgstr "Modifica" -#: admin/admin.php:3072 +#: admin/admin.php:3236 #: inc/page-builders/elementor/form-widget.php:346 #: assets/build/dashboard.js:2 msgid "Create New Form" @@ -765,8 +766,8 @@ msgstr "Visualizza modulo" msgid "View Forms" msgstr "Visualizza moduli" -#: admin/admin.php:1443 -#: admin/admin.php:1444 +#: admin/admin.php:1489 +#: admin/admin.php:1490 #: inc/post-types.php:274 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 @@ -8280,7 +8281,7 @@ msgstr "Sociale" msgid "Travel" msgstr "Viaggio" -#: inc/helper.php:2375 +#: inc/helper.php:2368 msgid "Blank Form" msgstr "Modulo Vuoto" @@ -8599,7 +8600,9 @@ msgstr "Si prega di aggiungere un'opzione props a MultiButtonsControl" msgid "Icon Library" msgstr "Libreria di icone" +#: admin/admin.php:4062 #: assets/build/blocks.js:172 +#: assets/build/dashboard.js:172 #: modules/gutenberg/build/blocks.js:4 msgid "Close" msgstr "Chiudi" @@ -8888,9 +8891,9 @@ msgstr "Aggiungi blocchi alla barra delle azioni rapide" msgid "Re-arrange block inside Quick Action Bar" msgstr "Riorganizza il blocco all'interno della Barra delle Azioni Rapide" -#: admin/admin.php:1236 -#: admin/admin.php:1237 -#: admin/admin.php:4129 +#: admin/admin.php:1282 +#: admin/admin.php:1283 +#: admin/admin.php:4737 #: assets/build/blocks.js:172 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 @@ -9263,7 +9266,7 @@ msgstr "Centro" msgid "Right" msgstr "Destra" -#: inc/form-submit.php:1344 +#: inc/form-submit.php:1452 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Google reCAPTCHA" @@ -9273,7 +9276,7 @@ msgstr "Google reCAPTCHA" msgid "CloudFlare Turnstile" msgstr "CloudFlare Turnstile" -#: inc/form-submit.php:1348 +#: inc/form-submit.php:1456 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "hCaptcha" @@ -9569,7 +9572,7 @@ msgstr "Chiave del sito" msgid "Secret Key" msgstr "Chiave segreta" -#: inc/form-submit.php:1352 +#: inc/form-submit.php:1460 #: assets/build/settings.js:172 msgid "Cloudflare Turnstile" msgstr "Cloudflare Turnstile" @@ -9594,7 +9597,8 @@ msgstr "Connetti ora" msgid "Authentication Failed" msgstr "Autenticazione fallita" -#: admin/admin.php:3516 +#: admin/admin.php:4061 +#: assets/build/dashboard.js:172 #: assets/build/templatePicker.js:172 msgid "Contact Support" msgstr "Contatta il supporto" @@ -10566,18 +10570,18 @@ msgstr "Sblocca Aggiungi Nota" msgid "With the SureForms Starter plan, enhance your submitted form entries by adding personalized notes for better clarity and tracking." msgstr "Con il piano SureForms Starter, migliora le tue voci di modulo inviate aggiungendo note personalizzate per una maggiore chiarezza e tracciabilità." -#: inc/form-submit.php:990 +#: inc/form-submit.php:1046 msgid "Email notification passed to the sending server" msgstr "Notifica email passata al server di invio" #. translators: Here, %s is the comma separated emails list. -#: inc/form-submit.php:1063 +#: inc/form-submit.php:1130 #, php-format msgid "Email notification recipient: %s" msgstr "Destinatario della notifica email: %s" #. translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). -#: inc/form-submit.php:1080 +#: inc/form-submit.php:1147 #, php-format msgid "Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s" msgstr "Il server di posta elettronica non è riuscito a inviare la notifica email. Destinatario: %1$s. Motivo: %2$s" @@ -10620,7 +10624,7 @@ msgstr "Sblocca moduli conversazionali" msgid "With the SureForms Pro Plan, you can transform your forms into engaging conversational layouts for a seamless user experience." msgstr "Con il piano SureForms Pro, puoi trasformare i tuoi moduli in layout conversazionali coinvolgenti per un'esperienza utente senza interruzioni." -#: admin/admin.php:1075 +#: admin/admin.php:1121 msgid "Get SureForms Pro" msgstr "Ottieni SureForms Pro" @@ -10832,24 +10836,24 @@ msgstr "Seleziona sfumatura" msgid "reCAPTCHA" msgstr "reCAPTCHA" -#: inc/form-submit.php:1288 +#: inc/form-submit.php:1396 msgid "Captcha validation failed. No error code provided." msgstr "La validazione del captcha non è riuscita. Nessun codice di errore fornito." -#: inc/form-submit.php:1289 +#: inc/form-submit.php:1397 msgid "Captcha validation failed." msgstr "La validazione del captcha è fallita." -#: inc/form-submit.php:1356 +#: inc/form-submit.php:1464 msgid "Unknown Captcha" msgstr "Captcha sconosciuto" -#: inc/form-submit.php:1357 +#: inc/form-submit.php:1465 msgid "Invalid captcha type." msgstr "Tipo di captcha non valido." #. translators: %s is the captcha title. -#: inc/form-submit.php:1370 +#: inc/form-submit.php:1478 #, php-format msgid "%s verification failed. Please contact your site administrator." msgstr "La verifica di %s non è riuscita. Si prega di contattare l'amministratore del sito." @@ -11246,8 +11250,8 @@ msgstr "Descrivi il modulo che desideri creare" msgid "We are building your form…" msgstr "Stiamo creando il tuo modulo…" -#: admin/admin.php:1351 -#: admin/admin.php:1352 +#: admin/admin.php:1397 +#: admin/admin.php:1398 msgid "SMTP" msgstr "SMTP" @@ -11379,31 +11383,32 @@ msgstr "Abilita la Notifica Amministratore" msgid "Admin notifications keep you informed about new form entries since your last visit." msgstr "Le notifiche dell'amministratore ti tengono informato sui nuovi invii di moduli dalla tua ultima visita." -#: admin/admin.php:2789 -#: admin/admin.php:2908 -#: admin/admin.php:2946 -#: admin/admin.php:2967 -#: admin/admin.php:3298 -#: admin/admin.php:3596 +#: admin/admin.php:2869 +#: admin/admin.php:2922 +#: admin/admin.php:3072 +#: admin/admin.php:3110 +#: admin/admin.php:3131 +#: admin/admin.php:3462 +#: admin/admin.php:3842 msgid "Unauthorized user." msgstr "Utente non autorizzato." #. translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong -#: admin/admin.php:2917 +#: admin/admin.php:3081 #, php-format msgid "%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s" msgstr "%1$sInizia %2$scostruendo il tuo primo modulo%3$s.%4$s%5$sScopri la potenza del nostro intuitivo Costruttore di Moduli AI%6$s" -#: admin/admin.php:2928 +#: admin/admin.php:3092 msgid "SureForms is waiting for you!" msgstr "SureForms ti sta aspettando!" -#: admin/admin.php:2930 +#: admin/admin.php:3094 msgid "Build My First Form" msgstr "Crea il mio primo modulo" -#: admin/admin.php:2931 -#: admin/admin.php:3418 +#: admin/admin.php:3095 +#: admin/admin.php:3650 msgid "Dismiss" msgstr "Chiudi" @@ -11423,51 +11428,51 @@ msgstr "Accesso" msgid "Register" msgstr "Registrati" -#: admin/admin.php:3327 +#: admin/admin.php:3491 msgid "Recent Entries" msgstr "Voci recenti" -#: admin/admin.php:3328 +#: admin/admin.php:3492 msgid "( Last 7 days )" msgstr "( Ultimi 7 giorni )" -#: admin/admin.php:4070 +#: admin/admin.php:4678 msgid "Use Conditional Logic to show only what matters" msgstr "Usa la logica condizionale per mostrare solo ciò che conta" -#: admin/admin.php:4071 +#: admin/admin.php:4679 msgid "Split your form into steps to keep it easy" msgstr "Dividi il tuo modulo in passaggi per mantenerlo semplice" -#: admin/admin.php:4072 +#: admin/admin.php:4680 msgid "Let people upload files directly to your form" msgstr "Consenti alle persone di caricare file direttamente nel tuo modulo" -#: admin/admin.php:4073 +#: admin/admin.php:4681 msgid "Turn responses into downloadable PDFs automatically" msgstr "Trasforma automaticamente le risposte in PDF scaricabili" -#: admin/admin.php:4074 +#: admin/admin.php:4682 msgid "Let users sign with a simple signature field" msgstr "Consenti agli utenti di firmare con un semplice campo firma" -#: admin/admin.php:4075 +#: admin/admin.php:4683 msgid "Connect your form to other tools using webhooks" msgstr "Collega il tuo modulo ad altri strumenti utilizzando i webhook" -#: admin/admin.php:4076 +#: admin/admin.php:4684 msgid "Use Conversational Forms for a chat-like experience" msgstr "Usa moduli conversazionali per un'esperienza simile a una chat" -#: admin/admin.php:4077 +#: admin/admin.php:4685 msgid "Let users register or log in through your form" msgstr "Consenti agli utenti di registrarsi o accedere tramite il tuo modulo" -#: admin/admin.php:4078 +#: admin/admin.php:4686 msgid "Build forms that create WordPress user accounts" msgstr "Crea moduli che creano account utente WordPress" -#: admin/admin.php:4079 +#: admin/admin.php:4687 msgid "Add calculations to auto-total scores or prices" msgstr "Aggiungi calcoli per totalizzare automaticamente i punteggi o i prezzi" @@ -11666,6 +11671,7 @@ msgstr "Ottieni più controllo, flussi di lavoro più rapidi e una personalizzaz msgid "Selected features require %1$s - use code %2$s to get 10% off on any plan." msgstr "Le funzionalità selezionate richiedono %1$s - usa il codice %2$s per ottenere il 10% di sconto su qualsiasi piano." +#: admin/admin.php:4060 #: assets/build/dashboard.js:172 msgid "Copied" msgstr "Copiato" @@ -11889,7 +11895,7 @@ msgstr "Esplora SureDash" msgid "Something went wrong. We have logged the error for further investigation" msgstr "Qualcosa è andato storto. Abbiamo registrato l'errore per ulteriori indagini." -#: inc/field-validation.php:413 +#: inc/field-validation.php:452 msgid "Field is not valid." msgstr "Il campo non è valido." @@ -11957,8 +11963,8 @@ msgstr "Miniatura video di SureForms" msgid "Expected format for emails - email@sureforms.com or John Doe " msgstr "Formato previsto per le email - email@sureforms.com o John Doe " -#: admin/admin.php:1471 -#: admin/admin.php:1472 +#: admin/admin.php:1517 +#: admin/admin.php:1518 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -12173,7 +12179,7 @@ msgstr "Eleva il tuo SEO e scala le classifiche di ricerca senza sforzo!" msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress." msgstr "Aumenta la visibilità del tuo sito web con l'automazione intelligente della SEO. Ottimizza i contenuti, monitora le prestazioni delle parole chiave e ottieni informazioni utili, tutto all'interno di WordPress." -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -15372,12 +15378,12 @@ msgid "Stripe Settings" msgstr "Impostazioni di Stripe" #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. -#: admin/admin.php:2281 +#: admin/admin.php:2335 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version." msgstr "SureForms %1$s richiede almeno %2$s %3$s per funzionare correttamente. Si prega di aggiornare all'ultima versione." -#: admin/admin.php:2294 +#: admin/admin.php:2348 msgid "Update Now" msgstr "Aggiorna ora" @@ -15425,8 +15431,8 @@ msgstr "Posizione del simbolo di valuta" msgid "Select the position of the currency symbol relative to the amount." msgstr "Seleziona la posizione del simbolo della valuta rispetto all'importo." -#: admin/admin.php:1414 -#: admin/admin.php:1415 +#: admin/admin.php:1460 +#: admin/admin.php:1461 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -15440,41 +15446,41 @@ msgstr "Seleziona la posizione del simbolo della valuta rispetto all'importo." msgid "Learn" msgstr "Impara" -#: admin/admin.php:2635 +#: admin/admin.php:2689 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!" msgstr "Incredibile! SureForms sta alimentando i tuoi moduli e le tue sottomissioni - continuiamo a crescere insieme!" -#: admin/admin.php:2636 +#: admin/admin.php:2690 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?" msgstr "Se SureForms ti è stato utile, ti dispiacerebbe prendere un momento per lasciare una recensione a 5 stelle su WordPress.org?" -#: admin/admin.php:2647 -#: admin/admin.php:2711 +#: admin/admin.php:2701 +#: admin/admin.php:2765 msgid "Maybe later" msgstr "Forse più tardi" -#: admin/admin.php:2653 +#: admin/admin.php:2707 msgid "I already did" msgstr "L'ho già fatto" -#: admin/admin.php:2702 +#: admin/admin.php:2756 msgid "SureForms is ready to power your forms — explore what's possible!" msgstr "SureForms è pronto a potenziare i tuoi moduli — scopri cosa è possibile!" -#: admin/admin.php:2703 +#: admin/admin.php:2757 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard." msgstr "Gestisci i tuoi moduli, traccia le sottomissioni e scopri funzionalità come AI Form Builder, integrazioni di pagamento e altro ancora dalla dashboard di SureForms." -#: admin/admin.php:2706 +#: admin/admin.php:2760 msgid "Go to Dashboard" msgstr "Vai al cruscotto" -#: admin/admin.php:2717 +#: admin/admin.php:2771 msgid "I already know" msgstr "Lo so già" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2981 +#: admin/admin.php:3854 msgid "Invalid parameters." msgstr "Parametri non validi." @@ -15858,10 +15864,10 @@ msgid "This form is not yet available. Check back after the scheduled start time msgstr "Questo modulo non è ancora disponibile. Ricontrolla dopo l'orario di inizio programmato." #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -15884,48 +15890,48 @@ msgstr "Questo modulo non è ancora disponibile. Ricontrolla dopo l'orario di in msgid "Security verification failed. Please refresh the page and try again." msgstr "Verifica di sicurezza fallita. Si prega di aggiornare la pagina e riprovare." -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "I dati del modulo non sono stati trovati." -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "Manca l'ID del modulo." -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "Spiacente, non hai il permesso di accedere a questa risorsa." -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "Non hai il permesso di inviare questo modulo." -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "Questo modulo non è più disponibile." -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "Si prega di controllare il modulo per eventuali errori." -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "Il tuo invio è stato segnalato come spam. Per favore riprova." -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "Impossibile inviare il modulo. Per favore riprova." -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "Nessun plugin SMTP rilevato. Si prega di configurare un plugin SMTP per abilitare l'invio di email." -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "Invio email fallito per una ragione sconosciuta." -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "Nessuna email è stata inviata." @@ -16438,7 +16444,7 @@ msgid "Duplicate form?" msgstr "Duplicare il modulo?" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:938 msgid "An error occurred while submitting your form. Please try again." msgstr "Si è verificato un errore durante l'invio del modulo. Si prega di riprovare." @@ -16815,28 +16821,28 @@ msgstr "Segreto del webhook non configurato." msgid "Invalid webhook signature." msgstr "Firma del webhook non valida." -#: admin/admin.php:1253 -#: admin/admin.php:1851 +#: admin/admin.php:1299 +#: admin/admin.php:1905 #: assets/build/formEditor.js:172 msgid "Quizzes" msgstr "Quiz" -#: admin/admin.php:1252 +#: admin/admin.php:1298 msgid "Quiz Entries" msgstr "Voci del quiz" -#: admin/admin.php:1255 -#: admin/admin.php:1288 -#: admin/admin.php:1321 +#: admin/admin.php:1301 +#: admin/admin.php:1334 +#: admin/admin.php:1367 #: assets/build/settings.js:172 msgid "New" msgstr "Nuovo" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "Modulo non valido." -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "Troppe richieste. Si prega di riprovare a breve." @@ -17345,9 +17351,9 @@ msgstr "Perfetto per l'istruzione, la formazione e i quiz divertenti" msgid "Select this to create a quiz with scored questions and graded results." msgstr "Seleziona questo per creare un quiz con domande a punteggio e risultati valutati." -#: admin/admin.php:1285 -#: admin/admin.php:1286 -#: admin/admin.php:1856 +#: admin/admin.php:1331 +#: admin/admin.php:1332 +#: admin/admin.php:1910 msgid "Survey Reports" msgstr "Rapporti di indagine" @@ -17753,7 +17759,7 @@ msgid "Error creating SureForms Form." msgstr "Errore nella creazione del modulo SureForms." #. translators: %s represents the minimum number of characters required -#: inc/field-validation.php:426 +#: inc/field-validation.php:465 #: inc/translatable.php:94 #, php-format msgid "Please enter at least %s characters." @@ -17911,9 +17917,9 @@ msgstr "La risposta dell'IA era vuota. Per favore, perfeziona il tuo prompt e ri msgid "Unable to build form fields from the AI response." msgstr "Impossibile creare i campi del modulo dalla risposta dell'AI." -#: admin/admin.php:1318 -#: admin/admin.php:1319 -#: admin/admin.php:1861 +#: admin/admin.php:1364 +#: admin/admin.php:1365 +#: admin/admin.php:1915 msgid "Partial Entries" msgstr "Voci parziali" @@ -18784,23 +18790,23 @@ msgstr "L'importo del pagamento non può essere verificato per questo modulo. Mo msgid "Cancellation is not supported for this payment gateway." msgstr "La cancellazione non è supportata per questo gateway di pagamento." -#: admin/admin.php:3039 +#: admin/admin.php:3203 msgid "SureForms AI Quick Draft" msgstr "SureForms AI Bozza Rapida" -#: admin/admin.php:3058 +#: admin/admin.php:3222 msgid "Describe the form and let SureForms AI generate it for you." msgstr "Descrivi il modulo e lascia che SureForms AI lo generi per te." -#: admin/admin.php:3061 +#: admin/admin.php:3225 msgid "Describe your form" msgstr "Descrivi il tuo modulo" -#: admin/admin.php:3068 +#: admin/admin.php:3232 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "Esempio: Crea un modulo di contatto con campi per nome, email, telefono e messaggio." -#: admin/admin.php:3109 +#: admin/admin.php:3273 msgid "Redirecting..." msgstr "Reindirizzamento in corso..." @@ -18813,14 +18819,14 @@ msgid "The prompt is too long. Please shorten it and try again." msgstr "Il prompt è troppo lungo. Per favore accorcialo e riprova." #. translators: %s: maximum characters allowed before the @ symbol. -#: inc/field-validation.php:448 +#: inc/field-validation.php:487 #: inc/translatable.php:97 #, php-format msgid "The part before @ may not exceed %s characters." msgstr "La parte prima di @ non può superare %s caratteri." #. translators: %s: maximum characters allowed after the @ symbol. -#: inc/field-validation.php:454 +#: inc/field-validation.php:493 #: inc/translatable.php:100 #, php-format msgid "The part after @ may not exceed %s characters." @@ -18907,74 +18913,74 @@ msgstr "Spiacente, non sei autorizzato a visualizzare questo modulo." msgid "This form requires a payment. Please complete the payment and submit again." msgstr "Questo modulo richiede un pagamento. Si prega di completare il pagamento e inviare di nuovo." -#: admin/admin.php:552 +#: admin/admin.php:598 msgid "You are not allowed to update this prompt." msgstr "Non sei autorizzato ad aggiornare questo prompt." -#: admin/admin.php:595 +#: admin/admin.php:641 msgid "Finish setting up your form" msgstr "Completa la configurazione del tuo modulo" -#: admin/admin.php:627 +#: admin/admin.php:673 msgid "Review or edit your form" msgstr "Rivedi o modifica il tuo modulo" -#: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:674 +#: admin/admin.php:4515 msgid "Edit form" msgstr "Modifica modulo" -#: admin/admin.php:633 +#: admin/admin.php:679 msgid "Personalize the Thank You message" msgstr "Personalizza il messaggio di ringraziamento" -#: admin/admin.php:634 +#: admin/admin.php:680 msgid "Edit message" msgstr "Modifica messaggio" -#: admin/admin.php:639 +#: admin/admin.php:685 msgid "Choose who gets notified of new replies" msgstr "Scegli chi riceve notifiche di nuove risposte" -#: admin/admin.php:640 +#: admin/admin.php:686 msgid "Set up email" msgstr "Configura email" -#: admin/admin.php:648 +#: admin/admin.php:694 msgid "your form" msgstr "il tuo modulo" #. translators: %s: form title. -#: admin/admin.php:651 +#: admin/admin.php:697 #, php-format msgid "Finish setting up %s" msgstr "Completa la configurazione di %s" #. translators: %s: form title. -#: admin/admin.php:659 +#: admin/admin.php:705 #, php-format msgid "View %s (opens in a new tab)" msgstr "Visualizza %s (si apre in una nuova scheda)" -#: admin/admin.php:664 +#: admin/admin.php:710 msgid "Customize your form to get it ready for real submissions:" msgstr "Personalizza il tuo modulo per prepararlo a ricevere invii reali:" -#: admin/admin.php:3896 +#: admin/admin.php:4504 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "Abbiamo già creato questo modulo per te. Completa la personalizzazione in modo che sia pronto a raccogliere invii reali." #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4509 #, php-format msgid "Finish setting up “%s”" msgstr "Completa la configurazione di “%s”" -#: admin/admin.php:3914 +#: admin/admin.php:4522 msgid "Edit the Thank You message" msgstr "Modifica il messaggio di ringraziamento" -#: admin/admin.php:3920 +#: admin/admin.php:4528 msgid "Set where replies go" msgstr "Imposta dove vanno le risposte" @@ -19002,123 +19008,90 @@ msgstr "Per favore scegli un file immagine." msgid "This form could not be submitted. Please refresh the page and try again." msgstr "Non è stato possibile inviare questo modulo. Per favore aggiorna la pagina e riprova." -#: admin/admin.php:2341 -#: admin/admin.php:2430 +#: admin/admin.php:2395 +#: admin/admin.php:2484 msgid "Your SureForms database is up to date. New form entries will be saved as usual." msgstr "Il tuo database SureForms è aggiornato. Le nuove voci del modulo verranno salvate come al solito." -#: admin/admin.php:2355 -#: admin/admin.php:2439 +#: admin/admin.php:2409 +#: admin/admin.php:2493 #: inc/rest-api.php:1447 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support." msgstr "SureForms non è riuscito a completare l'aggiornamento del database. Il tuo hosting potrebbe non consentire a SureForms di creare tabelle nel database — contatta il tuo provider di hosting o il supporto di SureForms." -#: admin/admin.php:2358 +#: admin/admin.php:2412 msgid "Contact support" msgstr "Contatta il supporto" -#: admin/admin.php:2378 +#: admin/admin.php:2432 msgid "Database update needed" msgstr "Aggiornamento del database necessario" -#: admin/admin.php:2384 +#: admin/admin.php:2438 msgid "Fix now" msgstr "Correggi ora" -#: admin/admin.php:2453 +#: admin/admin.php:2507 msgid "SureForms — database update needed" msgstr "SureForms — aggiornamento del database necessario" -#: admin/admin.php:2458 +#: admin/admin.php:2512 msgid "Update database" msgstr "Aggiorna il database" -#: admin/admin.php:2478 +#: admin/admin.php:2532 msgid "You do not have permission to update the database." msgstr "Non hai il permesso di aggiornare il database." -#: admin/admin.php:3437 +#: admin/admin.php:3669 msgid "You do not have permission to do this." msgstr "Non hai il permesso di fare questo." -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "%d invio del modulo consecutivo non è stato completato." -msgstr[1] "" - -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." -msgstr "I visitatori potrebbero non riuscire a contattarti e quelle voci non sono state salvate." - -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." -msgstr "Gli invii dei moduli vengono completati normalmente." - -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." -msgstr "Non è stato trovato alcun plugin di caching che necessita di configurazione." - #. translators: %s: caching plugin name. -#: admin/admin.php:3551 +#: admin/admin.php:4209 msgid "%s may interfere with your forms." msgstr "%s potrebbe interferire con i tuoi moduli." -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." -msgstr "Il caching e l'ottimizzazione JavaScript possono servire una copia obsoleta del tuo modulo o caricare i suoi script in modo disordinato." - -#: admin/admin.php:3555 +#: admin/admin.php:4185 +#: admin/admin.php:4213 msgid "Help Me Fix" msgstr "Aiutami a risolvere" -#: admin/admin.php:3650 +#: admin/admin.php:4258 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "SureForms ha trovato le tue voci del modulo memorizzate con un prefisso di tabella del database diverso. Riconnetterle richiede un momento e le tue voci esistenti verranno mantenute." -#: admin/admin.php:3653 +#: admin/admin.php:4261 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "SureForms deve aggiornare il tuo database prima di poter salvare nuove voci del modulo. Questo richiede solo un momento e non cambierà i tuoi moduli o il contenuto esistente. Le voci inviate prima di ora non possono essere recuperate da qui." #. translators: %s: site host. -#: admin/admin.php:4265 +#: admin/admin.php:5032 msgid "SureForms: form submissions are failing on %s" msgstr "SureForms: gli invii dei moduli stanno fallendo su %s" -#: admin/admin.php:4274 +#: admin/admin.php:4990 msgid "Debug log: no entries recorded." msgstr "Registro di debug: nessuna voce registrata." #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4995 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "Registro di debug (le più recenti %1$d di %2$d voci)" -#: admin/admin.php:4288 -msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." -msgstr "Le voci più vecchie sono state omesse per mantenere questa email entro la lunghezza accettata da un client di posta. Il registro completo può essere scaricato da SureForms → Impostazioni → Generale." - -#: admin/admin.php:4318 +#: admin/admin.php:5158 msgid "Hello SureForms support," msgstr "Ciao supporto SureForms," -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." -msgstr[0] "SureForms ha registrato %d invio del modulo consecutivo che non è stato completato." -msgstr[1] "" - -#: admin/admin.php:4332 +#: admin/admin.php:5182 msgid "Site details" msgstr "Dettagli del sito" -#: admin/admin.php:4335 +#: admin/admin.php:5185 msgid "not active" msgstr "non attivo" -#: admin/admin.php:4338 +#: admin/admin.php:5188 msgid "none detected" msgstr "nessuno rilevato" @@ -19126,7 +19099,7 @@ msgstr "nessuno rilevato" msgid "No form submission failures have been recorded." msgstr "Non sono stati registrati fallimenti nell'invio del modulo." -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "Non trovato." @@ -19211,6 +19184,193 @@ msgstr "Visualizzazioni e Conversione Moduli" msgid "Logs" msgstr "Registri" +#: admin/admin.php:2831 +msgid "Previous notice" +msgstr "Avviso precedente" + +#: admin/admin.php:2832 +msgid "Next notice" +msgstr "Avviso successivo" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2834 +msgid "%1$d of %2$d" +msgstr "%1$d di %2$d" + +#: admin/admin.php:2891 +msgid "Nothing to report." +msgstr "Niente da segnalare." + +#: admin/admin.php:4057 +#: assets/build/dashboard.js:172 +msgid "Details" +msgstr "Dettagli" + +#: admin/admin.php:4058 +msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it." +msgstr "Cosa abbiamo registrato su questo problema. Copialo nella tua richiesta di supporto in modo che possiamo partire dalla causa piuttosto che da una sua descrizione." + +#: admin/admin.php:4059 +#: assets/build/dashboard.js:172 +msgid "Copy details" +msgstr "Copia dettagli" + +#: admin/admin.php:4068 +msgid "Copy the details first, so you have them to paste." +msgstr "Copia prima i dettagli, così li hai da incollare." + +#: admin/admin.php:4072 +msgid "Copied. Contact Support is now available." +msgstr "Copiato. Contatta il Supporto è ora disponibile." + +#: admin/admin.php:4073 +msgid "Your browser would not let us copy. Select the text above and copy it by hand." +msgstr "Il tuo browser non ci ha permesso di copiare. Seleziona il testo sopra e copialo manualmente." + +#: admin/admin.php:4076 +msgid "Recorded diagnostics" +msgstr "Diagnostica registrata" + +#: admin/admin.php:4080 +msgid "Collecting the details…" +msgstr "Raccolta dei dettagli in corso…" + +#: admin/admin.php:4081 +msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there." +msgstr "Non siamo riusciti a raccogliere i dettagli. Contatta il Supporto e descrivi cosa è successo, e da lì ci occuperemo noi." + +#. translators: %s: form title. +#: admin/admin.php:4108 +msgid "We noticed a form submission failure on %s." +msgstr "Abbiamo notato un errore di invio del modulo su %s." + +#: admin/admin.php:4109 +msgid "We noticed a form submission failure." +msgstr "Abbiamo notato un errore di invio del modulo." + +#: admin/admin.php:4110 +msgid "Visitors may not be able to reach you, and their entries were not saved." +msgstr "I visitatori potrebbero non essere in grado di contattarti e le loro voci non sono state salvate." + +#. translators: %s: form title. +#: admin/admin.php:4115 +msgid "We noticed a notification failure on %s." +msgstr "Abbiamo notato un errore di notifica su %s." + +#: admin/admin.php:4116 +msgid "We noticed a notification failure." +msgstr "Abbiamo notato un errore di notifica." + +#: admin/admin.php:4117 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "La voce è stata salvata, ma non siamo riusciti a inviare l'email a riguardo. Potrebbero arrivare nuove voci senza che tu lo sappia." + +#. translators: %s: form title. +#: admin/admin.php:4132 +msgid "We noticed an integration failure on %s." +msgstr "Abbiamo notato un errore di integrazione su %s." + +#: admin/admin.php:4133 +msgid "We noticed an integration failure." +msgstr "Abbiamo notato un errore di integrazione." + +#: admin/admin.php:4134 +msgid "The entry was saved, but we could not send it to a connected service." +msgstr "La voce è stata salvata, ma non siamo riusciti a inviarla a un servizio connesso." + +#: admin/admin.php:4158 +msgid "View details" +msgstr "Visualizza dettagli" + +#: admin/admin.php:4212 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "La cache può mostrare ai visitatori una copia vecchia del tuo modulo o caricare i suoi script nell'ordine sbagliato." + +#: admin/admin.php:5005 +msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General." +msgstr "Le voci più vecchie sono state omesse per mantenere leggibile questo estratto. Il log completo può essere scaricato da SureForms → Impostazioni → Generale." + +#. translators: %s: site host. +#: admin/admin.php:5034 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "SureForms ha registrato invii di moduli su %s che non sono stati completati." + +#. translators: %s: site host. +#: admin/admin.php:5038 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "SureForms: le email di notifica non vengono inviate su %s" + +#. translators: %s: site host. +#: admin/admin.php:5040 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "SureForms ha salvato le voci su %s ma non è riuscito a inviare le email di notifica per esse." + +#. translators: %s: site host. +#: admin/admin.php:5044 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "SureForms: un'integrazione non sta ricevendo voci su %s" + +#. translators: %s: site host. +#: admin/admin.php:5046 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "SureForms ha salvato le voci su %s ma non è riuscito a passarle a un servizio connesso." + +#. translators: %s: site host. +#: admin/admin.php:5056 +msgid "SureForms: a problem with the forms on %s" +msgstr "SureForms: un problema con i moduli su %s" + +#. translators: %s: site host. +#: admin/admin.php:5058 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "SureForms ha registrato un problema con i moduli su %s." + +#. translators: %d: number of failed submissions. +#: admin/admin.php:5083 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." +msgstr[0] "SureForms ha registrato %d invio di modulo che non è stato completato." +msgstr[1] "" + +#. translators: %d: number of failed notifications. +#: admin/admin.php:5095 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "SureForms ha salvato %d voce ma non è riuscito a inviare l'email di notifica per essa." +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:5107 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "SureForms ha salvato %d voce ma non è riuscito a passarla a un servizio connesso." +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:5119 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "SureForms ha registrato %d problema con i moduli su questo sito." +msgstr[1] "" + +#. translators: %s: form title. +#: admin/admin.php:5169 +msgid "Form: %s" +msgstr "Modulo: %s" + +#: admin/admin.php:5189 +msgid "none recorded" +msgstr "nessuno registrato" + +#. translators: %s: date and time of the previous report, in the site's timezone. +#: admin/admin.php:5212 +msgid "Previously reported: %s" +msgstr "Segnalato in precedenza: %s" + +#: assets/build/settings.js:172 +msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures." +msgstr "Le visualizzazioni e gli invii da parte di chiunque possa modificare il sito sono normalmente omessi, quindi testare i propri moduli non modifica queste cifre." + #: inc/post-types.php:265 msgctxt "post type general name" msgid "Forms" diff --git a/languages/sureforms-nl_NL-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-nl_NL-51635fe6489fc8288d603fe596c755ca.json index 01259ca62..6cab43be8 100644 --- a/languages/sureforms-nl_NL-51635fe6489fc8288d603fe596c755ca.json +++ b/languages/sureforms-nl_NL-51635fe6489fc8288d603fe596c755ca.json @@ -1 +1 @@ -{"translation-revision-date":"2024-12-13T12:33:48+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Dashboard"],"Settings":["Instellingen"],"Entries":["Inzendingen"],"Status":["Status"],"Form":["Formulier"],"Activated":["Geactiveerd"],"Activate":["Activeren"],"Address":["Adres"],"Checkbox":["Selectievakje"],"Dropdown":["Keuzemenu"],"Email":["E-mail"],"Number":["Nummer"],"Phone":["Telefoon"],"Textarea":["Tekstvak"],"Monday":["Maandag"],"Forms":["Formulieren"],"New Form Submission - %s":["Nieuwe formulierinzending - %s"],"GitHub":["GitHub"],"(no title)":["(geen titel)"],"General":["Algemeen"],"No tags available":["Geen tags beschikbaar"],"Back":["Terug"],"Generic tags":["Algemene tags"],"Other":["Anders"],"Confirm":["Bevestigen"],"Cancel":["Annuleren"],"Install":["Installeren"],"Plugin Installation failed, Please try again later.":["Installatie van de plugin mislukt, probeer het later opnieuw."],"Plugin activation failed, Please try again later.":["Plug-in activering mislukt, probeer het later opnieuw."],"Integrations":["Integraties"],"What's New?":["Wat is er nieuw?"],"Core":["Kern"],"Unlicensed":["Ongeautoriseerd"],"Connecting\u2026":["Verbinden\u2026"],"Install & Activate":["Installeren & Activeren"],"Compliance Settings":["Instellingen voor naleving"],"Enable GDPR Compliance":["Schakel GDPR-naleving in"],"Never store entry data after form submission":["Sla nooit invoergegevens op na het indienen van het formulier"],"When enabled this form will never store Entries.":["Wanneer ingeschakeld, zal dit formulier nooit inzendingen opslaan."],"Automatically delete entries":["Automatisch items verwijderen"],"When enabled this form will automatically delete entries after a certain period of time.":["Wanneer ingeschakeld, zal dit formulier automatisch inzendingen na een bepaalde periode verwijderen."],"Entries older than the days set will be deleted automatically.":["Inzendingen ouder dan de ingestelde dagen worden automatisch verwijderd."],"Visual":["Visueel"],"HTML":["HTML"],"All Data":["Alle gegevens"],"Add Shortcode":["Voeg shortcode toe"],"Form input tags":["Formulierveldlabels"],"Comma separated values are also accepted.":["Door komma's gescheiden waarden worden ook geaccepteerd."],"Email Notification":["E-mailmelding"],"Name":["Naam"],"Send Email To":["E-mail verzenden naar"],"Subject":["Onderwerp"],"CC":["CC"],"BCC":["BCC"],"Reply To":["Antwoord aan"],"Add Key":["Sleutel toevoegen"],"Add Value":["Waarde toevoegen"],"Add":["Toevoegen"],"Confirmation Message":["Bevestigingsbericht"],"After Form Submission":["Na het indienen van het formulier"],"Hide Form":["Formulier verbergen"],"Reset Form":["Formulier opnieuw instellen"],"Custom URL":["Aangepaste URL"],"Add Query Parameters":["Queryparameters toevoegen"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Selecteer of u sleutel-waardeparen wilt toevoegen voor formuliervelden om op te nemen in queryparameters"],"Query Parameters":["Queryparameters"],"Success Message":["Succesbericht"],"Redirect":["Doorsturen"],"Redirect to":["Doorsturen naar"],"Page":["Pagina"],"Form Confirmation":["Formulierbevestiging"],"Confirmation Type":["Bevestigingstype"],"Clear":["Duidelijk"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Onzichtbaar"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validaties"],"Spam Protection":["Spam Bescherming"],"If this option is turned on, the user's IP address will be saved with the form data":["Als deze optie is ingeschakeld, wordt het IP-adres van de gebruiker samen met de formuliergegevens opgeslagen"],"Enable Honeypot Security":["Honeypot-beveiliging inschakelen"],"Enable Honeypot Security for better spam protection":["Schakel Honeypot-beveiliging in voor betere spambeveiliging"],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s vertegenwoordigt het minimum aantal selecties dat nodig is. Bijvoorbeeld: \"Minimaal 2 selecties zijn vereist.\""],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s vertegenwoordigt het maximale aantal toegestane selecties. Bijvoorbeeld: \"Maximaal 4 selecties zijn toegestaan.\""],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s vertegenwoordigt de minimale keuzes die nodig zijn. Bijvoorbeeld: \"Minimaal 1 selectie is vereist.\""],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s vertegenwoordigt het maximale aantal toegestane keuzes. Bijvoorbeeld: \"Maximaal 3 selecties zijn toegestaan.\""]," Error Message":["Foutmelding"],"Email Summaries":["E-mailoverzichten"],"Tuesday":["Dinsdag"],"Wednesday":["Woensdag"],"Thursday":["Donderdag"],"Friday":["Vrijdag"],"Saturday":["Zaterdag"],"Sunday":["Zondag"],"Schedule Reports":["Rapporten plannen"],"Auto":["Auto"],"Light":["Licht"],"Dark":["Donker"],"Turnstile":["Draaikruis"],"Get Keys":["Verkrijg sleutels"],"Documentation":["Documentatie"],"Site Key":["Sitecode"],"Secret Key":["Geheime Sleutel"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Weergavemodus"],"Text":["Tekst"],"Test Email":["Test e-mail"],"IP Logging":["IP-logboek"],"Honeypot":["Honeypot"],"Confirmation Email Mismatch Message":["Bevestigingsmail komt niet overeen bericht"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s vertegenwoordigt de minimale invoerwaarde. Bijvoorbeeld: \"Minimale waarde is 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s vertegenwoordigt de maximale invoerwaarde. Bijvoorbeeld: \"Maximale waarde is 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Verbind met OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Het huidige 'Van E-mailadres' komt niet overeen met de domeinnaam van uw website (%1$s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd. Probeer in plaats daarvan een Van-adres te gebruiken dat overeenkomt met uw website-domein (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Het huidige 'Van E-mailadres' komt niet overeen met de domeinnaam van uw website (%s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd."],"We strongly recommend that you install the free ":["We raden ten zeerste aan dat u de gratis installeert"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin! De Setup Wizard maakt het gemakkelijk om je e-mails te repareren."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Probeer afwisselend een Van-adres te gebruiken dat overeenkomt met uw website-domein (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Voer een geldig e-mailadres in. Uw meldingen worden niet verzonden als het veld niet correct is ingevuld."],"From Name":["Van Naam"],"From Email":["Van e-mail"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Het huidige 'Van E-mailadres' komt mogelijk niet overeen met de domeinnaam van uw website (%1$s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd. Probeer in plaats daarvan een Van-adres te gebruiken dat overeenkomt met uw website domein (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Het huidige 'Van E-mailadres' komt mogelijk niet overeen met de domeinnaam van uw website (%s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Klaar om verder te gaan dan het gratis plan?"],"Upgrade now":["Nu upgraden"],"and unlock the full power of SureForms!":["en ontgrendel de volledige kracht van SureForms!"],"Upgrade SureForms":["Upgrade SureForms"],"Upgrade Now":["Nu upgraden"],"Entries older than the selected days will be deleted.":["Inzendingen ouder dan de geselecteerde dagen worden verwijderd."],"Entries Time Period":["Invoerperiode"],"Notifications can use only one From Email so please enter a single address.":["Meldingen kunnen slechts \u00e9\u00e9n Van E-mailadres gebruiken, dus voer alstublieft \u00e9\u00e9n enkel adres in."],"Select Page to redirect":["Selecteer pagina om te omleiden"],"Search for a page":["Zoek naar een pagina"],"Select a page":["Selecteer een pagina"],"Form Validation":["Formuliercontrole"],"Required Error Messages":["Vereiste foutmeldingen"],"Other Error Messages":["Andere foutmeldingen"],"Input Field Unique":["Uniek invoerveld"],"Email Field Unique":["E-mailveld uniek"],"Invalid URL":["Ongeldige URL"],"Phone Field Unique":["Telefoonveld Uniek"],"Invalid Field Number Block":["Ongeldige Veldnummer Blok"],"Invalid Email":["Ongeldig e-mailadres"],"Number Minimum Value":["Nummer Minimumwaarde"],"Number Maximum Value":["Maximale waarde van het nummer"],"Dropdown Minimum Selections":["Minimale selecties in dropdown"],"Dropdown Maximum Selections":["Maximale selecties in dropdown"],"Multiple Choice Minimum Selections":["Meerdere keuzes minimumselecties"],"Multiple Choice Maximum Selections":["Meerdere Keuzes Maximale Selecties"],"Input Field":["Invoerveld"],"Email Field":["E-mailveld"],"URL Field":["URL-veld"],"Phone Field":["Telefoonveld"],"Textarea Field":["Tekstvakveld"],"Checkbox Field":["Selectievakjeveld"],"Dropdown Field":["Keuzelijstveld"],"Multiple Choice Field":["Meerkeuzeveld"],"Address Field":["Adresveld"],"Number Field":["Nummer veld"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Om de reCAPTCHA-functie op uw SureForms in te schakelen, schakelt u de reCAPTCHA-optie in uw blokinstellingen in en selecteert u de versie. Voeg hier de Google reCAPTCHA-secret en site key toe. reCAPTCHA wordt aan uw pagina toegevoegd aan de voorkant."],"Enter your %s here":["Voer hier uw %s in"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Om hCAPTCHA in te schakelen, voeg uw site-sleutel en geheime sleutel toe. Configureer deze instellingen binnen het individuele formulier."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Om Cloudflare Turnstile in te schakelen, voeg uw site-sleutel en geheime sleutel toe. Configureer deze instellingen binnen het individuele formulier."],"Save":["Opslaan"],"Anonymous Analytics":["Anonieme Analytics"],"Learn More":["Meer informatie"],"Admin Notification":["Beheerdersmelding"],"Enable Admin Notification":["Beheerdermelding inschakelen"],"Admin notifications keep you informed about new form entries since your last visit.":["Beheerdersmeldingen houden je op de hoogte van nieuwe formulierinzendingen sinds je laatste bezoek."],"Skip":["Overslaan"],"Continue":["Doorgaan"],"Maximum Number of Entries":["Maximaal aantal vermeldingen"],"Maximum Entries":["Maximale invoer"],"Response Description After Maximum Entries":["Beschrijving van de reactie na maximale invoer"],"Get Started":["Beginnen"],"Integration":["Integratie"],"Connect Native Integrations with SureForms":["Verbind native integraties met SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Ontgrendel krachtige integraties in het Premium-plan om je workflows te automatiseren en SureForms direct te verbinden met je favoriete tools."],"Send form submissions straight to CRMs, email, and marketing platforms":["Stuur formulierinzendingen rechtstreeks naar CRM's, e-mail en marketingplatforms"],"Automate repetitive tasks with seamless data syncing":["Automatiseer repetitieve taken met naadloze gegevenssynchronisatie"],"Access exclusive native integrations for faster workflows":["Toegang tot exclusieve native integraties voor snellere workflows"],"Expected format for emails - email@sureforms.com or John Doe ":["Verwacht formaat voor e-mails - email@sureforms.com of John Doe "],"Payments":["Betalingen"],"Stripe account disconnected successfully.":["Stripe-account succesvol losgekoppeld."],"Failed to create webhook.":["Het is niet gelukt om de webhook te maken."],"Failed to connect to Stripe.":["Verbinding met Stripe mislukt."],"Webhook":["Webhook"],"Knowledge Base":["Kennisbank"],"What\u2019s New":["Wat is er nieuw"],"out of":["uit"],"No entries found":["Geen items gevonden"],"delete":["verwijderen"],"Please type \"%s\" in the input box":["Typ alstublieft \"%s\" in het invoerveld"],"To confirm, type \"%s\" in the box below:":["Om te bevestigen, typ \"%s\" in het vak hieronder:"],"Type \"%s\"":["Typ \"%s\""],"Go to OttoKit Settings":["Ga naar OttoKit-instellingen"],"USD - US Dollar":["USD - Amerikaanse dollar"],"Payment Mode":["Betaalwijze"],"Test Mode":["Testmodus"],"Live Mode":["Live-modus"],"Action":["Actie"],"General Settings":["Algemene instellingen"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Stel e-mailsamenvattingen, beheerderswaarschuwingen en gegevensvoorkeuren in om uw formulieren eenvoudig te beheren."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Pas standaardfoutmeldingen aan die worden weergegeven wanneer gebruikers ongeldige of onvolledige formulierinvoer indienen."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Schakel spambeveiliging voor uw formulieren in met behulp van CAPTCHA-diensten of honeypot-beveiliging."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Verbind en beheer uw betalingsgateways om veilig transacties via uw formulieren te accepteren."],"1% transaction and payment gateway fees apply.":["1% transactiekosten en kosten voor de betalingsgateway zijn van toepassing."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["2,9% transactiekosten en kosten voor de betalingsgateway zijn van toepassing. Activeer de licentie om de transactiekosten te verlagen."],"2.9% transaction and payment gateway fees apply.":["Er zijn 2,9% transactiekosten en kosten voor de betalingsgateway van toepassing."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Bezoek %1$s, verwijder een ongebruikte webhook en klik vervolgens hieronder om het opnieuw te proberen."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms kon geen webhook maken omdat je Stripe-account geen gratis slots meer heeft. Webhooks zijn nodig om updates over betalingen te ontvangen."],"Stripe Dashboard":["Stripe-dashboard"],"Creating\u2026":["Aan het cre\u00ebren\u2026"],"Create Webhook":["Webhook maken"],"Successfully connected to Stripe!":["Succesvol verbonden met Stripe!"],"Invalid response from server. Please try again.":["Ongeldig antwoord van de server. Probeer het alstublieft opnieuw."],"Failed to disconnect Stripe account.":["Het is niet gelukt om de Stripe-account los te koppelen."],"Webhook created successfully!":["Webhook succesvol aangemaakt!"],"Select Currency":["Selecteer valuta"],"Select the default currency for payment forms.":["Selecteer de standaardvaluta voor betalingsformulieren."],"Connection Status":["Verbindingsstatus"],"Disconnect Stripe Account":["Stripe-account loskoppelen"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Weet u zeker dat u uw Stripe-account wilt loskoppelen? Dit zal alle actieve betalingen, abonnementen en formuliertransacties die aan dit account zijn gekoppeld, stoppen."],"Disconnect":["Verbreken"],"Disconnecting\u2026":["Verbinding verbreken\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook succesvol verbonden, alle Stripe-evenementen worden gevolgd."],"Connect your Stripe account to start accepting payments through your forms.":["Verbind uw Stripe-account om betalingen via uw formulieren te accepteren."],"Connect to Stripe":["Verbinden met Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Maak veilig verbinding met Stripe met slechts een paar klikken om betalingen te accepteren!"],"Set the total number of submissions allowed for this form.":["Stel het totale aantal toegestane inzendingen voor dit formulier in."],"Payment Methods":["Betaalmethoden"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Met de testmodus kunt u betalingen verwerken zonder echte kosten. Schakel over naar de Live-modus voor daadwerkelijke transacties."],"General Payment Settings":["Algemene betalingsinstellingen"],"These settings apply to all payment gateways.":["Deze instellingen zijn van toepassing op alle betalingsgateways."],"Stripe Settings":["Stripe-instellingen"],"Left ($100)":["Links ($100)"],"Right (100$)":["Rechts (100$)"],"Left Space ($ 100)":["Linker ruimte ($ 100)"],"Right Space (100 $)":["Rechterruimte (100 $)"],"Currency Sign Position":["Positie van het valutateken"],"Select the position of the currency symbol relative to the amount.":["Selecteer de positie van het valutasymbool ten opzichte van het bedrag."],"Learn":["Leren"],"Enable email summaries":["E-mailoverzichten inschakelen"],"Enable IP logging":["IP-logboek inschakelen"],"Turn on Admin Notification from here.":["Zet hier de beheerdersmelding aan."],"New":["Nieuw"],"Send entries to 100+ popular apps.":["Verzend inzendingen naar meer dan 100 populaire apps."],"Build automated workflows that run instantly.":["Bouw geautomatiseerde workflows die direct worden uitgevoerd."],"Create custom app integrations using our Custom App feature.":["Maak aangepaste app-integraties met behulp van onze functie Aangepaste App."],"Keep your tools in sync automatically.":["Houd je gereedschap automatisch gesynchroniseerd."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Hiermee wordt OttoKit op je WordPress-site ge\u00efnstalleerd en geactiveerd om automatiseringsfuncties mogelijk te maken."],"Automate Your Forms with OttoKit":["Automatiseer uw formulieren met OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Elke formulierinzending zou iets moeten activeren \u2014 een Slack-melding, een CRM-lead, een opvolg-e-mail of een nieuwe rij in Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configureer AI-clientmachtigingen en MCP-serverinstellingen."],"View documentation":["Documentatie bekijken"],"Copy to clipboard":["Kopi\u00ebren naar klembord"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) of %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Claude Code"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (project) of ~\/.claude.json (globaal)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (project) of settings.json > mcp.servers (globaal)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml of config.json"],"Your client's MCP configuration file":["Het MCP-configuratiebestand van uw klant"],"Connect Your AI Client":["Verbind uw AI-client"],"AI Client":["AI-client"],"Create an Application Password \u2014 ":["Maak een applicatiewachtwoord aan \u2014"],"Open Application Passwords":["Open toepassingswachtwoorden"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Of gebruik deze CLI-opdracht om de server snel toe te voegen (je moet nog steeds de omgevingsvariabelen instellen):"],"Copy the JSON config below into: ":["Kopieer de JSON-configuratie hieronder naar:"],"Replace \"your-application-password\" with the password from Step 1.":["Vervang \"your-application-password\" door het wachtwoord uit Stap 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 het MCP-eindpunt van je site. WP_API_USERNAME \u2014 je WordPress-gebruikersnaam. WP_API_PASSWORD \u2014 het applicatiewachtwoord dat je hebt gegenereerd."],"View setup docs":["Bekijk de installatiehandleidingen"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["De MCP Adapter-plugin is ge\u00efnstalleerd maar niet actief. Activeer het om MCP-instellingen te configureren."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["De MCP Adapter-plugin is vereist om AI-clients met uw formulieren te verbinden. Download en installeer het vanaf GitHub en activeer het vervolgens."],"Download the latest release from":["Download de nieuwste release van"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installeer de plugin via Plugins > Nieuwe plugin toevoegen > Plugin uploaden."],"Activate the MCP Adapter plugin.":["Activeer de MCP Adapter-plugin."],"Activating\u2026":["Activeren\u2026"],"Activate MCP Adapter":["Activeer MCP-adapter"],"Download MCP Adapter":["Download MCP Adapter"],"Experimental":["Experimenteel"],"Enable Abilities":["Vermogen inschakelen"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registreer SureForms-mogelijkheden bij de WordPress Abilities API. Wanneer ingeschakeld, kunnen AI-clients uw formulieren en inzendingen opsommen, lezen, maken, bewerken en verwijderen. Wanneer uitgeschakeld, worden er geen mogelijkheden geregistreerd en kunnen AI-clients geen acties uitvoeren op uw formulieren."],"Abilities API \u2014 Edit":["Vaardigheden-API \u2014 Bewerken"],"Enable Edit Abilities":["Bewerkingsmogelijkheden inschakelen"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Wanneer ingeschakeld, kunnen AI-clients nieuwe formulieren maken, formuliertitels, velden en instellingen bijwerken, formulieren dupliceren en de status van inzendingen wijzigen. Wanneer uitgeschakeld, worden deze mogelijkheden uitgeschakeld en kunnen AI-clients alleen uw gegevens lezen."],"Abilities API \u2014 Delete":["Vaardigheden API \u2014 Verwijderen"],"Enable Delete Abilities":["Verwijdermogelijkheden inschakelen"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Wanneer ingeschakeld, kunnen AI-clients formulieren en invoer permanent verwijderen. Verwijderde gegevens kunnen niet worden hersteld. Wanneer uitgeschakeld, worden verwijdermogelijkheden uitgeschakeld en kunnen AI-clients geen gegevens verwijderen."],"MCP Server":["MCP-server"],"Enable MCP Server":["MCP-server inschakelen"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Maakt een speciale SureForms MCP-eindpunt aan waarmee AI-clients zoals Claude verbinding kunnen maken. Wanneer deze is uitgeschakeld, wordt het eindpunt verwijderd en kunnen externe AI-clients geen SureForms-mogelijkheden ontdekken of oproepen."],"Learn more":["Meer informatie"],"MCP Adapter Required":["MCP-adapter vereist"],"Heading 1":["Kop 1"],"Heading 2":["Kop 2"],"Heading 3":["Kop 3"],"Heading 4":["Kop 4"],"Heading 5":["Kop 5"],"Heading 6":["Kop 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configureer de Google Maps API-sleutel voor adres-autocompletie en kaartvoorbeeld."],"Help shape the future of SureForms":["Help de toekomst van SureForms vormgeven"],"Enable Google Address Autocomplete":["Google-adres automatisch aanvullen inschakelen"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Upgrade naar het SureForms Business Plan om Google-aangedreven adres-autocompletie met interactieve kaartvoorvertoning aan uw formulieren toe te voegen."],"Auto-suggest addresses as users type for faster, error-free submissions":["Stel automatisch adressen voor terwijl gebruikers typen voor snellere, foutloze inzendingen"],"Show an interactive map preview with draggable pin for precise locations":["Toon een interactieve kaartvoorvertoning met een sleepbare pin voor precieze locaties"],"Automatically populate address fields like city, state, and postal code":["Automatisch adresvelden zoals stad, staat en postcode invullen"],"This form is now closed as we've received all the entries.":["Dit formulier is nu gesloten omdat we alle inzendingen hebben ontvangen."],"Thank you for contacting us! We will be in touch with you shortly.":["Bedankt dat u contact met ons heeft opgenomen! We nemen binnenkort contact met u op."],"Saving\u2026":["Opslaan\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Wanneer ingeschakeld, zal dit formulier het IP-adres van de gebruiker, de naam van de browser en de naam van het apparaat niet opslaan in de inzendingen."],"Form data":["Formuliergegevens"],"Unsaved changes":["Niet-opgeslagen wijzigingen"],"Keep editing":["Blijf bewerken"],"Global Defaults":["Globale Standaarden"],"Form Restrictions":["Formulierbeperkingen"],"Configure default settings that apply to newly created forms.":["Stel de standaardinstellingen in die van toepassing zijn op nieuw aangemaakte formulieren."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Verzamel niet-gevoelige informatie van uw website, zoals de gebruikte PHP-versie en functies, om ons te helpen bugs sneller op te lossen, slimmere beslissingen te nemen en functies te bouwen die echt belangrijk voor u zijn."],"Failed to load pages. Please refresh and try again.":["Het laden van pagina's is mislukt. Vernieuw de pagina en probeer het opnieuw."],"Failed to load settings. Please refresh and try again.":["Het laden van de instellingen is mislukt. Vernieuw de pagina en probeer het opnieuw."],"Form Validation fields cannot be left blank.":["Formulier validatievelden mogen niet leeg worden gelaten."],"Recipient email is required when email summaries are enabled.":["Ontvanger e-mail is vereist wanneer e-mailoverzichten zijn ingeschakeld."],"Please enter a valid recipient email.":["Voer een geldig e-mailadres van de ontvanger in."],"Settings saved.":["Instellingen opgeslagen."],"Failed to save settings.":["Opslaan van instellingen mislukt."],"Some settings failed to save. Please retry.":["Sommige instellingen konden niet worden opgeslagen. Probeer het opnieuw."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Sommige velden hebben niet-opgeslagen wijzigingen. Verwijder ze om door te gaan, of blijf om je wijzigingen op te slaan."],"Discard & switch":["Weggooien & overschakelen"],"Import":["Importeren"],"Migration":["Migratie"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importeer formulieren van Contact Form 7, WPForms, Gravity Forms en andere plugins in SureForms."],"Could not generate the import preview.":["Kon de importvoorbeeldweergave niet genereren."],"Import failed. Please try again or check your error logs.":["Importeren mislukt. Probeer het opnieuw of controleer uw foutlogboeken."],"Go back":["Ga terug"],"Update & import":["Bijwerken & importeren"],"Confirm & import":["Bevestigen en importeren"],"Form #%s":["Formulier #%s"],"%d field will import":["%d veld zal worden ge\u00efmporteerd"],"Some fields can't be migrated yet":["Sommige velden kunnen nog niet worden gemigreerd"],"These fields will be skipped - add them manually after the form is created:":["Deze velden worden overgeslagen - voeg ze handmatig toe nadat het formulier is aangemaakt:"],"Some forms could not be parsed":["Sommige formulieren konden niet worden geparsed"],"Update existing":["Bestaande bijwerken"],"Create a new copy":["Maak een nieuwe kopie"],"Could not load forms for this source.":["Kon formulieren voor deze bron niet laden."],"(untitled form)":["(naamloos formulier)"],"Previously imported":["Eerder ge\u00efmporteerd"],"Open existing SureForms form in a new tab":["Open bestaande SureForms-formulier in een nieuw tabblad"],"On re-import":["Bij herimporteren"],"No forms found in this plugin.":["Geen formulieren gevonden in deze plugin."],"%1$d of %2$d form selected":["%1$d van %2$d formulier geselecteerd"],"Preview update":["Voorbeeld bijwerken"],"Preview import":["Voorbeeld importeren"],"Migration complete":["Migratie voltooid"],"Nothing was imported":["Niets is ge\u00efmporteerd"],"%d form was imported into SureForms.":["%d formulier is ge\u00efmporteerd in SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Geen van de geselecteerde formulieren kon worden gemigreerd. Zie de onderstaande waarschuwingen voor details."],"Imported forms":["Ge\u00efmporteerde formulieren"],"Edit in SureForms":["Bewerken in SureForms"],"Some fields were skipped during import":["Er zijn enkele velden overgeslagen tijdens het importeren"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Voeg deze handmatig toe in de formuliereditor - ze hebben nog geen SureForms-equivalent:"],"Some forms failed to import":["Sommige formulieren konden niet worden ge\u00efmporteerd"],"Import more forms":["Meer formulieren importeren"],"Could not load the list of importable plugins.":["Kon de lijst met importeerbare plug-ins niet laden."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Er zijn geen ondersteunde formulier-plugins actief op deze site. Activeer er een (zoals Contact Form 7) met ten minste \u00e9\u00e9n formulier om het in SureForms te importeren."],"Plugin":["Invoegtoepassing"],"%d form":["%d formulier"],"No forms":["Geen formulieren"],"Multiple choice":["Meerkeuze"],"Consent":["Toestemming"],"The image uploaded but could not be inserted. Please try again.":["De afbeelding is ge\u00fcpload, maar kon niet worden ingevoegd. Probeer het opnieuw."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["De afbeelding kon niet worden ge\u00fcpload. Het kan te groot zijn, of u heeft mogelijk geen toestemming om bestanden te uploaden."],"Please choose an image file.":["Kies een afbeeldingsbestand."],"Show views and conversion rate":["Toon weergaven en conversieratio"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Voegt de kolommen Weergaven en Conversieratio toe aan de Formulierenlijst. Een weergave wordt eenmaal per paginabezoek geteld wanneer het formulier op het scherm verschijnt, en de conversieratio is het aandeel van die weergaven dat eindigde in een inzending. Het tellen begint de eerste keer dat u dit inschakelt, dus inzendingen die daarvoor zijn ontvangen, worden niet meegeteld voor de ratio. Het daarna uitschakelen verbergt alleen de kolommen \u2014 het tellen gaat door, zodat de cijfers up-to-date zijn als u het weer inschakelt."],"Log cleared.":["Log gewist."],"Could not clear the log.":["Kon de log niet wissen."],"Enable logs":["Logs inschakelen"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registreert formulierindieningsfouten die door de browser worden gerapporteerd, zodat je de log naar ondersteuning kunt sturen in plaats van de console te lezen. Er wordt niets geschreven terwijl je formulieren werken, en ingediende waarden worden nooit opgeslagen."],"Download log":["Log downloaden"],"Confirm delete":["Verwijderen bevestigen"],"Log is full \u2014 download and clear it to keep recording.":["Log is vol \u2014 download en wis het om te blijven opnemen."],"Form Views & Conversion":["Formulierweergaven & Conversie"],"Logs":["Logs"],"Quill heading picker: default paragraph style\u0004Normal":["Normaal"]}}} \ No newline at end of file +{"translation-revision-date":"2024-12-13T12:33:48+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Dashboard"],"Settings":["Instellingen"],"Entries":["Inzendingen"],"Status":["Status"],"Form":["Formulier"],"Activated":["Geactiveerd"],"Activate":["Activeren"],"Address":["Adres"],"Checkbox":["Selectievakje"],"Dropdown":["Keuzemenu"],"Email":["E-mail"],"Number":["Nummer"],"Phone":["Telefoon"],"Textarea":["Tekstvak"],"Monday":["Maandag"],"Forms":["Formulieren"],"New Form Submission - %s":["Nieuwe formulierinzending - %s"],"GitHub":["GitHub"],"(no title)":["(geen titel)"],"General":["Algemeen"],"No tags available":["Geen tags beschikbaar"],"Back":["Terug"],"Generic tags":["Algemene tags"],"Other":["Anders"],"Confirm":["Bevestigen"],"Cancel":["Annuleren"],"Install":["Installeren"],"Plugin Installation failed, Please try again later.":["Installatie van de plugin mislukt, probeer het later opnieuw."],"Plugin activation failed, Please try again later.":["Plug-in activering mislukt, probeer het later opnieuw."],"Integrations":["Integraties"],"What's New?":["Wat is er nieuw?"],"Core":["Kern"],"Unlicensed":["Ongeautoriseerd"],"Connecting\u2026":["Verbinden\u2026"],"Install & Activate":["Installeren & Activeren"],"Compliance Settings":["Instellingen voor naleving"],"Enable GDPR Compliance":["Schakel GDPR-naleving in"],"Never store entry data after form submission":["Sla nooit invoergegevens op na het indienen van het formulier"],"When enabled this form will never store Entries.":["Wanneer ingeschakeld, zal dit formulier nooit inzendingen opslaan."],"Automatically delete entries":["Automatisch items verwijderen"],"When enabled this form will automatically delete entries after a certain period of time.":["Wanneer ingeschakeld, zal dit formulier automatisch inzendingen na een bepaalde periode verwijderen."],"Entries older than the days set will be deleted automatically.":["Inzendingen ouder dan de ingestelde dagen worden automatisch verwijderd."],"Visual":["Visueel"],"HTML":["HTML"],"All Data":["Alle gegevens"],"Add Shortcode":["Voeg shortcode toe"],"Form input tags":["Formulierveldlabels"],"Comma separated values are also accepted.":["Door komma's gescheiden waarden worden ook geaccepteerd."],"Email Notification":["E-mailmelding"],"Name":["Naam"],"Send Email To":["E-mail verzenden naar"],"Subject":["Onderwerp"],"CC":["CC"],"BCC":["BCC"],"Reply To":["Antwoord aan"],"Add Key":["Sleutel toevoegen"],"Add Value":["Waarde toevoegen"],"Add":["Toevoegen"],"Confirmation Message":["Bevestigingsbericht"],"After Form Submission":["Na het indienen van het formulier"],"Hide Form":["Formulier verbergen"],"Reset Form":["Formulier opnieuw instellen"],"Custom URL":["Aangepaste URL"],"Add Query Parameters":["Queryparameters toevoegen"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Selecteer of u sleutel-waardeparen wilt toevoegen voor formuliervelden om op te nemen in queryparameters"],"Query Parameters":["Queryparameters"],"Success Message":["Succesbericht"],"Redirect":["Doorsturen"],"Redirect to":["Doorsturen naar"],"Page":["Pagina"],"Form Confirmation":["Formulierbevestiging"],"Confirmation Type":["Bevestigingstype"],"Clear":["Duidelijk"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Onzichtbaar"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validaties"],"Spam Protection":["Spam Bescherming"],"If this option is turned on, the user's IP address will be saved with the form data":["Als deze optie is ingeschakeld, wordt het IP-adres van de gebruiker samen met de formuliergegevens opgeslagen"],"Enable Honeypot Security":["Honeypot-beveiliging inschakelen"],"Enable Honeypot Security for better spam protection":["Schakel Honeypot-beveiliging in voor betere spambeveiliging"],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s vertegenwoordigt het minimum aantal selecties dat nodig is. Bijvoorbeeld: \"Minimaal 2 selecties zijn vereist.\""],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s vertegenwoordigt het maximale aantal toegestane selecties. Bijvoorbeeld: \"Maximaal 4 selecties zijn toegestaan.\""],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s vertegenwoordigt de minimale keuzes die nodig zijn. Bijvoorbeeld: \"Minimaal 1 selectie is vereist.\""],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s vertegenwoordigt het maximale aantal toegestane keuzes. Bijvoorbeeld: \"Maximaal 3 selecties zijn toegestaan.\""]," Error Message":["Foutmelding"],"Email Summaries":["E-mailoverzichten"],"Tuesday":["Dinsdag"],"Wednesday":["Woensdag"],"Thursday":["Donderdag"],"Friday":["Vrijdag"],"Saturday":["Zaterdag"],"Sunday":["Zondag"],"Schedule Reports":["Rapporten plannen"],"Auto":["Auto"],"Light":["Licht"],"Dark":["Donker"],"Turnstile":["Draaikruis"],"Get Keys":["Verkrijg sleutels"],"Documentation":["Documentatie"],"Site Key":["Sitecode"],"Secret Key":["Geheime Sleutel"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Weergavemodus"],"Text":["Tekst"],"Test Email":["Test e-mail"],"IP Logging":["IP-logboek"],"Honeypot":["Honeypot"],"Confirmation Email Mismatch Message":["Bevestigingsmail komt niet overeen bericht"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s vertegenwoordigt de minimale invoerwaarde. Bijvoorbeeld: \"Minimale waarde is 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s vertegenwoordigt de maximale invoerwaarde. Bijvoorbeeld: \"Maximale waarde is 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Verbind met OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Het huidige 'Van E-mailadres' komt niet overeen met de domeinnaam van uw website (%1$s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd. Probeer in plaats daarvan een Van-adres te gebruiken dat overeenkomt met uw website-domein (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Het huidige 'Van E-mailadres' komt niet overeen met de domeinnaam van uw website (%s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd."],"We strongly recommend that you install the free ":["We raden ten zeerste aan dat u de gratis installeert"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin! De Setup Wizard maakt het gemakkelijk om je e-mails te repareren."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Probeer afwisselend een Van-adres te gebruiken dat overeenkomt met uw website-domein (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Voer een geldig e-mailadres in. Uw meldingen worden niet verzonden als het veld niet correct is ingevuld."],"From Name":["Van Naam"],"From Email":["Van e-mail"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Het huidige 'Van E-mailadres' komt mogelijk niet overeen met de domeinnaam van uw website (%1$s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd. Probeer in plaats daarvan een Van-adres te gebruiken dat overeenkomt met uw website domein (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Het huidige 'Van E-mailadres' komt mogelijk niet overeen met de domeinnaam van uw website (%s). Dit kan ervoor zorgen dat uw notificatie-e-mails worden geblokkeerd of als spam worden gemarkeerd."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Klaar om verder te gaan dan het gratis plan?"],"Upgrade now":["Nu upgraden"],"and unlock the full power of SureForms!":["en ontgrendel de volledige kracht van SureForms!"],"Upgrade SureForms":["Upgrade SureForms"],"Upgrade Now":["Nu upgraden"],"Entries older than the selected days will be deleted.":["Inzendingen ouder dan de geselecteerde dagen worden verwijderd."],"Entries Time Period":["Invoerperiode"],"Notifications can use only one From Email so please enter a single address.":["Meldingen kunnen slechts \u00e9\u00e9n Van E-mailadres gebruiken, dus voer alstublieft \u00e9\u00e9n enkel adres in."],"Select Page to redirect":["Selecteer pagina om te omleiden"],"Search for a page":["Zoek naar een pagina"],"Select a page":["Selecteer een pagina"],"Form Validation":["Formuliercontrole"],"Required Error Messages":["Vereiste foutmeldingen"],"Other Error Messages":["Andere foutmeldingen"],"Input Field Unique":["Uniek invoerveld"],"Email Field Unique":["E-mailveld uniek"],"Invalid URL":["Ongeldige URL"],"Phone Field Unique":["Telefoonveld Uniek"],"Invalid Field Number Block":["Ongeldige Veldnummer Blok"],"Invalid Email":["Ongeldig e-mailadres"],"Number Minimum Value":["Nummer Minimumwaarde"],"Number Maximum Value":["Maximale waarde van het nummer"],"Dropdown Minimum Selections":["Minimale selecties in dropdown"],"Dropdown Maximum Selections":["Maximale selecties in dropdown"],"Multiple Choice Minimum Selections":["Meerdere keuzes minimumselecties"],"Multiple Choice Maximum Selections":["Meerdere Keuzes Maximale Selecties"],"Input Field":["Invoerveld"],"Email Field":["E-mailveld"],"URL Field":["URL-veld"],"Phone Field":["Telefoonveld"],"Textarea Field":["Tekstvakveld"],"Checkbox Field":["Selectievakjeveld"],"Dropdown Field":["Keuzelijstveld"],"Multiple Choice Field":["Meerkeuzeveld"],"Address Field":["Adresveld"],"Number Field":["Nummer veld"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Om de reCAPTCHA-functie op uw SureForms in te schakelen, schakelt u de reCAPTCHA-optie in uw blokinstellingen in en selecteert u de versie. Voeg hier de Google reCAPTCHA-secret en site key toe. reCAPTCHA wordt aan uw pagina toegevoegd aan de voorkant."],"Enter your %s here":["Voer hier uw %s in"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Om hCAPTCHA in te schakelen, voeg uw site-sleutel en geheime sleutel toe. Configureer deze instellingen binnen het individuele formulier."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Om Cloudflare Turnstile in te schakelen, voeg uw site-sleutel en geheime sleutel toe. Configureer deze instellingen binnen het individuele formulier."],"Save":["Opslaan"],"Anonymous Analytics":["Anonieme Analytics"],"Learn More":["Meer informatie"],"Admin Notification":["Beheerdersmelding"],"Enable Admin Notification":["Beheerdermelding inschakelen"],"Admin notifications keep you informed about new form entries since your last visit.":["Beheerdersmeldingen houden je op de hoogte van nieuwe formulierinzendingen sinds je laatste bezoek."],"Skip":["Overslaan"],"Continue":["Doorgaan"],"Maximum Number of Entries":["Maximaal aantal vermeldingen"],"Maximum Entries":["Maximale invoer"],"Response Description After Maximum Entries":["Beschrijving van de reactie na maximale invoer"],"Get Started":["Beginnen"],"Integration":["Integratie"],"Connect Native Integrations with SureForms":["Verbind native integraties met SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Ontgrendel krachtige integraties in het Premium-plan om je workflows te automatiseren en SureForms direct te verbinden met je favoriete tools."],"Send form submissions straight to CRMs, email, and marketing platforms":["Stuur formulierinzendingen rechtstreeks naar CRM's, e-mail en marketingplatforms"],"Automate repetitive tasks with seamless data syncing":["Automatiseer repetitieve taken met naadloze gegevenssynchronisatie"],"Access exclusive native integrations for faster workflows":["Toegang tot exclusieve native integraties voor snellere workflows"],"Expected format for emails - email@sureforms.com or John Doe ":["Verwacht formaat voor e-mails - email@sureforms.com of John Doe "],"Payments":["Betalingen"],"Stripe account disconnected successfully.":["Stripe-account succesvol losgekoppeld."],"Failed to create webhook.":["Het is niet gelukt om de webhook te maken."],"Failed to connect to Stripe.":["Verbinding met Stripe mislukt."],"Webhook":["Webhook"],"Knowledge Base":["Kennisbank"],"What\u2019s New":["Wat is er nieuw"],"out of":["uit"],"No entries found":["Geen items gevonden"],"delete":["verwijderen"],"Please type \"%s\" in the input box":["Typ alstublieft \"%s\" in het invoerveld"],"To confirm, type \"%s\" in the box below:":["Om te bevestigen, typ \"%s\" in het vak hieronder:"],"Type \"%s\"":["Typ \"%s\""],"Go to OttoKit Settings":["Ga naar OttoKit-instellingen"],"USD - US Dollar":["USD - Amerikaanse dollar"],"Payment Mode":["Betaalwijze"],"Test Mode":["Testmodus"],"Live Mode":["Live-modus"],"Action":["Actie"],"General Settings":["Algemene instellingen"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Stel e-mailsamenvattingen, beheerderswaarschuwingen en gegevensvoorkeuren in om uw formulieren eenvoudig te beheren."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Pas standaardfoutmeldingen aan die worden weergegeven wanneer gebruikers ongeldige of onvolledige formulierinvoer indienen."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Schakel spambeveiliging voor uw formulieren in met behulp van CAPTCHA-diensten of honeypot-beveiliging."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Verbind en beheer uw betalingsgateways om veilig transacties via uw formulieren te accepteren."],"1% transaction and payment gateway fees apply.":["1% transactiekosten en kosten voor de betalingsgateway zijn van toepassing."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["2,9% transactiekosten en kosten voor de betalingsgateway zijn van toepassing. Activeer de licentie om de transactiekosten te verlagen."],"2.9% transaction and payment gateway fees apply.":["Er zijn 2,9% transactiekosten en kosten voor de betalingsgateway van toepassing."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Bezoek %1$s, verwijder een ongebruikte webhook en klik vervolgens hieronder om het opnieuw te proberen."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms kon geen webhook maken omdat je Stripe-account geen gratis slots meer heeft. Webhooks zijn nodig om updates over betalingen te ontvangen."],"Stripe Dashboard":["Stripe-dashboard"],"Creating\u2026":["Aan het cre\u00ebren\u2026"],"Create Webhook":["Webhook maken"],"Successfully connected to Stripe!":["Succesvol verbonden met Stripe!"],"Invalid response from server. Please try again.":["Ongeldig antwoord van de server. Probeer het alstublieft opnieuw."],"Failed to disconnect Stripe account.":["Het is niet gelukt om de Stripe-account los te koppelen."],"Webhook created successfully!":["Webhook succesvol aangemaakt!"],"Select Currency":["Selecteer valuta"],"Select the default currency for payment forms.":["Selecteer de standaardvaluta voor betalingsformulieren."],"Connection Status":["Verbindingsstatus"],"Disconnect Stripe Account":["Stripe-account loskoppelen"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Weet u zeker dat u uw Stripe-account wilt loskoppelen? Dit zal alle actieve betalingen, abonnementen en formuliertransacties die aan dit account zijn gekoppeld, stoppen."],"Disconnect":["Verbreken"],"Disconnecting\u2026":["Verbinding verbreken\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook succesvol verbonden, alle Stripe-evenementen worden gevolgd."],"Connect your Stripe account to start accepting payments through your forms.":["Verbind uw Stripe-account om betalingen via uw formulieren te accepteren."],"Connect to Stripe":["Verbinden met Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Maak veilig verbinding met Stripe met slechts een paar klikken om betalingen te accepteren!"],"Set the total number of submissions allowed for this form.":["Stel het totale aantal toegestane inzendingen voor dit formulier in."],"Payment Methods":["Betaalmethoden"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Met de testmodus kunt u betalingen verwerken zonder echte kosten. Schakel over naar de Live-modus voor daadwerkelijke transacties."],"General Payment Settings":["Algemene betalingsinstellingen"],"These settings apply to all payment gateways.":["Deze instellingen zijn van toepassing op alle betalingsgateways."],"Stripe Settings":["Stripe-instellingen"],"Left ($100)":["Links ($100)"],"Right (100$)":["Rechts (100$)"],"Left Space ($ 100)":["Linker ruimte ($ 100)"],"Right Space (100 $)":["Rechterruimte (100 $)"],"Currency Sign Position":["Positie van het valutateken"],"Select the position of the currency symbol relative to the amount.":["Selecteer de positie van het valutasymbool ten opzichte van het bedrag."],"Learn":["Leren"],"Enable email summaries":["E-mailoverzichten inschakelen"],"Enable IP logging":["IP-logboek inschakelen"],"Turn on Admin Notification from here.":["Zet hier de beheerdersmelding aan."],"New":["Nieuw"],"Send entries to 100+ popular apps.":["Verzend inzendingen naar meer dan 100 populaire apps."],"Build automated workflows that run instantly.":["Bouw geautomatiseerde workflows die direct worden uitgevoerd."],"Create custom app integrations using our Custom App feature.":["Maak aangepaste app-integraties met behulp van onze functie Aangepaste App."],"Keep your tools in sync automatically.":["Houd je gereedschap automatisch gesynchroniseerd."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Hiermee wordt OttoKit op je WordPress-site ge\u00efnstalleerd en geactiveerd om automatiseringsfuncties mogelijk te maken."],"Automate Your Forms with OttoKit":["Automatiseer uw formulieren met OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Elke formulierinzending zou iets moeten activeren \u2014 een Slack-melding, een CRM-lead, een opvolg-e-mail of een nieuwe rij in Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configureer AI-clientmachtigingen en MCP-serverinstellingen."],"View documentation":["Documentatie bekijken"],"Copy to clipboard":["Kopi\u00ebren naar klembord"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) of %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Claude Code"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (project) of ~\/.claude.json (globaal)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (project) of settings.json > mcp.servers (globaal)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml of config.json"],"Your client's MCP configuration file":["Het MCP-configuratiebestand van uw klant"],"Connect Your AI Client":["Verbind uw AI-client"],"AI Client":["AI-client"],"Create an Application Password \u2014 ":["Maak een applicatiewachtwoord aan \u2014"],"Open Application Passwords":["Open toepassingswachtwoorden"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Of gebruik deze CLI-opdracht om de server snel toe te voegen (je moet nog steeds de omgevingsvariabelen instellen):"],"Copy the JSON config below into: ":["Kopieer de JSON-configuratie hieronder naar:"],"Replace \"your-application-password\" with the password from Step 1.":["Vervang \"your-application-password\" door het wachtwoord uit Stap 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 het MCP-eindpunt van je site. WP_API_USERNAME \u2014 je WordPress-gebruikersnaam. WP_API_PASSWORD \u2014 het applicatiewachtwoord dat je hebt gegenereerd."],"View setup docs":["Bekijk de installatiehandleidingen"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["De MCP Adapter-plugin is ge\u00efnstalleerd maar niet actief. Activeer het om MCP-instellingen te configureren."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["De MCP Adapter-plugin is vereist om AI-clients met uw formulieren te verbinden. Download en installeer het vanaf GitHub en activeer het vervolgens."],"Download the latest release from":["Download de nieuwste release van"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installeer de plugin via Plugins > Nieuwe plugin toevoegen > Plugin uploaden."],"Activate the MCP Adapter plugin.":["Activeer de MCP Adapter-plugin."],"Activating\u2026":["Activeren\u2026"],"Activate MCP Adapter":["Activeer MCP-adapter"],"Download MCP Adapter":["Download MCP Adapter"],"Experimental":["Experimenteel"],"Enable Abilities":["Vermogen inschakelen"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registreer SureForms-mogelijkheden bij de WordPress Abilities API. Wanneer ingeschakeld, kunnen AI-clients uw formulieren en inzendingen opsommen, lezen, maken, bewerken en verwijderen. Wanneer uitgeschakeld, worden er geen mogelijkheden geregistreerd en kunnen AI-clients geen acties uitvoeren op uw formulieren."],"Abilities API \u2014 Edit":["Vaardigheden-API \u2014 Bewerken"],"Enable Edit Abilities":["Bewerkingsmogelijkheden inschakelen"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Wanneer ingeschakeld, kunnen AI-clients nieuwe formulieren maken, formuliertitels, velden en instellingen bijwerken, formulieren dupliceren en de status van inzendingen wijzigen. Wanneer uitgeschakeld, worden deze mogelijkheden uitgeschakeld en kunnen AI-clients alleen uw gegevens lezen."],"Abilities API \u2014 Delete":["Vaardigheden API \u2014 Verwijderen"],"Enable Delete Abilities":["Verwijdermogelijkheden inschakelen"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Wanneer ingeschakeld, kunnen AI-clients formulieren en invoer permanent verwijderen. Verwijderde gegevens kunnen niet worden hersteld. Wanneer uitgeschakeld, worden verwijdermogelijkheden uitgeschakeld en kunnen AI-clients geen gegevens verwijderen."],"MCP Server":["MCP-server"],"Enable MCP Server":["MCP-server inschakelen"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Maakt een speciale SureForms MCP-eindpunt aan waarmee AI-clients zoals Claude verbinding kunnen maken. Wanneer deze is uitgeschakeld, wordt het eindpunt verwijderd en kunnen externe AI-clients geen SureForms-mogelijkheden ontdekken of oproepen."],"Learn more":["Meer informatie"],"MCP Adapter Required":["MCP-adapter vereist"],"Heading 1":["Kop 1"],"Heading 2":["Kop 2"],"Heading 3":["Kop 3"],"Heading 4":["Kop 4"],"Heading 5":["Kop 5"],"Heading 6":["Kop 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configureer de Google Maps API-sleutel voor adres-autocompletie en kaartvoorbeeld."],"Help shape the future of SureForms":["Help de toekomst van SureForms vormgeven"],"Enable Google Address Autocomplete":["Google-adres automatisch aanvullen inschakelen"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Upgrade naar het SureForms Business Plan om Google-aangedreven adres-autocompletie met interactieve kaartvoorvertoning aan uw formulieren toe te voegen."],"Auto-suggest addresses as users type for faster, error-free submissions":["Stel automatisch adressen voor terwijl gebruikers typen voor snellere, foutloze inzendingen"],"Show an interactive map preview with draggable pin for precise locations":["Toon een interactieve kaartvoorvertoning met een sleepbare pin voor precieze locaties"],"Automatically populate address fields like city, state, and postal code":["Automatisch adresvelden zoals stad, staat en postcode invullen"],"This form is now closed as we've received all the entries.":["Dit formulier is nu gesloten omdat we alle inzendingen hebben ontvangen."],"Thank you for contacting us! We will be in touch with you shortly.":["Bedankt dat u contact met ons heeft opgenomen! We nemen binnenkort contact met u op."],"Saving\u2026":["Opslaan\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Wanneer ingeschakeld, zal dit formulier het IP-adres van de gebruiker, de naam van de browser en de naam van het apparaat niet opslaan in de inzendingen."],"Form data":["Formuliergegevens"],"Unsaved changes":["Niet-opgeslagen wijzigingen"],"Keep editing":["Blijf bewerken"],"Global Defaults":["Globale Standaarden"],"Form Restrictions":["Formulierbeperkingen"],"Configure default settings that apply to newly created forms.":["Stel de standaardinstellingen in die van toepassing zijn op nieuw aangemaakte formulieren."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Verzamel niet-gevoelige informatie van uw website, zoals de gebruikte PHP-versie en functies, om ons te helpen bugs sneller op te lossen, slimmere beslissingen te nemen en functies te bouwen die echt belangrijk voor u zijn."],"Failed to load pages. Please refresh and try again.":["Het laden van pagina's is mislukt. Vernieuw de pagina en probeer het opnieuw."],"Failed to load settings. Please refresh and try again.":["Het laden van de instellingen is mislukt. Vernieuw de pagina en probeer het opnieuw."],"Form Validation fields cannot be left blank.":["Formulier validatievelden mogen niet leeg worden gelaten."],"Recipient email is required when email summaries are enabled.":["Ontvanger e-mail is vereist wanneer e-mailoverzichten zijn ingeschakeld."],"Please enter a valid recipient email.":["Voer een geldig e-mailadres van de ontvanger in."],"Settings saved.":["Instellingen opgeslagen."],"Failed to save settings.":["Opslaan van instellingen mislukt."],"Some settings failed to save. Please retry.":["Sommige instellingen konden niet worden opgeslagen. Probeer het opnieuw."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Sommige velden hebben niet-opgeslagen wijzigingen. Verwijder ze om door te gaan, of blijf om je wijzigingen op te slaan."],"Discard & switch":["Weggooien & overschakelen"],"Import":["Importeren"],"Migration":["Migratie"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importeer formulieren van Contact Form 7, WPForms, Gravity Forms en andere plugins in SureForms."],"Could not generate the import preview.":["Kon de importvoorbeeldweergave niet genereren."],"Import failed. Please try again or check your error logs.":["Importeren mislukt. Probeer het opnieuw of controleer uw foutlogboeken."],"Go back":["Ga terug"],"Update & import":["Bijwerken & importeren"],"Confirm & import":["Bevestigen en importeren"],"Form #%s":["Formulier #%s"],"%d field will import":["%d veld zal worden ge\u00efmporteerd"],"Some fields can't be migrated yet":["Sommige velden kunnen nog niet worden gemigreerd"],"These fields will be skipped - add them manually after the form is created:":["Deze velden worden overgeslagen - voeg ze handmatig toe nadat het formulier is aangemaakt:"],"Some forms could not be parsed":["Sommige formulieren konden niet worden geparsed"],"Update existing":["Bestaande bijwerken"],"Create a new copy":["Maak een nieuwe kopie"],"Could not load forms for this source.":["Kon formulieren voor deze bron niet laden."],"(untitled form)":["(naamloos formulier)"],"Previously imported":["Eerder ge\u00efmporteerd"],"Open existing SureForms form in a new tab":["Open bestaande SureForms-formulier in een nieuw tabblad"],"On re-import":["Bij herimporteren"],"No forms found in this plugin.":["Geen formulieren gevonden in deze plugin."],"%1$d of %2$d form selected":["%1$d van %2$d formulier geselecteerd"],"Preview update":["Voorbeeld bijwerken"],"Preview import":["Voorbeeld importeren"],"Migration complete":["Migratie voltooid"],"Nothing was imported":["Niets is ge\u00efmporteerd"],"%d form was imported into SureForms.":["%d formulier is ge\u00efmporteerd in SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Geen van de geselecteerde formulieren kon worden gemigreerd. Zie de onderstaande waarschuwingen voor details."],"Imported forms":["Ge\u00efmporteerde formulieren"],"Edit in SureForms":["Bewerken in SureForms"],"Some fields were skipped during import":["Er zijn enkele velden overgeslagen tijdens het importeren"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Voeg deze handmatig toe in de formuliereditor - ze hebben nog geen SureForms-equivalent:"],"Some forms failed to import":["Sommige formulieren konden niet worden ge\u00efmporteerd"],"Import more forms":["Meer formulieren importeren"],"Could not load the list of importable plugins.":["Kon de lijst met importeerbare plug-ins niet laden."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Er zijn geen ondersteunde formulier-plugins actief op deze site. Activeer er een (zoals Contact Form 7) met ten minste \u00e9\u00e9n formulier om het in SureForms te importeren."],"Plugin":["Invoegtoepassing"],"%d form":["%d formulier"],"No forms":["Geen formulieren"],"Multiple choice":["Meerkeuze"],"Consent":["Toestemming"],"The image uploaded but could not be inserted. Please try again.":["De afbeelding is ge\u00fcpload, maar kon niet worden ingevoegd. Probeer het opnieuw."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["De afbeelding kon niet worden ge\u00fcpload. Het kan te groot zijn, of u heeft mogelijk geen toestemming om bestanden te uploaden."],"Please choose an image file.":["Kies een afbeeldingsbestand."],"Show views and conversion rate":["Toon weergaven en conversieratio"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Voegt de kolommen Weergaven en Conversieratio toe aan de Formulierenlijst. Een weergave wordt eenmaal per paginabezoek geteld wanneer het formulier op het scherm verschijnt, en de conversieratio is het aandeel van die weergaven dat eindigde in een inzending. Het tellen begint de eerste keer dat u dit inschakelt, dus inzendingen die daarvoor zijn ontvangen, worden niet meegeteld voor de ratio. Het daarna uitschakelen verbergt alleen de kolommen \u2014 het tellen gaat door, zodat de cijfers up-to-date zijn als u het weer inschakelt."],"Log cleared.":["Log gewist."],"Could not clear the log.":["Kon de log niet wissen."],"Enable logs":["Logs inschakelen"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registreert formulierindieningsfouten die door de browser worden gerapporteerd, zodat je de log naar ondersteuning kunt sturen in plaats van de console te lezen. Er wordt niets geschreven terwijl je formulieren werken, en ingediende waarden worden nooit opgeslagen."],"Download log":["Log downloaden"],"Confirm delete":["Verwijderen bevestigen"],"Log is full \u2014 download and clear it to keep recording.":["Log is vol \u2014 download en wis het om te blijven opnemen."],"Form Views & Conversion":["Formulierweergaven & Conversie"],"Logs":["Logs"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Weergaven en inzendingen door iedereen die de site kan bewerken, worden normaal weggelaten, zodat het testen van uw eigen formulieren deze cijfers niet verandert."],"Quill heading picker: default paragraph style\u0004Normal":["Normaal"]}}} \ No newline at end of file diff --git a/languages/sureforms-nl_NL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-nl_NL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json index 07826e531..2483096b4 100644 --- a/languages/sureforms-nl_NL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json +++ b/languages/sureforms-nl_NL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json @@ -1 +1 @@ -{"translation-revision-date":"2024-12-13T12:33:48+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Dashboard"],"Settings":["Instellingen"],"Entries":["Inzendingen"],"Activated":["Geactiveerd"],"Advanced Fields":["Geavanceerde velden"],"Select Form":["Selecteer formulier"],"Create New Form":["Nieuw formulier maken"],"Forms":["Formulieren"],"Copy":["Kopi\u00ebren"],"Business":["Zakelijk"],"Next":["Volgende"],"Back":["Terug"],"Cancel":["Annuleren"],"This is where your form views will appear":["Hier verschijnen uw formulierweergaven"],"Install":["Installeren"],"Plugin Installation failed, Please try again later.":["Installatie van de plugin mislukt, probeer het later opnieuw."],"Plugin activation failed, Please try again later.":["Plug-in activering mislukt, probeer het later opnieuw."],"What's New?":["Wat is er nieuw?"],"Core":["Kern"],"Unlicensed":["Ongeautoriseerd"],"Upgrade":["Upgrade"],"Webhooks":["Webhooks"],"Install & Activate":["Installeren & Activeren"],"Conditional Logic":["Conditionele logica"],"Premium":["Premium"],"Welcome to SureForms!":["Welkom bij SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms is een WordPress-plugin waarmee gebruikers mooie formulieren kunnen maken via een drag-and-drop-interface, zonder dat er code nodig is. Het integreert met de WordPress-blokeditor."],"Read Full Guide":["Lees de volledige gids"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Aangepaste WordPress-formulieren GEMAKKELIJK GEMAAKT"],"No Date":["Geen datum"],"Invalid Date":["Ongeldige datum"],"Ready to go beyond free plan?":["Klaar om verder te gaan dan het gratis plan?"],"Upgrade now":["Nu upgraden"],"and unlock the full power of SureForms!":["en ontgrendel de volledige kracht van SureForms!"],"Upgrade SureForms":["Upgrade SureForms"],"Open Support Ticket":["Ondersteuningsticket openen"],"Help Center":["Helpcentrum"],"Join our Community on Facebook":["Word lid van onze community op Facebook"],"Leave Us a Review":["Laat een recensie achter"],"Quick Access":["Snelle toegang"],"Upgrade Now":["Nu upgraden"],"Clear Filters":["Filters wissen"],"Unnamed Form":["Naamloos formulier"],"Forms Overview":["Overzicht van formulieren"],"Clear Form Filters":["Formulierfilters wissen"],"Clear Date Filters":["Wis datumfilters"],"Select Date Range":["Selecteer datumbereik"],"Apply":["Toepassen"],"Please wait for the data to load":["Wacht alstublieft tot de gegevens zijn geladen"],"No entries to display":["Geen items om weer te geven"],"Once you create a form and start receiving submissions, the data will appear here.":["Zodra je een formulier maakt en inzendingen begint te ontvangen, zullen de gegevens hier verschijnen."],"Free":["Gratis"],"All Forms":["Alle formulieren"],"Guided Setup":["Geleide installatie"],"Exit Guided Setup":["Geleide installatie afsluiten"],"Skip":["Overslaan"],"Build beautiful forms visually":["Bouw prachtige formulieren visueel"],"Works perfectly on mobile":["Werkt perfect op mobiel"],"Easy to connect with automation tools":["Eenvoudig te verbinden met automatiseringstools"],"Welcome to SureForms":["Welkom bij SureForms"],"Smart, Quick and Powerful Forms.":["Slimme, snelle en krachtige formulieren."],"Let's Get Started":["Laten we beginnen"],"Build up to 10 forms using AI":["Bouw tot 10 formulieren met behulp van AI"],"A secure and private connection":["Een veilige en priv\u00e9verbinding"],"Smart form drafts based on your input":["Slimme formulierontwerpen op basis van uw invoer"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Beginnen met een leeg formulier is niet altijd gemakkelijk. Onze AI kan helpen door een conceptformulier te maken op basis van wat je probeert te doen \u2014 dit bespaart je tijd en geeft je een duidelijke richting."],"To do this, you'll need to connect your account.":["Om dit te doen, moet je je account verbinden."],"Let AI Help You Build Smarter, Faster Forms":["Laat AI u helpen om slimmere, snellere formulieren te maken"],"Here's what that gives you:":["Dit is wat dat je oplevert:"],"Continue":["Doorgaan"],"Connect":["Verbinden"],"Works smoothly with forms made using SureForms":["Werkt soepel met formulieren gemaakt met SureForms"],"Helps your emails reach the inbox instead of spam":["Helpt uw e-mails de inbox te bereiken in plaats van de spam"],"Setup is straightforward, even if you're not technical":["De installatie is eenvoudig, zelfs als je niet technisch bent"],"Lightweight and easy to use without adding clutter":["Lichtgewicht en eenvoudig te gebruiken zonder rommel toe te voegen"],"Make Sure Your Emails Get Delivered":["Zorg ervoor dat uw e-mails worden afgeleverd"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["De meeste WordPress-sites hebben moeite om e-mails betrouwbaar te verzenden, wat betekent dat formulierinzendingen van uw site mogelijk uw inbox niet bereiken \u2014 of in de spam terechtkomen."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail is een eenvoudige SMTP-plug-in die ervoor zorgt dat je e-mails daadwerkelijk worden afgeleverd."],"What you will get:":["Wat je zult krijgen:"],"Install SureMail":["Installeer SureMail"],"AI Form Generation":["AI Formuliergeneratie"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Ben je het beu om formulieren handmatig te maken? Laat AI het werk voor je doen. Beschrijf het gewoon en onze AI maakt in enkele seconden je perfecte formulier."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Breek complexe formulieren op in eenvoudige stappen, waardoor overweldiging wordt verminderd en voltooiingspercentages worden verhoogd. Leid gebruikers soepel door het proces"],"Conditional Fields":["Voorwaardelijke Velden"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Toon of verberg velden op basis van gebruikersantwoorden. Stel de juiste vragen en toon alleen wat nodig is om formulieren schoon en relevant te houden."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Verbeter uw formulieren met geavanceerde velden zoals multi-bestandsupload, beoordelingsvelden en datum- en tijdkiezers om rijkere, flexibele gegevens te verzamelen."],"Conversational Forms":["Gespreksvormen"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Maak formulieren die aanvoelen als een gesprek. E\u00e9n vraag tegelijk houdt gebruikers betrokken en maakt het invullen van formulieren eenvoudig."],"Digital Signatures":["Digitale handtekeningen"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Verzamel juridisch bindende digitale handtekeningen direct in uw formulieren voor overeenkomsten, goedkeuringen en contracten."],"Calculators":["Rekenmachines"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Voeg interactieve rekenmachines toe aan uw formulieren voor directe schattingen, offertes en berekeningen voor uw gebruikers."],"User Registration and Login":["Gebruikersregistratie en inloggen"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Sta bezoekers toe om zich te registreren en in te loggen op uw site. Nuttig voor lidmaatschappen, gemeenschappen of elke site die gebruikers toegang nodig heeft."],"PDF Generation Made Simple":["PDF-generatie eenvoudig gemaakt"],"Custom App":["Aangepaste app"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Verzamel gegevens, stuur ze naar externe applicaties voor verwerking en toon resultaten direct \u2014 alles naadloos ge\u00efntegreerd om dynamische, interactieve gebruikerservaringen te cre\u00ebren."],"Select Your Features":["Selecteer uw functies"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Krijg meer controle, snellere workflows en diepere aanpassingsmogelijkheden \u2014 allemaal ontworpen om je te helpen betere websites te bouwen met minder inspanning."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Geselecteerde functies vereisen %1$s - gebruik code %2$s om 10% korting te krijgen op elk plan."],"Copied":["Gekopieerd"],"Style your form to better match your site's design":["Stijl uw formulier zodat het beter bij het ontwerp van uw site past"],"Add spam protection to block common bot submissions":["Voeg spambeveiliging toe om veelvoorkomende botinzendingen te blokkeren"],"Get weekly email reports with a summary of form activity":["Ontvang wekelijkse e-mailrapporten met een samenvatting van de formulieractiviteit"],"You're All Set! \ud83d\ude80":["Je bent helemaal klaar! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Gebruik onze AI-formulierbouwer om snel aan de slag te gaan, of bouw je formulier vanaf nul als je al weet wat je nodig hebt. Je formulieren zijn klaar om te maken, te delen en te verbinden met je sitebezoekers."],"Final Touches That Make a Difference:":["Laatste details die het verschil maken:"],"Build Your First Form":["Bouw je eerste formulier"],"File Uploads":["Bestandsuploads"],"Signature & Rating":["Handtekening & Beoordeling"],"Calculation Forms":["Berekeningsformulieren"],"And Much More\u2026":["En nog veel meer\u2026"],"Upgrade to Pro":["Upgrade naar Pro"],"Unlock Premium Features":["Ontgrendel Premiumfuncties"],"Build Better Forms with SureForms":["Bouw betere formulieren met SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Voeg geavanceerde velden, conversatie-indelingen en slimme logica toe om formulieren te maken die gebruikers betrekken en betere gegevens vastleggen."],"SureForms Video Thumbnail":["SureForms Video Miniatuur"],"Payments":["Betalingen"],"Knowledge Base":["Kennisbank"],"What\u2019s New":["Wat is er nieuw"],"Importing\u2026":["Importeren\u2026"],"Learn":["Leren"],"Unable to complete action. Please try again.":["Actie kan niet worden voltooid. Probeer het opnieuw."],"Supercharge Your Workflow":["Geef je workflow een boost"],"Spam protection included":["Spam bescherming inbegrepen"],"Multistep Forms":["Meertrapsformulieren"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Stuur formulierinvoer direct naar elk extern systeem of eindpunt om geavanceerde workflows aan te sturen."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Zet formulierinvoeren automatisch om in schone, klaar-om-te-downloaden PDF's. Perfect voor archivering, delen, opslaan of het georganiseerd houden van zaken."],"Set up confirmation messages and email notifications for each entry":["Stel bevestigingsberichten en e-mailmeldingen in voor elke invoer"],"Something went wrong. Please try again.":["Er is iets misgegaan. Probeer het alstublieft opnieuw."],"Payment Forms":["Betalingsformulieren"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Ontvang betalingen direct via uw formulieren. Accepteer eenmalige en terugkerende betalingen naadloos."],"SureForms %s":["SureForms %s"],"First name is required.":["Voornaam is verplicht."],"Please enter a valid email address.":["Voer een geldig e-mailadres in."],"Email address is required.":["E-mailadres is vereist."],"This is required.":["Dit is vereist."],"Okay, just one last step\u2026":["Ok\u00e9, nog maar \u00e9\u00e9n laatste stap\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Help ons uw SureForms-ervaring aan te passen door iets over uzelf te delen."],"First Name":["Voornaam"],"Enter your first name":["Voer uw voornaam in"],"Last Name":["Achternaam"],"Enter your last name":["Voer uw achternaam in"],"Email Address":["E-mailadres"],"Enter your email address":["Voer uw e-mailadres in"],"Privacy Policy":["Privacybeleid"],"Finish":["Voltooien"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Blijf op de hoogte en help SureForms vorm te geven! Ontvang updates over functies en help ons SureForms te verbeteren door te delen hoe je de plugin gebruikt. Privacybeleid<\/a>."],"%d form ready":["%d formulier klaar"],"%d already imported":["%d al ge\u00efmporteerd"],"(unnamed source)":["(naamloze bron)"],"All forms already imported":["Alle formulieren zijn al ge\u00efmporteerd"],"Import forms":["Formulieren importeren"],"Import %d form":["%d formulier importeren"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Er is iets misgegaan tijdens het importeren. U kunt het opnieuw proberen, overslaan of Instellingen \u2192 Migratie openen."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Geen andere formulier-plugins gedetecteerd. U kunt op elk moment importeren via Instellingen \u2192 Migratie."],"Forms imported":["Formulieren ge\u00efmporteerd"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formulier van %2$s is nu in SureForms, klaar om te publiceren, stijlen en verbinden."],"%d form imported":["%d formulier ge\u00efmporteerd"],"%d form could not be imported":["%d formulier kon niet worden ge\u00efmporteerd"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d veldtype werd niet ondersteund. U kunt het handmatig opnieuw opbouwen in SureForms."],"Bring your existing forms with you":["Breng uw bestaande formulieren mee"],"We detected forms in another plugin. Pick one to import into SureForms.":["We hebben formulieren in een andere plugin gedetecteerd. Kies er een om te importeren in SureForms."],"Choose a form plugin to import from":["Kies een formulierplugin om uit te importeren"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Meer dan \u00e9\u00e9n plugin importeren? U kunt later extra bronnen importeren via Instellingen \u2192 Migratie."],"Import did not complete":["Importeren is niet voltooid"],"I'll do this later":["Ik doe dit later"],"Retry":["Opnieuw proberen"],"Form Checks":["Formuliercontroles"],"Collapse form checks":["Formuliercontroles samenvouwen"],"Expand form checks":["Formuliercontroles uitvouwen"],"Ignore":["Negeren"],"Working\u2026":["Bezig\u2026"]}}} \ No newline at end of file +{"translation-revision-date":"2024-12-13T12:33:48+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Dashboard"],"Settings":["Instellingen"],"Entries":["Inzendingen"],"Activated":["Geactiveerd"],"Advanced Fields":["Geavanceerde velden"],"Select Form":["Selecteer formulier"],"Create New Form":["Nieuw formulier maken"],"Forms":["Formulieren"],"Copy":["Kopi\u00ebren"],"Business":["Zakelijk"],"Next":["Volgende"],"Back":["Terug"],"Close":["Sluiten"],"Cancel":["Annuleren"],"This is where your form views will appear":["Hier verschijnen uw formulierweergaven"],"Install":["Installeren"],"Plugin Installation failed, Please try again later.":["Installatie van de plugin mislukt, probeer het later opnieuw."],"Plugin activation failed, Please try again later.":["Plug-in activering mislukt, probeer het later opnieuw."],"What's New?":["Wat is er nieuw?"],"Core":["Kern"],"Unlicensed":["Ongeautoriseerd"],"Upgrade":["Upgrade"],"Webhooks":["Webhooks"],"Install & Activate":["Installeren & Activeren"],"Contact Support":["Neem contact op met de ondersteuning"],"Conditional Logic":["Conditionele logica"],"Premium":["Premium"],"Welcome to SureForms!":["Welkom bij SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms is een WordPress-plugin waarmee gebruikers mooie formulieren kunnen maken via een drag-and-drop-interface, zonder dat er code nodig is. Het integreert met de WordPress-blokeditor."],"Read Full Guide":["Lees de volledige gids"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Aangepaste WordPress-formulieren GEMAKKELIJK GEMAAKT"],"No Date":["Geen datum"],"Invalid Date":["Ongeldige datum"],"Ready to go beyond free plan?":["Klaar om verder te gaan dan het gratis plan?"],"Upgrade now":["Nu upgraden"],"and unlock the full power of SureForms!":["en ontgrendel de volledige kracht van SureForms!"],"Upgrade SureForms":["Upgrade SureForms"],"Open Support Ticket":["Ondersteuningsticket openen"],"Help Center":["Helpcentrum"],"Join our Community on Facebook":["Word lid van onze community op Facebook"],"Leave Us a Review":["Laat een recensie achter"],"Quick Access":["Snelle toegang"],"Upgrade Now":["Nu upgraden"],"Clear Filters":["Filters wissen"],"Unnamed Form":["Naamloos formulier"],"Forms Overview":["Overzicht van formulieren"],"Clear Form Filters":["Formulierfilters wissen"],"Clear Date Filters":["Wis datumfilters"],"Select Date Range":["Selecteer datumbereik"],"Apply":["Toepassen"],"Please wait for the data to load":["Wacht alstublieft tot de gegevens zijn geladen"],"No entries to display":["Geen items om weer te geven"],"Once you create a form and start receiving submissions, the data will appear here.":["Zodra je een formulier maakt en inzendingen begint te ontvangen, zullen de gegevens hier verschijnen."],"Free":["Gratis"],"All Forms":["Alle formulieren"],"Guided Setup":["Geleide installatie"],"Exit Guided Setup":["Geleide installatie afsluiten"],"Skip":["Overslaan"],"Build beautiful forms visually":["Bouw prachtige formulieren visueel"],"Works perfectly on mobile":["Werkt perfect op mobiel"],"Easy to connect with automation tools":["Eenvoudig te verbinden met automatiseringstools"],"Welcome to SureForms":["Welkom bij SureForms"],"Smart, Quick and Powerful Forms.":["Slimme, snelle en krachtige formulieren."],"Let's Get Started":["Laten we beginnen"],"Build up to 10 forms using AI":["Bouw tot 10 formulieren met behulp van AI"],"A secure and private connection":["Een veilige en priv\u00e9verbinding"],"Smart form drafts based on your input":["Slimme formulierontwerpen op basis van uw invoer"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Beginnen met een leeg formulier is niet altijd gemakkelijk. Onze AI kan helpen door een conceptformulier te maken op basis van wat je probeert te doen \u2014 dit bespaart je tijd en geeft je een duidelijke richting."],"To do this, you'll need to connect your account.":["Om dit te doen, moet je je account verbinden."],"Let AI Help You Build Smarter, Faster Forms":["Laat AI u helpen om slimmere, snellere formulieren te maken"],"Here's what that gives you:":["Dit is wat dat je oplevert:"],"Continue":["Doorgaan"],"Connect":["Verbinden"],"Works smoothly with forms made using SureForms":["Werkt soepel met formulieren gemaakt met SureForms"],"Helps your emails reach the inbox instead of spam":["Helpt uw e-mails de inbox te bereiken in plaats van de spam"],"Setup is straightforward, even if you're not technical":["De installatie is eenvoudig, zelfs als je niet technisch bent"],"Lightweight and easy to use without adding clutter":["Lichtgewicht en eenvoudig te gebruiken zonder rommel toe te voegen"],"Make Sure Your Emails Get Delivered":["Zorg ervoor dat uw e-mails worden afgeleverd"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["De meeste WordPress-sites hebben moeite om e-mails betrouwbaar te verzenden, wat betekent dat formulierinzendingen van uw site mogelijk uw inbox niet bereiken \u2014 of in de spam terechtkomen."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail is een eenvoudige SMTP-plug-in die ervoor zorgt dat je e-mails daadwerkelijk worden afgeleverd."],"What you will get:":["Wat je zult krijgen:"],"Install SureMail":["Installeer SureMail"],"AI Form Generation":["AI Formuliergeneratie"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Ben je het beu om formulieren handmatig te maken? Laat AI het werk voor je doen. Beschrijf het gewoon en onze AI maakt in enkele seconden je perfecte formulier."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Breek complexe formulieren op in eenvoudige stappen, waardoor overweldiging wordt verminderd en voltooiingspercentages worden verhoogd. Leid gebruikers soepel door het proces"],"Conditional Fields":["Voorwaardelijke Velden"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Toon of verberg velden op basis van gebruikersantwoorden. Stel de juiste vragen en toon alleen wat nodig is om formulieren schoon en relevant te houden."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Verbeter uw formulieren met geavanceerde velden zoals multi-bestandsupload, beoordelingsvelden en datum- en tijdkiezers om rijkere, flexibele gegevens te verzamelen."],"Conversational Forms":["Gespreksvormen"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Maak formulieren die aanvoelen als een gesprek. E\u00e9n vraag tegelijk houdt gebruikers betrokken en maakt het invullen van formulieren eenvoudig."],"Digital Signatures":["Digitale handtekeningen"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Verzamel juridisch bindende digitale handtekeningen direct in uw formulieren voor overeenkomsten, goedkeuringen en contracten."],"Calculators":["Rekenmachines"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Voeg interactieve rekenmachines toe aan uw formulieren voor directe schattingen, offertes en berekeningen voor uw gebruikers."],"User Registration and Login":["Gebruikersregistratie en inloggen"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Sta bezoekers toe om zich te registreren en in te loggen op uw site. Nuttig voor lidmaatschappen, gemeenschappen of elke site die gebruikers toegang nodig heeft."],"PDF Generation Made Simple":["PDF-generatie eenvoudig gemaakt"],"Custom App":["Aangepaste app"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Verzamel gegevens, stuur ze naar externe applicaties voor verwerking en toon resultaten direct \u2014 alles naadloos ge\u00efntegreerd om dynamische, interactieve gebruikerservaringen te cre\u00ebren."],"Select Your Features":["Selecteer uw functies"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Krijg meer controle, snellere workflows en diepere aanpassingsmogelijkheden \u2014 allemaal ontworpen om je te helpen betere websites te bouwen met minder inspanning."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Geselecteerde functies vereisen %1$s - gebruik code %2$s om 10% korting te krijgen op elk plan."],"Copied":["Gekopieerd"],"Style your form to better match your site's design":["Stijl uw formulier zodat het beter bij het ontwerp van uw site past"],"Add spam protection to block common bot submissions":["Voeg spambeveiliging toe om veelvoorkomende botinzendingen te blokkeren"],"Get weekly email reports with a summary of form activity":["Ontvang wekelijkse e-mailrapporten met een samenvatting van de formulieractiviteit"],"You're All Set! \ud83d\ude80":["Je bent helemaal klaar! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Gebruik onze AI-formulierbouwer om snel aan de slag te gaan, of bouw je formulier vanaf nul als je al weet wat je nodig hebt. Je formulieren zijn klaar om te maken, te delen en te verbinden met je sitebezoekers."],"Final Touches That Make a Difference:":["Laatste details die het verschil maken:"],"Build Your First Form":["Bouw je eerste formulier"],"File Uploads":["Bestandsuploads"],"Signature & Rating":["Handtekening & Beoordeling"],"Calculation Forms":["Berekeningsformulieren"],"And Much More\u2026":["En nog veel meer\u2026"],"Upgrade to Pro":["Upgrade naar Pro"],"Unlock Premium Features":["Ontgrendel Premiumfuncties"],"Build Better Forms with SureForms":["Bouw betere formulieren met SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Voeg geavanceerde velden, conversatie-indelingen en slimme logica toe om formulieren te maken die gebruikers betrekken en betere gegevens vastleggen."],"SureForms Video Thumbnail":["SureForms Video Miniatuur"],"Payments":["Betalingen"],"Knowledge Base":["Kennisbank"],"What\u2019s New":["Wat is er nieuw"],"Importing\u2026":["Importeren\u2026"],"Learn":["Leren"],"Unable to complete action. Please try again.":["Actie kan niet worden voltooid. Probeer het opnieuw."],"Supercharge Your Workflow":["Geef je workflow een boost"],"Spam protection included":["Spam bescherming inbegrepen"],"Multistep Forms":["Meertrapsformulieren"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Stuur formulierinvoer direct naar elk extern systeem of eindpunt om geavanceerde workflows aan te sturen."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Zet formulierinvoeren automatisch om in schone, klaar-om-te-downloaden PDF's. Perfect voor archivering, delen, opslaan of het georganiseerd houden van zaken."],"Set up confirmation messages and email notifications for each entry":["Stel bevestigingsberichten en e-mailmeldingen in voor elke invoer"],"Something went wrong. Please try again.":["Er is iets misgegaan. Probeer het alstublieft opnieuw."],"Payment Forms":["Betalingsformulieren"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Ontvang betalingen direct via uw formulieren. Accepteer eenmalige en terugkerende betalingen naadloos."],"SureForms %s":["SureForms %s"],"First name is required.":["Voornaam is verplicht."],"Please enter a valid email address.":["Voer een geldig e-mailadres in."],"Email address is required.":["E-mailadres is vereist."],"This is required.":["Dit is vereist."],"Okay, just one last step\u2026":["Ok\u00e9, nog maar \u00e9\u00e9n laatste stap\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Help ons uw SureForms-ervaring aan te passen door iets over uzelf te delen."],"First Name":["Voornaam"],"Enter your first name":["Voer uw voornaam in"],"Last Name":["Achternaam"],"Enter your last name":["Voer uw achternaam in"],"Email Address":["E-mailadres"],"Enter your email address":["Voer uw e-mailadres in"],"Privacy Policy":["Privacybeleid"],"Finish":["Voltooien"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Blijf op de hoogte en help SureForms vorm te geven! Ontvang updates over functies en help ons SureForms te verbeteren door te delen hoe je de plugin gebruikt. Privacybeleid<\/a>."],"%d form ready":["%d formulier klaar"],"%d already imported":["%d al ge\u00efmporteerd"],"(unnamed source)":["(naamloze bron)"],"All forms already imported":["Alle formulieren zijn al ge\u00efmporteerd"],"Import forms":["Formulieren importeren"],"Import %d form":["%d formulier importeren"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Er is iets misgegaan tijdens het importeren. U kunt het opnieuw proberen, overslaan of Instellingen \u2192 Migratie openen."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Geen andere formulier-plugins gedetecteerd. U kunt op elk moment importeren via Instellingen \u2192 Migratie."],"Forms imported":["Formulieren ge\u00efmporteerd"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formulier van %2$s is nu in SureForms, klaar om te publiceren, stijlen en verbinden."],"%d form imported":["%d formulier ge\u00efmporteerd"],"%d form could not be imported":["%d formulier kon niet worden ge\u00efmporteerd"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d veldtype werd niet ondersteund. U kunt het handmatig opnieuw opbouwen in SureForms."],"Bring your existing forms with you":["Breng uw bestaande formulieren mee"],"We detected forms in another plugin. Pick one to import into SureForms.":["We hebben formulieren in een andere plugin gedetecteerd. Kies er een om te importeren in SureForms."],"Choose a form plugin to import from":["Kies een formulierplugin om uit te importeren"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Meer dan \u00e9\u00e9n plugin importeren? U kunt later extra bronnen importeren via Instellingen \u2192 Migratie."],"Import did not complete":["Importeren is niet voltooid"],"I'll do this later":["Ik doe dit later"],"Retry":["Opnieuw proberen"],"Form Checks":["Formuliercontroles"],"Collapse form checks":["Formuliercontroles samenvouwen"],"Expand form checks":["Formuliercontroles uitvouwen"],"Ignore":["Negeren"],"Working\u2026":["Bezig\u2026"],"Details":["Details"],"Copy details":["Details kopi\u00ebren"]}}} \ No newline at end of file diff --git a/languages/sureforms-nl_NL.mo b/languages/sureforms-nl_NL.mo index b1b9343de2519a683f0a951f43a4d88bc0bd3031..5ffcab50eba58578e6cd0c18bfe1baaab21269f5 100644 GIT binary patch delta 76504 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 delta 72809 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" msgstr "Verwacht formaat voor e-mails - email@sureforms.com of John Doe " -#: admin/admin.php:1471 -#: admin/admin.php:1472 +#: admin/admin.php:1517 +#: admin/admin.php:1518 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -12173,7 +12179,7 @@ msgstr "Verhoog uw SEO en klim moeiteloos in de zoekresultaten!" msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress." msgstr "Verhoog de zichtbaarheid van je website met slimme SEO-automatisering. Optimaliseer content, volg de prestaties van zoekwoorden en krijg bruikbare inzichten, allemaal binnen WordPress." -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -15372,12 +15378,12 @@ msgid "Stripe Settings" msgstr "Stripe-instellingen" #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. -#: admin/admin.php:2281 +#: admin/admin.php:2335 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version." msgstr "SureForms %1$s vereist minimaal %2$s %3$s om goed te functioneren. Werk alstublieft bij naar de nieuwste versie." -#: admin/admin.php:2294 +#: admin/admin.php:2348 msgid "Update Now" msgstr "Nu bijwerken" @@ -15425,8 +15431,8 @@ msgstr "Positie van het valutateken" msgid "Select the position of the currency symbol relative to the amount." msgstr "Selecteer de positie van het valutasymbool ten opzichte van het bedrag." -#: admin/admin.php:1414 -#: admin/admin.php:1415 +#: admin/admin.php:1460 +#: admin/admin.php:1461 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -15440,41 +15446,41 @@ msgstr "Selecteer de positie van het valutasymbool ten opzichte van het bedrag." msgid "Learn" msgstr "Leren" -#: admin/admin.php:2635 +#: admin/admin.php:2689 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!" msgstr "Geweldig! SureForms ondersteunt je formulieren en inzendingen - laten we samen blijven groeien!" -#: admin/admin.php:2636 +#: admin/admin.php:2690 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?" msgstr "Als SureForms nuttig is geweest, zou u dan een moment willen nemen om een 5-sterrenbeoordeling achter te laten op WordPress.org?" -#: admin/admin.php:2647 -#: admin/admin.php:2711 +#: admin/admin.php:2701 +#: admin/admin.php:2765 msgid "Maybe later" msgstr "Misschien later" -#: admin/admin.php:2653 +#: admin/admin.php:2707 msgid "I already did" msgstr "Ik heb het al gedaan" -#: admin/admin.php:2702 +#: admin/admin.php:2756 msgid "SureForms is ready to power your forms — explore what's possible!" msgstr "SureForms is klaar om uw formulieren van stroom te voorzien — ontdek wat er mogelijk is!" -#: admin/admin.php:2703 +#: admin/admin.php:2757 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard." msgstr "Beheer uw formulieren, volg inzendingen en ontdek functies zoals AI Form Builder, betalingsintegraties en meer vanaf het SureForms-dashboard." -#: admin/admin.php:2706 +#: admin/admin.php:2760 msgid "Go to Dashboard" msgstr "Ga naar Dashboard" -#: admin/admin.php:2717 +#: admin/admin.php:2771 msgid "I already know" msgstr "Ik weet het al" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2981 +#: admin/admin.php:3854 msgid "Invalid parameters." msgstr "Ongeldige parameters." @@ -15858,10 +15864,10 @@ msgid "This form is not yet available. Check back after the scheduled start time msgstr "Dit formulier is nog niet beschikbaar. Kom terug na de geplande starttijd." #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -15884,48 +15890,48 @@ msgstr "Dit formulier is nog niet beschikbaar. Kom terug na de geplande starttij msgid "Security verification failed. Please refresh the page and try again." msgstr "Beveiligingsverificatie mislukt. Vernieuw de pagina en probeer het opnieuw." -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "Formuliergegevens werden niet gevonden." -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "Formulier-ID ontbreekt." -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "Sorry, je hebt geen toestemming om deze bron te openen." -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "U heeft geen toestemming om dit formulier in te dienen." -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "Dit formulier is niet langer beschikbaar." -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "Controleer het formulier op fouten." -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "Je inzending is gemarkeerd als spam. Probeer het opnieuw." -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "Kan formulier niet verzenden. Probeer het opnieuw." -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "Er is geen SMTP-plug-in gedetecteerd. Configureer een SMTP-plug-in om het verzenden van e-mails mogelijk te maken." -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "E-mail verzenden mislukt om een onbekende reden." -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "Er zijn geen e-mails verzonden." @@ -16438,7 +16444,7 @@ msgid "Duplicate form?" msgstr "Formulier dupliceren?" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:938 msgid "An error occurred while submitting your form. Please try again." msgstr "Er is een fout opgetreden bij het indienen van uw formulier. Probeer het alstublieft opnieuw." @@ -16815,28 +16821,28 @@ msgstr "Webhook-geheim niet geconfigureerd." msgid "Invalid webhook signature." msgstr "Ongeldige webhook-handtekening." -#: admin/admin.php:1253 -#: admin/admin.php:1851 +#: admin/admin.php:1299 +#: admin/admin.php:1905 #: assets/build/formEditor.js:172 msgid "Quizzes" msgstr "Quizzen" -#: admin/admin.php:1252 +#: admin/admin.php:1298 msgid "Quiz Entries" msgstr "Quizinzendingen" -#: admin/admin.php:1255 -#: admin/admin.php:1288 -#: admin/admin.php:1321 +#: admin/admin.php:1301 +#: admin/admin.php:1334 +#: admin/admin.php:1367 #: assets/build/settings.js:172 msgid "New" msgstr "Nieuw" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "Ongeldig formulier." -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "Te veel verzoeken. Probeer het binnenkort opnieuw." @@ -17345,9 +17351,9 @@ msgstr "Perfect voor educatie, training en leuke trivia" msgid "Select this to create a quiz with scored questions and graded results." msgstr "Selecteer dit om een quiz te maken met gescoorde vragen en beoordeelde resultaten." -#: admin/admin.php:1285 -#: admin/admin.php:1286 -#: admin/admin.php:1856 +#: admin/admin.php:1331 +#: admin/admin.php:1332 +#: admin/admin.php:1910 msgid "Survey Reports" msgstr "Onderzoeksrapporten" @@ -17753,7 +17759,7 @@ msgid "Error creating SureForms Form." msgstr "Fout bij het maken van SureForms-formulier." #. translators: %s represents the minimum number of characters required -#: inc/field-validation.php:426 +#: inc/field-validation.php:465 #: inc/translatable.php:94 #, php-format msgid "Please enter at least %s characters." @@ -17911,9 +17917,9 @@ msgstr "De AI-reactie was leeg. Gelieve uw prompt te verfijnen en het opnieuw te msgid "Unable to build form fields from the AI response." msgstr "Kan formuliervelden niet opbouwen vanuit de AI-reactie." -#: admin/admin.php:1318 -#: admin/admin.php:1319 -#: admin/admin.php:1861 +#: admin/admin.php:1364 +#: admin/admin.php:1365 +#: admin/admin.php:1915 msgid "Partial Entries" msgstr "Gedeeltelijke invoer" @@ -18784,23 +18790,23 @@ msgstr "Het betalingsbedrag kon niet worden geverifieerd voor dit formulier. Bew msgid "Cancellation is not supported for this payment gateway." msgstr "Annulering wordt niet ondersteund voor deze betalingsgateway." -#: admin/admin.php:3039 +#: admin/admin.php:3203 msgid "SureForms AI Quick Draft" msgstr "SureForms AI Snelle Schets" -#: admin/admin.php:3058 +#: admin/admin.php:3222 msgid "Describe the form and let SureForms AI generate it for you." msgstr "Beschrijf het formulier en laat SureForms AI het voor je genereren." -#: admin/admin.php:3061 +#: admin/admin.php:3225 msgid "Describe your form" msgstr "Beschrijf je formulier" -#: admin/admin.php:3068 +#: admin/admin.php:3232 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "Voorbeeld: Maak een contactformulier met velden voor naam, e-mail, telefoon en bericht." -#: admin/admin.php:3109 +#: admin/admin.php:3273 msgid "Redirecting..." msgstr "Omleiden..." @@ -18813,14 +18819,14 @@ msgid "The prompt is too long. Please shorten it and try again." msgstr "De prompt is te lang. Verkort deze en probeer het opnieuw." #. translators: %s: maximum characters allowed before the @ symbol. -#: inc/field-validation.php:448 +#: inc/field-validation.php:487 #: inc/translatable.php:97 #, php-format msgid "The part before @ may not exceed %s characters." msgstr "Het gedeelte voor @ mag niet meer dan %s tekens bevatten." #. translators: %s: maximum characters allowed after the @ symbol. -#: inc/field-validation.php:454 +#: inc/field-validation.php:493 #: inc/translatable.php:100 #, php-format msgid "The part after @ may not exceed %s characters." @@ -18907,74 +18913,74 @@ msgstr "Sorry, je mag dit formulier niet weergeven." msgid "This form requires a payment. Please complete the payment and submit again." msgstr "Dit formulier vereist een betaling. Voltooi de betaling en dien opnieuw in." -#: admin/admin.php:552 +#: admin/admin.php:598 msgid "You are not allowed to update this prompt." msgstr "Je mag deze prompt niet bijwerken." -#: admin/admin.php:595 +#: admin/admin.php:641 msgid "Finish setting up your form" msgstr "Voltooi het instellen van je formulier" -#: admin/admin.php:627 +#: admin/admin.php:673 msgid "Review or edit your form" msgstr "Bekijk of bewerk je formulier" -#: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:674 +#: admin/admin.php:4515 msgid "Edit form" msgstr "Formulier bewerken" -#: admin/admin.php:633 +#: admin/admin.php:679 msgid "Personalize the Thank You message" msgstr "Personaliseer het bedankbericht" -#: admin/admin.php:634 +#: admin/admin.php:680 msgid "Edit message" msgstr "Bericht bewerken" -#: admin/admin.php:639 +#: admin/admin.php:685 msgid "Choose who gets notified of new replies" msgstr "Kies wie op de hoogte wordt gesteld van nieuwe reacties" -#: admin/admin.php:640 +#: admin/admin.php:686 msgid "Set up email" msgstr "E-mail instellen" -#: admin/admin.php:648 +#: admin/admin.php:694 msgid "your form" msgstr "je formulier" #. translators: %s: form title. -#: admin/admin.php:651 +#: admin/admin.php:697 #, php-format msgid "Finish setting up %s" msgstr "Voltooi het instellen van %s" #. translators: %s: form title. -#: admin/admin.php:659 +#: admin/admin.php:705 #, php-format msgid "View %s (opens in a new tab)" msgstr "Bekijk %s (opent in een nieuw tabblad)" -#: admin/admin.php:664 +#: admin/admin.php:710 msgid "Customize your form to get it ready for real submissions:" msgstr "Pas je formulier aan om het klaar te maken voor echte inzendingen:" -#: admin/admin.php:3896 +#: admin/admin.php:4504 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "We hebben dit formulier al voor je gemaakt. Voltooi de aanpassing zodat het klaar is om echte inzendingen te verzamelen." #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4509 #, php-format msgid "Finish setting up “%s”" msgstr "Voltooi het instellen van “%s”" -#: admin/admin.php:3914 +#: admin/admin.php:4522 msgid "Edit the Thank You message" msgstr "Bewerk het bedankbericht" -#: admin/admin.php:3920 +#: admin/admin.php:4528 msgid "Set where replies go" msgstr "Stel in waar reacties naartoe gaan" @@ -19002,123 +19008,90 @@ msgstr "Kies een afbeeldingsbestand." msgid "This form could not be submitted. Please refresh the page and try again." msgstr "Dit formulier kon niet worden verzonden. Vernieuw de pagina en probeer het opnieuw." -#: admin/admin.php:2341 -#: admin/admin.php:2430 +#: admin/admin.php:2395 +#: admin/admin.php:2484 msgid "Your SureForms database is up to date. New form entries will be saved as usual." msgstr "Uw SureForms-database is up-to-date. Nieuwe formulierinzendingen worden zoals gebruikelijk opgeslagen." -#: admin/admin.php:2355 -#: admin/admin.php:2439 +#: admin/admin.php:2409 +#: admin/admin.php:2493 #: inc/rest-api.php:1447 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support." msgstr "SureForms kon de database niet voltooien. Uw hosting staat mogelijk niet toe dat SureForms databasetabellen maakt — neem contact op met uw hostingprovider of SureForms-ondersteuning." -#: admin/admin.php:2358 +#: admin/admin.php:2412 msgid "Contact support" msgstr "Contact opnemen met ondersteuning" -#: admin/admin.php:2378 +#: admin/admin.php:2432 msgid "Database update needed" msgstr "Database-update nodig" -#: admin/admin.php:2384 +#: admin/admin.php:2438 msgid "Fix now" msgstr "Nu oplossen" -#: admin/admin.php:2453 +#: admin/admin.php:2507 msgid "SureForms — database update needed" msgstr "SureForms — database-update nodig" -#: admin/admin.php:2458 +#: admin/admin.php:2512 msgid "Update database" msgstr "Database bijwerken" -#: admin/admin.php:2478 +#: admin/admin.php:2532 msgid "You do not have permission to update the database." msgstr "U heeft geen toestemming om de database bij te werken." -#: admin/admin.php:3437 +#: admin/admin.php:3669 msgid "You do not have permission to do this." msgstr "U heeft geen toestemming om dit te doen." -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "%d formulierinzending op rij kon niet worden voltooid." -msgstr[1] "" - -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." -msgstr "Bezoekers kunnen u mogelijk niet bereiken en die inzendingen zijn niet opgeslagen." - -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." -msgstr "Formulierinzendingen worden normaal voltooid." - -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." -msgstr "Er is geen caching-plugin gevonden die moet worden geconfigureerd." - #. translators: %s: caching plugin name. -#: admin/admin.php:3551 +#: admin/admin.php:4209 msgid "%s may interfere with your forms." msgstr "%s kan uw formulieren verstoren." -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." -msgstr "Caching en JavaScript-optimalisatie kunnen een verouderde kopie van uw formulier serveren of de scripts in de verkeerde volgorde laden." - -#: admin/admin.php:3555 +#: admin/admin.php:4185 +#: admin/admin.php:4213 msgid "Help Me Fix" msgstr "Help mij oplossen" -#: admin/admin.php:3650 +#: admin/admin.php:4258 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "SureForms heeft uw formulierinzendingen gevonden onder een andere databasetabelprefix. Het opnieuw verbinden duurt een moment en uw bestaande inzendingen blijven behouden." -#: admin/admin.php:3653 +#: admin/admin.php:4261 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "SureForms moet uw database bijwerken voordat het nieuwe formulierinzendingen kan opslaan. Dit duurt slechts een moment en verandert uw formulieren of bestaande inhoud niet. Inzendingen die voor nu zijn ingediend, kunnen hier niet worden hersteld." #. translators: %s: site host. -#: admin/admin.php:4265 +#: admin/admin.php:5032 msgid "SureForms: form submissions are failing on %s" msgstr "SureForms: formulierinzendingen mislukken op %s" -#: admin/admin.php:4274 +#: admin/admin.php:4990 msgid "Debug log: no entries recorded." msgstr "Debuglog: geen inzendingen geregistreerd." #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4995 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "Debuglog (meest recente %1$d van %2$d inzendingen)" -#: admin/admin.php:4288 -msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." -msgstr "Oudere inzendingen zijn weggelaten om deze e-mail binnen de lengte te houden die een mailclient accepteert. Het volledige logboek kan worden gedownload van SureForms → Instellingen → Algemeen." - -#: admin/admin.php:4318 +#: admin/admin.php:5158 msgid "Hello SureForms support," msgstr "Hallo SureForms-ondersteuning," -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." -msgstr[0] "SureForms heeft %d formulierinzending op rij geregistreerd die niet kon worden voltooid." -msgstr[1] "" - -#: admin/admin.php:4332 +#: admin/admin.php:5182 msgid "Site details" msgstr "Sitegegevens" -#: admin/admin.php:4335 +#: admin/admin.php:5185 msgid "not active" msgstr "niet actief" -#: admin/admin.php:4338 +#: admin/admin.php:5188 msgid "none detected" msgstr "geen gedetecteerd" @@ -19126,7 +19099,7 @@ msgstr "geen gedetecteerd" msgid "No form submission failures have been recorded." msgstr "Er zijn geen mislukte formulierinzendingen geregistreerd." -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "Niet gevonden." @@ -19211,6 +19184,193 @@ msgstr "Formulierweergaven & Conversie" msgid "Logs" msgstr "Logs" +#: admin/admin.php:2831 +msgid "Previous notice" +msgstr "Vorige melding" + +#: admin/admin.php:2832 +msgid "Next notice" +msgstr "Volgende melding" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2834 +msgid "%1$d of %2$d" +msgstr "%1$d van %2$d" + +#: admin/admin.php:2891 +msgid "Nothing to report." +msgstr "Niets te melden." + +#: admin/admin.php:4057 +#: assets/build/dashboard.js:172 +msgid "Details" +msgstr "Details" + +#: admin/admin.php:4058 +msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it." +msgstr "Wat we over dit probleem hebben vastgelegd. Kopieer het in uw ondersteuningsverzoek, zodat we kunnen beginnen bij de oorzaak in plaats van een beschrijving ervan." + +#: admin/admin.php:4059 +#: assets/build/dashboard.js:172 +msgid "Copy details" +msgstr "Details kopiëren" + +#: admin/admin.php:4068 +msgid "Copy the details first, so you have them to paste." +msgstr "Kopieer eerst de details, zodat u ze kunt plakken." + +#: admin/admin.php:4072 +msgid "Copied. Contact Support is now available." +msgstr "Gekopieerd. Contact opnemen met ondersteuning is nu beschikbaar." + +#: admin/admin.php:4073 +msgid "Your browser would not let us copy. Select the text above and copy it by hand." +msgstr "Uw browser liet ons niet kopiëren. Selecteer de tekst hierboven en kopieer deze handmatig." + +#: admin/admin.php:4076 +msgid "Recorded diagnostics" +msgstr "Opgenomen diagnostiek" + +#: admin/admin.php:4080 +msgid "Collecting the details…" +msgstr "Details verzamelen…" + +#: admin/admin.php:4081 +msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there." +msgstr "We konden de details niet verzamelen. Neem contact op met de ondersteuning en beschrijf wat er is gebeurd, dan nemen we het vanaf daar over." + +#. translators: %s: form title. +#: admin/admin.php:4108 +msgid "We noticed a form submission failure on %s." +msgstr "We hebben een fout bij het indienen van het formulier opgemerkt op %s." + +#: admin/admin.php:4109 +msgid "We noticed a form submission failure." +msgstr "We hebben een fout bij het indienen van het formulier opgemerkt." + +#: admin/admin.php:4110 +msgid "Visitors may not be able to reach you, and their entries were not saved." +msgstr "Bezoekers kunnen u mogelijk niet bereiken en hun inzendingen zijn niet opgeslagen." + +#. translators: %s: form title. +#: admin/admin.php:4115 +msgid "We noticed a notification failure on %s." +msgstr "We hebben een fout bij de melding opgemerkt op %s." + +#: admin/admin.php:4116 +msgid "We noticed a notification failure." +msgstr "We hebben een fout bij de melding opgemerkt." + +#: admin/admin.php:4117 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "De inzending is opgeslagen, maar we konden de e-mail erover niet verzenden. Nieuwe inzendingen kunnen binnenkomen zonder dat u het weet." + +#. translators: %s: form title. +#: admin/admin.php:4132 +msgid "We noticed an integration failure on %s." +msgstr "We hebben een fout bij de integratie opgemerkt op %s." + +#: admin/admin.php:4133 +msgid "We noticed an integration failure." +msgstr "We hebben een fout bij de integratie opgemerkt." + +#: admin/admin.php:4134 +msgid "The entry was saved, but we could not send it to a connected service." +msgstr "De inzending is opgeslagen, maar we konden deze niet naar een verbonden service sturen." + +#: admin/admin.php:4158 +msgid "View details" +msgstr "Details bekijken" + +#: admin/admin.php:4212 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "Caching kan bezoekers een oude kopie van uw formulier laten zien of de scripts in de verkeerde volgorde laden." + +#: admin/admin.php:5005 +msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General." +msgstr "Oudere inzendingen zijn weggelaten om dit fragment leesbaar te houden. Het volledige logboek kan worden gedownload van SureForms → Instellingen → Algemeen." + +#. translators: %s: site host. +#: admin/admin.php:5034 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "SureForms heeft formulierinzendingen geregistreerd op %s die niet konden worden voltooid." + +#. translators: %s: site host. +#: admin/admin.php:5038 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "SureForms: notificatie-e-mails worden niet verzonden op %s" + +#. translators: %s: site host. +#: admin/admin.php:5040 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "SureForms heeft inzendingen opgeslagen op %s maar kon de notificatie-e-mails ervoor niet verzenden." + +#. translators: %s: site host. +#: admin/admin.php:5044 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "SureForms: een integratie ontvangt geen inzendingen op %s" + +#. translators: %s: site host. +#: admin/admin.php:5046 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "SureForms heeft inzendingen opgeslagen op %s maar kon deze niet doorsturen naar een verbonden service." + +#. translators: %s: site host. +#: admin/admin.php:5056 +msgid "SureForms: a problem with the forms on %s" +msgstr "SureForms: een probleem met de formulieren op %s" + +#. translators: %s: site host. +#: admin/admin.php:5058 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "SureForms heeft een probleem met de formulieren op %s geregistreerd." + +#. translators: %d: number of failed submissions. +#: admin/admin.php:5083 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." +msgstr[0] "SureForms heeft %d formulierinzending geregistreerd die niet kon worden voltooid." +msgstr[1] "" + +#. translators: %d: number of failed notifications. +#: admin/admin.php:5095 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "SureForms heeft %d inzending opgeslagen maar kon de notificatie-e-mail ervoor niet verzenden." +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:5107 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "SureForms heeft %d inzending opgeslagen maar kon deze niet doorsturen naar een verbonden service." +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:5119 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "SureForms heeft %d probleem met de formulieren op deze site geregistreerd." +msgstr[1] "" + +#. translators: %s: form title. +#: admin/admin.php:5169 +msgid "Form: %s" +msgstr "Formulier: %s" + +#: admin/admin.php:5189 +msgid "none recorded" +msgstr "geen geregistreerd" + +#. translators: %s: date and time of the previous report, in the site's timezone. +#: admin/admin.php:5212 +msgid "Previously reported: %s" +msgstr "Eerder gemeld: %s" + +#: assets/build/settings.js:172 +msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures." +msgstr "Weergaven en inzendingen door iedereen die de site kan bewerken, worden normaal weggelaten, zodat het testen van uw eigen formulieren deze cijfers niet verandert." + #: inc/post-types.php:265 msgctxt "post type general name" msgid "Forms" diff --git a/languages/sureforms-pl_PL-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-pl_PL-51635fe6489fc8288d603fe596c755ca.json index 5de595f4f..eeacebb35 100644 --- a/languages/sureforms-pl_PL-51635fe6489fc8288d603fe596c755ca.json +++ b/languages/sureforms-pl_PL-51635fe6489fc8288d603fe596c755ca.json @@ -1 +1 @@ -{"translation-revision-date":"2025-01-02T08:44:41+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Panel sterowania"],"Settings":["Ustawienia"],"Entries":["Wpisy"],"Status":["Status"],"Form":["Formularz"],"Activated":["Aktywowany"],"Activate":["Aktywuj"],"Address":["Adres"],"Checkbox":["Pole wyboru"],"Dropdown":["Lista rozwijana"],"Email":["E-mail"],"Number":["Numer"],"Phone":["Telefon"],"Textarea":["Pole tekstowe"],"Monday":["Poniedzia\u0142ek"],"Forms":["Formularze"],"New Form Submission - %s":["Nowe przes\u0142anie formularza - %s"],"GitHub":["GitHub"],"(no title)":["(brak tytu\u0142u)"],"General":["Og\u00f3lny"],"No tags available":["Brak dost\u0119pnych tag\u00f3w"],"Back":["Wstecz"],"Generic tags":["Og\u00f3lne tagi"],"Other":["Inne"],"Confirm":["Potwierd\u017a"],"Cancel":["Anuluj"],"Install":["Zainstaluj"],"Plugin Installation failed, Please try again later.":["Instalacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"Plugin activation failed, Please try again later.":["Aktywacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"Integrations":["Integracje"],"What's New?":["Co nowego?"],"Core":["Rdze\u0144"],"Unlicensed":["Bez licencji"],"Connecting\u2026":["\u0141\u0105czenie\u2026"],"Install & Activate":["Zainstaluj i aktywuj"],"Compliance Settings":["Ustawienia zgodno\u015bci"],"Enable GDPR Compliance":["W\u0142\u0105cz zgodno\u015b\u0107 z RODO"],"Never store entry data after form submission":["Nigdy nie przechowuj danych wej\u015bciowych po przes\u0142aniu formularza"],"When enabled this form will never store Entries.":["Gdy jest w\u0142\u0105czony, ten formularz nigdy nie b\u0119dzie przechowywa\u0142 wpis\u00f3w."],"Automatically delete entries":["Automatycznie usu\u0144 wpisy"],"When enabled this form will automatically delete entries after a certain period of time.":["Po w\u0142\u0105czeniu ten formularz automatycznie usunie wpisy po okre\u015blonym czasie."],"Entries older than the days set will be deleted automatically.":["Wpisy starsze ni\u017c ustawione dni zostan\u0105 usuni\u0119te automatycznie."],"Visual":["Wizualny"],"HTML":["HTML"],"All Data":["Wszystkie dane"],"Add Shortcode":["Dodaj shortcode"],"Form input tags":["Tagi wej\u015bciowe formularza"],"Comma separated values are also accepted.":["Warto\u015bci oddzielone przecinkami s\u0105 r\u00f3wnie\u017c akceptowane."],"Email Notification":["Powiadomienie e-mail"],"Name":["Imi\u0119"],"Send Email To":["Wy\u015blij e-mail do"],"Subject":["Temat"],"CC":["DW"],"BCC":["UDW"],"Reply To":["Odpowiedz do"],"Add Key":["Dodaj klucz"],"Add Value":["Dodaj warto\u015b\u0107"],"Add":["Dodaj"],"Confirmation Message":["Wiadomo\u015b\u0107 potwierdzaj\u0105ca"],"After Form Submission":["Po przes\u0142aniu formularza"],"Hide Form":["Ukryj formularz"],"Reset Form":["Zresetuj formularz"],"Custom URL":["Niestandardowy URL"],"Add Query Parameters":["Dodaj parametry zapytania"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Wybierz, je\u015bli chcesz doda\u0107 pary klucz-warto\u015b\u0107 dla p\u00f3l formularza do uwzgl\u0119dnienia w parametrach zapytania"],"Query Parameters":["Parametry zapytania"],"Success Message":["Komunikat o sukcesie"],"Redirect":["Przekieruj"],"Redirect to":["Przekieruj do"],"Page":["Strona"],"Form Confirmation":["Potwierdzenie formularza"],"Confirmation Type":["Typ potwierdzenia"],"Clear":["Wyczy\u015b\u0107"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Niewidoczna"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Walidacje"],"Spam Protection":["Ochrona przed spamem"],"Email Summaries":["Podsumowania e-maili"],"Tuesday":["Wtorek"],"Wednesday":["\u015aroda"],"Thursday":["Czwartek"],"Friday":["Pi\u0105tek"],"Saturday":["Sobota"],"Sunday":["Niedziela"],"Test Email":["Testowy email"],"Schedule Reports":["Zaplanuj raporty"],"IP Logging":["Rejestrowanie IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Je\u015bli ta opcja jest w\u0142\u0105czona, adres IP u\u017cytkownika zostanie zapisany wraz z danymi formularza"],"Confirmation Email Mismatch Message":["Wiadomo\u015b\u0107 o niezgodno\u015bci e-maila potwierdzaj\u0105cego"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s oznacza minimaln\u0105 warto\u015b\u0107 wej\u015bciow\u0105. Na przyk\u0142ad: \"Minimalna warto\u015b\u0107 to 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s oznacza maksymaln\u0105 warto\u015b\u0107 wej\u015bciow\u0105. Na przyk\u0142ad: \"Maksymalna warto\u015b\u0107 to 100.\""],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s oznacza minimaln\u0105 liczb\u0119 wymaganych wybor\u00f3w. Na przyk\u0142ad: \u201eWymagane s\u0105 co najmniej 2 wybory\u201d."],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s oznacza maksymaln\u0105 liczb\u0119 dozwolonych wybor\u00f3w. Na przyk\u0142ad: \u201eDozwolone s\u0105 maksymalnie 4 wybory.\u201d"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s oznacza minimaln\u0105 liczb\u0119 potrzebnych wybor\u00f3w. Na przyk\u0142ad: \u201eWymagany jest co najmniej 1 wyb\u00f3r.\u201d"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s oznacza maksymaln\u0105 liczb\u0119 dozwolonych wybor\u00f3w. Na przyk\u0142ad: \u201eDozwolone s\u0105 maksymalnie 3 wybory\u201d."]," Error Message":["Komunikat o b\u0142\u0119dzie"],"Auto":["Samoch\u00f3d"],"Light":["\u015awiat\u0142o"],"Dark":["Ciemny"],"Turnstile":["Bramka"],"Honeypot":["Pu\u0142apka"],"Get Keys":["Pobierz klucze"],"Documentation":["Dokumentacja"],"Site Key":["Klucz strony"],"Secret Key":["Klucz tajny"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Tryb wygl\u0105du"],"Enable Honeypot Security":["W\u0142\u0105cz zabezpieczenie Honeypot"],"Enable Honeypot Security for better spam protection":["W\u0142\u0105cz Honeypot Security dla lepszej ochrony przed spamem"],"Text":["Tekst"],"OttoKit":["OttoKit"],"Connect with OttoKit":["Po\u0142\u0105cz si\u0119 z OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Bie\u017c\u0105cy adres \u201eOd e-mail\u201d nie pasuje do nazwy domeny Twojej strony internetowej (%1$s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami b\u0119d\u0105 blokowane lub oznaczane jako spam. Alternatywnie, spr\u00f3buj u\u017cy\u0107 adresu Od, kt\u00f3ry pasuje do domeny Twojej strony internetowej (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Obecny adres \u201eOd e-mail\u201d nie pasuje do nazwy domeny Twojej strony internetowej (%s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami zostan\u0105 zablokowane lub oznaczone jako spam."],"We strongly recommend that you install the free ":["Zalecamy zdecydowanie zainstalowanie darmowego"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["wtyczka! Kreator konfiguracji u\u0142atwia napraw\u0119 Twoich e-maili."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternatywnie spr\u00f3buj u\u017cy\u0107 adresu nadawcy, kt\u00f3ry pasuje do domeny Twojej strony internetowej (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Prosz\u0119 wprowadzi\u0107 prawid\u0142owy adres e-mail. Powiadomienia nie b\u0119d\u0105 wysy\u0142ane, je\u015bli pole nie zostanie wype\u0142nione poprawnie."],"From Name":["Od Nazwa"],"From Email":["Z e-maila"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Bie\u017c\u0105cy adres \u201eOd e-mail\u201d mo\u017ce nie pasowa\u0107 do nazwy domeny Twojej strony internetowej (%1$s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami zostan\u0105 zablokowane lub oznaczone jako spam. Alternatywnie, spr\u00f3buj u\u017cy\u0107 adresu Od, kt\u00f3ry pasuje do domeny Twojej strony internetowej (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Obecny adres \u201eOd e-mail\u201d mo\u017ce nie pasowa\u0107 do nazwy domeny Twojej strony internetowej (%s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami zostan\u0105 zablokowane lub oznaczone jako spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Gotowy, aby wyj\u015b\u0107 poza darmowy plan?"],"Upgrade now":["Zaktualizuj teraz"],"and unlock the full power of SureForms!":["i odblokuj pe\u0142n\u0105 moc SureForms!"],"Upgrade SureForms":["Uaktualnij SureForms"],"Upgrade Now":["Zaktualizuj teraz"],"Entries older than the selected days will be deleted.":["Wpisy starsze ni\u017c wybrane dni zostan\u0105 usuni\u0119te."],"Entries Time Period":["Okres czasu wpis\u00f3w"],"Notifications can use only one From Email so please enter a single address.":["Powiadomienia mog\u0105 u\u017cywa\u0107 tylko jednego adresu e-mail nadawcy, wi\u0119c prosz\u0119 poda\u0107 jeden adres."],"Select Page to redirect":["Wybierz stron\u0119 do przekierowania"],"Search for a page":["Wyszukaj stron\u0119"],"Select a page":["Wybierz stron\u0119"],"Form Validation":["Walidacja formularza"],"Required Error Messages":["Wymagane komunikaty o b\u0142\u0119dach"],"Other Error Messages":["Inne komunikaty o b\u0142\u0119dach"],"Input Field Unique":["Pole wej\u015bciowe unikalne"],"Email Field Unique":["Pole e-mail musi by\u0107 unikalne"],"Invalid URL":["Nieprawid\u0142owy URL"],"Phone Field Unique":["Pole telefonu unikalne"],"Invalid Field Number Block":["Nieprawid\u0142owy blok numeru pola"],"Invalid Email":["Nieprawid\u0142owy adres e-mail"],"Number Minimum Value":["Minimalna warto\u015b\u0107 liczby"],"Number Maximum Value":["Maksymalna warto\u015b\u0107 liczby"],"Dropdown Minimum Selections":["Minimalna liczba wybor\u00f3w w rozwijanym menu"],"Dropdown Maximum Selections":["Maksymalna liczba wybor\u00f3w w rozwijanym menu"],"Multiple Choice Minimum Selections":["Minimalna liczba wybor\u00f3w w pytaniach wielokrotnego wyboru"],"Multiple Choice Maximum Selections":["Wielokrotny wyb\u00f3r maksymalna liczba zaznacze\u0144"],"Input Field":["Pole wej\u015bciowe"],"Email Field":["Pole e-mail"],"URL Field":["Pole URL"],"Phone Field":["Pole telefonu"],"Textarea Field":["Pole tekstowe"],"Checkbox Field":["Pole wyboru"],"Dropdown Field":["Pole rozwijane"],"Multiple Choice Field":["Pole wyboru wielokrotnego"],"Address Field":["Pole adresu"],"Number Field":["Pole liczby"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Aby w\u0142\u0105czy\u0107 funkcj\u0119 reCAPTCHA w SureForms, w\u0142\u0105cz opcj\u0119 reCAPTCHA w ustawieniach blok\u00f3w i wybierz wersj\u0119. Dodaj tutaj tajny klucz i klucz witryny Google reCAPTCHA. reCAPTCHA zostanie dodana do Twojej strony na froncie."],"Enter your %s here":["Wprowad\u017a tutaj sw\u00f3j %s"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Aby w\u0142\u0105czy\u0107 hCAPTCHA, dodaj klucz witryny i klucz tajny. Skonfiguruj te ustawienia w poszczeg\u00f3lnych formularzach."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Aby w\u0142\u0105czy\u0107 Cloudflare Turnstile, dodaj klucz witryny i klucz tajny. Skonfiguruj te ustawienia w ramach poszczeg\u00f3lnego formularza."],"Save":["Zapisz"],"Anonymous Analytics":["Anonimowa Analiza"],"Learn More":["Dowiedz si\u0119 wi\u0119cej"],"Admin Notification":["Powiadomienie administratora"],"Enable Admin Notification":["W\u0142\u0105cz powiadomienia administratora"],"Admin notifications keep you informed about new form entries since your last visit.":["Powiadomienia administracyjne informuj\u0105 Ci\u0119 o nowych wpisach formularza od Twojej ostatniej wizyty."],"Skip":["Pomi\u0144"],"Continue":["Kontynuuj"],"Maximum Number of Entries":["Maksymalna liczba wpis\u00f3w"],"Maximum Entries":["Maksymalna liczba wpis\u00f3w"],"Response Description After Maximum Entries":["Opis odpowiedzi po maksymalnej liczbie wpis\u00f3w"],"Get Started":["Rozpocznij"],"Integration":["Integracja"],"Connect Native Integrations with SureForms":["Po\u0142\u0105cz natywne integracje z SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Odblokuj pot\u0119\u017cne integracje w planie Premium, aby zautomatyzowa\u0107 swoje przep\u0142ywy pracy i po\u0142\u0105czy\u0107 SureForms bezpo\u015brednio z ulubionymi narz\u0119dziami."],"Send form submissions straight to CRMs, email, and marketing platforms":["Wysy\u0142aj zg\u0142oszenia formularzy bezpo\u015brednio do CRM, e-maili i platform marketingowych"],"Automate repetitive tasks with seamless data syncing":["Zautomatyzuj powtarzalne zadania dzi\u0119ki p\u0142ynnej synchronizacji danych"],"Access exclusive native integrations for faster workflows":["Uzyskaj dost\u0119p do ekskluzywnych natywnych integracji dla szybszych przep\u0142yw\u00f3w pracy"],"Expected format for emails - email@sureforms.com or John Doe ":["Oczekiwany format dla e-maili - email@sureforms.com lub John Doe "],"Payments":["P\u0142atno\u015bci"],"Stripe account disconnected successfully.":["Konto Stripe zosta\u0142o pomy\u015blnie od\u0142\u0105czone."],"Failed to create webhook.":["Nie uda\u0142o si\u0119 utworzy\u0107 webhooka."],"Failed to connect to Stripe.":["Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 ze Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Baza wiedzy"],"What\u2019s New":["Co nowego"],"out of":["z"],"No entries found":["Nie znaleziono wpis\u00f3w"],"delete":["usu\u0144"],"Please type \"%s\" in the input box":["Prosz\u0119 wpisa\u0107 \"%s\" w polu wej\u015bciowym"],"To confirm, type \"%s\" in the box below:":["Aby potwierdzi\u0107, wpisz \"%s\" w polu poni\u017cej:"],"Type \"%s\"":["Wpisz \"%s\""],"Go to OttoKit Settings":["Przejd\u017a do ustawie\u0144 OttoKit"],"USD - US Dollar":["USD - Dolar ameryka\u0144ski"],"Payment Mode":["Tryb p\u0142atno\u015bci"],"Test Mode":["Tryb testowy"],"Live Mode":["Tryb na \u017cywo"],"Action":["Akcja"],"General Settings":["Ustawienia og\u00f3lne"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Skonfiguruj podsumowania e-mail, alerty administratora i preferencje danych, aby \u0142atwo zarz\u0105dza\u0107 swoimi formularzami."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Dostosuj domy\u015blne komunikaty o b\u0142\u0119dach wy\u015bwietlane, gdy u\u017cytkownicy przesy\u0142aj\u0105 nieprawid\u0142owe lub niekompletne wpisy formularza."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["W\u0142\u0105cz ochron\u0119 przed spamem dla swoich formularzy, korzystaj\u0105c z us\u0142ug CAPTCHA lub zabezpiecze\u0144 typu honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Po\u0142\u0105cz i zarz\u0105dzaj swoimi bramkami p\u0142atno\u015bci, aby bezpiecznie akceptowa\u0107 transakcje za po\u015brednictwem swoich formularzy."],"1% transaction and payment gateway fees apply.":["Obowi\u0105zuje 1% op\u0142ata transakcyjna i op\u0142ata za bramk\u0119 p\u0142atnicz\u0105."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Obowi\u0105zuj\u0105 op\u0142aty za transakcje i bramki p\u0142atnicze w wysoko\u015bci 2,9%. Aktywuj licencj\u0119, aby zmniejszy\u0107 op\u0142aty transakcyjne."],"2.9% transaction and payment gateway fees apply.":["Obowi\u0105zuj\u0105 op\u0142aty za transakcje i bramki p\u0142atnicze w wysoko\u015bci 2,9%."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Prosz\u0119 odwiedzi\u0107 %1$s, usun\u0105\u0107 nieu\u017cywany webhook, a nast\u0119pnie klikn\u0105\u0107 poni\u017cej, aby spr\u00f3bowa\u0107 ponownie."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms nie m\u00f3g\u0142 utworzy\u0107 webhooka, poniewa\u017c na Twoim koncie Stripe sko\u0144czy\u0142y si\u0119 darmowe sloty. Webhooki s\u0105 potrzebne do otrzymywania aktualizacji o p\u0142atno\u015bciach."],"Stripe Dashboard":["Pulpit Stripe"],"Creating\u2026":["Tworzenie\u2026"],"Create Webhook":["Utw\u00f3rz Webhook"],"Successfully connected to Stripe!":["Pomy\u015blnie po\u0142\u0105czono ze Stripe!"],"Invalid response from server. Please try again.":["Nieprawid\u0142owa odpowied\u017a z serwera. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"Failed to disconnect Stripe account.":["Nie uda\u0142o si\u0119 od\u0142\u0105czy\u0107 konta Stripe."],"Webhook created successfully!":["Webhook utworzony pomy\u015blnie!"],"Select Currency":["Wybierz walut\u0119"],"Select the default currency for payment forms.":["Wybierz domy\u015bln\u0105 walut\u0119 dla formularzy p\u0142atno\u015bci."],"Connection Status":["Status po\u0142\u0105czenia"],"Disconnect Stripe Account":["Od\u0142\u0105cz konto Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Czy na pewno chcesz od\u0142\u0105czy\u0107 swoje konto Stripe? Spowoduje to zatrzymanie wszystkich aktywnych p\u0142atno\u015bci, subskrypcji i transakcji formularzy powi\u0105zanych z tym kontem."],"Disconnect":["Roz\u0142\u0105cz"],"Disconnecting\u2026":["Roz\u0142\u0105czanie\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook zosta\u0142 pomy\u015blnie po\u0142\u0105czony, wszystkie zdarzenia Stripe s\u0105 \u015bledzone."],"Connect your Stripe account to start accepting payments through your forms.":["Po\u0142\u0105cz swoje konto Stripe, aby zacz\u0105\u0107 przyjmowa\u0107 p\u0142atno\u015bci za po\u015brednictwem formularzy."],"Connect to Stripe":["Po\u0142\u0105cz z Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Bezpiecznie po\u0142\u0105cz si\u0119 ze Stripe kilkoma klikni\u0119ciami, aby zacz\u0105\u0107 akceptowa\u0107 p\u0142atno\u015bci!"],"Set the total number of submissions allowed for this form.":["Ustaw ca\u0142kowit\u0105 liczb\u0119 dozwolonych zg\u0142osze\u0144 dla tego formularza."],"Payment Methods":["Metody p\u0142atno\u015bci"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Tryb testowy pozwala na przetwarzanie p\u0142atno\u015bci bez rzeczywistych op\u0142at. Prze\u0142\u0105cz si\u0119 na tryb na \u017cywo, aby dokonywa\u0107 rzeczywistych transakcji."],"General Payment Settings":["Og\u00f3lne ustawienia p\u0142atno\u015bci"],"These settings apply to all payment gateways.":["Te ustawienia maj\u0105 zastosowanie do wszystkich bramek p\u0142atniczych."],"Stripe Settings":["Ustawienia Stripe"],"Left ($100)":["Lewo ($100)"],"Right (100$)":["Prawo (100$)"],"Left Space ($ 100)":["Wolna przestrze\u0144 ($ 100)"],"Right Space (100 $)":["Prawe Miejsce (100 $)"],"Currency Sign Position":["Pozycja znaku waluty"],"Select the position of the currency symbol relative to the amount.":["Wybierz pozycj\u0119 symbolu waluty wzgl\u0119dem kwoty."],"Learn":["Ucz si\u0119"],"Enable email summaries":["W\u0142\u0105cz podsumowania e-mail"],"Enable IP logging":["W\u0142\u0105cz logowanie IP"],"Turn on Admin Notification from here.":["W\u0142\u0105cz powiadomienia administratora st\u0105d."],"New":["Nowy"],"Send entries to 100+ popular apps.":["Wy\u015blij wpisy do ponad 100 popularnych aplikacji."],"Build automated workflows that run instantly.":["Tw\u00f3rz zautomatyzowane przep\u0142ywy pracy, kt\u00f3re dzia\u0142aj\u0105 natychmiast."],"Create custom app integrations using our Custom App feature.":["Tw\u00f3rz niestandardowe integracje aplikacji za pomoc\u0105 funkcji Niestandardowa Aplikacja."],"Keep your tools in sync automatically.":["Utrzymuj swoje narz\u0119dzia w synchronizacji automatycznie."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["To zainstaluje i aktywuje OttoKit na Twojej stronie WordPress, aby w\u0142\u0105czy\u0107 funkcje automatyzacji."],"Automate Your Forms with OttoKit":["Zautomatyzuj swoje formularze za pomoc\u0105 OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Ka\u017cde przes\u0142anie formularza powinno wywo\u0142a\u0107 jak\u0105\u015b akcj\u0119 \u2014 alert w Slacku, lead w CRM, e-mail z przypomnieniem lub nowy wiersz w Arkuszach Google."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Skonfiguruj uprawnienia klienta AI i ustawienia serwera MCP."],"View documentation":["Wy\u015bwietl dokumentacj\u0119"],"Copy to clipboard":["Kopiuj do schowka"],"Claude Desktop":["Pulpit Claude"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) lub %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Kod Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (projekt) lub ~\/.claude.json (globalny)"],"Cursor":["Kursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (projekt) lub settings.json > mcp.servers (globalne)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml lub config.json"],"Your client's MCP configuration file":["Plik konfiguracyjny MCP Twojego klienta"],"Connect Your AI Client":["Po\u0142\u0105cz swojego klienta AI"],"AI Client":["Klient AI"],"Create an Application Password \u2014 ":["Utw\u00f3rz has\u0142o aplikacji \u2014"],"Open Application Passwords":["Otw\u00f3rz has\u0142a aplikacji"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Lub u\u017cyj tego polecenia CLI, aby szybko doda\u0107 serwer (nadal b\u0119dziesz musia\u0142 ustawi\u0107 zmienne \u015brodowiskowe):"],"Copy the JSON config below into: ":["Skopiuj poni\u017csz\u0105 konfiguracj\u0119 JSON do:"],"Replace \"your-application-password\" with the password from Step 1.":["Zast\u0105p \"your-application-password\" has\u0142em z Kroku 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 punkt ko\u0144cowy MCP Twojej witryny. WP_API_USERNAME \u2014 Twoja nazwa u\u017cytkownika WordPress. WP_API_PASSWORD \u2014 has\u0142o aplikacji, kt\u00f3re wygenerowa\u0142e\u015b."],"View setup docs":["Wy\u015bwietl dokumentacj\u0119 konfiguracji"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Wtyczka MCP Adapter jest zainstalowana, ale nieaktywna. Aktywuj j\u0105, aby skonfigurowa\u0107 ustawienia MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Wtyczka MCP Adapter jest wymagana do po\u0142\u0105czenia klient\u00f3w AI z Twoimi formularzami. Pobierz i zainstaluj j\u0105 z GitHub, a nast\u0119pnie aktywuj."],"Download the latest release from":["Pobierz najnowsz\u0105 wersj\u0119 z"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Zainstaluj wtyczk\u0119, przechodz\u0105c do Wtyczki > Dodaj now\u0105 wtyczk\u0119 > Prze\u015blij wtyczk\u0119."],"Activate the MCP Adapter plugin.":["Aktywuj wtyczk\u0119 MCP Adapter."],"Activating\u2026":["Aktywowanie\u2026"],"Activate MCP Adapter":["Aktywuj adapter MCP"],"Download MCP Adapter":["Pobierz adapter MCP"],"Experimental":["Eksperymentalny"],"Enable Abilities":["W\u0142\u0105cz umiej\u0119tno\u015bci"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Zarejestruj mo\u017cliwo\u015bci SureForms w API Umiej\u0119tno\u015bci WordPress. Po w\u0142\u0105czeniu klienci AI mog\u0105 wy\u015bwietla\u0107, czyta\u0107, tworzy\u0107, edytowa\u0107 i usuwa\u0107 Twoje formularze i wpisy. Po wy\u0142\u0105czeniu \u017cadne umiej\u0119tno\u015bci nie s\u0105 rejestrowane i klienci AI nie mog\u0105 wykonywa\u0107 \u017cadnych dzia\u0142a\u0144 na Twoich formularzach."],"Abilities API \u2014 Edit":["API umiej\u0119tno\u015bci \u2014 Edytuj"],"Enable Edit Abilities":["W\u0142\u0105cz mo\u017cliwo\u015bci edycji"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Po w\u0142\u0105czeniu klienci AI mog\u0105 tworzy\u0107 nowe formularze, aktualizowa\u0107 tytu\u0142y formularzy, pola i ustawienia, duplikowa\u0107 formularze oraz modyfikowa\u0107 statusy wpis\u00f3w. Po wy\u0142\u0105czeniu te mo\u017cliwo\u015bci s\u0105 wyrejestrowane i klienci AI mog\u0105 jedynie odczytywa\u0107 Twoje dane."],"Abilities API \u2014 Delete":["API umiej\u0119tno\u015bci \u2014 Usu\u0144"],"Enable Delete Abilities":["W\u0142\u0105cz mo\u017cliwo\u015b\u0107 usuwania"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Po w\u0142\u0105czeniu klienci AI mog\u0105 trwale usuwa\u0107 formularze i wpisy. Usuni\u0119tych danych nie mo\u017cna odzyska\u0107. Po wy\u0142\u0105czeniu, mo\u017cliwo\u015bci usuwania s\u0105 wyrejestrowane i klienci AI nie mog\u0105 usuwa\u0107 \u017cadnych danych."],"MCP Server":["Serwer MCP"],"Enable MCP Server":["W\u0142\u0105cz serwer MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Tworzy dedykowany punkt ko\u0144cowy SureForms MCP, do kt\u00f3rego mog\u0105 si\u0119 pod\u0142\u0105czy\u0107 klienci AI, tacy jak Claude. Gdy jest wy\u0142\u0105czony, punkt ko\u0144cowy jest usuwany i zewn\u0119trzni klienci AI nie mog\u0105 odkrywa\u0107 ani wywo\u0142ywa\u0107 \u017cadnych funkcji SureForms."],"Learn more":["Dowiedz si\u0119 wi\u0119cej"],"MCP Adapter Required":["Wymagany adapter MCP"],"Heading 1":["Nag\u0142\u00f3wek 1"],"Heading 2":["Nag\u0142\u00f3wek 2"],"Heading 3":["Nag\u0142\u00f3wek 3"],"Heading 4":["Nag\u0142\u00f3wek 4"],"Heading 5":["Nag\u0142\u00f3wek 5"],"Heading 6":["Nag\u0142\u00f3wek 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Skonfiguruj klucz API Google Maps do autouzupe\u0142niania adres\u00f3w i podgl\u0105du mapy."],"Help shape the future of SureForms":["Pom\u00f3\u017c kszta\u0142towa\u0107 przysz\u0142o\u015b\u0107 SureForms"],"Enable Google Address Autocomplete":["W\u0142\u0105cz autouzupe\u0142nianie adres\u00f3w Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Uaktualnij do planu SureForms Business, aby doda\u0107 autouzupe\u0142nianie adres\u00f3w zasilane przez Google z interaktywnym podgl\u0105dem mapy do swoich formularzy."],"Auto-suggest addresses as users type for faster, error-free submissions":["Automatyczne sugerowanie adres\u00f3w podczas pisania przez u\u017cytkownik\u00f3w dla szybszych, bezb\u0142\u0119dnych zg\u0142osze\u0144"],"Show an interactive map preview with draggable pin for precise locations":["Poka\u017c interaktywny podgl\u0105d mapy z przeci\u0105galnym pinezk\u0105 dla precyzyjnych lokalizacji"],"Automatically populate address fields like city, state, and postal code":["Automatycznie wype\u0142niaj pola adresowe, takie jak miasto, wojew\u00f3dztwo i kod pocztowy"],"This form is now closed as we've received all the entries.":["Formularz jest teraz zamkni\u0119ty, poniewa\u017c otrzymali\u015bmy wszystkie zg\u0142oszenia."],"Thank you for contacting us! We will be in touch with you shortly.":["Dzi\u0119kujemy za skontaktowanie si\u0119 z nami! Wkr\u00f3tce si\u0119 z Tob\u0105 skontaktujemy."],"Saving\u2026":["Zapisywanie\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Po w\u0142\u0105czeniu tego formularza nie b\u0119d\u0105 przechowywane adres IP u\u017cytkownika, nazwa przegl\u0105darki i nazwa urz\u0105dzenia w wpisach."],"Form data":["Dane formularza"],"Unsaved changes":["Niezapisane zmiany"],"Keep editing":["Kontynuuj edytowanie"],"Global Defaults":["Domy\u015blne ustawienia globalne"],"Form Restrictions":["Ograniczenia formularza"],"Configure default settings that apply to newly created forms.":["Skonfiguruj domy\u015blne ustawienia, kt\u00f3re maj\u0105 zastosowanie do nowo utworzonych formularzy."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Zbieraj niesensytywne informacje z Twojej strony internetowej, takie jak wersja PHP i u\u017cywane funkcje, aby pom\u00f3c nam szybciej naprawia\u0107 b\u0142\u0119dy, podejmowa\u0107 m\u0105drzejsze decyzje i tworzy\u0107 funkcje, kt\u00f3re naprawd\u0119 maj\u0105 dla Ciebie znaczenie."],"Failed to load pages. Please refresh and try again.":["Nie uda\u0142o si\u0119 za\u0142adowa\u0107 stron. Od\u015bwie\u017c i spr\u00f3buj ponownie."],"Failed to load settings. Please refresh and try again.":["Nie uda\u0142o si\u0119 za\u0142adowa\u0107 ustawie\u0144. Od\u015bwie\u017c stron\u0119 i spr\u00f3buj ponownie."],"Form Validation fields cannot be left blank.":["Pola walidacji formularza nie mog\u0105 pozosta\u0107 puste."],"Recipient email is required when email summaries are enabled.":["Adres e-mail odbiorcy jest wymagany, gdy w\u0142\u0105czone s\u0105 podsumowania e-mail."],"Please enter a valid recipient email.":["Prosz\u0119 wprowadzi\u0107 prawid\u0142owy adres e-mail odbiorcy."],"Settings saved.":["Ustawienia zapisane."],"Failed to save settings.":["Nie uda\u0142o si\u0119 zapisa\u0107 ustawie\u0144."],"Some settings failed to save. Please retry.":["Niekt\u00f3re ustawienia nie zosta\u0142y zapisane. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Niekt\u00f3re pola maj\u0105 niezapisane zmiany. Odrzu\u0107 je, aby kontynuowa\u0107, lub zosta\u0144, aby zapisa\u0107 swoje edycje."],"Discard & switch":["Odrzu\u0107 i prze\u0142\u0105cz"],"Import":["Import"],"Migration":["Migracja"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importuj formularze z Contact Form 7, WPForms, Gravity Forms i innych wtyczek do SureForms."],"Could not generate the import preview.":["Nie mo\u017cna wygenerowa\u0107 podgl\u0105du importu."],"Import failed. Please try again or check your error logs.":["Importowanie nie powiod\u0142o si\u0119. Spr\u00f3buj ponownie lub sprawd\u017a swoje dzienniki b\u0142\u0119d\u00f3w."],"Go back":["Wr\u00f3\u0107"],"Update & import":["Aktualizuj i importuj"],"Confirm & import":["Potwierd\u017a i zaimportuj"],"Form #%s":["Formularz #%s"],"%d field will import":["%d pole zostanie zaimportowane"],"Some fields can't be migrated yet":["Niekt\u00f3re pola nie mog\u0105 jeszcze zosta\u0107 przeniesione"],"These fields will be skipped - add them manually after the form is created:":["Te pola zostan\u0105 pomini\u0119te - dodaj je r\u0119cznie po utworzeniu formularza:"],"Some forms could not be parsed":["Niekt\u00f3re formularze nie mog\u0142y zosta\u0107 przetworzone"],"Update existing":["Zaktualizuj istniej\u0105ce"],"Create a new copy":["Utw\u00f3rz now\u0105 kopi\u0119"],"Could not load forms for this source.":["Nie mo\u017cna za\u0142adowa\u0107 formularzy dla tego \u017ar\u00f3d\u0142a."],"(untitled form)":["(formularz bez tytu\u0142u)"],"Previously imported":["Poprzednio zaimportowane"],"Open existing SureForms form in a new tab":["Otw\u00f3rz istniej\u0105cy formularz SureForms w nowej karcie"],"On re-import":["Przy ponownym imporcie"],"No forms found in this plugin.":["Nie znaleziono formularzy w tej wtyczce."],"%1$d of %2$d form selected":["Wybrano %1$d z %2$d formularzy"],"Preview update":["Podgl\u0105d aktualizacji"],"Preview import":["Podgl\u0105d importu"],"Migration complete":["Migracja zako\u0144czona"],"Nothing was imported":["Nic nie zosta\u0142o zaimportowane"],"%d form was imported into SureForms.":["%d formularz zosta\u0142 zaimportowany do SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["\u017badna z wybranych form nie mog\u0142a zosta\u0107 przeniesiona. Zobacz poni\u017csze ostrze\u017cenia, aby uzyska\u0107 szczeg\u00f3\u0142y."],"Imported forms":["Zaimportowane formularze"],"Edit in SureForms":["Edytuj w SureForms"],"Some fields were skipped during import":["Niekt\u00f3re pola zosta\u0142y pomini\u0119te podczas importu"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Dodaj je r\u0119cznie w edytorze formularzy - nie maj\u0105 jeszcze odpowiednika w SureForms:"],"Some forms failed to import":["Niekt\u00f3re formularze nie zosta\u0142y zaimportowane"],"Import more forms":["Importuj wi\u0119cej formularzy"],"Could not load the list of importable plugins.":["Nie mo\u017cna za\u0142adowa\u0107 listy wtyczek do zaimportowania."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Na tej stronie nie s\u0105 aktywne \u017cadne obs\u0142ugiwane wtyczki formularzy. Aktywuj jedn\u0105 (tak\u0105 jak Contact Form 7) z co najmniej jednym formularzem, aby zaimportowa\u0107 go do SureForms."],"Plugin":["Wtyczka"],"%d form":["%d forma"],"No forms":["Brak formularzy"],"Multiple choice":["Wielokrotny wyb\u00f3r"],"Consent":["Zgoda"],"The image uploaded but could not be inserted. Please try again.":["Obraz zosta\u0142 przes\u0142any, ale nie mo\u017cna go by\u0142o wstawi\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["Nie mo\u017cna przes\u0142a\u0107 obrazu. Mo\u017ce by\u0107 zbyt du\u017cy lub mo\u017cesz nie mie\u0107 uprawnie\u0144 do przesy\u0142ania plik\u00f3w."],"Please choose an image file.":["Prosz\u0119 wybra\u0107 plik graficzny."],"Show views and conversion rate":["Poka\u017c widoki i wska\u017anik konwersji"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Dodaje kolumny Widoki i Wska\u017anik konwersji do listy Formularzy. Widok jest liczony raz na wizyt\u0119 na stronie, gdy formularz pojawia si\u0119 na ekranie, a wska\u017anik konwersji to udzia\u0142 tych widok\u00f3w, kt\u00f3re zako\u0144czy\u0142y si\u0119 przes\u0142aniem. Liczenie rozpoczyna si\u0119, gdy pierwszy raz to w\u0142\u0105czysz, wi\u0119c przes\u0142ania otrzymane przed tym momentem nie s\u0105 liczone do wska\u017anika. Wy\u0142\u0105czenie tego p\u00f3\u017aniej tylko ukrywa kolumny \u2014 liczenie trwa, wi\u0119c dane s\u0105 aktualne, je\u015bli ponownie to w\u0142\u0105czysz."],"Log cleared.":["Dziennik wyczyszczony."],"Could not clear the log.":["Nie mo\u017cna wyczy\u015bci\u0107 dziennika."],"Enable logs":["W\u0142\u0105cz dzienniki"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Rejestruje niepowodzenia przesy\u0142ania formularzy zg\u0142aszane przez przegl\u0105dark\u0119, dzi\u0119ki czemu mo\u017cesz wys\u0142a\u0107 dziennik do wsparcia zamiast czyta\u0107 konsol\u0119. Nic nie jest zapisywane, gdy Twoje formularze dzia\u0142aj\u0105, a przes\u0142ane warto\u015bci nigdy nie s\u0105 przechowywane."],"Download log":["Pobierz dziennik"],"Confirm delete":["Potwierd\u017a usuni\u0119cie"],"Log is full \u2014 download and clear it to keep recording.":["Dziennik jest pe\u0142ny \u2014 pobierz i wyczy\u015b\u0107 go, aby kontynuowa\u0107 rejestrowanie."],"Form Views & Conversion":["Widoki formularzy i konwersja"],"Logs":["Dzienniki"],"Quill heading picker: default paragraph style\u0004Normal":["Normalny"]}}} \ No newline at end of file +{"translation-revision-date":"2025-01-02T08:44:41+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Panel sterowania"],"Settings":["Ustawienia"],"Entries":["Wpisy"],"Status":["Status"],"Form":["Formularz"],"Activated":["Aktywowany"],"Activate":["Aktywuj"],"Address":["Adres"],"Checkbox":["Pole wyboru"],"Dropdown":["Lista rozwijana"],"Email":["E-mail"],"Number":["Numer"],"Phone":["Telefon"],"Textarea":["Pole tekstowe"],"Monday":["Poniedzia\u0142ek"],"Forms":["Formularze"],"New Form Submission - %s":["Nowe przes\u0142anie formularza - %s"],"GitHub":["GitHub"],"(no title)":["(brak tytu\u0142u)"],"General":["Og\u00f3lny"],"No tags available":["Brak dost\u0119pnych tag\u00f3w"],"Back":["Wstecz"],"Generic tags":["Og\u00f3lne tagi"],"Other":["Inne"],"Confirm":["Potwierd\u017a"],"Cancel":["Anuluj"],"Install":["Zainstaluj"],"Plugin Installation failed, Please try again later.":["Instalacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"Plugin activation failed, Please try again later.":["Aktywacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"Integrations":["Integracje"],"What's New?":["Co nowego?"],"Core":["Rdze\u0144"],"Unlicensed":["Bez licencji"],"Connecting\u2026":["\u0141\u0105czenie\u2026"],"Install & Activate":["Zainstaluj i aktywuj"],"Compliance Settings":["Ustawienia zgodno\u015bci"],"Enable GDPR Compliance":["W\u0142\u0105cz zgodno\u015b\u0107 z RODO"],"Never store entry data after form submission":["Nigdy nie przechowuj danych wej\u015bciowych po przes\u0142aniu formularza"],"When enabled this form will never store Entries.":["Gdy jest w\u0142\u0105czony, ten formularz nigdy nie b\u0119dzie przechowywa\u0142 wpis\u00f3w."],"Automatically delete entries":["Automatycznie usu\u0144 wpisy"],"When enabled this form will automatically delete entries after a certain period of time.":["Po w\u0142\u0105czeniu ten formularz automatycznie usunie wpisy po okre\u015blonym czasie."],"Entries older than the days set will be deleted automatically.":["Wpisy starsze ni\u017c ustawione dni zostan\u0105 usuni\u0119te automatycznie."],"Visual":["Wizualny"],"HTML":["HTML"],"All Data":["Wszystkie dane"],"Add Shortcode":["Dodaj shortcode"],"Form input tags":["Tagi wej\u015bciowe formularza"],"Comma separated values are also accepted.":["Warto\u015bci oddzielone przecinkami s\u0105 r\u00f3wnie\u017c akceptowane."],"Email Notification":["Powiadomienie e-mail"],"Name":["Imi\u0119"],"Send Email To":["Wy\u015blij e-mail do"],"Subject":["Temat"],"CC":["DW"],"BCC":["UDW"],"Reply To":["Odpowiedz do"],"Add Key":["Dodaj klucz"],"Add Value":["Dodaj warto\u015b\u0107"],"Add":["Dodaj"],"Confirmation Message":["Wiadomo\u015b\u0107 potwierdzaj\u0105ca"],"After Form Submission":["Po przes\u0142aniu formularza"],"Hide Form":["Ukryj formularz"],"Reset Form":["Zresetuj formularz"],"Custom URL":["Niestandardowy URL"],"Add Query Parameters":["Dodaj parametry zapytania"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Wybierz, je\u015bli chcesz doda\u0107 pary klucz-warto\u015b\u0107 dla p\u00f3l formularza do uwzgl\u0119dnienia w parametrach zapytania"],"Query Parameters":["Parametry zapytania"],"Success Message":["Komunikat o sukcesie"],"Redirect":["Przekieruj"],"Redirect to":["Przekieruj do"],"Page":["Strona"],"Form Confirmation":["Potwierdzenie formularza"],"Confirmation Type":["Typ potwierdzenia"],"Clear":["Wyczy\u015b\u0107"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Niewidoczna"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Walidacje"],"Spam Protection":["Ochrona przed spamem"],"Email Summaries":["Podsumowania e-maili"],"Tuesday":["Wtorek"],"Wednesday":["\u015aroda"],"Thursday":["Czwartek"],"Friday":["Pi\u0105tek"],"Saturday":["Sobota"],"Sunday":["Niedziela"],"Test Email":["Testowy email"],"Schedule Reports":["Zaplanuj raporty"],"IP Logging":["Rejestrowanie IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Je\u015bli ta opcja jest w\u0142\u0105czona, adres IP u\u017cytkownika zostanie zapisany wraz z danymi formularza"],"Confirmation Email Mismatch Message":["Wiadomo\u015b\u0107 o niezgodno\u015bci e-maila potwierdzaj\u0105cego"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s oznacza minimaln\u0105 warto\u015b\u0107 wej\u015bciow\u0105. Na przyk\u0142ad: \"Minimalna warto\u015b\u0107 to 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s oznacza maksymaln\u0105 warto\u015b\u0107 wej\u015bciow\u0105. Na przyk\u0142ad: \"Maksymalna warto\u015b\u0107 to 100.\""],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s oznacza minimaln\u0105 liczb\u0119 wymaganych wybor\u00f3w. Na przyk\u0142ad: \u201eWymagane s\u0105 co najmniej 2 wybory\u201d."],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s oznacza maksymaln\u0105 liczb\u0119 dozwolonych wybor\u00f3w. Na przyk\u0142ad: \u201eDozwolone s\u0105 maksymalnie 4 wybory.\u201d"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s oznacza minimaln\u0105 liczb\u0119 potrzebnych wybor\u00f3w. Na przyk\u0142ad: \u201eWymagany jest co najmniej 1 wyb\u00f3r.\u201d"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s oznacza maksymaln\u0105 liczb\u0119 dozwolonych wybor\u00f3w. Na przyk\u0142ad: \u201eDozwolone s\u0105 maksymalnie 3 wybory\u201d."]," Error Message":["Komunikat o b\u0142\u0119dzie"],"Auto":["Samoch\u00f3d"],"Light":["\u015awiat\u0142o"],"Dark":["Ciemny"],"Turnstile":["Bramka"],"Honeypot":["Pu\u0142apka"],"Get Keys":["Pobierz klucze"],"Documentation":["Dokumentacja"],"Site Key":["Klucz strony"],"Secret Key":["Klucz tajny"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Tryb wygl\u0105du"],"Enable Honeypot Security":["W\u0142\u0105cz zabezpieczenie Honeypot"],"Enable Honeypot Security for better spam protection":["W\u0142\u0105cz Honeypot Security dla lepszej ochrony przed spamem"],"Text":["Tekst"],"OttoKit":["OttoKit"],"Connect with OttoKit":["Po\u0142\u0105cz si\u0119 z OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Bie\u017c\u0105cy adres \u201eOd e-mail\u201d nie pasuje do nazwy domeny Twojej strony internetowej (%1$s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami b\u0119d\u0105 blokowane lub oznaczane jako spam. Alternatywnie, spr\u00f3buj u\u017cy\u0107 adresu Od, kt\u00f3ry pasuje do domeny Twojej strony internetowej (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Obecny adres \u201eOd e-mail\u201d nie pasuje do nazwy domeny Twojej strony internetowej (%s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami zostan\u0105 zablokowane lub oznaczone jako spam."],"We strongly recommend that you install the free ":["Zalecamy zdecydowanie zainstalowanie darmowego"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["wtyczka! Kreator konfiguracji u\u0142atwia napraw\u0119 Twoich e-maili."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternatywnie spr\u00f3buj u\u017cy\u0107 adresu nadawcy, kt\u00f3ry pasuje do domeny Twojej strony internetowej (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Prosz\u0119 wprowadzi\u0107 prawid\u0142owy adres e-mail. Powiadomienia nie b\u0119d\u0105 wysy\u0142ane, je\u015bli pole nie zostanie wype\u0142nione poprawnie."],"From Name":["Od Nazwa"],"From Email":["Z e-maila"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["Bie\u017c\u0105cy adres \u201eOd e-mail\u201d mo\u017ce nie pasowa\u0107 do nazwy domeny Twojej strony internetowej (%1$s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami zostan\u0105 zablokowane lub oznaczone jako spam. Alternatywnie, spr\u00f3buj u\u017cy\u0107 adresu Od, kt\u00f3ry pasuje do domeny Twojej strony internetowej (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["Obecny adres \u201eOd e-mail\u201d mo\u017ce nie pasowa\u0107 do nazwy domeny Twojej strony internetowej (%s). Mo\u017ce to spowodowa\u0107, \u017ce Twoje e-maile z powiadomieniami zostan\u0105 zablokowane lub oznaczone jako spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Gotowy, aby wyj\u015b\u0107 poza darmowy plan?"],"Upgrade now":["Zaktualizuj teraz"],"and unlock the full power of SureForms!":["i odblokuj pe\u0142n\u0105 moc SureForms!"],"Upgrade SureForms":["Uaktualnij SureForms"],"Upgrade Now":["Zaktualizuj teraz"],"Entries older than the selected days will be deleted.":["Wpisy starsze ni\u017c wybrane dni zostan\u0105 usuni\u0119te."],"Entries Time Period":["Okres czasu wpis\u00f3w"],"Notifications can use only one From Email so please enter a single address.":["Powiadomienia mog\u0105 u\u017cywa\u0107 tylko jednego adresu e-mail nadawcy, wi\u0119c prosz\u0119 poda\u0107 jeden adres."],"Select Page to redirect":["Wybierz stron\u0119 do przekierowania"],"Search for a page":["Wyszukaj stron\u0119"],"Select a page":["Wybierz stron\u0119"],"Form Validation":["Walidacja formularza"],"Required Error Messages":["Wymagane komunikaty o b\u0142\u0119dach"],"Other Error Messages":["Inne komunikaty o b\u0142\u0119dach"],"Input Field Unique":["Pole wej\u015bciowe unikalne"],"Email Field Unique":["Pole e-mail musi by\u0107 unikalne"],"Invalid URL":["Nieprawid\u0142owy URL"],"Phone Field Unique":["Pole telefonu unikalne"],"Invalid Field Number Block":["Nieprawid\u0142owy blok numeru pola"],"Invalid Email":["Nieprawid\u0142owy adres e-mail"],"Number Minimum Value":["Minimalna warto\u015b\u0107 liczby"],"Number Maximum Value":["Maksymalna warto\u015b\u0107 liczby"],"Dropdown Minimum Selections":["Minimalna liczba wybor\u00f3w w rozwijanym menu"],"Dropdown Maximum Selections":["Maksymalna liczba wybor\u00f3w w rozwijanym menu"],"Multiple Choice Minimum Selections":["Minimalna liczba wybor\u00f3w w pytaniach wielokrotnego wyboru"],"Multiple Choice Maximum Selections":["Wielokrotny wyb\u00f3r maksymalna liczba zaznacze\u0144"],"Input Field":["Pole wej\u015bciowe"],"Email Field":["Pole e-mail"],"URL Field":["Pole URL"],"Phone Field":["Pole telefonu"],"Textarea Field":["Pole tekstowe"],"Checkbox Field":["Pole wyboru"],"Dropdown Field":["Pole rozwijane"],"Multiple Choice Field":["Pole wyboru wielokrotnego"],"Address Field":["Pole adresu"],"Number Field":["Pole liczby"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Aby w\u0142\u0105czy\u0107 funkcj\u0119 reCAPTCHA w SureForms, w\u0142\u0105cz opcj\u0119 reCAPTCHA w ustawieniach blok\u00f3w i wybierz wersj\u0119. Dodaj tutaj tajny klucz i klucz witryny Google reCAPTCHA. reCAPTCHA zostanie dodana do Twojej strony na froncie."],"Enter your %s here":["Wprowad\u017a tutaj sw\u00f3j %s"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Aby w\u0142\u0105czy\u0107 hCAPTCHA, dodaj klucz witryny i klucz tajny. Skonfiguruj te ustawienia w poszczeg\u00f3lnych formularzach."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Aby w\u0142\u0105czy\u0107 Cloudflare Turnstile, dodaj klucz witryny i klucz tajny. Skonfiguruj te ustawienia w ramach poszczeg\u00f3lnego formularza."],"Save":["Zapisz"],"Anonymous Analytics":["Anonimowa Analiza"],"Learn More":["Dowiedz si\u0119 wi\u0119cej"],"Admin Notification":["Powiadomienie administratora"],"Enable Admin Notification":["W\u0142\u0105cz powiadomienia administratora"],"Admin notifications keep you informed about new form entries since your last visit.":["Powiadomienia administracyjne informuj\u0105 Ci\u0119 o nowych wpisach formularza od Twojej ostatniej wizyty."],"Skip":["Pomi\u0144"],"Continue":["Kontynuuj"],"Maximum Number of Entries":["Maksymalna liczba wpis\u00f3w"],"Maximum Entries":["Maksymalna liczba wpis\u00f3w"],"Response Description After Maximum Entries":["Opis odpowiedzi po maksymalnej liczbie wpis\u00f3w"],"Get Started":["Rozpocznij"],"Integration":["Integracja"],"Connect Native Integrations with SureForms":["Po\u0142\u0105cz natywne integracje z SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Odblokuj pot\u0119\u017cne integracje w planie Premium, aby zautomatyzowa\u0107 swoje przep\u0142ywy pracy i po\u0142\u0105czy\u0107 SureForms bezpo\u015brednio z ulubionymi narz\u0119dziami."],"Send form submissions straight to CRMs, email, and marketing platforms":["Wysy\u0142aj zg\u0142oszenia formularzy bezpo\u015brednio do CRM, e-maili i platform marketingowych"],"Automate repetitive tasks with seamless data syncing":["Zautomatyzuj powtarzalne zadania dzi\u0119ki p\u0142ynnej synchronizacji danych"],"Access exclusive native integrations for faster workflows":["Uzyskaj dost\u0119p do ekskluzywnych natywnych integracji dla szybszych przep\u0142yw\u00f3w pracy"],"Expected format for emails - email@sureforms.com or John Doe ":["Oczekiwany format dla e-maili - email@sureforms.com lub John Doe "],"Payments":["P\u0142atno\u015bci"],"Stripe account disconnected successfully.":["Konto Stripe zosta\u0142o pomy\u015blnie od\u0142\u0105czone."],"Failed to create webhook.":["Nie uda\u0142o si\u0119 utworzy\u0107 webhooka."],"Failed to connect to Stripe.":["Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 ze Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Baza wiedzy"],"What\u2019s New":["Co nowego"],"out of":["z"],"No entries found":["Nie znaleziono wpis\u00f3w"],"delete":["usu\u0144"],"Please type \"%s\" in the input box":["Prosz\u0119 wpisa\u0107 \"%s\" w polu wej\u015bciowym"],"To confirm, type \"%s\" in the box below:":["Aby potwierdzi\u0107, wpisz \"%s\" w polu poni\u017cej:"],"Type \"%s\"":["Wpisz \"%s\""],"Go to OttoKit Settings":["Przejd\u017a do ustawie\u0144 OttoKit"],"USD - US Dollar":["USD - Dolar ameryka\u0144ski"],"Payment Mode":["Tryb p\u0142atno\u015bci"],"Test Mode":["Tryb testowy"],"Live Mode":["Tryb na \u017cywo"],"Action":["Akcja"],"General Settings":["Ustawienia og\u00f3lne"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Skonfiguruj podsumowania e-mail, alerty administratora i preferencje danych, aby \u0142atwo zarz\u0105dza\u0107 swoimi formularzami."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Dostosuj domy\u015blne komunikaty o b\u0142\u0119dach wy\u015bwietlane, gdy u\u017cytkownicy przesy\u0142aj\u0105 nieprawid\u0142owe lub niekompletne wpisy formularza."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["W\u0142\u0105cz ochron\u0119 przed spamem dla swoich formularzy, korzystaj\u0105c z us\u0142ug CAPTCHA lub zabezpiecze\u0144 typu honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Po\u0142\u0105cz i zarz\u0105dzaj swoimi bramkami p\u0142atno\u015bci, aby bezpiecznie akceptowa\u0107 transakcje za po\u015brednictwem swoich formularzy."],"1% transaction and payment gateway fees apply.":["Obowi\u0105zuje 1% op\u0142ata transakcyjna i op\u0142ata za bramk\u0119 p\u0142atnicz\u0105."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Obowi\u0105zuj\u0105 op\u0142aty za transakcje i bramki p\u0142atnicze w wysoko\u015bci 2,9%. Aktywuj licencj\u0119, aby zmniejszy\u0107 op\u0142aty transakcyjne."],"2.9% transaction and payment gateway fees apply.":["Obowi\u0105zuj\u0105 op\u0142aty za transakcje i bramki p\u0142atnicze w wysoko\u015bci 2,9%."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Prosz\u0119 odwiedzi\u0107 %1$s, usun\u0105\u0107 nieu\u017cywany webhook, a nast\u0119pnie klikn\u0105\u0107 poni\u017cej, aby spr\u00f3bowa\u0107 ponownie."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms nie m\u00f3g\u0142 utworzy\u0107 webhooka, poniewa\u017c na Twoim koncie Stripe sko\u0144czy\u0142y si\u0119 darmowe sloty. Webhooki s\u0105 potrzebne do otrzymywania aktualizacji o p\u0142atno\u015bciach."],"Stripe Dashboard":["Pulpit Stripe"],"Creating\u2026":["Tworzenie\u2026"],"Create Webhook":["Utw\u00f3rz Webhook"],"Successfully connected to Stripe!":["Pomy\u015blnie po\u0142\u0105czono ze Stripe!"],"Invalid response from server. Please try again.":["Nieprawid\u0142owa odpowied\u017a z serwera. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"Failed to disconnect Stripe account.":["Nie uda\u0142o si\u0119 od\u0142\u0105czy\u0107 konta Stripe."],"Webhook created successfully!":["Webhook utworzony pomy\u015blnie!"],"Select Currency":["Wybierz walut\u0119"],"Select the default currency for payment forms.":["Wybierz domy\u015bln\u0105 walut\u0119 dla formularzy p\u0142atno\u015bci."],"Connection Status":["Status po\u0142\u0105czenia"],"Disconnect Stripe Account":["Od\u0142\u0105cz konto Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Czy na pewno chcesz od\u0142\u0105czy\u0107 swoje konto Stripe? Spowoduje to zatrzymanie wszystkich aktywnych p\u0142atno\u015bci, subskrypcji i transakcji formularzy powi\u0105zanych z tym kontem."],"Disconnect":["Roz\u0142\u0105cz"],"Disconnecting\u2026":["Roz\u0142\u0105czanie\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook zosta\u0142 pomy\u015blnie po\u0142\u0105czony, wszystkie zdarzenia Stripe s\u0105 \u015bledzone."],"Connect your Stripe account to start accepting payments through your forms.":["Po\u0142\u0105cz swoje konto Stripe, aby zacz\u0105\u0107 przyjmowa\u0107 p\u0142atno\u015bci za po\u015brednictwem formularzy."],"Connect to Stripe":["Po\u0142\u0105cz z Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Bezpiecznie po\u0142\u0105cz si\u0119 ze Stripe kilkoma klikni\u0119ciami, aby zacz\u0105\u0107 akceptowa\u0107 p\u0142atno\u015bci!"],"Set the total number of submissions allowed for this form.":["Ustaw ca\u0142kowit\u0105 liczb\u0119 dozwolonych zg\u0142osze\u0144 dla tego formularza."],"Payment Methods":["Metody p\u0142atno\u015bci"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Tryb testowy pozwala na przetwarzanie p\u0142atno\u015bci bez rzeczywistych op\u0142at. Prze\u0142\u0105cz si\u0119 na tryb na \u017cywo, aby dokonywa\u0107 rzeczywistych transakcji."],"General Payment Settings":["Og\u00f3lne ustawienia p\u0142atno\u015bci"],"These settings apply to all payment gateways.":["Te ustawienia maj\u0105 zastosowanie do wszystkich bramek p\u0142atniczych."],"Stripe Settings":["Ustawienia Stripe"],"Left ($100)":["Lewo ($100)"],"Right (100$)":["Prawo (100$)"],"Left Space ($ 100)":["Wolna przestrze\u0144 ($ 100)"],"Right Space (100 $)":["Prawe Miejsce (100 $)"],"Currency Sign Position":["Pozycja znaku waluty"],"Select the position of the currency symbol relative to the amount.":["Wybierz pozycj\u0119 symbolu waluty wzgl\u0119dem kwoty."],"Learn":["Ucz si\u0119"],"Enable email summaries":["W\u0142\u0105cz podsumowania e-mail"],"Enable IP logging":["W\u0142\u0105cz logowanie IP"],"Turn on Admin Notification from here.":["W\u0142\u0105cz powiadomienia administratora st\u0105d."],"New":["Nowy"],"Send entries to 100+ popular apps.":["Wy\u015blij wpisy do ponad 100 popularnych aplikacji."],"Build automated workflows that run instantly.":["Tw\u00f3rz zautomatyzowane przep\u0142ywy pracy, kt\u00f3re dzia\u0142aj\u0105 natychmiast."],"Create custom app integrations using our Custom App feature.":["Tw\u00f3rz niestandardowe integracje aplikacji za pomoc\u0105 funkcji Niestandardowa Aplikacja."],"Keep your tools in sync automatically.":["Utrzymuj swoje narz\u0119dzia w synchronizacji automatycznie."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["To zainstaluje i aktywuje OttoKit na Twojej stronie WordPress, aby w\u0142\u0105czy\u0107 funkcje automatyzacji."],"Automate Your Forms with OttoKit":["Zautomatyzuj swoje formularze za pomoc\u0105 OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Ka\u017cde przes\u0142anie formularza powinno wywo\u0142a\u0107 jak\u0105\u015b akcj\u0119 \u2014 alert w Slacku, lead w CRM, e-mail z przypomnieniem lub nowy wiersz w Arkuszach Google."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Skonfiguruj uprawnienia klienta AI i ustawienia serwera MCP."],"View documentation":["Wy\u015bwietl dokumentacj\u0119"],"Copy to clipboard":["Kopiuj do schowka"],"Claude Desktop":["Pulpit Claude"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) lub %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Kod Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (projekt) lub ~\/.claude.json (globalny)"],"Cursor":["Kursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (projekt) lub settings.json > mcp.servers (globalne)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml lub config.json"],"Your client's MCP configuration file":["Plik konfiguracyjny MCP Twojego klienta"],"Connect Your AI Client":["Po\u0142\u0105cz swojego klienta AI"],"AI Client":["Klient AI"],"Create an Application Password \u2014 ":["Utw\u00f3rz has\u0142o aplikacji \u2014"],"Open Application Passwords":["Otw\u00f3rz has\u0142a aplikacji"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Lub u\u017cyj tego polecenia CLI, aby szybko doda\u0107 serwer (nadal b\u0119dziesz musia\u0142 ustawi\u0107 zmienne \u015brodowiskowe):"],"Copy the JSON config below into: ":["Skopiuj poni\u017csz\u0105 konfiguracj\u0119 JSON do:"],"Replace \"your-application-password\" with the password from Step 1.":["Zast\u0105p \"your-application-password\" has\u0142em z Kroku 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 punkt ko\u0144cowy MCP Twojej witryny. WP_API_USERNAME \u2014 Twoja nazwa u\u017cytkownika WordPress. WP_API_PASSWORD \u2014 has\u0142o aplikacji, kt\u00f3re wygenerowa\u0142e\u015b."],"View setup docs":["Wy\u015bwietl dokumentacj\u0119 konfiguracji"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Wtyczka MCP Adapter jest zainstalowana, ale nieaktywna. Aktywuj j\u0105, aby skonfigurowa\u0107 ustawienia MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Wtyczka MCP Adapter jest wymagana do po\u0142\u0105czenia klient\u00f3w AI z Twoimi formularzami. Pobierz i zainstaluj j\u0105 z GitHub, a nast\u0119pnie aktywuj."],"Download the latest release from":["Pobierz najnowsz\u0105 wersj\u0119 z"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Zainstaluj wtyczk\u0119, przechodz\u0105c do Wtyczki > Dodaj now\u0105 wtyczk\u0119 > Prze\u015blij wtyczk\u0119."],"Activate the MCP Adapter plugin.":["Aktywuj wtyczk\u0119 MCP Adapter."],"Activating\u2026":["Aktywowanie\u2026"],"Activate MCP Adapter":["Aktywuj adapter MCP"],"Download MCP Adapter":["Pobierz adapter MCP"],"Experimental":["Eksperymentalny"],"Enable Abilities":["W\u0142\u0105cz umiej\u0119tno\u015bci"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Zarejestruj mo\u017cliwo\u015bci SureForms w API Umiej\u0119tno\u015bci WordPress. Po w\u0142\u0105czeniu klienci AI mog\u0105 wy\u015bwietla\u0107, czyta\u0107, tworzy\u0107, edytowa\u0107 i usuwa\u0107 Twoje formularze i wpisy. Po wy\u0142\u0105czeniu \u017cadne umiej\u0119tno\u015bci nie s\u0105 rejestrowane i klienci AI nie mog\u0105 wykonywa\u0107 \u017cadnych dzia\u0142a\u0144 na Twoich formularzach."],"Abilities API \u2014 Edit":["API umiej\u0119tno\u015bci \u2014 Edytuj"],"Enable Edit Abilities":["W\u0142\u0105cz mo\u017cliwo\u015bci edycji"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Po w\u0142\u0105czeniu klienci AI mog\u0105 tworzy\u0107 nowe formularze, aktualizowa\u0107 tytu\u0142y formularzy, pola i ustawienia, duplikowa\u0107 formularze oraz modyfikowa\u0107 statusy wpis\u00f3w. Po wy\u0142\u0105czeniu te mo\u017cliwo\u015bci s\u0105 wyrejestrowane i klienci AI mog\u0105 jedynie odczytywa\u0107 Twoje dane."],"Abilities API \u2014 Delete":["API umiej\u0119tno\u015bci \u2014 Usu\u0144"],"Enable Delete Abilities":["W\u0142\u0105cz mo\u017cliwo\u015b\u0107 usuwania"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Po w\u0142\u0105czeniu klienci AI mog\u0105 trwale usuwa\u0107 formularze i wpisy. Usuni\u0119tych danych nie mo\u017cna odzyska\u0107. Po wy\u0142\u0105czeniu, mo\u017cliwo\u015bci usuwania s\u0105 wyrejestrowane i klienci AI nie mog\u0105 usuwa\u0107 \u017cadnych danych."],"MCP Server":["Serwer MCP"],"Enable MCP Server":["W\u0142\u0105cz serwer MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Tworzy dedykowany punkt ko\u0144cowy SureForms MCP, do kt\u00f3rego mog\u0105 si\u0119 pod\u0142\u0105czy\u0107 klienci AI, tacy jak Claude. Gdy jest wy\u0142\u0105czony, punkt ko\u0144cowy jest usuwany i zewn\u0119trzni klienci AI nie mog\u0105 odkrywa\u0107 ani wywo\u0142ywa\u0107 \u017cadnych funkcji SureForms."],"Learn more":["Dowiedz si\u0119 wi\u0119cej"],"MCP Adapter Required":["Wymagany adapter MCP"],"Heading 1":["Nag\u0142\u00f3wek 1"],"Heading 2":["Nag\u0142\u00f3wek 2"],"Heading 3":["Nag\u0142\u00f3wek 3"],"Heading 4":["Nag\u0142\u00f3wek 4"],"Heading 5":["Nag\u0142\u00f3wek 5"],"Heading 6":["Nag\u0142\u00f3wek 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Skonfiguruj klucz API Google Maps do autouzupe\u0142niania adres\u00f3w i podgl\u0105du mapy."],"Help shape the future of SureForms":["Pom\u00f3\u017c kszta\u0142towa\u0107 przysz\u0142o\u015b\u0107 SureForms"],"Enable Google Address Autocomplete":["W\u0142\u0105cz autouzupe\u0142nianie adres\u00f3w Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Uaktualnij do planu SureForms Business, aby doda\u0107 autouzupe\u0142nianie adres\u00f3w zasilane przez Google z interaktywnym podgl\u0105dem mapy do swoich formularzy."],"Auto-suggest addresses as users type for faster, error-free submissions":["Automatyczne sugerowanie adres\u00f3w podczas pisania przez u\u017cytkownik\u00f3w dla szybszych, bezb\u0142\u0119dnych zg\u0142osze\u0144"],"Show an interactive map preview with draggable pin for precise locations":["Poka\u017c interaktywny podgl\u0105d mapy z przeci\u0105galnym pinezk\u0105 dla precyzyjnych lokalizacji"],"Automatically populate address fields like city, state, and postal code":["Automatycznie wype\u0142niaj pola adresowe, takie jak miasto, wojew\u00f3dztwo i kod pocztowy"],"This form is now closed as we've received all the entries.":["Formularz jest teraz zamkni\u0119ty, poniewa\u017c otrzymali\u015bmy wszystkie zg\u0142oszenia."],"Thank you for contacting us! We will be in touch with you shortly.":["Dzi\u0119kujemy za skontaktowanie si\u0119 z nami! Wkr\u00f3tce si\u0119 z Tob\u0105 skontaktujemy."],"Saving\u2026":["Zapisywanie\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Po w\u0142\u0105czeniu tego formularza nie b\u0119d\u0105 przechowywane adres IP u\u017cytkownika, nazwa przegl\u0105darki i nazwa urz\u0105dzenia w wpisach."],"Form data":["Dane formularza"],"Unsaved changes":["Niezapisane zmiany"],"Keep editing":["Kontynuuj edytowanie"],"Global Defaults":["Domy\u015blne ustawienia globalne"],"Form Restrictions":["Ograniczenia formularza"],"Configure default settings that apply to newly created forms.":["Skonfiguruj domy\u015blne ustawienia, kt\u00f3re maj\u0105 zastosowanie do nowo utworzonych formularzy."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Zbieraj niesensytywne informacje z Twojej strony internetowej, takie jak wersja PHP i u\u017cywane funkcje, aby pom\u00f3c nam szybciej naprawia\u0107 b\u0142\u0119dy, podejmowa\u0107 m\u0105drzejsze decyzje i tworzy\u0107 funkcje, kt\u00f3re naprawd\u0119 maj\u0105 dla Ciebie znaczenie."],"Failed to load pages. Please refresh and try again.":["Nie uda\u0142o si\u0119 za\u0142adowa\u0107 stron. Od\u015bwie\u017c i spr\u00f3buj ponownie."],"Failed to load settings. Please refresh and try again.":["Nie uda\u0142o si\u0119 za\u0142adowa\u0107 ustawie\u0144. Od\u015bwie\u017c stron\u0119 i spr\u00f3buj ponownie."],"Form Validation fields cannot be left blank.":["Pola walidacji formularza nie mog\u0105 pozosta\u0107 puste."],"Recipient email is required when email summaries are enabled.":["Adres e-mail odbiorcy jest wymagany, gdy w\u0142\u0105czone s\u0105 podsumowania e-mail."],"Please enter a valid recipient email.":["Prosz\u0119 wprowadzi\u0107 prawid\u0142owy adres e-mail odbiorcy."],"Settings saved.":["Ustawienia zapisane."],"Failed to save settings.":["Nie uda\u0142o si\u0119 zapisa\u0107 ustawie\u0144."],"Some settings failed to save. Please retry.":["Niekt\u00f3re ustawienia nie zosta\u0142y zapisane. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Niekt\u00f3re pola maj\u0105 niezapisane zmiany. Odrzu\u0107 je, aby kontynuowa\u0107, lub zosta\u0144, aby zapisa\u0107 swoje edycje."],"Discard & switch":["Odrzu\u0107 i prze\u0142\u0105cz"],"Import":["Import"],"Migration":["Migracja"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importuj formularze z Contact Form 7, WPForms, Gravity Forms i innych wtyczek do SureForms."],"Could not generate the import preview.":["Nie mo\u017cna wygenerowa\u0107 podgl\u0105du importu."],"Import failed. Please try again or check your error logs.":["Importowanie nie powiod\u0142o si\u0119. Spr\u00f3buj ponownie lub sprawd\u017a swoje dzienniki b\u0142\u0119d\u00f3w."],"Go back":["Wr\u00f3\u0107"],"Update & import":["Aktualizuj i importuj"],"Confirm & import":["Potwierd\u017a i zaimportuj"],"Form #%s":["Formularz #%s"],"%d field will import":["%d pole zostanie zaimportowane"],"Some fields can't be migrated yet":["Niekt\u00f3re pola nie mog\u0105 jeszcze zosta\u0107 przeniesione"],"These fields will be skipped - add them manually after the form is created:":["Te pola zostan\u0105 pomini\u0119te - dodaj je r\u0119cznie po utworzeniu formularza:"],"Some forms could not be parsed":["Niekt\u00f3re formularze nie mog\u0142y zosta\u0107 przetworzone"],"Update existing":["Zaktualizuj istniej\u0105ce"],"Create a new copy":["Utw\u00f3rz now\u0105 kopi\u0119"],"Could not load forms for this source.":["Nie mo\u017cna za\u0142adowa\u0107 formularzy dla tego \u017ar\u00f3d\u0142a."],"(untitled form)":["(formularz bez tytu\u0142u)"],"Previously imported":["Poprzednio zaimportowane"],"Open existing SureForms form in a new tab":["Otw\u00f3rz istniej\u0105cy formularz SureForms w nowej karcie"],"On re-import":["Przy ponownym imporcie"],"No forms found in this plugin.":["Nie znaleziono formularzy w tej wtyczce."],"%1$d of %2$d form selected":["Wybrano %1$d z %2$d formularzy"],"Preview update":["Podgl\u0105d aktualizacji"],"Preview import":["Podgl\u0105d importu"],"Migration complete":["Migracja zako\u0144czona"],"Nothing was imported":["Nic nie zosta\u0142o zaimportowane"],"%d form was imported into SureForms.":["%d formularz zosta\u0142 zaimportowany do SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["\u017badna z wybranych form nie mog\u0142a zosta\u0107 przeniesiona. Zobacz poni\u017csze ostrze\u017cenia, aby uzyska\u0107 szczeg\u00f3\u0142y."],"Imported forms":["Zaimportowane formularze"],"Edit in SureForms":["Edytuj w SureForms"],"Some fields were skipped during import":["Niekt\u00f3re pola zosta\u0142y pomini\u0119te podczas importu"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Dodaj je r\u0119cznie w edytorze formularzy - nie maj\u0105 jeszcze odpowiednika w SureForms:"],"Some forms failed to import":["Niekt\u00f3re formularze nie zosta\u0142y zaimportowane"],"Import more forms":["Importuj wi\u0119cej formularzy"],"Could not load the list of importable plugins.":["Nie mo\u017cna za\u0142adowa\u0107 listy wtyczek do zaimportowania."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Na tej stronie nie s\u0105 aktywne \u017cadne obs\u0142ugiwane wtyczki formularzy. Aktywuj jedn\u0105 (tak\u0105 jak Contact Form 7) z co najmniej jednym formularzem, aby zaimportowa\u0107 go do SureForms."],"Plugin":["Wtyczka"],"%d form":["%d forma"],"No forms":["Brak formularzy"],"Multiple choice":["Wielokrotny wyb\u00f3r"],"Consent":["Zgoda"],"The image uploaded but could not be inserted. Please try again.":["Obraz zosta\u0142 przes\u0142any, ale nie mo\u017cna go by\u0142o wstawi\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["Nie mo\u017cna przes\u0142a\u0107 obrazu. Mo\u017ce by\u0107 zbyt du\u017cy lub mo\u017cesz nie mie\u0107 uprawnie\u0144 do przesy\u0142ania plik\u00f3w."],"Please choose an image file.":["Prosz\u0119 wybra\u0107 plik graficzny."],"Show views and conversion rate":["Poka\u017c widoki i wska\u017anik konwersji"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Dodaje kolumny Widoki i Wska\u017anik konwersji do listy Formularzy. Widok jest liczony raz na wizyt\u0119 na stronie, gdy formularz pojawia si\u0119 na ekranie, a wska\u017anik konwersji to udzia\u0142 tych widok\u00f3w, kt\u00f3re zako\u0144czy\u0142y si\u0119 przes\u0142aniem. Liczenie rozpoczyna si\u0119, gdy pierwszy raz to w\u0142\u0105czysz, wi\u0119c przes\u0142ania otrzymane przed tym momentem nie s\u0105 liczone do wska\u017anika. Wy\u0142\u0105czenie tego p\u00f3\u017aniej tylko ukrywa kolumny \u2014 liczenie trwa, wi\u0119c dane s\u0105 aktualne, je\u015bli ponownie to w\u0142\u0105czysz."],"Log cleared.":["Dziennik wyczyszczony."],"Could not clear the log.":["Nie mo\u017cna wyczy\u015bci\u0107 dziennika."],"Enable logs":["W\u0142\u0105cz dzienniki"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Rejestruje niepowodzenia przesy\u0142ania formularzy zg\u0142aszane przez przegl\u0105dark\u0119, dzi\u0119ki czemu mo\u017cesz wys\u0142a\u0107 dziennik do wsparcia zamiast czyta\u0107 konsol\u0119. Nic nie jest zapisywane, gdy Twoje formularze dzia\u0142aj\u0105, a przes\u0142ane warto\u015bci nigdy nie s\u0105 przechowywane."],"Download log":["Pobierz dziennik"],"Confirm delete":["Potwierd\u017a usuni\u0119cie"],"Log is full \u2014 download and clear it to keep recording.":["Dziennik jest pe\u0142ny \u2014 pobierz i wyczy\u015b\u0107 go, aby kontynuowa\u0107 rejestrowanie."],"Form Views & Conversion":["Widoki formularzy i konwersja"],"Logs":["Dzienniki"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Widoki i przesy\u0142ania przez osoby, kt\u00f3re mog\u0105 edytowa\u0107 stron\u0119, s\u0105 zwykle pomijane, wi\u0119c testowanie w\u0142asnych formularzy nie zmienia tych danych."],"Quill heading picker: default paragraph style\u0004Normal":["Normalny"]}}} \ No newline at end of file diff --git a/languages/sureforms-pl_PL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-pl_PL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json index a926983f8..255df4817 100644 --- a/languages/sureforms-pl_PL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json +++ b/languages/sureforms-pl_PL-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json @@ -1 +1 @@ -{"translation-revision-date":"2025-01-02T08:44:41+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Panel sterowania"],"Settings":["Ustawienia"],"Entries":["Wpisy"],"Activated":["Aktywowany"],"Advanced Fields":["Pola zaawansowane"],"Select Form":["Wybierz formularz"],"Create New Form":["Utw\u00f3rz nowy formularz"],"Forms":["Formularze"],"Copy":["Kopiuj"],"Business":["Biznes"],"Next":["Dalej"],"Back":["Wstecz"],"Cancel":["Anuluj"],"This is where your form views will appear":["To tutaj pojawi\u0105 si\u0119 widoki twojego formularza"],"Install":["Zainstaluj"],"Plugin Installation failed, Please try again later.":["Instalacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"Plugin activation failed, Please try again later.":["Aktywacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"What's New?":["Co nowego?"],"Core":["Rdze\u0144"],"Unlicensed":["Bez licencji"],"Upgrade":["Aktualizacja"],"Webhooks":["Webhooks"],"Install & Activate":["Zainstaluj i aktywuj"],"Conditional Logic":["Logika warunkowa"],"Premium":["Premium"],"Welcome to SureForms!":["Witamy w SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms to wtyczka do WordPressa, kt\u00f3ra umo\u017cliwia u\u017cytkownikom tworzenie pi\u0119knie wygl\u0105daj\u0105cych formularzy za pomoc\u0105 interfejsu przeci\u0105gnij i upu\u015b\u0107, bez potrzeby kodowania. Integruje si\u0119 z edytorem blok\u00f3w WordPress."],"Read Full Guide":["Przeczytaj pe\u0142ny przewodnik"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Niestandardowe formularze WordPress UPROSZCZONE"],"No Date":["Brak daty"],"Invalid Date":["Nieprawid\u0142owa data"],"Ready to go beyond free plan?":["Gotowy, aby wyj\u015b\u0107 poza darmowy plan?"],"Upgrade now":["Zaktualizuj teraz"],"and unlock the full power of SureForms!":["i odblokuj pe\u0142n\u0105 moc SureForms!"],"Upgrade SureForms":["Uaktualnij SureForms"],"Open Support Ticket":["Otw\u00f3rz zg\u0142oszenie do pomocy technicznej"],"Help Center":["Centrum Pomocy"],"Join our Community on Facebook":["Do\u0142\u0105cz do naszej spo\u0142eczno\u015bci na Facebooku"],"Leave Us a Review":["Zostaw nam recenzj\u0119"],"Quick Access":["Szybki dost\u0119p"],"Upgrade Now":["Zaktualizuj teraz"],"Clear Filters":["Wyczy\u015b\u0107 filtry"],"Unnamed Form":["Formularz bez nazwy"],"Forms Overview":["Przegl\u0105d formularzy"],"Clear Form Filters":["Wyczy\u015b\u0107 filtry formularza"],"Clear Date Filters":["Wyczy\u015b\u0107 filtry daty"],"Select Date Range":["Wybierz zakres dat"],"Apply":["Zastosuj"],"Please wait for the data to load":["Prosz\u0119 poczeka\u0107 na za\u0142adowanie danych"],"No entries to display":["Brak wpis\u00f3w do wy\u015bwietlenia"],"Once you create a form and start receiving submissions, the data will appear here.":["Gdy utworzysz formularz i zaczniesz otrzymywa\u0107 zg\u0142oszenia, dane pojawi\u0105 si\u0119 tutaj."],"Free":["Bezp\u0142atny"],"All Forms":["Wszystkie formularze"],"Guided Setup":["Przewodnik konfiguracji"],"Exit Guided Setup":["Zako\u0144cz konfiguracj\u0119 z przewodnikiem"],"Skip":["Pomi\u0144"],"Build beautiful forms visually":["Tw\u00f3rz pi\u0119kne formularze wizualnie"],"Works perfectly on mobile":["Dzia\u0142a perfekcyjnie na urz\u0105dzeniach mobilnych"],"Easy to connect with automation tools":["\u0141atwe do po\u0142\u0105czenia z narz\u0119dziami automatyzacji"],"Welcome to SureForms":["Witamy w SureForms"],"Smart, Quick and Powerful Forms.":["Inteligentne, szybkie i pot\u0119\u017cne formularze."],"Let's Get Started":["Zacznijmy"],"Build up to 10 forms using AI":["Stw\u00f3rz do 10 formularzy za pomoc\u0105 AI"],"A secure and private connection":["Bezpieczne i prywatne po\u0142\u0105czenie"],"Smart form drafts based on your input":["Inteligentne szkice formularzy na podstawie Twojego wk\u0142adu"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Rozpocz\u0119cie od pustego formularza nie zawsze jest \u0142atwe. Nasza sztuczna inteligencja mo\u017ce pom\u00f3c, tworz\u0105c szkic formularza na podstawie tego, co pr\u00f3bujesz zrobi\u0107 \u2014 oszcz\u0119dzaj\u0105c Tw\u00f3j czas i daj\u0105c Ci jasny kierunek."],"To do this, you'll need to connect your account.":["Aby to zrobi\u0107, musisz po\u0142\u0105czy\u0107 swoje konto."],"Let AI Help You Build Smarter, Faster Forms":["Pozw\u00f3l, aby AI pomog\u0142a Ci tworzy\u0107 m\u0105drzejsze, szybsze formularze"],"Here's what that gives you:":["Oto, co to ci daje:"],"Continue":["Kontynuuj"],"Connect":["Po\u0142\u0105cz"],"Works smoothly with forms made using SureForms":["Dzia\u0142a p\u0142ynnie z formularzami stworzonymi za pomoc\u0105 SureForms"],"Helps your emails reach the inbox instead of spam":["Pomaga Twoim e-mailom dotrze\u0107 do skrzynki odbiorczej zamiast do spamu"],"Setup is straightforward, even if you're not technical":["Konfiguracja jest prosta, nawet je\u015bli nie jeste\u015b techniczny"],"Lightweight and easy to use without adding clutter":["Lekki i \u0142atwy w u\u017cyciu, bez dodawania ba\u0142aganu"],"Make Sure Your Emails Get Delivered":["Upewnij si\u0119, \u017ce Twoje e-maile s\u0105 dostarczane"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["Wi\u0119kszo\u015b\u0107 witryn WordPress ma trudno\u015bci z niezawodnym wysy\u0142aniem e-maili, co oznacza, \u017ce zg\u0142oszenia formularzy z Twojej strony mog\u0105 nie dotrze\u0107 do Twojej skrzynki odbiorczej \u2014 lub trafi\u0107 do spamu."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail to prosty plugin SMTP, kt\u00f3ry pomaga upewni\u0107 si\u0119, \u017ce Twoje e-maile faktycznie zostan\u0105 dostarczone."],"What you will get:":["Co otrzymasz:"],"Install SureMail":["Zainstaluj SureMail"],"AI Form Generation":["Generowanie formularzy AI"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Zm\u0119czony r\u0119cznym tworzeniem formularzy? Pozw\u00f3l, aby AI wykona\u0142a prac\u0119 za Ciebie. Wystarczy opisa\u0107, a nasza AI stworzy idealny formularz w kilka sekund."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Podziel z\u0142o\u017cone formularze na proste kroki, zmniejszaj\u0105c przyt\u0142oczenie i zwi\u0119kszaj\u0105c wska\u017aniki uko\u0144czenia. Prowad\u017a u\u017cytkownik\u00f3w p\u0142ynnie przez proces"],"Conditional Fields":["Pola warunkowe"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Poka\u017c lub ukryj pola w zale\u017cno\u015bci od odpowiedzi u\u017cytkownika. Zadawaj w\u0142a\u015bciwe pytania i wy\u015bwietlaj tylko to, co jest potrzebne, aby formularze by\u0142y przejrzyste i istotne."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Ulepsz swoje formularze za pomoc\u0105 zaawansowanych p\u00f3l, takich jak przesy\u0142anie wielu plik\u00f3w, pola oceny oraz selektory daty i czasu, aby zbiera\u0107 bogatsze i bardziej elastyczne dane."],"Conversational Forms":["Formy konwersacyjne"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Tw\u00f3rz formularze, kt\u00f3re przypominaj\u0105 rozmow\u0119. Jedno pytanie na raz utrzymuje zaanga\u017cowanie u\u017cytkownik\u00f3w i u\u0142atwia wype\u0142nianie formularzy."],"Digital Signatures":["Podpisy cyfrowe"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Zbieraj prawnie wi\u0105\u017c\u0105ce podpisy cyfrowe bezpo\u015brednio w swoich formularzach do um\u00f3w, zatwierdze\u0144 i kontrakt\u00f3w."],"Calculators":["Kalkulatory"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Dodaj interaktywne kalkulatory do swoich formularzy, aby zapewni\u0107 u\u017cytkownikom natychmiastowe szacunki, oferty i obliczenia."],"User Registration and Login":["Rejestracja i logowanie u\u017cytkownika"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Pozw\u00f3l odwiedzaj\u0105cym rejestrowa\u0107 si\u0119 i logowa\u0107 na Twojej stronie. Przydatne dla cz\u0142onkostwa, spo\u0142eczno\u015bci lub ka\u017cdej strony, kt\u00f3ra wymaga dost\u0119pu u\u017cytkownik\u00f3w."],"PDF Generation Made Simple":["Generowanie PDF w prosty spos\u00f3b"],"Custom App":["Aplikacja niestandardowa"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Zbieraj dane, wysy\u0142aj je do zewn\u0119trznych aplikacji do przetwarzania i natychmiast wy\u015bwietlaj wyniki \u2014 wszystko to p\u0142ynnie zintegrowane, aby tworzy\u0107 dynamiczne, interaktywne do\u015bwiadczenia u\u017cytkownika."],"Select Your Features":["Wybierz swoje funkcje"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Uzyskaj wi\u0119ksz\u0105 kontrol\u0119, szybsze przep\u0142ywy pracy i g\u0142\u0119bsz\u0105 personalizacj\u0119 \u2014 wszystko zaprojektowane, aby pom\u00f3c Ci tworzy\u0107 lepsze strony internetowe przy mniejszym wysi\u0142ku."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Wybrane funkcje wymagaj\u0105 %1$s - u\u017cyj kodu %2$s, aby uzyska\u0107 10% zni\u017cki na dowolny plan."],"Copied":["Skopiowano"],"Style your form to better match your site's design":["Dostosuj styl formularza, aby lepiej pasowa\u0142 do projektu Twojej strony"],"Add spam protection to block common bot submissions":["Dodaj ochron\u0119 przed spamem, aby blokowa\u0107 typowe zg\u0142oszenia bot\u00f3w"],"Get weekly email reports with a summary of form activity":["Otrzymuj cotygodniowe raporty e-mailowe z podsumowaniem aktywno\u015bci formularza"],"You're All Set! \ud83d\ude80":["Wszystko gotowe! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Skorzystaj z naszego kreatora formularzy AI, aby szybko rozpocz\u0105\u0107, lub stw\u00f3rz formularz od podstaw, je\u015bli ju\u017c wiesz, czego potrzebujesz. Twoje formularze s\u0105 gotowe do tworzenia, udost\u0119pniania i \u0142\u0105czenia z odwiedzaj\u0105cymi Twoj\u0105 stron\u0119."],"Final Touches That Make a Difference:":["Ostatnie szlify, kt\u00f3re robi\u0105 r\u00f3\u017cnic\u0119:"],"Build Your First Form":["Zbuduj sw\u00f3j pierwszy formularz"],"File Uploads":["Przesy\u0142anie plik\u00f3w"],"Signature & Rating":["Podpis i Ocena"],"Calculation Forms":["Formularze obliczeniowe"],"And Much More\u2026":["I wiele wi\u0119cej\u2026"],"Upgrade to Pro":["Uaktualnij do wersji Pro"],"Unlock Premium Features":["Odblokuj funkcje premium"],"Build Better Forms with SureForms":["Tw\u00f3rz lepsze formularze z SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Dodaj zaawansowane pola, konwersacyjne uk\u0142ady i inteligentn\u0105 logik\u0119, aby tworzy\u0107 formularze, kt\u00f3re anga\u017cuj\u0105 u\u017cytkownik\u00f3w i zbieraj\u0105 lepsze dane."],"SureForms Video Thumbnail":["Miniatura wideo SureForms"],"Payments":["P\u0142atno\u015bci"],"Knowledge Base":["Baza wiedzy"],"What\u2019s New":["Co nowego"],"Importing\u2026":["Importowanie\u2026"],"Learn":["Ucz si\u0119"],"Unable to complete action. Please try again.":["Nie mo\u017cna uko\u0144czy\u0107 akcji. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"Supercharge Your Workflow":["Podkr\u0119\u0107 sw\u00f3j przep\u0142yw pracy"],"Spam protection included":["W zestawie ochrona przed spamem"],"Multistep Forms":["Formularze wieloetapowe"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Wysy\u0142aj wpisy formularzy natychmiast do dowolnego zewn\u0119trznego systemu lub punktu ko\u0144cowego, aby zasila\u0107 zaawansowane przep\u0142ywy pracy."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Automatycznie przekszta\u0142caj wpisy formularzy w czyste, gotowe do pobrania pliki PDF. Idealne do rejestrowania, udost\u0119pniania, archiwizowania lub utrzymywania porz\u0105dku."],"Set up confirmation messages and email notifications for each entry":["Skonfiguruj wiadomo\u015bci potwierdzaj\u0105ce i powiadomienia e-mail dla ka\u017cdego wpisu"],"Something went wrong. Please try again.":["Co\u015b posz\u0142o nie tak. Prosz\u0119 spr\u00f3buj ponownie."],"Payment Forms":["Formularze p\u0142atno\u015bci"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Zbieraj p\u0142atno\u015bci bezpo\u015brednio przez swoje formularze. Akceptuj jednorazowe i cykliczne p\u0142atno\u015bci bezproblemowo."],"SureForms %s":["SureForms %s"],"First name is required.":["Imi\u0119 jest wymagane."],"Please enter a valid email address.":["Prosz\u0119 wprowadzi\u0107 prawid\u0142owy adres e-mail."],"Email address is required.":["Adres e-mail jest wymagany."],"This is required.":["To jest wymagane."],"Okay, just one last step\u2026":["W porz\u0105dku, jeszcze tylko jeden krok\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Pom\u00f3\u017c nam dostosowa\u0107 Twoje do\u015bwiadczenie z SureForms, dziel\u0105c si\u0119 kilkoma informacjami o sobie."],"First Name":["Imi\u0119"],"Enter your first name":["Wprowad\u017a swoje imi\u0119"],"Last Name":["Nazwisko"],"Enter your last name":["Wpisz swoje nazwisko"],"Email Address":["Adres e-mail"],"Enter your email address":["Wprowad\u017a sw\u00f3j adres e-mail"],"Privacy Policy":["Polityka prywatno\u015bci"],"Finish":["Zako\u0144cz"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Pozosta\u0144 na bie\u017c\u0105co i pom\u00f3\u017c kszta\u0142towa\u0107 SureForms! Otrzymuj aktualizacje funkcji i pom\u00f3\u017c nam w ulepszaniu SureForms, dziel\u0105c si\u0119 tym, jak u\u017cywasz wtyczki. Polityka prywatno\u015bci<\/a>."],"%d form ready":["%d formularz gotowy"],"%d already imported":["%d ju\u017c zaimportowano"],"(unnamed source)":["(nienazwane \u017ar\u00f3d\u0142o)"],"All forms already imported":["Wszystkie formularze ju\u017c zaimportowane"],"Import forms":["Importuj formularze"],"Import %d form":["Importuj %d formularz"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Co\u015b posz\u0142o nie tak podczas importowania. Mo\u017cesz spr\u00f3bowa\u0107 ponownie, pomin\u0105\u0107 lub otworzy\u0107 Ustawienia \u2192 Migracja."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Nie wykryto innych wtyczek formularzy. Mo\u017cesz zaimportowa\u0107 w dowolnym momencie z Ustawienia \u2192 Migracja."],"Forms imported":["Formularze zaimportowane"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formularz z %2$s jest teraz w SureForms, gotowy do publikacji, stylizacji i po\u0142\u0105czenia."],"%d form imported":["Zaimportowano %d formularz"],"%d form could not be imported":["Nie mo\u017cna zaimportowa\u0107 %d formularza"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d typ pola nie by\u0142 obs\u0142ugiwany. Mo\u017cesz go odbudowa\u0107 r\u0119cznie w SureForms."],"Bring your existing forms with you":["Przynie\u015b ze sob\u0105 swoje istniej\u0105ce formularze"],"We detected forms in another plugin. Pick one to import into SureForms.":["Wykryli\u015bmy formularze w innym wtyczce. Wybierz jeden do zaimportowania do SureForms."],"Choose a form plugin to import from":["Wybierz wtyczk\u0119 formularza do zaimportowania"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importujesz wi\u0119cej ni\u017c jedn\u0105 wtyczk\u0119? Mo\u017cesz zaimportowa\u0107 dodatkowe \u017ar\u00f3d\u0142a p\u00f3\u017aniej z Ustawienia \u2192 Migracja."],"Import did not complete":["Import nie zosta\u0142 zako\u0144czony"],"I'll do this later":["Zrobi\u0119 to p\u00f3\u017aniej"],"Retry":["Pon\u00f3w"],"Form Checks":["Kontrole formularzy"],"Collapse form checks":["Zwi\u0144 kontrole formularzy"],"Expand form checks":["Rozwi\u0144 kontrole formularzy"],"Ignore":["Ignoruj"],"Working\u2026":["Pracuj\u0119\u2026"]}}} \ No newline at end of file +{"translation-revision-date":"2025-01-02T08:44:41+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Panel sterowania"],"Settings":["Ustawienia"],"Entries":["Wpisy"],"Activated":["Aktywowany"],"Advanced Fields":["Pola zaawansowane"],"Select Form":["Wybierz formularz"],"Create New Form":["Utw\u00f3rz nowy formularz"],"Forms":["Formularze"],"Copy":["Kopiuj"],"Business":["Biznes"],"Next":["Dalej"],"Back":["Wstecz"],"Close":["Zamknij"],"Cancel":["Anuluj"],"This is where your form views will appear":["To tutaj pojawi\u0105 si\u0119 widoki twojego formularza"],"Install":["Zainstaluj"],"Plugin Installation failed, Please try again later.":["Instalacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"Plugin activation failed, Please try again later.":["Aktywacja wtyczki nie powiod\u0142a si\u0119, spr\u00f3buj ponownie p\u00f3\u017aniej."],"What's New?":["Co nowego?"],"Core":["Rdze\u0144"],"Unlicensed":["Bez licencji"],"Upgrade":["Aktualizacja"],"Webhooks":["Webhooks"],"Install & Activate":["Zainstaluj i aktywuj"],"Contact Support":["Skontaktuj si\u0119 z pomoc\u0105 techniczn\u0105"],"Conditional Logic":["Logika warunkowa"],"Premium":["Premium"],"Welcome to SureForms!":["Witamy w SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms to wtyczka do WordPressa, kt\u00f3ra umo\u017cliwia u\u017cytkownikom tworzenie pi\u0119knie wygl\u0105daj\u0105cych formularzy za pomoc\u0105 interfejsu przeci\u0105gnij i upu\u015b\u0107, bez potrzeby kodowania. Integruje si\u0119 z edytorem blok\u00f3w WordPress."],"Read Full Guide":["Przeczytaj pe\u0142ny przewodnik"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Niestandardowe formularze WordPress UPROSZCZONE"],"No Date":["Brak daty"],"Invalid Date":["Nieprawid\u0142owa data"],"Ready to go beyond free plan?":["Gotowy, aby wyj\u015b\u0107 poza darmowy plan?"],"Upgrade now":["Zaktualizuj teraz"],"and unlock the full power of SureForms!":["i odblokuj pe\u0142n\u0105 moc SureForms!"],"Upgrade SureForms":["Uaktualnij SureForms"],"Open Support Ticket":["Otw\u00f3rz zg\u0142oszenie do pomocy technicznej"],"Help Center":["Centrum Pomocy"],"Join our Community on Facebook":["Do\u0142\u0105cz do naszej spo\u0142eczno\u015bci na Facebooku"],"Leave Us a Review":["Zostaw nam recenzj\u0119"],"Quick Access":["Szybki dost\u0119p"],"Upgrade Now":["Zaktualizuj teraz"],"Clear Filters":["Wyczy\u015b\u0107 filtry"],"Unnamed Form":["Formularz bez nazwy"],"Forms Overview":["Przegl\u0105d formularzy"],"Clear Form Filters":["Wyczy\u015b\u0107 filtry formularza"],"Clear Date Filters":["Wyczy\u015b\u0107 filtry daty"],"Select Date Range":["Wybierz zakres dat"],"Apply":["Zastosuj"],"Please wait for the data to load":["Prosz\u0119 poczeka\u0107 na za\u0142adowanie danych"],"No entries to display":["Brak wpis\u00f3w do wy\u015bwietlenia"],"Once you create a form and start receiving submissions, the data will appear here.":["Gdy utworzysz formularz i zaczniesz otrzymywa\u0107 zg\u0142oszenia, dane pojawi\u0105 si\u0119 tutaj."],"Free":["Bezp\u0142atny"],"All Forms":["Wszystkie formularze"],"Guided Setup":["Przewodnik konfiguracji"],"Exit Guided Setup":["Zako\u0144cz konfiguracj\u0119 z przewodnikiem"],"Skip":["Pomi\u0144"],"Build beautiful forms visually":["Tw\u00f3rz pi\u0119kne formularze wizualnie"],"Works perfectly on mobile":["Dzia\u0142a perfekcyjnie na urz\u0105dzeniach mobilnych"],"Easy to connect with automation tools":["\u0141atwe do po\u0142\u0105czenia z narz\u0119dziami automatyzacji"],"Welcome to SureForms":["Witamy w SureForms"],"Smart, Quick and Powerful Forms.":["Inteligentne, szybkie i pot\u0119\u017cne formularze."],"Let's Get Started":["Zacznijmy"],"Build up to 10 forms using AI":["Stw\u00f3rz do 10 formularzy za pomoc\u0105 AI"],"A secure and private connection":["Bezpieczne i prywatne po\u0142\u0105czenie"],"Smart form drafts based on your input":["Inteligentne szkice formularzy na podstawie Twojego wk\u0142adu"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Rozpocz\u0119cie od pustego formularza nie zawsze jest \u0142atwe. Nasza sztuczna inteligencja mo\u017ce pom\u00f3c, tworz\u0105c szkic formularza na podstawie tego, co pr\u00f3bujesz zrobi\u0107 \u2014 oszcz\u0119dzaj\u0105c Tw\u00f3j czas i daj\u0105c Ci jasny kierunek."],"To do this, you'll need to connect your account.":["Aby to zrobi\u0107, musisz po\u0142\u0105czy\u0107 swoje konto."],"Let AI Help You Build Smarter, Faster Forms":["Pozw\u00f3l, aby AI pomog\u0142a Ci tworzy\u0107 m\u0105drzejsze, szybsze formularze"],"Here's what that gives you:":["Oto, co to ci daje:"],"Continue":["Kontynuuj"],"Connect":["Po\u0142\u0105cz"],"Works smoothly with forms made using SureForms":["Dzia\u0142a p\u0142ynnie z formularzami stworzonymi za pomoc\u0105 SureForms"],"Helps your emails reach the inbox instead of spam":["Pomaga Twoim e-mailom dotrze\u0107 do skrzynki odbiorczej zamiast do spamu"],"Setup is straightforward, even if you're not technical":["Konfiguracja jest prosta, nawet je\u015bli nie jeste\u015b techniczny"],"Lightweight and easy to use without adding clutter":["Lekki i \u0142atwy w u\u017cyciu, bez dodawania ba\u0142aganu"],"Make Sure Your Emails Get Delivered":["Upewnij si\u0119, \u017ce Twoje e-maile s\u0105 dostarczane"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["Wi\u0119kszo\u015b\u0107 witryn WordPress ma trudno\u015bci z niezawodnym wysy\u0142aniem e-maili, co oznacza, \u017ce zg\u0142oszenia formularzy z Twojej strony mog\u0105 nie dotrze\u0107 do Twojej skrzynki odbiorczej \u2014 lub trafi\u0107 do spamu."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail to prosty plugin SMTP, kt\u00f3ry pomaga upewni\u0107 si\u0119, \u017ce Twoje e-maile faktycznie zostan\u0105 dostarczone."],"What you will get:":["Co otrzymasz:"],"Install SureMail":["Zainstaluj SureMail"],"AI Form Generation":["Generowanie formularzy AI"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Zm\u0119czony r\u0119cznym tworzeniem formularzy? Pozw\u00f3l, aby AI wykona\u0142a prac\u0119 za Ciebie. Wystarczy opisa\u0107, a nasza AI stworzy idealny formularz w kilka sekund."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Podziel z\u0142o\u017cone formularze na proste kroki, zmniejszaj\u0105c przyt\u0142oczenie i zwi\u0119kszaj\u0105c wska\u017aniki uko\u0144czenia. Prowad\u017a u\u017cytkownik\u00f3w p\u0142ynnie przez proces"],"Conditional Fields":["Pola warunkowe"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Poka\u017c lub ukryj pola w zale\u017cno\u015bci od odpowiedzi u\u017cytkownika. Zadawaj w\u0142a\u015bciwe pytania i wy\u015bwietlaj tylko to, co jest potrzebne, aby formularze by\u0142y przejrzyste i istotne."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Ulepsz swoje formularze za pomoc\u0105 zaawansowanych p\u00f3l, takich jak przesy\u0142anie wielu plik\u00f3w, pola oceny oraz selektory daty i czasu, aby zbiera\u0107 bogatsze i bardziej elastyczne dane."],"Conversational Forms":["Formy konwersacyjne"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Tw\u00f3rz formularze, kt\u00f3re przypominaj\u0105 rozmow\u0119. Jedno pytanie na raz utrzymuje zaanga\u017cowanie u\u017cytkownik\u00f3w i u\u0142atwia wype\u0142nianie formularzy."],"Digital Signatures":["Podpisy cyfrowe"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Zbieraj prawnie wi\u0105\u017c\u0105ce podpisy cyfrowe bezpo\u015brednio w swoich formularzach do um\u00f3w, zatwierdze\u0144 i kontrakt\u00f3w."],"Calculators":["Kalkulatory"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Dodaj interaktywne kalkulatory do swoich formularzy, aby zapewni\u0107 u\u017cytkownikom natychmiastowe szacunki, oferty i obliczenia."],"User Registration and Login":["Rejestracja i logowanie u\u017cytkownika"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Pozw\u00f3l odwiedzaj\u0105cym rejestrowa\u0107 si\u0119 i logowa\u0107 na Twojej stronie. Przydatne dla cz\u0142onkostwa, spo\u0142eczno\u015bci lub ka\u017cdej strony, kt\u00f3ra wymaga dost\u0119pu u\u017cytkownik\u00f3w."],"PDF Generation Made Simple":["Generowanie PDF w prosty spos\u00f3b"],"Custom App":["Aplikacja niestandardowa"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Zbieraj dane, wysy\u0142aj je do zewn\u0119trznych aplikacji do przetwarzania i natychmiast wy\u015bwietlaj wyniki \u2014 wszystko to p\u0142ynnie zintegrowane, aby tworzy\u0107 dynamiczne, interaktywne do\u015bwiadczenia u\u017cytkownika."],"Select Your Features":["Wybierz swoje funkcje"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Uzyskaj wi\u0119ksz\u0105 kontrol\u0119, szybsze przep\u0142ywy pracy i g\u0142\u0119bsz\u0105 personalizacj\u0119 \u2014 wszystko zaprojektowane, aby pom\u00f3c Ci tworzy\u0107 lepsze strony internetowe przy mniejszym wysi\u0142ku."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Wybrane funkcje wymagaj\u0105 %1$s - u\u017cyj kodu %2$s, aby uzyska\u0107 10% zni\u017cki na dowolny plan."],"Copied":["Skopiowano"],"Style your form to better match your site's design":["Dostosuj styl formularza, aby lepiej pasowa\u0142 do projektu Twojej strony"],"Add spam protection to block common bot submissions":["Dodaj ochron\u0119 przed spamem, aby blokowa\u0107 typowe zg\u0142oszenia bot\u00f3w"],"Get weekly email reports with a summary of form activity":["Otrzymuj cotygodniowe raporty e-mailowe z podsumowaniem aktywno\u015bci formularza"],"You're All Set! \ud83d\ude80":["Wszystko gotowe! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Skorzystaj z naszego kreatora formularzy AI, aby szybko rozpocz\u0105\u0107, lub stw\u00f3rz formularz od podstaw, je\u015bli ju\u017c wiesz, czego potrzebujesz. Twoje formularze s\u0105 gotowe do tworzenia, udost\u0119pniania i \u0142\u0105czenia z odwiedzaj\u0105cymi Twoj\u0105 stron\u0119."],"Final Touches That Make a Difference:":["Ostatnie szlify, kt\u00f3re robi\u0105 r\u00f3\u017cnic\u0119:"],"Build Your First Form":["Zbuduj sw\u00f3j pierwszy formularz"],"File Uploads":["Przesy\u0142anie plik\u00f3w"],"Signature & Rating":["Podpis i Ocena"],"Calculation Forms":["Formularze obliczeniowe"],"And Much More\u2026":["I wiele wi\u0119cej\u2026"],"Upgrade to Pro":["Uaktualnij do wersji Pro"],"Unlock Premium Features":["Odblokuj funkcje premium"],"Build Better Forms with SureForms":["Tw\u00f3rz lepsze formularze z SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Dodaj zaawansowane pola, konwersacyjne uk\u0142ady i inteligentn\u0105 logik\u0119, aby tworzy\u0107 formularze, kt\u00f3re anga\u017cuj\u0105 u\u017cytkownik\u00f3w i zbieraj\u0105 lepsze dane."],"SureForms Video Thumbnail":["Miniatura wideo SureForms"],"Payments":["P\u0142atno\u015bci"],"Knowledge Base":["Baza wiedzy"],"What\u2019s New":["Co nowego"],"Importing\u2026":["Importowanie\u2026"],"Learn":["Ucz si\u0119"],"Unable to complete action. Please try again.":["Nie mo\u017cna uko\u0144czy\u0107 akcji. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."],"Supercharge Your Workflow":["Podkr\u0119\u0107 sw\u00f3j przep\u0142yw pracy"],"Spam protection included":["W zestawie ochrona przed spamem"],"Multistep Forms":["Formularze wieloetapowe"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Wysy\u0142aj wpisy formularzy natychmiast do dowolnego zewn\u0119trznego systemu lub punktu ko\u0144cowego, aby zasila\u0107 zaawansowane przep\u0142ywy pracy."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Automatycznie przekszta\u0142caj wpisy formularzy w czyste, gotowe do pobrania pliki PDF. Idealne do rejestrowania, udost\u0119pniania, archiwizowania lub utrzymywania porz\u0105dku."],"Set up confirmation messages and email notifications for each entry":["Skonfiguruj wiadomo\u015bci potwierdzaj\u0105ce i powiadomienia e-mail dla ka\u017cdego wpisu"],"Something went wrong. Please try again.":["Co\u015b posz\u0142o nie tak. Prosz\u0119 spr\u00f3buj ponownie."],"Payment Forms":["Formularze p\u0142atno\u015bci"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Zbieraj p\u0142atno\u015bci bezpo\u015brednio przez swoje formularze. Akceptuj jednorazowe i cykliczne p\u0142atno\u015bci bezproblemowo."],"SureForms %s":["SureForms %s"],"First name is required.":["Imi\u0119 jest wymagane."],"Please enter a valid email address.":["Prosz\u0119 wprowadzi\u0107 prawid\u0142owy adres e-mail."],"Email address is required.":["Adres e-mail jest wymagany."],"This is required.":["To jest wymagane."],"Okay, just one last step\u2026":["W porz\u0105dku, jeszcze tylko jeden krok\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Pom\u00f3\u017c nam dostosowa\u0107 Twoje do\u015bwiadczenie z SureForms, dziel\u0105c si\u0119 kilkoma informacjami o sobie."],"First Name":["Imi\u0119"],"Enter your first name":["Wprowad\u017a swoje imi\u0119"],"Last Name":["Nazwisko"],"Enter your last name":["Wpisz swoje nazwisko"],"Email Address":["Adres e-mail"],"Enter your email address":["Wprowad\u017a sw\u00f3j adres e-mail"],"Privacy Policy":["Polityka prywatno\u015bci"],"Finish":["Zako\u0144cz"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Pozosta\u0144 na bie\u017c\u0105co i pom\u00f3\u017c kszta\u0142towa\u0107 SureForms! Otrzymuj aktualizacje funkcji i pom\u00f3\u017c nam w ulepszaniu SureForms, dziel\u0105c si\u0119 tym, jak u\u017cywasz wtyczki. Polityka prywatno\u015bci<\/a>."],"%d form ready":["%d formularz gotowy"],"%d already imported":["%d ju\u017c zaimportowano"],"(unnamed source)":["(nienazwane \u017ar\u00f3d\u0142o)"],"All forms already imported":["Wszystkie formularze ju\u017c zaimportowane"],"Import forms":["Importuj formularze"],"Import %d form":["Importuj %d formularz"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Co\u015b posz\u0142o nie tak podczas importowania. Mo\u017cesz spr\u00f3bowa\u0107 ponownie, pomin\u0105\u0107 lub otworzy\u0107 Ustawienia \u2192 Migracja."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Nie wykryto innych wtyczek formularzy. Mo\u017cesz zaimportowa\u0107 w dowolnym momencie z Ustawienia \u2192 Migracja."],"Forms imported":["Formularze zaimportowane"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formularz z %2$s jest teraz w SureForms, gotowy do publikacji, stylizacji i po\u0142\u0105czenia."],"%d form imported":["Zaimportowano %d formularz"],"%d form could not be imported":["Nie mo\u017cna zaimportowa\u0107 %d formularza"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d typ pola nie by\u0142 obs\u0142ugiwany. Mo\u017cesz go odbudowa\u0107 r\u0119cznie w SureForms."],"Bring your existing forms with you":["Przynie\u015b ze sob\u0105 swoje istniej\u0105ce formularze"],"We detected forms in another plugin. Pick one to import into SureForms.":["Wykryli\u015bmy formularze w innym wtyczce. Wybierz jeden do zaimportowania do SureForms."],"Choose a form plugin to import from":["Wybierz wtyczk\u0119 formularza do zaimportowania"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importujesz wi\u0119cej ni\u017c jedn\u0105 wtyczk\u0119? Mo\u017cesz zaimportowa\u0107 dodatkowe \u017ar\u00f3d\u0142a p\u00f3\u017aniej z Ustawienia \u2192 Migracja."],"Import did not complete":["Import nie zosta\u0142 zako\u0144czony"],"I'll do this later":["Zrobi\u0119 to p\u00f3\u017aniej"],"Retry":["Pon\u00f3w"],"Form Checks":["Kontrole formularzy"],"Collapse form checks":["Zwi\u0144 kontrole formularzy"],"Expand form checks":["Rozwi\u0144 kontrole formularzy"],"Ignore":["Ignoruj"],"Working\u2026":["Pracuj\u0119\u2026"],"Details":["Szczeg\u00f3\u0142y"],"Copy details":["Skopiuj szczeg\u00f3\u0142y"]}}} \ No newline at end of file diff --git a/languages/sureforms-pl_PL.mo b/languages/sureforms-pl_PL.mo index 874783a27d3b236652eb0bd7351275539c5a9568..813ee4762650c2dcb273f008a4bb3a109632c2b6 100644 GIT binary patch delta 76438 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

h7Y4tGABAOx*)nR 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 delta 72783 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" msgstr "Oczekiwany format dla e-maili - email@sureforms.com lub John Doe " -#: admin/admin.php:1471 -#: admin/admin.php:1472 +#: admin/admin.php:1517 +#: admin/admin.php:1518 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -12173,7 +12179,7 @@ msgstr "Podnieś swoje SEO i wspinaj się w rankingach wyszukiwania bez wysiłku msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress." msgstr "Zwiększ widoczność swojej strony internetowej dzięki inteligentnej automatyzacji SEO. Optymalizuj treści, śledź wydajność słów kluczowych i uzyskaj praktyczne wskazówki, wszystko w WordPressie." -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -15372,12 +15378,12 @@ msgid "Stripe Settings" msgstr "Ustawienia Stripe" #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. -#: admin/admin.php:2281 +#: admin/admin.php:2335 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version." msgstr "SureForms %1$s wymaga co najmniej %2$s %3$s do prawidłowego działania. Proszę zaktualizować do najnowszej wersji." -#: admin/admin.php:2294 +#: admin/admin.php:2348 msgid "Update Now" msgstr "Zaktualizuj teraz" @@ -15425,8 +15431,8 @@ msgstr "Pozycja znaku waluty" msgid "Select the position of the currency symbol relative to the amount." msgstr "Wybierz pozycję symbolu waluty względem kwoty." -#: admin/admin.php:1414 -#: admin/admin.php:1415 +#: admin/admin.php:1460 +#: admin/admin.php:1461 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -15440,41 +15446,41 @@ msgstr "Wybierz pozycję symbolu waluty względem kwoty." msgid "Learn" msgstr "Ucz się" -#: admin/admin.php:2635 +#: admin/admin.php:2689 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!" msgstr "Niesamowite! SureForms napędza Twoje formularze i zgłoszenia - rozwijajmy się razem!" -#: admin/admin.php:2636 +#: admin/admin.php:2690 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?" msgstr "Jeśli SureForms okazał się pomocny, czy mógłbyś poświęcić chwilę na pozostawienie 5-gwiazdkowej recenzji na WordPress.org?" -#: admin/admin.php:2647 -#: admin/admin.php:2711 +#: admin/admin.php:2701 +#: admin/admin.php:2765 msgid "Maybe later" msgstr "Może później" -#: admin/admin.php:2653 +#: admin/admin.php:2707 msgid "I already did" msgstr "Już to zrobiłem" -#: admin/admin.php:2702 +#: admin/admin.php:2756 msgid "SureForms is ready to power your forms — explore what's possible!" msgstr "SureForms jest gotowy, aby zasilić Twoje formularze — odkryj, co jest możliwe!" -#: admin/admin.php:2703 +#: admin/admin.php:2757 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard." msgstr "Zarządzaj swoimi formularzami, śledź zgłoszenia i odkrywaj funkcje takie jak AI Form Builder, integracje płatności i inne z poziomu pulpitu SureForms." -#: admin/admin.php:2706 +#: admin/admin.php:2760 msgid "Go to Dashboard" msgstr "Przejdź do pulpitu nawigacyjnego" -#: admin/admin.php:2717 +#: admin/admin.php:2771 msgid "I already know" msgstr "Już wiem" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2981 +#: admin/admin.php:3854 msgid "Invalid parameters." msgstr "Nieprawidłowe parametry." @@ -15858,10 +15864,10 @@ msgid "This form is not yet available. Check back after the scheduled start time msgstr "Ten formularz nie jest jeszcze dostępny. Sprawdź ponownie po zaplanowanym czasie rozpoczęcia." #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -15884,48 +15890,48 @@ msgstr "Ten formularz nie jest jeszcze dostępny. Sprawdź ponownie po zaplanowa msgid "Security verification failed. Please refresh the page and try again." msgstr "Weryfikacja bezpieczeństwa nie powiodła się. Odśwież stronę i spróbuj ponownie." -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "Dane formularza nie zostały znalezione." -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "Brakuje identyfikatora formularza." -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "Przepraszamy, nie masz uprawnień do dostępu do tego zasobu." -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "Nie masz uprawnień do przesłania tego formularza." -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "Ten formularz nie jest już dostępny." -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "Proszę sprawdzić formularz pod kątem błędów." -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "Twoje zgłoszenie zostało oznaczone jako spam. Proszę spróbuj ponownie." -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "Nie można przesłać formularza. Proszę spróbować ponownie." -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "Nie wykryto wtyczki SMTP. Proszę skonfigurować wtyczkę SMTP, aby umożliwić wysyłanie e-maili." -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "Wysyłanie e-maila nie powiodło się z nieznanego powodu." -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "Nie wysłano żadnych e-maili." @@ -16438,7 +16444,7 @@ msgid "Duplicate form?" msgstr "Powielić formularz?" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:938 msgid "An error occurred while submitting your form. Please try again." msgstr "Wystąpił błąd podczas przesyłania formularza. Proszę spróbować ponownie." @@ -16815,28 +16821,28 @@ msgstr "Tajny klucz webhooka nie jest skonfigurowany." msgid "Invalid webhook signature." msgstr "Nieprawidłowy podpis webhooka." -#: admin/admin.php:1253 -#: admin/admin.php:1851 +#: admin/admin.php:1299 +#: admin/admin.php:1905 #: assets/build/formEditor.js:172 msgid "Quizzes" msgstr "Quizy" -#: admin/admin.php:1252 +#: admin/admin.php:1298 msgid "Quiz Entries" msgstr "Wpisy do quizu" -#: admin/admin.php:1255 -#: admin/admin.php:1288 -#: admin/admin.php:1321 +#: admin/admin.php:1301 +#: admin/admin.php:1334 +#: admin/admin.php:1367 #: assets/build/settings.js:172 msgid "New" msgstr "Nowy" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "Nieprawidłowy formularz." -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "Zbyt wiele żądań. Proszę spróbować ponownie za chwilę." @@ -17345,9 +17351,9 @@ msgstr "Idealne do edukacji, szkoleń i zabawnych ciekawostek" msgid "Select this to create a quiz with scored questions and graded results." msgstr "Wybierz to, aby utworzyć quiz z punktowanymi pytaniami i ocenianymi wynikami." -#: admin/admin.php:1285 -#: admin/admin.php:1286 -#: admin/admin.php:1856 +#: admin/admin.php:1331 +#: admin/admin.php:1332 +#: admin/admin.php:1910 msgid "Survey Reports" msgstr "Raporty z ankiet" @@ -17753,7 +17759,7 @@ msgid "Error creating SureForms Form." msgstr "Błąd podczas tworzenia formularza SureForms." #. translators: %s represents the minimum number of characters required -#: inc/field-validation.php:426 +#: inc/field-validation.php:465 #: inc/translatable.php:94 #, php-format msgid "Please enter at least %s characters." @@ -17911,9 +17917,9 @@ msgstr "Odpowiedź AI była pusta. Proszę dopracować swój prompt i spróbowa msgid "Unable to build form fields from the AI response." msgstr "Nie można utworzyć pól formularza z odpowiedzi AI." -#: admin/admin.php:1318 -#: admin/admin.php:1319 -#: admin/admin.php:1861 +#: admin/admin.php:1364 +#: admin/admin.php:1365 +#: admin/admin.php:1915 msgid "Partial Entries" msgstr "Częściowe wpisy" @@ -18784,23 +18790,23 @@ msgstr "Nie można zweryfikować kwoty płatności dla tego formularza. Proszę msgid "Cancellation is not supported for this payment gateway." msgstr "Anulowanie nie jest obsługiwane dla tej bramki płatności." -#: admin/admin.php:3039 +#: admin/admin.php:3203 msgid "SureForms AI Quick Draft" msgstr "SureForms AI Szybki Szkic" -#: admin/admin.php:3058 +#: admin/admin.php:3222 msgid "Describe the form and let SureForms AI generate it for you." msgstr "Opisz formularz, a SureForms AI wygeneruje go dla Ciebie." -#: admin/admin.php:3061 +#: admin/admin.php:3225 msgid "Describe your form" msgstr "Opisz swój formularz" -#: admin/admin.php:3068 +#: admin/admin.php:3232 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "Przykład: Utwórz formularz kontaktowy z polami na imię, email, telefon i wiadomość." -#: admin/admin.php:3109 +#: admin/admin.php:3273 msgid "Redirecting..." msgstr "Przekierowywanie..." @@ -18813,14 +18819,14 @@ msgid "The prompt is too long. Please shorten it and try again." msgstr "Podpowiedź jest zbyt długa. Skróć ją i spróbuj ponownie." #. translators: %s: maximum characters allowed before the @ symbol. -#: inc/field-validation.php:448 +#: inc/field-validation.php:487 #: inc/translatable.php:97 #, php-format msgid "The part before @ may not exceed %s characters." msgstr "Część przed @ nie może przekraczać %s znaków." #. translators: %s: maximum characters allowed after the @ symbol. -#: inc/field-validation.php:454 +#: inc/field-validation.php:493 #: inc/translatable.php:100 #, php-format msgid "The part after @ may not exceed %s characters." @@ -18907,74 +18913,74 @@ msgstr "Przepraszamy, nie masz uprawnień do renderowania tego formularza." msgid "This form requires a payment. Please complete the payment and submit again." msgstr "Ten formularz wymaga płatności. Proszę dokończyć płatność i przesłać ponownie." -#: admin/admin.php:552 +#: admin/admin.php:598 msgid "You are not allowed to update this prompt." msgstr "Nie masz uprawnień do aktualizacji tego polecenia." -#: admin/admin.php:595 +#: admin/admin.php:641 msgid "Finish setting up your form" msgstr "Dokończ konfigurowanie formularza" -#: admin/admin.php:627 +#: admin/admin.php:673 msgid "Review or edit your form" msgstr "Przejrzyj lub edytuj formularz" -#: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:674 +#: admin/admin.php:4515 msgid "Edit form" msgstr "Edytuj formularz" -#: admin/admin.php:633 +#: admin/admin.php:679 msgid "Personalize the Thank You message" msgstr "Spersonalizuj wiadomość podziękowania" -#: admin/admin.php:634 +#: admin/admin.php:680 msgid "Edit message" msgstr "Edytuj wiadomość" -#: admin/admin.php:639 +#: admin/admin.php:685 msgid "Choose who gets notified of new replies" msgstr "Wybierz, kto zostanie powiadomiony o nowych odpowiedziach" -#: admin/admin.php:640 +#: admin/admin.php:686 msgid "Set up email" msgstr "Skonfiguruj e-mail" -#: admin/admin.php:648 +#: admin/admin.php:694 msgid "your form" msgstr "twój formularz" #. translators: %s: form title. -#: admin/admin.php:651 +#: admin/admin.php:697 #, php-format msgid "Finish setting up %s" msgstr "Dokończ konfigurowanie %s" #. translators: %s: form title. -#: admin/admin.php:659 +#: admin/admin.php:705 #, php-format msgid "View %s (opens in a new tab)" msgstr "Zobacz %s (otwiera się w nowej karcie)" -#: admin/admin.php:664 +#: admin/admin.php:710 msgid "Customize your form to get it ready for real submissions:" msgstr "Dostosuj swój formularz, aby był gotowy do rzeczywistych zgłoszeń:" -#: admin/admin.php:3896 +#: admin/admin.php:4504 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "Już stworzyliśmy ten formularz dla Ciebie. Dokończ jego dostosowywanie, aby był gotowy do zbierania rzeczywistych zgłoszeń." #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4509 #, php-format msgid "Finish setting up “%s”" msgstr "Dokończ konfigurowanie „%s”" -#: admin/admin.php:3914 +#: admin/admin.php:4522 msgid "Edit the Thank You message" msgstr "Edytuj wiadomość podziękowania" -#: admin/admin.php:3920 +#: admin/admin.php:4528 msgid "Set where replies go" msgstr "Ustaw, gdzie mają trafiać odpowiedzi" @@ -19002,123 +19008,90 @@ msgstr "Proszę wybrać plik graficzny." msgid "This form could not be submitted. Please refresh the page and try again." msgstr "Nie można było przesłać tego formularza. Proszę odświeżyć stronę i spróbować ponownie." -#: admin/admin.php:2341 -#: admin/admin.php:2430 +#: admin/admin.php:2395 +#: admin/admin.php:2484 msgid "Your SureForms database is up to date. New form entries will be saved as usual." msgstr "Twoja baza danych SureForms jest aktualna. Nowe wpisy formularzy będą zapisywane jak zwykle." -#: admin/admin.php:2355 -#: admin/admin.php:2439 +#: admin/admin.php:2409 +#: admin/admin.php:2493 #: inc/rest-api.php:1447 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support." msgstr "SureForms nie mogło zakończyć aktualizacji bazy danych. Twój hosting może nie pozwalać SureForms na tworzenie tabel w bazie danych — skontaktuj się z dostawcą hostingu lub wsparciem SureForms." -#: admin/admin.php:2358 +#: admin/admin.php:2412 msgid "Contact support" msgstr "Skontaktuj się z pomocą techniczną" -#: admin/admin.php:2378 +#: admin/admin.php:2432 msgid "Database update needed" msgstr "Wymagana aktualizacja bazy danych" -#: admin/admin.php:2384 +#: admin/admin.php:2438 msgid "Fix now" msgstr "Napraw teraz" -#: admin/admin.php:2453 +#: admin/admin.php:2507 msgid "SureForms — database update needed" msgstr "SureForms — wymagana aktualizacja bazy danych" -#: admin/admin.php:2458 +#: admin/admin.php:2512 msgid "Update database" msgstr "Zaktualizuj bazę danych" -#: admin/admin.php:2478 +#: admin/admin.php:2532 msgid "You do not have permission to update the database." msgstr "Nie masz uprawnień do aktualizacji bazy danych." -#: admin/admin.php:3437 +#: admin/admin.php:3669 msgid "You do not have permission to do this." msgstr "Nie masz uprawnień do wykonania tej czynności." -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "%d przesłanie formularza z rzędu nie mogło zostać ukończone." -msgstr[1] "" - -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." -msgstr "Odwiedzający mogą nie być w stanie się z Tobą skontaktować, a te wpisy nie zostały zapisane." - -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." -msgstr "Przesyłanie formularzy przebiega normalnie." - -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." -msgstr "Nie znaleziono wtyczki buforującej, która wymaga konfiguracji." - #. translators: %s: caching plugin name. -#: admin/admin.php:3551 +#: admin/admin.php:4209 msgid "%s may interfere with your forms." msgstr "%s może zakłócać działanie Twoich formularzy." -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." -msgstr "Buforowanie i optymalizacja JavaScript mogą dostarczać nieaktualną kopię formularza lub ładować jego skrypty w niewłaściwej kolejności." - -#: admin/admin.php:3555 +#: admin/admin.php:4185 +#: admin/admin.php:4213 msgid "Help Me Fix" msgstr "Pomóż mi naprawić" -#: admin/admin.php:3650 +#: admin/admin.php:4258 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "SureForms znalazło Twoje wpisy formularzy przechowywane pod innym prefiksem tabeli bazy danych. Ponowne połączenie zajmuje chwilę, a Twoje istniejące wpisy zostaną zachowane." -#: admin/admin.php:3653 +#: admin/admin.php:4261 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "SureForms musi zaktualizować Twoją bazę danych, zanim będzie mogło zapisywać nowe wpisy formularzy. To zajmuje tylko chwilę i nie zmieni Twoich formularzy ani istniejącej zawartości. Wpisy przesłane przed tym momentem nie mogą być odzyskane stąd." #. translators: %s: site host. -#: admin/admin.php:4265 +#: admin/admin.php:5032 msgid "SureForms: form submissions are failing on %s" msgstr "SureForms: przesyłanie formularzy nie powiodło się na %s" -#: admin/admin.php:4274 +#: admin/admin.php:4990 msgid "Debug log: no entries recorded." msgstr "Dziennik debugowania: brak zapisanych wpisów." #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4995 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "Dziennik debugowania (najnowsze %1$d z %2$d wpisów)" -#: admin/admin.php:4288 -msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." -msgstr "Starsze wpisy zostały pominięte, aby ten e-mail mieścił się w długości akceptowanej przez klienta poczty. Pełny dziennik można pobrać z SureForms → Ustawienia → Ogólne." - -#: admin/admin.php:4318 +#: admin/admin.php:5158 msgid "Hello SureForms support," msgstr "Witaj wsparcie SureForms," -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." -msgstr[0] "SureForms zarejestrowało %d przesłanie formularza z rzędu, które nie mogło zostać ukończone." -msgstr[1] "" - -#: admin/admin.php:4332 +#: admin/admin.php:5182 msgid "Site details" msgstr "Szczegóły strony" -#: admin/admin.php:4335 +#: admin/admin.php:5185 msgid "not active" msgstr "nieaktywny" -#: admin/admin.php:4338 +#: admin/admin.php:5188 msgid "none detected" msgstr "nie wykryto" @@ -19126,7 +19099,7 @@ msgstr "nie wykryto" msgid "No form submission failures have been recorded." msgstr "Nie zarejestrowano żadnych niepowodzeń przesyłania formularzy." -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "Nie znaleziono." @@ -19211,6 +19184,193 @@ msgstr "Widoki formularzy i konwersja" msgid "Logs" msgstr "Dzienniki" +#: admin/admin.php:2831 +msgid "Previous notice" +msgstr "Poprzednie powiadomienie" + +#: admin/admin.php:2832 +msgid "Next notice" +msgstr "Następne powiadomienie" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2834 +msgid "%1$d of %2$d" +msgstr "%1$d z %2$d" + +#: admin/admin.php:2891 +msgid "Nothing to report." +msgstr "Brak raportu." + +#: admin/admin.php:4057 +#: assets/build/dashboard.js:172 +msgid "Details" +msgstr "Szczegóły" + +#: admin/admin.php:4058 +msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it." +msgstr "Co zarejestrowaliśmy na temat tego problemu. Skopiuj to do swojego zgłoszenia do wsparcia, abyśmy mogli zacząć od przyczyny, a nie jej opisu." + +#: admin/admin.php:4059 +#: assets/build/dashboard.js:172 +msgid "Copy details" +msgstr "Skopiuj szczegóły" + +#: admin/admin.php:4068 +msgid "Copy the details first, so you have them to paste." +msgstr "Najpierw skopiuj szczegóły, aby móc je wkleić." + +#: admin/admin.php:4072 +msgid "Copied. Contact Support is now available." +msgstr "Skopiowano. Kontakt z pomocą techniczną jest teraz dostępny." + +#: admin/admin.php:4073 +msgid "Your browser would not let us copy. Select the text above and copy it by hand." +msgstr "Twoja przeglądarka nie pozwoliła nam skopiować. Wybierz powyższy tekst i skopiuj go ręcznie." + +#: admin/admin.php:4076 +msgid "Recorded diagnostics" +msgstr "Zarejestrowane diagnostyki" + +#: admin/admin.php:4080 +msgid "Collecting the details…" +msgstr "Zbieranie szczegółów…" + +#: admin/admin.php:4081 +msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there." +msgstr "Nie udało się zebrać szczegółów. Skontaktuj się z pomocą techniczną i opisz, co się stało, a my zajmiemy się resztą." + +#. translators: %s: form title. +#: admin/admin.php:4108 +msgid "We noticed a form submission failure on %s." +msgstr "Zauważyliśmy błąd przesyłania formularza na %s." + +#: admin/admin.php:4109 +msgid "We noticed a form submission failure." +msgstr "Zauważyliśmy błąd przesyłania formularza." + +#: admin/admin.php:4110 +msgid "Visitors may not be able to reach you, and their entries were not saved." +msgstr "Odwiedzający mogą nie być w stanie się z Tobą skontaktować, a ich wpisy nie zostały zapisane." + +#. translators: %s: form title. +#: admin/admin.php:4115 +msgid "We noticed a notification failure on %s." +msgstr "Zauważyliśmy błąd powiadomienia na %s." + +#: admin/admin.php:4116 +msgid "We noticed a notification failure." +msgstr "Zauważyliśmy błąd powiadomienia." + +#: admin/admin.php:4117 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "Wpis został zapisany, ale nie udało się wysłać o nim e-maila. Nowe wpisy mogą przychodzić bez Twojej wiedzy." + +#. translators: %s: form title. +#: admin/admin.php:4132 +msgid "We noticed an integration failure on %s." +msgstr "Zauważyliśmy błąd integracji na %s." + +#: admin/admin.php:4133 +msgid "We noticed an integration failure." +msgstr "Zauważyliśmy błąd integracji." + +#: admin/admin.php:4134 +msgid "The entry was saved, but we could not send it to a connected service." +msgstr "Wpis został zapisany, ale nie udało się go wysłać do połączonej usługi." + +#: admin/admin.php:4158 +msgid "View details" +msgstr "Zobacz szczegóły" + +#: admin/admin.php:4212 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "Buforowanie może pokazywać odwiedzającym starą kopię formularza lub ładować jego skrypty w niewłaściwej kolejności." + +#: admin/admin.php:5005 +msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General." +msgstr "Starsze wpisy zostały pominięte, aby ten fragment był czytelny. Pełny dziennik można pobrać z SureForms → Ustawienia → Ogólne." + +#. translators: %s: site host. +#: admin/admin.php:5034 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "SureForms zarejestrował przesyłanie formularzy na %s, które nie mogły zostać ukończone." + +#. translators: %s: site host. +#: admin/admin.php:5038 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "SureForms: e-maile z powiadomieniami nie są wysyłane na %s" + +#. translators: %s: site host. +#: admin/admin.php:5040 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "SureForms zapisał wpisy na %s, ale nie udało się wysłać dla nich e-maili z powiadomieniami." + +#. translators: %s: site host. +#: admin/admin.php:5044 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "SureForms: integracja nie otrzymuje wpisów na %s" + +#. translators: %s: site host. +#: admin/admin.php:5046 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "SureForms zapisał wpisy na %s, ale nie udało się ich przekazać do połączonej usługi." + +#. translators: %s: site host. +#: admin/admin.php:5056 +msgid "SureForms: a problem with the forms on %s" +msgstr "SureForms: problem z formularzami na %s" + +#. translators: %s: site host. +#: admin/admin.php:5058 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "SureForms zarejestrował problem z formularzami na %s." + +#. translators: %d: number of failed submissions. +#: admin/admin.php:5083 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." +msgstr[0] "SureForms zarejestrował %d przesyłanie formularza, które nie mogło zostać ukończone." +msgstr[1] "" + +#. translators: %d: number of failed notifications. +#: admin/admin.php:5095 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "SureForms zapisał %d wpis, ale nie udało się wysłać dla niego e-maila z powiadomieniem." +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:5107 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "SureForms zapisał %d wpis, ale nie udało się go przekazać do połączonej usługi." +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:5119 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "SureForms zarejestrował %d problem z formularzami na tej stronie." +msgstr[1] "" + +#. translators: %s: form title. +#: admin/admin.php:5169 +msgid "Form: %s" +msgstr "Formularz: %s" + +#: admin/admin.php:5189 +msgid "none recorded" +msgstr "brak zarejestrowanych" + +#. translators: %s: date and time of the previous report, in the site's timezone. +#: admin/admin.php:5212 +msgid "Previously reported: %s" +msgstr "Zgłoszono wcześniej: %s" + +#: assets/build/settings.js:172 +msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures." +msgstr "Widoki i przesyłania przez osoby, które mogą edytować stronę, są zwykle pomijane, więc testowanie własnych formularzy nie zmienia tych danych." + #: inc/post-types.php:265 msgctxt "post type general name" msgid "Forms" diff --git a/languages/sureforms-pt_PT-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-pt_PT-51635fe6489fc8288d603fe596c755ca.json index f75ffad2a..4910478a4 100644 --- a/languages/sureforms-pt_PT-51635fe6489fc8288d603fe596c755ca.json +++ b/languages/sureforms-pt_PT-51635fe6489fc8288d603fe596c755ca.json @@ -1 +1 @@ -{"translation-revision-date":"2025-01-02T08:44:42+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Painel de Controle"],"Settings":["Configura\u00e7\u00f5es"],"Entries":["Entradas"],"Status":["Status"],"Form":["Formul\u00e1rio"],"Activated":["Ativado"],"Activate":["Ativar"],"Address":["Endere\u00e7o"],"Checkbox":["Caixa de sele\u00e7\u00e3o"],"Dropdown":["Menu suspenso"],"Email":["Email"],"Number":["N\u00famero"],"Phone":["Telefone"],"Textarea":["\u00c1rea de texto"],"Monday":["Segunda-feira"],"Forms":["Formul\u00e1rios"],"New Form Submission - %s":["Nova Submiss\u00e3o de Formul\u00e1rio - %s"],"GitHub":["GitHub"],"(no title)":["(sem t\u00edtulo)"],"General":["Geral"],"No tags available":["Sem etiquetas dispon\u00edveis"],"Back":["Voltar"],"Generic tags":["Etiquetas gen\u00e9ricas"],"Other":["Outro"],"Confirm":["Confirmar"],"Cancel":["Cancelar"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["A instala\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"Plugin activation failed, Please try again later.":["A ativa\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"Integrations":["Integra\u00e7\u00f5es"],"What's New?":["O que h\u00e1 de novo?"],"Core":["N\u00facleo"],"Unlicensed":["Sem licen\u00e7a"],"Connecting\u2026":["Conectando\u2026"],"Install & Activate":["Instalar e Ativar"],"Compliance Settings":["Configura\u00e7\u00f5es de Conformidade"],"Enable GDPR Compliance":["Ativar conformidade com o GDPR"],"Never store entry data after form submission":["Nunca armazene os dados de entrada ap\u00f3s o envio do formul\u00e1rio"],"When enabled this form will never store Entries.":["Quando ativado, este formul\u00e1rio nunca armazenar\u00e1 entradas."],"Automatically delete entries":["Excluir entradas automaticamente"],"When enabled this form will automatically delete entries after a certain period of time.":["Quando ativado, este formul\u00e1rio excluir\u00e1 automaticamente as entradas ap\u00f3s um determinado per\u00edodo de tempo."],"Entries older than the days set will be deleted automatically.":["As entradas mais antigas do que os dias definidos ser\u00e3o exclu\u00eddas automaticamente."],"Visual":["Visual"],"HTML":["HTML"],"All Data":["Todos os dados"],"Add Shortcode":["Adicionar Shortcode"],"Form input tags":["Etiquetas de entrada de formul\u00e1rio"],"Comma separated values are also accepted.":["Valores separados por v\u00edrgula tamb\u00e9m s\u00e3o aceitos."],"Email Notification":["Notifica\u00e7\u00e3o por Email"],"Name":["Nome"],"Send Email To":["Enviar Email Para"],"Subject":["Assunto"],"CC":["CC"],"BCC":["Cco"],"Reply To":["Responder a"],"Add Key":["Adicionar Chave"],"Add Value":["Adicionar Valor"],"Add":["Adicionar"],"Confirmation Message":["Mensagem de Confirma\u00e7\u00e3o"],"After Form Submission":["Ap\u00f3s o Envio do Formul\u00e1rio"],"Hide Form":["Ocultar Formul\u00e1rio"],"Reset Form":["Redefinir Formul\u00e1rio"],"Custom URL":["URL personalizada"],"Add Query Parameters":["Adicionar Par\u00e2metros de Consulta"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Selecione se deseja adicionar pares chave-valor para campos de formul\u00e1rio a serem inclu\u00eddos nos par\u00e2metros de consulta"],"Query Parameters":["Par\u00e2metros de Consulta"],"Success Message":["Mensagem de Sucesso"],"Redirect":["Redirecionar"],"Redirect to":["Redirecionar para"],"Page":["P\u00e1gina"],"Form Confirmation":["Confirma\u00e7\u00e3o de Formul\u00e1rio"],"Confirmation Type":["Tipo de Confirma\u00e7\u00e3o"],"Clear":["Limpar"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invis\u00edvel"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Valida\u00e7\u00f5es"],"Spam Protection":["Prote\u00e7\u00e3o contra Spam"],"Email Summaries":["Resumos de Email"],"Tuesday":["ter\u00e7a-feira"],"Wednesday":["Quarta-feira"],"Thursday":["Quinta-feira"],"Friday":["Sexta-feira"],"Saturday":["S\u00e1bado"],"Sunday":["Domingo"],"Test Email":["Email de teste"],"Schedule Reports":["Agendar Relat\u00f3rios"],"IP Logging":["Registro de IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Se esta op\u00e7\u00e3o estiver ativada, o endere\u00e7o IP do usu\u00e1rio ser\u00e1 salvo com os dados do formul\u00e1rio"],"Confirmation Email Mismatch Message":["Mensagem de Incompatibilidade de E-mail de Confirma\u00e7\u00e3o"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s representa o valor m\u00ednimo de entrada. Por exemplo: \"O valor m\u00ednimo \u00e9 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s representa o valor m\u00e1ximo de entrada. Por exemplo: \"O valor m\u00e1ximo \u00e9 100.\""],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s representa as sele\u00e7\u00f5es m\u00ednimas necess\u00e1rias. Por exemplo: \"S\u00e3o necess\u00e1rias no m\u00ednimo 2 sele\u00e7\u00f5es.\""],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s representa o n\u00famero m\u00e1ximo de sele\u00e7\u00f5es permitidas. Por exemplo: \"S\u00e3o permitidas no m\u00e1ximo 4 sele\u00e7\u00f5es.\""],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s representa as escolhas m\u00ednimas necess\u00e1rias. Por exemplo: \u201c\u00c9 necess\u00e1ria no m\u00ednimo 1 sele\u00e7\u00e3o.\u201d"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s representa o n\u00famero m\u00e1ximo de escolhas permitidas. Por exemplo: \"S\u00e3o permitidas no m\u00e1ximo 3 sele\u00e7\u00f5es.\""]," Error Message":["Mensagem de Erro"],"Auto":["Carro"],"Light":["Luz"],"Dark":["Escuro"],"Turnstile":["Catraca"],"Honeypot":["Pote de mel"],"Get Keys":["Obter Chaves"],"Documentation":["Documenta\u00e7\u00e3o"],"Site Key":["Chave do Site"],"Secret Key":["Chave Secreta"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Modo de Apar\u00eancia"],"Enable Honeypot Security":["Ativar Seguran\u00e7a Honeypot"],"Enable Honeypot Security for better spam protection":["Ative a Seguran\u00e7a Honeypot para melhor prote\u00e7\u00e3o contra spam"],"Text":["Texto"],"OttoKit":["OttoKit"],"Connect with OttoKit":["Conectar com OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["O endere\u00e7o de 'Email de Remetente' atual n\u00e3o corresponde ao nome de dom\u00ednio do seu site (%1$s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam. Alternativamente, tente usar um Endere\u00e7o de Remetente que corresponda ao dom\u00ednio do seu site (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["O endere\u00e7o de 'Email de Remetente' atual n\u00e3o corresponde ao nome de dom\u00ednio do seu site (%s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam."],"We strongly recommend that you install the free ":["Recomendamos fortemente que voc\u00ea instale o gratuito"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin! O Assistente de Configura\u00e7\u00e3o facilita a corre\u00e7\u00e3o dos seus e-mails."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativamente, tente usar um endere\u00e7o de remetente que corresponda ao dom\u00ednio do seu site (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Por favor, insira um endere\u00e7o de e-mail v\u00e1lido. Suas notifica\u00e7\u00f5es n\u00e3o ser\u00e3o enviadas se o campo n\u00e3o for preenchido corretamente."],"From Name":["De Nome"],"From Email":["De Email"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["O endere\u00e7o de 'Email de Remetente' atual pode n\u00e3o corresponder ao nome de dom\u00ednio do seu site (%1$s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam. Alternativamente, tente usar um Endere\u00e7o de Remetente que corresponda ao dom\u00ednio do seu site (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["O endere\u00e7o de 'Email de Remetente' atual pode n\u00e3o corresponder ao nome de dom\u00ednio do seu site (%s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Pronto para ir al\u00e9m do plano gratuito?"],"Upgrade now":["Atualize agora"],"and unlock the full power of SureForms!":["e desbloqueie todo o poder do SureForms!"],"Upgrade SureForms":["Atualizar SureForms"],"Upgrade Now":["Atualize agora"],"Entries older than the selected days will be deleted.":["As entradas mais antigas que os dias selecionados ser\u00e3o exclu\u00eddas."],"Entries Time Period":["Per\u00edodo de Tempo das Entradas"],"Notifications can use only one From Email so please enter a single address.":["As notifica\u00e7\u00f5es podem usar apenas um \u00fanico endere\u00e7o de e-mail de remetente, portanto, insira um \u00fanico endere\u00e7o."],"Select Page to redirect":["Selecione a p\u00e1gina para redirecionar"],"Search for a page":["Procurar uma p\u00e1gina"],"Select a page":["Selecione uma p\u00e1gina"],"Form Validation":["Valida\u00e7\u00e3o de Formul\u00e1rio"],"Required Error Messages":["Mensagens de Erro Obrigat\u00f3rias"],"Other Error Messages":["Outras Mensagens de Erro"],"Input Field Unique":["Campo de Entrada \u00danico"],"Email Field Unique":["Campo de Email \u00danico"],"Invalid URL":["URL inv\u00e1lido"],"Phone Field Unique":["Campo de Telefone \u00danico"],"Invalid Field Number Block":["Bloco de N\u00famero de Campo Inv\u00e1lido"],"Invalid Email":["Email inv\u00e1lido"],"Number Minimum Value":["Valor M\u00ednimo do N\u00famero"],"Number Maximum Value":["Valor M\u00e1ximo do N\u00famero"],"Dropdown Minimum Selections":["M\u00ednimo de Sele\u00e7\u00f5es no Dropdown"],"Dropdown Maximum Selections":["Sele\u00e7\u00f5es M\u00e1ximas do Dropdown"],"Multiple Choice Minimum Selections":["M\u00faltipla Escolha M\u00ednima de Sele\u00e7\u00f5es"],"Multiple Choice Maximum Selections":["Sele\u00e7\u00f5es M\u00e1ximas de M\u00faltipla Escolha"],"Input Field":["Campo de Entrada"],"Email Field":["Campo de Email"],"URL Field":["Campo de URL"],"Phone Field":["Campo de Telefone"],"Textarea Field":["Campo de \u00c1rea de Texto"],"Checkbox Field":["Campo de Caixa de Sele\u00e7\u00e3o"],"Dropdown Field":["Campo suspenso"],"Multiple Choice Field":["Campo de Escolha M\u00faltipla"],"Address Field":["Campo de Endere\u00e7o"],"Number Field":["Campo Num\u00e9rico"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Para ativar o recurso reCAPTCHA no seu SureForms, ative a op\u00e7\u00e3o reCAPTCHA nas configura\u00e7\u00f5es dos seus blocos e selecione a vers\u00e3o. Adicione a chave secreta e a chave do site do Google reCAPTCHA aqui. O reCAPTCHA ser\u00e1 adicionado \u00e0 sua p\u00e1gina no front-end."],"Enter your %s here":["Insira seu %s aqui"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Para ativar o hCAPTCHA, adicione sua chave do site e chave secreta. Configure essas configura\u00e7\u00f5es dentro do formul\u00e1rio individual."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Para ativar o Cloudflare Turnstile, adicione sua chave do site e chave secreta. Configure essas configura\u00e7\u00f5es dentro do formul\u00e1rio individual."],"Save":["Salvar"],"Anonymous Analytics":["An\u00e1lise An\u00f4nima"],"Learn More":["Saiba mais"],"Admin Notification":["Notifica\u00e7\u00e3o do Administrador"],"Enable Admin Notification":["Ativar Notifica\u00e7\u00e3o de Administrador"],"Admin notifications keep you informed about new form entries since your last visit.":["As notifica\u00e7\u00f5es de administrador mant\u00eam voc\u00ea informado sobre novas entradas de formul\u00e1rio desde sua \u00faltima visita."],"Skip":["Pular"],"Continue":["Continuar"],"Maximum Number of Entries":["N\u00famero M\u00e1ximo de Entradas"],"Maximum Entries":["Entradas M\u00e1ximas"],"Response Description After Maximum Entries":["Descri\u00e7\u00e3o da Resposta Ap\u00f3s o M\u00e1ximo de Entradas"],"Get Started":["Come\u00e7ar"],"Integration":["Integra\u00e7\u00e3o"],"Connect Native Integrations with SureForms":["Conecte Integra\u00e7\u00f5es Nativas com SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Desbloqueie integra\u00e7\u00f5es poderosas no plano Premium para automatizar seus fluxos de trabalho e conectar o SureForms diretamente com suas ferramentas favoritas."],"Send form submissions straight to CRMs, email, and marketing platforms":["Envie envios de formul\u00e1rios diretamente para CRMs, e-mail e plataformas de marketing"],"Automate repetitive tasks with seamless data syncing":["Automatize tarefas repetitivas com sincroniza\u00e7\u00e3o de dados perfeita"],"Access exclusive native integrations for faster workflows":["Acesse integra\u00e7\u00f5es nativas exclusivas para fluxos de trabalho mais r\u00e1pidos"],"Expected format for emails - email@sureforms.com or John Doe ":["Formato esperado para e-mails - email@sureforms.com ou John Doe "],"Payments":["Pagamentos"],"Stripe account disconnected successfully.":["Conta Stripe desconectada com sucesso."],"Failed to create webhook.":["Falha ao criar webhook."],"Failed to connect to Stripe.":["Falha ao conectar-se ao Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Base de Conhecimento"],"What\u2019s New":["O que h\u00e1 de novo"],"out of":["fora de"],"No entries found":["Nenhuma entrada encontrada"],"delete":["excluir"],"Please type \"%s\" in the input box":["Por favor, digite \"%s\" na caixa de entrada"],"To confirm, type \"%s\" in the box below:":["Para confirmar, digite \"%s\" na caixa abaixo:"],"Type \"%s\"":["Digite \"%s\""],"Go to OttoKit Settings":["V\u00e1 para as Configura\u00e7\u00f5es do OttoKit"],"USD - US Dollar":["USD - D\u00f3lar dos EUA"],"Payment Mode":["Modo de Pagamento"],"Test Mode":["Modo de Teste"],"Live Mode":["Modo ao Vivo"],"Action":["A\u00e7\u00e3o"],"General Settings":["Configura\u00e7\u00f5es Gerais"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Configure resumos de e-mail, alertas de administrador e prefer\u00eancias de dados para gerenciar seus formul\u00e1rios com facilidade."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personalize as mensagens de erro padr\u00e3o exibidas quando os usu\u00e1rios enviam entradas de formul\u00e1rio inv\u00e1lidas ou incompletas."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Ative a prote\u00e7\u00e3o contra spam para seus formul\u00e1rios usando servi\u00e7os CAPTCHA ou seguran\u00e7a honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Conecte e gerencie seus gateways de pagamento para aceitar transa\u00e7\u00f5es com seguran\u00e7a atrav\u00e9s de seus formul\u00e1rios."],"1% transaction and payment gateway fees apply.":["Aplicam-se taxas de 1% para transa\u00e7\u00f5es e gateways de pagamento."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Aplicam-se taxas de transa\u00e7\u00e3o e de gateway de pagamento de 2,9%. Ative a licen\u00e7a para reduzir as taxas de transa\u00e7\u00e3o."],"2.9% transaction and payment gateway fees apply.":["Aplicam-se taxas de transa\u00e7\u00e3o e de gateway de pagamento de 2,9%."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Por favor, visite %1$s, exclua um webhook n\u00e3o utilizado e, em seguida, clique abaixo para tentar novamente."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms n\u00e3o p\u00f4de criar um webhook porque sua conta Stripe ficou sem slots gratuitos. Webhooks s\u00e3o necess\u00e1rios para receber atualiza\u00e7\u00f5es sobre pagamentos."],"Stripe Dashboard":["Painel do Stripe"],"Creating\u2026":["Criando\u2026"],"Create Webhook":["Criar Webhook"],"Successfully connected to Stripe!":["Conectado com sucesso ao Stripe!"],"Invalid response from server. Please try again.":["Resposta inv\u00e1lida do servidor. Por favor, tente novamente."],"Failed to disconnect Stripe account.":["Falha ao desconectar a conta do Stripe."],"Webhook created successfully!":["Webhook criado com sucesso!"],"Select Currency":["Selecionar moeda"],"Select the default currency for payment forms.":["Selecione a moeda padr\u00e3o para os formul\u00e1rios de pagamento."],"Connection Status":["Status da Conex\u00e3o"],"Disconnect Stripe Account":["Desconectar Conta do Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Tem certeza de que deseja desconectar sua conta do Stripe? Isso interromper\u00e1 todos os pagamentos, assinaturas e transa\u00e7\u00f5es de formul\u00e1rio ativas conectadas a esta conta."],"Disconnect":["Desconectar"],"Disconnecting\u2026":["Desconectando\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook conectado com sucesso, todos os eventos do Stripe est\u00e3o sendo rastreados."],"Connect your Stripe account to start accepting payments through your forms.":["Conecte sua conta Stripe para come\u00e7ar a aceitar pagamentos atrav\u00e9s de seus formul\u00e1rios."],"Connect to Stripe":["Conectar ao Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Conecte-se com seguran\u00e7a ao Stripe com apenas alguns cliques para come\u00e7ar a aceitar pagamentos!"],"Set the total number of submissions allowed for this form.":["Defina o n\u00famero total de envios permitidos para este formul\u00e1rio."],"Payment Methods":["M\u00e9todos de Pagamento"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["O modo de teste permite processar pagamentos sem cobran\u00e7as reais. Mude para o modo ao vivo para transa\u00e7\u00f5es reais."],"General Payment Settings":["Configura\u00e7\u00f5es Gerais de Pagamento"],"These settings apply to all payment gateways.":["Essas configura\u00e7\u00f5es se aplicam a todos os gateways de pagamento."],"Stripe Settings":["Configura\u00e7\u00f5es do Stripe"],"Left ($100)":["Esquerda ($100)"],"Right (100$)":["Certo (100$)"],"Left Space ($ 100)":["Espa\u00e7o Esquerdo ($ 100)"],"Right Space (100 $)":["Espa\u00e7o Direito (100 $)"],"Currency Sign Position":["Posi\u00e7\u00e3o do Sinal de Moeda"],"Select the position of the currency symbol relative to the amount.":["Selecione a posi\u00e7\u00e3o do s\u00edmbolo da moeda em rela\u00e7\u00e3o ao valor."],"Learn":["Aprender"],"Enable email summaries":["Ativar resumos de e-mail"],"Enable IP logging":["Ativar registro de IP"],"Turn on Admin Notification from here.":["Ative a Notifica\u00e7\u00e3o de Administrador daqui."],"New":["Novo"],"Send entries to 100+ popular apps.":["Envie entradas para mais de 100 aplicativos populares."],"Build automated workflows that run instantly.":["Crie fluxos de trabalho automatizados que sejam executados instantaneamente."],"Create custom app integrations using our Custom App feature.":["Crie integra\u00e7\u00f5es de aplicativos personalizadas usando nosso recurso de Aplicativo Personalizado."],"Keep your tools in sync automatically.":["Mantenha suas ferramentas sincronizadas automaticamente."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Isso instalar\u00e1 e ativar\u00e1 o OttoKit no seu site WordPress para habilitar recursos de automa\u00e7\u00e3o."],"Automate Your Forms with OttoKit":["Automatize Seus Formul\u00e1rios com OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Cada envio de formul\u00e1rio deve acionar algo \u2014 um alerta no Slack, um lead no CRM, um e-mail de acompanhamento ou uma nova linha no Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configure as permiss\u00f5es do cliente de IA e as configura\u00e7\u00f5es do servidor MCP."],"View documentation":["Ver documenta\u00e7\u00e3o"],"Copy to clipboard":["Copiar para a \u00e1rea de transfer\u00eancia"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) ou %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["C\u00f3digo Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (projeto) ou ~\/.claude.json (global)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (projeto) ou settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml ou config.json"],"Your client's MCP configuration file":["O arquivo de configura\u00e7\u00e3o MCP do seu cliente"],"Connect Your AI Client":["Conecte seu cliente de IA"],"AI Client":["Cliente de IA"],"Create an Application Password \u2014 ":["Criar uma Senha de Aplicativo \u2014"],"Open Application Passwords":["Abrir Senhas de Aplicativos"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Ou use este comando CLI para adicionar o servidor rapidamente (voc\u00ea ainda precisar\u00e1 definir as vari\u00e1veis de ambiente):"],"Copy the JSON config below into: ":["Copie a configura\u00e7\u00e3o JSON abaixo em:"],"Replace \"your-application-password\" with the password from Step 1.":["Substitua \"your-application-password\" pela senha do Passo 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 o endpoint MCP do seu site. WP_API_USERNAME \u2014 seu nome de usu\u00e1rio do WordPress. WP_API_PASSWORD \u2014 a senha do aplicativo que voc\u00ea gerou."],"View setup docs":["Ver documentos de configura\u00e7\u00e3o"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["O plugin MCP Adapter est\u00e1 instalado, mas n\u00e3o est\u00e1 ativo. Ative-o para configurar as defini\u00e7\u00f5es do MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["O plugin MCP Adapter \u00e9 necess\u00e1rio para conectar clientes de IA aos seus formul\u00e1rios. Baixe e instale-o do GitHub, depois ative-o."],"Download the latest release from":["Baixe a vers\u00e3o mais recente de"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Instale o plugin atrav\u00e9s de Plugins > Adicionar Novo Plugin > Enviar Plugin."],"Activate the MCP Adapter plugin.":["Ativar o plugin do Adaptador MCP."],"Activating\u2026":["Ativando\u2026"],"Activate MCP Adapter":["Ativar Adaptador MCP"],"Download MCP Adapter":["Baixar Adaptador MCP"],"Experimental":["Experimental"],"Enable Abilities":["Ativar habilidades"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registre as habilidades do SureForms com a API de Habilidades do WordPress. Quando ativado, os clientes de IA podem listar, ler, criar, editar e excluir seus formul\u00e1rios e entradas. Quando desativado, nenhuma habilidade \u00e9 registrada e os clientes de IA n\u00e3o podem realizar nenhuma a\u00e7\u00e3o em seus formul\u00e1rios."],"Abilities API \u2014 Edit":["API de Habilidades \u2014 Editar"],"Enable Edit Abilities":["Ativar habilidades de edi\u00e7\u00e3o"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Quando ativado, os clientes de IA podem criar novos formul\u00e1rios, atualizar t\u00edtulos, campos e configura\u00e7\u00f5es de formul\u00e1rios, duplicar formul\u00e1rios e modificar status de entradas. Quando desativado, essas habilidades s\u00e3o desregistradas e os clientes de IA s\u00f3 podem ler seus dados."],"Abilities API \u2014 Delete":["API de Habilidades \u2014 Excluir"],"Enable Delete Abilities":["Ativar habilidades de exclus\u00e3o"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Quando ativado, os clientes de IA podem excluir permanentemente formul\u00e1rios e entradas. Os dados exclu\u00eddos n\u00e3o podem ser recuperados. Quando desativado, as habilidades de exclus\u00e3o s\u00e3o desregistradas e os clientes de IA n\u00e3o podem remover nenhum dado."],"MCP Server":["Servidor MCP"],"Enable MCP Server":["Ativar Servidor MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Cria um endpoint SureForms MCP dedicado ao qual clientes de IA como Claude podem se conectar. Quando desativado, o endpoint \u00e9 removido e clientes de IA externos n\u00e3o podem descobrir ou chamar nenhuma habilidade do SureForms."],"Learn more":["Saiba mais"],"MCP Adapter Required":["Adaptador MCP Necess\u00e1rio"],"Heading 1":["T\u00edtulo 1"],"Heading 2":["T\u00edtulo 2"],"Heading 3":["T\u00edtulo 3"],"Heading 4":["T\u00edtulo 4"],"Heading 5":["T\u00edtulo 5"],"Heading 6":["T\u00edtulo 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configure a chave da API do Google Maps para preenchimento autom\u00e1tico de endere\u00e7o e visualiza\u00e7\u00e3o do mapa."],"Help shape the future of SureForms":["Ajude a moldar o futuro do SureForms"],"Enable Google Address Autocomplete":["Ativar o preenchimento autom\u00e1tico de endere\u00e7os do Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Fa\u00e7a upgrade para o Plano de Neg\u00f3cios SureForms para adicionar a autocompleta\u00e7\u00e3o de endere\u00e7os com tecnologia do Google e visualiza\u00e7\u00e3o interativa de mapa aos seus formul\u00e1rios."],"Auto-suggest addresses as users type for faster, error-free submissions":["Sugira automaticamente endere\u00e7os \u00e0 medida que os usu\u00e1rios digitam para envios mais r\u00e1pidos e sem erros"],"Show an interactive map preview with draggable pin for precise locations":["Mostrar uma pr\u00e9-visualiza\u00e7\u00e3o de mapa interativo com um pino arrast\u00e1vel para locais precisos"],"Automatically populate address fields like city, state, and postal code":["Preencher automaticamente campos de endere\u00e7o como cidade, estado e c\u00f3digo postal"],"This form is now closed as we've received all the entries.":["Este formul\u00e1rio est\u00e1 agora fechado, pois recebemos todas as inscri\u00e7\u00f5es."],"Thank you for contacting us! We will be in touch with you shortly.":["Obrigado por nos contatar! Entraremos em contato com voc\u00ea em breve."],"Saving\u2026":["Salvando\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Quando ativado, este formul\u00e1rio n\u00e3o armazenar\u00e1 o IP do usu\u00e1rio, o nome do navegador e o nome do dispositivo nas entradas."],"Form data":["Dados do formul\u00e1rio"],"Unsaved changes":["Altera\u00e7\u00f5es n\u00e3o salvas"],"Keep editing":["Continue editando"],"Global Defaults":["Configura\u00e7\u00f5es Globais"],"Form Restrictions":["Restri\u00e7\u00f5es de Formul\u00e1rio"],"Configure default settings that apply to newly created forms.":["Configurar as configura\u00e7\u00f5es padr\u00e3o que se aplicam a formul\u00e1rios rec\u00e9m-criados."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Recolha informa\u00e7\u00f5es n\u00e3o sens\u00edveis do seu site, como a vers\u00e3o do PHP e as funcionalidades utilizadas, para nos ajudar a corrigir erros mais rapidamente, tomar decis\u00f5es mais inteligentes e desenvolver funcionalidades que realmente importam para voc\u00ea."],"Failed to load pages. Please refresh and try again.":["Falha ao carregar as p\u00e1ginas. Por favor, atualize e tente novamente."],"Failed to load settings. Please refresh and try again.":["Falha ao carregar as configura\u00e7\u00f5es. Por favor, atualize e tente novamente."],"Form Validation fields cannot be left blank.":["Os campos de valida\u00e7\u00e3o de formul\u00e1rio n\u00e3o podem ficar em branco."],"Recipient email is required when email summaries are enabled.":["O e-mail do destinat\u00e1rio \u00e9 necess\u00e1rio quando os resumos por e-mail est\u00e3o ativados."],"Please enter a valid recipient email.":["Por favor, insira um email de destinat\u00e1rio v\u00e1lido."],"Settings saved.":["Configura\u00e7\u00f5es salvas."],"Failed to save settings.":["Falha ao salvar as configura\u00e7\u00f5es."],"Some settings failed to save. Please retry.":["Algumas configura\u00e7\u00f5es n\u00e3o foram salvas. Por favor, tente novamente."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Alguns campos t\u00eam altera\u00e7\u00f5es n\u00e3o salvas. Descarte-as para continuar ou permane\u00e7a para salvar suas edi\u00e7\u00f5es."],"Discard & switch":["Descartar e mudar"],"Import":["Importar"],"Migration":["Migra\u00e7\u00e3o"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importe formul\u00e1rios do Contact Form 7, WPForms, Gravity Forms e outros plugins para o SureForms."],"Could not generate the import preview.":["N\u00e3o foi poss\u00edvel gerar a pr\u00e9-visualiza\u00e7\u00e3o da importa\u00e7\u00e3o."],"Import failed. Please try again or check your error logs.":["Falha na importa\u00e7\u00e3o. Por favor, tente novamente ou verifique seus logs de erro."],"Go back":["Voltar"],"Update & import":["Atualizar e importar"],"Confirm & import":["Confirmar e importar"],"Form #%s":["Formul\u00e1rio n\u00ba %s"],"%d field will import":["%d campo ser\u00e1 importado"],"Some fields can't be migrated yet":["Alguns campos ainda n\u00e3o podem ser migrados"],"These fields will be skipped - add them manually after the form is created:":["Esses campos ser\u00e3o ignorados - adicione-os manualmente ap\u00f3s a cria\u00e7\u00e3o do formul\u00e1rio:"],"Some forms could not be parsed":["Alguns formul\u00e1rios n\u00e3o puderam ser analisados"],"Update existing":["Atualizar existente"],"Create a new copy":["Criar uma nova c\u00f3pia"],"Could not load forms for this source.":["N\u00e3o foi poss\u00edvel carregar formul\u00e1rios para esta fonte."],"(untitled form)":["(formul\u00e1rio sem t\u00edtulo)"],"Previously imported":["Importado anteriormente"],"Open existing SureForms form in a new tab":["Abrir formul\u00e1rio SureForms existente em uma nova aba"],"On re-import":["Na reimporta\u00e7\u00e3o"],"No forms found in this plugin.":["Nenhum formul\u00e1rio encontrado neste plugin."],"%1$d of %2$d form selected":["%1$d de %2$d formul\u00e1rio selecionado"],"Preview update":["Pr\u00e9-visualiza\u00e7\u00e3o da atualiza\u00e7\u00e3o"],"Preview import":["Pr\u00e9-visualizar importa\u00e7\u00e3o"],"Migration complete":["Migra\u00e7\u00e3o conclu\u00edda"],"Nothing was imported":["Nada foi importado"],"%d form was imported into SureForms.":["%d formul\u00e1rio foi importado para o SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Nenhum dos formul\u00e1rios selecionados p\u00f4de ser migrado. Veja os avisos abaixo para mais detalhes."],"Imported forms":["Formul\u00e1rios importados"],"Edit in SureForms":["Editar no SureForms"],"Some fields were skipped during import":["Alguns campos foram ignorados durante a importa\u00e7\u00e3o"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Adicione-os manualmente no editor de formul\u00e1rios - ainda n\u00e3o t\u00eam equivalente SureForms:"],"Some forms failed to import":["Alguns formul\u00e1rios falharam ao importar"],"Import more forms":["Importar mais formul\u00e1rios"],"Could not load the list of importable plugins.":["N\u00e3o foi poss\u00edvel carregar a lista de plugins import\u00e1veis."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Nenhum plugin de formul\u00e1rio suportado est\u00e1 ativo neste site. Ative um (como o Contact Form 7) com pelo menos um formul\u00e1rio para import\u00e1-lo para o SureForms."],"Plugin":["Plugin"],"%d form":["%d formul\u00e1rio"],"No forms":["Sem formul\u00e1rios"],"Multiple choice":["M\u00faltipla escolha"],"Consent":["Consentimento"],"The image uploaded but could not be inserted. Please try again.":["A imagem foi carregada, mas n\u00e3o p\u00f4de ser inserida. Por favor, tente novamente."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["A imagem n\u00e3o p\u00f4de ser carregada. Pode ser muito grande ou voc\u00ea pode n\u00e3o ter permiss\u00e3o para carregar arquivos."],"Please choose an image file.":["Por favor, escolha um arquivo de imagem."],"Show views and conversion rate":["Mostrar visualiza\u00e7\u00f5es e taxa de convers\u00e3o"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Adiciona as colunas de Visualiza\u00e7\u00f5es e Taxa de Convers\u00e3o \u00e0 lista de Formul\u00e1rios. Uma visualiza\u00e7\u00e3o \u00e9 contada uma vez por visita \u00e0 p\u00e1gina quando o formul\u00e1rio aparece na tela, e a taxa de convers\u00e3o \u00e9 a parcela dessas visualiza\u00e7\u00f5es que terminou em um envio. A contagem come\u00e7a na primeira vez que voc\u00ea ativa isso, ent\u00e3o os envios recebidos antes disso n\u00e3o s\u00e3o contados na taxa. Desativ\u00e1-lo posteriormente apenas oculta as colunas \u2014 a contagem continua, ent\u00e3o os n\u00fameros est\u00e3o atualizados se voc\u00ea ativ\u00e1-lo novamente."],"Log cleared.":["Log limpo."],"Could not clear the log.":["N\u00e3o foi poss\u00edvel limpar o log."],"Enable logs":["Ativar logs"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registra falhas de envio de formul\u00e1rios relatadas pelo navegador, para que voc\u00ea possa enviar o log para o suporte em vez de ler o console. Nada \u00e9 registrado enquanto seus formul\u00e1rios est\u00e3o funcionando, e os valores enviados nunca s\u00e3o armazenados."],"Download log":["Baixar log"],"Confirm delete":["Confirmar exclus\u00e3o"],"Log is full \u2014 download and clear it to keep recording.":["O log est\u00e1 cheio \u2014 baixe e limpe-o para continuar registrando."],"Form Views & Conversion":["Visualiza\u00e7\u00f5es e Convers\u00e3o de Formul\u00e1rios"],"Logs":["Logs"],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}} \ No newline at end of file +{"translation-revision-date":"2025-01-02T08:44:42+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Painel de Controle"],"Settings":["Configura\u00e7\u00f5es"],"Entries":["Entradas"],"Status":["Status"],"Form":["Formul\u00e1rio"],"Activated":["Ativado"],"Activate":["Ativar"],"Address":["Endere\u00e7o"],"Checkbox":["Caixa de sele\u00e7\u00e3o"],"Dropdown":["Menu suspenso"],"Email":["Email"],"Number":["N\u00famero"],"Phone":["Telefone"],"Textarea":["\u00c1rea de texto"],"Monday":["Segunda-feira"],"Forms":["Formul\u00e1rios"],"New Form Submission - %s":["Nova Submiss\u00e3o de Formul\u00e1rio - %s"],"GitHub":["GitHub"],"(no title)":["(sem t\u00edtulo)"],"General":["Geral"],"No tags available":["Sem etiquetas dispon\u00edveis"],"Back":["Voltar"],"Generic tags":["Etiquetas gen\u00e9ricas"],"Other":["Outro"],"Confirm":["Confirmar"],"Cancel":["Cancelar"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["A instala\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"Plugin activation failed, Please try again later.":["A ativa\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"Integrations":["Integra\u00e7\u00f5es"],"What's New?":["O que h\u00e1 de novo?"],"Core":["N\u00facleo"],"Unlicensed":["Sem licen\u00e7a"],"Connecting\u2026":["Conectando\u2026"],"Install & Activate":["Instalar e Ativar"],"Compliance Settings":["Configura\u00e7\u00f5es de Conformidade"],"Enable GDPR Compliance":["Ativar conformidade com o GDPR"],"Never store entry data after form submission":["Nunca armazene os dados de entrada ap\u00f3s o envio do formul\u00e1rio"],"When enabled this form will never store Entries.":["Quando ativado, este formul\u00e1rio nunca armazenar\u00e1 entradas."],"Automatically delete entries":["Excluir entradas automaticamente"],"When enabled this form will automatically delete entries after a certain period of time.":["Quando ativado, este formul\u00e1rio excluir\u00e1 automaticamente as entradas ap\u00f3s um determinado per\u00edodo de tempo."],"Entries older than the days set will be deleted automatically.":["As entradas mais antigas do que os dias definidos ser\u00e3o exclu\u00eddas automaticamente."],"Visual":["Visual"],"HTML":["HTML"],"All Data":["Todos os dados"],"Add Shortcode":["Adicionar Shortcode"],"Form input tags":["Etiquetas de entrada de formul\u00e1rio"],"Comma separated values are also accepted.":["Valores separados por v\u00edrgula tamb\u00e9m s\u00e3o aceitos."],"Email Notification":["Notifica\u00e7\u00e3o por Email"],"Name":["Nome"],"Send Email To":["Enviar Email Para"],"Subject":["Assunto"],"CC":["CC"],"BCC":["Cco"],"Reply To":["Responder a"],"Add Key":["Adicionar Chave"],"Add Value":["Adicionar Valor"],"Add":["Adicionar"],"Confirmation Message":["Mensagem de Confirma\u00e7\u00e3o"],"After Form Submission":["Ap\u00f3s o Envio do Formul\u00e1rio"],"Hide Form":["Ocultar Formul\u00e1rio"],"Reset Form":["Redefinir Formul\u00e1rio"],"Custom URL":["URL personalizada"],"Add Query Parameters":["Adicionar Par\u00e2metros de Consulta"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Selecione se deseja adicionar pares chave-valor para campos de formul\u00e1rio a serem inclu\u00eddos nos par\u00e2metros de consulta"],"Query Parameters":["Par\u00e2metros de Consulta"],"Success Message":["Mensagem de Sucesso"],"Redirect":["Redirecionar"],"Redirect to":["Redirecionar para"],"Page":["P\u00e1gina"],"Form Confirmation":["Confirma\u00e7\u00e3o de Formul\u00e1rio"],"Confirmation Type":["Tipo de Confirma\u00e7\u00e3o"],"Clear":["Limpar"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invis\u00edvel"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Valida\u00e7\u00f5es"],"Spam Protection":["Prote\u00e7\u00e3o contra Spam"],"Email Summaries":["Resumos de Email"],"Tuesday":["ter\u00e7a-feira"],"Wednesday":["Quarta-feira"],"Thursday":["Quinta-feira"],"Friday":["Sexta-feira"],"Saturday":["S\u00e1bado"],"Sunday":["Domingo"],"Test Email":["Email de teste"],"Schedule Reports":["Agendar Relat\u00f3rios"],"IP Logging":["Registro de IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Se esta op\u00e7\u00e3o estiver ativada, o endere\u00e7o IP do usu\u00e1rio ser\u00e1 salvo com os dados do formul\u00e1rio"],"Confirmation Email Mismatch Message":["Mensagem de Incompatibilidade de E-mail de Confirma\u00e7\u00e3o"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s representa o valor m\u00ednimo de entrada. Por exemplo: \"O valor m\u00ednimo \u00e9 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s representa o valor m\u00e1ximo de entrada. Por exemplo: \"O valor m\u00e1ximo \u00e9 100.\""],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s representa as sele\u00e7\u00f5es m\u00ednimas necess\u00e1rias. Por exemplo: \"S\u00e3o necess\u00e1rias no m\u00ednimo 2 sele\u00e7\u00f5es.\""],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s representa o n\u00famero m\u00e1ximo de sele\u00e7\u00f5es permitidas. Por exemplo: \"S\u00e3o permitidas no m\u00e1ximo 4 sele\u00e7\u00f5es.\""],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s representa as escolhas m\u00ednimas necess\u00e1rias. Por exemplo: \u201c\u00c9 necess\u00e1ria no m\u00ednimo 1 sele\u00e7\u00e3o.\u201d"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s representa o n\u00famero m\u00e1ximo de escolhas permitidas. Por exemplo: \"S\u00e3o permitidas no m\u00e1ximo 3 sele\u00e7\u00f5es.\""]," Error Message":["Mensagem de Erro"],"Auto":["Carro"],"Light":["Luz"],"Dark":["Escuro"],"Turnstile":["Catraca"],"Honeypot":["Pote de mel"],"Get Keys":["Obter Chaves"],"Documentation":["Documenta\u00e7\u00e3o"],"Site Key":["Chave do Site"],"Secret Key":["Chave Secreta"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Modo de Apar\u00eancia"],"Enable Honeypot Security":["Ativar Seguran\u00e7a Honeypot"],"Enable Honeypot Security for better spam protection":["Ative a Seguran\u00e7a Honeypot para melhor prote\u00e7\u00e3o contra spam"],"Text":["Texto"],"OttoKit":["OttoKit"],"Connect with OttoKit":["Conectar com OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["O endere\u00e7o de 'Email de Remetente' atual n\u00e3o corresponde ao nome de dom\u00ednio do seu site (%1$s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam. Alternativamente, tente usar um Endere\u00e7o de Remetente que corresponda ao dom\u00ednio do seu site (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["O endere\u00e7o de 'Email de Remetente' atual n\u00e3o corresponde ao nome de dom\u00ednio do seu site (%s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam."],"We strongly recommend that you install the free ":["Recomendamos fortemente que voc\u00ea instale o gratuito"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin! O Assistente de Configura\u00e7\u00e3o facilita a corre\u00e7\u00e3o dos seus e-mails."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativamente, tente usar um endere\u00e7o de remetente que corresponda ao dom\u00ednio do seu site (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Por favor, insira um endere\u00e7o de e-mail v\u00e1lido. Suas notifica\u00e7\u00f5es n\u00e3o ser\u00e3o enviadas se o campo n\u00e3o for preenchido corretamente."],"From Name":["De Nome"],"From Email":["De Email"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["O endere\u00e7o de 'Email de Remetente' atual pode n\u00e3o corresponder ao nome de dom\u00ednio do seu site (%1$s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam. Alternativamente, tente usar um Endere\u00e7o de Remetente que corresponda ao dom\u00ednio do seu site (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["O endere\u00e7o de 'Email de Remetente' atual pode n\u00e3o corresponder ao nome de dom\u00ednio do seu site (%s). Isso pode fazer com que seus e-mails de notifica\u00e7\u00e3o sejam bloqueados ou marcados como spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Pronto para ir al\u00e9m do plano gratuito?"],"Upgrade now":["Atualize agora"],"and unlock the full power of SureForms!":["e desbloqueie todo o poder do SureForms!"],"Upgrade SureForms":["Atualizar SureForms"],"Upgrade Now":["Atualize agora"],"Entries older than the selected days will be deleted.":["As entradas mais antigas que os dias selecionados ser\u00e3o exclu\u00eddas."],"Entries Time Period":["Per\u00edodo de Tempo das Entradas"],"Notifications can use only one From Email so please enter a single address.":["As notifica\u00e7\u00f5es podem usar apenas um \u00fanico endere\u00e7o de e-mail de remetente, portanto, insira um \u00fanico endere\u00e7o."],"Select Page to redirect":["Selecione a p\u00e1gina para redirecionar"],"Search for a page":["Procurar uma p\u00e1gina"],"Select a page":["Selecione uma p\u00e1gina"],"Form Validation":["Valida\u00e7\u00e3o de Formul\u00e1rio"],"Required Error Messages":["Mensagens de Erro Obrigat\u00f3rias"],"Other Error Messages":["Outras Mensagens de Erro"],"Input Field Unique":["Campo de Entrada \u00danico"],"Email Field Unique":["Campo de Email \u00danico"],"Invalid URL":["URL inv\u00e1lido"],"Phone Field Unique":["Campo de Telefone \u00danico"],"Invalid Field Number Block":["Bloco de N\u00famero de Campo Inv\u00e1lido"],"Invalid Email":["Email inv\u00e1lido"],"Number Minimum Value":["Valor M\u00ednimo do N\u00famero"],"Number Maximum Value":["Valor M\u00e1ximo do N\u00famero"],"Dropdown Minimum Selections":["M\u00ednimo de Sele\u00e7\u00f5es no Dropdown"],"Dropdown Maximum Selections":["Sele\u00e7\u00f5es M\u00e1ximas do Dropdown"],"Multiple Choice Minimum Selections":["M\u00faltipla Escolha M\u00ednima de Sele\u00e7\u00f5es"],"Multiple Choice Maximum Selections":["Sele\u00e7\u00f5es M\u00e1ximas de M\u00faltipla Escolha"],"Input Field":["Campo de Entrada"],"Email Field":["Campo de Email"],"URL Field":["Campo de URL"],"Phone Field":["Campo de Telefone"],"Textarea Field":["Campo de \u00c1rea de Texto"],"Checkbox Field":["Campo de Caixa de Sele\u00e7\u00e3o"],"Dropdown Field":["Campo suspenso"],"Multiple Choice Field":["Campo de Escolha M\u00faltipla"],"Address Field":["Campo de Endere\u00e7o"],"Number Field":["Campo Num\u00e9rico"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Para ativar o recurso reCAPTCHA no seu SureForms, ative a op\u00e7\u00e3o reCAPTCHA nas configura\u00e7\u00f5es dos seus blocos e selecione a vers\u00e3o. Adicione a chave secreta e a chave do site do Google reCAPTCHA aqui. O reCAPTCHA ser\u00e1 adicionado \u00e0 sua p\u00e1gina no front-end."],"Enter your %s here":["Insira seu %s aqui"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Para ativar o hCAPTCHA, adicione sua chave do site e chave secreta. Configure essas configura\u00e7\u00f5es dentro do formul\u00e1rio individual."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Para ativar o Cloudflare Turnstile, adicione sua chave do site e chave secreta. Configure essas configura\u00e7\u00f5es dentro do formul\u00e1rio individual."],"Save":["Salvar"],"Anonymous Analytics":["An\u00e1lise An\u00f4nima"],"Learn More":["Saiba mais"],"Admin Notification":["Notifica\u00e7\u00e3o do Administrador"],"Enable Admin Notification":["Ativar Notifica\u00e7\u00e3o de Administrador"],"Admin notifications keep you informed about new form entries since your last visit.":["As notifica\u00e7\u00f5es de administrador mant\u00eam voc\u00ea informado sobre novas entradas de formul\u00e1rio desde sua \u00faltima visita."],"Skip":["Pular"],"Continue":["Continuar"],"Maximum Number of Entries":["N\u00famero M\u00e1ximo de Entradas"],"Maximum Entries":["Entradas M\u00e1ximas"],"Response Description After Maximum Entries":["Descri\u00e7\u00e3o da Resposta Ap\u00f3s o M\u00e1ximo de Entradas"],"Get Started":["Come\u00e7ar"],"Integration":["Integra\u00e7\u00e3o"],"Connect Native Integrations with SureForms":["Conecte Integra\u00e7\u00f5es Nativas com SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Desbloqueie integra\u00e7\u00f5es poderosas no plano Premium para automatizar seus fluxos de trabalho e conectar o SureForms diretamente com suas ferramentas favoritas."],"Send form submissions straight to CRMs, email, and marketing platforms":["Envie envios de formul\u00e1rios diretamente para CRMs, e-mail e plataformas de marketing"],"Automate repetitive tasks with seamless data syncing":["Automatize tarefas repetitivas com sincroniza\u00e7\u00e3o de dados perfeita"],"Access exclusive native integrations for faster workflows":["Acesse integra\u00e7\u00f5es nativas exclusivas para fluxos de trabalho mais r\u00e1pidos"],"Expected format for emails - email@sureforms.com or John Doe ":["Formato esperado para e-mails - email@sureforms.com ou John Doe "],"Payments":["Pagamentos"],"Stripe account disconnected successfully.":["Conta Stripe desconectada com sucesso."],"Failed to create webhook.":["Falha ao criar webhook."],"Failed to connect to Stripe.":["Falha ao conectar-se ao Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Base de Conhecimento"],"What\u2019s New":["O que h\u00e1 de novo"],"out of":["fora de"],"No entries found":["Nenhuma entrada encontrada"],"delete":["excluir"],"Please type \"%s\" in the input box":["Por favor, digite \"%s\" na caixa de entrada"],"To confirm, type \"%s\" in the box below:":["Para confirmar, digite \"%s\" na caixa abaixo:"],"Type \"%s\"":["Digite \"%s\""],"Go to OttoKit Settings":["V\u00e1 para as Configura\u00e7\u00f5es do OttoKit"],"USD - US Dollar":["USD - D\u00f3lar dos EUA"],"Payment Mode":["Modo de Pagamento"],"Test Mode":["Modo de Teste"],"Live Mode":["Modo ao Vivo"],"Action":["A\u00e7\u00e3o"],"General Settings":["Configura\u00e7\u00f5es Gerais"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Configure resumos de e-mail, alertas de administrador e prefer\u00eancias de dados para gerenciar seus formul\u00e1rios com facilidade."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personalize as mensagens de erro padr\u00e3o exibidas quando os usu\u00e1rios enviam entradas de formul\u00e1rio inv\u00e1lidas ou incompletas."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Ative a prote\u00e7\u00e3o contra spam para seus formul\u00e1rios usando servi\u00e7os CAPTCHA ou seguran\u00e7a honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Conecte e gerencie seus gateways de pagamento para aceitar transa\u00e7\u00f5es com seguran\u00e7a atrav\u00e9s de seus formul\u00e1rios."],"1% transaction and payment gateway fees apply.":["Aplicam-se taxas de 1% para transa\u00e7\u00f5es e gateways de pagamento."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Aplicam-se taxas de transa\u00e7\u00e3o e de gateway de pagamento de 2,9%. Ative a licen\u00e7a para reduzir as taxas de transa\u00e7\u00e3o."],"2.9% transaction and payment gateway fees apply.":["Aplicam-se taxas de transa\u00e7\u00e3o e de gateway de pagamento de 2,9%."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Por favor, visite %1$s, exclua um webhook n\u00e3o utilizado e, em seguida, clique abaixo para tentar novamente."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms n\u00e3o p\u00f4de criar um webhook porque sua conta Stripe ficou sem slots gratuitos. Webhooks s\u00e3o necess\u00e1rios para receber atualiza\u00e7\u00f5es sobre pagamentos."],"Stripe Dashboard":["Painel do Stripe"],"Creating\u2026":["Criando\u2026"],"Create Webhook":["Criar Webhook"],"Successfully connected to Stripe!":["Conectado com sucesso ao Stripe!"],"Invalid response from server. Please try again.":["Resposta inv\u00e1lida do servidor. Por favor, tente novamente."],"Failed to disconnect Stripe account.":["Falha ao desconectar a conta do Stripe."],"Webhook created successfully!":["Webhook criado com sucesso!"],"Select Currency":["Selecionar moeda"],"Select the default currency for payment forms.":["Selecione a moeda padr\u00e3o para os formul\u00e1rios de pagamento."],"Connection Status":["Status da Conex\u00e3o"],"Disconnect Stripe Account":["Desconectar Conta do Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Tem certeza de que deseja desconectar sua conta do Stripe? Isso interromper\u00e1 todos os pagamentos, assinaturas e transa\u00e7\u00f5es de formul\u00e1rio ativas conectadas a esta conta."],"Disconnect":["Desconectar"],"Disconnecting\u2026":["Desconectando\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook conectado com sucesso, todos os eventos do Stripe est\u00e3o sendo rastreados."],"Connect your Stripe account to start accepting payments through your forms.":["Conecte sua conta Stripe para come\u00e7ar a aceitar pagamentos atrav\u00e9s de seus formul\u00e1rios."],"Connect to Stripe":["Conectar ao Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Conecte-se com seguran\u00e7a ao Stripe com apenas alguns cliques para come\u00e7ar a aceitar pagamentos!"],"Set the total number of submissions allowed for this form.":["Defina o n\u00famero total de envios permitidos para este formul\u00e1rio."],"Payment Methods":["M\u00e9todos de Pagamento"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["O modo de teste permite processar pagamentos sem cobran\u00e7as reais. Mude para o modo ao vivo para transa\u00e7\u00f5es reais."],"General Payment Settings":["Configura\u00e7\u00f5es Gerais de Pagamento"],"These settings apply to all payment gateways.":["Essas configura\u00e7\u00f5es se aplicam a todos os gateways de pagamento."],"Stripe Settings":["Configura\u00e7\u00f5es do Stripe"],"Left ($100)":["Esquerda ($100)"],"Right (100$)":["Certo (100$)"],"Left Space ($ 100)":["Espa\u00e7o Esquerdo ($ 100)"],"Right Space (100 $)":["Espa\u00e7o Direito (100 $)"],"Currency Sign Position":["Posi\u00e7\u00e3o do Sinal de Moeda"],"Select the position of the currency symbol relative to the amount.":["Selecione a posi\u00e7\u00e3o do s\u00edmbolo da moeda em rela\u00e7\u00e3o ao valor."],"Learn":["Aprender"],"Enable email summaries":["Ativar resumos de e-mail"],"Enable IP logging":["Ativar registro de IP"],"Turn on Admin Notification from here.":["Ative a Notifica\u00e7\u00e3o de Administrador daqui."],"New":["Novo"],"Send entries to 100+ popular apps.":["Envie entradas para mais de 100 aplicativos populares."],"Build automated workflows that run instantly.":["Crie fluxos de trabalho automatizados que sejam executados instantaneamente."],"Create custom app integrations using our Custom App feature.":["Crie integra\u00e7\u00f5es de aplicativos personalizadas usando nosso recurso de Aplicativo Personalizado."],"Keep your tools in sync automatically.":["Mantenha suas ferramentas sincronizadas automaticamente."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Isso instalar\u00e1 e ativar\u00e1 o OttoKit no seu site WordPress para habilitar recursos de automa\u00e7\u00e3o."],"Automate Your Forms with OttoKit":["Automatize Seus Formul\u00e1rios com OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Cada envio de formul\u00e1rio deve acionar algo \u2014 um alerta no Slack, um lead no CRM, um e-mail de acompanhamento ou uma nova linha no Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configure as permiss\u00f5es do cliente de IA e as configura\u00e7\u00f5es do servidor MCP."],"View documentation":["Ver documenta\u00e7\u00e3o"],"Copy to clipboard":["Copiar para a \u00e1rea de transfer\u00eancia"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) ou %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["C\u00f3digo Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (projeto) ou ~\/.claude.json (global)"],"Cursor":["Cursor"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (projeto) ou settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml ou config.json"],"Your client's MCP configuration file":["O arquivo de configura\u00e7\u00e3o MCP do seu cliente"],"Connect Your AI Client":["Conecte seu cliente de IA"],"AI Client":["Cliente de IA"],"Create an Application Password \u2014 ":["Criar uma Senha de Aplicativo \u2014"],"Open Application Passwords":["Abrir Senhas de Aplicativos"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Ou use este comando CLI para adicionar o servidor rapidamente (voc\u00ea ainda precisar\u00e1 definir as vari\u00e1veis de ambiente):"],"Copy the JSON config below into: ":["Copie a configura\u00e7\u00e3o JSON abaixo em:"],"Replace \"your-application-password\" with the password from Step 1.":["Substitua \"your-application-password\" pela senha do Passo 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 o endpoint MCP do seu site. WP_API_USERNAME \u2014 seu nome de usu\u00e1rio do WordPress. WP_API_PASSWORD \u2014 a senha do aplicativo que voc\u00ea gerou."],"View setup docs":["Ver documentos de configura\u00e7\u00e3o"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["O plugin MCP Adapter est\u00e1 instalado, mas n\u00e3o est\u00e1 ativo. Ative-o para configurar as defini\u00e7\u00f5es do MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["O plugin MCP Adapter \u00e9 necess\u00e1rio para conectar clientes de IA aos seus formul\u00e1rios. Baixe e instale-o do GitHub, depois ative-o."],"Download the latest release from":["Baixe a vers\u00e3o mais recente de"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Instale o plugin atrav\u00e9s de Plugins > Adicionar Novo Plugin > Enviar Plugin."],"Activate the MCP Adapter plugin.":["Ativar o plugin do Adaptador MCP."],"Activating\u2026":["Ativando\u2026"],"Activate MCP Adapter":["Ativar Adaptador MCP"],"Download MCP Adapter":["Baixar Adaptador MCP"],"Experimental":["Experimental"],"Enable Abilities":["Ativar habilidades"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registre as habilidades do SureForms com a API de Habilidades do WordPress. Quando ativado, os clientes de IA podem listar, ler, criar, editar e excluir seus formul\u00e1rios e entradas. Quando desativado, nenhuma habilidade \u00e9 registrada e os clientes de IA n\u00e3o podem realizar nenhuma a\u00e7\u00e3o em seus formul\u00e1rios."],"Abilities API \u2014 Edit":["API de Habilidades \u2014 Editar"],"Enable Edit Abilities":["Ativar habilidades de edi\u00e7\u00e3o"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Quando ativado, os clientes de IA podem criar novos formul\u00e1rios, atualizar t\u00edtulos, campos e configura\u00e7\u00f5es de formul\u00e1rios, duplicar formul\u00e1rios e modificar status de entradas. Quando desativado, essas habilidades s\u00e3o desregistradas e os clientes de IA s\u00f3 podem ler seus dados."],"Abilities API \u2014 Delete":["API de Habilidades \u2014 Excluir"],"Enable Delete Abilities":["Ativar habilidades de exclus\u00e3o"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Quando ativado, os clientes de IA podem excluir permanentemente formul\u00e1rios e entradas. Os dados exclu\u00eddos n\u00e3o podem ser recuperados. Quando desativado, as habilidades de exclus\u00e3o s\u00e3o desregistradas e os clientes de IA n\u00e3o podem remover nenhum dado."],"MCP Server":["Servidor MCP"],"Enable MCP Server":["Ativar Servidor MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Cria um endpoint SureForms MCP dedicado ao qual clientes de IA como Claude podem se conectar. Quando desativado, o endpoint \u00e9 removido e clientes de IA externos n\u00e3o podem descobrir ou chamar nenhuma habilidade do SureForms."],"Learn more":["Saiba mais"],"MCP Adapter Required":["Adaptador MCP Necess\u00e1rio"],"Heading 1":["T\u00edtulo 1"],"Heading 2":["T\u00edtulo 2"],"Heading 3":["T\u00edtulo 3"],"Heading 4":["T\u00edtulo 4"],"Heading 5":["T\u00edtulo 5"],"Heading 6":["T\u00edtulo 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configure a chave da API do Google Maps para preenchimento autom\u00e1tico de endere\u00e7o e visualiza\u00e7\u00e3o do mapa."],"Help shape the future of SureForms":["Ajude a moldar o futuro do SureForms"],"Enable Google Address Autocomplete":["Ativar o preenchimento autom\u00e1tico de endere\u00e7os do Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Fa\u00e7a upgrade para o Plano de Neg\u00f3cios SureForms para adicionar a autocompleta\u00e7\u00e3o de endere\u00e7os com tecnologia do Google e visualiza\u00e7\u00e3o interativa de mapa aos seus formul\u00e1rios."],"Auto-suggest addresses as users type for faster, error-free submissions":["Sugira automaticamente endere\u00e7os \u00e0 medida que os usu\u00e1rios digitam para envios mais r\u00e1pidos e sem erros"],"Show an interactive map preview with draggable pin for precise locations":["Mostrar uma pr\u00e9-visualiza\u00e7\u00e3o de mapa interativo com um pino arrast\u00e1vel para locais precisos"],"Automatically populate address fields like city, state, and postal code":["Preencher automaticamente campos de endere\u00e7o como cidade, estado e c\u00f3digo postal"],"This form is now closed as we've received all the entries.":["Este formul\u00e1rio est\u00e1 agora fechado, pois recebemos todas as inscri\u00e7\u00f5es."],"Thank you for contacting us! We will be in touch with you shortly.":["Obrigado por nos contatar! Entraremos em contato com voc\u00ea em breve."],"Saving\u2026":["Salvando\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Quando ativado, este formul\u00e1rio n\u00e3o armazenar\u00e1 o IP do usu\u00e1rio, o nome do navegador e o nome do dispositivo nas entradas."],"Form data":["Dados do formul\u00e1rio"],"Unsaved changes":["Altera\u00e7\u00f5es n\u00e3o salvas"],"Keep editing":["Continue editando"],"Global Defaults":["Configura\u00e7\u00f5es Globais"],"Form Restrictions":["Restri\u00e7\u00f5es de Formul\u00e1rio"],"Configure default settings that apply to newly created forms.":["Configurar as configura\u00e7\u00f5es padr\u00e3o que se aplicam a formul\u00e1rios rec\u00e9m-criados."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Recolha informa\u00e7\u00f5es n\u00e3o sens\u00edveis do seu site, como a vers\u00e3o do PHP e as funcionalidades utilizadas, para nos ajudar a corrigir erros mais rapidamente, tomar decis\u00f5es mais inteligentes e desenvolver funcionalidades que realmente importam para voc\u00ea."],"Failed to load pages. Please refresh and try again.":["Falha ao carregar as p\u00e1ginas. Por favor, atualize e tente novamente."],"Failed to load settings. Please refresh and try again.":["Falha ao carregar as configura\u00e7\u00f5es. Por favor, atualize e tente novamente."],"Form Validation fields cannot be left blank.":["Os campos de valida\u00e7\u00e3o de formul\u00e1rio n\u00e3o podem ficar em branco."],"Recipient email is required when email summaries are enabled.":["O e-mail do destinat\u00e1rio \u00e9 necess\u00e1rio quando os resumos por e-mail est\u00e3o ativados."],"Please enter a valid recipient email.":["Por favor, insira um email de destinat\u00e1rio v\u00e1lido."],"Settings saved.":["Configura\u00e7\u00f5es salvas."],"Failed to save settings.":["Falha ao salvar as configura\u00e7\u00f5es."],"Some settings failed to save. Please retry.":["Algumas configura\u00e7\u00f5es n\u00e3o foram salvas. Por favor, tente novamente."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Alguns campos t\u00eam altera\u00e7\u00f5es n\u00e3o salvas. Descarte-as para continuar ou permane\u00e7a para salvar suas edi\u00e7\u00f5es."],"Discard & switch":["Descartar e mudar"],"Import":["Importar"],"Migration":["Migra\u00e7\u00e3o"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importe formul\u00e1rios do Contact Form 7, WPForms, Gravity Forms e outros plugins para o SureForms."],"Could not generate the import preview.":["N\u00e3o foi poss\u00edvel gerar a pr\u00e9-visualiza\u00e7\u00e3o da importa\u00e7\u00e3o."],"Import failed. Please try again or check your error logs.":["Falha na importa\u00e7\u00e3o. Por favor, tente novamente ou verifique seus logs de erro."],"Go back":["Voltar"],"Update & import":["Atualizar e importar"],"Confirm & import":["Confirmar e importar"],"Form #%s":["Formul\u00e1rio n\u00ba %s"],"%d field will import":["%d campo ser\u00e1 importado"],"Some fields can't be migrated yet":["Alguns campos ainda n\u00e3o podem ser migrados"],"These fields will be skipped - add them manually after the form is created:":["Esses campos ser\u00e3o ignorados - adicione-os manualmente ap\u00f3s a cria\u00e7\u00e3o do formul\u00e1rio:"],"Some forms could not be parsed":["Alguns formul\u00e1rios n\u00e3o puderam ser analisados"],"Update existing":["Atualizar existente"],"Create a new copy":["Criar uma nova c\u00f3pia"],"Could not load forms for this source.":["N\u00e3o foi poss\u00edvel carregar formul\u00e1rios para esta fonte."],"(untitled form)":["(formul\u00e1rio sem t\u00edtulo)"],"Previously imported":["Importado anteriormente"],"Open existing SureForms form in a new tab":["Abrir formul\u00e1rio SureForms existente em uma nova aba"],"On re-import":["Na reimporta\u00e7\u00e3o"],"No forms found in this plugin.":["Nenhum formul\u00e1rio encontrado neste plugin."],"%1$d of %2$d form selected":["%1$d de %2$d formul\u00e1rio selecionado"],"Preview update":["Pr\u00e9-visualiza\u00e7\u00e3o da atualiza\u00e7\u00e3o"],"Preview import":["Pr\u00e9-visualizar importa\u00e7\u00e3o"],"Migration complete":["Migra\u00e7\u00e3o conclu\u00edda"],"Nothing was imported":["Nada foi importado"],"%d form was imported into SureForms.":["%d formul\u00e1rio foi importado para o SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Nenhum dos formul\u00e1rios selecionados p\u00f4de ser migrado. Veja os avisos abaixo para mais detalhes."],"Imported forms":["Formul\u00e1rios importados"],"Edit in SureForms":["Editar no SureForms"],"Some fields were skipped during import":["Alguns campos foram ignorados durante a importa\u00e7\u00e3o"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Adicione-os manualmente no editor de formul\u00e1rios - ainda n\u00e3o t\u00eam equivalente SureForms:"],"Some forms failed to import":["Alguns formul\u00e1rios falharam ao importar"],"Import more forms":["Importar mais formul\u00e1rios"],"Could not load the list of importable plugins.":["N\u00e3o foi poss\u00edvel carregar a lista de plugins import\u00e1veis."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Nenhum plugin de formul\u00e1rio suportado est\u00e1 ativo neste site. Ative um (como o Contact Form 7) com pelo menos um formul\u00e1rio para import\u00e1-lo para o SureForms."],"Plugin":["Plugin"],"%d form":["%d formul\u00e1rio"],"No forms":["Sem formul\u00e1rios"],"Multiple choice":["M\u00faltipla escolha"],"Consent":["Consentimento"],"The image uploaded but could not be inserted. Please try again.":["A imagem foi carregada, mas n\u00e3o p\u00f4de ser inserida. Por favor, tente novamente."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["A imagem n\u00e3o p\u00f4de ser carregada. Pode ser muito grande ou voc\u00ea pode n\u00e3o ter permiss\u00e3o para carregar arquivos."],"Please choose an image file.":["Por favor, escolha um arquivo de imagem."],"Show views and conversion rate":["Mostrar visualiza\u00e7\u00f5es e taxa de convers\u00e3o"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Adiciona as colunas de Visualiza\u00e7\u00f5es e Taxa de Convers\u00e3o \u00e0 lista de Formul\u00e1rios. Uma visualiza\u00e7\u00e3o \u00e9 contada uma vez por visita \u00e0 p\u00e1gina quando o formul\u00e1rio aparece na tela, e a taxa de convers\u00e3o \u00e9 a parcela dessas visualiza\u00e7\u00f5es que terminou em um envio. A contagem come\u00e7a na primeira vez que voc\u00ea ativa isso, ent\u00e3o os envios recebidos antes disso n\u00e3o s\u00e3o contados na taxa. Desativ\u00e1-lo posteriormente apenas oculta as colunas \u2014 a contagem continua, ent\u00e3o os n\u00fameros est\u00e3o atualizados se voc\u00ea ativ\u00e1-lo novamente."],"Log cleared.":["Log limpo."],"Could not clear the log.":["N\u00e3o foi poss\u00edvel limpar o log."],"Enable logs":["Ativar logs"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registra falhas de envio de formul\u00e1rios relatadas pelo navegador, para que voc\u00ea possa enviar o log para o suporte em vez de ler o console. Nada \u00e9 registrado enquanto seus formul\u00e1rios est\u00e3o funcionando, e os valores enviados nunca s\u00e3o armazenados."],"Download log":["Baixar log"],"Confirm delete":["Confirmar exclus\u00e3o"],"Log is full \u2014 download and clear it to keep recording.":["O log est\u00e1 cheio \u2014 baixe e limpe-o para continuar registrando."],"Form Views & Conversion":["Visualiza\u00e7\u00f5es e Convers\u00e3o de Formul\u00e1rios"],"Logs":["Logs"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Visualiza\u00e7\u00f5es e envios por qualquer pessoa que possa editar o site normalmente s\u00e3o deixados de fora, para que testar seus pr\u00f3prios formul\u00e1rios n\u00e3o altere esses n\u00fameros."],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}} \ No newline at end of file diff --git a/languages/sureforms-pt_PT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-pt_PT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json index 675bb3596..41ecfb087 100644 --- a/languages/sureforms-pt_PT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json +++ b/languages/sureforms-pt_PT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json @@ -1 +1 @@ -{"translation-revision-date":"2025-01-02T08:44:42+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Painel de Controle"],"Settings":["Configura\u00e7\u00f5es"],"Entries":["Entradas"],"Activated":["Ativado"],"Advanced Fields":["Campos Avan\u00e7ados"],"Select Form":["Selecionar Formul\u00e1rio"],"Create New Form":["Criar Novo Formul\u00e1rio"],"Forms":["Formul\u00e1rios"],"Copy":["Copiar"],"Business":["Neg\u00f3cio"],"Next":["Pr\u00f3ximo"],"Back":["Voltar"],"Cancel":["Cancelar"],"This is where your form views will appear":["\u00c9 aqui que suas visualiza\u00e7\u00f5es de formul\u00e1rio aparecer\u00e3o"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["A instala\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"Plugin activation failed, Please try again later.":["A ativa\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"What's New?":["O que h\u00e1 de novo?"],"Core":["N\u00facleo"],"Unlicensed":["Sem licen\u00e7a"],"Upgrade":["Atualizar"],"Webhooks":["Webhooks"],"Install & Activate":["Instalar e Ativar"],"Conditional Logic":["L\u00f3gica Condicional"],"Premium":["Premium"],"Welcome to SureForms!":["Bem-vindo ao SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms \u00e9 um plugin do WordPress que permite aos usu\u00e1rios criar formul\u00e1rios bonitos atrav\u00e9s de uma interface de arrastar e soltar, sem necessidade de codifica\u00e7\u00e3o. Ele se integra com o editor de blocos do WordPress."],"Read Full Guide":["Leia o Guia Completo"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Formul\u00e1rios Personalizados para WordPress FEITOS SIMPLES"],"No Date":["Sem data"],"Invalid Date":["Data Inv\u00e1lida"],"Ready to go beyond free plan?":["Pronto para ir al\u00e9m do plano gratuito?"],"Upgrade now":["Atualize agora"],"and unlock the full power of SureForms!":["e desbloqueie todo o poder do SureForms!"],"Upgrade SureForms":["Atualizar SureForms"],"Open Support Ticket":["Abrir Chamado de Suporte"],"Help Center":["Centro de Ajuda"],"Join our Community on Facebook":["Junte-se \u00e0 nossa comunidade no Facebook"],"Leave Us a Review":["Deixe-nos uma avalia\u00e7\u00e3o"],"Quick Access":["Acesso R\u00e1pido"],"Upgrade Now":["Atualize agora"],"Clear Filters":["Limpar Filtros"],"Unnamed Form":["Formul\u00e1rio Sem Nome"],"Forms Overview":["Vis\u00e3o Geral dos Formul\u00e1rios"],"Clear Form Filters":["Limpar Filtros do Formul\u00e1rio"],"Clear Date Filters":["Limpar Filtros de Data"],"Select Date Range":["Selecionar Intervalo de Datas"],"Apply":["Aplicar"],"Please wait for the data to load":["Por favor, aguarde o carregamento dos dados"],"No entries to display":["Sem entradas para exibir"],"Once you create a form and start receiving submissions, the data will appear here.":["Assim que voc\u00ea criar um formul\u00e1rio e come\u00e7ar a receber submiss\u00f5es, os dados aparecer\u00e3o aqui."],"Free":["Gr\u00e1tis"],"All Forms":["Todas as Formas"],"Guided Setup":["Configura\u00e7\u00e3o Guiada"],"Exit Guided Setup":["Sair da Configura\u00e7\u00e3o Guiada"],"Skip":["Pular"],"Build beautiful forms visually":["Crie formul\u00e1rios bonitos visualmente"],"Works perfectly on mobile":["Funciona perfeitamente no celular"],"Easy to connect with automation tools":["F\u00e1cil de conectar com ferramentas de automa\u00e7\u00e3o"],"Welcome to SureForms":["Bem-vindo ao SureForms"],"Smart, Quick and Powerful Forms.":["Formul\u00e1rios Inteligentes, R\u00e1pidos e Poderosos."],"Let's Get Started":["Vamos come\u00e7ar"],"Build up to 10 forms using AI":["Crie at\u00e9 10 formul\u00e1rios usando IA"],"A secure and private connection":["Uma conex\u00e3o segura e privada"],"Smart form drafts based on your input":["Rascunhos de formul\u00e1rios inteligentes com base no seu input"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Come\u00e7ar a partir de um formul\u00e1rio em branco nem sempre \u00e9 f\u00e1cil. Nossa IA pode ajudar criando um rascunho de formul\u00e1rio com base no que voc\u00ea est\u00e1 tentando fazer \u2014 economizando tempo e dando-lhe uma dire\u00e7\u00e3o clara."],"To do this, you'll need to connect your account.":["Para fazer isso, voc\u00ea precisar\u00e1 conectar sua conta."],"Let AI Help You Build Smarter, Faster Forms":["Deixe a IA Ajud\u00e1-lo a Criar Formul\u00e1rios Mais Inteligentes e R\u00e1pidos"],"Here's what that gives you:":["Aqui est\u00e1 o que isso lhe d\u00e1:"],"Continue":["Continuar"],"Connect":["Conectar"],"Works smoothly with forms made using SureForms":["Funciona perfeitamente com formul\u00e1rios feitos usando SureForms"],"Helps your emails reach the inbox instead of spam":["Ajuda seus e-mails a chegarem na caixa de entrada em vez de irem para o spam"],"Setup is straightforward, even if you're not technical":["A configura\u00e7\u00e3o \u00e9 simples, mesmo se voc\u00ea n\u00e3o for t\u00e9cnico"],"Lightweight and easy to use without adding clutter":["Leve e f\u00e1cil de usar sem adicionar desordem"],"Make Sure Your Emails Get Delivered":["Certifique-se de que seus e-mails sejam entregues"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["A maioria dos sites WordPress tem dificuldade em enviar e-mails de forma confi\u00e1vel, o que significa que os envios de formul\u00e1rios do seu site podem n\u00e3o chegar \u00e0 sua caixa de entrada \u2014 ou acabar no spam."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail \u00e9 um plugin SMTP simples que ajuda a garantir que seus e-mails sejam realmente entregues."],"What you will get:":["O que voc\u00ea receber\u00e1:"],"Install SureMail":["Instalar SureMail"],"AI Form Generation":["Gera\u00e7\u00e3o de Formul\u00e1rios por IA"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Cansado de criar formul\u00e1rios manualmente? Deixe a IA fazer o trabalho por voc\u00ea. Basta descrever e nossa IA criar\u00e1 seu formul\u00e1rio perfeito em segundos."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Divida formul\u00e1rios complexos em etapas simples, reduzindo a sobrecarga e aumentando as taxas de conclus\u00e3o. Guie os usu\u00e1rios suavemente pelo processo"],"Conditional Fields":["Campos Condicionais"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Mostrar ou ocultar campos com base nas respostas do usu\u00e1rio. Fa\u00e7a as perguntas certas e exiba apenas o que \u00e9 necess\u00e1rio para manter os formul\u00e1rios limpos e relevantes."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Melhore seus formul\u00e1rios com campos avan\u00e7ados, como upload de m\u00faltiplos arquivos, campos de avalia\u00e7\u00e3o e seletores de data e hora para coletar dados mais ricos e flex\u00edveis."],"Conversational Forms":["Formas de Conversa\u00e7\u00e3o"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Crie formul\u00e1rios que pare\u00e7am uma conversa. Uma pergunta de cada vez mant\u00e9m os usu\u00e1rios engajados e facilita o preenchimento do formul\u00e1rio."],"Digital Signatures":["Assinaturas Digitais"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Recolha assinaturas digitais legalmente vinculativas diretamente nos seus formul\u00e1rios para acordos, aprova\u00e7\u00f5es e contratos."],"Calculators":["Calculadoras"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Adicione calculadoras interativas aos seus formul\u00e1rios para estimativas, cota\u00e7\u00f5es e c\u00e1lculos instant\u00e2neos para seus usu\u00e1rios."],"User Registration and Login":["Registro e Login de Usu\u00e1rio"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Permita que os visitantes se registrem e fa\u00e7am login no seu site. \u00datil para associa\u00e7\u00f5es, comunidades ou qualquer site que necessite de acesso de usu\u00e1rios."],"PDF Generation Made Simple":["Gera\u00e7\u00e3o de PDF Simplificada"],"Custom App":["Aplicativo Personalizado"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Coletar dados, envi\u00e1-los para aplicativos externos para processamento e exibir resultados instantaneamente \u2014 tudo integrado de forma harmoniosa para criar experi\u00eancias de usu\u00e1rio din\u00e2micas e interativas."],"Select Your Features":["Selecione suas funcionalidades"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Obtenha mais controle, fluxos de trabalho mais r\u00e1pidos e personaliza\u00e7\u00e3o mais profunda \u2014 tudo projetado para ajud\u00e1-lo a construir melhores sites com menos esfor\u00e7o."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Recursos selecionados requerem %1$s - use o c\u00f3digo %2$s para obter 10% de desconto em qualquer plano."],"Copied":["Copiado"],"Style your form to better match your site's design":["Estilize seu formul\u00e1rio para combinar melhor com o design do seu site"],"Add spam protection to block common bot submissions":["Adicione prote\u00e7\u00e3o contra spam para bloquear envios comuns de bots"],"Get weekly email reports with a summary of form activity":["Receba relat\u00f3rios semanais por e-mail com um resumo da atividade do formul\u00e1rio"],"You're All Set! \ud83d\ude80":["Est\u00e1 tudo pronto! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Use nosso construtor de formul\u00e1rios de IA para come\u00e7ar rapidamente ou construa seu formul\u00e1rio do zero se voc\u00ea j\u00e1 souber o que precisa. Seus formul\u00e1rios est\u00e3o prontos para criar, compartilhar e conectar com os visitantes do seu site."],"Final Touches That Make a Difference:":["Toques Finais Que Fazem a Diferen\u00e7a:"],"Build Your First Form":["Crie Seu Primeiro Formul\u00e1rio"],"File Uploads":["Envios de Arquivos"],"Signature & Rating":["Assinatura e Avalia\u00e7\u00e3o"],"Calculation Forms":["Formul\u00e1rios de C\u00e1lculo"],"And Much More\u2026":["E muito mais\u2026"],"Upgrade to Pro":["Atualize para o Pro"],"Unlock Premium Features":["Desbloquear Recursos Premium"],"Build Better Forms with SureForms":["Construa Melhores Formul\u00e1rios com SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Adicione campos avan\u00e7ados, layouts conversacionais e l\u00f3gica inteligente para criar formul\u00e1rios que envolvam os usu\u00e1rios e capturem dados melhores."],"SureForms Video Thumbnail":["Miniatura de V\u00eddeo do SureForms"],"Payments":["Pagamentos"],"Knowledge Base":["Base de Conhecimento"],"What\u2019s New":["O que h\u00e1 de novo"],"Importing\u2026":["Importando\u2026"],"Learn":["Aprender"],"Unable to complete action. Please try again.":["N\u00e3o foi poss\u00edvel completar a a\u00e7\u00e3o. Por favor, tente novamente."],"Supercharge Your Workflow":["Supercarregue seu fluxo de trabalho"],"Spam protection included":["Prote\u00e7\u00e3o contra spam inclu\u00edda"],"Multistep Forms":["Formul\u00e1rios em v\u00e1rias etapas"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Envie entradas de formul\u00e1rio instantaneamente para qualquer sistema externo ou endpoint para impulsionar fluxos de trabalho avan\u00e7ados."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Transforme automaticamente as entradas de formul\u00e1rios em PDFs limpos e prontos para download. Perfeito para registros, compartilhamento, arquivamento ou para manter tudo organizado."],"Set up confirmation messages and email notifications for each entry":["Configure mensagens de confirma\u00e7\u00e3o e notifica\u00e7\u00f5es por e-mail para cada entrada"],"Something went wrong. Please try again.":["Algo deu errado. Por favor, tente novamente."],"Payment Forms":["Formas de Pagamento"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Cobre pagamentos diretamente atrav\u00e9s dos seus formul\u00e1rios. Aceite pagamentos \u00fanicos e recorrentes de forma fluida."],"SureForms %s":["SureForms %s"],"First name is required.":["O primeiro nome \u00e9 obrigat\u00f3rio."],"Please enter a valid email address.":["Por favor, insira um endere\u00e7o de e-mail v\u00e1lido."],"Email address is required.":["Endere\u00e7o de e-mail \u00e9 obrigat\u00f3rio."],"This is required.":["Isto \u00e9 necess\u00e1rio."],"Okay, just one last step\u2026":["Ok, s\u00f3 mais um \u00faltimo passo\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Ajude-nos a personalizar sua experi\u00eancia com o SureForms compartilhando um pouco sobre voc\u00ea."],"First Name":["Primeiro Nome"],"Enter your first name":["Insira seu primeiro nome"],"Last Name":["Sobrenome"],"Enter your last name":["Digite seu sobrenome"],"Email Address":["Endere\u00e7o de Email"],"Enter your email address":["Insira o seu endere\u00e7o de e-mail"],"Privacy Policy":["Pol\u00edtica de Privacidade"],"Finish":["Terminar"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Fique por dentro e ajude a moldar o SureForms! Receba atualiza\u00e7\u00f5es de recursos e nos ajude a melhorar o SureForms compartilhando como voc\u00ea usa o plugin. Pol\u00edtica de Privacidade<\/a>."],"%d form ready":["%d formul\u00e1rio pronto"],"%d already imported":["%d j\u00e1 importados"],"(unnamed source)":["(fonte sem nome)"],"All forms already imported":["Todos os formul\u00e1rios j\u00e1 importados"],"Import forms":["Importar formul\u00e1rios"],"Import %d form":["Importar %d formul\u00e1rio"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Algo deu errado durante a importa\u00e7\u00e3o. Voc\u00ea pode tentar novamente, pular ou abrir Configura\u00e7\u00f5es \u2192 Migra\u00e7\u00e3o."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Nenhum outro plugin de formul\u00e1rio detectado. Voc\u00ea pode importar a qualquer momento em Configura\u00e7\u00f5es \u2192 Migra\u00e7\u00e3o."],"Forms imported":["Formul\u00e1rios importados"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formul\u00e1rio de %2$s est\u00e1 agora no SureForms, pronto para publicar, estilizar e conectar."],"%d form imported":["%d formul\u00e1rio importado"],"%d form could not be imported":["%d formul\u00e1rio n\u00e3o p\u00f4de ser importado"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d tipo de campo n\u00e3o era suportado. Voc\u00ea pode reconstru\u00ed-lo manualmente dentro do SureForms."],"Bring your existing forms with you":["Traga seus formul\u00e1rios existentes com voc\u00ea"],"We detected forms in another plugin. Pick one to import into SureForms.":["Detect\u00e1mos formul\u00e1rios noutro plugin. Escolha um para importar para o SureForms."],"Choose a form plugin to import from":["Escolha um plugin de formul\u00e1rio para importar de"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importando mais de um plugin? Voc\u00ea pode importar fontes adicionais mais tarde em Configura\u00e7\u00f5es \u2192 Migra\u00e7\u00e3o."],"Import did not complete":["A importa\u00e7\u00e3o n\u00e3o foi conclu\u00edda"],"I'll do this later":["Vou fazer isso mais tarde"],"Retry":["Tentar novamente"],"Form Checks":["Verifica\u00e7\u00f5es de Formul\u00e1rio"],"Collapse form checks":["Recolher verifica\u00e7\u00f5es de formul\u00e1rio"],"Expand form checks":["Expandir verifica\u00e7\u00f5es de formul\u00e1rio"],"Ignore":["Ignorar"],"Working\u2026":["Trabalhando\u2026"]}}} \ No newline at end of file +{"translation-revision-date":"2025-01-02T08:44:42+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Painel de Controle"],"Settings":["Configura\u00e7\u00f5es"],"Entries":["Entradas"],"Activated":["Ativado"],"Advanced Fields":["Campos Avan\u00e7ados"],"Select Form":["Selecionar Formul\u00e1rio"],"Create New Form":["Criar Novo Formul\u00e1rio"],"Forms":["Formul\u00e1rios"],"Copy":["Copiar"],"Business":["Neg\u00f3cio"],"Next":["Pr\u00f3ximo"],"Back":["Voltar"],"Close":["Fechar"],"Cancel":["Cancelar"],"This is where your form views will appear":["\u00c9 aqui que suas visualiza\u00e7\u00f5es de formul\u00e1rio aparecer\u00e3o"],"Install":["Instalar"],"Plugin Installation failed, Please try again later.":["A instala\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"Plugin activation failed, Please try again later.":["A ativa\u00e7\u00e3o do plugin falhou, por favor, tente novamente mais tarde."],"What's New?":["O que h\u00e1 de novo?"],"Core":["N\u00facleo"],"Unlicensed":["Sem licen\u00e7a"],"Upgrade":["Atualizar"],"Webhooks":["Webhooks"],"Install & Activate":["Instalar e Ativar"],"Contact Support":["Contactar Suporte"],"Conditional Logic":["L\u00f3gica Condicional"],"Premium":["Premium"],"Welcome to SureForms!":["Bem-vindo ao SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms \u00e9 um plugin do WordPress que permite aos usu\u00e1rios criar formul\u00e1rios bonitos atrav\u00e9s de uma interface de arrastar e soltar, sem necessidade de codifica\u00e7\u00e3o. Ele se integra com o editor de blocos do WordPress."],"Read Full Guide":["Leia o Guia Completo"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Formul\u00e1rios Personalizados para WordPress FEITOS SIMPLES"],"No Date":["Sem data"],"Invalid Date":["Data Inv\u00e1lida"],"Ready to go beyond free plan?":["Pronto para ir al\u00e9m do plano gratuito?"],"Upgrade now":["Atualize agora"],"and unlock the full power of SureForms!":["e desbloqueie todo o poder do SureForms!"],"Upgrade SureForms":["Atualizar SureForms"],"Open Support Ticket":["Abrir Chamado de Suporte"],"Help Center":["Centro de Ajuda"],"Join our Community on Facebook":["Junte-se \u00e0 nossa comunidade no Facebook"],"Leave Us a Review":["Deixe-nos uma avalia\u00e7\u00e3o"],"Quick Access":["Acesso R\u00e1pido"],"Upgrade Now":["Atualize agora"],"Clear Filters":["Limpar Filtros"],"Unnamed Form":["Formul\u00e1rio Sem Nome"],"Forms Overview":["Vis\u00e3o Geral dos Formul\u00e1rios"],"Clear Form Filters":["Limpar Filtros do Formul\u00e1rio"],"Clear Date Filters":["Limpar Filtros de Data"],"Select Date Range":["Selecionar Intervalo de Datas"],"Apply":["Aplicar"],"Please wait for the data to load":["Por favor, aguarde o carregamento dos dados"],"No entries to display":["Sem entradas para exibir"],"Once you create a form and start receiving submissions, the data will appear here.":["Assim que voc\u00ea criar um formul\u00e1rio e come\u00e7ar a receber submiss\u00f5es, os dados aparecer\u00e3o aqui."],"Free":["Gr\u00e1tis"],"All Forms":["Todas as Formas"],"Guided Setup":["Configura\u00e7\u00e3o Guiada"],"Exit Guided Setup":["Sair da Configura\u00e7\u00e3o Guiada"],"Skip":["Pular"],"Build beautiful forms visually":["Crie formul\u00e1rios bonitos visualmente"],"Works perfectly on mobile":["Funciona perfeitamente no celular"],"Easy to connect with automation tools":["F\u00e1cil de conectar com ferramentas de automa\u00e7\u00e3o"],"Welcome to SureForms":["Bem-vindo ao SureForms"],"Smart, Quick and Powerful Forms.":["Formul\u00e1rios Inteligentes, R\u00e1pidos e Poderosos."],"Let's Get Started":["Vamos come\u00e7ar"],"Build up to 10 forms using AI":["Crie at\u00e9 10 formul\u00e1rios usando IA"],"A secure and private connection":["Uma conex\u00e3o segura e privada"],"Smart form drafts based on your input":["Rascunhos de formul\u00e1rios inteligentes com base no seu input"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Come\u00e7ar a partir de um formul\u00e1rio em branco nem sempre \u00e9 f\u00e1cil. Nossa IA pode ajudar criando um rascunho de formul\u00e1rio com base no que voc\u00ea est\u00e1 tentando fazer \u2014 economizando tempo e dando-lhe uma dire\u00e7\u00e3o clara."],"To do this, you'll need to connect your account.":["Para fazer isso, voc\u00ea precisar\u00e1 conectar sua conta."],"Let AI Help You Build Smarter, Faster Forms":["Deixe a IA Ajud\u00e1-lo a Criar Formul\u00e1rios Mais Inteligentes e R\u00e1pidos"],"Here's what that gives you:":["Aqui est\u00e1 o que isso lhe d\u00e1:"],"Continue":["Continuar"],"Connect":["Conectar"],"Works smoothly with forms made using SureForms":["Funciona perfeitamente com formul\u00e1rios feitos usando SureForms"],"Helps your emails reach the inbox instead of spam":["Ajuda seus e-mails a chegarem na caixa de entrada em vez de irem para o spam"],"Setup is straightforward, even if you're not technical":["A configura\u00e7\u00e3o \u00e9 simples, mesmo se voc\u00ea n\u00e3o for t\u00e9cnico"],"Lightweight and easy to use without adding clutter":["Leve e f\u00e1cil de usar sem adicionar desordem"],"Make Sure Your Emails Get Delivered":["Certifique-se de que seus e-mails sejam entregues"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["A maioria dos sites WordPress tem dificuldade em enviar e-mails de forma confi\u00e1vel, o que significa que os envios de formul\u00e1rios do seu site podem n\u00e3o chegar \u00e0 sua caixa de entrada \u2014 ou acabar no spam."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail \u00e9 um plugin SMTP simples que ajuda a garantir que seus e-mails sejam realmente entregues."],"What you will get:":["O que voc\u00ea receber\u00e1:"],"Install SureMail":["Instalar SureMail"],"AI Form Generation":["Gera\u00e7\u00e3o de Formul\u00e1rios por IA"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Cansado de criar formul\u00e1rios manualmente? Deixe a IA fazer o trabalho por voc\u00ea. Basta descrever e nossa IA criar\u00e1 seu formul\u00e1rio perfeito em segundos."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Divida formul\u00e1rios complexos em etapas simples, reduzindo a sobrecarga e aumentando as taxas de conclus\u00e3o. Guie os usu\u00e1rios suavemente pelo processo"],"Conditional Fields":["Campos Condicionais"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Mostrar ou ocultar campos com base nas respostas do usu\u00e1rio. Fa\u00e7a as perguntas certas e exiba apenas o que \u00e9 necess\u00e1rio para manter os formul\u00e1rios limpos e relevantes."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Melhore seus formul\u00e1rios com campos avan\u00e7ados, como upload de m\u00faltiplos arquivos, campos de avalia\u00e7\u00e3o e seletores de data e hora para coletar dados mais ricos e flex\u00edveis."],"Conversational Forms":["Formas de Conversa\u00e7\u00e3o"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Crie formul\u00e1rios que pare\u00e7am uma conversa. Uma pergunta de cada vez mant\u00e9m os usu\u00e1rios engajados e facilita o preenchimento do formul\u00e1rio."],"Digital Signatures":["Assinaturas Digitais"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Recolha assinaturas digitais legalmente vinculativas diretamente nos seus formul\u00e1rios para acordos, aprova\u00e7\u00f5es e contratos."],"Calculators":["Calculadoras"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Adicione calculadoras interativas aos seus formul\u00e1rios para estimativas, cota\u00e7\u00f5es e c\u00e1lculos instant\u00e2neos para seus usu\u00e1rios."],"User Registration and Login":["Registro e Login de Usu\u00e1rio"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Permita que os visitantes se registrem e fa\u00e7am login no seu site. \u00datil para associa\u00e7\u00f5es, comunidades ou qualquer site que necessite de acesso de usu\u00e1rios."],"PDF Generation Made Simple":["Gera\u00e7\u00e3o de PDF Simplificada"],"Custom App":["Aplicativo Personalizado"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Coletar dados, envi\u00e1-los para aplicativos externos para processamento e exibir resultados instantaneamente \u2014 tudo integrado de forma harmoniosa para criar experi\u00eancias de usu\u00e1rio din\u00e2micas e interativas."],"Select Your Features":["Selecione suas funcionalidades"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Obtenha mais controle, fluxos de trabalho mais r\u00e1pidos e personaliza\u00e7\u00e3o mais profunda \u2014 tudo projetado para ajud\u00e1-lo a construir melhores sites com menos esfor\u00e7o."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Recursos selecionados requerem %1$s - use o c\u00f3digo %2$s para obter 10% de desconto em qualquer plano."],"Copied":["Copiado"],"Style your form to better match your site's design":["Estilize seu formul\u00e1rio para combinar melhor com o design do seu site"],"Add spam protection to block common bot submissions":["Adicione prote\u00e7\u00e3o contra spam para bloquear envios comuns de bots"],"Get weekly email reports with a summary of form activity":["Receba relat\u00f3rios semanais por e-mail com um resumo da atividade do formul\u00e1rio"],"You're All Set! \ud83d\ude80":["Est\u00e1 tudo pronto! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Use nosso construtor de formul\u00e1rios de IA para come\u00e7ar rapidamente ou construa seu formul\u00e1rio do zero se voc\u00ea j\u00e1 souber o que precisa. Seus formul\u00e1rios est\u00e3o prontos para criar, compartilhar e conectar com os visitantes do seu site."],"Final Touches That Make a Difference:":["Toques Finais Que Fazem a Diferen\u00e7a:"],"Build Your First Form":["Crie Seu Primeiro Formul\u00e1rio"],"File Uploads":["Envios de Arquivos"],"Signature & Rating":["Assinatura e Avalia\u00e7\u00e3o"],"Calculation Forms":["Formul\u00e1rios de C\u00e1lculo"],"And Much More\u2026":["E muito mais\u2026"],"Upgrade to Pro":["Atualize para o Pro"],"Unlock Premium Features":["Desbloquear Recursos Premium"],"Build Better Forms with SureForms":["Construa Melhores Formul\u00e1rios com SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Adicione campos avan\u00e7ados, layouts conversacionais e l\u00f3gica inteligente para criar formul\u00e1rios que envolvam os usu\u00e1rios e capturem dados melhores."],"SureForms Video Thumbnail":["Miniatura de V\u00eddeo do SureForms"],"Payments":["Pagamentos"],"Knowledge Base":["Base de Conhecimento"],"What\u2019s New":["O que h\u00e1 de novo"],"Importing\u2026":["Importando\u2026"],"Learn":["Aprender"],"Unable to complete action. Please try again.":["N\u00e3o foi poss\u00edvel completar a a\u00e7\u00e3o. Por favor, tente novamente."],"Supercharge Your Workflow":["Supercarregue seu fluxo de trabalho"],"Spam protection included":["Prote\u00e7\u00e3o contra spam inclu\u00edda"],"Multistep Forms":["Formul\u00e1rios em v\u00e1rias etapas"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Envie entradas de formul\u00e1rio instantaneamente para qualquer sistema externo ou endpoint para impulsionar fluxos de trabalho avan\u00e7ados."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Transforme automaticamente as entradas de formul\u00e1rios em PDFs limpos e prontos para download. Perfeito para registros, compartilhamento, arquivamento ou para manter tudo organizado."],"Set up confirmation messages and email notifications for each entry":["Configure mensagens de confirma\u00e7\u00e3o e notifica\u00e7\u00f5es por e-mail para cada entrada"],"Something went wrong. Please try again.":["Algo deu errado. Por favor, tente novamente."],"Payment Forms":["Formas de Pagamento"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Cobre pagamentos diretamente atrav\u00e9s dos seus formul\u00e1rios. Aceite pagamentos \u00fanicos e recorrentes de forma fluida."],"SureForms %s":["SureForms %s"],"First name is required.":["O primeiro nome \u00e9 obrigat\u00f3rio."],"Please enter a valid email address.":["Por favor, insira um endere\u00e7o de e-mail v\u00e1lido."],"Email address is required.":["Endere\u00e7o de e-mail \u00e9 obrigat\u00f3rio."],"This is required.":["Isto \u00e9 necess\u00e1rio."],"Okay, just one last step\u2026":["Ok, s\u00f3 mais um \u00faltimo passo\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Ajude-nos a personalizar sua experi\u00eancia com o SureForms compartilhando um pouco sobre voc\u00ea."],"First Name":["Primeiro Nome"],"Enter your first name":["Insira seu primeiro nome"],"Last Name":["Sobrenome"],"Enter your last name":["Digite seu sobrenome"],"Email Address":["Endere\u00e7o de Email"],"Enter your email address":["Insira o seu endere\u00e7o de e-mail"],"Privacy Policy":["Pol\u00edtica de Privacidade"],"Finish":["Terminar"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Fique por dentro e ajude a moldar o SureForms! Receba atualiza\u00e7\u00f5es de recursos e nos ajude a melhorar o SureForms compartilhando como voc\u00ea usa o plugin. Pol\u00edtica de Privacidade<\/a>."],"%d form ready":["%d formul\u00e1rio pronto"],"%d already imported":["%d j\u00e1 importados"],"(unnamed source)":["(fonte sem nome)"],"All forms already imported":["Todos os formul\u00e1rios j\u00e1 importados"],"Import forms":["Importar formul\u00e1rios"],"Import %d form":["Importar %d formul\u00e1rio"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Algo deu errado durante a importa\u00e7\u00e3o. Voc\u00ea pode tentar novamente, pular ou abrir Configura\u00e7\u00f5es \u2192 Migra\u00e7\u00e3o."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Nenhum outro plugin de formul\u00e1rio detectado. Voc\u00ea pode importar a qualquer momento em Configura\u00e7\u00f5es \u2192 Migra\u00e7\u00e3o."],"Forms imported":["Formul\u00e1rios importados"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formul\u00e1rio de %2$s est\u00e1 agora no SureForms, pronto para publicar, estilizar e conectar."],"%d form imported":["%d formul\u00e1rio importado"],"%d form could not be imported":["%d formul\u00e1rio n\u00e3o p\u00f4de ser importado"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d tipo de campo n\u00e3o era suportado. Voc\u00ea pode reconstru\u00ed-lo manualmente dentro do SureForms."],"Bring your existing forms with you":["Traga seus formul\u00e1rios existentes com voc\u00ea"],"We detected forms in another plugin. Pick one to import into SureForms.":["Detect\u00e1mos formul\u00e1rios noutro plugin. Escolha um para importar para o SureForms."],"Choose a form plugin to import from":["Escolha um plugin de formul\u00e1rio para importar de"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importando mais de um plugin? Voc\u00ea pode importar fontes adicionais mais tarde em Configura\u00e7\u00f5es \u2192 Migra\u00e7\u00e3o."],"Import did not complete":["A importa\u00e7\u00e3o n\u00e3o foi conclu\u00edda"],"I'll do this later":["Vou fazer isso mais tarde"],"Retry":["Tentar novamente"],"Form Checks":["Verifica\u00e7\u00f5es de Formul\u00e1rio"],"Collapse form checks":["Recolher verifica\u00e7\u00f5es de formul\u00e1rio"],"Expand form checks":["Expandir verifica\u00e7\u00f5es de formul\u00e1rio"],"Ignore":["Ignorar"],"Working\u2026":["Trabalhando\u2026"],"Details":["Detalhes"],"Copy details":["Copiar detalhes"]}}} \ No newline at end of file diff --git a/languages/sureforms-pt_PT.mo b/languages/sureforms-pt_PT.mo index 3596a03cbe7017b41d60ed264c8495db6c3ef15e..621e72a057814c7f681a3b9e9943145730eb6a02 100644 GIT binary patch delta 76380 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 delta 72804 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

5!FTe`;7rHBoHws_ns-wHA9ah8S5H5US z4pzleSPzRg4ku%GtV?+c`ti9MJ-c&Xo1VA@JEC7&m*UNM99v z80)nPQ*;A5kV)vt_#nDA9zz%DLTrjl&;cID)W83CmJ46ZbVJxqIidy82Q$#jRE;)5 z52Ds+NA1zY+8OO=K)gQ+P4%7VHl2uW(`o1cXWYR4_km}ru))P>N3WqHTZLxe6Lj?- zL_0W(Hk7G#IB;^JnJI>L)Bvx=7HF!cUTZtZKIo^}KEQ{yRT~oY6IDnd9Dat*u5>CRN?*FA+l%wLZo5EtL zg6{Ks&?9yUdQ|Sk%9yib*x&WhK<`ASYAl+eDd^&T6wTB;^uy?7bTO}s_1|Oa&;Ji| z;hdaBzffemIW&|D{qQJ&9-WoZhFhY$p=+!kjs`Ln4PY9&MrL3?T!?1wBAU_cox*c@ zF=oroT(51XdZ1K_<%VbqTcQp1MmrpaHZ(roe;CcobI}!Odt1|xch$Ov2kPN6%5Bh&&qXhxU(vF33(prtM_4ji1s74SkIwz~vA#j~5O@nT zpc`X3*_{g`?SrQBw)nuk=m;Oc()b9v7?+_V-HbN$1)7l`(Dx6a0soGp@$Xnav`6UY zF7*Bs%y9od!i96T6rH1um^z`*wQ&fYv)|A)Z~-e|{#%0AqPt@V`UT~FbenyRweSx# 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 Ti%%(AqT(6F&8wDZl=*)EFse`S diff --git a/languages/sureforms-pt_PT.po b/languages/sureforms-pt_PT.po index e032b2c41..3e10f455c 100644 --- a/languages/sureforms-pt_PT.po +++ b/languages/sureforms-pt_PT.po @@ -14,9 +14,9 @@ msgstr "" #. Plugin Name of the plugin #. Author of the plugin #: sureforms.php -#: admin/admin.php:1152 -#: admin/admin.php:1153 -#: admin/admin.php:3021 +#: admin/admin.php:1198 +#: admin/admin.php:1199 +#: admin/admin.php:3185 #: inc/abilities/abilities-registrar.php:139 #: inc/gutenberg-hooks.php:109 #: inc/page-builders/bricks/elements/form-widget.php:67 @@ -43,8 +43,8 @@ msgstr "Uma maneira simples, mas poderosa, de criar formulários modernos para o msgid "https://sureforms.com/" msgstr "https://sureforms.com/" -#: admin/admin.php:1165 -#: admin/admin.php:1166 +#: admin/admin.php:1211 +#: admin/admin.php:1212 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -59,9 +59,9 @@ msgstr "https://sureforms.com/" msgid "Dashboard" msgstr "Painel de Controle" -#: admin/admin.php:1183 -#: admin/admin.php:1184 -#: admin/admin.php:1616 +#: admin/admin.php:1229 +#: admin/admin.php:1230 +#: admin/admin.php:1662 #: inc/compatibility/multilingual/string-collector.php:236 #: inc/compatibility/multilingual/string-collector.php:245 #: inc/compatibility/multilingual/string-collector.php:313 @@ -79,15 +79,15 @@ msgstr "Painel de Controle" msgid "Settings" msgstr "Configurações" -#: admin/admin.php:1452 -#: admin/admin.php:1453 +#: admin/admin.php:1498 +#: admin/admin.php:1499 #: inc/post-types.php:270 msgid "New Form" msgstr "Novo Formulário" -#: admin/admin.php:1461 -#: admin/admin.php:1462 -#: admin/admin.php:3340 +#: admin/admin.php:1507 +#: admin/admin.php:1508 +#: admin/admin.php:3504 #: inc/generate-form-markup.php:211 #: inc/generate-form-markup.php:224 #: inc/global-settings/email-summary.php:225 @@ -105,7 +105,7 @@ msgid "Entries" msgstr "Entradas" #. Translators: Post Title. -#: admin/admin.php:1704 +#: admin/admin.php:1750 #, php-format msgid "Edit %1$s" msgstr "Editar %1$s" @@ -118,7 +118,7 @@ msgstr "Editar %1$s" #: inc/duplicate-form.php:191 #: inc/export.php:126 #: inc/export.php:184 -#: inc/forms-data.php:88 +#: inc/forms-data.php:108 #: inc/global-settings/global-settings.php:89 #: inc/global-settings/global-settings.php:639 #: inc/rest-api.php:203 @@ -126,18 +126,18 @@ msgid "Nonce verification failed." msgstr "A verificação do nonce falhou." #. translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name. -#: admin/admin.php:2572 +#: admin/admin.php:2626 #, php-format msgid "Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more." msgstr "Por favor, %1$sative%2$s sua cópia do %3$s para obter novos recursos, acessar suporte, receber notificações de atualizações e muito mais." #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag. -#: admin/admin.php:2590 +#: admin/admin.php:2644 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s." msgstr "O SureForms %1$s requer um mínimo de %2$s %3$s para funcionar corretamente. Por favor, atualize para a versão mais recente %4$saqui%5$s." -#: admin/admin.php:3339 +#: admin/admin.php:3503 #: inc/global-settings/email-summary.php:224 #: assets/build/entries.js:172 #: assets/build/payments.js:172 @@ -207,7 +207,7 @@ msgstr "Lixo" msgid "Published" msgstr "Publicado" -#: admin/admin.php:3331 +#: admin/admin.php:3495 msgid "View" msgstr "Visualizar" @@ -299,11 +299,11 @@ msgstr "Nenhum plugin especificado" msgid "Plugin Successfully Activated" msgstr "Plugin ativado com sucesso" -#: admin/admin.php:1780 +#: admin/admin.php:1834 msgid "Activating..." msgstr "Ativando..." -#: admin/admin.php:1781 +#: admin/admin.php:1835 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/formEditor.js:172 @@ -318,17 +318,17 @@ msgstr "Ativando..." msgid "Activated" msgstr "Ativado" -#: admin/admin.php:1782 +#: admin/admin.php:1836 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Activate" msgstr "Ativar" -#: admin/admin.php:1783 +#: admin/admin.php:1837 msgid "Installing..." msgstr "Instalando..." -#: admin/admin.php:1784 +#: admin/admin.php:1838 msgid "Installed" msgstr "Instalado" @@ -336,12 +336,13 @@ msgstr "Instalado" msgid "You do not have permission to access this page." msgstr "Você não tem permissão para acessar esta página." -#: admin/admin.php:2784 -#: admin/admin.php:2912 -#: admin/admin.php:2950 -#: admin/admin.php:2971 -#: admin/admin.php:3294 -#: admin/admin.php:3601 +#: admin/admin.php:2874 +#: admin/admin.php:2917 +#: admin/admin.php:3076 +#: admin/admin.php:3114 +#: admin/admin.php:3135 +#: admin/admin.php:3458 +#: admin/admin.php:3847 #: inc/admin-ajax.php:164 #: inc/payments/admin/admin-handler.php:638 #: inc/payments/stripe/admin-stripe-handler.php:80 @@ -354,7 +355,7 @@ msgid "Form ID is required." msgstr "O ID do formulário é obrigatório." #: inc/admin-ajax.php:184 -#: inc/form-submit.php:1153 +#: inc/form-submit.php:1261 msgid "Invalid form ID." msgstr "ID de formulário inválido." @@ -593,28 +594,28 @@ msgstr "Telefone" msgid "Textarea" msgstr "Área de texto" -#: inc/form-submit.php:231 +#: inc/form-submit.php:281 msgid "Cloudflare Turnstile secret key is invalid." msgstr "A chave secreta do Cloudflare Turnstile é inválida." -#: inc/form-submit.php:238 +#: inc/form-submit.php:288 msgid "Cloudflare Turnstile response is missing." msgstr "Resposta do Cloudflare Turnstile está faltando." -#: inc/form-submit.php:282 +#: inc/form-submit.php:332 msgid "hCaptcha secret key is invalid." msgstr "A chave secreta do hCaptcha é inválida." -#: inc/form-submit.php:289 +#: inc/form-submit.php:339 msgid "hCaptcha response is missing." msgstr "Falta a resposta do hCaptcha." -#: inc/form-submit.php:330 -#: inc/form-submit.php:589 +#: inc/form-submit.php:380 +#: inc/form-submit.php:639 msgid "Form data is not found." msgstr "Os dados do formulário não foram encontrados." -#: inc/forms-data.php:68 +#: inc/forms-data.php:88 msgid "Sorry, you are not allowed to view the form." msgstr "Desculpe, você não tem permissão para visualizar o formulário." @@ -667,7 +668,7 @@ msgstr "O valor precisa ser único." msgid "Sorry, you are not allowed to perform this action." msgstr "Desculpe, você não tem permissão para realizar esta ação." -#: admin/admin.php:2639 +#: admin/admin.php:2693 msgid "Rate SureForms" msgstr "Avalie SureForms" @@ -728,7 +729,7 @@ msgstr "Editar Formulário" msgid "Edit" msgstr "Editar" -#: admin/admin.php:3072 +#: admin/admin.php:3236 #: inc/page-builders/elementor/form-widget.php:346 #: assets/build/dashboard.js:2 msgid "Create New Form" @@ -765,8 +766,8 @@ msgstr "Ver Formulário" msgid "View Forms" msgstr "Ver Formulários" -#: admin/admin.php:1443 -#: admin/admin.php:1444 +#: admin/admin.php:1489 +#: admin/admin.php:1490 #: inc/post-types.php:274 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 @@ -8280,7 +8281,7 @@ msgstr "Social" msgid "Travel" msgstr "Viagem" -#: inc/helper.php:2375 +#: inc/helper.php:2368 msgid "Blank Form" msgstr "Formulário em Branco" @@ -8599,7 +8600,9 @@ msgstr "Por favor, adicione uma opção de propriedades ao MultiButtonsControl" msgid "Icon Library" msgstr "Biblioteca de Ícones" +#: admin/admin.php:4062 #: assets/build/blocks.js:172 +#: assets/build/dashboard.js:172 #: modules/gutenberg/build/blocks.js:4 msgid "Close" msgstr "Fechar" @@ -8888,9 +8891,9 @@ msgstr "Adicionar blocos à Barra de Ação Rápida" msgid "Re-arrange block inside Quick Action Bar" msgstr "Reorganizar bloco dentro da Barra de Ação Rápida" -#: admin/admin.php:1236 -#: admin/admin.php:1237 -#: admin/admin.php:4129 +#: admin/admin.php:1282 +#: admin/admin.php:1283 +#: admin/admin.php:4737 #: assets/build/blocks.js:172 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 @@ -9263,7 +9266,7 @@ msgstr "Centro" msgid "Right" msgstr "Certo" -#: inc/form-submit.php:1344 +#: inc/form-submit.php:1452 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "Google reCAPTCHA" @@ -9273,7 +9276,7 @@ msgstr "Google reCAPTCHA" msgid "CloudFlare Turnstile" msgstr "CloudFlare Turnstile" -#: inc/form-submit.php:1348 +#: inc/form-submit.php:1456 #: assets/build/formEditor.js:172 #: assets/build/settings.js:172 msgid "hCaptcha" @@ -9569,7 +9572,7 @@ msgstr "Chave do Site" msgid "Secret Key" msgstr "Chave Secreta" -#: inc/form-submit.php:1352 +#: inc/form-submit.php:1460 #: assets/build/settings.js:172 msgid "Cloudflare Turnstile" msgstr "Cloudflare Turnstile" @@ -9594,7 +9597,8 @@ msgstr "Conectar agora" msgid "Authentication Failed" msgstr "Falha na autenticação" -#: admin/admin.php:3516 +#: admin/admin.php:4061 +#: assets/build/dashboard.js:172 #: assets/build/templatePicker.js:172 msgid "Contact Support" msgstr "Contactar Suporte" @@ -10566,18 +10570,18 @@ msgstr "Desbloquear Adicionar Nota" msgid "With the SureForms Starter plan, enhance your submitted form entries by adding personalized notes for better clarity and tracking." msgstr "Com o plano SureForms Starter, melhore suas entradas de formulário enviadas adicionando notas personalizadas para maior clareza e acompanhamento." -#: inc/form-submit.php:990 +#: inc/form-submit.php:1046 msgid "Email notification passed to the sending server" msgstr "Notificação por email enviada para o servidor de envio" #. translators: Here, %s is the comma separated emails list. -#: inc/form-submit.php:1063 +#: inc/form-submit.php:1130 #, php-format msgid "Email notification recipient: %s" msgstr "Destinatário da notificação por e-mail: %s" #. translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). -#: inc/form-submit.php:1080 +#: inc/form-submit.php:1147 #, php-format msgid "Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s" msgstr "O servidor de e-mail não conseguiu enviar a notificação por e-mail. Destinatário: %1$s. Motivo: %2$s" @@ -10620,7 +10624,7 @@ msgstr "Desbloquear Formulários Conversacionais" msgid "With the SureForms Pro Plan, you can transform your forms into engaging conversational layouts for a seamless user experience." msgstr "Com o Plano SureForms Pro, você pode transformar seus formulários em layouts conversacionais envolventes para uma experiência do usuário perfeita." -#: admin/admin.php:1075 +#: admin/admin.php:1121 msgid "Get SureForms Pro" msgstr "Obtenha o SureForms Pro" @@ -10832,24 +10836,24 @@ msgstr "Selecionar Gradiente" msgid "reCAPTCHA" msgstr "reCAPTCHA" -#: inc/form-submit.php:1288 +#: inc/form-submit.php:1396 msgid "Captcha validation failed. No error code provided." msgstr "A validação do Captcha falhou. Nenhum código de erro fornecido." -#: inc/form-submit.php:1289 +#: inc/form-submit.php:1397 msgid "Captcha validation failed." msgstr "Falha na validação do captcha." -#: inc/form-submit.php:1356 +#: inc/form-submit.php:1464 msgid "Unknown Captcha" msgstr "Captcha Desconhecido" -#: inc/form-submit.php:1357 +#: inc/form-submit.php:1465 msgid "Invalid captcha type." msgstr "Tipo de captcha inválido." #. translators: %s is the captcha title. -#: inc/form-submit.php:1370 +#: inc/form-submit.php:1478 #, php-format msgid "%s verification failed. Please contact your site administrator." msgstr "A verificação de %s falhou. Por favor, entre em contato com o administrador do seu site." @@ -11246,8 +11250,8 @@ msgstr "Descreva o formulário que você deseja criar" msgid "We are building your form…" msgstr "Estamos construindo seu formulário…" -#: admin/admin.php:1351 -#: admin/admin.php:1352 +#: admin/admin.php:1397 +#: admin/admin.php:1398 msgid "SMTP" msgstr "SMTP" @@ -11379,31 +11383,32 @@ msgstr "Ativar Notificação de Administrador" msgid "Admin notifications keep you informed about new form entries since your last visit." msgstr "As notificações de administrador mantêm você informado sobre novas entradas de formulário desde sua última visita." -#: admin/admin.php:2789 -#: admin/admin.php:2908 -#: admin/admin.php:2946 -#: admin/admin.php:2967 -#: admin/admin.php:3298 -#: admin/admin.php:3596 +#: admin/admin.php:2869 +#: admin/admin.php:2922 +#: admin/admin.php:3072 +#: admin/admin.php:3110 +#: admin/admin.php:3131 +#: admin/admin.php:3462 +#: admin/admin.php:3842 msgid "Unauthorized user." msgstr "Usuário não autorizado." #. translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong -#: admin/admin.php:2917 +#: admin/admin.php:3081 #, php-format msgid "%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s" msgstr "%1$sComece por %2$scriar o seu primeiro formulário%3$s.%4$s%5$sExperimente o poder do nosso intuitivo Criador de Formulários com IA%6$s" -#: admin/admin.php:2928 +#: admin/admin.php:3092 msgid "SureForms is waiting for you!" msgstr "SureForms está esperando por você!" -#: admin/admin.php:2930 +#: admin/admin.php:3094 msgid "Build My First Form" msgstr "Construir Meu Primeiro Formulário" -#: admin/admin.php:2931 -#: admin/admin.php:3418 +#: admin/admin.php:3095 +#: admin/admin.php:3650 msgid "Dismiss" msgstr "Dispensar" @@ -11423,51 +11428,51 @@ msgstr "Entrar" msgid "Register" msgstr "Registrar" -#: admin/admin.php:3327 +#: admin/admin.php:3491 msgid "Recent Entries" msgstr "Entradas Recentes" -#: admin/admin.php:3328 +#: admin/admin.php:3492 msgid "( Last 7 days )" msgstr "( Últimos 7 dias )" -#: admin/admin.php:4070 +#: admin/admin.php:4678 msgid "Use Conditional Logic to show only what matters" msgstr "Use lógica condicional para mostrar apenas o que importa" -#: admin/admin.php:4071 +#: admin/admin.php:4679 msgid "Split your form into steps to keep it easy" msgstr "Divida seu formulário em etapas para mantê-lo simples" -#: admin/admin.php:4072 +#: admin/admin.php:4680 msgid "Let people upload files directly to your form" msgstr "Permita que as pessoas enviem arquivos diretamente para o seu formulário" -#: admin/admin.php:4073 +#: admin/admin.php:4681 msgid "Turn responses into downloadable PDFs automatically" msgstr "Transforme as respostas em PDFs baixáveis automaticamente" -#: admin/admin.php:4074 +#: admin/admin.php:4682 msgid "Let users sign with a simple signature field" msgstr "Permita que os usuários assinem com um campo de assinatura simples" -#: admin/admin.php:4075 +#: admin/admin.php:4683 msgid "Connect your form to other tools using webhooks" msgstr "Conecte seu formulário a outras ferramentas usando webhooks" -#: admin/admin.php:4076 +#: admin/admin.php:4684 msgid "Use Conversational Forms for a chat-like experience" msgstr "Use Formulários de Conversa para uma experiência semelhante a um chat" -#: admin/admin.php:4077 +#: admin/admin.php:4685 msgid "Let users register or log in through your form" msgstr "Permita que os usuários se registrem ou façam login através do seu formulário" -#: admin/admin.php:4078 +#: admin/admin.php:4686 msgid "Build forms that create WordPress user accounts" msgstr "Crie formulários que criem contas de usuário no WordPress" -#: admin/admin.php:4079 +#: admin/admin.php:4687 msgid "Add calculations to auto-total scores or prices" msgstr "Adicione cálculos para totalizar automaticamente pontuações ou preços" @@ -11666,6 +11671,7 @@ msgstr "Obtenha mais controle, fluxos de trabalho mais rápidos e personalizaç msgid "Selected features require %1$s - use code %2$s to get 10% off on any plan." msgstr "Recursos selecionados requerem %1$s - use o código %2$s para obter 10% de desconto em qualquer plano." +#: admin/admin.php:4060 #: assets/build/dashboard.js:172 msgid "Copied" msgstr "Copiado" @@ -11889,7 +11895,7 @@ msgstr "Explore o SureDash" msgid "Something went wrong. We have logged the error for further investigation" msgstr "Algo deu errado. Registramos o erro para investigação posterior" -#: inc/field-validation.php:413 +#: inc/field-validation.php:452 msgid "Field is not valid." msgstr "O campo não é válido." @@ -11957,8 +11963,8 @@ msgstr "Miniatura de Vídeo do SureForms" msgid "Expected format for emails - email@sureforms.com or John Doe " msgstr "Formato esperado para e-mails - email@sureforms.com ou John Doe " -#: admin/admin.php:1471 -#: admin/admin.php:1472 +#: admin/admin.php:1517 +#: admin/admin.php:1518 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -12173,7 +12179,7 @@ msgstr "Eleve seu SEO e suba nos rankings de busca sem esforço!" msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress." msgstr "Aumente a visibilidade do seu site com automação inteligente de SEO. Otimize o conteúdo, acompanhe o desempenho das palavras-chave e obtenha insights acionáveis, tudo dentro do WordPress." -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -15372,12 +15378,12 @@ msgid "Stripe Settings" msgstr "Configurações do Stripe" #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. -#: admin/admin.php:2281 +#: admin/admin.php:2335 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version." msgstr "O SureForms %1$s requer o mínimo %2$s %3$s para funcionar corretamente. Por favor, atualize para a versão mais recente." -#: admin/admin.php:2294 +#: admin/admin.php:2348 msgid "Update Now" msgstr "Atualizar agora" @@ -15425,8 +15431,8 @@ msgstr "Posição do Sinal de Moeda" msgid "Select the position of the currency symbol relative to the amount." msgstr "Selecione a posição do símbolo da moeda em relação ao valor." -#: admin/admin.php:1414 -#: admin/admin.php:1415 +#: admin/admin.php:1460 +#: admin/admin.php:1461 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -15440,41 +15446,41 @@ msgstr "Selecione a posição do símbolo da moeda em relação ao valor." msgid "Learn" msgstr "Aprender" -#: admin/admin.php:2635 +#: admin/admin.php:2689 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!" msgstr "Incrível! SureForms está alimentando seus formulários e submissões - vamos continuar crescendo juntos!" -#: admin/admin.php:2636 +#: admin/admin.php:2690 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?" msgstr "Se o SureForms foi útil, você se importaria de deixar um comentário de 5 estrelas no WordPress.org?" -#: admin/admin.php:2647 -#: admin/admin.php:2711 +#: admin/admin.php:2701 +#: admin/admin.php:2765 msgid "Maybe later" msgstr "Talvez mais tarde" -#: admin/admin.php:2653 +#: admin/admin.php:2707 msgid "I already did" msgstr "Eu já fiz" -#: admin/admin.php:2702 +#: admin/admin.php:2756 msgid "SureForms is ready to power your forms — explore what's possible!" msgstr "SureForms está pronto para potencializar seus formulários — explore o que é possível!" -#: admin/admin.php:2703 +#: admin/admin.php:2757 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard." msgstr "Gerencie seus formulários, acompanhe as submissões e descubra recursos como o Construtor de Formulários com IA, integrações de pagamento e muito mais no painel do SureForms." -#: admin/admin.php:2706 +#: admin/admin.php:2760 msgid "Go to Dashboard" msgstr "Ir para o Painel" -#: admin/admin.php:2717 +#: admin/admin.php:2771 msgid "I already know" msgstr "Eu já sei" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2981 +#: admin/admin.php:3854 msgid "Invalid parameters." msgstr "Parâmetros inválidos." @@ -15858,10 +15864,10 @@ msgid "This form is not yet available. Check back after the scheduled start time msgstr "Este formulário ainda não está disponível. Verifique novamente após o horário de início programado." #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -15884,48 +15890,48 @@ msgstr "Este formulário ainda não está disponível. Verifique novamente após msgid "Security verification failed. Please refresh the page and try again." msgstr "A verificação de segurança falhou. Por favor, atualize a página e tente novamente." -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "Os dados do formulário não foram encontrados." -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "O ID do formulário está ausente." -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "Desculpe, você não tem permissão para acessar este recurso." -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "Você não tem permissão para enviar este formulário." -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "Este formulário não está mais disponível." -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "Por favor, verifique o formulário em busca de erros." -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "Sua submissão foi marcada como spam. Por favor, tente novamente." -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "Não foi possível enviar o formulário. Por favor, tente novamente." -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "Nenhum plugin SMTP detectado. Por favor, configure um plugin SMTP para habilitar o envio de e-mails." -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "O envio do e-mail falhou por uma razão desconhecida." -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "Nenhum e-mail foi enviado." @@ -16438,7 +16444,7 @@ msgid "Duplicate form?" msgstr "Duplicar formulário?" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:938 msgid "An error occurred while submitting your form. Please try again." msgstr "Ocorreu um erro ao enviar o seu formulário. Por favor, tente novamente." @@ -16815,28 +16821,28 @@ msgstr "Segredo do webhook não configurado." msgid "Invalid webhook signature." msgstr "Assinatura de webhook inválida." -#: admin/admin.php:1253 -#: admin/admin.php:1851 +#: admin/admin.php:1299 +#: admin/admin.php:1905 #: assets/build/formEditor.js:172 msgid "Quizzes" msgstr "Questionários" -#: admin/admin.php:1252 +#: admin/admin.php:1298 msgid "Quiz Entries" msgstr "Entradas do Quiz" -#: admin/admin.php:1255 -#: admin/admin.php:1288 -#: admin/admin.php:1321 +#: admin/admin.php:1301 +#: admin/admin.php:1334 +#: admin/admin.php:1367 #: assets/build/settings.js:172 msgid "New" msgstr "Novo" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "Formulário inválido." -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "Solicitações demais. Por favor, tente novamente em breve." @@ -17345,9 +17351,9 @@ msgstr "Perfeito para educação, treinamento e curiosidades divertidas" msgid "Select this to create a quiz with scored questions and graded results." msgstr "Selecione isto para criar um questionário com perguntas pontuadas e resultados avaliados." -#: admin/admin.php:1285 -#: admin/admin.php:1286 -#: admin/admin.php:1856 +#: admin/admin.php:1331 +#: admin/admin.php:1332 +#: admin/admin.php:1910 msgid "Survey Reports" msgstr "Relatórios de Pesquisa" @@ -17753,7 +17759,7 @@ msgid "Error creating SureForms Form." msgstr "Erro ao criar o formulário SureForms." #. translators: %s represents the minimum number of characters required -#: inc/field-validation.php:426 +#: inc/field-validation.php:465 #: inc/translatable.php:94 #, php-format msgid "Please enter at least %s characters." @@ -17911,9 +17917,9 @@ msgstr "A resposta da IA estava vazia. Por favor, refine seu prompt e tente nova msgid "Unable to build form fields from the AI response." msgstr "Não foi possível criar campos de formulário a partir da resposta da IA." -#: admin/admin.php:1318 -#: admin/admin.php:1319 -#: admin/admin.php:1861 +#: admin/admin.php:1364 +#: admin/admin.php:1365 +#: admin/admin.php:1915 msgid "Partial Entries" msgstr "Entradas Parciais" @@ -18784,23 +18790,23 @@ msgstr "O valor do pagamento não pôde ser verificado para este formulário. Po msgid "Cancellation is not supported for this payment gateway." msgstr "O cancelamento não é suportado para este gateway de pagamento." -#: admin/admin.php:3039 +#: admin/admin.php:3203 msgid "SureForms AI Quick Draft" msgstr "SureForms AI Rascunho Rápido" -#: admin/admin.php:3058 +#: admin/admin.php:3222 msgid "Describe the form and let SureForms AI generate it for you." msgstr "Descreva o formulário e deixe o SureForms AI gerá-lo para você." -#: admin/admin.php:3061 +#: admin/admin.php:3225 msgid "Describe your form" msgstr "Descreva seu formulário" -#: admin/admin.php:3068 +#: admin/admin.php:3232 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "Exemplo: Crie um formulário de contato com campos de nome, email, telefone e mensagem." -#: admin/admin.php:3109 +#: admin/admin.php:3273 msgid "Redirecting..." msgstr "Redirecionando..." @@ -18813,14 +18819,14 @@ msgid "The prompt is too long. Please shorten it and try again." msgstr "O prompt é muito longo. Por favor, encurte-o e tente novamente." #. translators: %s: maximum characters allowed before the @ symbol. -#: inc/field-validation.php:448 +#: inc/field-validation.php:487 #: inc/translatable.php:97 #, php-format msgid "The part before @ may not exceed %s characters." msgstr "A parte antes do @ não pode exceder %s caracteres." #. translators: %s: maximum characters allowed after the @ symbol. -#: inc/field-validation.php:454 +#: inc/field-validation.php:493 #: inc/translatable.php:100 #, php-format msgid "The part after @ may not exceed %s characters." @@ -18907,74 +18913,74 @@ msgstr "Desculpe, você não tem permissão para renderizar este formulário." msgid "This form requires a payment. Please complete the payment and submit again." msgstr "Este formulário requer um pagamento. Por favor, complete o pagamento e envie novamente." -#: admin/admin.php:552 +#: admin/admin.php:598 msgid "You are not allowed to update this prompt." msgstr "Você não tem permissão para atualizar este prompt." -#: admin/admin.php:595 +#: admin/admin.php:641 msgid "Finish setting up your form" msgstr "Conclua a configuração do seu formulário" -#: admin/admin.php:627 +#: admin/admin.php:673 msgid "Review or edit your form" msgstr "Revise ou edite seu formulário" -#: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:674 +#: admin/admin.php:4515 msgid "Edit form" msgstr "Editar formulário" -#: admin/admin.php:633 +#: admin/admin.php:679 msgid "Personalize the Thank You message" msgstr "Personalize a mensagem de agradecimento" -#: admin/admin.php:634 +#: admin/admin.php:680 msgid "Edit message" msgstr "Editar mensagem" -#: admin/admin.php:639 +#: admin/admin.php:685 msgid "Choose who gets notified of new replies" msgstr "Escolha quem será notificado sobre novas respostas" -#: admin/admin.php:640 +#: admin/admin.php:686 msgid "Set up email" msgstr "Configurar e-mail" -#: admin/admin.php:648 +#: admin/admin.php:694 msgid "your form" msgstr "seu formulário" #. translators: %s: form title. -#: admin/admin.php:651 +#: admin/admin.php:697 #, php-format msgid "Finish setting up %s" msgstr "Conclua a configuração de %s" #. translators: %s: form title. -#: admin/admin.php:659 +#: admin/admin.php:705 #, php-format msgid "View %s (opens in a new tab)" msgstr "Visualizar %s (abre em uma nova aba)" -#: admin/admin.php:664 +#: admin/admin.php:710 msgid "Customize your form to get it ready for real submissions:" msgstr "Personalize seu formulário para deixá-lo pronto para envios reais:" -#: admin/admin.php:3896 +#: admin/admin.php:4504 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "Já criamos este formulário para você. Termine de personalizá-lo para que esteja pronto para coletar envios reais." #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4509 #, php-format msgid "Finish setting up “%s”" msgstr "Conclua a configuração de “%s”" -#: admin/admin.php:3914 +#: admin/admin.php:4522 msgid "Edit the Thank You message" msgstr "Edite a mensagem de agradecimento" -#: admin/admin.php:3920 +#: admin/admin.php:4528 msgid "Set where replies go" msgstr "Defina para onde vão as respostas" @@ -19002,123 +19008,90 @@ msgstr "Por favor, escolha um arquivo de imagem." msgid "This form could not be submitted. Please refresh the page and try again." msgstr "Este formulário não pôde ser enviado. Por favor, atualize a página e tente novamente." -#: admin/admin.php:2341 -#: admin/admin.php:2430 +#: admin/admin.php:2395 +#: admin/admin.php:2484 msgid "Your SureForms database is up to date. New form entries will be saved as usual." msgstr "Seu banco de dados SureForms está atualizado. Novas entradas de formulário serão salvas como de costume." -#: admin/admin.php:2355 -#: admin/admin.php:2439 +#: admin/admin.php:2409 +#: admin/admin.php:2493 #: inc/rest-api.php:1447 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support." msgstr "SureForms não conseguiu concluir a atualização do banco de dados. Sua hospedagem pode não permitir que o SureForms crie tabelas de banco de dados — por favor, entre em contato com seu provedor de hospedagem ou com o suporte do SureForms." -#: admin/admin.php:2358 +#: admin/admin.php:2412 msgid "Contact support" msgstr "Contatar suporte" -#: admin/admin.php:2378 +#: admin/admin.php:2432 msgid "Database update needed" msgstr "Atualização do banco de dados necessária" -#: admin/admin.php:2384 +#: admin/admin.php:2438 msgid "Fix now" msgstr "Corrigir agora" -#: admin/admin.php:2453 +#: admin/admin.php:2507 msgid "SureForms — database update needed" msgstr "SureForms — atualização do banco de dados necessária" -#: admin/admin.php:2458 +#: admin/admin.php:2512 msgid "Update database" msgstr "Atualizar banco de dados" -#: admin/admin.php:2478 +#: admin/admin.php:2532 msgid "You do not have permission to update the database." msgstr "Você não tem permissão para atualizar o banco de dados." -#: admin/admin.php:3437 +#: admin/admin.php:3669 msgid "You do not have permission to do this." msgstr "Você não tem permissão para fazer isso." -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "%d envio de formulário consecutivo não pôde ser concluído." -msgstr[1] "" - -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." -msgstr "Os visitantes podem não conseguir entrar em contato com você, e essas entradas não foram salvas." - -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." -msgstr "Os envios de formulários estão sendo concluídos normalmente." - -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." -msgstr "Nenhum plugin de cache que precise de configuração foi encontrado." - #. translators: %s: caching plugin name. -#: admin/admin.php:3551 +#: admin/admin.php:4209 msgid "%s may interfere with your forms." msgstr "%s pode interferir com seus formulários." -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." -msgstr "O cache e a otimização de JavaScript podem servir uma cópia desatualizada do seu formulário ou carregar seus scripts fora de ordem." - -#: admin/admin.php:3555 +#: admin/admin.php:4185 +#: admin/admin.php:4213 msgid "Help Me Fix" msgstr "Ajude-me a corrigir" -#: admin/admin.php:3650 +#: admin/admin.php:4258 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "SureForms encontrou suas entradas de formulário armazenadas sob um prefixo de tabela de banco de dados diferente. Reconectá-las leva um momento, e suas entradas existentes serão mantidas." -#: admin/admin.php:3653 +#: admin/admin.php:4261 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "SureForms precisa atualizar seu banco de dados antes que possa salvar novas entradas de formulário. Isso leva apenas um momento e não alterará seus formulários ou conteúdo existente. Entradas enviadas antes de agora não podem ser recuperadas daqui." #. translators: %s: site host. -#: admin/admin.php:4265 +#: admin/admin.php:5032 msgid "SureForms: form submissions are failing on %s" msgstr "SureForms: os envios de formulário estão falhando em %s" -#: admin/admin.php:4274 +#: admin/admin.php:4990 msgid "Debug log: no entries recorded." msgstr "Log de depuração: nenhuma entrada registrada." #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4995 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "Log de depuração (mais recente %1$d de %2$d entradas)" -#: admin/admin.php:4288 -msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." -msgstr "Entradas mais antigas foram deixadas de fora para manter este e-mail dentro do comprimento aceito por um cliente de e-mail. O log completo pode ser baixado de SureForms → Configurações → Geral." - -#: admin/admin.php:4318 +#: admin/admin.php:5158 msgid "Hello SureForms support," msgstr "Olá suporte do SureForms," -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." -msgstr[0] "SureForms registrou %d envio de formulário consecutivo que não pôde ser concluído." -msgstr[1] "" - -#: admin/admin.php:4332 +#: admin/admin.php:5182 msgid "Site details" msgstr "Detalhes do site" -#: admin/admin.php:4335 +#: admin/admin.php:5185 msgid "not active" msgstr "não ativo" -#: admin/admin.php:4338 +#: admin/admin.php:5188 msgid "none detected" msgstr "nenhum detectado" @@ -19126,7 +19099,7 @@ msgstr "nenhum detectado" msgid "No form submission failures have been recorded." msgstr "Nenhuma falha de envio de formulário foi registrada." -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "Não encontrado." @@ -19211,6 +19184,193 @@ msgstr "Visualizações e Conversão de Formulários" msgid "Logs" msgstr "Logs" +#: admin/admin.php:2831 +msgid "Previous notice" +msgstr "Aviso anterior" + +#: admin/admin.php:2832 +msgid "Next notice" +msgstr "Próximo aviso" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2834 +msgid "%1$d of %2$d" +msgstr "%1$d de %2$d" + +#: admin/admin.php:2891 +msgid "Nothing to report." +msgstr "Nada a relatar." + +#: admin/admin.php:4057 +#: assets/build/dashboard.js:172 +msgid "Details" +msgstr "Detalhes" + +#: admin/admin.php:4058 +msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it." +msgstr "O que registramos sobre este problema. Copie-o em sua solicitação de suporte para que possamos começar pela causa e não por uma descrição dela." + +#: admin/admin.php:4059 +#: assets/build/dashboard.js:172 +msgid "Copy details" +msgstr "Copiar detalhes" + +#: admin/admin.php:4068 +msgid "Copy the details first, so you have them to paste." +msgstr "Copie os detalhes primeiro, para que você os tenha para colar." + +#: admin/admin.php:4072 +msgid "Copied. Contact Support is now available." +msgstr "Copiado. Contatar Suporte agora está disponível." + +#: admin/admin.php:4073 +msgid "Your browser would not let us copy. Select the text above and copy it by hand." +msgstr "Seu navegador não nos permitiu copiar. Selecione o texto acima e copie-o manualmente." + +#: admin/admin.php:4076 +msgid "Recorded diagnostics" +msgstr "Diagnósticos registrados" + +#: admin/admin.php:4080 +msgid "Collecting the details…" +msgstr "Coletando os detalhes…" + +#: admin/admin.php:4081 +msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there." +msgstr "Não conseguimos coletar os detalhes. Contate o Suporte e descreva o que aconteceu, e nós cuidaremos disso a partir daí." + +#. translators: %s: form title. +#: admin/admin.php:4108 +msgid "We noticed a form submission failure on %s." +msgstr "Notamos uma falha de envio de formulário em %s." + +#: admin/admin.php:4109 +msgid "We noticed a form submission failure." +msgstr "Notamos uma falha de envio de formulário." + +#: admin/admin.php:4110 +msgid "Visitors may not be able to reach you, and their entries were not saved." +msgstr "Os visitantes podem não conseguir entrar em contato com você, e suas entradas não foram salvas." + +#. translators: %s: form title. +#: admin/admin.php:4115 +msgid "We noticed a notification failure on %s." +msgstr "Notamos uma falha de notificação em %s." + +#: admin/admin.php:4116 +msgid "We noticed a notification failure." +msgstr "Notamos uma falha de notificação." + +#: admin/admin.php:4117 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "A entrada foi salva, mas não conseguimos enviar o e-mail sobre ela. Novas entradas podem estar chegando sem que você saiba." + +#. translators: %s: form title. +#: admin/admin.php:4132 +msgid "We noticed an integration failure on %s." +msgstr "Notamos uma falha de integração em %s." + +#: admin/admin.php:4133 +msgid "We noticed an integration failure." +msgstr "Notamos uma falha de integração." + +#: admin/admin.php:4134 +msgid "The entry was saved, but we could not send it to a connected service." +msgstr "A entrada foi salva, mas não conseguimos enviá-la para um serviço conectado." + +#: admin/admin.php:4158 +msgid "View details" +msgstr "Ver detalhes" + +#: admin/admin.php:4212 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "O cache pode mostrar aos visitantes uma cópia antiga do seu formulário ou carregar seus scripts na ordem errada." + +#: admin/admin.php:5005 +msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General." +msgstr "Entradas mais antigas foram deixadas de fora para manter este trecho legível. O log completo pode ser baixado de SureForms → Configurações → Geral." + +#. translators: %s: site host. +#: admin/admin.php:5034 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "SureForms registrou envios de formulário em %s que não puderam ser concluídos." + +#. translators: %s: site host. +#: admin/admin.php:5038 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "SureForms: e-mails de notificação não estão sendo enviados em %s" + +#. translators: %s: site host. +#: admin/admin.php:5040 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "SureForms salvou entradas em %s, mas não conseguiu enviar os e-mails de notificação para elas." + +#. translators: %s: site host. +#: admin/admin.php:5044 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "SureForms: uma integração não está recebendo entradas em %s" + +#. translators: %s: site host. +#: admin/admin.php:5046 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "SureForms salvou entradas em %s, mas não conseguiu passá-las para um serviço conectado." + +#. translators: %s: site host. +#: admin/admin.php:5056 +msgid "SureForms: a problem with the forms on %s" +msgstr "SureForms: um problema com os formulários em %s" + +#. translators: %s: site host. +#: admin/admin.php:5058 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "SureForms registrou um problema com os formulários em %s." + +#. translators: %d: number of failed submissions. +#: admin/admin.php:5083 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." +msgstr[0] "SureForms registrou %d envio de formulário que não pôde ser concluído." +msgstr[1] "" + +#. translators: %d: number of failed notifications. +#: admin/admin.php:5095 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "SureForms salvou %d entrada, mas não conseguiu enviar o e-mail de notificação para ela." +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:5107 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "SureForms salvou %d entrada, mas não conseguiu passá-la para um serviço conectado." +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:5119 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "SureForms registrou %d problema com os formulários neste site." +msgstr[1] "" + +#. translators: %s: form title. +#: admin/admin.php:5169 +msgid "Form: %s" +msgstr "Formulário: %s" + +#: admin/admin.php:5189 +msgid "none recorded" +msgstr "nenhum registrado" + +#. translators: %s: date and time of the previous report, in the site's timezone. +#: admin/admin.php:5212 +msgid "Previously reported: %s" +msgstr "Relatado anteriormente: %s" + +#: assets/build/settings.js:172 +msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures." +msgstr "Visualizações e envios por qualquer pessoa que possa editar o site normalmente são deixados de fora, para que testar seus próprios formulários não altere esses números." + #: inc/post-types.php:265 msgctxt "post type general name" msgid "Forms" From 57a7ced8be074402ed1b4fcbc4c638abb68e3363 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Sat, 12 Sep 2026 11:20:09 +0530 Subject: [PATCH 56/58] fix: the rating notice appeared alongside a failure warning From Vansh: "Rate SureForms 5 stars" rendered directly beneath "We noticed a notification failure on Contact Form". All three engagement notices -- rating, Getting Started and the Thank You prompt -- already asked has_action_item_warnings() before showing. The problem was what that answered. It re-stated its conditions rather than reading them, and the restatement was narrower than the display: has_persistent_failures() reads the `submission` counter alone, while the notices and the Form Checks panel warn on any open failure in any of the three categories. So a notification or integration failure left the gate false. Measured, one open notification failure with the caching advice dismissed: warning_notices_on_screen ["notification_error"] has_action_item_warnings false Submission was covered only incidentally, because FAULT_THRESHOLD is 1. Raise it and submission would have joined them. Derived from get_first_party_action_items() now -- the function that builds those warnings -- so the gate cannot drift from the display again. Both constraints the old version documented are kept: it still does not call get_action_items(), which records an impression as a side effect and must never run from a show_if; and it still reads the first-party set specifically, so an item contributed through `srfm_action_items` cannot suppress notices unrelated to it. Verified get_first_party_action_items() has no side effects of its own -- the impression tracking sits one level up in get_action_items(). The test asserts the gate and get_action_items() together, because the defect was the two disagreeing and checking the gate alone is what let it through. It covers submission as well, so raising FAULT_THRESHOLD cannot quietly reopen this. Pre-existing and untouched: the two Test_Thankyou_Prompt_Notice failures are test-order interference on next-release itself, confirmed against a stash. Co-Authored-By: Claude Opus 5 (1M context) --- admin/admin.php | 59 +++++++++++++++++---------- tests/unit/admin/test-admin.php | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/admin/admin.php b/admin/admin.php index 96ab0cf4d..25a308164 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -3682,18 +3682,33 @@ public function handle_dismiss_action_item_link() { } /** - * Whether anything is currently wrong enough to warrant a notice. - * - * Deliberately re-derives rather than calling get_action_items(), which - * records an impression as a side effect and must not run from a show_if - * callback. It re-derives the two first-party conditions only -- a persistent - * failure and an active caching plugin -- so an item contributed through - * `srfm_action_items` does not suppress the notices this gates. - * - * Returns false with logging disabled, which is what makes the rating, - * getting-started and Thank-You notices eligible again on a site that has - * turned this surface off. Intended: those notices are gated on nothing being - * wrong, and with the surface off there is nothing being reported. + * Whether a first-party warning is currently on screen. + * + * Asked from the show_if of the rating, Getting Started and Thank You notices, + * all of which are gated on nothing being wrong. "Wrong" has to mean the same + * thing here as it does to the person looking at the screen. + * + * It used to re-state the conditions instead of reading them, and the + * restatement was narrower than the display: has_persistent_failures() reads + * the `submission` counter alone, while the notices and the Form Checks panel + * warn on any open failure in any of the three categories. So an open + * notification or integration failure left this false, and the review ask + * appeared directly beneath "We noticed a notification failure on Contact + * Form". Submission was covered only incidentally, by FAULT_THRESHOLD being 1 -- + * raise that and it would have joined them. + * + * Derived from get_first_party_action_items() now, which is the thing that + * builds those warnings, so the gate cannot drift from the display again. + * + * Two constraints kept from the previous version. It must not call + * get_action_items(): that records an impression as a side effect and must + * never run from a show_if. And it reads the first-party set specifically, so + * an item contributed through `srfm_action_items` cannot suppress notices that + * have nothing to do with it. + * + * Returns false with logging disabled, which is what makes those notices + * eligible again on a site that has turned this surface off. Intended: with the + * surface off there is nothing being reported. * * @since 2.12.6 * @return bool @@ -3703,17 +3718,21 @@ public function has_action_item_warnings() { return false; } - if ( Client_Logger::has_persistent_failures() ) { - return true; - } + foreach ( $this->get_first_party_action_items() as $item ) { + if ( ! is_array( $item ) ) { + continue; + } - if ( '' === Helper::get_active_caching_plugin() ) { - return false; - } + $status = Helper::get_string_value( $item['status'] ?? '' ); - $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) ); + // Matches the renderers: 'success' is a passing check and an empty + // status is not a warning either, so neither suppresses anything. + if ( 'success' !== $status && '' !== $status ) { + return true; + } + } - return ! in_array( 'caching_plugin', $dismissed, true ); + return false; } /** diff --git a/tests/unit/admin/test-admin.php b/tests/unit/admin/test-admin.php index 04fa40c9f..730143302 100644 --- a/tests/unit/admin/test-admin.php +++ b/tests/unit/admin/test-admin.php @@ -2371,6 +2371,77 @@ public function test_has_action_item_warnings() { } + /** + * Every category that puts a warning on screen also silences the review ask. + * + * The gate used to re-state its conditions rather than read them, and the + * restatement was narrower than the display: has_persistent_failures() reads the + * `submission` counter alone, while the notices warn on any open failure in any + * of the three categories. So "Rate SureForms 5 stars" rendered directly beneath + * "We noticed a notification failure on Contact Form". + * + * Submission passed before this fix, but only because FAULT_THRESHOLD is 1. It is + * covered here anyway so raising that threshold cannot quietly reopen the hole. + * + * Asserted against get_action_items() in the same breath, because the defect was + * the gate and the display disagreeing -- checking the gate alone is what let it + * through. + */ + public function test_every_warning_category_suppresses_the_engagement_notices() { + wp_set_current_user( $this->make_user( 'administrator' ) ); + + // Caching advice out of the way, so the fault is the only thing in play. + Helper::update_srfm_option( 'dismissed_action_items', [ 'caching_plugin' ] ); + + $quiet = static function () { + return [ 'akismet/akismet.php' ]; + }; + + add_filter( 'pre_option_active_plugins', $quiet ); + + foreach ( [ 'notification', 'integration', 'submission' ] as $category ) { + delete_option( Client_Logger::FAILURES_OPTION ); + Client_Logger::record_failure( $category, 42, 'Contact Form' ); + Admin::reset_action_items_cache(); + + $on_screen = 0; + + foreach ( Admin::get_instance()->get_action_items() as $item ) { + $status = Helper::get_string_value( is_array( $item ) ? $item['status'] ?? '' : '' ); + + if ( 'success' !== $status && '' !== $status ) { + $on_screen++; + } + } + + $this->assertGreaterThan( + 0, + $on_screen, + sprintf( 'Fixture check: a %s failure must actually render a warning.', $category ) + ); + + $this->assertTrue( + Admin::get_instance()->has_action_item_warnings(), + sprintf( 'A %s failure is on screen, so the review ask must stand down.', $category ) + ); + } + + // A healthy site still gets asked, so the gate is not simply stuck on. + delete_option( Client_Logger::FAILURES_OPTION ); + Admin::reset_action_items_cache(); + + $this->assertFalse( + Admin::get_instance()->has_action_item_warnings(), + 'With nothing wrong the engagement notices must still be eligible.' + ); + + remove_filter( 'pre_option_active_plugins', $quiet ); + + Helper::update_srfm_option( 'dismissed_action_items', [] ); + delete_option( Client_Logger::FAILURES_OPTION ); + Admin::reset_action_items_cache(); + } + /** * Dismissing an advisory must clear the warning state, or the engagement * notices stay suppressed forever on a site with a caching plugin. From db2f0a2f23cede193e554951ca45cb783848ff8c Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Sat, 12 Sep 2026 14:06:24 +0530 Subject: [PATCH 57/58] chore: 2.12.7 ships 12th September, and carries the security props line Release date moved from the 11th to the 12th, and the Patchstack disclosure added to the 2.12.7 changelog: * Fix: This update addressed a security bug. Props to nh4tvd from Patchstack for reporting it responsibly to our team. Worded and positioned to match 2.12.6's own props line -- last in the list, after the Fix entries -- so the changelog reads consistently release to release. This one names the individual reporter as well as Patchstack, which 2.12.6's did not. README.md is generated from readme.txt by `grunt readme`, so it is regenerated here rather than hand-edited. The regeneration introduced no churn beyond the three changed lines. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 3 ++- readme.txt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53084c108..59f74f8e1 100644 --- a/README.md +++ b/README.md @@ -426,7 +426,7 @@ Visit the [SureForms features page](https://sureforms.com/features/?utm_source=w You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to validate, triage, and handle security reports. ## Changelog ## -### 2.12.7 - 11th September 2026 ### +### 2.12.7 - 12th September 2026 ### * Improvement: Submission failure notices now offer View details, so you can read and copy the full diagnostics before contacting support. * Improvement: The Form Checks panel now appears only when something genuinely needs attention, with clearer wording, keeping the sidebar focused. * Fix: After-submission actions now complete reliably when a form redirects on success. @@ -434,6 +434,7 @@ You can report security issues through our [Bug Bounty Program](https://brainsto * Fix: Form submissions no longer drop fields when a caching plugin serves an older copy of the page, so every submitted field is saved to the entry. * Fix: The conversion rate no longer counts submissions made by site editors while previewing a form. * Fix: The Edit Form icon is no longer oversized in Divi. +* Fix: This update addressed a security bug. Props to nh4tvd from Patchstack for reporting it responsibly to our team. ### 2.12.6 - 2nd September 2026 ### * New: Added an Enable Logs option that records form submission failures to a downloadable file, making it easier to diagnose issues on a live site. * New: Added Views and Conversion Rate columns to the Forms list, so you can see how each form is performing at a glance. diff --git a/readme.txt b/readme.txt index 76d18d5a7..6a97559f9 100644 --- a/readme.txt +++ b/readme.txt @@ -426,7 +426,7 @@ Visit the [SureForms features page](https://sureforms.com/features/?utm_source=w You can report security issues through our [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/). We collaborate with Patchstack to validate, triage, and handle security reports. == Changelog == -= 2.12.7 - 11th September 2026 = += 2.12.7 - 12th September 2026 = * Improvement: Submission failure notices now offer View details, so you can read and copy the full diagnostics before contacting support. * Improvement: The Form Checks panel now appears only when something genuinely needs attention, with clearer wording, keeping the sidebar focused. * Fix: After-submission actions now complete reliably when a form redirects on success. @@ -434,6 +434,7 @@ You can report security issues through our [Bug Bounty Program](https://brainsto * Fix: Form submissions no longer drop fields when a caching plugin serves an older copy of the page, so every submitted field is saved to the entry. * Fix: The conversion rate no longer counts submissions made by site editors while previewing a form. * Fix: The Edit Form icon is no longer oversized in Divi. +* Fix: This update addressed a security bug. Props to nh4tvd from Patchstack for reporting it responsibly to our team. = 2.12.6 - 2nd September 2026 = * New: Added an Enable Logs option that records form submission failures to a downloadable file, making it easier to diagnose issues on a live site. * New: Added Views and Conversion Rate columns to the Forms list, so you can see how each form is performing at a glance. From 127ef25b62500c14a7d8865c695dc56207418447 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Mon, 14 Sep 2026 10:21:41 +0530 Subject: [PATCH 58/58] chore: strip internal-only paths from public mirror sync --- .claude/agents/code-simplifier.md | 48 - .claude/agents/srfm-block-checker.md | 60 - .claude/commands/sureforms:investigate.md | 67 - .claude/commands/sureforms:optimize-readme.md | 170 --- .claude/commands/sureforms:sync-public.md | 299 ---- .claude/commands/sureforms:update-readme.md | 327 ----- .claude/commands/sureforms:version-bump.md | 204 --- .claude/commands/sync-release-branches.md | 96 -- .claude/settings.json | 73 - .../workflows/push-asset-readme-update.yml | 28 - .github/workflows/push-to-deploy.yml | 31 - .github/workflows/release-pr-template.yml | 41 - .github/workflows/release-tag-draft.yml | 83 -- .github/workflows/update-translations.yml | 205 --- .scripts/git-hooks/pre-commit | 106 -- ARCHITECTURE.md | 355 ----- CLAUDE.md | 180 --- COMPREHENSIVE_ANALYSIS.md | 304 ----- PRODUCT_ANALYSIS.md | 251 ---- TECHNICAL_OVERVIEW.md | 393 ------ bin/build-zip.sh | 47 - bin/checkout-and-build | 21 - bin/i18n.sh | 75 -- docs/01-getting-started/GETTING-STARTED.md | 647 --------- docs/04-backend/DATABASE-SCHEMA.md | 962 ------------- docs/04-backend/HOOKS-REFERENCE.md | 1055 --------------- docs/05-frontend/FRONTEND-GUIDE.md | 1197 ----------------- docs/FORM_MIGRATION_OVERVIEW.md | 77 -- docs/FORM_MIGRATION_PLAN.md | 458 ------- docs/wiki/AI-Form-Builder.md | 103 -- docs/wiki/Admin-Dashboard.md | 121 -- docs/wiki/Architecture-Overview.md | 294 ---- docs/wiki/Block-Editor-Controls.md | 109 -- docs/wiki/Changelog.md | 39 - docs/wiki/Contributing-Guide.md | 205 --- docs/wiki/Data-Export-Import.md | 120 -- docs/wiki/Database-Schema.md | 173 --- docs/wiki/Deployment-Guide.md | 109 -- docs/wiki/Email-Notifications.md | 86 -- docs/wiki/Environment-Configuration.md | 197 --- docs/wiki/Form-Fields-Architecture.md | 137 -- docs/wiki/Form-Submission-Flow.md | 191 --- docs/wiki/Frontend-Assets.md | 122 -- docs/wiki/Getting-Started.md | 149 -- docs/wiki/Gutenberg-Blocks.md | 129 -- docs/wiki/Home.md | 58 - docs/wiki/Payment-Integration.md | 127 -- docs/wiki/REST-API-Reference.md | 114 -- docs/wiki/State-Management.md | 109 -- docs/wiki/Testing-Guide.md | 172 --- docs/wiki/Troubleshooting-FAQ.md | 232 ---- docs/wiki/Using-WPML-With-SureForms.md | 243 ---- docs/wiki/WordPress-Hooks-Reference.md | 152 --- docs/wiki/_Footer.md | 3 - docs/wiki/_Sidebar.md | 36 - inc/abilities/CLAUDE.md | 66 - internal-docs/README.md | 160 --- internal-docs/ai-agent-guide.md | 459 ------- internal-docs/apis.md | 418 ------ internal-docs/architecture.md | 329 ----- internal-docs/codebase-map.md | 303 ----- internal-docs/coding-standards.md | 312 ----- internal-docs/faq.md | 1003 -------------- internal-docs/glossary.md | 723 ---------- internal-docs/maintenance.md | 842 ------------ internal-docs/onboarding.md | 1086 --------------- internal-docs/product-vision.md | 688 ---------- internal-docs/troubleshooting.md | 1031 -------------- internal-docs/ui-and-copy.md | 1077 --------------- tests/play/specs/CLAUDE.md | 324 ----- tests/play/specs/TODO.md | 244 ---- 71 files changed, 20455 deletions(-) delete mode 100644 .claude/agents/code-simplifier.md delete mode 100644 .claude/agents/srfm-block-checker.md delete mode 100644 .claude/commands/sureforms:investigate.md delete mode 100644 .claude/commands/sureforms:optimize-readme.md delete mode 100644 .claude/commands/sureforms:sync-public.md delete mode 100644 .claude/commands/sureforms:update-readme.md delete mode 100644 .claude/commands/sureforms:version-bump.md delete mode 100644 .claude/commands/sync-release-branches.md delete mode 100644 .claude/settings.json delete mode 100644 .github/workflows/push-asset-readme-update.yml delete mode 100644 .github/workflows/push-to-deploy.yml delete mode 100644 .github/workflows/release-pr-template.yml delete mode 100644 .github/workflows/release-tag-draft.yml delete mode 100644 .github/workflows/update-translations.yml delete mode 100644 .scripts/git-hooks/pre-commit delete mode 100644 ARCHITECTURE.md delete mode 100644 CLAUDE.md delete mode 100644 COMPREHENSIVE_ANALYSIS.md delete mode 100644 PRODUCT_ANALYSIS.md delete mode 100644 TECHNICAL_OVERVIEW.md delete mode 100644 bin/build-zip.sh delete mode 100755 bin/checkout-and-build delete mode 100644 bin/i18n.sh delete mode 100644 docs/01-getting-started/GETTING-STARTED.md delete mode 100644 docs/04-backend/DATABASE-SCHEMA.md delete mode 100644 docs/04-backend/HOOKS-REFERENCE.md delete mode 100644 docs/05-frontend/FRONTEND-GUIDE.md delete mode 100644 docs/FORM_MIGRATION_OVERVIEW.md delete mode 100644 docs/FORM_MIGRATION_PLAN.md delete mode 100644 docs/wiki/AI-Form-Builder.md delete mode 100644 docs/wiki/Admin-Dashboard.md delete mode 100644 docs/wiki/Architecture-Overview.md delete mode 100644 docs/wiki/Block-Editor-Controls.md delete mode 100644 docs/wiki/Changelog.md delete mode 100644 docs/wiki/Contributing-Guide.md delete mode 100644 docs/wiki/Data-Export-Import.md delete mode 100644 docs/wiki/Database-Schema.md delete mode 100644 docs/wiki/Deployment-Guide.md delete mode 100644 docs/wiki/Email-Notifications.md delete mode 100644 docs/wiki/Environment-Configuration.md delete mode 100644 docs/wiki/Form-Fields-Architecture.md delete mode 100644 docs/wiki/Form-Submission-Flow.md delete mode 100644 docs/wiki/Frontend-Assets.md delete mode 100644 docs/wiki/Getting-Started.md delete mode 100644 docs/wiki/Gutenberg-Blocks.md delete mode 100644 docs/wiki/Home.md delete mode 100644 docs/wiki/Payment-Integration.md delete mode 100644 docs/wiki/REST-API-Reference.md delete mode 100644 docs/wiki/State-Management.md delete mode 100644 docs/wiki/Testing-Guide.md delete mode 100644 docs/wiki/Troubleshooting-FAQ.md delete mode 100644 docs/wiki/Using-WPML-With-SureForms.md delete mode 100644 docs/wiki/WordPress-Hooks-Reference.md delete mode 100644 docs/wiki/_Footer.md delete mode 100644 docs/wiki/_Sidebar.md delete mode 100644 inc/abilities/CLAUDE.md delete mode 100644 internal-docs/README.md delete mode 100644 internal-docs/ai-agent-guide.md delete mode 100644 internal-docs/apis.md delete mode 100644 internal-docs/architecture.md delete mode 100644 internal-docs/codebase-map.md delete mode 100644 internal-docs/coding-standards.md delete mode 100644 internal-docs/faq.md delete mode 100644 internal-docs/glossary.md delete mode 100644 internal-docs/maintenance.md delete mode 100644 internal-docs/onboarding.md delete mode 100644 internal-docs/product-vision.md delete mode 100644 internal-docs/troubleshooting.md delete mode 100644 internal-docs/ui-and-copy.md delete mode 100644 tests/play/specs/CLAUDE.md delete mode 100644 tests/play/specs/TODO.md diff --git a/.claude/agents/code-simplifier.md b/.claude/agents/code-simplifier.md deleted file mode 100644 index 2dfa2039d..000000000 --- a/.claude/agents/code-simplifier.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: code-simplifier -description: Reviews recent code changes and suggests simplifications — reduces complexity, removes dead code, flattens nesting, applies DRY. -tools: Read, Glob, Grep -model: sonnet ---- - -You are a code simplification expert for a WordPress/React plugin. - -## Your Task - -Review the recently changed files and suggest concrete simplifications. Do NOT make changes — only report findings. - -## What to Look For - -### PHP -- Duplicated logic that can be extracted into a shared method -- Deeply nested conditionals that can be flattened with early returns -- Dead code (unused variables, unreachable branches, commented-out code) -- Verbose array/string operations that WordPress has helpers for -- Overly complex conditionals that can be simplified - -### JavaScript/React -- Components doing too much — can they be split? -- Duplicated JSX or logic across components -- State that could be derived instead of stored -- useEffect with dependencies that could be simplified -- Unnecessary re-renders from inline objects/functions in props - -## Output Format - -For each finding: - -``` -### file-path:line-number -**Type:** DRY | Complexity | Dead Code | Optimization -**Current:** Brief description of current code -**Suggestion:** How to simplify (1-2 sentences) -**Impact:** Low | Medium | High -``` - -End with a summary: total findings by type and priority. - -## Rules -- Only report findings with clear, concrete improvements -- No subjective style preferences — only measurable simplifications -- Skip third-party code (`inc/lib/`, `node_modules/`, `vendor/`) -- Be precise with line numbers diff --git a/.claude/agents/srfm-block-checker.md b/.claude/agents/srfm-block-checker.md deleted file mode 100644 index ec7bbf8b9..000000000 --- a/.claude/agents/srfm-block-checker.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: srfm-block-checker -description: Audits SureForms block render files for security issues — checks output escaping, input sanitization, ABSPATH guards, and WPCS compliance. -tools: Read, Glob, Grep -model: sonnet ---- - -You are a WordPress security reviewer specialized in Gutenberg block rendering. - -## Your Task - -Audit SureForms block PHP files for common security and quality issues. - -## Files to Check - -1. All block render files: `inc/blocks/*/block.php` -2. All field markup files: `inc/fields/*.php` -3. The base block class: `inc/blocks/base.php` - -## Checklist for Each File - -For every file, check and report: - -### Security -- [ ] **ABSPATH guard** — File starts with `if ( ! defined( 'ABSPATH' ) ) { exit; }` -- [ ] **Output escaping** — All `echo` statements use `esc_html()`, `esc_attr()`, `wp_kses_post()`, or `wp_kses()`. Flag any raw `echo $var` without escaping (note: `// phpcs:ignore` comments may indicate intentional bypass — flag these for review) -- [ ] **Attribute sanitization** — Block attributes accessed from `$attributes` are sanitized before use -- [ ] **No direct `$_GET`/`$_POST`/`$_REQUEST`** — Block render should never read superglobals directly - -### Quality -- [ ] **`@since` tag present** — Class and methods have `@since` docblock tags -- [ ] **Namespace follows convention** — Uses `SRFM\Inc\Blocks\{BlockName}` pattern -- [ ] **Extends Base** — Block class extends `SRFM\Inc\Blocks\Base` -- [ ] **Return type** — `render()` method returns `string|bool` - -## Output Format - -For each file, output: - -``` -### block-name/block.php -- PASS: ABSPATH guard present -- WARN: Line 33 — raw echo with phpcs:ignore, verify markup() output is pre-escaped -- PASS: No superglobals used -- FAIL: Missing @since tag on render() method -``` - -End with a summary table: - -| Block | Security | Quality | Issues | -|-------|----------|---------|--------| -| input | WARN | FAIL | 2 | -| email | PASS | PASS | 0 | - -## Important - -- Be precise — include line numbers for every issue -- Distinguish FAIL (must fix) from WARN (review needed) -- Do NOT suggest code changes — only report findings -- Check ALL block files, not just a sample diff --git a/.claude/commands/sureforms:investigate.md b/.claude/commands/sureforms:investigate.md deleted file mode 100644 index d171756ec..000000000 --- a/.claude/commands/sureforms:investigate.md +++ /dev/null @@ -1,67 +0,0 @@ -# Investigate Issue - -Investigate a reported issue using parallel subagents for speed. - -## Input - -$ARGUMENTS — GitHub issue URL, Jira ticket ID, or a description of the issue. - -If no arguments provided, stop and ask: "Please provide an issue URL, Jira ticket ID, or describe the issue." - -## Step 1 — Read the Issue - -- If a GitHub URL: use `gh issue view` to read the issue -- If a Jira ticket: use the Jira MCP tools to read the ticket -- If a description: use the provided text directly - -Extract: title, description, steps to reproduce, expected vs actual behavior, screenshots, labels. - -## Step 2 — Classify - -Determine the type: -- **Bug** — something is broken, unexpected behavior, error, regression -- **Feature** — new functionality requested -- **Improvement** — enhancement to existing functionality - -Tell the user: "This looks like a **[type]**. Proceeding with [investigation/planning]." - -## Step 3 — Investigate with Subagents - -Use subagents to parallelize the investigation: - -### For Bugs: -Launch these subagents in parallel: -1. **Code Search Agent** — Search the codebase for files related to the bug (grep for keywords, function names, error messages, related hooks/filters) -2. **Test Coverage Agent** — Check if there are existing tests covering this area, identify gaps -3. **Impact Analysis Agent** — Trace all usages of the affected code to understand blast radius - -Once all agents report back, synthesize findings: -- Present the probable root cause with file path and line number -- Show which other areas might be affected -- Suggest a fix with a clear explanation -- Ask the user: "Should I apply this fix?" -- If yes — apply the fix, run linters, and suggest running relevant tests - -### For Features: -1. Enter plan mode -2. Launch subagents in parallel: - - **Pattern Discovery Agent** — Find similar existing features in the codebase to follow the same patterns - - **Impact Analysis Agent** — Identify all files that will need changes -3. Use findings to outline implementation steps -4. Present the plan for approval - -### For Improvements: -1. Launch subagents in parallel: - - **Code Search Agent** — Read the relevant existing code - - **Impact Analysis Agent** — Trace dependencies to understand what might break -2. Suggest the minimal changes needed -3. Ask the user: "Should I implement these changes?" -4. If yes — apply changes and run linters - -## Rules -- Always use subagents for investigation to maximize speed -- Show your reasoning — which files you checked and why -- Be precise with file paths and line numbers -- For bugs, prioritize finding the root cause over applying a quick patch -- Never make changes without asking first -- After any code change, run the verification checklist from CLAUDE.md diff --git a/.claude/commands/sureforms:optimize-readme.md b/.claude/commands/sureforms:optimize-readme.md deleted file mode 100644 index d7721ed71..000000000 --- a/.claude/commands/sureforms:optimize-readme.md +++ /dev/null @@ -1,170 +0,0 @@ -# SureForms readme.txt search-ranking optimizer - -Optimize the three readme.txt header fields that WordPress.org search actually scores — the -plugin **title**, the **short description**, and the **Tags** line — so SureForms ranks better for -its target keywords, without touching anything release-managed and without breaking WordPress.org -guidelines. - -This command **only rewrites copy**. It never bumps a version, never edits `Stable tag`, and never -invents facts. - -## Arguments - -Parse from: `$ARGUMENTS` - -- `keywords` — optional, comma-separated. The search terms to optimize for, most important first. - Default (SureForms' live priorities): `form, contact form, form builder, payment form, survey`. -- `--apply` — optional. Write the changes to `readme.txt`. Without it, run in **dry-run**: print the - proposed header and the reasoning, change nothing. - -Default is dry-run. The title is the plugin's public display name, so always show the before/after -and get a "yes" from the user before writing it, even with `--apply`. - -## Working directory - -Run from the sureforms plugin root — the directory containing `sureforms.php` and `readme.txt`. - ---- - -## How WordPress.org scores a search result (the facts this command relies on) - -Source of truth — read it if a rule below is ever in doubt: -`https://github.com/WordPress/wordpress.org/blob/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-search.php` - -Final score = **where the term matches** × **six quality signals**. - -**Where the term matches (per-field weight):** - -| Field | Weight | Editable here? | -|---|---|---| -| Plugin name (a whole word) **or** slug | 5 | Title: yes. Slug (`sureforms`): **no — immutable** | -| Author name | 3 | No | -| Term as a substring inside a title word (`form` in `SureForms`, via the `title.engram` analyzer) | 2 | Indirect | -| Short description **or** Tags (`excerpt`, `description`, `tags`) | 2 | **Yes — this is the main lever** | -| Anywhere else in the readme (description body, FAQ, features) | 0.1 | Not worth optimizing | - -Search analyzers split the **slug on hyphens**, so `contact-form-7` indexes as `contact` · `form` · -`7` — each a whole-word ×5 match. `sureforms` is one indivisible token and can never produce a -standalone `form`, so it only ever earns the ×2 `title.engram` substring credit. The slug is -permanent on WordPress.org; do not plan around changing it. - -**The six quality signals (multipliers) — none live in these three fields**, so this command never -touches them. Kept here only so the analysis can explain *why* a readme edit can't move quality, and -so a stale `Tested up to` gets flagged (not changed). Verbatim `function_score` params from the -[ranking code](https://github.com/WordPress/wordpress.org/blob/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-search.php#L313-L381): - -| Signal | Config | Rule of thumb | -|---|---|---| -| Last updated (`plugin_modified`) | decay, origin today, offset 180d, scale 360d, decay 0.5 | free for 6 months; ~×0.5 at ~18 months | -| Tested up to (`tested`) | decay, origin current WP, offset 0.1, scale 0.4, decay 0.6 | half a major version behind ≈ ×0.6 | -| Active installs (`active_installs`) | `log2p`, factor 0.375, missing 1 | log-scaled; 10k→100k ≈ 100k→1M | -| Under-1M penalty | decay, filter ≤ 1,000,000, origin 1M, scale 900k, decay 0.75 | extra ~×0.85 handicap below 1M installs | -| Support resolved (`support_threads_resolved`) | `log2p`, factor 0.25, missing 0.5 | resolved **percentage** scored directly | -| Star rating (`rating`) | `sqrt`, factor 0.25, missing 2.5 | only the **average** counts, never the review count | - -**The three consequences that drive every rewrite below:** - -1. The slug is `sureforms` (one word), so a search for `form` never gets the 5-point whole-word name - credit. The closest recoverable credit is a **standalone** high-value word in the *title* - (`Contact Form Builder` → `form` is its own word), which beats the 2-point substring credit from - `SureForms` alone. -2. Score is **concentrated per field** (Elasticsearch length-normalizes each field). A shorter title - and a tight short description give each keyword a larger share; padding words dilute *every* - keyword in the field. Past a point, adding a keyword to the title *lowers* the score of the ones - already there — keyword-stuffing the title is actively counterproductive. -3. For a **multi-word** target (`contact form`, `form builder`, `payment form`), position and - adjacency matter, not just presence. The winners hold the phrase as **consecutive tokens early in - a short field** — e.g. `Contact Form` as tokens 2–3 of a 4-token title beats the same words - buried mid-way through a 14-token one. This is the single factor that lets a 20k-install, 2.7★ - plugin outrank a 500k-install, 4.9★ one for "contact form". - ---- - -## Hard constraints — never cross these - -These protect the listing from being flagged or rejected. A rewrite that violates any of them is -worse than no rewrite. - -- **Short description: 150 characters max.** WordPress.org truncates past that. Count it. -- **Tags: only the first 5 are indexed.** Extra tags are dead weight — never list more than 5. -- **No competitor trademarks as keywords.** Never add `wpforms`, `gravity forms`, `ninja forms`, - `contact form 7`, `elementor`, `jetpack`, etc. to the title, tags, or short description. This is a - direct WordPress.org guideline violation and risks removal. -- **No keyword stuffing.** Repeating a keyword doesn't stack — the field is scored once per term. - `Form Builder, Form Maker, Form Creator, Forms` is stuffing; write natural phrases a human reads. -- **Only claim real features.** Every keyword must map to a shipped capability. Survey/Quiz/Calculator - are Business-tier — keep them phrased the way the current short description does (e.g. "in SureForms - Business") so the claim stays honest. Never add a keyword for a feature SureForms doesn't have. -- **Never edit** `Stable tag`, `Requires at least`, `Requires PHP`, `Contributors`, `License`, or the - `Plugin Name:` header in `sureforms.php`. If `Tested up to` is behind the current WordPress - release, only *report* it — do not change it (it must reflect a real tested version). -- **Keep the `=== ... ===` and header syntax intact** — WordPress.org's parser is strict. - ---- - -## Steps - -### 1 — Read the current header - -Read the top block of `readme.txt` (through the short-description line). Capture the current title, -short description (with its character count), and tags. - -### 2 — Score the current fields - -For each target keyword, note where it currently lands (whole word in title = 5, substring = 2, in -short description / tags = 2, or absent). For **multi-word** keywords, also record whether the words -are adjacent and how early they sit — a phrase split apart or buried late is a weak match even if all -its words are present. Count the title's token length: a long title (say > ~9 words) is itself a -finding, because it dilutes every keyword in the ×5 field. List the gaps — a target keyword at 0.1 -weight, absent, non-adjacent, or stranded at the end of a long title is the opportunity. - -### 3 — Propose the rewrite - -Produce candidate values for the three fields, applying the two consequences above: - -- **Title** — front-load the highest-value keywords as **standalone words**, drop filler, keep it - reading like a real product name. Aim for ≤ ~9 words. Apply consequences 2 and 3: - - Put the **single most important target early** and as its own word (`Form Builder`, not only - `SureForms`). - - For a multi-word target, keep its words **adjacent and near the front** — e.g. lead with - `SureForms – Contact Form Builder` so `Contact Form` sits in tokens 2–3, then append the - lower-priority terms. Do not scatter the phrase across the title or repeat it. - - Stop adding keywords once the title reads like stuffing — each extra token shrinks the share of - the ones already there. Example shape: - `SureForms – Contact Form Builder, Payment Form, Survey, Quiz & Calculator`. -- **Short description** — one natural sentence, ≤ 150 chars, carrying the top 2–3 keywords that the - title couldn't. Report the exact character count. -- **Tags** — exactly the 5 highest-value distinct keywords, no overlap-for-overlap's-sake, no - trademarks. - -For **each** field, give a one-line rationale tied to the scoring model (which keyword moved from -which weight to which, or why a word was cut). - -### 4 — Show before/after - -Print a compact before → after for all three fields, the character count for the short description, -and any constraint check that mattered (e.g. "tags trimmed to 5", "short desc = 143/150"). If -`Tested up to` looks stale versus the current WordPress release, note it here as a separate -recommendation — do not fold it into the edit. - -### 5 — Apply (only with `--apply` and after title confirmation) - -On confirmation, use `Edit` to replace exactly the title line, the short-description line, and the -`Tags:` line in `readme.txt`. Change nothing else. Re-read the three lines and confirm the header -still parses (syntax intact, short desc ≤ 150, ≤ 5 tags). - -### 6 — Report - -Summarize what changed and what was deliberately left alone (versions, quality signals). Remind the -user that title/short-description/tags are also worth mirroring wherever the marketing site controls -the same listing, and that the quality-signal wins (resolving support threads, crossing 1M installs, -keeping `Tested up to` current) live outside this file. - ---- - -## Notes - -- Dry-run first. Only pass `--apply` once the copy reads well to a human — search reads it, but so do - buyers. -- This is intentionally a copy tool. If a rewrite ever tempts you to change a version or a quality - signal to "help the score", stop — that's out of scope and, for `Tested up to`, dishonest. diff --git a/.claude/commands/sureforms:sync-public.md b/.claude/commands/sureforms:sync-public.md deleted file mode 100644 index 08f2bba4c..000000000 --- a/.claude/commands/sureforms:sync-public.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -allowed-tools: Bash(git:*), Bash(gh:*), Bash(rm:*), Bash(rmdir:*), Bash(mktemp:*), Bash(echo:*), Bash(cd:*), Bash(test:*) -description: Sync private master to the sureforms-public mirror, stripping internal-only paths in one commit ---- - -# SureForms — Sync to Public Mirror - -Sync `brainstormforce/sureforms@master` (private, this repo) to `brainstormforce/sureforms-public@master` (public WordPress.org-facing mirror), stripping internal-only paths. The sync branch is bulk re-signed (verified signatures) and capped with a merge commit against the mirror so the public PR diff stays clean. - -This skill is the **only sanctioned way** to sync the public mirror. Running raw `git push mirror …` skips the strip and re-leaks internal artifacts. - -## Stripped paths (single source of truth) - -Edit this list when the leak surface changes — there is no other place to update. - -``` -.claude -.scripts/git-hooks -internal-docs -docs -CLAUDE.md # ALL CLAUDE.md at ANY depth (root + nested, e.g. inc/abilities/, tests/play/specs/) — stripped via `git ls-files '*CLAUDE.md'` (matches the root file too), NOT just the root path -ARCHITECTURE.md -COMPREHENSIVE_ANALYSIS.md -PRODUCT_ANALYSIS.md -TECHNICAL_OVERVIEW.md -tests/play/specs/TODO.md -.github/workflows/push-to-deploy.yml -.github/workflows/push-asset-readme-update.yml -.github/workflows/release-tag-draft.yml -.github/workflows/update-translations.yml -.github/workflows/release-pr-template.yml -bin/build-zip.sh -bin/checkout-and-build -bin/i18n.sh -``` - -These paths must NOT appear on the public mirror. They legitimately exist on private `master` and stay there (internal release CI, AI tooling, internal team wiki, internal dev docs under `docs/`). - -> **README.md is the ONLY markdown doc that should appear in the public PR diff.** After stripping, sanity-check with `git ls-files '*.md'` — anything beyond `README.md`, third-party `inc/lib/**` readmes, `modules/gutenberg/readme.md`, and `tests/play/README.md` is a likely leak. (A prior sync leaked `inc/abilities/CLAUDE.md`, `tests/play/specs/CLAUDE.md`, and the whole `docs/` tree because the strip matched only the root `CLAUDE.md`.) - -## Preconditions - -Working directory: any worktree of this repo. - -Required remotes: -- `origin` → `https://github.com/brainstormforce/sureforms` (private) -- public-mirror remote → `https://github.com/brainstormforce/sureforms-public` (public). May be named `mirror` **or** `public` — detect with `git remote -v` and use whichever points at `sureforms-public`. Examples below use `$MIRROR`. - -Verify with `git remote -v`. Abort if neither a `mirror` nor `public` remote points at `sureforms-public`. - -### ⚠️ `sureforms-public` ruleset constraints (READ FIRST) - -The public repo enforces a branch ruleset (since 2026-06-01) that the previous version of this skill violated. This flow is built around them: - -1. **Every commit needs a *verified* signature.** Signing is not enough — GitHub must mark it **Verified**, which requires BOTH the signing key registered on the pushing account as a **Signing Key** (an *Authentication* key does NOT count) AND the **committer email** verified on that account. **The skill uses the identity of whoever runs it** — your local `git config user.name` / `user.email` / `user.signingkey`. Before running, confirm: your `user.signingkey` points at an SSH public-key file (`*.pub`, since `gpg.format=ssh`) that is registered as a **Signing Key** on *your* GitHub account, and your `user.email` is a **verified** email on that same account. Hardware-backed keys (e.g. Secretive) verify fine but prompt for a tap **per commit**, so they cannot bulk-sign ~150 commits — use an on-disk passphrase-less key for the bulk re-sign. -2. **`sync/master` is force-push protected.** Push each sync to a **fresh** branch `sync/master-` (`-v2`, `-v3`… if re-pushing the same day — reusing a branch makes the ruleset re-evaluate the mirror's own unsigned ancestors and reject). -3. **Claude's auto-mode guardrail blocks the agent from pushing to the public mirror.** The agent prepares everything and hands the user the exact `git push` to run themselves. `gh pr` / `gh api` ARE agent-allowed. - -## Instructions - -Follow steps sequentially. If any step fails, jump to **Error Recovery**. - -### Step 1: Record current state - -```bash -ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) -HAD_STASH=false -if [ -n "$(git status --porcelain)" ]; then - git stash push -m "sureforms-sync-public auto-stash" - HAD_STASH=true -fi -``` - -### Step 2: Detect the mirror remote + signing identity, then fetch - -Detect the public-mirror remote (named `mirror` or `public`) once, and capture the running user's signing identity. These vars are used throughout: - -```bash -MIRROR=$(git remote -v | awk '/sureforms-public(\.git)?[[:space:]]/{print $1; exit}') -[ -n "$MIRROR" ] || { echo "No remote points at sureforms-public — add one and retry"; exit 1; } - -# Identity of whoever runs the skill (must satisfy the ruleset — see Preconditions) -SIGN_NAME=$(git config user.name) -SIGN_EMAIL=$(git config user.email) -SIGN_KEY=$(git config user.signingkey) # expected: path to an SSH *.pub registered as a Signing Key -[ -n "$SIGN_NAME" ] && [ -n "$SIGN_EMAIL" ] && [ -n "$SIGN_KEY" ] || { echo "git user.name/user.email/user.signingkey must all be set"; exit 1; } - -git fetch origin master -git fetch "$MIRROR" master sync/master 2>/dev/null || git fetch "$MIRROR" master -``` - -### Step 3: Detect no-op - -```bash -PRIVATE_TIP=$(git rev-parse origin/master) -PUBLIC_TIP=$(git rev-parse "$MIRROR/master") -``` - -If `PRIVATE_TIP` == `PUBLIC_TIP`, report "Public mirror is already up to date with private master — nothing to sync." Run **Step 8 cleanup** and exit. - -Capture the upstream commit range and count for reporting: - -```bash -COMMIT_COUNT=$(git rev-list --count "$PUBLIC_TIP..$PRIVATE_TIP") -``` - -### Step 4: Stage cleaned tree on a temp worktree - -Use a temp worktree so the developer's current working tree is never modified. - -```bash -WORKTREE=$(mktemp -d)/srfm-sync-public -git worktree add "$WORKTREE" "$PRIVATE_TIP" -cd "$WORKTREE" -``` - -### Step 5: Strip internal paths idempotently - -For each path in the **Stripped paths** list above, only act if it exists: - -```bash -for p in \ - .claude \ - .scripts/git-hooks \ - internal-docs \ - docs \ - ARCHITECTURE.md \ - COMPREHENSIVE_ANALYSIS.md \ - PRODUCT_ANALYSIS.md \ - TECHNICAL_OVERVIEW.md \ - tests/play/specs/TODO.md \ - .github/workflows/push-to-deploy.yml \ - .github/workflows/push-asset-readme-update.yml \ - .github/workflows/release-tag-draft.yml \ - .github/workflows/update-translations.yml \ - .github/workflows/release-pr-template.yml \ - bin/build-zip.sh \ - bin/checkout-and-build \ - bin/i18n.sh \ -; do - if [ -e "$p" ]; then - git rm -r --quiet "$p" - fi -done - -# Strip ALL CLAUDE.md at any depth (root + nested) — NOT just the root path. -# No pipe-to-while: a subshell would swallow a failing `git rm`. These paths never -# contain spaces, so word-splitting the list is safe. -CLAUDE_FILES=$(git ls-files '*CLAUDE.md') -if [ -n "$CLAUDE_FILES" ]; then - git rm --quiet $CLAUDE_FILES -fi - -# Remove now-empty .scripts directory if applicable -if [ -d .scripts ] && [ -z "$(ls -A .scripts 2>/dev/null)" ]; then - rmdir .scripts -fi - -# Sanity check (FAIL-CLOSED): README.md must be the ONLY non-third-party markdown -# doc left. This control already leaked once — a detected anomaly must HALT the -# sync, not just warn. The check runs in the temp worktree before any push, so -# aborting here means nothing internal reaches the mirror. -LEAKS=$(git ls-files '*.md' | grep -vE '^(README\.md|inc/lib/.*|modules/gutenberg/readme\.md|tests/play/README\.md|inc/abilities/ABILITIES\.md)$') || true -if [ -n "$LEAKS" ]; then - printf 'ABORT: unexpected markdown would be published:\n%s\n' "$LEAKS" - exit 1 -fi -echo "OK: only README.md + known readmes" -``` - -> If the sanity check aborts (`exit 1`), treat it as a failed step: jump to **Error Recovery** to tear down the temp worktree and restore the developer's branch/stash before investigating which path needs adding to the **Stripped paths** list. - -### Step 6: Commit the strip, then bulk re-sign ALL commits - -First commit the strip (committer is the running user — must be the verified email, see ruleset constraints): - -```bash -STRIPPED=false -if ! git diff --cached --quiet; then - STRIPPED=true - STRIPPED_PATHS=$(git diff --cached --name-only | tr '\n' ' ') - git commit -m "chore: strip internal-only paths from public mirror sync" -fi -``` - -Then re-sign **every** commit new to the mirror so they all carry verified signatures. Use `filter-branch` (it reuses each commit's tree + parents and only adds a signature → zero conflicts; `rebase --rebase-merges` re-merges and hits conflicts): - -> **Note:** this rewrites the **committer** of all re-signed commits to `$SIGN_EMAIL` (the original **author** of each commit is preserved). That committer/attribution change on the mirror is intentional — it's what lets the running user's registered key produce verified signatures. - -```bash -BASE=$(git merge-base "$MIRROR/master" "$PRIVATE_TIP") -FILTER_BRANCH_SQUELCH_WARNING=1 git \ - -c gpg.format=ssh -c user.signingkey="$SIGN_KEY" \ - filter-branch -f \ - --env-filter "export GIT_COMMITTER_NAME=\"$SIGN_NAME\"; export GIT_COMMITTER_EMAIL=\"$SIGN_EMAIL\"" \ - --commit-filter "git -c gpg.format=ssh -c user.signingkey=\"$SIGN_KEY\" commit-tree -S \"\$@\"" \ - -- "$BASE..HEAD" -``` - -### Step 7: Cap with a merge commit on the mirror, push to a fresh branch - -Cap the branch with a merge commit whose **first parent is `$MIRROR/master`**. This makes GitHub diff the PR against `$MIRROR/master` (which has no internal files), so the public PR shows **only real upstream changes — no internal-file deletions**. - -```bash -SIGNED_TIP=$(git rev-parse HEAD) -MERGE=$(git -c gpg.format=ssh -c user.signingkey="$SIGN_KEY" \ - -c user.name="$SIGN_NAME" -c user.email="$SIGN_EMAIL" \ - commit-tree -S "$(git rev-parse HEAD^{tree})" -p "$MIRROR/master" -p "$SIGNED_TIP" \ - -m "Sync master from upstream") -BRANCH="sync/master-$(date +%Y%m%d)" -git branch -f "$BRANCH" "$MERGE" -``` - -Then **the user runs the push** (the agent's push to the public mirror is blocked by Claude's guardrail). Push to a **fresh** branch — never `sync/master` (force-protected), and never reuse an existing branch (its prior value makes the ruleset re-evaluate the mirror's unsigned ancestors): - -```bash -git push "$MIRROR" "$BRANCH:refs/heads/$BRANCH" -``` - -If the push is rejected for "verified signatures", isolate before re-signing ~150 commits: push a single test commit signed the same way to a throwaway branch and confirm it's accepted (and `gh api .../commits/ -q .commit.verification.verified` is `true`). - -### Step 8: Open or update the sync PR - -Check whether an open PR already exists for the current sync branch → `master` on `brainstormforce/sureforms-public`: - -```bash -EXISTING_PR=$(gh pr list -R brainstormforce/sureforms-public \ - --base master --head "$BRANCH" --state open \ - --json number -q '.[0].number') -``` - -- **If `EXISTING_PR` is non-empty:** comment a refresh note: - - ```bash - gh pr comment "$EXISTING_PR" -R brainstormforce/sureforms-public \ - --body "Refreshed: synced \`$COMMIT_COUNT\` commit(s) from private master ($PUBLIC_TIP..$PRIVATE_TIP). Strip applied: $STRIPPED." - ``` - -- **If `EXISTING_PR` is empty:** open a new PR. - - Title: `Sync master from upstream` - - Body: upstream commit range, count, strip status, and a "## Highlights" section (`git log --oneline "$PUBLIC_TIP..$PRIVATE_TIP"`). Note that the diff is computed against `public/master` (merge-cap) so no internal files appear. - - ```bash - gh pr create -R brainstormforce/sureforms-public \ - --base master --head "$BRANCH" \ - --title "Sync master from upstream" \ - --body "$PR_BODY" - ``` - - Close/delete any superseded sync branch + PR: - - ```bash - gh pr close -R brainstormforce/sureforms-public - gh api -X DELETE repos/brainstormforce/sureforms-public/git/refs/heads/ - ``` - -### Step 9: Tear down the temp worktree - -```bash -cd - -git worktree remove --force "$WORKTREE" -``` - -### Step 10: Restore developer state - -```bash -git checkout "$ORIGINAL_BRANCH" -if [ "$HAD_STASH" = "true" ]; then - git stash pop -fi -``` - -## Output to user - -Report at the end: - -- Upstream range: `..` (`$COMMIT_COUNT` commits) -- Whether the strip commit was created (`$STRIPPED`); if true, list the stripped paths -- PR URL — new or existing - -## Error Recovery - -If any step fails: - -1. **`cd` back to the original repo dir** if currently inside `$WORKTREE`. -2. **Remove temp worktree** if it was created: `git worktree remove --force "$WORKTREE"` (ignore errors). -3. **Restore branch and stash** as in Step 10. -4. Surface the failing command and its error to the user — do not retry blindly. - -Common failure modes: - -- **Push rejected, "Commits must have verified signatures"** — a commit is signed by an unregistered key or committed under an unverified email. Confirm your `git config user.signingkey` (the `*.pub`) is registered as a **Signing Key** (not Authentication) on your GitHub account and your `git config user.email` is a verified email there. Test with a single throwaway commit before re-signing ~150. -- **Push rejected, "Cannot force-push to this branch"** — you targeted `sync/master`; use a fresh `sync/master-` branch instead. -- **Push rejected naming a few commits already on `master`** — you reused/updated an existing branch; push to a brand-new branch name so the ruleset only evaluates the new commits. -- **Agent push "denied by auto mode classifier"** — expected; the user must run the `git push` to the mirror themselves. -- **`gh pr create` fails with "PR already exists"** — Step 8 detection missed a draft PR. Re-run Step 8 with `--state all` and reuse / reopen the existing PR. -- **No mirror remote** — add it and re-run: `git remote add public https://github.com/brainstormforce/sureforms-public`. diff --git a/.claude/commands/sureforms:update-readme.md b/.claude/commands/sureforms:update-readme.md deleted file mode 100644 index b3d9d8a6f..000000000 --- a/.claude/commands/sureforms:update-readme.md +++ /dev/null @@ -1,327 +0,0 @@ ---- -description: Audit and update the WordPress.org readme.txt — analyze new features, research competitors, draft changes, and create a PR ---- - -# SureForms readme.txt Update - -Comprehensive audit and update of the `readme.txt` (and `README.md`) for the WordPress.org listing. - -## Arguments - -Parse from: $ARGUMENTS - -Expected format: `[version] [--full]` - -- `version` — optional. Baseline version to analyze changes since (e.g., `2.5.0`). If not provided, auto-detect from the most recent `= X.X.X` changelog header in `readme.txt`. -- `--full` — optional flag. Perform a full rewrite of all sections instead of an incremental update. - -If no arguments are given, auto-detect the baseline and proceed with incremental mode. - -## Working Directory - -All commands and file paths are relative to the sureforms plugin root — the directory containing `sureforms.php`. - ---- - -## Phase 1: Analyze What's New - -### Step 1.1 — Determine Baseline Version - -- If `version` argument was provided, use that as the baseline. -- Otherwise, read the `== Changelog ==` section of `readme.txt`, extract the most recent version header (`= X.X.X - ... =`), and use that version as the baseline. -- Resolve the git tag: `v` (e.g., `v2.5.0`). Verify it exists with `git tag -l "v"`. -- If the tag does not exist, list available tags (`git tag --sort=-version:refname | head -10`) and ask the user to pick one. - -### Step 1.2 — Gather Changes Since Baseline - -Launch **3 Explore subagents in parallel** (single message, 3 Agent tool calls): - -**Agent 1 — Git History Analysis:** -- Run `git log v..HEAD --oneline` to get all commits since the baseline. -- Categorize commits: new features, improvements, bug fixes, refactors. -- Focus on commits touching: `inc/blocks/`, `inc/fields/`, `inc/payments/`, `inc/ai-form-builder/`, `modules/`, `src/blocks/`, `inc/page-builders/`. -- Report: list of significant user-facing changes with commit hashes and affected areas. - -**Agent 2 — Free Codebase Feature Scan:** -- List all blocks in `inc/blocks/` — compare against the readme's "Input Fields" and feature bullets. -- Check `inc/payments/` for payment providers. -- Check `inc/ai-form-builder/` for AI feature updates. -- Check `modules/` for any new modules. -- Check `inc/global-settings/` for new settings sections. -- Check `inc/rest-api.php` for new REST endpoints. -- Check `inc/page-builders/` for page builder compatibility additions. -- Report: features present in code but missing from readme. - -**Agent 3 — Pro Codebase Feature Scan:** -- Check `../sureforms-pro/inc/pro/native-integrations/integrations/` — list ALL integration JSON files and compare against the readme's integration list. -- Check `../sureforms-pro/inc/blocks/` for pro-only blocks. -- Check `../sureforms-pro/inc/pro/` subdirectories for pro features (conversational-form, save-resume, signature, zapier, etc.). -- Check `../sureforms-pro/inc/extensions/` for conditional logic, field-restrictions, etc. -- If `../sureforms-pro/` does not exist, skip this agent and warn: "Pro plugin not found. Only free features will be analyzed." -- Report: pro features/integrations present in code but missing from readme. - -### Step 1.3 — Present Gap Analysis - -Merge all agent results and present a table: - -``` -┌─────────────────────────────────────┬────────┬──────────────────────────────┐ -│ Area │ Status │ Details │ -├─────────────────────────────────────┼────────┼──────────────────────────────┤ -│ Free field types match readme │ ✅/❌ │ Code: N, Readme: M │ -│ Pro blocks mentioned │ ✅/❌ │ Missing: ... │ -│ Integration list up to date │ ✅/❌ │ Missing: ... │ -│ Pro features all listed │ ✅/❌ │ Missing: ... │ -│ Payment providers accurate │ ✅/❌ │ │ -│ Page builder compat listed │ ✅/❌ │ │ -│ "Tested up to" WP version │ ✅/❌ │ Readme: X.X, Current: Y.Y │ -│ New features since v │ ℹ️ │ N features found │ -└─────────────────────────────────────┴────────┴──────────────────────────────┘ -``` - -Tell the user: "Phase 1 complete. Found **N gaps** and **M new features** to incorporate. Proceeding to competitor research." - ---- - -## Phase 2: Competitor & Market Research - -### Step 2.1 — Research Competitor READMEs - -Use **WebSearch** to run these searches in parallel: - -1. `"WPForms" site:wordpress.org/plugins description features` -2. `"Gravity Forms" site:wordpress.org/plugins description features` -3. `"Fluent Forms" site:wordpress.org/plugins fluentform features` -4. `"Formidable Forms" site:wordpress.org/plugins description features` -5. `"Contact Form 7" site:wordpress.org/plugins description` -6. `"WordPress.org readme.txt best practices 2025 2026"` - -For each competitor, use **WebFetch** on the top result URL if needed to extract: -- How they structure feature lists -- Marketing language and benefit-focused copy -- How they distinguish free vs pro features -- FAQ structure and common questions -- Trust signals (install counts, star ratings, testimonials) - -### Step 2.2 — Present Research Summary - -Show a comparison table: - -``` -Feature Highlighted by Competitors │ SureForms Has It? │ In Readme? -──────────────────────────────────────┼───────────────────┼────────── -Conditional Logic │ ✅ (Pro) │ ✅/❌ -File Upload │ ✅ (Pro) │ ✅/❌ -Drag & Drop Builder │ ✅ (Free) │ ✅/❌ -... │ │ -``` - -Also note: -- SEO-relevant keywords competitors use that SureForms is missing. -- Structural patterns that work well (comparison tables, benefit-focused headers). -- Any WordPress.org formatting trends. - -Tell the user: "Phase 2 complete. Ready to draft the update. Shall I proceed?" - -**⏸️ WAIT for user confirmation before proceeding to Phase 3.** - ---- - -## Phase 3: Draft the Update - -### Step 3.1 — Determine Scope - -- If `--full` flag was passed: draft ALL sections from scratch. -- Otherwise: draft only sections that need changes based on Phase 1 and Phase 2 findings. - -### Step 3.2 — Draft Each Section - -For each section that needs updating, present in this format: - -``` -━━━ SECTION: [Section Name] ━━━ - -── CURRENT ── -[Current text from readme.txt, abbreviated if very long] - -── PROPOSED ── -[New text] - -── CHANGES ── -• Added: [what was added] -• Removed: [what was removed, if any] -• Reworded: [what was reworded and why] -``` - -Sections to evaluate (in order): - -1. **Plugin header** — Stable tag, Tested up to, Tags (optimize for WordPress.org search) -2. **Short description** — Must be under 150 characters. Make it compelling and keyword-rich. -3. **Long description** — Feature bullets, AI section, payment section, field types list -4. **Premium features section** — Integration list, pro-only features, pro field types -5. **FAQ** — Add questions for new features, update outdated answers -6. **3rd Party Services** — Add any new external services used -7. **Compatibility lists** — Themes and plugins - -### Step 3.3 — WordPress.org Compliance Check - -Verify the draft against WordPress.org readme standards: -- ✅ Short description is under 150 characters -- ✅ No disallowed HTML in description -- ✅ `== Changelog ==` section exists and is properly formatted -- ✅ `== Frequently Asked Questions ==` uses correct `= Question =` format -- ✅ No broken markdown that WordPress.org would render incorrectly -- ✅ Stable tag matches the version in the main plugin file - -### Step 3.4 — Present Full Draft - -Present all drafted sections together with a summary: - -``` -📋 Readme Update Summary -───────────────────────────────────── -Sections modified: N -Lines added: +X -Lines removed: -Y -New features listed: Z -New integrations: W -New FAQ entries: Q -WP.org compliance: ✅ All checks pass -``` - -Tell the user: "Please review the draft above. You can: -1. **Approve** — proceed to apply changes and create PR -2. **Request edits** — tell me what to change -3. **Abort** — cancel the update" - -**⏸️ WAIT for explicit user approval. Do NOT proceed without it.** - ---- - -## Phase 4: User Review & Iteration - -This phase is a loop: - -1. If user **requests edits** → apply them to the draft and re-present the affected sections. Ask again for approval. -2. If user **approves** → proceed to Phase 5. -3. If user **aborts** → stop and report "Readme update cancelled. No changes were made." - ---- - -## Phase 5: Apply & Create PR - -### Step 5.1 — Create Branch - -```bash -git fetch origin -git checkout dev -git pull origin dev -git checkout -b chore/update-readme -``` - -If `chore/update-readme` already exists, append a date suffix: `chore/update-readme-YYYYMMDD`. - -### Step 5.2 — Apply Changes - -Edit `readme.txt` with the approved draft changes. Apply section by section — do NOT rewrite sections that weren't modified. - -### Step 5.3 — Regenerate README.md - -```bash -npx grunt readme -``` - -Verify `README.md` was updated. If `grunt readme` fails, check that `npm install` has been run and `grunt-wp-readme-to-markdown` is in `devDependencies`. - -### Step 5.4 — Verify - -Read the updated `readme.txt` and confirm: -- Stable tag matches current plugin version -- Tested up to is correct -- Short description is under 150 characters -- All sections are properly formatted -- No stray draft markers or debug text - -### Step 5.5 — Commit and Push - -```bash -git add readme.txt README.md -git commit -m "chore: update readme.txt for WordPress.org listing - -Update plugin description, feature lists, integrations, FAQ, -and compatibility information to reflect current capabilities. - -Co-Authored-By: Claude Opus 4.6 (1M context) " -git push -u origin chore/update-readme -``` - -### Step 5.6 — Create PR - -```bash -gh pr create \ - --title "chore: Update readme.txt for WordPress.org" \ - --base dev \ - --body "$(cat <<'EOF' -## Summary -- Updated readme.txt to reflect current plugin capabilities -- Regenerated README.md via `grunt readme` - -## What Changed -[List each section that was modified and what changed] - -## Research Notes -- Competitor READMEs reviewed: WPForms, Gravity Forms, Fluent Forms, Formidable, CF7 -- Gaps identified and addressed: [count] - -## Test Plan -- [ ] Preview on WordPress.org readme validator -- [ ] Verify all links in the readme are working -- [ ] Confirm feature claims match actual plugin capabilities -- [ ] Verify short description is under 150 characters - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - -### Step 5.7 — Final Report - -``` -✅ Readme Update Complete -───────────────────────────────────────── -✅ Phase 1: Gap analysis complete -✅ Phase 2: Competitor research complete -✅ Phase 3: Draft created -✅ Phase 4: User review passed -✅ Phase 5: Changes applied -✅ Branch: chore/update-readme (from dev) -✅ readme.txt updated -✅ README.md regenerated -✅ PR: - -Sections updated: - • [List each modified section] -``` - ---- - -## Rules - -- **NEVER modify readme.txt without user approval.** Always present the full draft first. -- **NEVER invent features.** Only document what actually exists in the codebase. -- **NEVER modify changelog entries.** The changelog is managed by `/sureforms:version-bump`. -- **ALWAYS branch from `dev`**, never from `master` or `next-release`. -- **ALWAYS run `npx grunt readme`** after modifying `readme.txt` to regenerate `README.md`. -- **ALWAYS verify feature claims** by checking the actual codebase, not just commit messages. -- **Keep the 150-character limit** for the WordPress.org short description. -- **Maintain the existing voice and tone** — professional, benefit-focused, not overly salesy. - -## Error Recovery - -| Symptom | Fix | -|---------|-----| -| `grunt readme` fails | Run `npm install` first; verify `grunt-wp-readme-to-markdown` is in `devDependencies` | -| Branch already exists | Append date suffix: `chore/update-readme-YYYYMMDD` | -| Pro plugin not found at `../sureforms-pro/` | Skip pro analysis, warn user, document only free features | -| Git tag not found | List available tags and ask user to pick one | -| WebSearch unavailable | Skip Phase 2, proceed with codebase analysis only, note the limitation | diff --git a/.claude/commands/sureforms:version-bump.md b/.claude/commands/sureforms:version-bump.md deleted file mode 100644 index 5f48a2e1f..000000000 --- a/.claude/commands/sureforms:version-bump.md +++ /dev/null @@ -1,204 +0,0 @@ -# SureForms Release Version Bump - -Automates the SureForms version bump release checklist end-to-end. - -## Arguments - -Parse from: $ARGUMENTS - -Expected format: ` [pro-version] [branch]` - -- `version` — **required**. Version number to release (e.g., `2.5.2`). If not provided, stop immediately and tell the user: `"Version number is required. Usage: /sureforms:version-bump [pro-version] [branch]"` -- `pro-version` — optional. Version to set for `SRFM_PRO_RECOMMENDED_VER`. If not provided, ask: `"What should SRFM_PRO_RECOMMENDED_VER be set to? (default: )"` and default to the core version if the user presses Enter or skips. -- `branch` — optional. Source branch to cut from — must be `dev` or `next-release`. Default: `next-release`. - -## Working Directory - -All commands and file paths are relative to the sureforms plugin root. Detect it as the directory containing `sureforms.php` — typically the current working directory or a `sureforms/` subdirectory of it. - ---- - -## Steps - -### Step 1 — Validate Arguments - -- Extract `version`, `pro-version`, and `branch` from `$ARGUMENTS`. -- Abort with usage message if `version` is missing. -- If `branch` is provided but not `dev` or `next-release`, abort: `"Invalid branch. Must be 'dev' or 'next-release'."` -- If `pro-version` was not passed as an argument, prompt the user for it (default: same as `version`). - ---- - -### Step 2 — Create Version Bump Branch - -Run from the sureforms plugin root: - -```bash -git fetch origin -git checkout -git pull origin -git checkout -b version-bump- -``` - -Confirm the new branch was created successfully before continuing. - ---- - -### Step 3 — Run Grunt Version Bump - -```bash -npx grunt version-bump --ver= -``` - -This runs `bumpup` (updates `package.json`) and `replace` (updates version strings in `readme.txt` and `sureforms.php`). - ---- - -### Step 4 — Verify package.json and package-lock.json - -- Read `package.json` → confirm `"version"` equals ``. -- Read `package-lock.json` → confirm the top-level `"version"` and `packages[""].version` both equal ``. -- If `package-lock.json` is out of sync, patch **only** the two version string values — do not alter any whitespace, indentation, line endings, or surrounding characters: - - Top-level: replace only the value in `"version": ""` → `"version": ""` - - Inside `"packages": { "": { ... } }`: replace only the value in `"version": ""` → `"version": ""` - - The resulting diff must show **exactly 2 lines changed** — only the version string on each line. No other bytes in the file should change. - - Do **not** run `npm install` — dependency conflicts can cause it to fail and it is not needed for this change. - ---- - -### Step 5 — Verify readme.txt — Stable tag - -- Read `readme.txt` and confirm `Stable tag: `. -- If incorrect, edit the line to the correct value. - ---- - -### Step 6 — Verify readme.txt — Tested up to - -- Fetch the latest WordPress version by running: - ```bash - curl -s "https://api.wordpress.org/core/version-check/1.7/" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4 - ``` -- Store the result as ``. -- Read `readme.txt` and check the current `Tested up to:` value. -- If it does **not** match ``, update the line to `Tested up to: `. -- Confirm the final value in the file after any edit. - ---- - -### Step 7 — Verify sureforms.php — Plugin Header Version - -- Read `sureforms.php` lines 1–15. -- Confirm `Version: ` in the plugin header comment. -- If incorrect, edit to the correct value. - ---- - -### Step 8 — Verify SRFM_VER - -- Confirm `define( 'SRFM_VER', '' );` in `sureforms.php`. -- If incorrect, edit to the correct value. - ---- - -### Step 9 — Verify SRFM_PRO_RECOMMENDED_VER - -- Confirm `define( 'SRFM_PRO_RECOMMENDED_VER', '' );` in `sureforms.php`. -- If incorrect, edit to the correct value. - ---- - -### Step 10 — Verify Changelog Entry in readme.txt - -- Read the `== Changelog ==` section of `readme.txt`. -- **Check for a placeholder entry** matching `= x.x.x =` (case-insensitive): - - If **found**: ask the user: `"What is the release date for ? (e.g. 19th February 2026)"`. Replace the placeholder header with `= - =`, matching the exact formatting of existing changelog entries (spaces around the `=`, date style matching what is already present). -- **Check for an entry** matching `= -`: - - If **missing** after the above: warn the user and **pause** — they must add a changelog entry for `` before the process can continue. The expected format is: - ``` - = - DD Month YYYY = - * New: ... - * Improvement: ... - * Fix: ... - ``` - - If **found**: confirm the date is present and the format matches SureForms standards. -- **Polish each bullet line** of the `` entry: - - Fix any grammatical or spelling errors. - - Rewrite passive or vague phrasing into clear, active, benefit-led language (e.g. "Fixed bug where X" → "Fix: Resolved an issue where X to ensure Y"). - - Keep the improvements **subtle and factual** — do not invent features or exaggerate. The prefix (`New:`, `Improvement:`, `Fix:`) must stay unchanged. - - Show the user a before/after diff of any lines you changed and ask for confirmation before writing. -- Then sort the bullet lines of the `` entry so they follow this group order: - 1. `* New:` lines - 2. `* Improvement:` lines - 3. `* Fix:` lines - 4. Any other prefixes come last -- Within each group, sort lines **alphabetically** (case-insensitive) by the text that follows the prefix. -- Write the re-ordered lines back to `readme.txt` if any reordering was needed, and report whether changes were made. - ---- - -### Step 11 — Trim Changelog to 3 Entries - -- Parse the `== Changelog ==` section of `readme.txt` to identify all version entries (lines matching `= X.X.X - ...=`). -- Keep only the **3 most recent** entries (the new `` entry plus the 2 entries immediately before it), including all their bullet lines. -- Remove all older entries that follow the 3rd entry, up to but not including the next `==` section header. -- Write the trimmed content back to `readme.txt`. -- Report how many entries were removed (e.g., "Removed 4 old changelog entries"). - ---- - -### Step 12 — Generate README.md - -```bash -npx grunt readme -``` - ---- - -### Step 13 — Generate POT File - -```bash -npm run makepot -``` - ---- - -### Step 14 — Commit and Open PR - -```bash -git add -A -git commit -m "Version Bump " -git push origin version-bump- -gh pr create \ - --repo brainstormforce/sureforms \ - --title "Version Bump " \ - --base \ - --label "Release PR,skip-title-check" -``` - ---- - -### Step 15 — Final Report - -Print a status summary for every step, then the PR URL: - -``` -Release Bump: -───────────────────────────────────────── -✅ Branch created: version-bump- (from ) -✅ grunt version-bump: completed -✅ package.json: -✅ package-lock.json: -✅ readme.txt Stable tag: -✅ readme.txt Tested up to: -✅ sureforms.php Version: -✅ SRFM_VER: -✅ SRFM_PRO_RECOMMENDED_VER: -✅ Changelog entry polished, confirmed, and sorted (New → Improvement → Fix, alphabetical within groups) -✅ Changelog trimmed to 3 entries (N removed) -✅ README.md generated -✅ POT file generated -✅ PR opened: -``` - -If any step failed (❌) or needs attention (⚠️), list the required actions at the bottom. diff --git a/.claude/commands/sync-release-branches.md b/.claude/commands/sync-release-branches.md deleted file mode 100644 index 210f07139..000000000 --- a/.claude/commands/sync-release-branches.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -allowed-tools: Bash(git:*) -auto-approve: true -description: Sync master into dev and dev into next-release after a release ---- - -# Sync Release Branches - -After a release, sync the three main branches: master → dev → next-release. - -## Instructions - -Follow these steps sequentially. If any step fails, jump to the Error Recovery section. - -### Step 1: Record current state - -1. Record the current branch: -```bash -git rev-parse --abbrev-ref HEAD -``` -Store this as ORIGINAL_BRANCH. - -2. Check for uncommitted changes: -```bash -git status --porcelain -``` -If there is ANY output, stash the changes: -```bash -git stash -``` -Store HAD_STASH=true. Otherwise HAD_STASH=false. - -### Step 2: Fetch latest from remote - -```bash -git fetch origin -``` - -### Step 3: Update master - -```bash -git checkout master -git pull origin master -``` - -### Step 4: Merge master into dev - -```bash -git checkout dev -git pull origin dev -git pull origin master -``` - -If there are merge conflicts, STOP. Tell the user: "Merge conflicts while merging master into dev. Please resolve them manually." List the conflicting files. - -If merge succeeds: -```bash -git push origin dev -``` - -### Step 5: Merge dev into next-release - -```bash -git checkout next-release -git pull origin next-release -git pull origin dev -``` - -If there are merge conflicts, STOP. Tell the user: "Merge conflicts while merging dev into next-release. Please resolve them manually." List the conflicting files. - -If merge succeeds: -```bash -git push origin next-release -``` - -### Step 6: Return to original branch - -```bash -git checkout ${ORIGINAL_BRANCH} -``` - -If HAD_STASH=true: -```bash -git stash pop -``` - -Report success: "Branch sync complete. master → dev → next-release are now in sync." - -## Error Recovery - -If any step fails after switching branches: -1. Switch back to original branch: `git checkout ${ORIGINAL_BRANCH}` -2. Restore stash if needed: `git stash pop` (only if HAD_STASH=true) -3. Report the error to the user. - -NEVER force-push. If a merge has conflicts, stop and let the user resolve them. diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 02b620f27..000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "if echo \"$CLAUDE_BASH_COMMAND\" | grep -q '^git commit'; then echo '--- PreCommit: Running checks ---' && composer lint 2>&1 | tail -5 && composer phpstan 2>&1 | tail -5 && composer insights 2>&1 | tail -5; fi" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "case \"$CLAUDE_FILE_PATH\" in *.php) vendor/bin/phpcbf \"$CLAUDE_FILE_PATH\" 2>/dev/null || true;; *.js|*.jsx) npx wp-scripts lint-js --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null || true;; *.scss|*.css) npx wp-scripts lint-style --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null || true;; esac" - } - ] - } - ] - }, - "permissions": { - "allow": [ - "Bash(npm run lint-js*)", - "Bash(npm run lint-css*)", - "Bash(npm run pretty*)", - "Bash(npm run build*)", - "Bash(npm run start)", - "Bash(npm run test:unit)", - "Bash(npm run test:e2e)", - "Bash(npm run play:*)", - "Bash(npm run makepot)", - "Bash(npm run i18n:*)", - "Bash(composer lint*)", - "Bash(composer format*)", - "Bash(composer test*)", - "Bash(composer phpstan*)", - "Bash(composer insights*)", - "Bash(npx playwright test*)", - "Bash(npx grunt *)", - "Bash(composer install*)", - "Bash(npm install*)", - "Bash(curl *)", - "Bash(git status*)", - "Bash(git diff*)", - "Bash(git log*)", - "Bash(git branch*)", - "Bash(git show*)", - "Bash(git stash*)", - "Bash(git checkout*)", - "Bash(git add*)", - "Bash(git commit*)", - "Bash(git merge*)", - "Bash(git rebase*)", - "Bash(git fetch*)", - "Bash(git pull*)", - "Bash(gh pr *)", - "Bash(gh issue *)", - "Bash(gh api *)", - "Bash(ls *)", - "Bash(wc *)", - "Bash(which *)", - "Bash(node -e *)", - "Bash(php -r *)", - "Bash(wp *)" - ], - "deny": [] - } -} diff --git a/.github/workflows/push-asset-readme-update.yml b/.github/workflows/push-asset-readme-update.yml deleted file mode 100644 index de4769aaf..000000000 --- a/.github/workflows/push-asset-readme-update.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Plugin asset/readme update -on: - push: - branches: - - master - paths: - - 'readme.txt' - - '.wordpress-org/**' -jobs: - master: - name: Push to main - runs-on: ubuntu-22.04 - environment: production - steps: - - uses: actions/checkout@master - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - - name: Build - run: | - npm ci - npm run build - - name: WordPress.org plugin asset/readme update - uses: 10up/action-wordpress-plugin-asset-update@stable - env: - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} diff --git a/.github/workflows/push-to-deploy.yml b/.github/workflows/push-to-deploy.yml deleted file mode 100644 index e10b21ba8..000000000 --- a/.github/workflows/push-to-deploy.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Deploy to WordPress.org -on: - release: - types: [published] -jobs: - tag: - name: New tag - runs-on: ubuntu-22.04 - environment: production - steps: - - uses: actions/checkout@master - - name: Verify release tag is reachable from master - run: | - git fetch --no-tags --depth=50 origin master:refs/remotes/origin/master - if ! git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/master; then - echo "::error::Tag $GITHUB_REF_NAME ($GITHUB_SHA) is not reachable from master. Refusing to deploy." - exit 1 - fi - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - - name: Build - run: | - npm ci - npm run build - - name: WordPress Plugin Deploy - uses: 10up/action-wordpress-plugin-deploy@master - env: - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} diff --git a/.github/workflows/release-pr-template.yml b/.github/workflows/release-pr-template.yml deleted file mode 100644 index b296fcd31..000000000 --- a/.github/workflows/release-pr-template.yml +++ /dev/null @@ -1,41 +0,0 @@ - -name: Update checklist for Release PR - -on: - pull_request: - types: [ labeled ] - -jobs: - update_pr: - name: Update checklist for Release PR - if: ${{ github.event.label.name == 'Release PR' }} - runs-on: ubuntu-latest - steps: - - name: Check Branch - id: check-branch - run: | - if [[ ${{ github.base_ref }} == master || ${{ github.base_ref }} == dev || ${{ github.base_ref }} == next-release ]]; then - echo ::set-output name=match::true - fi - - uses: tzkhan/pr-update-action@v2 - if: steps.check-branch.outputs.match == 'true' - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - base-branch-regex: '[A-Za-z\d-_.\\/]+' - head-branch-regex: '[A-Za-z\d-_.\\/]+' - body-template: | - --- - ### Release Checklist: - - [ ] Run to update version number : `grunt version-bump --ver=` - - [ ] Verify the version number in `package.json` and `package-lock.json` - - [ ] Verify `Stable tag` is `` in readme.txt - - [ ] Verify `Tested upto` is set to latest tested version of WordPress - - [ ] Update version in `sureforms.php` in plugin description - - [ ] Verify constant `SRFM_VER` in `sureforms.php` with latest version of SureForms - - [ ] Verify constant `SRFM_PRO_RECOMMENDED_VER` in `sureforms.php` with compatible version of SureForms Pro - - [ ] Verify changelog `date` and `content` as per SureForms standards - - [ ] Generate README.md : `grunt readme` - - [ ] Generate POT file : `npm run makepot` - --- - body-update-action: 'suffix' - body-uppercase-base-match: false diff --git a/.github/workflows/release-tag-draft.yml b/.github/workflows/release-tag-draft.yml deleted file mode 100644 index 55a6449bf..000000000 --- a/.github/workflows/release-tag-draft.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Create a release tag - -on: - push: - branches: - - master - paths: - # Release happens only if the version number in main file is updated. - - 'sureforms.php' - -jobs: - build-artifact: - name: Build Release Artifact - runs-on: ubuntu-22.04 - outputs: - current_version: ${{ steps.get_version.outputs.current_version }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: master - - - name: Initialize mandatory git config - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Compute current version - id: get_version - run: | - CURRENT_VERSION=$(jq --raw-output '.version' package.json) - echo "current_version=${CURRENT_VERSION}" >> $GITHUB_OUTPUT - - - name: Install Node.js and npm - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Build plugin ZIP file - run: bash ./bin/build-zip.sh - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: sureforms-plugin - path: ./artifact/sureforms.zip - - create-release: - name: Create Release Draft and Attach Asset - needs: [build-artifact] - runs-on: ubuntu-latest - steps: - - name: Set Release Version - id: get_release_version - env: - VERSION: ${{ needs.build-artifact.outputs.current_version }} - run: echo "version=$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' )" >> $GITHUB_OUTPUT - - - name: Download Plugin Zip Artifact - uses: actions/download-artifact@v8 - with: - name: sureforms-plugin - - - name: Create Release Draft - id: create_release - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: Version ${{ steps.get_release_version.outputs.version }} - tag_name: "v${{ steps.get_release_version.outputs.version }}" - target_commitish: master - draft: true - prerelease: false - body: 'Replace changelog here' - files: ./sureforms.zip diff --git a/.github/workflows/update-translations.yml b/.github/workflows/update-translations.yml deleted file mode 100644 index 38ca26957..000000000 --- a/.github/workflows/update-translations.yml +++ /dev/null @@ -1,205 +0,0 @@ -name: Update Translations - -on: - issue_comment: - types: [created] - -# Only allow one i18n job per PR at a time. -concurrency: - group: i18n-${{ github.event.issue.number }} - cancel-in-progress: true - -permissions: - contents: write - pull-requests: write - issues: write - -jobs: - i18n: - # Only trusted users may trigger a run — this job executes branch build - # scripts with secrets, so gate on the commenter's association. - if: | - github.event.comment.body == '/i18n' && - github.event.issue.pull_request != null && - contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) - runs-on: ubuntu-latest - - steps: - # Acknowledge the command with a reaction - - name: Add reaction to comment - uses: peter-evans/create-or-update-comment@v5 - with: - comment-id: ${{ github.event.comment.id }} - reactions: rocket - - # Get PR metadata (head branch, fork detection) - - name: Get PR details - id: pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}) - HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref') - HEAD_REPO=$(echo "$PR_JSON" | jq -r '.head.repo.full_name') - IS_FORK=$(echo "$PR_JSON" | jq -r '.head.repo.full_name != .base.repo.full_name') - - echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" - echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT" - echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT" - - # Block fork PRs — secrets are not available - - name: Block fork PRs - if: steps.pr.outputs.is_fork == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**i18n update skipped**: This PR is from a fork. Translation updates require repository secrets and cannot run on fork PRs." - exit 1 - - # Notify that the workflow has started - - name: Comment starting - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**Translations update started** :hourglass_flowing_sand: - Building and generating translations for 7 languages (nl, fr, de, es, it, pt, pl)... - [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - # Checkout the actual PR branch (not default branch) - - name: Checkout PR branch - uses: actions/checkout@v6 - with: - ref: ${{ steps.pr.outputs.head_ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '24.16.0' - cache: 'npm' - - # Pin WP-CLI to a fixed release. `wp i18n make-json` stamps - # "generator":"WP-CLI/" into every languages/*.json, so - # installing "latest" rewrites all JSON files whenever WP-CLI - # ships a new version. Bump this pin deliberately. - - name: Install WP-CLI - run: | - curl -sSfL -o wp-cli.phar https://github.com/wp-cli/wp-cli/releases/download/v2.12.0/wp-cli-2.12.0.phar - chmod +x wp-cli.phar - sudo mv wp-cli.phar /usr/local/bin/wp - wp --info - - - name: Install dependencies - run: npm ci - - # Build first so makepot picks up all the latest strings - - name: Build - run: npm run build - - # Run the full i18n pipeline (makepot -> po -> gpt-translate -> mo -> json) - - name: Run i18n script - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - set -euo pipefail - bash bin/i18n.sh - - # Check if any files changed - - name: Check for changes - id: changes - run: | - if git diff --quiet; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - CHANGED_FILES=$(git diff --name-only | wc -l | tr -d ' ') - echo "file_count=$CHANGED_FILES" >> "$GITHUB_OUTPUT" - fi - - # Push the i18n branch at the current PR-head tip. This only creates a - # branch ref pointing at an existing commit — no new (unverified) commit - # object is introduced here. - - name: Create i18n branch - if: steps.changes.outputs.changed == 'true' - env: - # Pass via env so the branch name can't inject shell commands. - HEAD_REF: ${{ steps.pr.outputs.head_ref }} - run: | - BRANCH="i18n/$HEAD_REF" - echo "BRANCH=$BRANCH" >> "$GITHUB_ENV" - git push --force origin "HEAD:refs/heads/$BRANCH" - - # Commit the generated translation files through the GitHub GraphQL API - # so the commit is signed by GitHub's GPG key and shows as "Verified" - # (a plain `git push` over HTTPS is never signed). Pinned to the v0.2.20 - # commit SHA for supply-chain safety. - - name: Commit translations (verified) - if: steps.changes.outputs.changed == 'true' - uses: planetscale/ghcommit-action@25309d8005ac7c3bcd61d3fe19b69e0fe47dbdde # v0.2.20 - with: - commit_message: | - chore: update i18n translations - - Auto-generated by /i18n command on PR #${{ github.event.issue.number }} - repo: ${{ github.repository }} - branch: ${{ env.BRANCH }} - file_pattern: '.' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Open the translations PR against the original PR's head branch. - - name: Create PR - if: steps.changes.outputs.changed == 'true' - id: create_pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Pass via env so the base branch name can't inject shell commands. - HEAD_REF: ${{ steps.pr.outputs.head_ref }} - run: | - PR_URL=$(gh pr create \ - --title "chore: update i18n translations" \ - --body "This PR updates translation files for 7 languages (nl, fr, de, es, it, pt, pl). - - Triggered by \`/i18n\` command on #${{ github.event.issue.number }}." \ - --base "$HEAD_REF" \ - --head "$BRANCH") - - echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - - # Comment on the original PR with a link to the translations PR - - name: Comment success - if: steps.changes.outputs.changed == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**Translations PR created** :white_check_mark: - ${{ steps.changes.outputs.file_count }} files changed across 7 languages. - Review and merge: ${{ steps.create_pr.outputs.pr_url }}" - - - name: Comment no changes - if: steps.changes.outputs.changed == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**No translation changes** :white_check_mark: - All translation files are already up to date." - - # On any failure, comment with a link to the logs - - name: Comment failure - if: failure() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --body "**Translation update failed** :x: - [View workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details." diff --git a/.scripts/git-hooks/pre-commit b/.scripts/git-hooks/pre-commit deleted file mode 100644 index ce0ba23ea..000000000 --- a/.scripts/git-hooks/pre-commit +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/sh - -# Check PHPCS before commit -# Ref - -# https://github.com/smgladkovskiy/phpcs-git-pre-commit/blob/master/src/pre-commit -# https://dev.to/akdevcraft/git-pre-hook-pre-commit-hook-with-npm-project-44p2 - -echo "***** Running pre-commit ******" - -PROJECT=$(php -r "echo dirname(dirname(dirname(realpath('$0'))));") -STAGED_FILES_CMD=$(git diff --cached --name-only --diff-filter=ACMR HEAD | grep \\.php) -UNSTAGED_FILES_CMD=$(git diff --name-only --diff-filter=ACMR | grep \\.php) - -# Determine if a file list is passed -if [ "$#" -eq 1 ] -then - oIFS=$IFS - IFS=' - ' - SFILES="$1" - IFS=$oIFS -fi - -SFILES=${SFILES:-$STAGED_FILES_CMD} - -STAGED_BUT_MODIFIED_FILES=$(php -r "\$sfiles=(explode(\"\\n\", '$SFILES'));\$usfiles=(explode(\"\\n\", '$UNSTAGED_FILES_CMD'));echo implode(\"\\n\",array_intersect(\$usfiles,\$sfiles));") - -if [ -z "$STAGED_BUT_MODIFIED_FILES" ]; then - echo "OK" -else - echo "Files staged but then modified:\n" - echo "${STAGED_BUT_MODIFIED_FILES}" - exit 1 -fi - -# echo $PROJECT -# echo $SFILES - -echo "Checking files for PHP Lint..." -for FILE in $SFILES -do - # echo "$PROJECT/$FILE" - php -l -d display_errors=0 "./$FILE" - if [ $? != 0 ] - then - echo "Fix the error before commit." - exit 1 - fi - FILES="$FILES ./$FILE" -done - -if [ "$FILES" = "" ] -then - echo "All good..." - exit 0 -fi - -if [ "$FILES" != "" ] -then - echo "Running PHPCS (Code Sniffer) Check..." - composer lint -n $FILES - if [ $? != 0 ] - then - echo "Please fix the PHPCS errors before commit!" - echo " => Run this command for automatic fixes." - echo " composer format-- $FILES" - exit 1 - fi -fi - -if [ "$FILES" != "" ] -then - echo "Running PHPStan Check..." - composer phpstan - if [ $? != 0 ] - then - echo "Please fix the PHPStan errors before commit!" - echo " => Run this command to check PHPStan errors." - echo " composer phpstan" - exit 1 - fi -fi - -if [ "$FILES" != "" ] -then - echo "Running CSS linting issues..." - npm run lint-css - if [ $? != 0 ] - then - echo "Please fix the CSS issues before commit!" - exit 1 - fi -fi - -if [ "$FILES" != "" ] -then - echo "Running JS linting issues..." - npm run lint-js - if [ $? != 0 ] - then - echo "Please fix the JS issues before commit!" - exit 1 - fi -fi - -exit $? diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 2afeea1d5..000000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,355 +0,0 @@ -# SureForms Architecture Documentation - -## Table of Contents - -1. [Introduction](#introduction) -2. [System Architecture](#system-architecture) -3. [Core Components](#core-components) -4. [Data Flow](#data-flow) -5. [Form Building Process](#form-building-process) -6. [AI Integration](#ai-integration) -7. [Form Submission and Processing](#form-submission-and-processing) -8. [Security Features](#security-features) -9. [Extension Points](#extension-points) -10. [Technical Debt and Improvement Areas](#technical-debt-and-improvement-areas) -11. [Development Guidelines](#development-guidelines) - -## Introduction - -SureForms is a modern WordPress form builder plugin designed to provide an intuitive, no-code form building experience. It leverages WordPress's native block editor (Gutenberg) to create a seamless form building experience that integrates naturally with the WordPress ecosystem. - -### Key Features - -- **Native WordPress Integration**: Built on top of the WordPress block editor -- **AI-Powered Form Generation**: Create forms quickly using AI suggestions -- **No-Code Flexibility**: Advanced features without requiring coding knowledge -- **Mobile-First Design**: Responsive forms that work across all devices -- **Modern UI/UX**: Clean, intuitive interface for both admins and users -- **Security Features**: Anti-spam protection, GDPR compliance, and data security -- **Extensibility**: Developer-friendly architecture with hooks and filters - -## System Architecture - -SureForms follows a modular architecture pattern, separating concerns into distinct components that interact through well-defined interfaces. - -```mermaid -graph TD - A[WordPress Core] --> B[SureForms Plugin] - B --> C[Admin Interface] - B --> D[Form Builder] - B --> E[Form Renderer] - B --> F[Form Processor] - B --> G[Database Layer] - B --> H[AI Integration] - - C --> I[Settings Management] - C --> J[Form Management] - C --> K[Entry Management] - - D --> L[Block Editor Integration] - D --> M[Form Templates] - D --> N[Field Components] - - E --> O[Frontend Rendering] - E --> P[Validation] - - F --> Q[Submission Handler] - F --> R[Email Notifications] - F --> S[Confirmation Handler] - - G --> T[Custom Post Types] - G --> U[Custom Database Tables] - G --> V[Metadata Storage] - - H --> W[AI Form Generator] - H --> X[AI Middleware] -``` - -## Core Components - -### Plugin Loader (`Plugin_Loader`) - -The central initialization class that bootstraps the plugin, registers hooks, and loads core components. It follows a singleton pattern to ensure only one instance exists. - -### Post Types (`Post_Types`) - -Registers and manages custom post types for forms and entries. Handles the admin UI customizations for these post types. - -### Form Submit (`Form_Submit`) - -Processes form submissions through REST API endpoints, validates input data, handles file uploads, processes anti-spam measures, and triggers email notifications. - -### AI Form Builder (`AI_Form_Builder`) - -Integrates with AI services to generate form structures based on user prompts. Communicates with middleware services to process AI requests. - -### Database Layer - -Custom database tables for storing form entries and related data, separate from the WordPress post tables for better performance and data organization. - -### Frontend Assets (`Frontend_Assets`) - -Manages the loading of CSS and JavaScript assets for the frontend rendering of forms. - -### Email Notifications - -Handles the sending of email notifications based on form submissions, with support for multiple notification templates and recipients. - -## Data Flow - -### Form Creation Flow - -```mermaid -sequenceDiagram - participant User - participant WordPress - participant FormBuilder - participant Database - - User->>WordPress: Access Form Builder - WordPress->>FormBuilder: Initialize Builder Interface - FormBuilder->>User: Display Form Builder UI - - alt Manual Form Creation - User->>FormBuilder: Add/Configure Form Fields - FormBuilder->>User: Update UI Preview - else AI-Assisted Creation - User->>FormBuilder: Enter Form Description - FormBuilder->>AI: Send Form Description - AI->>FormBuilder: Return Suggested Form Structure - FormBuilder->>User: Display AI-Generated Form - User->>FormBuilder: Modify/Approve Form - end - - User->>FormBuilder: Save Form - FormBuilder->>Database: Store Form Configuration - Database->>WordPress: Return Form ID - WordPress->>User: Confirm Form Creation -``` - -### Form Submission Flow - -```mermaid -sequenceDiagram - participant User - participant Frontend - participant REST_API - participant Validation - participant AntiSpam - participant Database - participant EmailSystem - - User->>Frontend: Fill Form - Frontend->>User: Validate Client-Side - User->>Frontend: Submit Form - Frontend->>REST_API: POST Form Data - - REST_API->>Validation: Validate Data - Validation->>REST_API: Validation Result - - REST_API->>AntiSpam: Check for Spam - AntiSpam->>REST_API: Spam Check Result - - alt Valid Submission - REST_API->>Database: Store Entry - Database->>REST_API: Entry ID - REST_API->>EmailSystem: Trigger Notifications - EmailSystem->>REST_API: Email Status - REST_API->>Frontend: Success Response - Frontend->>User: Show Confirmation - else Invalid Submission - REST_API->>Frontend: Error Response - Frontend->>User: Show Error Message - end -``` - -## Form Building Process - -SureForms uses WordPress's block editor as the foundation for its form builder. This approach provides several advantages: - -1. **Familiar Interface**: Users already familiar with WordPress's editor can easily adapt -2. **Native Integration**: Seamless integration with WordPress core functionality -3. **Extensibility**: Ability to leverage the block ecosystem - -### Block Structure - -Forms are composed of various block types: - -- **Container Blocks**: Group and organize form fields -- **Field Blocks**: Individual form inputs (text, email, checkbox, etc.) -- **Layout Blocks**: Control the visual arrangement of fields -- **Special Blocks**: Submit buttons, GDPR notices, etc. - -Each block has its own edit and save components, following the WordPress block API pattern. - -### Form Data Storage - -Form configurations are stored as custom post types with: - -- Post content: Stores the block structure (serialized blocks) -- Post meta: Stores form settings, styling options, and other configuration data - -## AI Integration - -SureForms features an AI-powered form generation system that allows users to create forms by describing them in natural language. - -```mermaid -graph TD - A[User Input] --> B[AI Middleware] - B --> C[AI Service] - C --> D[Form Structure Generation] - D --> E[Field Mapping] - E --> F[Block Generation] - F --> G[Form Preview] - G --> H[User Edits] - H --> I[Final Form] -``` - -### AI Form Generation Process - -1. User provides a description of the desired form -2. The description is sent to the AI middleware -3. AI analyzes the description and generates a structured form definition -4. The form definition is mapped to SureForms field types -5. Blocks are generated and inserted into the editor -6. User can review and modify the generated form - -## Form Submission and Processing - -### Submission Handling - -Form submissions are processed through a custom REST API endpoint that: - -1. Validates the form data against defined rules -2. Checks for spam using various methods (honeypot, reCAPTCHA, etc.) -3. Processes file uploads if present -4. Stores the submission in the database -5. Triggers email notifications -6. Returns appropriate success/error responses - -### Email Notifications - -The email notification system supports: - -- Multiple notification templates per form -- Dynamic content using smart tags -- HTML email formatting -- Conditional sending based on form data -- Custom headers (CC, BCC, Reply-To) - -```mermaid -graph TD - A[Form Submission] --> B[Email Notification System] - B --> C{Multiple Recipients?} - C -->|Yes| D[Process Each Recipient] - C -->|No| E[Single Email] - D --> F[Parse Email Template] - E --> F - F --> G[Replace Smart Tags] - G --> H[Apply Email Template] - H --> I[Send Email] - I --> J{Success?} - J -->|Yes| K[Log Success] - J -->|No| L[Log Failure] -``` - -## Security Features - -SureForms implements multiple security measures to protect against common vulnerabilities: - -### Anti-Spam Protection - -- **Honeypot Fields**: Hidden fields to catch automated submissions -- **reCAPTCHA Integration**: Multiple versions of Google reCAPTCHA -- **hCaptcha Support**: Alternative to reCAPTCHA -- **Cloudflare Turnstile**: Modern CAPTCHA alternative - -### Data Protection - -- **GDPR Compliance**: Options to enable GDPR-compliant data handling -- **Data Encryption**: Sensitive data can be encrypted in storage -- **Auto-Delete Entries**: Automatic deletion of entries after a specified period - -### Input Validation - -- **Client-Side Validation**: Immediate feedback to users -- **Server-Side Validation**: Thorough validation of all submitted data -- **File Upload Security**: Strict file type and size validation - -## Extension Points - -SureForms provides several extension points for developers: - -### WordPress Hooks - -```php -// Example of filter hook for email notification -add_filter('srfm_email_notification', function($parsed, $submission_data, $item, $form_data) { - // Modify email content or recipients - return $parsed; -}, 10, 4); - -// Example of action hook before form submission -add_action('srfm_before_submission', function($form_data) { - // Perform custom actions before form processing -}); -``` - -### JavaScript API - -```javascript -// Example of extending the form validation -window.sureFormsHooks.addFilter( - 'srfm.validation.rules', - 'my-plugin/custom-validation', - function(rules, fieldData) { - // Add custom validation rules - return rules; - } -); -``` - -## Technical Debt and Improvement Areas - -1. **Performance Optimization**: - - Lazy loading of form assets - - Improved caching strategies - -2. **Code Refactoring**: - - Further modularization of components - - Consistent naming conventions - -3. **Testing Coverage**: - - Increase unit and integration test coverage - - Automated UI testing - -## Development Guidelines - -### Coding Standards - -SureForms follows WordPress coding standards with some additional guidelines: - -- PHP: PSR-12 with WordPress-specific adaptations -- JavaScript: ESLint with WordPress configuration -- CSS: Follows WordPress CSS coding standards - -### Development Workflow - -1. **Feature Planning**: Document requirements and design -2. **Development**: Implement features with appropriate tests -3. **Code Review**: Peer review for quality assurance -4. **Testing**: Automated and manual testing -5. **Documentation**: Update technical and user documentation -6. **Release**: Version tagging and deployment - -### Version Control - -- Feature branches for new development -- Pull request workflow for code review -- Semantic versioning for releases - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining the power of the block editor with advanced features like AI form generation. Its modular architecture and extensive extension points make it both user-friendly and developer-friendly, while its focus on security and performance ensures a reliable experience for all users. - -The plugin continues to evolve with a focus on improving user experience, expanding integration capabilities, and optimizing performance across all devices and platforms. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index fb62799fc..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,180 +0,0 @@ -# SureForms - CLAUDE.md - -## Project Overview -SureForms is a WordPress form builder plugin (v2.5.1) by Brainstorm Force. AI-powered drag-and-drop form building using the Gutenberg block editor. - -- **Plugin slug:** `sureforms` | **Text domain:** `sureforms` -- **Prefix:** `SRFM_` (constants), `srfm` (slug), `sureforms_` (post types/DB) -- **Minimum PHP:** 7.4 | **Minimum WP:** 6.4 - -## Tech Stack -- **Backend:** PHP 7.4+ / WordPress Plugin API / Custom REST endpoints -- **Frontend:** React 18 + WordPress Gutenberg blocks -- **Styling:** TailwindCSS 3.4 + SASS + PostCSS -- **Build:** WordPress Scripts (Webpack 5) + Grunt -- **Testing:** PHPUnit 9 (unit) + Playwright (E2E) -- **Static Analysis:** PHPStan level 9 + PHPCS (WordPress-Extra) -- **Node:** 24.16.0 (Volta) | **UI Library:** @bsf/force-ui - -## Commands - -### Build -```bash -npm run start # Dev server with watch -npm run build # Full production build -npm run build:script # Webpack only -npm run build:sass # SASS only -``` - -### Lint & Format -```bash -npm run lint-js # ESLint -npm run lint-js:fix # ESLint auto-fix -npm run lint-css # Stylelint -composer lint # PHPCS -composer format # PHPCBF auto-fix -composer phpstan # PHPStan level 9 -composer insights # PHP Insights -``` - -### Test -```bash -composer test # PHPUnit -composer test:coverage # PHPUnit with coverage -npm run test:unit # JS unit tests -npm run play:up # Start WP test env -npm run play:run # Playwright E2E -npm run play:stop # Stop WP test env -``` - -### i18n -```bash -npm run makepot # Generate .pot -npm run i18n:po # Update .po from .pot -npm run i18n:mo # Compile .mo -npm run i18n:json # JSON translations -``` - -## Architecture - -### Directory Structure -``` -sureforms/ -├── sureforms.php # Entry point, constants -├── plugin-loader.php # Bootstrap -├── inc/ # PHP backend -│ ├── helper.php # Central utility (large — minimize changes) -│ ├── rest-api.php # REST endpoints -│ ├── form-submit.php # Submission handler -│ ├── entries.php # Entry management -│ ├── post-types.php # CPT registration -│ ├── blocks/ # PHP block rendering -│ ├── fields/ # Field type handlers -│ ├── database/ # Custom DB tables -│ ├── payments/ # Stripe integration -│ ├── ai-form-builder/ # AI form generation -│ ├── global-settings/ # Plugin settings -│ ├── page-builders/ # Elementor/Bricks compat -│ └── lib/ # Third-party (DO NOT modify) -├── src/ # React/JS source -│ ├── admin/ # Admin UI -│ ├── blocks/ # Gutenberg block JS -│ ├── components/ # Shared React components -│ ├── store/ # WordPress data store -│ ├── utils/ # JS utilities -│ └── srfm-controls/ # Custom block controls -├── assets/ # Compiled output (gitignored) -├── sass/ # SASS source -├── modules/ # Feature modules -├── templates/ # PHP templates -├── tests/ # PHPUnit + Playwright + Docker -└── languages/ # Translation files -``` - -### Webpack Aliases -``` -@Admin → src/admin/ @Blocks → src/blocks/ @Controls → src/srfm-controls/ -@Components → src/components/ @Utils → src/utils/ @Svg → assets/svg/ -@Attributes → src/blocks-attributes/ @Image → images/ @IncBlocks → inc/blocks/ -``` - -### Key Data Structures -- **Post Type:** `sureforms_form` — Forms as CPT with block content -- **Custom Tables:** `sureforms_entries`, `sureforms_payments` -- **REST Namespace:** Custom endpoints in `inc/rest-api.php` - -## Code Rules - -### PHP -- Follow WPCS. Use `sureforms` text domain for all translatable strings -- `@since x.x.x` on all new functions/classes (updated in release PRs) -- NEVER `echo` without escaping — use `esc_html()`, `esc_attr()`, `wp_kses_post()` -- NEVER use `$_GET/$_POST/$_REQUEST` directly — use `sanitize_text_field( wp_unslash( ... ) )` -- NEVER skip security verification on endpoints/AJAX handlers - - **Public form submission endpoints** use HMAC tokens via `Submit_Token::verify()` (`inc/submit-token.php`) — NOT nonces. These are cache-safe and session-independent. The `// phpcs:ignore WordPress.Security.NonceVerification.Missing` comment is expected on these handlers. - - **Admin/authenticated endpoints** still use traditional nonces (`wp_verify_nonce()` / `check_ajax_referer()`) -- NEVER skip capability checks — use `current_user_can()` before privileged operations -- NEVER use `wp_die()` in REST callbacks — use `WP_Error` with proper response codes -- NEVER hardcode table names — use `$wpdb->prefix . 'sureforms_entries'` -- ALWAYS use `wp_json_encode()`, `wp_remote_get/post()`, `gmdate()`, `wp_safe_redirect()` - -### JavaScript/React -- Follow WordPress Scripts ESLint config -- Use WordPress data stores (`useSelect`/`useDispatch`) — never raw fetch in blocks -- Import from `@wordpress/*` packages — never import React directly -- Use `__()` from `@wordpress/i18n` — never hardcode user-facing strings -- Use TailwindCSS utility classes; use `@bsf/force-ui` for admin UI -- NEVER use `dangerouslySetInnerHTML` — use `RawHTML` from `@wordpress/element` - - **Deliberate exception:** the entries admin view — `EntryDataSection.js` and - `EntryLogsSection.js` — inserts `sanitizeEntryValue.js` output directly. - (Pre-existing unrelated sites: `src/components/presets/index.js`, - `src/components/image/index.js`.) Two separate rules apply here: - - **Never re-parse the sanitized string.** Feeding sanitizer output to a - SECOND HTML parser (e.g. `html-react-parser`) is what caused the stored-XSS - CVE-2026-18406: the re-parse decoded entities DOMPurify had deliberately - left inert. Sanitizer-straight-to-DOM is DOMPurify's documented, mXSS-safe - contract, so any post-sanitize transform of that string reopens the hole. - - **`RawHTML` is not the fix here, but it is not dangerous either.** It does - not re-parse — it hands the string straight to `dangerouslySetInnerHTML`, - so it is exactly as safe as what ships in these files. It is avoided only - for structural reasons: it renders a `
`, which breaks the inline - `` + `whitespace-pre-wrap` layout, and it adds no safety over the - explicit call. - -### General -- NEVER create files unless absolutely necessary — prefer editing existing files -- NEVER proactively create documentation files unless explicitly requested - -## Gotchas -- **Custom DB tables** introduced in v0.0.13 — migration is irreversible -- **PHPStan level 9** is strict; check baseline before adding new ignores -- **Grunt** still used alongside Webpack for CSS minification and release packaging -- **`inc/lib/`** is third-party code — do not lint or modify -- **Node 24.16.0** pinned via Volta — other versions may cause build issues - -## Verification Before Done -After code changes, verify before reporting done: - -**JS:** `npm run lint-js` → `npm run build:script` -**SASS:** `npm run build:sass` (if touched) - -Checklist: -- Re-read the diff for obvious issues -- No debug code left (`console.log`, `error_log`, `var_dump`) -- All new functions have `@since x.x.x` -- All user-facing strings use `__()` / `_e()` -- Security checks on new endpoints/handlers (HMAC token for public, nonce for admin) -- For significant changes, suggest `npm run play:run` - -## Self-Improvement Loop -- When corrected, add a rule to **Learned Rules** so the mistake never repeats -- When a pattern causes a bug or CI failure, document it immediately -- Periodically prune outdated rules - -## Learned Rules - -- NEVER flag `phpcs:ignore WordPress.Security.NonceVerification.Missing` as a security issue on public form submission handlers (form submit, field validation, Stripe/PayPal payment intents) — these use HMAC token verification (`Submit_Token::verify()`) instead of nonces for page-cache compatibility. The HMAC token is embedded in the form at render time and verified server-side without any session dependency. -- ALWAYS use `Submit_Token::generate($form_id)` / `Submit_Token::verify($token, $form_id)` for public-facing form security — nonces (`wp_verify_nonce`) are only for admin/authenticated endpoints. - -## Current Focus - diff --git a/COMPREHENSIVE_ANALYSIS.md b/COMPREHENSIVE_ANALYSIS.md deleted file mode 100644 index aed786d38..000000000 --- a/COMPREHENSIVE_ANALYSIS.md +++ /dev/null @@ -1,304 +0,0 @@ -# SureForms: Comprehensive Analysis - -This document provides a multi-perspective analysis of the SureForms WordPress plugin, examining it from software architecture, development, and product management viewpoints. - -## Table of Contents - -1. [Software Architecture Analysis](#software-architecture-analysis) -2. [Development Perspective](#development-perspective) -3. [Product Management Analysis](#product-management-analysis) -4. [Integration Capabilities](#integration-capabilities) -5. [Future Development Roadmap](#future-development-roadmap) - -## Software Architecture Analysis - -### Core Architecture - -SureForms follows a modular architecture pattern with clear separation of concerns. The plugin is built around several key components: - -```mermaid -graph TD - A[WordPress Core] --> B[SureForms Plugin] - B --> C[Plugin_Loader] - C --> D[Core Components] - D --> E[Post_Types] - D --> F[Form_Submit] - D --> G[Block_Patterns] - D --> H[Frontend_Assets] - D --> I[AI_Form_Builder] - D --> J[Rest_Api] - D --> K[Database] - K --> L[Custom Tables] - K --> M[Post Meta] -``` - -The architecture leverages WordPress's plugin system and follows object-oriented principles with extensive use of the singleton pattern for core components. - -### Data Flow Architecture - -The data flow within SureForms follows a clear pattern: - -```mermaid -sequenceDiagram - participant User - participant Frontend - participant BlockEditor - participant REST_API - participant Database - - User->>BlockEditor: Create Form - BlockEditor->>Database: Store Form Configuration - User->>Frontend: View & Submit Form - Frontend->>REST_API: Submit Form Data - REST_API->>Database: Store Submission - REST_API->>Frontend: Return Response -``` - -This separation ensures clean data handling and allows for extensibility at various points in the process. - -### Component Analysis - -#### Plugin Loader - -The `Plugin_Loader` class serves as the entry point for the plugin, handling initialization, autoloading, and hook registration. It follows the singleton pattern to ensure only one instance exists. - -#### Post Types - -The `Post_Types` class manages custom post types for forms and entries, including admin UI customizations, shortcode functionality, and metadata registration. - -#### Form Submit - -The `Form_Submit` class processes form submissions through REST API endpoints, handling validation, file uploads, anti-spam measures, and email notifications. - -#### Database Layer - -SureForms uses both WordPress custom post types and custom database tables: -- Custom post type `sureforms_form` for form configurations -- Custom table `{prefix}_srfm_entries` for form submissions - -This hybrid approach leverages WordPress's content management for forms while optimizing data storage for submissions. - -#### AI Integration - -The AI form builder component connects with external AI services through a middleware layer, translating natural language descriptions into structured form definitions. - -### Technical Debt Assessment - -Current areas of technical debt include: - -1. **Performance Optimization** - - Opportunity for more aggressive asset loading strategies - - Potential for improved caching mechanisms - -2. **Code Organization** - - Some components have grown large and could benefit from further modularization - - Inconsistent naming conventions in some areas - -3. **Testing Coverage** - - Limited automated testing for JavaScript components - - Opportunity for more comprehensive integration tests - -## Development Perspective - -### Code Quality - -The codebase demonstrates several positive qualities: - -- **Consistent Structure**: Well-organized directory structure -- **Modern Practices**: Use of namespaces, autoloading, and OOP principles -- **Documentation**: Comprehensive inline documentation -- **Separation of Concerns**: Clear boundaries between components - -Areas for improvement include: - -- **Testing Coverage**: Expanding unit and integration tests -- **Consistent Naming**: Standardizing naming conventions across all components -- **Performance Profiling**: Identifying and addressing performance bottlenecks - -### Development Workflow - -The development process appears to follow modern practices: - -- **Version Control**: Git-based workflow with feature branches -- **Code Review**: Evidence of peer review processes -- **Continuous Integration**: GitHub Actions for automated testing -- **Semantic Versioning**: Clear versioning strategy - -### Extension Points - -SureForms provides several extension mechanisms: - -1. **WordPress Hooks** - - Actions for process intervention - - Filters for data modification - -2. **JavaScript API** - - Client-side hooks for extending functionality - - Custom events for integration - -3. **Block API** - - Ability to register custom form field blocks - - Block filters for modifying existing blocks - -4. **Template System** - - Customizable templates for form rendering - - Theme override capabilities - -### Technology Stack - -The plugin leverages a modern technology stack: - -- **PHP**: Core server-side logic -- **JavaScript/React**: Block editor integration and frontend functionality -- **CSS/SCSS**: Styling with preprocessor support -- **MySQL**: Database storage -- **REST API**: Data communication -- **AI Services**: Form generation capabilities - -## Product Management Analysis - -### User Segments - -SureForms targets several distinct user segments: - -1. **Website Owners** - - Need: Simple form creation without technical knowledge - - Value: Intuitive interface, pre-designed templates - -2. **Designers** - - Need: Visually appealing forms that match site design - - Value: Design customization, aesthetic controls - -3. **Developers** - - Need: Extensible platform with clean APIs - - Value: Hooks, filters, and documentation - -4. **No-Code Professionals** - - Need: Advanced functionality without coding - - Value: Conditional logic, multi-step forms - -### Feature Analysis - -The feature set is well-aligned with user needs: - -| Feature | User Segment | Value Proposition | -|---------|--------------|-------------------| -| Block Editor Integration | All | Familiar, native WordPress experience | -| AI Form Generation | All | Rapid form creation with minimal effort | -| Mobile-First Design | All | Forms work well on all devices | -| Advanced Field Types | No-Code Professionals | Complex form capabilities without coding | -| Developer Hooks | Developers | Extensibility for custom needs | -| Visual Customization | Designers | Forms match site aesthetics | -| Anti-Spam Features | All | Protection from spam submissions | -| GDPR Compliance | All | Legal compliance and data protection | - -### Competitive Positioning - -SureForms differentiates itself through: - -1. **Native WordPress Integration**: Using the block editor rather than a custom interface -2. **AI-Powered Creation**: Faster form building through AI assistance -3. **Design Focus**: Emphasis on visual appeal and user experience -4. **Developer-Friendly**: Clean architecture and extension points - -### Market Fit - -The product demonstrates strong market fit by addressing several key pain points: - -1. **Complex Form Building**: Simplifying the process through AI and intuitive interface -2. **Design Limitations**: Providing modern styling out-of-the-box -3. **Mobile Responsiveness**: Ensuring forms work well on all devices -4. **Technical Barriers**: Enabling advanced features without coding requirements - -## Integration Capabilities - -### WordPress Ecosystem - -SureForms integrates well with the WordPress ecosystem: - -- **Themes**: Compatible with major themes including Astra, Kadence, GeneratePress -- **Page Builders**: Works with Elementor, Beaver Builder, Divi -- **Plugins**: Compatible with popular plugins like WooCommerce, Easy Digital Downloads - -### External Services - -The plugin offers integration with several external services: - -- **reCAPTCHA**: Anti-spam protection -- **hCaptcha**: Alternative CAPTCHA solution -- **Cloudflare Turnstile**: Modern CAPTCHA alternative -- **AI Services**: Form generation capabilities - -### API Architecture - -The API architecture facilitates integration: - -```mermaid -graph LR - A[WordPress] --> B[SureForms Core] - B --> C[REST API] - C --> D[External Integrations] - D --> E[CRM Systems] - D --> F[Email Marketing] - D --> G[Payment Processors] - D --> H[AI Services] -``` - -This architecture allows for flexible integration with various third-party services. - -## Future Development Roadmap - -Based on the analysis, several development priorities emerge: - -### Short-Term Priorities - -1. **Performance Optimization** - - Implement lazy loading for form assets - - Optimize database queries for large form submissions - -2. **Code Quality Improvements** - - Increase test coverage - - Standardize naming conventions - - Refactor larger components - -3. **Integration Expansion** - - Add more third-party service integrations - - Enhance existing integrations with deeper functionality - -### Medium-Term Priorities - -1. **Advanced AI Capabilities** - - Improve form generation accuracy - - Add form analytics and insights - -2. **Enhanced User Experience** - - Streamline the form building process - - Improve form submission handling - -3. **Developer Tools** - - Create comprehensive API documentation - - Develop example extensions - -### Long-Term Vision - -1. **Form Ecosystem** - - Develop a marketplace for extensions - - Create specialized form solutions for different industries - -2. **Enterprise Features** - - Advanced workflow automation - - Enhanced security and compliance features - -3. **SaaS Potential** - - Explore hosted form solution alongside WordPress plugin - - Develop cloud-based form management - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining native block editor integration with AI-powered form generation. Its architecture demonstrates good separation of concerns and extensibility, while its feature set addresses key user needs across multiple segments. - -The product is well-positioned in the competitive form builder market, with clear differentiation through its WordPress integration, AI capabilities, and design focus. Future development should focus on expanding integrations, enhancing AI capabilities, and improving performance to maintain this competitive advantage. - -From a technical perspective, the codebase is well-structured but would benefit from increased test coverage and some refactoring of larger components. The extension system provides good flexibility for developers, though more comprehensive documentation would enhance this further. - -Overall, SureForms shows strong potential for continued growth and adoption, particularly as the WordPress ecosystem continues to embrace the block editor and AI-powered tools become increasingly important in the website building process. diff --git a/PRODUCT_ANALYSIS.md b/PRODUCT_ANALYSIS.md deleted file mode 100644 index a6c4e595e..000000000 --- a/PRODUCT_ANALYSIS.md +++ /dev/null @@ -1,251 +0,0 @@ -# SureForms Product Analysis - -## Executive Summary - -SureForms is a modern WordPress form builder plugin that differentiates itself through its native WordPress integration, AI-powered form generation, and focus on user experience. This analysis examines the product from multiple perspectives to provide a comprehensive understanding of its strengths, challenges, and opportunities for growth. - -## Table of Contents - -1. [Product Overview](#product-overview) -2. [Market Analysis](#market-analysis) -3. [User Experience Analysis](#user-experience-analysis) -4. [Technical Analysis](#technical-analysis) -5. [Competitive Analysis](#competitive-analysis) -6. [SWOT Analysis](#swot-analysis) -7. [Growth Opportunities](#growth-opportunities) -8. [Recommendations](#recommendations) - -## Product Overview - -SureForms is a WordPress form builder plugin that leverages the native WordPress block editor (Gutenberg) to provide an intuitive form building experience. Key features include: - -- **Native WordPress Integration**: Built on top of the WordPress block editor -- **AI-Powered Form Generation**: Create forms quickly using AI suggestions -- **No-Code Flexibility**: Advanced features without requiring coding knowledge -- **Mobile-First Design**: Responsive forms that work across all devices -- **Modern UI/UX**: Clean, intuitive interface for both admins and users -- **Security Features**: Anti-spam protection, GDPR compliance, and data security -- **Extensibility**: Developer-friendly architecture with hooks and filters - -The plugin is currently at version 1.7.0 and requires WordPress 6.4+ and PHP 7.4+. - -## Market Analysis - -### Target Audience - -SureForms targets several key user segments: - -1. **Website Owners**: Individuals and businesses who manage their own WordPress websites and need to collect information from visitors -2. **Designers**: Professionals who value good design and want to create visually appealing forms -3. **Developers**: Technical users who need a developer-friendly solution with clean APIs and hooks -4. **No-Code Professionals**: Users who need advanced form features without coding skills - -### Market Positioning - -SureForms positions itself as a modern alternative to traditional form builders, emphasizing: - -1. **Seamless WordPress Integration**: Using the native block editor rather than a separate interface -2. **Design-First Approach**: Focusing on aesthetics and user experience -3. **AI-Powered Efficiency**: Leveraging AI to speed up form creation -4. **Simplicity Without Compromise**: Providing advanced features in an accessible way - -### Market Trends - -Several trends in the WordPress form builder market are relevant to SureForms: - -1. **Shift to Block Editor**: Growing adoption of Gutenberg and block-based themes -2. **AI Integration**: Increasing use of AI for content and design generation -3. **Mobile-First Design**: Continued emphasis on mobile responsiveness -4. **Privacy Compliance**: Growing importance of GDPR and other privacy regulations -5. **No-Code Movement**: Rising demand for powerful tools that don't require coding skills - -## User Experience Analysis - -### Form Creation Experience - -SureForms provides a streamlined form creation experience: - -- **Block-Based Building**: Drag-and-drop interface using familiar WordPress blocks -- **AI Form Generation**: Natural language prompts to quickly create form structures -- **Visual Editing**: Real-time preview of forms as they're built -- **Templates**: Pre-designed form templates for common use cases - -### Form Submission Experience - -The end-user experience for form submission is optimized for: - -- **Responsive Design**: Forms adapt to different screen sizes -- **Inline Validation**: Real-time feedback on form inputs -- **Accessibility**: Support for screen readers and keyboard navigation -- **Confirmation Messages**: Customizable success messages and redirects - -### Admin Experience - -The admin interface focuses on: - -- **Intuitive Management**: Easy access to forms, entries, and settings -- **Data Visualization**: Visual reporting of form submission data -- **Entry Management**: Tools for viewing, filtering, and exporting submissions -- **Email Notifications**: Customizable email alerts for new submissions - -## Technical Analysis - -### Architecture - -SureForms follows a modular architecture with clear separation of concerns: - -- **Core Plugin**: Handles initialization, hooks, and basic functionality -- **Block System**: Manages form field blocks and their behavior -- **Form Processing**: Handles form submissions and data storage -- **AI Integration**: Connects with AI services for form generation -- **Extension System**: Provides hooks and filters for customization - -### Performance - -The plugin is designed with performance in mind: - -- **Optimized Asset Loading**: CSS and JS loaded only when needed -- **Efficient Database Usage**: Custom tables with appropriate indexes -- **Caching Strategies**: Caching of frequently accessed data -- **Minimal Dependencies**: Limited reliance on external libraries - -### Security - -Security features include: - -- **Input Validation**: Thorough validation of all user inputs -- **Anti-Spam Measures**: Multiple options for spam prevention -- **Data Protection**: GDPR compliance features -- **File Upload Security**: Strict validation of uploaded files - -### Extensibility - -The plugin provides several extension points: - -- **WordPress Hooks**: Actions and filters for modifying behavior -- **JavaScript API**: Client-side hooks for extending functionality -- **Block API**: Ability to create custom form field blocks -- **Template System**: Customizable templates for form rendering - -## Competitive Analysis - -### Key Competitors - -1. **Gravity Forms**: Established premium form builder with extensive features -2. **WPForms**: User-friendly form builder with a focus on simplicity -3. **Formidable Forms**: Advanced form builder with data management features -4. **Ninja Forms**: Modular form builder with add-on marketplace -5. **Contact Form 7**: Simple, free form builder with large user base - -### Competitive Advantages - -SureForms differentiates itself through: - -1. **Native Block Editor Integration**: More seamless WordPress experience -2. **AI-Powered Form Creation**: Faster form building process -3. **Modern Design Focus**: Better out-of-the-box aesthetics -4. **Developer-Friendly Architecture**: Clean APIs and extension points -5. **Mobile-First Approach**: Better experience on mobile devices - -### Competitive Challenges - -Areas where competitors may have advantages: - -1. **Ecosystem Maturity**: Established competitors have larger add-on ecosystems -2. **Market Recognition**: Lower brand awareness as a newer entrant -3. **Feature Depth**: Some competitors have more specialized features -4. **Integration Breadth**: Fewer third-party service integrations - -## SWOT Analysis - -### Strengths - -- Native integration with WordPress block editor -- AI-powered form generation capabilities -- Modern, clean design aesthetic -- Mobile-first responsive approach -- Developer-friendly architecture -- Focus on user experience - -### Weaknesses - -- Newer entrant in a mature market -- Smaller ecosystem of extensions and add-ons -- Limited third-party integrations compared to competitors -- Requires newer versions of WordPress and PHP - -### Opportunities - -- Growing adoption of the WordPress block editor -- Increasing demand for AI-powered tools -- Rising importance of mobile-friendly forms -- Expanding market for no-code solutions -- Potential for vertical-specific form solutions - -### Threats - -- Established competitors with loyal user bases -- WordPress core changes affecting block editor functionality -- Potential AI regulation affecting AI form generation -- Market commoditization driving price competition -- Alternative form solutions outside WordPress ecosystem - -## Growth Opportunities - -### Product Development - -1. **Expanded AI Capabilities**: More sophisticated form generation and analysis -2. **Vertical-Specific Templates**: Forms tailored to specific industries or use cases -3. **Advanced Conditional Logic**: More powerful dynamic form behavior -4. **Enhanced Data Analysis**: Better visualization and reporting of form data -5. **Multi-Step Form Improvements**: More sophisticated multi-page form experiences - -### Market Expansion - -1. **Agency Partner Program**: Dedicated resources for agencies and developers -2. **Vertical Marketing**: Targeted solutions for specific industries -3. **Educational Content**: Tutorials and resources to drive adoption -4. **Integration Partnerships**: Collaborations with complementary services -5. **International Expansion**: Localization for key markets - -### Business Model - -1. **Freemium Model**: Free core plugin with premium add-ons or features -2. **Subscription Tiers**: Different service levels for different user needs -3. **White-Label Solutions**: Customizable versions for agencies and resellers -4. **Enterprise Offerings**: Custom solutions for larger organizations -5. **Marketplace**: Platform for third-party extensions - -## Recommendations - -### Short-Term (0-6 months) - -1. **Expand Integration Ecosystem**: Add connections to popular CRM and email marketing platforms -2. **Enhance AI Form Builder**: Improve accuracy and capabilities of AI form generation -3. **Develop Educational Content**: Create tutorials and documentation to drive adoption -4. **Optimize Performance**: Further improve loading speed and resource usage -5. **Implement User Feedback System**: Gather structured feedback from early adopters - -### Medium-Term (6-12 months) - -1. **Launch Partner Program**: Develop resources for agencies and developers -2. **Create Vertical Solutions**: Develop specialized templates for key industries -3. **Expand Block Library**: Add more specialized form field types -4. **Improve Analytics**: Enhance data visualization and reporting capabilities -5. **Develop Extension Marketplace**: Create platform for third-party add-ons - -### Long-Term (12+ months) - -1. **Develop Enterprise Features**: Add capabilities for larger organizations -2. **Explore SaaS Model**: Consider hosted form solution alongside WordPress plugin -3. **Implement Advanced AI**: Add predictive analytics and personalization -4. **Create Form Ecosystem**: Develop complementary products and services -5. **Explore International Markets**: Localize for key global markets - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining the power of the native block editor with AI-powered form generation. Its focus on user experience, design, and developer-friendly architecture positions it well in the competitive form builder market. - -By leveraging its strengths in WordPress integration and AI capabilities while addressing its challenges in market recognition and ecosystem development, SureForms has significant potential for growth. The recommended strategies focus on expanding integrations, enhancing AI capabilities, developing educational resources, and creating specialized solutions for key market segments. - -With continued development and strategic focus, SureForms can establish itself as a leading WordPress form solution for the modern web. diff --git a/TECHNICAL_OVERVIEW.md b/TECHNICAL_OVERVIEW.md deleted file mode 100644 index 9d2d44140..000000000 --- a/TECHNICAL_OVERVIEW.md +++ /dev/null @@ -1,393 +0,0 @@ -# SureForms Technical Overview - -## Introduction - -SureForms is a modern WordPress form builder plugin that leverages the native WordPress block editor (Gutenberg) to provide an intuitive, no-code form building experience. This document provides a comprehensive technical overview of the plugin's architecture, components, and functionality. - -## Table of Contents - -1. [Plugin Architecture](#plugin-architecture) -2. [Core Components](#core-components) -3. [Database Structure](#database-structure) -4. [Form Building System](#form-building-system) -5. [Form Submission Process](#form-submission-process) -6. [AI Integration](#ai-integration) -7. [Security Features](#security-features) -8. [Extension Points](#extension-points) -9. [Performance Considerations](#performance-considerations) -10. [Future Development](#future-development) - -## Plugin Architecture - -SureForms follows a modular architecture pattern with clear separation of concerns. The plugin is structured around several key components that interact through well-defined interfaces. - -### Directory Structure - -``` -sureforms/ -├── admin/ # Admin-specific functionality -├── api/ # REST API endpoints -├── assets/ # CSS, JS, and other assets -├── inc/ # Core functionality -│ ├── ai-form-builder/ # AI form generation -│ ├── blocks/ # Block registration -│ ├── compatibility/ # Theme/plugin compatibility -│ ├── database/ # Custom database tables -│ ├── email/ # Email notification system -│ ├── fields/ # Form field definitions -│ ├── global-settings/ # Global plugin settings -│ ├── lib/ # Third-party libraries -│ ├── page-builders/ # Page builder integrations -│ ├── single-form-settings/ # Form-specific settings -│ └── traits/ # Shared PHP traits -├── languages/ # Internationalization files -├── modules/ # Feature modules -│ ├── gutenberg/ # Gutenberg integration -│ └── quick-action-sidebar/ # Quick action UI -├── src/ # JavaScript source files -│ ├── admin/ # Admin JS -│ ├── blocks/ # Block JS -│ ├── components/ # Reusable React components -│ ├── lib/ # JS libraries -│ ├── store/ # Redux store -│ ├── styles/ # SCSS files -│ └── utils/ # Utility functions -└── templates/ # Template files -``` - -### Initialization Flow - -The plugin follows a structured initialization process: - -1. `sureforms.php` - Main plugin file that defines constants and includes the plugin loader -2. `plugin-loader.php` - Initializes the plugin and registers core components -3. Core components are loaded through the `Plugin_Loader` class using autoloading -4. WordPress hooks are registered to integrate with the WordPress lifecycle - -```mermaid -graph TD - A[sureforms.php] --> B[plugin-loader.php] - B --> C[Plugin_Loader class] - C --> D[Register hooks] - C --> E[Autoload classes] - C --> F[Load textdomain] - D --> G[Init core components] - G --> H[Post_Types] - G --> I[Form_Submit] - G --> J[Block_Patterns] - G --> K[Frontend_Assets] - G --> L[AI_Form_Builder] - G --> M[Rest_Api] -``` - -## Core Components - -### Plugin_Loader - -The central initialization class that bootstraps the plugin, registers hooks, and loads core components. It follows a singleton pattern to ensure only one instance exists. - -```php -class Plugin_Loader { - private static $instance = null; - - public static function get_instance() { - if (null === self::$instance) { - self::$instance = new self(); - do_action('srfm_core_loaded'); - } - return self::$instance; - } - - // Initialization methods... -} -``` - -### Post_Types - -Registers and manages custom post types for forms and entries. Handles the admin UI customizations for these post types. - -Key features: -- Registers `sureforms_form` post type for storing form configurations -- Adds custom columns to the admin list view -- Implements shortcode functionality -- Manages form metadata registration - -### Form_Submit - -Processes form submissions through REST API endpoints, validates input data, handles file uploads, processes anti-spam measures, and triggers email notifications. - -Key features: -- Registers `/sureforms/v1/submit-form` REST API endpoint -- Validates form submissions against defined rules -- Processes file uploads with security checks -- Handles anti-spam measures (honeypot, reCAPTCHA, hCaptcha, Cloudflare Turnstile) -- Stores submissions in the database -- Triggers email notifications - -### AI_Form_Builder - -Integrates with AI services to generate form structures based on user prompts. Communicates with middleware services to process AI requests. - -Key features: -- Processes natural language descriptions into form structures -- Maps AI-generated fields to SureForms field types -- Generates block structures for the editor - -## Database Structure - -SureForms uses both WordPress custom post types and custom database tables to store data efficiently. - -### Custom Post Types - -- **sureforms_form**: Stores form configurations - - Post content: Serialized blocks representing the form structure - - Post meta: Form settings, styling options, and other configuration data - -### Custom Database Tables - -- **{prefix}_srfm_entries**: Stores form submissions - - ID: Primary key - - form_id: Associated form ID - - user_id: Submitter's user ID (if logged in) - - form_data: JSON-encoded submission data - - submission_info: Browser, IP, and device information - - status: Entry status (read, unread, trash) - - logs: Activity logs for the entry - - created_at: Submission timestamp - -```mermaid -erDiagram - FORMS ||--o{ ENTRIES : "has" - FORMS { - int ID - string post_title - text post_content - string post_status - datetime post_date - } - ENTRIES { - int ID - int form_id - int user_id - json form_data - json submission_info - string status - json logs - datetime created_at - } - FORMS ||--o{ FORM_META : "has" - FORM_META { - int meta_id - int post_id - string meta_key - mixed meta_value - } -``` - -## Form Building System - -SureForms uses WordPress's block editor as the foundation for its form builder. - -### Block Structure - -Forms are composed of various block types: - -- **Container Blocks**: Group and organize form fields -- **Field Blocks**: Individual form inputs (text, email, checkbox, etc.) -- **Layout Blocks**: Control the visual arrangement of fields -- **Special Blocks**: Submit buttons, GDPR notices, etc. - -Each block has its own edit and save components, following the WordPress block API pattern. - -### Block Registration - -Blocks are registered using the WordPress block registration API: - -```javascript -registerBlockType('sureforms/input', { - title: __('Text Field', 'sureforms'), - icon: 'text', - category: 'sureforms', - attributes: { - // Block attributes - }, - edit: Edit, - save: Save -}); -``` - -### Form Rendering - -Forms are rendered on the frontend using a combination of server-side rendering and client-side JavaScript: - -1. The form shortcode or block triggers the `Generate_Form_Markup::get_form_markup()` method -2. The method retrieves the form configuration and generates the HTML markup -3. Frontend JavaScript initializes the form functionality (validation, submission, etc.) - -## Form Submission Process - -### Client-Side Flow - -1. User fills out the form -2. Client-side validation checks for errors -3. Form data is collected and serialized -4. AJAX request is sent to the REST API endpoint -5. Response is processed and appropriate feedback is shown to the user - -### Server-Side Flow - -1. REST API endpoint receives the form submission -2. Data is validated and sanitized -3. Anti-spam checks are performed -4. If GDPR compliance is enabled, appropriate data handling is applied -5. Submission is stored in the database (unless configured not to) -6. Email notifications are triggered -7. Response is sent back to the client - -```mermaid -sequenceDiagram - participant User - participant Browser - participant REST_API - participant Database - participant Email - - User->>Browser: Fill form - Browser->>Browser: Validate input - User->>Browser: Submit form - Browser->>REST_API: POST /sureforms/v1/submit-form - REST_API->>REST_API: Validate data - REST_API->>REST_API: Anti-spam check - REST_API->>Database: Store submission - REST_API->>Email: Send notifications - REST_API->>Browser: Return response - Browser->>User: Show confirmation -``` - -## AI Integration - -SureForms features an AI-powered form generation system that allows users to create forms by describing them in natural language. - -### AI Form Generation Process - -1. User provides a description of the desired form -2. The description is sent to the AI middleware -3. AI analyzes the description and generates a structured form definition -4. The form definition is mapped to SureForms field types -5. Blocks are generated and inserted into the editor -6. User can review and modify the generated form - -### AI Middleware - -The AI middleware acts as a bridge between SureForms and the AI service: - -1. Receives the form description from SureForms -2. Processes the description using AI models -3. Returns a structured form definition -4. Handles authentication and rate limiting - -## Security Features - -SureForms implements multiple security measures to protect against common vulnerabilities: - -### Anti-Spam Protection - -- **Honeypot Fields**: Hidden fields to catch automated submissions -- **reCAPTCHA Integration**: Multiple versions of Google reCAPTCHA -- **hCaptcha Support**: Alternative to reCAPTCHA -- **Cloudflare Turnstile**: Modern CAPTCHA alternative - -### Data Protection - -- **GDPR Compliance**: Options to enable GDPR-compliant data handling -- **Data Encryption**: Sensitive data can be encrypted in storage -- **Auto-Delete Entries**: Automatic deletion of entries after a specified period - -### Input Validation - -- **Client-Side Validation**: Immediate feedback to users -- **Server-Side Validation**: Thorough validation of all submitted data -- **File Upload Security**: Strict file type and size validation - -## Extension Points - -SureForms provides several extension points for developers: - -### WordPress Hooks - -```php -// Example of filter hook for email notification -add_filter('srfm_email_notification', function($parsed, $submission_data, $item, $form_data) { - // Modify email content or recipients - return $parsed; -}, 10, 4); - -// Example of action hook before form submission -add_action('srfm_before_submission', function($form_data) { - // Perform custom actions before form processing -}); -``` - -### JavaScript API - -```javascript -// Example of extending the form validation -window.sureFormsHooks.addFilter( - 'srfm.validation.rules', - 'my-plugin/custom-validation', - function(rules, fieldData) { - // Add custom validation rules - return rules; - } -); -``` - -## Performance Considerations - -SureForms is designed with performance in mind: - -### Asset Loading - -- CSS and JavaScript assets are loaded only when needed -- Assets are minified and optimized for production -- Critical CSS is inlined for faster rendering - -### Database Optimization - -- Custom database tables with appropriate indexes -- Efficient queries with proper WHERE clauses -- Caching of frequently accessed data - -### Form Rendering - -- Server-side rendering for initial form display -- Progressive enhancement for JavaScript features -- Lazy loading of heavy components - -## Future Development - -Planned enhancements for future versions: - -1. **Enhanced AI Capabilities** - - More sophisticated form generation - - AI-powered form analytics - -2. **Advanced Integrations** - - More third-party service integrations - - Improved CRM connections - -3. **Performance Optimizations** - - Further asset optimization - - Enhanced caching strategies - -4. **Accessibility Improvements** - - Better screen reader support - - Keyboard navigation enhancements - -5. **Developer Tools** - - More comprehensive API documentation - - Additional extension points - -## Conclusion - -SureForms represents a modern approach to WordPress form building, combining the power of the block editor with advanced features like AI form generation. Its modular architecture and extensive extension points make it both user-friendly and developer-friendly, while its focus on security and performance ensures a reliable experience for all users. diff --git a/bin/build-zip.sh b/bin/build-zip.sh deleted file mode 100644 index 0bf38a51d..000000000 --- a/bin/build-zip.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -# Exit if any command fails. -set -e - -# Enable nicer messaging for build status. -BLUE_BOLD='\033[1;34m'; -GREEN_BOLD='\033[1;32m'; -RED_BOLD='\033[1;31m'; -YELLOW_BOLD='\033[1;33m'; -COLOR_RESET='\033[0m'; -error () { - echo -e "\n${RED_BOLD}$1${COLOR_RESET}\n" -} -status () { - echo -e "\n${BLUE_BOLD}$1${COLOR_RESET}\n" -} -success () { - echo -e "\n${GREEN_BOLD}$1${COLOR_RESET}\n" -} -warning () { - echo -e "\n${YELLOW_BOLD}$1${COLOR_RESET}\n" -} - - -status "💃 Time to build the plugin ZIP file 🕺" - -if [ ! -d "artifact" ]; then - mkdir "artifact" -fi - - -# Copy files for zip. -rsync -rc --delete --exclude-from ".distignore" "./" "artifact/sureforms" - -# Go to directory -cd artifact - -# Create a zip copied files. -zip -r sureforms.zip "./sureforms" - -if [ "no-clean" != "$1" ]; then - # Removed copied files folder. - rm -rf sureforms -fi - -success "Done. Your sureforms zip is ready..! 🎉" diff --git a/bin/checkout-and-build b/bin/checkout-and-build deleted file mode 100755 index c7529e42e..000000000 --- a/bin/checkout-and-build +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Script to checkout a branch and build the project - -# Check if branch name is provided as an argument -if [ -z "$1" ]; then - echo "Error: No branch name provided." - echo "Usage: ./checkout-and-build.sh " - exit 1 -fi - -# Assign the first argument to a variable -branch_name=$1 - -# Checkout the specified branch -git checkout "$branch_name" - -# pull the latest version -git pull origin "$branch_name" - -# Run the build command -npm run build diff --git a/bin/i18n.sh b/bin/i18n.sh deleted file mode 100644 index bd71a862a..000000000 --- a/bin/i18n.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -# Run release script -#bash bin/i18n.sh -# -# Generates/updates the .pot, .po, .mo and JS .json translation files for the -# BSF Top 20 languages standard. Run at release time. New locales are created -# automatically via msginit, so the .po files do not need to be committed. - -# Always run from the plugin root so relative paths resolve. -cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 - -# Target locales (BSF Top 20). Format: : -LANGS=( - "de_DE:de_DE" "es_ES:es_ES" "fr_FR:fr_FR" "it_IT:it_IT" "nl_NL:nl_NL" - "pl_PL:pl_PL" "pt_PT:pt_PT" "id_ID:id_ID" "pt_BR:pt_BR" "ru_RU:ru_RU" - "tr_TR:tr_TR" "ja:ja_JP" "zh_CN:zh_CN" "ar:ar" "sv_SE:sv_SE" - "vi:vi_VN" "he_IL:he_IL" "th:th_TH" "el:el_GR" "cs_CZ:cs_CZ" -) - -# Run textdomain updates and generate the POT file -echo "Running textdomain update and POT file generation..." -npm run makepot - -# Create a .po for any target locale that does not exist yet. -echo "Ensuring .po files exist for all target locales..." -for entry in "${LANGS[@]}"; do - file="${entry%%:*}"; loc="${entry##*:}" - po="languages/sureforms-${file}.po" - if [ ! -f "$po" ]; then - echo " creating $po ($loc)" - msginit --input=languages/sureforms.pot --output="$po" --locale="$loc" --no-translator - fi -done - -# Update PO files -echo "Updating PO files..." -npm run i18n:po - -# Translate using GPT-PO (only empty msgstr entries are filled). -echo "Translating PO files using GPT-PO..." -npm run i18n:gptpo:nl -npm run i18n:gptpo:fr -npm run i18n:gptpo:de -npm run i18n:gptpo:es -npm run i18n:gptpo:it -npm run i18n:gptpo:pt -npm run i18n:gptpo:pl -npm run i18n:gptpo:id -npm run i18n:gptpo:ptbr -npm run i18n:gptpo:ru -npm run i18n:gptpo:tr -npm run i18n:gptpo:ja -npm run i18n:gptpo:zh -npm run i18n:gptpo:ar -npm run i18n:gptpo:sv -npm run i18n:gptpo:vi -npm run i18n:gptpo:he -npm run i18n:gptpo:th -npm run i18n:gptpo:el -npm run i18n:gptpo:cs - -# Update PO files again after translation -echo "Updating PO files again..." -npm run i18n:po - -# Generate MO files -echo "Generating MO files..." -npm run i18n:mo - -# Generate JSON translation files -echo "Generating JSON translation files..." -npm run i18n:json - -echo "All commands executed successfully." diff --git a/docs/01-getting-started/GETTING-STARTED.md b/docs/01-getting-started/GETTING-STARTED.md deleted file mode 100644 index a9c0b0c34..000000000 --- a/docs/01-getting-started/GETTING-STARTED.md +++ /dev/null @@ -1,647 +0,0 @@ -# Getting Started with SureForms Development - -> **Goal:** Get productive in less than 30 minutes -> **Audience:** New developers, contractors, open-source contributors -> **Prerequisites:** Basic WordPress, PHP, and React knowledge - -## Prerequisites - -Before you begin, ensure you have: - -- **PHP 7.4+** with extensions: mysqli, mbstring, curl, json -- **Composer 2.3+** for PHP dependencies -- **Node.js 18.15+** (check with `node --version`, use nvm: `nvm use`) -- **npm** (comes with Node.js) -- **Git** for version control -- **Code editor** (VS Code recommended with extensions: PHP Intelephense, ESLint, Prettier) -- **Docker** (optional, for wp-env local environment) - ---- - -## Quick Start (5 Minutes) - -### 1. Clone Repository -```bash -git clone https://github.com/brainstormforce/sureforms.git -cd sureforms -``` - -### 2. Install Dependencies -```bash -# PHP dependencies -composer install - -# JavaScript dependencies -npm install -``` - -This installs: -- **PHP:** Action Scheduler, NPS Survey, Analytics libraries -- **JavaScript:** React 18, WordPress scripts, TailwindCSS, Webpack, testing tools - -### 3. Build Assets -```bash -npm run build -``` - -This compiles: -- JavaScript bundles (Webpack → assets/build/) -- CSS from SASS (sass/ → assets/css/) -- Minified production files - -**Time:** ~2-3 minutes - -### 4. Start Local Environment - -**Option A: Using wp-env (Recommended)** -```bash -npm run play:up -``` - -This creates a Docker-based WordPress installation: -- **URL:** http://localhost:8888 -- **Admin:** http://localhost:8888/wp-admin -- **Username:** admin -- **Password:** password - -**Option B: Manual WordPress Setup** -1. Install WordPress locally (MAMP, Local by Flywheel, or XAMPP) -2. Symlink or copy plugin to `wp-content/plugins/sureforms` -3. Activate plugin in WordPress admin - -### 5. Activate Plugin - -Visit http://localhost:8888/wp-admin/plugins.php and activate "SureForms". - -**You're now ready to develop!** 🎉 - ---- - -## Project Structure Overview - -``` -sureforms/ -├── sureforms.php # Main plugin file (entry point) -├── plugin-loader.php # Bootstrap and autoloader -│ -├── inc/ # PHP backend logic (176+ files) -│ ├── form-submit.php # Form submission handler -│ ├── post-types.php # Custom post type registration -│ ├── rest-api.php # REST API endpoints -│ ├── helper.php # Utility functions -│ ├── blocks/ # Gutenberg block PHP classes -│ │ ├── base.php # Abstract base for blocks -│ │ ├── register.php # Block registration -│ │ ├── input/ # Text input block -│ │ └── [14+ more blocks] -│ ├── database/ # Custom database layer -│ │ ├── base.php # Abstract CRUD operations -│ │ ├── register.php # Table initialization -│ │ └── tables/ -│ │ ├── entries.php # Form submissions table -│ │ └── payments.php # Payment transactions table -│ └── [many more directories] -│ -├── src/ # JavaScript/React source -│ ├── blocks/ # Gutenberg block components -│ │ ├── blocks.js # Block registration entry -│ │ ├── input/ # Input block React component -│ │ │ ├── edit.js # Editor interface -│ │ │ ├── save.js # Frontend save (usually null) -│ │ │ └── block.json # Block metadata -│ │ └── [14+ more blocks] -│ ├── admin/ # Admin dashboard React apps -│ │ ├── dashboard/ # Forms listing -│ │ ├── settings/ # Settings page -│ │ ├── entries/ # Entry management -│ │ └── single-form-settings/ # Form editor -│ ├── components/ # Reusable React components -│ ├── store/ # WordPress Data Store (Redux) -│ └── utils/ # Helper functions -│ -├── assets/ # Compiled build output -│ ├── build/ # Webpack bundles -│ ├── css/ # Compiled CSS -│ └── js/ # Compiled JavaScript -│ -├── sass/ # SCSS source files -├── tests/ # PHPUnit and Playwright tests -├── webpack.config.js # Build configuration -├── package.json # npm dependencies and scripts -└── composer.json # PHP dependencies -``` - -### Key Entry Points - -**PHP Initialization:** -``` -sureforms.php (defines constants) - → plugin-loader.php (Plugin_Loader class) - → Autoloader registration - → WordPress hooks: plugins_loaded, init, admin_init -``` - -**JavaScript Entry:** -- **Blocks:** [src/blocks/index.js](../../src/blocks/index.js) -- **Admin Pages:** [src/admin/{page}/index.js](../../src/admin/) - ---- - -## Development Workflow - -### Daily Development - -**1. Start Development Server** -```bash -npm start -``` - -This starts Webpack in watch mode: -- Automatically rebuilds on file changes -- Hot reload for React components -- Source maps for debugging - -**2. Make Changes** -- Edit PHP files in `inc/` or `admin/` -- Edit React components in `src/` -- Edit styles in `sass/` - -**3. See Changes** -- **Admin:** Refresh browser (Cmd/Ctrl + R) -- **Frontend:** Hard refresh (Cmd/Ctrl + Shift + R) to clear cache - -### Before Committing - -**CRITICAL: Always run these commands before committing** - -```bash -# 1. Fix JavaScript linting issues -npm run lint-js:fix - -# 2. Fix CSS linting issues -npm run lint-css:fix - -# 3. Production build (REQUIRED) -npm run build - -# 4. Check PHP coding standards -composer phpcs - -# 5. Run static analysis -composer phpstan - -# 6. Run tests (optional but recommended) -composer test -npm run test:e2e -``` - -**If all pass, commit:** -```bash -git add . -git commit -m "feat: your change description" -git push -``` - -### Commit Message Format - -Follow [Conventional Commits](https://www.conventionalcommits.org/): - -``` -feat: add date picker field block -fix: resolve form submission nonce error -docs: update API endpoint documentation -style: format code with PHPCS -refactor: simplify validation logic -test: add unit tests for Helper class -chore: update dependencies -``` - ---- - -## Common Development Tasks - -### Task 1: Create a Test Form - -**Time:** 5 minutes - -1. Navigate to **SureForms → Add New** in WordPress admin -2. Block editor opens with empty form -3. Add fields by clicking "+" button: - - Add "Input" block → Configure as "Name" - - Add "Email" block → Configure as "Email Address" - - Add "Textarea" block → Configure as "Message" -4. Click "Publish" -5. View form on frontend or use shortcode `[sureforms id="{ID}"]` - -### Task 2: Submit a Test Form - -**Time:** 2 minutes - -1. Visit form on frontend -2. Fill out fields -3. Click "Submit" -4. Check **SureForms → Entries** to see submission - -### Task 3: Add a New Setting to Existing Block - -**Time:** 30-45 minutes -**Difficulty:** Easy -**Files Modified:** 2 - -**Example: Add "Max Length" setting to Input block** - -**Step 1:** Update [src/blocks/input/edit.js](../../src/blocks/input/edit.js) - -Find the `` section and add: -```javascript -import { TextControl } from '@wordpress/components'; - -// Inside InspectorControls > PanelBody - setAttributes({ maxLength: parseInt(value) })} - help={__('Maximum number of characters allowed', 'sureforms')} -/> -``` - -**Step 2:** Update [src/blocks/input/block.json](../../src/blocks/input/block.json) - -Add to `attributes` object: -```json -"maxLength": { - "type": "number", - "default": 0 -} -``` - -**Step 3:** Build and test -```bash -npm run build -# Refresh block editor -# Test setting in Inspector panel -``` - -**Step 4:** Update PHP rendering (if needed) - -Edit [inc/blocks/input/class-input.php](../../inc/blocks/input/class-input.php) to use the attribute: -```php -$max_length = $attributes['maxLength'] ?? 0; - -// In HTML output - 0): ?> - maxlength="" - -/> -``` - -### Task 4: Debug a Form Submission Issue - -**Time:** 10-20 minutes - -**Symptoms:** Form submission fails silently, no error message - -**Debug Steps:** - -1. **Check Browser Console** (F12 → Console tab) - - Look for JavaScript errors - - Check Network tab for failed AJAX requests - - Look for 403 (nonce error) or 500 (server error) - -2. **Check PHP Error Logs** -```bash -# If using wp-env -npm run play:logs - -# Or tail WordPress debug log -tail -f wp-content/debug.log -``` - -3. **Enable WordPress Debug Mode** (wp-config.php) -```php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -define('SCRIPT_DEBUG', true); -``` - -4. **Test REST API Directly** -```bash -# Get nonce from browser (inspect form, look for data-nonce attribute) -curl -X POST "http://localhost:8888/wp-json/sureforms/v1/submit-form" \ - -H "X-WP-Nonce: YOUR_NONCE_HERE" \ - -H "Content-Type: application/json" \ - -d '{"form_id":123,"field_data":{"email":{"value":"test@example.com"}}}' -``` - -5. **Common Fixes:** - - **Nonce expired:** Refresh page - - **Spam protection:** Disable reCAPTCHA temporarily - - **PHP error:** Check debug.log for syntax errors - - **REST API disabled:** Check .htaccess or security plugins - ---- - -## Testing Your Changes - -### Unit Tests (PHP) - -```bash -# Run all PHPUnit tests -composer test - -# Run specific test file -./vendor/bin/phpunit tests/unit/test-helper.php - -# Generate coverage report -composer test:coverage -``` - -### E2E Tests (Playwright) - -```bash -# Run all E2E tests -npm run test:e2e - -# Run specific test -npx playwright test tests/play/specs/basic-form-test-submit-text-field.spec.js - -# Run in headed mode (see browser) -npx playwright test --headed - -# Debug mode -npx playwright test --debug -``` - -### Manual Testing Checklist - -After making changes, test: -- [ ] Form appears in block editor -- [ ] Settings panel works -- [ ] Form renders on frontend -- [ ] Form submission works -- [ ] Entry appears in admin -- [ ] Email notification sent (if configured) -- [ ] No console errors -- [ ] No PHP errors in debug.log - ---- - -## Debugging Tips - -### Enable Debugging - -**WordPress Debug Mode** (wp-config.php): -```php -define('WP_DEBUG', true); // Enable debug mode -define('WP_DEBUG_LOG', true); // Log to wp-content/debug.log -define('WP_DEBUG_DISPLAY', false); // Don't display errors on page -define('SCRIPT_DEBUG', true); // Load unminified JS/CSS -define('SAVEQUERIES', true); // Log database queries -``` - -**Plugin Debug Mode** (wp-config.php): -```php -define('SRFM_DEBUG', true); // Load unminified assets -``` - -### View Logs - -**PHP Errors:** -```bash -tail -f wp-content/debug.log -``` - -**JavaScript Console:** -- Open browser DevTools (F12) -- Console tab shows errors and logs -- Network tab shows AJAX requests - -### Common Issues - -#### Issue: "Class not found" Error -**Cause:** Namespace doesn't match file path -**Fix:** Ensure `SRFM\Inc\Blocks\Input` maps to `inc/blocks/input/class-input.php` - -#### Issue: Blocks Don't Appear in Editor -**Cause:** JavaScript not rebuilt -**Fix:** -```bash -npm run build -# Hard refresh browser (Cmd/Ctrl + Shift + R) -``` - -#### Issue: Form Submission Fails -**Cause:** Nonce expired or invalid -**Fix:** Refresh page to get new nonce - -#### Issue: Styling Not Applied -**Cause:** CSS not compiled or cached -**Fix:** -```bash -npm run build:sass -# Hard refresh browser -``` - -#### Issue: PHP Syntax Error After Edit -**Cause:** Syntax mistake in PHP file -**Fix:** Check debug.log, fix syntax, refresh - ---- - -## Next Steps - -Now that you're set up, here's how to continue learning: - -### 1. Read Core Documentation (2-3 hours) - -**Essential:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide with constraints and patterns -- [ARCHITECTURE.md](../02-architecture/ARCHITECTURE.md) - System design overview - -**Important:** -- [CODING-STANDARDS.md](../03-development/CODING-STANDARDS.md) - Code style rules -- [DATABASE-SCHEMA.md](../04-backend/DATABASE-SCHEMA.md) - Data model -- [FRONTEND-GUIDE.md](../05-frontend/FRONTEND-GUIDE.md) - React/Gutenberg patterns - -### 2. Explore the Codebase (2-3 hours) - -**Follow the Request Flow:** - -**Admin Page Load:** -``` -1. WordPress loads: admin.php?page=sureforms_forms -2. admin/admin.php → render_forms_page() -3. Outputs:
-4. React app mounts from assets/build/forms.js -5. Fetches data from REST API -``` - -**Form Submission:** -``` -1. User fills form → formSubmit.js validates -2. AJAX POST → /wp-json/sureforms/v1/submit-form -3. inc/form-submit.php → handle_form_submission() -4. Validates → Saves to wp_srfm_entries table -5. Sends email notifications -6. Returns success response -``` - -### 3. Study Example Implementations - -**Reference Block:** [inc/blocks/input/](../../inc/blocks/input/) -- See complete block implementation -- PHP class extends Base -- React component structure -- Attribute schema in block.json - -**Reference React App:** [src/admin/dashboard/](../../src/admin/dashboard/) -- React Query for data fetching -- React Router for navigation -- Component structure - -### 4. Make Your First Contribution - -**Choose from "Good First Issue" labels:** -- Add documentation -- Fix typos -- Add unit tests -- Improve error messages -- Add new field setting - -**Contribution Process:** -1. Fork repository -2. Create feature branch: `git checkout -b feat/your-feature` -3. Make changes -4. Run tests and linters -5. Commit with conventional commit message -6. Push and create pull request -7. Address review feedback - ---- - -## Getting Help - -### Documentation -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [docs/](../) - Full documentation -- [README.md](../../README.md) - Plugin overview - -### Community -- **GitHub Issues:** https://github.com/brainstormforce/sureforms/issues -- **Pull Requests:** https://github.com/brainstormforce/sureforms/pulls - -### Code Questions - -**When asking for help, include:** -1. What you're trying to do -2. What you've tried -3. Error messages (browser console + PHP debug.log) -4. Code snippets -5. Environment (WordPress version, PHP version, plugin version) - ---- - -## Development Tools - -### Recommended VS Code Extensions - -- **PHP Intelephense** - PHP intelligence and autocomplete -- **ESLint** - JavaScript linting -- **Prettier** - Code formatting -- **EditorConfig** - Consistent editor config -- **WordPress Snippets** - WordPress function snippets -- **GitLens** - Git integration -- **Auto Rename Tag** - HTML tag renaming -- **Path Intellisense** - File path autocomplete - -### Browser Extensions - -- **React Developer Tools** - Inspect React components -- **Redux DevTools** - Inspect WordPress Data Store state - -### Command Line Tools - -```bash -# Install WP-CLI globally (optional but helpful) -curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -chmod +x wp-cli.phar -sudo mv wp-cli.phar /usr/local/bin/wp - -# Useful WP-CLI commands -wp plugin list -wp post list --post_type=sureforms_form -wp cache flush -``` - ---- - -## Keyboard Shortcuts (Block Editor) - -| Action | Mac | Windows | -|--------|-----|---------| -| Save | Cmd + S | Ctrl + S | -| Add block | / | / | -| Duplicate block | Cmd + Shift + D | Ctrl + Shift + D | -| Delete block | Shift + Alt + Z | Shift + Alt + Z | -| Block settings | Cmd + Shift + , | Ctrl + Shift + , | -| Undo | Cmd + Z | Ctrl + Z | -| Redo | Cmd + Shift + Z | Ctrl + Shift + Z | - ---- - -## Quick Reference - -### Important npm Scripts - -```bash -npm start # Watch mode with hot reload -npm run build # Production build -npm run build:script # JavaScript only -npm run build:sass # CSS only -npm run lint-js # Check JavaScript -npm run lint-js:fix # Fix JavaScript -npm run lint-css # Check CSS -npm run lint-css:fix # Fix CSS -npm test:unit # JavaScript unit tests -npm run test:e2e # Playwright E2E tests -npm run play:up # Start wp-env -npm run play:stop # Stop wp-env -npm run play:down # Destroy wp-env -npm run makepot # Generate translation template -``` - -### Important Composer Scripts - -```bash -composer install # Install dependencies -composer test # Run PHPUnit tests -composer test:coverage # Generate coverage report -composer phpcs # Check coding standards -composer phpcbf # Fix coding standards -composer phpstan # Static analysis -composer phpinsights # Code quality metrics -``` - -### Important File Paths - -| Purpose | Path | -|---------|------| -| Main plugin file | sureforms.php | -| Plugin loader | plugin-loader.php | -| Form submission | inc/form-submit.php | -| REST API | inc/rest-api.php | -| Helper functions | inc/helper.php | -| Block base class | inc/blocks/base.php | -| Database entries | inc/database/tables/entries.php | -| Webpack config | webpack.config.js | -| TailwindCSS config | tailwind.config.js | - ---- - -**Congratulations!** You're now ready to contribute to SureForms. Happy coding! 🚀 - -**Questions?** Check [CLAUDE.md](../../CLAUDE.md) or create an issue on GitHub. diff --git a/docs/04-backend/DATABASE-SCHEMA.md b/docs/04-backend/DATABASE-SCHEMA.md deleted file mode 100644 index 68f5de98f..000000000 --- a/docs/04-backend/DATABASE-SCHEMA.md +++ /dev/null @@ -1,962 +0,0 @@ -# SureForms Database Schema - -> **Purpose:** Document custom database tables, WordPress post types, and data migration strategies -> **Audience:** Backend developers, database administrators -> **Last Updated:** 2026-02-06 -> **Plugin Version:** 2.5.0 - -## Overview - -SureForms uses a **hybrid data storage approach** combining WordPress native tables with custom optimized tables: - -1. **WordPress Custom Post Type (`sureforms_form`)** - Form configurations and structure -2. **Custom Table (`wp_srfm_entries`)** - Form submission data (performance-optimized) -3. **Custom Table (`wp_srfm_payments`)** - Payment transaction tracking - -**Why Custom Tables?** -- **Performance:** Separate from WordPress posts/postmeta tables (millions of rows) -- **Query Optimization:** Custom indexes for form-specific queries -- **Data Integrity:** Structured schema with foreign keys -- **Scalability:** Handle large volumes of form submissions efficiently - ---- - -## Custom Tables - -### Table: `wp_srfm_entries` - -**Purpose:** Store form submission data separate from WordPress posts for optimized query performance. - -**Schema Definition:** [inc/database/tables/entries.php](../../inc/database/tables/entries.php) -**Table Version:** 1 -**Character Set:** utf8mb4_unicode_ci - -#### SQL Schema - -```sql -CREATE TABLE wp_srfm_entries ( - ID BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY, - form_id BIGINT(20) UNSIGNED, - user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, - form_data LONGTEXT, - logs LONGTEXT, - notes LONGTEXT, - submission_info LONGTEXT, - status VARCHAR(10), - type VARCHAR(20), - extras LONGTEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_form_id (form_id), - INDEX idx_user_id (user_id), - INDEX idx_form_id_created_at_status (form_id, created_at, status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -#### Column Descriptions - -| Column | Type | Nullable | Default | Description | -|--------|------|----------|---------|-------------| -| `ID` | BIGINT(20) UNSIGNED | No | AUTO_INCREMENT | Primary key, unique entry identifier | -| `form_id` | BIGINT(20) UNSIGNED | No | - | Foreign key to wp_posts.ID (sureforms_form post type) | -| `user_id` | BIGINT(20) UNSIGNED | No | 0 | Foreign key to wp_users.ID (0 for guest submissions) | -| `form_data` | LONGTEXT | Yes | NULL | JSON-encoded submission data (field values) | -| `logs` | LONGTEXT | Yes | NULL | JSON-encoded activity logs array | -| `notes` | LONGTEXT | Yes | NULL | JSON-encoded admin notes array | -| `submission_info` | LONGTEXT | Yes | NULL | JSON-encoded metadata (IP, browser, device, referrer) | -| `status` | VARCHAR(10) | Yes | 'unread' | Entry status: 'unread', 'read', 'trash' | -| `type` | VARCHAR(20) | Yes | NULL | Form type: 'quiz', 'standard', NULL (standard default) | -| `extras` | LONGTEXT | Yes | NULL | JSON-encoded additional data | -| `created_at` | TIMESTAMP | No | CURRENT_TIMESTAMP | Entry creation timestamp | -| `updated_at` | TIMESTAMP | No | CURRENT_TIMESTAMP ON UPDATE | Last modification timestamp | - -#### Indexes - -**Purpose:** Optimize query performance for common access patterns - -| Index Name | Columns | Type | Purpose | -|------------|---------|------|---------| -| PRIMARY | ID | PRIMARY KEY | Unique entry lookup | -| idx_form_id | form_id | INDEX | Filter entries by form | -| idx_user_id | user_id | INDEX | Filter entries by user | -| idx_form_id_created_at_status | form_id, created_at, status | COMPOSITE INDEX | Optimize form dashboard queries with date/status filters | - -**Index Usage Examples:** -```sql --- Uses idx_form_id index -SELECT * FROM wp_srfm_entries WHERE form_id = 123; - --- Uses idx_form_id_created_at_status composite index -SELECT * FROM wp_srfm_entries -WHERE form_id = 123 - AND status = 'unread' - AND created_at > '2026-01-01' -ORDER BY created_at DESC; -``` - -#### JSON Column Structures - -**form_data** (LONGTEXT, JSON): -```json -{ - "field_slug": { - "value": "user input", - "label": "Field Label", - "type": "text" - }, - "email": { - "value": "user@example.com", - "label": "Email Address", - "type": "email" - }, - "phone": { - "value": "+1234567890", - "label": "Phone Number", - "type": "phone" - } -} -``` - -**submission_info** (LONGTEXT, JSON): -```json -{ - "ip_address": "192.168.1.1", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...", - "browser": "Chrome", - "browser_version": "120.0.0.0", - "device": "Desktop", - "os": "Mac OS X", - "referrer": "https://example.com/landing-page", - "timestamp": 1706800000 -} -``` - -**logs** (LONGTEXT, JSON): -```json -[ - { - "title": "Entry Created", - "messages": ["Entry submitted successfully"], - "timestamp": "2026-02-06 10:30:00" - }, - { - "title": "Email Sent", - "messages": [ - "Notification sent to admin@example.com", - "User confirmation sent to user@example.com" - ], - "timestamp": "2026-02-06 10:30:05" - }, - { - "title": "Integration Triggered", - "messages": ["Data sent to MailChimp", "Subscriber added successfully"], - "timestamp": "2026-02-06 10:30:10" - } -] -``` - -**notes** (LONGTEXT, JSON): -```json -[ - { - "note": "Follow up required", - "author": "admin", - "timestamp": "2026-02-06 12:00:00" - } -] -``` - -**extras** (LONGTEXT, JSON): -```json -{ - "quiz_score": 85, - "quiz_result": "passed", - "conversational_form": true, - "utm_source": "google", - "utm_campaign": "summer_2026" -} -``` - -#### Status Values - -| Status | Description | -|--------|-------------| -| `unread` | New submission, not yet viewed by admin (default) | -| `read` | Admin has viewed the submission | -| `trash` | Soft-deleted, can be restored or permanently deleted | - -#### Type Values - -| Type | Description | -|------|-------------| -| NULL or `standard` | Regular form submission (default) | -| `quiz` | Quiz form submission with score/results | -| Other custom types | Extensible for future form types | - -#### Migration History - -**Version 1:** -- **Renamed Column:** `user_data` → `form_data` (v0.0.13) - - Reason: More descriptive name - - Migration: ALTER TABLE RENAME COLUMN - - Backward compatibility: Old queries updated - -- **Added Columns:** - - `type` VARCHAR(20) (v0.0.13) - - `extras` LONGTEXT (v0.0.13) - - `user_id` BIGINT(20) (v0.0.13) - ---- - -### Table: `wp_srfm_payments` - -**Purpose:** Store payment transaction records for Stripe integration and payment tracking. - -**Schema Definition:** [inc/database/tables/payments.php](../../inc/database/tables/payments.php) -**Table Version:** 1 -**Character Set:** utf8mb4_unicode_ci - -#### SQL Schema - -```sql -CREATE TABLE wp_srfm_payments ( - id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY, - form_id BIGINT(20) UNSIGNED, - block_id VARCHAR(255) NOT NULL, - status VARCHAR(50) NOT NULL, - total_amount DECIMAL(26,8) NOT NULL, - refunded_amount DECIMAL(26,8) NOT NULL, - currency VARCHAR(10) NOT NULL, - entry_id BIGINT(20) UNSIGNED NOT NULL, - gateway VARCHAR(20) NOT NULL, - type VARCHAR(30) NOT NULL, - mode VARCHAR(20) NOT NULL, - transaction_id VARCHAR(50) NOT NULL, - customer_id VARCHAR(50) NOT NULL, - subscription_id VARCHAR(50) NOT NULL, - subscription_status VARCHAR(20) NOT NULL, - parent_subscription_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, - payment_data LONGTEXT, - extra LONGTEXT, - log LONGTEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - srfm_txn_id VARCHAR(100) NOT NULL, - customer_email VARCHAR(255) NOT NULL, - customer_name VARCHAR(255) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -#### Column Descriptions - -| Column | Type | Nullable | Default | Description | -|--------|------|----------|---------|-------------| -| `id` | BIGINT(20) UNSIGNED | No | AUTO_INCREMENT | Primary key, unique payment identifier | -| `form_id` | BIGINT(20) UNSIGNED | No | - | Foreign key to wp_posts.ID (form) | -| `block_id` | VARCHAR(255) | No | '' | Block client ID (unique identifier for payment field) | -| `status` | VARCHAR(50) | No | 'pending' | Payment status (Stripe-specific) | -| `total_amount` | DECIMAL(26,8) | No | 0.00000000 | Total amount after discount (8 decimal precision) | -| `refunded_amount` | DECIMAL(26,8) | No | 0.00000000 | Total refunded amount | -| `currency` | VARCHAR(10) | No | '' | ISO 4217 currency code (USD, EUR, GBP, etc.) | -| `entry_id` | BIGINT(20) UNSIGNED | No | 0 | Foreign key to wp_srfm_entries.ID | -| `gateway` | VARCHAR(20) | No | '' | Payment gateway ('stripe') | -| `type` | VARCHAR(30) | No | '' | Payment type: 'payment', 'subscription', 'renewal' | -| `mode` | VARCHAR(20) | No | '' | Payment mode: 'test' or 'live' | -| `transaction_id` | VARCHAR(50) | No | '' | Gateway transaction ID (e.g., Stripe pi_xxx) | -| `customer_id` | VARCHAR(50) | No | '' | Gateway customer ID (e.g., Stripe cus_xxx) | -| `subscription_id` | VARCHAR(50) | No | '' | Gateway subscription ID (e.g., Stripe sub_xxx) | -| `subscription_status` | VARCHAR(20) | No | '' | Subscription status ('active', 'canceled', etc.) | -| `parent_subscription_id` | BIGINT(20) UNSIGNED | No | 0 | Parent subscription payment ID (for renewal payments) | -| `payment_data` | LONGTEXT | Yes | NULL | JSON-encoded gateway-specific data | -| `extra` | LONGTEXT | Yes | NULL | JSON-encoded additional metadata | -| `log` | LONGTEXT | Yes | NULL | JSON-encoded payment activity logs | -| `created_at` | TIMESTAMP | No | CURRENT_TIMESTAMP | Payment creation timestamp | -| `updated_at` | TIMESTAMP | No | CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| `srfm_txn_id` | VARCHAR(100) | No | '' | SureForms transaction ID (custom format) | -| `customer_email` | VARCHAR(255) | No | '' | Customer email address | -| `customer_name` | VARCHAR(255) | No | '' | Customer full name | - -#### Valid Enum Values - -**Payment Status** (Stripe-specific): -- `pending` - Payment initiated but not completed -- `succeeded` - Payment completed successfully -- `failed` - Payment failed -- `canceled` - Payment canceled by user or admin -- `requires_action` - Requires additional authentication (3D Secure) -- `requires_payment_method` - Payment method needs to be provided -- `processing` - Payment is being processed -- `refunded` - Full refund processed -- `partially_refunded` - Partial refund processed - -**Currency Codes** (ISO 4217): -``` -USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, SEK, NZD, -MXN, SGD, HKD, NOK, TRY, RUB, INR, BRL, ZAR, KRW -``` - -**Payment Gateway:** -- `stripe` (currently only Stripe supported) - -**Payment Mode:** -- `test` - Test/sandbox mode -- `live` - Production mode - -**Subscription Status** (Stripe-specific): -- `active` - Subscription is active -- `canceled` - Subscription canceled -- `past_due` - Payment failed, retrying -- `unpaid` - Payment failed, no retry -- `trialing` - In trial period -- `incomplete` - Initial payment not completed -- `incomplete_expired` - Initial payment window expired -- `paused` - Subscription paused - -#### JSON Column Structures - -**payment_data** (LONGTEXT, JSON): -```json -{ - "stripe_payment_intent": { - "id": "pi_1234567890", - "amount": 4999, - "currency": "usd", - "status": "succeeded", - "created": 1706800000 - }, - "refunds": { - "re_1234567890": { - "refund_id": "re_1234567890", - "amount": 1000, - "status": "succeeded", - "created": 1706900000, - "reason": "requested_by_customer" - } - } -} -``` - -**extra** (LONGTEXT, JSON): -```json -{ - "discount_code": "SUMMER2026", - "discount_amount": 10.00, - "original_amount": 59.99, - "tax_amount": 5.00, - "metadata": { - "utm_source": "google", - "utm_campaign": "summer_sale" - } -} -``` - -**log** (LONGTEXT, JSON): -```json -[ - { - "title": "Payment Initiated", - "message": "Stripe Payment Intent created", - "timestamp": "2026-02-06 10:30:00" - }, - { - "title": "Payment Succeeded", - "message": "Payment completed successfully", - "timestamp": "2026-02-06 10:30:15" - } -] -``` - ---- - -## WordPress Custom Post Type - -### Post Type: `sureforms_form` - -**Purpose:** Store form configurations and structure using WordPress's content management system. - -**Registration:** [inc/post-types.php](../../inc/post-types.php) -**Supports:** title, editor, custom-fields -**Public:** false (admin-only) -**Has Archive:** false -**Rewrite:** false - -#### Post Fields - -| Field | Type | Description | -|-------|------|-------------| -| `post_title` | VARCHAR(200) | Form name (user-editable) | -| `post_content` | LONGTEXT | Serialized Gutenberg blocks (form structure) | -| `post_status` | VARCHAR(20) | 'draft', 'publish', 'trash' | -| `post_type` | VARCHAR(20) | 'sureforms_form' (constant) | -| `post_author` | BIGINT(20) | WordPress user ID who created the form | -| `post_date` | DATETIME | Form creation date | -| `post_modified` | DATETIME | Last modification date | - -#### Post Meta Keys - -Form-specific settings stored in `wp_postmeta`: - -| Meta Key | Description | Data Type | Example | -|----------|-------------|-----------|---------| -| `_srfm_form_settings` | General form settings | Serialized array | [complex settings object] | -| `_srfm_email_notifications` | Email notification rules | Serialized array | Multiple notification configs | -| `_srfm_confirmation_settings` | Post-submission behavior | Serialized array | Message or redirect URL | -| `_srfm_spam_protection` | Anti-spam settings | Serialized array | reCAPTCHA, honeypot config | -| `_srfm_gdpr_settings` | GDPR compliance options | Serialized array | Consent, auto-delete rules | -| `_srfm_styling_settings` | Form visual customization | Serialized array | Colors, fonts, spacing | -| `_srfm_conditional_logic` | Conditional field display | Serialized array | Field visibility rules | -| `_srfm_integrations_webhooks` | Third-party integrations | Serialized array | MailChimp, webhooks | -| `_srfm_instant_form_settings` | Instant form configuration | Serialized array | AI-generated form data | -| `_srfm_conversational_form` | Conversational mode settings | Serialized array | Step-by-step form config | -| `_srfm_premium_common` | Pro features settings | Serialized array | Pro-only configurations | -| `_srfm_user_registration_settings` | User registration integration | Serialized array | Create user on submission | - -#### Block Structure (post_content) - -Forms stored as serialized Gutenberg blocks (HTML comments): - -```html - - - - - - - - - -``` - ---- - -## Database Schema Migrations - -### Version Control System - -**Implementation:** Each table class maintains a `$table_version` property. - -**Version Storage:** WordPress options table -``` -Option Name: srfm_entries_version -Option Value: 1 (integer) - -Option Name: srfm_payments_version -Option Value: 1 (integer) -``` - -### Migration Flow - -``` -1. Plugin loads → Database\Register::init() -2. For each table: - a. Get stored version from wp_options - b. Compare with code version ($table_version) - c. If mismatch: run upgrade() method - d. Execute schema changes - e. Update version in wp_options -``` - -### Migration Methods - -**Base Class Methods** ([inc/database/base.php](../../inc/database/base.php)): - -| Method | Purpose | -|--------|---------| -| `maybe_create_table()` | Create table if not exists | -| `maybe_add_new_columns()` | Add new columns to existing table | -| `maybe_rename_columns()` | Rename columns (e.g., user_data → form_data) | -| `upgrade($old_version)` | Custom migration logic per version | - -### Adding a New Column (Example) - -**Step 1:** Increment table version in table class -```php -// In inc/database/tables/entries.php -protected $table_version = 2; // Increment from 1 to 2 -``` - -**Step 2:** Add column to schema -```php -public function get_schema() { - return [ - // ... existing columns - 'new_column' => [ - 'type' => 'string', - 'default' => '', - ], - ]; -} -``` - -**Step 3:** Add column definition -```php -public function get_new_columns_definition() { - return [ - 'new_column VARCHAR(255) DEFAULT "" AFTER status', - ]; -} -``` - -**Step 4:** Implement upgrade logic (if needed) -```php -public function upgrade($old_version) { - global $wpdb; - - if ($old_version < 2) { - // Additional migration logic beyond column addition - $wpdb->query("UPDATE {$this->table_name} SET new_column = 'default_value'"); - } -} -``` - -### Migration Best Practices - -1. **Never Delete Columns** - Mark as deprecated instead - ```php - // DON'T: ALTER TABLE DROP COLUMN old_column - // DO: Leave column, document as deprecated - ``` - -2. **Always Provide Defaults** - Avoid NULL for new columns - ```php - 'new_column VARCHAR(255) DEFAULT ""' // Good - 'new_column VARCHAR(255)' // Bad (no default) - ``` - -3. **Test with Production Data** - Use staging environment first -4. **Log All Migrations** - Use error_log() for debugging -5. **Backwards Compatible** - Old code must work during migration -6. **Atomic Operations** - Use transactions where possible - ---- - -## Common Query Patterns - -### Get All Entries for a Form - -```php -use SRFM\Inc\Database\Tables\Entries; - -$entries = Entries::get_all([ - 'where' => [ - [ - ['key' => 'form_id', 'compare' => '=', 'value' => 123], - ['key' => 'status', 'compare' => '!=', 'value' => 'trash'], - ] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', - 'limit' => 20, - 'offset' => 0 -]); -``` - -**Generated SQL:** -```sql -SELECT * FROM wp_srfm_entries -WHERE form_id = 123 - AND status != 'trash' -ORDER BY created_at DESC -LIMIT 0, 20 -``` - -### Get Entry Count by Status - -```php -$unread_count = Entries::get_total_entries_by_status('unread', $form_id); -$all_count = Entries::get_total_entries_by_status('all', $form_id); -``` - -**Generated SQL:** -```sql --- Unread count -SELECT COUNT(*) FROM wp_srfm_entries -WHERE status = 'unread' AND form_id = 123 - --- All count (excluding trash) -SELECT COUNT(*) FROM wp_srfm_entries -WHERE status != 'trash' AND form_id = 123 -``` - -### Get Entries with Payments - -```php -global $wpdb; - -$entries_with_payments = $wpdb->get_results($wpdb->prepare( - "SELECT e.*, p.total_amount, p.status as payment_status, p.currency - FROM {$wpdb->prefix}srfm_entries e - INNER JOIN {$wpdb->prefix}srfm_payments p ON e.ID = p.entry_id - WHERE e.form_id = %d - AND p.status = 'succeeded' - ORDER BY e.created_at DESC", - $form_id -)); -``` - -### Get Payments by Status - -```php -use SRFM\Inc\Database\Tables\Payments; - -$succeeded_payments = Payments::get_all([ - 'where' => [ - [ - ['key' => 'form_id', 'compare' => '=', 'value' => 123], - ['key' => 'status', 'compare' => '=', 'value' => 'succeeded'], - ] - ], - 'orderby' => 'created_at', - 'order' => 'DESC' -]); -``` - ---- - -## Performance Optimization - -### Indexing Strategy - -**Why Indexes Matter:** -- 10,000+ entries: ~50ms query time with indexes vs ~500ms without -- Composite indexes optimize multi-column WHERE clauses -- Index order matters: (form_id, created_at, status) ≠ (status, form_id, created_at) - -**Index Usage Guidelines:** - -1. **Use Indexed Columns in WHERE Clauses** -```php -// GOOD - Uses idx_form_id index -$entries = Entries::get_all(['where' => [[['key' => 'form_id', 'compare' => '=', 'value' => 123]]]]); - -// BAD - No index on 'type' column (full table scan) -$entries = Entries::get_all(['where' => [[['key' => 'type', 'compare' => '=', 'value' => 'quiz']]]]); -``` - -2. **Leverage Composite Indexes** -```php -// GOOD - Uses idx_form_id_created_at_status composite index -$entries = Entries::get_all([ - 'where' => [ - [ - ['key' => 'form_id', 'compare' => '=', 'value' => 123], - ['key' => 'created_at', 'compare' => '>', 'value' => '2026-01-01'], - ['key' => 'status', 'compare' => '=', 'value' => 'unread'], - ] - ] -]); -``` - -3. **Avoid SELECT *** -```php -// GOOD - Specify columns -$entries = Entries::get_instance()->get_results( - ['form_id' => 123], - 'ID, form_data, created_at' -); - -// BAD - Selects all columns including large LONGTEXT -$entries = Entries::get_all(['where' => [[['key' => 'form_id', 'compare' => '=', 'value' => 123]]]]); -``` - -### Pagination Best Practices - -```php -// Paginate large result sets -$per_page = 20; -$page = 2; -$offset = ($page - 1) * $per_page; - -$entries = Entries::get_all([ - 'where' => [[['key' => 'form_id', 'compare' => '=', 'value' => 123]]], - 'limit' => $per_page, - 'offset' => $offset, - 'orderby' => 'created_at', - 'order' => 'DESC' -]); - -// Cache total count separately -$total = Entries::get_total_entries_by_status('all', 123); -$total_pages = ceil($total / $per_page); -``` - -### JSON Column Optimization - -**MySQL 5.7+ JSON Functions:** -```sql --- Extract specific field from form_data JSON -SELECT - ID, - JSON_EXTRACT(form_data, '$.email.value') as email -FROM wp_srfm_entries -WHERE form_id = 123; - --- Search within JSON -SELECT * FROM wp_srfm_entries -WHERE JSON_EXTRACT(form_data, '$.email.value') = 'user@example.com'; -``` - -**Performance Warning:** -- JSON queries are slower than indexed columns -- Avoid wildcard searches in JSON (`LIKE '%value%'`) -- Consider extracting frequently-queried JSON fields to dedicated columns - ---- - -## Data Retention & GDPR - -### Auto-Delete Configuration - -**Setting Location:** [inc/single-form-settings/compliance-settings.php](../../inc/single-form-settings/compliance-settings.php) - -**Per-Form Settings:** -```php -$retention_days = get_post_meta($form_id, '_srfm_entry_retention_days', true); -// Values: 0 (never delete), 7, 30, 60, 90, 365 (days) -``` - -**Implementation:** WordPress Cron (Action Scheduler) - -```php -// Scheduled task runs daily at 2 AM -add_action('srfm_daily_scheduled_action', 'srfm_auto_delete_entries'); - -function srfm_auto_delete_entries() { - global $wpdb; - - // Get forms with auto-delete enabled - $forms = $wpdb->get_results(" - SELECT post_id, meta_value as retention_days - FROM {$wpdb->postmeta} - WHERE meta_key = '_srfm_entry_retention_days' - AND meta_value > 0 - "); - - foreach ($forms as $form) { - $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$form->retention_days} days")); - - // Delete entries older than cutoff - $wpdb->delete( - $wpdb->prefix . 'srfm_entries', - [ - 'form_id' => $form->post_id, - 'created_at < ' => $cutoff_date - ] - ); - } -} -``` - -### Manual Entry Management - -**Soft Delete (Trash):** -```php -Entries::update($entry_id, ['status' => 'trash']); -``` - -**Permanent Delete:** -```php -// Fires srfm_before_delete_entry hook -Entries::delete($entry_id); -``` - -**Restore from Trash:** -```php -Entries::update($entry_id, ['status' => 'unread']); -``` - ---- - -## Backup & Recovery - -### Backup Recommendations - -**1. Full Database Backup** (before plugin updates) -```bash -mysqldump -u username -p database_name > sureforms_backup.sql -``` - -**2. Table-Specific Dumps** -```bash -# Entries table -mysqldump -u username -p database_name wp_srfm_entries > entries_backup.sql - -# Payments table -mysqldump -u username -p database_name wp_srfm_payments > payments_backup.sql - -# Form configurations (WordPress tables) -mysqldump -u username -p database_name wp_posts wp_postmeta \ - --where="post_type='sureforms_form'" > forms_backup.sql -``` - -**3. Export Entries via Admin UI** -- Navigate to SureForms → Entries -- Select entries → Export CSV -- Includes all form data and metadata - -### Restore Process - -```bash -# Restore from backup -mysql -u username -p database_name < entries_backup.sql - -# Restore specific table -mysql -u username -p database_name < wp_srfm_entries.sql -``` - ---- - -## Troubleshooting - -### Issue: Missing Tables - -**Symptom:** "Table doesn't exist" errors in PHP logs -**Cause:** Tables not created during activation -**Fix:** -1. Deactivate plugin -2. Delete option: `srfm_entries_version`, `srfm_payments_version` -3. Reactivate plugin (triggers table creation) - -### Issue: Charset/Collation Errors - -**Symptom:** Garbled text in submissions (emoji, special characters) -**Cause:** Incorrect charset (not utf8mb4) -**Fix:** -```sql -ALTER TABLE wp_srfm_entries -CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE wp_srfm_payments -CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### Issue: JSON Parsing Errors - -**Symptom:** "Invalid JSON" errors when retrieving form_data -**Cause:** Unescaped quotes in field values -**Fix:** -```php -// Always use wp_json_encode() for JSON storage -$form_data_json = wp_json_encode($form_data); - -// Always use json_decode() with error handling -$form_data = json_decode($form_data_json, true); -if (json_last_error() !== JSON_ERROR_NONE) { - error_log('JSON decode error: ' . json_last_error_msg()); -} -``` - -### Issue: Slow Queries - -**Symptom:** Dashboard loads slowly with many entries -**Cause:** Missing indexes or inefficient queries -**Debug:** -```sql --- Check query execution plan -EXPLAIN SELECT * FROM wp_srfm_entries -WHERE form_id = 123 - AND status = 'unread' -ORDER BY created_at DESC -LIMIT 20; - --- Should show "Using index" in Extra column -``` - -**Fix:** -1. Ensure indexes exist (check `SHOW INDEX FROM wp_srfm_entries`) -2. Use indexed columns in WHERE clauses -3. Add pagination (LIMIT/OFFSET) - ---- - -## Entity Relationship Diagram - -```mermaid -erDiagram - wp_posts ||--o{ wp_srfm_entries : "has many" - wp_posts ||--o{ wp_srfm_payments : "has many" - wp_srfm_entries ||--o| wp_srfm_payments : "may have" - wp_users ||--o{ wp_srfm_entries : "submits" - - wp_posts { - bigint ID PK - varchar post_type "sureforms_form" - longtext post_content "Gutenberg blocks" - varchar post_status "draft|publish|trash" - } - - wp_srfm_entries { - bigint ID PK - bigint form_id FK - bigint user_id FK - longtext form_data "JSON" - varchar status "unread|read|trash" - timestamp created_at - } - - wp_srfm_payments { - bigint id PK - bigint form_id FK - bigint entry_id FK - varchar status "pending|succeeded|failed" - decimal total_amount - varchar currency - varchar transaction_id - timestamp created_at - } - - wp_users { - bigint ID PK - varchar user_email - } -``` - ---- - -## Summary - -**Storage Strategy:** -- **Forms:** WordPress custom post type (native CMS benefits) -- **Entries:** Custom table (performance optimization) -- **Payments:** Custom table (financial data tracking) - -**Key Design Decisions:** -- Custom tables for scalability (handle millions of entries) -- JSON columns for flexible schema (form fields vary per form) -- Composite indexes for query optimization -- Version-controlled migrations for safe schema updates - -**Maintenance:** -- Regular backups before plugin updates -- Monitor table sizes (OPTIMIZE TABLE if needed) -- Review slow query logs -- Test migrations on staging first - ---- - -**Related Documentation:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [ARCHITECTURE.md](../02-architecture/ARCHITECTURE.md) - System design -- [CODING-STANDARDS.md](../03-development/CODING-STANDARDS.md) - Code style -- [SECURITY.md](../07-security/SECURITY.md) - Security practices diff --git a/docs/04-backend/HOOKS-REFERENCE.md b/docs/04-backend/HOOKS-REFERENCE.md deleted file mode 100644 index 35b5c4c12..000000000 --- a/docs/04-backend/HOOKS-REFERENCE.md +++ /dev/null @@ -1,1055 +0,0 @@ -# SureForms Hooks Reference - -> **Purpose:** Complete reference of all WordPress actions and filters for extending SureForms -> **Audience:** Plugin developers, theme developers, integrators -> **Last Updated:** 2026-02-06 -> **Plugin Version:** 2.5.0 - -## Overview - -SureForms provides **450+ hooks** (actions and filters) for extending functionality without modifying core files. This document catalogs the most commonly used hooks with usage examples. - -**Hook Naming Convention:** -- **Prefix:** All hooks start with `srfm_` -- **Format:** `srfm_{context}_{action}` or `srfm_{data_type}_{modification}` -- **Examples:** `srfm_before_submission`, `srfm_field_validation`, `srfm_email_content` - ---- - -## Table of Contents - -- [Plugin Lifecycle](#plugin-lifecycle) -- [Form Submission](#form-submission) -- [Field Validation](#field-validation) -- [Email Notifications](#email-notifications) -- [Form Rendering](#form-rendering) -- [Data Modification](#data-modification) -- [Database Operations](#database-operations) -- [Payment Processing](#payment-processing) -- [Block Registration](#block-registration) -- [Admin Operations](#admin-operations) -- [Integration Points](#integration-points) - ---- - -## Plugin Lifecycle - -### `srfm_core_loaded` - -Fires after the plugin core is fully loaded and initialized. - -**Location:** [plugin-loader.php](../../plugin-loader.php) -**Since:** 0.0.1 -**Parameters:** None - -**Usage:** -```php -add_action('srfm_core_loaded', function() { - // Plugin is ready, initialize custom extensions - error_log('SureForms plugin loaded'); - - // Register custom integrations - my_custom_sureforms_integration(); -}); -``` - -### `srfm_init` - -Fires during WordPress `init` hook after SureForms initialization. - -**Location:** [plugin-loader.php](../../plugin-loader.php) -**Since:** 0.0.1 -**Parameters:** None - -**Usage:** -```php -add_action('srfm_init', function() { - // Register custom post types, taxonomies, or other WordPress entities - register_post_type('custom_form_data', [ - 'public' => false, - 'supports' => ['title'] - ]); -}); -``` - ---- - -## Form Submission - -### `srfm_before_submission` - -Fires immediately before form data is processed and saved to database. Use to validate or modify data before processing. - -**Location:** [inc/form-submit.php:525](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $form_data` - Submitted form data including form_id, fields, and metadata - -**Usage:** -```php -add_action('srfm_before_submission', function($form_data) { - $form_id = $form_data['form_id'] ?? 0; - - // Log submission attempt - error_log(sprintf('Form %d submission started', $form_id)); - - // Perform custom validation - if ($form_id === 123) { - $email = $form_data['email']['value'] ?? ''; - if (strpos($email, '@company.com') === false) { - // Throw exception to halt submission - throw new Exception('Only company emails allowed'); - } - } -}, 10, 1); -``` - -### `srfm_after_submission_process` - -Fires after form entry is saved to database but before email notifications are sent. - -**Location:** [inc/form-submit.php:556](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $form_data` - Complete form data including entry_id - -**Usage:** -```php -add_action('srfm_after_submission_process', function($form_data) { - $entry_id = $form_data['entry_id'] ?? 0; - $form_id = $form_data['form_id'] ?? 0; - - // Trigger third-party integration - if ($form_id === 123) { - $email = $form_data['email']['value'] ?? ''; - $name = $form_data['name']['value'] ?? ''; - - // Add to MailChimp - mailchimp_add_subscriber($email, $name); - - // Send to CRM - crm_create_lead([ - 'email' => $email, - 'name' => $name, - 'source' => 'sureforms' - ]); - } -}, 10, 1); -``` - -### `srfm_form_submit` - -Fires after complete form submission process (entry saved, emails sent). - -**Location:** [inc/form-submit.php:548,644](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $response` - Submission response with entry_id, success status, message - -**Usage:** -```php -add_action('srfm_form_submit', function($response) { - if ($response['success']) { - $entry_id = $response['entry_id']; - - // Custom logging - error_log(sprintf('Form submission %d completed', $entry_id)); - - // Update custom database - global $wpdb; - $wpdb->insert('wp_custom_leads', [ - 'sureforms_entry_id' => $entry_id, - 'created_at' => current_time('mysql') - ]); - } -}, 10, 1); -``` - ---- - -## Field Validation - -### `srfm_field_validation` - -Filters field validation result. Return `WP_Error` to fail validation. - -**Location:** [inc/field-validation.php](../../inc/field-validation.php) -**Since:** 0.0.1 -**Parameters:** -- `bool|WP_Error $is_valid` - Current validation status -- `mixed $field_value` - Field value to validate -- `array $field_data` - Field metadata (type, label, required, etc.) -- `int $form_id` - Form post ID - -**Return:** `bool|WP_Error` - True if valid, WP_Error if invalid - -**Usage:** -```php -add_filter('srfm_field_validation', function($is_valid, $field_value, $field_data, $form_id) { - // Custom validation for specific field type - if ($field_data['type'] === 'text' && $field_data['slug'] === 'phone') { - // Validate phone format - if (!preg_match('/^\+?[1-9]\d{1,14}$/', $field_value)) { - return new WP_Error( - 'invalid_phone', - __('Please enter a valid phone number', 'your-plugin') - ); - } - } - - // Custom validation for specific form - if ($form_id === 123 && $field_data['slug'] === 'age') { - if (intval($field_value) < 18) { - return new WP_Error( - 'age_restriction', - __('You must be 18 or older', 'your-plugin') - ); - } - } - - return $is_valid; -}, 10, 4); -``` - -### `srfm_validate_{field_type}` - -Filters validation for specific field type. - -**Location:** [inc/field-validation.php](../../inc/field-validation.php) -**Since:** Various -**Available Types:** `email`, `phone`, `url`, `number`, `text`, etc. - -**Usage:** -```php -// Custom email validation -add_filter('srfm_validate_email', function($is_valid, $value, $field_data) { - // Block disposable email domains - $disposable_domains = ['tempmail.com', 'throwaway.email']; - $domain = substr(strrchr($value, '@'), 1); - - if (in_array($domain, $disposable_domains)) { - return new WP_Error( - 'disposable_email', - __('Disposable email addresses are not allowed', 'your-plugin') - ); - } - - return $is_valid; -}, 10, 3); -``` - ---- - -## Email Notifications - -### `srfm_email_notification_should_send` - -Filters whether email notification should be sent. - -**Location:** [inc/form-submit.php:796](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $email_notification` - Email configuration -- `array $submission_data` - Submitted form data -- `array $form_data` - Complete form metadata - -**Return:** `array|false` - Email config to send, false to skip - -**Usage:** -```php -add_filter('srfm_email_notification_should_send', function($email_notification, $submission_data, $form_data) { - $form_id = $form_data['form_id'] ?? 0; - - // Only send email for specific form if user opted in - if ($form_id === 123) { - $opted_in = $submission_data['email_optin']['value'] ?? false; - if (!$opted_in) { - return false; // Skip email - } - } - - return $email_notification; -}, 10, 3); -``` - -### `srfm_email_notification` - -Filters parsed email data before sending. - -**Location:** [inc/form-submit.php:809](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $parsed` - Parsed email data (to, subject, message, headers) -- `array $submission_data` - Submitted form data -- `array $item` - Email notification configuration -- `array $form_data` - Form metadata - -**Return:** `array` - Modified email data - -**Usage:** -```php -add_filter('srfm_email_notification', function($parsed, $submission_data, $item, $form_data) { - // Add custom header - $parsed['headers'][] = 'X-Form-ID: ' . ($form_data['form_id'] ?? 0); - - // Modify subject based on submission data - if (isset($submission_data['priority']['value']) && $submission_data['priority']['value'] === 'high') { - $parsed['subject'] = '[URGENT] ' . $parsed['subject']; - } - - // Add custom footer to message - $parsed['message'] .= "\n\n---\nSubmitted via SureForms"; - - return $parsed; -}, 10, 4); -``` - -### `srfm_before_email_send` - -Action fired immediately before email is sent via `wp_mail()`. - -**Location:** [inc/form-submit.php:812](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $parsed` - Email data (to, subject, message, headers) -- `array $submission_data` - Submitted form data -- `array $item` - Email notification configuration -- `array $form_data` - Form metadata - -**Usage:** -```php -add_action('srfm_before_email_send', function($parsed, $submission_data, $item, $form_data) { - // Log email send attempt - error_log(sprintf( - 'Sending email to %s for form %d', - $parsed['to'], - $form_data['form_id'] ?? 0 - )); - - // Save email to custom table for tracking - global $wpdb; - $wpdb->insert('wp_email_log', [ - 'recipient' => $parsed['to'], - 'subject' => $parsed['subject'], - 'sent_at' => current_time('mysql') - ]); -}, 10, 4); -``` - -### `srfm_after_email_send` - -Action fired after email is sent. - -**Location:** [inc/form-submit.php:904-905](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `array $parsed` - Email data -- `array $submission_data` - Form data -- `array $item` - Email config -- `array $form_data` - Form metadata -- `bool $sent` - Whether email was sent successfully - -**Usage:** -```php -add_action('srfm_after_email_send', function($parsed, $submission_data, $item, $form_data, $sent) { - if (!$sent) { - // Email failed to send - error_log(sprintf( - 'Email failed for form %d to %s', - $form_data['form_id'] ?? 0, - $parsed['to'] - )); - - // Send alert to admin - wp_mail( - get_option('admin_email'), - 'Form email notification failed', - sprintf('Failed to send notification for form %d', $form_data['form_id']) - ); - } -}, 10, 5); -``` - ---- - -## Data Modification - -### `srfm_form_submit_data` - -Filters complete form submission data before processing. - -**Location:** [inc/form-submit.php:474](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $form_data` - Complete form data - -**Return:** `array` - Modified form data - -**Usage:** -```php -add_filter('srfm_form_submit_data', function($form_data) { - // Add custom calculated field - if (isset($form_data['price'], $form_data['quantity'])) { - $form_data['total'] = [ - 'value' => $form_data['price']['value'] * $form_data['quantity']['value'], - 'label' => 'Total Amount', - 'type' => 'calculated' - ]; - } - - // Add timestamp - $form_data['submitted_timestamp'] = [ - 'value' => current_time('mysql'), - 'label' => 'Submission Time', - 'type' => 'hidden' - ]; - - return $form_data; -}, 10, 1); -``` - -### `srfm_before_entry_data` - -Filters entry data before saving to database. - -**Location:** [inc/form-submit.php:605-606](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `array $entries_data` - Entry data to be saved -- `int $form_id` - Form post ID - -**Return:** `array` - Modified entry data - -**Usage:** -```php -add_filter('srfm_before_entry_data', function($entries_data, $form_id) { - // Add custom extras data - $entries_data['extras'] = [ - 'ip_country' => get_user_ip_country(), - 'utm_source' => $_GET['utm_source'] ?? '', - 'utm_campaign' => $_GET['utm_campaign'] ?? '', - 'referrer' => wp_get_referer() - ]; - - // Modify status based on conditions - if ($form_id === 123) { - $entries_data['status'] = 'pending_review'; // Custom status - } - - return $entries_data; -}, 10, 2); -``` - -### `srfm_form_submission_response` - -Filters final submission response sent to frontend. - -**Location:** [inc/form-submit.php:660](../../inc/form-submit.php) -**Since:** 2.4.0 -**Parameters:** -- `array $response` - Response data (success, message, entry_id) -- `array $form_data` - Form data -- `array $submission_data` - Submission data - -**Return:** `array` - Modified response - -**Usage:** -```php -add_filter('srfm_form_submission_response', function($response, $form_data, $submission_data) { - // Add custom data to response - if ($response['success']) { - $response['custom_redirect_url'] = 'https://example.com/thank-you'; - $response['tracking_code'] = 'FB-' . $response['entry_id']; - - // Add custom message - $response['custom_message'] = sprintf( - 'Thank you! Your reference number is %d', - $response['entry_id'] - ); - } - - return $response; -}, 10, 3); -``` - -### `srfm_process_field_value` - -Filters individual field value during processing. - -**Location:** [inc/form-submit.php:1211-1212](../../inc/form-submit.php) -**Since:** 0.0.1 -**Parameters:** -- `mixed $value` - Field value -- `array $field` - Field data -- `array $form_data` - Complete form data - -**Return:** `mixed` - Modified field value - -**Usage:** -```php -add_filter('srfm_process_field_value', function($value, $field, $form_data) { - // Normalize phone numbers - if ($field['type'] === 'phone') { - $value = preg_replace('/[^0-9+]/', '', $value); - } - - // Convert text to uppercase for specific field - if ($field['slug'] === 'reference_code') { - $value = strtoupper($value); - } - - // Encrypt sensitive data - if ($field['slug'] === 'ssn') { - $value = encrypt_data($value); - } - - return $value; -}, 10, 3); -``` - ---- - -## Form Rendering - -### `srfm_form_markup` - -Filters complete form HTML before rendering. - -**Location:** [inc/generate-form-markup.php](../../inc/generate-form-markup.php) -**Since:** 0.0.1 -**Parameters:** -- `string $markup` - Generated HTML -- `int $form_id` - Form post ID -- `array $blocks` - Gutenberg blocks array - -**Return:** `string` - Modified HTML - -**Usage:** -```php -add_filter('srfm_form_markup', function($markup, $form_id, $blocks) { - // Add custom wrapper - $markup = '
' . $markup . '
'; - - // Add custom analytics tracking - $markup .= sprintf( - '', - $form_id - ); - - return $markup; -}, 10, 3); -``` - -### `srfm_field_markup` - -Filters individual field HTML. - -**Location:** [inc/fields/base.php](../../inc/fields/base.php) -**Since:** 0.0.1 -**Parameters:** -- `string $markup` - Field HTML -- `array $attributes` - Block attributes -- `string $field_type` - Field type (input, email, etc.) - -**Return:** `string` - Modified HTML - -**Usage:** -```php -add_filter('srfm_field_markup', function($markup, $attributes, $field_type) { - // Add custom class to all email fields - if ($field_type === 'email') { - $markup = str_replace( - 'class="srfm-field', - 'class="srfm-field custom-email-field', - $markup - ); - } - - // Add tooltips - if (!empty($attributes['help_text'])) { - $markup .= sprintf( - '%s', - esc_html($attributes['help_text']) - ); - } - - return $markup; -}, 10, 3); -``` - -### `srfm_form_class` - -Filters CSS classes applied to form element. - -**Location:** [inc/generate-form-markup.php](../../inc/generate-form-markup.php) -**Since:** 0.0.1 -**Parameters:** -- `array $classes` - CSS class array -- `int $form_id` - Form post ID - -**Return:** `array` - Modified classes - -**Usage:** -```php -add_filter('srfm_form_class', function($classes, $form_id) { - // Add custom class - $classes[] = 'custom-form'; - - // Add class based on form ID - $classes[] = 'form-' . $form_id; - - // Add class based on form type - $form_type = get_post_meta($form_id, '_srfm_form_type', true); - if ($form_type === 'contact') { - $classes[] = 'contact-form'; - } - - return $classes; -}, 10, 2); -``` - ---- - -## Database Operations - -### `srfm_before_delete_entry` - -Action fired before entry is permanently deleted from database. - -**Location:** [inc/database/tables/entries.php:297](../../inc/database/tables/entries.php) -**Since:** 0.0.13 -**Parameters:** -- `int $entry_id` - Entry ID being deleted - -**Usage:** -```php -add_action('srfm_before_delete_entry', function($entry_id) { - // Log deletion - error_log(sprintf('Deleting entry %d', $entry_id)); - - // Clean up related data - global $wpdb; - $wpdb->delete('wp_custom_entry_meta', ['entry_id' => $entry_id]); - - // Archive entry before deletion - $entry = SRFM\Inc\Database\Tables\Entries::get($entry_id); - if ($entry) { - $wpdb->insert('wp_deleted_entries_archive', [ - 'entry_id' => $entry_id, - 'form_data' => wp_json_encode($entry), - 'deleted_at' => current_time('mysql') - ]); - } -}, 10, 1); -``` - ---- - -## Payment Processing - -### `srfm_before_payment_process` - -Action fired before payment processing starts. - -**Location:** [inc/payments/front-end.php](../../inc/payments/front-end.php) -**Since:** 2.0.0 -**Parameters:** -- `array $payment_data` - Payment data -- `int $form_id` - Form post ID - -**Usage:** -```php -add_action('srfm_before_payment_process', function($payment_data, $form_id) { - // Log payment attempt - error_log(sprintf( - 'Processing payment of %s %s for form %d', - $payment_data['amount'], - $payment_data['currency'], - $form_id - )); - - // Verify inventory before charging - if ($form_id === 123) { - $product_id = $payment_data['product_id'] ?? 0; - if (!check_inventory($product_id)) { - throw new Exception('Product out of stock'); - } - } -}, 10, 2); -``` - -### `srfm_after_payment_success` - -Action fired after successful payment. - -**Location:** [inc/payments/front-end.php](../../inc/payments/front-end.php) -**Since:** 2.0.0 -**Parameters:** -- `int $payment_id` - Payment record ID -- `array $payment_data` - Payment data -- `int $entry_id` - Entry ID - -**Usage:** -```php -add_action('srfm_after_payment_success', function($payment_id, $payment_data, $entry_id) { - // Send receipt - $customer_email = $payment_data['customer_email']; - wp_mail( - $customer_email, - 'Payment Receipt', - sprintf('Payment of %s received', $payment_data['total_amount']) - ); - - // Update inventory - update_inventory($payment_data['product_id'], -1); - - // Trigger fulfillment - trigger_order_fulfillment($payment_id); -}, 10, 3); -``` - ---- - -## Block Registration - -### `srfm_block_registration_args` - -Filters block registration arguments. - -**Location:** [inc/blocks/register.php](../../inc/blocks/register.php) -**Since:** 0.0.1 -**Parameters:** -- `array $args` - Block registration args -- `string $block_name` - Block name (sureforms/input, etc.) - -**Return:** `array` - Modified args - -**Usage:** -```php -add_filter('srfm_block_registration_args', function($args, $block_name) { - // Modify render callback - if ($block_name === 'sureforms/input') { - $original_callback = $args['render_callback']; - $args['render_callback'] = function($attributes, $content) use ($original_callback) { - // Wrap output in custom div - $output = call_user_func($original_callback, $attributes, $content); - return '
' . $output . '
'; - }; - } - - return $args; -}, 10, 2); -``` - ---- - -## Admin Operations - -### `srfm_after_form_created` - -Action fired after new form is created. - -**Location:** [inc/create-new-form.php](../../inc/create-new-form.php) -**Since:** 0.0.1 -**Parameters:** -- `int $form_id` - Created form post ID - -**Usage:** -```php -add_action('srfm_after_form_created', function($form_id) { - // Set default form settings - update_post_meta($form_id, '_srfm_custom_setting', 'default_value'); - - // Log form creation - error_log(sprintf('New form created: %d', $form_id)); - - // Notify admin - wp_mail( - get_option('admin_email'), - 'New form created', - sprintf('Form ID %d was created', $form_id) - ); -}, 10, 1); -``` - ---- - -## Form Restrictions - -### `srfm_form_restriction_message` - -Filters restriction message shown to users. - -**Location:** [inc/form-submit.php:267](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `string $message` - Default restriction message -- `int $form_id` - Form post ID -- `array $form_restriction` - Restriction settings - -**Return:** `string` - Modified message - -**Usage:** -```php -add_filter('srfm_form_restriction_message', function($message, $form_id, $form_restriction) { - // Custom message based on restriction type - if ($form_restriction['type'] === 'schedule' && $form_restriction['state'] === 'not_started') { - $message = sprintf( - 'This form will open on %s', - date('F j, Y', strtotime($form_restriction['start_date'])) - ); - } - - return $message; -}, 10, 3); -``` - -### `srfm_additional_restriction_check` - -Filters whether form has additional restrictions beyond scheduling. - -**Location:** [inc/form-submit.php:276](../../inc/form-submit.php) -**Since:** Various -**Parameters:** -- `bool $is_restricted` - Whether form is restricted -- `int $form_id` - Form post ID -- `array $form_data` - Form data - -**Return:** `bool` - True to restrict, false to allow - -**Usage:** -```php -add_filter('srfm_additional_restriction_check', function($is_restricted, $form_id, $form_data) { - // Restrict based on user role - if ($form_id === 123 && !current_user_can('subscriber')) { - return true; // Restrict - } - - // Restrict based on IP - $user_ip = $_SERVER['REMOTE_ADDR']; - if (is_ip_blocked($user_ip)) { - return true; - } - - // Restrict based on previous submissions - $user_email = $form_data['email']['value'] ?? ''; - if ($user_email && has_recent_submission($form_id, $user_email)) { - return true; // Already submitted recently - } - - return $is_restricted; -}, 10, 3); -``` - ---- - -## Integration Points - -### Third-Party Service Integration Example - -```php -/** - * Complete integration example: Send form data to CRM after submission - */ -class SureForms_CRM_Integration { - public function __construct() { - add_action('srfm_after_submission_process', [$this, 'send_to_crm'], 10, 1); - add_action('srfm_before_delete_entry', [$this, 'delete_from_crm'], 10, 1); - } - - public function send_to_crm($form_data) { - // Only for contact forms - if ($form_data['form_id'] !== 123) { - return; - } - - $crm_data = [ - 'email' => $form_data['email']['value'] ?? '', - 'name' => $form_data['name']['value'] ?? '', - 'phone' => $form_data['phone']['value'] ?? '', - 'company' => $form_data['company']['value'] ?? '', - 'source' => 'sureforms', - 'entry_id' => $form_data['entry_id'] ?? 0 - ]; - - $response = wp_remote_post('https://crm.example.com/api/leads', [ - 'body' => json_encode($crm_data), - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . get_option('crm_api_key') - ] - ]); - - if (is_wp_error($response)) { - error_log('CRM sync failed: ' . $response->get_error_message()); - } else { - $crm_id = json_decode(wp_remote_retrieve_body($response))->id; - // Store CRM ID with entry - update_post_meta($form_data['entry_id'], '_crm_lead_id', $crm_id); - } - } - - public function delete_from_crm($entry_id) { - $crm_id = get_post_meta($entry_id, '_crm_lead_id', true); - if ($crm_id) { - wp_remote_request('https://crm.example.com/api/leads/' . $crm_id, [ - 'method' => 'DELETE', - 'headers' => [ - 'Authorization' => 'Bearer ' . get_option('crm_api_key') - ] - ]); - } - } -} - -new SureForms_CRM_Integration(); -``` - ---- - -## Hook Priority Guidelines - -**Priority Values:** -- **5:** Execute before default behavior -- **10:** Default priority (most hooks) -- **15:** Execute after default behavior -- **20+:** Late execution -- **100+:** Very late (cleanup, logging) - -**Example:** -```php -// Execute before SureForms processes submission -add_action('srfm_before_submission', 'my_function', 5); - -// Execute at default time -add_action('srfm_before_submission', 'my_function', 10); - -// Execute after SureForms processes submission -add_action('srfm_before_submission', 'my_function', 15); -``` - ---- - -## Debugging Hooks - -### Enable Hook Debugging - -```php -// Add to wp-config.php -define('SRFM_DEBUG_HOOKS', true); - -// Log all SureForms hooks -add_action('all', function($hook) { - if (strpos($hook, 'srfm_') === 0) { - error_log('Hook fired: ' . $hook); - } -}); -``` - -### View Hook Execution Order - -```php -// Log hook priority and callback -add_action('all', function($hook) { - if (strpos($hook, 'srfm_') === 0) { - global $wp_filter; - if (isset($wp_filter[$hook])) { - foreach ($wp_filter[$hook]->callbacks as $priority => $callbacks) { - foreach ($callbacks as $callback) { - error_log(sprintf( - 'Hook: %s, Priority: %d, Callback: %s', - $hook, - $priority, - is_array($callback['function']) ? - get_class($callback['function'][0]) . '::' . $callback['function'][1] : - $callback['function'] - )); - } - } - } - } -}); -``` - ---- - -## Best Practices - -### 1. Always Check Hook Existence - -```php -if (function_exists('add_action')) { - add_action('srfm_after_submission_process', 'my_function'); -} -``` - -### 2. Use Proper Nonces for Custom Forms - -```php -add_filter('srfm_before_entry_data', function($data, $form_id) { - // Verify custom nonce if adding custom fields - if (isset($_POST['custom_nonce'])) { - if (!wp_verify_nonce($_POST['custom_nonce'], 'custom_action')) { - return new WP_Error('nonce_failed', 'Security check failed'); - } - } - return $data; -}, 10, 2); -``` - -### 3. Return Correct Data Types - -```php -// Filter must return array -add_filter('srfm_form_submit_data', function($data) { - // ✅ GOOD - returns array - return array_merge($data, ['custom' => 'value']); - - // ❌ BAD - returns nothing - // (void return breaks functionality) -}, 10, 1); -``` - -### 4. Handle Errors Gracefully - -```php -add_action('srfm_after_submission_process', function($form_data) { - try { - // Risky operation - send_to_external_api($form_data); - } catch (Exception $e) { - error_log('External API failed: ' . $e->getMessage()); - // Don't throw - allow submission to continue - } -}, 10, 1); -``` - ---- - -## Summary - -**Hook Categories:** -- **Lifecycle:** 2 hooks (core_loaded, init) -- **Submission:** 10+ hooks (before, during, after) -- **Validation:** 5+ hooks (field-specific and general) -- **Email:** 5+ hooks (before, during, after send) -- **Rendering:** 10+ hooks (markup, classes, attributes) -- **Data:** 8+ hooks (modify, filter, transform) -- **Database:** 3+ hooks (before/after operations) -- **Payment:** 6+ hooks (process, success, failure) -- **Admin:** 5+ hooks (form creation, updates) - -**Total:** 450+ hooks across the plugin - -**Auto-Generation Note:** This document contains the most commonly used hooks. For a complete list, run the hook extraction script: -```bash -php scripts/extract-hooks.php > docs/COMPLETE-HOOKS-LIST.md -``` - ---- - -**Related Documentation:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [ARCHITECTURE.md](../02-architecture/ARCHITECTURE.md) - System design -- [REST-API.md](REST-API.md) - REST API endpoints -- [DATABASE-SCHEMA.md](DATABASE-SCHEMA.md) - Database tables diff --git a/docs/05-frontend/FRONTEND-GUIDE.md b/docs/05-frontend/FRONTEND-GUIDE.md deleted file mode 100644 index d26f09ee8..000000000 --- a/docs/05-frontend/FRONTEND-GUIDE.md +++ /dev/null @@ -1,1197 +0,0 @@ -# SureForms Frontend Development Guide - -> **Purpose:** Comprehensive guide for React/Gutenberg block development -> **Audience:** Frontend developers working with React, Gutenberg, and WordPress -> **Last Updated:** 2026-02-06 -> **Plugin Version:** 2.5.0 - -## Overview - -SureForms frontend is built with modern JavaScript technologies integrated deeply with WordPress's Gutenberg block editor: - -- **React 18.2:** UI components and interactive features -- **Gutenberg Blocks:** WordPress block editor integration for form building -- **TailwindCSS 3.4:** Utility-first CSS framework -- **React Query 5.x:** Server state management and caching -- **@wordpress/scripts 26.2:** Build tooling (Webpack 5 wrapper) - ---- - -## Table of Contents - -- [Architecture Overview](#architecture-overview) -- [Directory Structure](#directory-structure) -- [Creating Gutenberg Blocks](#creating-gutenberg-blocks) -- [State Management](#state-management) -- [Component Patterns](#component-patterns) -- [Styling](#styling) -- [Form Validation](#form-validation) -- [Internationalization](#internationalization) -- [Performance Optimization](#performance-optimization) -- [Testing](#testing) - ---- - -## Architecture Overview - -### Technology Stack - -``` -┌─────────────────────────────────────────┐ -│ Admin UI (React Apps) │ -│ Dashboard, Settings, Entries, Editor │ -└──────────────┬──────────────────────────┘ - │ - ┌──────────┴──────────┐ - │ │ -┌───▼────────┐ ┌──────▼──────┐ -│ Gutenberg │ │ Admin │ -│ Blocks │ │ Pages │ -│ (Editor) │ │ (Router) │ -└───┬────────┘ └──────┬──────┘ - │ │ - └──────────┬─────────┘ - │ - ┌──────────▼──────────┐ - │ State Management │ - │ Data Store + Hooks │ - └──────────┬──────────┘ - │ - ┌──────────▼──────────┐ - │ REST API │ - │ /sureforms/v1/* │ - └─────────────────────┘ -``` - -### Component Layers - -1. **Gutenberg Block Layer** - Form builder blocks (src/blocks/) -2. **Admin UI Layer** - React SPA pages (src/admin/) -3. **Shared Components** - Reusable UI (src/components/) -4. **State Layer** - Data stores (src/store/) -5. **API Layer** - WordPress REST API - ---- - -## Directory Structure - -``` -src/ -├── blocks/ # Gutenberg block components -│ ├── blocks.js # Block registration entry -│ ├── register-block.js # Block utilities -│ │ -│ ├── sform/ # Form container block -│ │ ├── index.js # Block registration -│ │ ├── edit.js # Editor component -│ │ ├── save.js # Frontend save (returns null) -│ │ ├── block.json # Block metadata -│ │ ├── components/ # Block-specific components -│ │ └── style.scss # Block styles -│ │ -│ ├── input/ # Text input field block -│ │ ├── edit.js # Editor with InspectorControls -│ │ ├── save.js # Server-side rendering -│ │ └── block.json # Attributes schema -│ │ -│ └── [14+ more blocks] # Email, textarea, checkbox, etc. -│ -├── admin/ # Admin dashboard React apps -│ ├── dashboard/ # Forms listing page -│ │ ├── index.js # Entry point -│ │ ├── FormsTable.js # Table component -│ │ ├── Analytics.js # Analytics widgets -│ │ └── styles.scss -│ │ -│ ├── single-form-settings/ # Form editor -│ │ ├── Editor.js # Main editor wrapper -│ │ ├── InstantForm.js # Live preview -│ │ ├── tabs/ # Settings tabs -│ │ │ ├── GeneralSettings.js -│ │ │ └── StyleSettings.js -│ │ └── components/ # Editor components -│ │ -│ ├── entries/ # Entry management -│ ├── settings/ # Global settings -│ ├── payment/ # Payment settings -│ └── onboarding/ # Setup wizard -│ -├── components/ # Shared React components -│ ├── ui/ # UI primitives -│ │ ├── Button/ -│ │ ├── Input/ -│ │ ├── Modal/ -│ │ └── Tooltip/ -│ │ -│ ├── form/ # Form-specific -│ │ ├── FieldWrapper/ -│ │ ├── ValidationMessage/ -│ │ └── FormContainer/ -│ │ -│ ├── inspector-tabs/ # Block editor tabs -│ ├── color-switch-control/ # Color picker -│ ├── spacing-control/ # Responsive spacing -│ └── [40+ component dirs] -│ -├── srfm-controls/ # Gutenberg block controls -│ ├── index.js # Control registry -│ ├── getPreviewType.js # Responsive preview -│ └── [18+ control definitions] -│ -├── store/ # WordPress Data Store (Redux) -│ ├── store.js # Store creation -│ ├── actions.js # Action creators -│ ├── reducer.js # Reducer function -│ ├── selectors.js # Selectors -│ └── constants.js # Action types -│ -├── utils/ # Utility functions -│ ├── Helpers.js # Common helpers -│ ├── datePickerHelper.js -│ └── addressFieldHelper.js -│ -└── styles/ # Global styles - ├── typography/ - └── breakpoints/ -``` - -**Build Output:** -``` -assets/build/ -├── formEditor.js # Form editor bundle (~1.9MB) -├── blocks.js # Gutenberg blocks (~1.3MB) -├── dashboard.js # Dashboard page (~1.3MB) -├── entries.js # Entries page (~728KB) -├── forms.js # Forms listing (~727KB) -├── settings.js # Settings page (~1.1MB) -├── formSubmit.js # Frontend submission (~60KB) -└── *.asset.php # Webpack dependency manifests -``` - ---- - -## Creating Gutenberg Blocks - -### Block Anatomy - -Every form field block consists of: -1. **block.json** - Metadata and attributes schema -2. **edit.js** - Editor component (React) -3. **save.js** - Frontend rendering (usually null, PHP renders) -4. **style.scss** - Block-specific styles -5. **PHP class** - Server-side registration and rendering - -### Step-by-Step: Create a New Block - -#### Step 1: Create Block Directory - -```bash -mkdir -p src/blocks/custom-field -cd src/blocks/custom-field -``` - -#### Step 2: Create block.json - -**File:** [src/blocks/custom-field/block.json](../../src/blocks/custom-field/block.json) - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, - "name": "sureforms/custom-field", - "title": "Custom Field", - "category": "sureforms", - "icon": "text", - "description": "A custom form field", - "keywords": ["form", "custom", "field"], - "supports": { - "html": false, - "anchor": false, - "customClassName": false - }, - "attributes": { - "label": { - "type": "string", - "default": "Custom Field" - }, - "slug": { - "type": "string", - "default": "custom_field" - }, - "placeholder": { - "type": "string", - "default": "" - }, - "required": { - "type": "boolean", - "default": false - }, - "helpText": { - "type": "string", - "default": "" - } - }, - "editorScript": "file:./index.js", - "editorStyle": "file:./editor.scss", - "style": "file:./style.scss" -} -``` - -#### Step 3: Create edit.js (Editor Component) - -**File:** [src/blocks/custom-field/edit.js](../../src/blocks/custom-field/edit.js) - -```javascript -import { __ } from '@wordpress/i18n'; -import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; -import { PanelBody, TextControl, ToggleControl, TextareaControl } from '@wordpress/components'; - -export default function Edit({ attributes, setAttributes, clientId }) { - const { label, slug, placeholder, required, helpText } = attributes; - - const blockProps = useBlockProps({ - className: 'srfm-block srfm-custom-field-block', - }); - - return ( - <> - {/* Inspector Panel (Right Sidebar) */} - - - setAttributes({ label: value })} - help={__('Field label shown to users', 'sureforms')} - /> - - setAttributes({ slug: value })} - help={__('Unique identifier for this field', 'sureforms')} - /> - - setAttributes({ placeholder: value })} - /> - - setAttributes({ required: value })} - /> - - setAttributes({ helpText: value })} - help={__('Additional instructions for users', 'sureforms')} - /> - - - - {/* Block Preview in Editor */} -
-
- - - - - {helpText && ( - - {helpText} - - )} -
-
- - ); -} -``` - -#### Step 4: Create save.js - -**File:** [src/blocks/custom-field/save.js](../../src/blocks/custom-field/save.js) - -```javascript -/** - * Save function - return null for dynamic blocks (rendered server-side). - * - * SureForms blocks are rendered server-side via PHP for: - * - Better security (no client-side data exposure) - * - Consistent rendering across contexts - * - Easier form submission handling - */ -export default function save() { - return null; -} -``` - -#### Step 5: Create index.js (Registration) - -**File:** [src/blocks/custom-field/index.js](../../src/blocks/custom-field/index.js) - -```javascript -import { registerBlockType } from '@wordpress/blocks'; -import Edit from './edit'; -import save from './save'; -import metadata from './block.json'; - -// Register block -registerBlockType(metadata.name, { - ...metadata, - edit: Edit, - save, -}); -``` - -#### Step 6: Add to blocks.js - -**File:** [src/blocks/blocks.js](../../src/blocks/blocks.js) - -```javascript -// ... other imports -import './custom-field'; -``` - -#### Step 7: Create PHP Server-Side Rendering - -**File:** [inc/blocks/custom-field/class-custom-field.php](../../inc/blocks/custom-field/class-custom-field.php) - -```php - -
- - - - - /> - - - - - - -
- { - setValue(e.target.value); - - // Validate - if (e.target.value.length < 3) { - setError('Minimum 3 characters'); - } else { - setError(''); - } - }; - - return ( -
- - {error && {error}} -
- ); -} -``` - -### 2. WordPress Data Store (Redux-based) - -For block editor state and form metadata. - -**Define Store** ([src/store/store.js](../../src/store/store.js)): - -```javascript -import { createReduxStore, register } from '@wordpress/data'; -import apiFetch from '@wordpress/api-fetch'; - -const DEFAULT_STATE = { - formSettings: {}, - blockAttributes: {}, - isLoading: false, -}; - -const actions = { - setFormSettings(settings) { - return { - type: 'SET_FORM_SETTINGS', - settings, - }; - }, - - updateBlockAttribute(blockId, attribute, value) { - return { - type: 'UPDATE_BLOCK_ATTRIBUTE', - blockId, - attribute, - value, - }; - }, - - *fetchFormSettings(formId) { - yield { type: 'SET_LOADING', isLoading: true }; - - try { - const settings = yield apiFetch({ - path: `/sureforms/v1/forms/${formId}/settings`, - }); - - return actions.setFormSettings(settings); - } catch (error) { - console.error('Failed to fetch settings:', error); - return { type: 'FETCH_ERROR', error }; - } - }, -}; - -const selectors = { - getFormSettings(state) { - return state.formSettings; - }, - - getBlockAttribute(state, blockId, attribute) { - return state.blockAttributes[blockId]?.[attribute]; - }, - - isLoading(state) { - return state.isLoading; - }, -}; - -const reducer = (state = DEFAULT_STATE, action) => { - switch (action.type) { - case 'SET_FORM_SETTINGS': - return { - ...state, - formSettings: action.settings, - isLoading: false, - }; - - case 'UPDATE_BLOCK_ATTRIBUTE': - return { - ...state, - blockAttributes: { - ...state.blockAttributes, - [action.blockId]: { - ...state.blockAttributes[action.blockId], - [action.attribute]: action.value, - }, - }, - }; - - case 'SET_LOADING': - return { - ...state, - isLoading: action.isLoading, - }; - - default: - return state; - } -}; - -export const store = createReduxStore('srfm/forms', { - reducer, - actions, - selectors, -}); - -register(store); -``` - -**Use Store in Components:** - -```javascript -import { useSelect, useDispatch } from '@wordpress/data'; - -function FormEditor({ formId }) { - // Select data from store - const { formSettings, isLoading } = useSelect((select) => ({ - formSettings: select('srfm/forms').getFormSettings(), - isLoading: select('srfm/forms').isLoading(), - })); - - // Dispatch actions to store - const { fetchFormSettings, setFormSettings } = useDispatch('srfm/forms'); - - useEffect(() => { - fetchFormSettings(formId); - }, [formId]); - - if (isLoading) { - return ; - } - - return ( -
-

{formSettings.title}

- {/* Editor UI */} -
- ); -} -``` - -### 3. React Query (Server State) - -For API responses and caching. - -**Setup Query Client** ([src/admin/dashboard/index.js](../../src/admin/dashboard/index.js)): - -```javascript -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, // 5 minutes - retry: 1, - }, - }, -}); - -function App() { - return ( - - - - ); -} -``` - -**Create Custom Hooks** ([src/hooks/useFormData.js](../../src/hooks/)): - -```javascript -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import apiFetch from '@wordpress/api-fetch'; - -// Fetch form data -export function useFormData(formId) { - return useQuery({ - queryKey: ['form', formId], - queryFn: async () => { - const response = await apiFetch({ - path: `/sureforms/v1/forms/${formId}`, - }); - return response; - }, - enabled: !!formId, - }); -} - -// Update form data -export function useUpdateForm() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ formId, data }) => { - return await apiFetch({ - path: `/sureforms/v1/forms/${formId}`, - method: 'PUT', - data, - }); - }, - onSuccess: (data, variables) => { - // Invalidate and refetch - queryClient.invalidateQueries(['form', variables.formId]); - }, - }); -} - -// Fetch entries -export function useEntries(formId, page = 1) { - return useQuery({ - queryKey: ['entries', formId, page], - queryFn: async () => { - return await apiFetch({ - path: `/sureforms/v1/forms/${formId}/entries?page=${page}`, - }); - }, - keepPreviousData: true, // For pagination - }); -} -``` - -**Use in Components:** - -```javascript -function FormEditor({ formId }) { - const { data: form, isLoading, error } = useFormData(formId); - const updateForm = useUpdateForm(); - - const handleSave = async () => { - try { - await updateForm.mutateAsync({ - formId, - data: { title: 'Updated Title' }, - }); - // Success toast - toast.success('Form saved!'); - } catch (error) { - // Error toast - toast.error('Failed to save form'); - } - }; - - if (isLoading) return ; - if (error) return
Error: {error.message}
; - - return ( -
-

{form.title}

- -
- ); -} -``` - ---- - -## Component Patterns - -### Functional Components with Hooks - -**Always use functional components** (not class components): - -```javascript -// ✅ GOOD - Functional component -function MyComponent({ prop1, prop2 }) { - const [state, setState] = useState(null); - - useEffect(() => { - // Side effects - }, []); - - return
{/* JSX */}
; -} - -// ❌ BAD - Class component (deprecated pattern) -class MyComponent extends React.Component { - // Don't use class components in SureForms -} -``` - -### Props Destructuring - -```javascript -// ✅ GOOD - Destructure in signature -function Button({ label, onClick, disabled = false, variant = 'primary' }) { - return ( - - ); -} - -// ❌ BAD - Access via props object -function Button(props) { - return ; -} -``` - -### Conditional Rendering - -```javascript -function FormField({ label, required, error }) { - return ( -
- - - {/* Ternary for if/else */} - {error ? ( - {error} - ) : ( - Optional - )} - - {/* Conditional with && */} - {!error && Valid} -
- ); -} -``` - -### Event Handlers - -```javascript -function FormContainer() { - // useCallback for stable reference - const handleSubmit = useCallback((event) => { - event.preventDefault(); - // Handle submit - }, []); - - // Arrow function for inline with parameters - const handleFieldChange = useCallback((fieldId, value) => { - setFormData((prev) => ({ - ...prev, - [fieldId]: value, - })); - }, []); - - return ( -
- handleFieldChange('name', e.target.value)} - /> -
- ); -} -``` - ---- - -## Styling - -### TailwindCSS (Preferred) - -Use Tailwind utility classes for rapid development: - -```javascript -function Card({ title, children }) { - return ( -
-

- {title} -

-
- {children} -
-
- ); -} -``` - -**Responsive Design:** - -```javascript -
- {/* Full width mobile, half on tablet, third on desktop */} -
- -
- {/* Responsive text size */} -
-``` - -### Custom SCSS (When Needed) - -**Use BEM methodology** with `.srfm-` prefix: - -```scss -// style.scss -.srfm-custom-field { - // Block - &__label { - // Element - @apply block text-sm font-medium text-gray-700 mb-2; - - .required { - // Nested element - @apply text-red-500 ml-1; - } - } - - &__input { - // Element - @apply w-full px-4 py-2 border border-gray-300 rounded-md; - @apply focus:outline-none focus:ring-2 focus:ring-blue-500; - - &:disabled { - // Modifier via pseudo-class - @apply bg-gray-100 cursor-not-allowed; - } - } - - &--error { - // Block modifier - .srfm-custom-field__input { - @apply border-red-500; - } - } -} -``` - -### Conditional Classes (classnames) - -```javascript -import classnames from 'classnames'; - -function Button({ variant, size, disabled, className }) { - const classes = classnames( - 'srfm-button', - `srfm-button--${variant}`, - `srfm-button--${size}`, - { - 'srfm-button--disabled': disabled, - }, - className // Allow override - ); - - return ; -} -``` - ---- - -## Form Validation - -### Client-Side Validation (Frontend) - -**Inline validation:** - -```javascript -import { useState } from 'react'; - -function EmailField({ value, onChange, required }) { - const [error, setError] = useState(''); - - const validateEmail = (email) => { - const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return regex.test(email); - }; - - const handleBlur = () => { - if (required && !value) { - setError(__('Email is required', 'sureforms')); - } else if (value && !validateEmail(value)) { - setError(__('Please enter a valid email', 'sureforms')); - } else { - setError(''); - } - }; - - return ( -
- - {error && {error}} -
- ); -} -``` - -**Form-level validation:** - -```javascript -function FormContainer() { - const [formData, setFormData] = useState({}); - const [errors, setErrors] = useState({}); - - const validate = () => { - const newErrors = {}; - - if (!formData.name) { - newErrors.name = 'Name is required'; - } - - if (!formData.email || !isValidEmail(formData.email)) { - newErrors.email = 'Valid email is required'; - } - - if (formData.age && formData.age < 18) { - newErrors.age = 'Must be 18 or older'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validate()) { - return; - } - - // Submit form - try { - await submitForm(formData); - toast.success('Form submitted!'); - } catch (error) { - toast.error('Submission failed'); - } - }; - - return ( -
- {/* Form fields */} - -
- ); -} -``` - ---- - -## Internationalization (i18n) - -### Translation Functions - -```javascript -import { __, _n, sprintf } from '@wordpress/i18n'; - -// Simple translation -const label = __('Submit Form', 'sureforms'); - -// Plural forms -const message = _n( - 'One field is required', - '%d fields are required', - count, - 'sureforms' -); - -// String interpolation -const greeting = sprintf( - __('Hello, %s!', 'sureforms'), - userName -); -``` - -### Best Practices - -**✅ DO:** -```javascript -// Use sprintf for variables -const msg = sprintf(__('Form %s saved', 'sureforms'), formName); - -// Separate strings for different contexts -const saveButton = __('Save', 'sureforms'); -const saveForm = __('Save Form', 'sureforms'); -``` - -**❌ DON'T:** -```javascript -// Never use template literals in translations -const msg = __(`Form ${formName} saved`, 'sureforms'); // ❌ WRONG - -// Never concatenate -const msg = __('Form ', 'sureforms') + formName + __(' saved', 'sureforms'); // ❌ WRONG -``` - ---- - -## Performance Optimization - -### Code Splitting - -```javascript -import { lazy, Suspense } from 'react'; - -const HeavyComponent = lazy(() => import('./HeavyComponent')); - -function App() { - return ( - }> - - - ); -} -``` - -### Memoization - -```javascript -import { useMemo, useCallback } from 'react'; - -function DataTable({ data, onRowClick }) { - // Memoize expensive calculations - const sortedData = useMemo(() => { - console.log('Sorting data...'); - return data.sort((a, b) => a.name.localeCompare(b.name)); - }, [data]); // Only recompute when data changes - - // Memoize callbacks - const handleRowClick = useCallback( - (row) => { - onRowClick(row); - }, - [onRowClick] - ); - - return {/* Render sortedData */}
; -} -``` - -### React.memo - -```javascript -import { memo } from 'react'; - -// Prevent re-renders if props haven't changed -const ExpensiveComponent = memo(function ExpensiveComponent({ data }) { - console.log('Rendering...'); - return
{/* Complex rendering */}
; -}); -``` - ---- - -## Testing - -### Component Testing (Jest + React Testing Library) - -```javascript -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import FormField from '../FormField'; - -describe('FormField', () => { - it('renders with label', () => { - render(); - expect(screen.getByText('Name')).toBeInTheDocument(); - }); - - it('shows required indicator', () => { - render(); - expect(screen.getByText('*')).toBeInTheDocument(); - }); - - it('calls onChange when value changes', () => { - const handleChange = jest.fn(); - render(); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: 'test' } }); - - expect(handleChange).toHaveBeenCalledWith('test'); - }); - - it('displays error message', () => { - render(); - expect(screen.getByText('This field is required')).toBeInTheDocument(); - }); -}); -``` - ---- - -## Summary - -**Key Takeaways:** - -1. **Use React 18 functional components** with hooks -2. **Gutenberg blocks** are registered in JavaScript, rendered in PHP -3. **State management** uses three layers: local, WordPress Data Store, React Query -4. **Styling** prefers TailwindCSS utilities, custom SCSS for complex needs -5. **Validation** happens client-side (UX) and server-side (security) -6. **i18n** uses `@wordpress/i18n` functions, no template literals -7. **Performance** optimized with memoization, code splitting, virtualization - ---- - -**Related Documentation:** -- [CLAUDE.md](../../CLAUDE.md) - AI agent guide -- [GETTING-STARTED.md](../01-getting-started/GETTING-STARTED.md) - Setup -- [CODING-STANDARDS.md](../03-development/CODING-STANDARDS.md) - Code style -- [TESTING.md](../08-testing/TESTING.md) - Testing guide diff --git a/docs/FORM_MIGRATION_OVERVIEW.md b/docs/FORM_MIGRATION_OVERVIEW.md deleted file mode 100644 index dd7fac4d6..000000000 --- a/docs/FORM_MIGRATION_OVERVIEW.md +++ /dev/null @@ -1,77 +0,0 @@ -# SureForms — Form Migration (Overview) - -> Looking for the full technical plan? See `FORM_MIGRATION_PLAN.md` in this folder. - -## What this feature does - -Lets users move their existing forms into SureForms from the other popular form-builder plugins, in one click — no copy-pasting, no rebuilding. - -## Which plugins we'll support - -| Source plugin | Approx. user base | Status | -|---|---|---| -| Contact Form 7 | 10M+ sites | **Available in v1** | -| WPForms | 6M+ sites | Coming next | -| Gravity Forms | ~650K sites | Coming next | -| Ninja Forms | 600K+ sites | Later release | -| Caldera Forms | ~50K sites (unmaintained plugin) | Later release | - -Combined audience: roughly **17 million sites** that could move to SureForms. - -## What an import gives you - -When a user clicks "Import", we bring across: - -1. **The form's fields** — text, email, phone, dropdown, checkbox, radio, file URL, number, textarea, consent checkboxes, etc. They show up as native SureForms blocks, ready to edit in the Gutenberg editor. -2. **The form's title** — same as the original. -3. **A working admin notification email** — recipient is the site admin, subject mentions the form name, body uses `{all_data}` so submissions go straight to your inbox. -4. **A success message** — a clean "Thank you" confirmation shown after submission. - -The imported form works on day one. Users can polish it in SureForms' editor. - -## What doesn't transfer yet (and why) - -| Thing | Why not | -|---|---| -| File-upload fields | SureForms doesn't have a file-upload block yet — coming separately. | -| Quiz / signature / rating fields | No SureForms equivalent yet. | -| The exact email-body text from CF7 | CF7 uses a different placeholder syntax (`[your-name]` vs SureForms' smart tags). Trying to translate it gets unreliable, so we ship a clean default instead. WPForms and Gravity emails will come through fully when those are added. | -| Multi-step / page breaks | Waiting on a SureForms multi-step block. | - -When a field can't be migrated, the import screen lists it under **"Some fields were skipped"** so the user knows what to add by hand. - -## How users will find it - -A new **Migration** tab inside **SureForms → Settings**, at the bottom of the sidebar. The workflow is four short steps: - -1. **Choose a source** — tiles show each detected plugin and how many forms it has. -2. **Pick which forms** — checkbox list; "Select all" if you want everything. -3. **Preview** — see what the migrated form will look like before anything is saved. -4. **Confirm & import** — done. The result screen has direct "Edit in SureForms" links. - -If a user re-runs the import for the same source form, we **update** the existing SureForms version instead of creating a duplicate. - -## What's already built - -- The Migration tab is live in **SureForms → Settings → Migration** -- Contact Form 7 imports work end to end (fields + notification + confirmation) -- "Preview before importing" works — nothing gets saved until the user confirms -- Skipped fields are surfaced clearly to the user - -## What's next - -1. **WPForms importer** — including its full email-notification and confirmation rules -2. **Gravity Forms importer** — including notifications, scheduling, entry limits -3. **Ninja Forms + Caldera Forms importers** -4. **Entry migration** — bringing across historical form submissions (capped at 1,000 per form to keep imports fast) -5. **Filling in field gaps** — once SureForms ships file-upload, multi-step, and signature blocks, the migrator will start carrying those over too - -## Why we chose this approach - -We modelled the migrator on **Fluent Forms**' approach, which has been running in production on 700K+ sites for years. Same flow (source → forms → preview → import), same safety net (re-imports update rather than duplicate), and the same honest stance on edge cases like CF7 email templates. - -## Where to find things (for engineers) - -- Full technical plan with code structure, REST endpoints, field-mapping tables: `FORM_MIGRATION_PLAN.md` -- PHP code: `inc/migrator/` -- Admin tab UI: `src/admin/settings/migration/` diff --git a/docs/FORM_MIGRATION_PLAN.md b/docs/FORM_MIGRATION_PLAN.md deleted file mode 100644 index 9d7c2a9a8..000000000 --- a/docs/FORM_MIGRATION_PLAN.md +++ /dev/null @@ -1,458 +0,0 @@ -# SureForms — Form Migration Implementation Plan - -## 1. Goal - -Build a one-click migrator that imports forms (and optionally entries) from the 5 major WordPress form plugins into SureForms, modelled on Fluent Forms' production-tested migrator architecture (~5,400 LOC across 5 sources, deployed on 700K+ sites). - -**Sources to support in v1**: Contact Form 7, WPForms (Lite + Pro), Gravity Forms, Ninja Forms, Caldera Forms. - -## 2. Why Fluent Forms' pattern - -Fluent's full migrator source was read end-to-end (`fluentform/app/Services/Migrator/`). Their architecture is: - -- **One abstract base class** with the import loop, idempotency map, and a ~1,200-line canonical field dictionary. -- **Thin per-source subclasses** (500–900 LOC each) that only do (a) `source_type → fluent_id` mapping and (b) source-specific args formatting. -- **4 AJAX endpoints** for: list sources, list forms in source, import forms, import entries. -- **Template Method pattern** — base class drives the flow, subclasses fill in 5 abstract methods (`exist`, `get_source_forms`, `get_source_form_id`, `get_source_form_name`, `build_form_content`, `get_form_metas`). - -It's clean, proven, and easy to extend. SureForms will adopt the same shape, adapted for: - -1. **REST instead of AJAX** — SureForms already uses `/sureforms/v1/` namespace (`inc/rest-api.php`); AJAX would be inconsistent. -2. **Block markup output instead of JSON** — SureForms forms are Gutenberg block strings in `post_content` of the `sureforms_form` CPT, not JSON in a custom table. So the importer's final stage is to serialize block markup, not assemble a flat field array. -3. **15 SureForms blocks as targets** — instead of Fluent's ~25 field types. - -## 3. Sources to import — confirmed field type maps - -These are lifted directly from Fluent's `fieldTypes()` methods in each migrator class. Total source field types we need to handle: **110** (with overlap). - -### 3.1 Contact Form 7 (13 form-tag types → SureForms blocks) -Source: `wpcf7_contact_form` post type, `_form` meta (shortcode template). - -| CF7 form-tag | SureForms block | Notes | -|---|---|---| -| `text`, `text*` | `srfm/input` | type=text | -| `email`, `email*` | `srfm/email` | | -| `url`, `url*` | `srfm/url` | | -| `tel`, `tel*` | `srfm/phone` | | -| `number`, `number*` | `srfm/number` | | -| `range` | `srfm/number` | slider mode | -| `date` | `srfm/input` | type=date (no native date block) | -| `textarea`, `textarea*` | `srfm/textarea` | | -| `select`, `select*` | `srfm/dropdown` | `multiple` → multi-select | -| `checkbox`, `checkbox*` | `srfm/checkbox` | | -| `radio` | `srfm/multi-choice` | | -| `acceptance` | `srfm/gdpr` | tnc_html → content | -| `file`, `file*` | unsupported in v1 | no native `srfm/file` — flag | -| `quiz`, `captchar`, `hidden` | skipped | legacy / not in v1 scope | - -Detection: `defined('WPCF7_PLUGIN')`. Entries: requires Flamingo plugin. - -### 3.2 WPForms (22 field types → SureForms blocks) -Source: CPT `wpforms`, field data as JSON in `post_content`. - -| WPForms type | SureForms block | Notes | -|---|---|---| -| `text` | `srfm/input` | | -| `email` | `srfm/email` | | -| `url` | `srfm/url` | | -| `phone` | `srfm/phone` | | -| `number`, `number-slider` | `srfm/number` | | -| `textarea`, `richtext` | `srfm/textarea` | rich → fallback | -| `select`, `multi_select` | `srfm/dropdown` | | -| `radio` | `srfm/multi-choice` | | -| `checkbox` | `srfm/checkbox` | | -| `name` | `srfm/input × 3` | split first/middle/last | -| `address` | `srfm/address` | structured fields | -| `date-time` | `srfm/input` | type=date until native block | -| `password` | `srfm/input` | type=password | -| `hidden` | `srfm/input` | type=hidden | -| `html`, `divider` | structural — core/html or section break | | -| `pagebreak` | unsupported in v1 | no native multi-step yet | -| `file-upload`, `rating`, `layout` | unsupported in v1 | flag | - -Detection: `function_exists('wpforms')`. Entries: WPForms Pro only. - -### 3.3 Gravity Forms (22 field types → SureForms blocks) -Source: custom tables `gf_form` + `gf_form_meta` (serialized PHP array). - -| Gravity type | SureForms block | Notes | -|---|---|---| -| `email` | `srfm/email` | | -| `text` | `srfm/input` | | -| `name` | `srfm/input × 3` | first/middle/last sub-inputs | -| `hidden` | `srfm/input` (type=hidden) | | -| `textarea` | `srfm/textarea` | | -| `website` | `srfm/url` | | -| `phone` | `srfm/phone` | | -| `select`, `multiselect` | `srfm/dropdown` | | -| `checkbox` | `srfm/checkbox` | | -| `radio` | `srfm/multi-choice` | | -| `date`, `time` | `srfm/input` (type=date/time) | | -| `number` | `srfm/number` | | -| `address` | `srfm/address` | | -| `consent` | `srfm/gdpr` | | -| `captcha` | unsupported | external service | -| `html` | core/html (or skip) | | -| `section` | section break / divider | | -| `page` | unsupported in v1 | multi-step | -| `list` | unsupported in v1 | repeater not in srfm | -| `fileupload` | unsupported in v1 | no srfm/file | - -Detection: `class_exists('GFForms')`. Entries: `gf_entry`, `gf_entry_meta` tables. - -### 3.4 Ninja Forms (27 field type aliases → SureForms blocks) -Source: custom tables `nf3_forms`, `nf3_fields`, `nf3_field_meta` (JOIN-based). - -| Ninja type | SureForms block | Notes | -|---|---|---| -| `email` | `srfm/email` | | -| `textbox`, `firstname`, `lastname`, `address`, `city`, `zip` | `srfm/input` | (Ninja splits address into individual text fields) | -| `liststate`, `listcountry` | `srfm/dropdown` | | -| `textarea` | `srfm/textarea` | | -| `phone` | `srfm/phone` | | -| `select`, `listselect` | `srfm/dropdown` | | -| `listmultiselect` | `srfm/dropdown` (multi) | | -| `radio`, `listradio`, `listimage`, `toggle_switch` | `srfm/multi-choice` | | -| `checkbox`, `listcheckbox` | `srfm/checkbox` | | -| `date` | `srfm/input` (type=date) | | -| `number` | `srfm/number` | | -| `hidden` | `srfm/input` (type=hidden) | | -| `html` | core/html | | -| `hr` | section divider | | -| `starrating` | unsupported in v1 | no srfm/rating | -| `repeater` | unsupported in v1 | | -| `recaptcha` | spam protection — emit as form-level setting | | -| `submit` | handled by `srfm/inline-button` | | - -Detection: `defined('NF_SERVER_URL')`. Entries: `nf_sub` CPT. - -### 3.5 Caldera Forms (26 field types → SureForms blocks) -Source: `Caldera_Forms_Forms::get_forms()` static API. Caldera is unmaintained — migration is a one-shot rescue. - -| Caldera type | SureForms block | Notes | -|---|---|---| -| `email` | `srfm/email` | | -| `text` | `srfm/input` | | -| `hidden` | `srfm/input` (type=hidden) | | -| `textarea`, `paragraph`, `wysiwyg` | `srfm/textarea` | | -| `url` | `srfm/url` | | -| `phone`, `phone_better` | `srfm/phone` | | -| `select`, `dropdown` | `srfm/dropdown` | | -| `filtered_select2` | `srfm/dropdown` (multi) | | -| `radio`, `toggle_switch` | `srfm/multi-choice` | | -| `checkbox` | `srfm/checkbox` | | -| `date_picker`, `date` | `srfm/input` (type=date) | | -| `number`, `range`, `calculation` | `srfm/number` | | -| `range_slider` | `srfm/number` (slider) | | -| `gdpr` | `srfm/gdpr` | | -| `section_break`, `html` | structural | | -| `color_picker`, `star_rating`, `file`, `cf2_file`, `advanced_file` | unsupported in v1 | flag | -| `button` | handled by `srfm/inline-button` | | - -Detection: `defined('CFCORE_VER')`. Caldera abandoned in 2022 — entries critical. - -## 4. SureForms target blocks (the destination schema) - -From `find sureforms/inc/blocks/*/block.json` — **15 blocks** total: - -| Block | Type | Used for | -|---|---|---| -| `srfm/form` | wrapper | Outer form container | -| `srfm/input` | field | Text/email/url/phone/password/date/hidden (HTML type via attr) | -| `srfm/email` | field | Email input | -| `srfm/textarea` | field | Paragraph text | -| `srfm/url` | field | URL input | -| `srfm/number` | field | Numeric input + slider mode | -| `srfm/phone` | field | International phone | -| `srfm/dropdown` | field | Select / multi-select | -| `srfm/multi-choice` | field | Radio / multiple choice | -| `srfm/checkbox` | field | Checkbox group | -| `srfm/address` | field | Structured address (line1/2/city/state/zip/country) | -| `srfm/gdpr` | field | Consent checkbox | -| `srfm/payment` | field | Stripe payment | -| `srfm/payment-history` | display | Past payments table | -| `srfm/inline-button` | structural | Submit / action button | - -**Gaps acknowledged**: no native rich-text, file upload, signature, rating, date-picker, multi-step (page break), or repeater blocks. Fields mapped to these in source plugins are flagged as `unsupported_fields[]` in the v1 response and surfaced in the import UI as warnings. They will be added in subsequent releases. - -## 5. Code layout (mirrors Fluent's, adapted to SureForms conventions) - -``` -sureforms/inc/migrator/ -├── class-migrator-bootstrap.php # ~200 LOC — REST routes, capability checks, factory -├── class-base-migrator.php # ~800 LOC — abstract import loop + idempotency + helpers -├── class-block-templates.php # ~600 LOC — canonical srfm/* block markup templates -├── importers/ -│ ├── class-cf7-importer.php # ~600 LOC — shortcode regex parser -│ ├── class-wpforms-importer.php # ~700 LOC — JSON decode -│ ├── class-gravity-importer.php # ~800 LOC — serialized array parser -│ ├── class-ninja-importer.php # ~600 LOC — JOIN across nf3_* tables -│ └── class-caldera-importer.php # ~500 LOC — Caldera static API -└── views/admin-page.php # ~150 LOC — admin page mount point - -sureforms/src/admin/migrator/ # React UI (~800 LOC) -├── MigratorApp.jsx # Top-level component -├── SourceList.jsx # Tile per detected source -├── FormSelector.jsx # Pick forms to import -├── ImportPreview.jsx # Dry-run rendered output -├── ImportResult.jsx # Success + unsupported warnings -└── EntryImporter.jsx # Per-form entry import -``` - -**Total estimate: ~5,750 LOC** for all 5 sources (close to Fluent's ~5,400 LOC, with the extra slack accounting for SureForms' block-markup serialization). - -## 6. REST API design - -Namespace: `/wp-json/sureforms/v1/migrator/` (extending the existing `/sureforms/v1/` namespace from `inc/rest-api.php`). - -| Method | Route | Body / Query | Returns | -|---|---|---|---| -| GET | `/migrator/sources` | — | `[{key, title, installed, form_count, supports_entries}]` | -| GET | `/migrator/sources/{key}/forms` | — | `[{id, name, field_count, imported_srfm_id, last_imported_at}]` | -| POST | `/migrator/sources/{key}/import` | `{form_ids: [], dry_run: bool, behavior: {}, post_status?: 'draft'\|'publish'}` | `{imported: [{srfm_id, edit_url, source_id}], failed: [], skipped: [], unsupported_fields: [], preview?: {}}` | -| POST | `/migrator/sources/{key}/forms/{id}/entries` _(planned — P3, not yet registered)_ | `{limit?: 1000}` | `{count_imported, count_skipped, count_total}` | - -**Security model** (per `sureforms/CLAUDE.md`): -- Every route's `permission_callback` requires `current_user_can('manage_options')`. -- REST cookie nonce (`X-WP-Nonce`) for all routes — these are admin-only, no public access. -- Sanitize `form_ids` via `array_map('absint', ...)`, `key` via allowlist `['cf7','wpforms','gravity','ninja','caldera']`. - -## 7. Canonical block template dictionary - -Analogue of Fluent's 1,200-line `defaultFieldConfig()` array, but emitting Gutenberg block markup strings instead of field-config arrays. - -One method per target block in `class-block-templates.php`: - -```php -final class SRFM_Block_Templates { - - public static function input( array $args ): string { - $defaults = [ - 'label' => '', - 'placeholder' => '', - 'required' => false, - 'field_type' => 'text', // text|email|number|password|date|hidden - 'help_text' => '', - 'default_val' => '', - 'min_length' => '', - 'max_length' => '', - 'css_class' => '', - ]; - $attrs = wp_parse_args( $args, $defaults ); - // Match the schema in src/blocks/input/block.json - $json = wp_json_encode( self::strip_empty( [ - 'label' => $attrs['label'], - 'placeholder' => $attrs['placeholder'], - 'required' => $attrs['required'], - 'fieldType' => $attrs['field_type'], - 'help' => $attrs['help_text'], - 'defaultValue' => $attrs['default_val'], - 'minLength' => $attrs['min_length'], - 'maxLength' => $attrs['max_length'], - 'className' => $attrs['css_class'], - ] ) ); - return "\n"; - } - - public static function email( array $args ): string { /* ... */ } - public static function dropdown( array $args ): string { /* ... */ } - public static function checkbox( array $args ): string { /* ... */ } - // ... one per srfm/* block - - public static function form_wrapper( string $inner_content ): string { - return "\n{$inner_content}\n"; - } -} -``` - -The base migrator's import loop concatenates the per-field strings, wraps them in `form_wrapper()`, and persists via `wp_insert_post(['post_type' => 'sureforms_form', 'post_content' => $markup])`. - -## 8. Base migrator contract (abstract methods) - -```php -abstract class SRFM_Base_Migrator { - - protected string $key; // 'cf7' | 'wpforms' | 'gravity' | 'ninja' | 'caldera' - protected string $title; // Display name - protected array $unsupported_fields = []; - - // Per-source plumbing (abstract — subclasses implement) - abstract public function exist(): bool; - abstract protected function get_source_forms(): array; - abstract protected function get_source_form_id( array $form ); - abstract protected function get_source_form_name( array $form ): string; - abstract protected function build_form_content( array $form ): string; // concatenated block markup - abstract protected function get_form_metas( array $form ): array; // notifications, confirmations - // NOTE: get_entries() is planned for the entries-migration phase (P3); not shipped yet. - - // Shared (in base): - public function list_forms(): array; - public function count_source_forms(): int; - public function import_forms( array $selected_ids = [], bool $dry_run = false, array $behavior = [], string $post_status = 'publish' ): array; - protected function find_existing_srfm_id( $source_id ): int; - protected function record_import_mapping( int $srfm_id, $source_id ): void; -} -``` - -## 9. Idempotent re-imports - -Same pattern as Fluent: a single `wp_option('srfm_imported_forms_map')` storing: - -```php -[ - 123 => [ 'source_id' => 'cf7-45', 'source_key' => 'cf7', 'last_imported' => '2026-05-22 10:00:00' ], - 124 => [ 'source_id' => '8', 'source_key' => 'wpforms', 'last_imported' => '2026-05-22 10:01:00' ], -] -``` - -`is_already_imported()`: -1. Look up `source_key` + `source_id` in the map. -2. If found and the matching `sureforms_form` post still exists → **update** (not insert). -3. If the SureForms form was deleted → prune the stale entry, fresh insert. - -## 10. Unsupported field tracking - -Same pattern as Fluent: -- `$this->unsupported_fields[]` is pushed each time a source field's type isn't in the source's `field_type_map`, capturing the field's user-facing label (not internal id). -- Returned in the REST response as `unsupported_fields: []`. -- Admin UI renders these as a yellow callout: "These fields couldn't be migrated to SureForms — add them manually after import: Field A, Field B, Field C." - -## 11. Entry migration - -- **Form-by-form, not bulk** — same as Fluent. -- **Hard cap of 1,000 entries per form**, override via the `srfm/entry_migration_max_limit` filter. -- **Idempotent**: import deletes existing SureForms entries for that form first (via the same `resetEntries()` pattern Fluent uses) before inserting. -- **Files**: copy via `WP_Filesystem_Direct::copy()` into `wp-content/uploads/sureforms/migrated/`. - -## 12. Admin UI flow - -Lives at **WordPress → SureForms → Tools → Migrator** (new sub-tab next to existing export/import). - -``` -[ Step 1: Select source ] Detected plugins shown as tiles with logo + install count - ▼ click "Migrate from Contact Form 7" - -[ Step 2: Select forms ] Table of forms in source (name | fields | already-imported badge) - ▼ bulk select + "Preview" - -[ Step 3: Dry-run preview ] Server returns block markup; UI renders in a code pane - + warning callout for unsupported fields - ▼ "Confirm & Import" - -[ Step 4: Result ] "Imported X forms." Links: [Edit in SureForms] [Import entries] - Unsupported-fields list (if any) -``` - -The dry-run preview is the **one improvement over Fluent's UX** — it answers the user's "is this going to break anything?" anxiety before they commit. - -## 13. Implementation phasing - -Four mergeable PRs, each ~7–10 working days: - -| Phase | Deliverable | LOC | -|---|---|---| -| **P1: Foundation** | `Base_Migrator`, `Block_Templates`, REST routes, admin page shell, CF7 importer end-to-end | ~2,400 | -| **P2: WPForms + Gravity** | Two more importers + dry-run preview UI | ~1,700 | -| **P3: Ninja + Caldera** | Final two importers + entry import flow | ~1,300 | -| **P4: Polish** | Field-mapping overrides, entry import UI, docs, E2E tests | ~350 | - -Total: ~5,750 LOC delivered across ~4–5 weeks. - -## 14. Testing approach - -- **Unit tests** (PHPUnit, per `composer test`): per importer, fixture-driven — store sample source-form payloads in `tests/fixtures/migrator/{source}/*.json` and assert the produced block markup string matches a golden snapshot. -- **Integration tests**: a real CF7 / WPForms / Gravity form created via their PHP APIs in a `tests/Integration/Migrator/` test, imported via `Base_Migrator::import_forms()`, then assert a `sureforms_form` post exists with the right block content. -- **Playwright E2E** (per `npm run play:run`): full admin flow — open Migrator, pick CF7, select a form, dry-run, confirm import, verify the new form renders correctly on the front-end. -- **Manual exploratory**: all 5 source plugins are already installed locally, so the test environment is real, not mocked. - -## 15. Verification (when is this done) - -1. All 5 importers exist under `sureforms/inc/migrator/importers/` and pass their PHPUnit fixtures. -2. REST endpoints respond correctly: `GET /sureforms/v1/migrator/sources` lists the 5 sources with `installed: true` for whichever plugins are active. -3. Admin page at SureForms → Tools → Migrator renders the tile-based source picker. -4. Dry-run for a non-trivial CF7 form (text + email + textarea + radio + acceptance) produces valid Gutenberg block markup that renders correctly in the SureForms editor. -5. Re-importing the same source form updates the existing SureForms form (not a duplicate). -6. Entry migration imports up to 1,000 entries for a WPForms Pro form and the entries appear in SureForms' Entries admin. -7. Unsupported-field warnings appear in the UI for source fields without a SureForms target (e.g. CF7 `[file]`, WPForms `rating`). -8. PHPStan level 9 passes (`composer phpstan`) and PHPCS passes (`composer lint`). - -## 16. Reference source files (Fluent Forms — production reference) - -| Fluent file | Our equivalent | Why useful | -|---|---|---| -| `app/Services/Migrator/Bootstrap.php` | `class-migrator-bootstrap.php` | AJAX → REST route mapping | -| `app/Services/Migrator/Classes/BaseMigrator.php` | `class-base-migrator.php` | Import loop, idempotency, file migration helpers | -| `app/Services/Migrator/Classes/ContactForm7Migrator.php` | `class-cf7-importer.php` | Battle-tested shortcode regex parser (`formatAsFluentField()`) | -| `app/Services/Migrator/Classes/WpFormsMigrator.php` | `class-wpforms-importer.php` | JSON-decode + 22-entry field map | -| `app/Services/Migrator/Classes/GravityFormsMigrator.php` | `class-gravity-importer.php` | Serialized-array parsing + Name/Address sub-input handling | -| `app/Services/Migrator/Classes/NinjaFormsMigrator.php` | `class-ninja-importer.php` | JOIN across `nf3_*` tables via Ninja's PHP API | -| `app/Services/Migrator/Classes/CalderaMigrator.php` | `class-caldera-importer.php` | `Caldera_Forms_Forms` static API | - -Fluent Forms is GPL-2.0+ — we can lift regexes and parsing patterns directly with attribution comments. The `defaultFieldConfig()` array structure should be re-written for SureForms blocks, not copied (different output format). - -## 16a. Metadata migration - -Form metadata (email notifications, confirmation messages, compliance flags, integrations) lives **outside** the block-markup pipeline — it's persisted as `sureforms_form` post meta. The migrator handles it via the abstract `Base_Migrator::get_form_metas()` method which returns a `meta_key => meta_value` array passed to `wp_insert_post()` / `wp_update_post()` via `meta_input`. - -### What every importer must produce - -A SureForms-shaped payload. The two keys every importer should populate are: - -| SureForms meta key | Schema (registered in `inc/post-types.php`) | -|---|---| -| `_srfm_email_notification` | Array of objects: `{id, status, is_raw_format, name, email_to, email_reply_to, from_name, from_email, email_cc, email_bcc, subject, email_body}` | -| `_srfm_form_confirmation` | Array of objects: `{id, confirmation_type, page_url, custom_url, message, submission_action, enable_query_params, query_params}` | - -Other meta keys exist (`_srfm_compliance`, `_srfm_conditional_logic`, `_srfm_forms_styling`, `_srfm_integrations_webhooks`) but are not required for a usable imported form — they default to SureForms' built-in `register_post_meta` defaults if not provided. - -### What each source can give us (and what we ship in v1) - -| Source | Notifications | Confirmations | Other meta | Status in v1 | -|---|---|---|---|---| -| **CF7** | `_mail`, `_mail_2` use `[field-name]` shortcodes — incompatible with SureForms smart tags | `_messages` array (success/error text) | None | **Stub only** — admin notification to `{admin_email}` with `{all_data}` body, generic success message. Form is usable; users can refine in Single Form Settings. (Matches Fluent's CF7 approach for the same reason.) | -| **WPForms** | `settings.notifications[]` — fully structured (subject/body/recipient/CC/BCC/replyTo/fromName) | `settings.confirmations[]` — multi-confirmation with conditional logic | Webhooks | **Full migration planned in P2** | -| **Gravity** | Notifications, conditional notifications, routing | Confirmations, scheduling, login-required, entry limits | Advanced validation | **Full migration planned in P2** | -| **Ninja** | `Actions` API — type `email`, `successmessage`, `redirect`, `save` | Same Actions API | — | **Full migration planned in P3** | -| **Caldera** | `mailer` config in form settings | `successMessage` field | — | **Full migration planned in P3** | - -### Why CF7 is special - -CF7's email body uses `[your-name]`, `[your-email]` style shortcodes that reference the form-tag `name` attribute. Translating these to SureForms' `{slug_field_id}` smart-tag system requires either (a) maintaining a per-form name→slug mapping during import, or (b) rewriting the body string with regex substitutions that match the field tokens we emitted. Both approaches add ~400 LOC and break easily on edge cases (multi-line bodies, mixed shortcodes/text, CF7 special mail tags like `[_remote_ip]`). - -Fluent Forms — with 700K+ installs and 5+ years of migrator iteration — landed on the same stub-only conclusion for CF7. v1 matches that decision; if user demand surfaces, CF7 mail-template translation is a separate scoped feature. - -### Code pointers (live) - -- **Abstract method**: `Base_Migrator::get_form_metas()` — `inc/migrator/base-migrator.php` -- **Wire-up**: `Base_Migrator::insert_form_post()` / `update_form_post()` both accept a `$metas` arg and pass it to `wp_insert_post()` via `meta_input` -- **CF7 implementation**: `Cf7_Importer::get_form_metas()` — `inc/migrator/importers/cf7-importer.php` -- **SureForms meta schemas** (the contract we have to match): `inc/post-types.php` lines 911-1010 (notification), 1086-1182 (confirmation) - -### Verification - -After importing a CF7 form via the Migration tab, open the resulting SureForms form's settings: -- **Email Notifications** tab → should show one enabled notification named "Admin Notification Email" with subject "New submission: {form title}" and body `{all_data}`. -- **Form Confirmation** tab → should show a centered "Thank you" success message, "Same page" confirmation type. - -If those panels are empty, the meta payload didn't persist — check `wp_options.srfm_imported_forms_map` and `wp_postmeta` for the form id. - -## 17. Out of scope for v1 - -- Polls / Quizzes (Forminator, Gravity surveys) — no SureForms equivalent. -- Formidable Views — front-end data display is its own feature. -- WS Form — niche, defer. -- Multi-step / page break preservation — wait for native `srfm/page-break` block. -- File-upload, signature, rating, repeater fields — wait for native blocks (flagged as unsupported until then). -- Bulk source-selection ("import everything from everywhere") — one source at a time in v1. - -## 18. Sources verified live (May 2026) - -All wordpress.org plugin pages were fetched live during research: - -- [Contact Form 7](https://wordpress.org/plugins/contact-form-7/) — 10M+ installs, v6.1.6 -- [WPForms Lite](https://wordpress.org/plugins/wpforms-lite/) — 6M+ installs, v1.10.0.5 -- [Gravity Forms docs](https://docs.gravityforms.com/form-fields/) — ~655K live sites, commercial -- [Ninja Forms](https://wordpress.org/plugins/ninja-forms/) — 600K+ installs, v3.14.4 -- Caldera Forms — unmaintained since 2022, ~50K legacy sites - -**Combined migration pool: ~17.3M active sites** — the addressable target for v1. diff --git a/docs/wiki/AI-Form-Builder.md b/docs/wiki/AI-Form-Builder.md deleted file mode 100644 index a35a46a4e..000000000 --- a/docs/wiki/AI-Form-Builder.md +++ /dev/null @@ -1,103 +0,0 @@ -# AI Form Builder - -SureForms includes an AI-powered form generator that creates complete forms from natural language descriptions. - -## Architecture - -``` -inc/ai-form-builder/ -|-- ai-form-builder.php # Main AI builder class -|-- ai-auth.php # Authentication and billing -|-- ai-helper.php # AI utility functions -|-- field-mapping.php # Maps AI response to Gutenberg blocks -``` - -## Authentication Flow - -The `AI_Auth` class handles authentication with the SureForms middleware API: - -1. User initiates authentication from admin settings -2. `GET /initiate-auth` REST endpoint generates an auth URL -3. User is redirected to the billing portal (`SRFM_BILLING_PORTAL`) -4. After authentication, an encrypted access key is returned -5. `POST /handle-access-key` decrypts and stores the key in WordPress options -6. Subsequent AI requests use this key for authorization - -### Constants - -| Constant | URL | Purpose | -|----------|-----|---------| -| `SRFM_AI_MIDDLEWARE` | `https://credits.startertemplates.com/sureforms/` | AI credits API | -| `SRFM_MIDDLEWARE_BASE_URL` | `https://api.sureforms.com/` | API middleware | -| `SRFM_BILLING_PORTAL` | `https://billing.sureforms.com/` | Billing portal | - -## Form Generation Process - -### Step 1: User Input - -The user provides a natural language description of the form they want (e.g., "Create a contact form with name, email, phone, and message fields"). - -### Step 2: AI Generation - -`POST /generate-form` sends the description to the AI middleware: - -``` -User prompt -> REST API -> AI_Form_Builder::generate_ai_form() - -> External AI middleware API call - -> AI generates form structure as JSON - -> Returns field definitions -``` - -### Step 3: Field Mapping - -`POST /map-fields` converts the AI response to SureForms Gutenberg blocks: - -``` -AI JSON response -> Field_Mapping::generate_gutenberg_fields_from_questions() - -> Maps each AI field to a SureForms block type - -> Generates block attributes - -> Creates Gutenberg block markup - -> Returns blocks ready for the editor -``` - -### Field Type Mapping - -The `Field_Mapping` class maps AI-suggested field types to SureForms blocks: - -| AI Field Type | SureForms Block | -|---------------|----------------| -| text / name | `srfm/input` | -| email | `srfm/email` | -| phone / tel | `srfm/phone` | -| textarea / message | `srfm/textarea` | -| number | `srfm/number` | -| url / website | `srfm/url` | -| select / dropdown | `srfm/dropdown` | -| checkbox | `srfm/checkbox` | -| radio / multiple choice | `srfm/multichoice` | -| address | `srfm/address` | - -## REST API Endpoints - -| Method | Endpoint | Handler | Description | -|--------|----------|---------|-------------| -| POST | `/generate-form` | `AI_Form_Builder::generate_ai_form` | Send prompt to AI, get form structure | -| POST | `/map-fields` | `Field_Mapping::generate_gutenberg_fields_from_questions` | Convert AI fields to blocks | -| GET | `/initiate-auth` | `AI_Auth::get_auth_url` | Get billing portal auth URL | -| POST | `/handle-access-key` | `AI_Auth::handle_access_key` | Save decrypted access key | - -All endpoints require `manage_options` capability. - -## AI Helper - -The `AI_Helper` class provides utility functions for: -- API key management -- Credit balance checking -- Request formatting -- Error handling for AI middleware responses - -## Related Pages - -- [REST API Reference](REST-API-Reference) -- All API endpoints -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block types the AI maps to -- [Architecture Overview](Architecture-Overview) -- AI classes in bootstrap flow diff --git a/docs/wiki/Admin-Dashboard.md b/docs/wiki/Admin-Dashboard.md deleted file mode 100644 index 9c16aea85..000000000 --- a/docs/wiki/Admin-Dashboard.md +++ /dev/null @@ -1,121 +0,0 @@ -# Admin Dashboard - -The SureForms admin dashboard is a React single-page application (SPA) integrated into the WordPress admin area. - -## Architecture - -### Entry Point - -The admin React app is bootstrapped in `src/admin/editor-scripts.js`, which serves as the Webpack entry point. It mounts the React app to a DOM container registered by the PHP `Admin` class. - -### Tech Stack - -| Technology | Purpose | -|------------|---------| -| React 18 | UI rendering | -| react-router-dom 6 | Client-side routing | -| @tanstack/react-query 5 | Server state management and data fetching | -| @wordpress/api-fetch | REST API client with nonce handling | -| @wordpress/data | WordPress data store integration | -| @bsf/force-ui | BSF component library (buttons, modals, inputs, etc.) | -| Tailwind CSS 3 | Utility-first styling | -| react-hot-toast | Toast notifications | - -### Admin Pages - -The React app includes the following pages/routes: - -| Route | Component | Description | -|-------|-----------|-------------| -| Dashboard | `src/admin/dashboard/` | Overview with charts and stats | -| Forms | `src/admin/forms/` | Forms listing with search, filter, bulk actions | -| Entries | `src/admin/entries/` | Entry listing and management | -| Settings | `src/admin/settings/` | Global plugin settings | -| Payments | `src/admin/payment/` | Payment transactions listing | -| Single Form Settings | `src/admin/single-form-settings/` | Per-form configuration | - -### Shared Components - -Common admin components are in `src/admin/components/`: - -- `PageHeader` -- Consistent page header with title and actions -- `TemplatePicker` -- Form template selection UI -- Loading skeletons (via `react-loading-skeleton`) -- Modal dialogs and confirmation prompts - -## WordPress Integration - -### Menu Registration - -The PHP `Admin` class (`admin/admin.php`) registers the WordPress admin menu: - -```php -add_menu_page( - 'SureForms', - 'SureForms', - 'manage_options', - 'sureforms_menu', - [ $this, 'render_admin_page' ], - 'dashicons-feedback', - 30 -); -``` - -### Asset Enqueuing - -Admin assets are enqueued only on SureForms admin pages: - -- React app bundle (from `assets/build/`) -- Tailwind CSS styles -- Localized data via `wp_localize_script()` providing: - - REST API URL and nonce - - Current user data - - Plugin settings - - Form data - -## Data Fetching - -### @tanstack/react-query - -Used for server state management with automatic caching, refetching, and invalidation: - -```javascript -const { data, isLoading } = useQuery({ - queryKey: ['entries', filters], - queryFn: () => apiFetch({ path: '/sureforms/v1/entries/list', ... }), -}); -``` - -### @wordpress/api-fetch - -WordPress API client with automatic nonce injection: - -```javascript -import apiFetch from '@wordpress/api-fetch'; - -apiFetch({ path: '/sureforms/v1/form-data' }) - .then(data => { /* handle response */ }); -``` - -## Force UI Component Library - -`@bsf/force-ui` is BSF's internal component library providing: - -- Buttons, inputs, selects, toggles -- Modals and dialogs -- Tables and data display -- Navigation components -- Consistent design system - -Components are imported directly: - -```javascript -import { Button, Modal, Input } from '@bsf/force-ui'; -``` - -## Related Pages - -- [State Management](State-Management) -- WordPress data store -- [Frontend Assets](Frontend-Assets) -- Asset loading pipeline -- [REST API Reference](REST-API-Reference) -- API endpoints used by dashboard -- [Environment Configuration](Environment-Configuration) -- Build setup diff --git a/docs/wiki/Architecture-Overview.md b/docs/wiki/Architecture-Overview.md deleted file mode 100644 index 523f18cc8..000000000 --- a/docs/wiki/Architecture-Overview.md +++ /dev/null @@ -1,294 +0,0 @@ -# Architecture Overview - -This page describes the high-level architecture of the SureForms WordPress plugin (v2.5.1), covering the bootstrap flow, namespace conventions, directory layout, design patterns, and data flow. - -## High-Level Architecture Diagram - -``` -+------------------------------------------------------+ -| WordPress Core | -| (hooks: plugins_loaded, init, rest_api_init, etc.) | -+---------------------------+--------------------------+ - | - +--------v---------+ - | sureforms.php | - | (entry point, | - | constants) | - +--------+---------+ - | - +--------v---------+ - | plugin-loader.php| - | (Plugin_Loader) | - | - autoloader | - | - hook setup | - | - class init | - +--------+---------+ - | - +------------------+------------------+ - | | | - plugins_loaded(99) init admin_init - load_plugin() load_classes() activation_redirect() - | | - +------v------+ +------v------+ - | Core Classes| | Admin/Block | - | Post_Types | | Register | - | Form_Submit | | Admin | - | Rest_Api | | Payments | - | Helper | | Duplicate | - | ...20+ more | +-------------+ - +------+------+ - | - +------v------+ +-------------+ +-------------+ - | Database | | REST API | | Frontend | - | Register | | /sureforms/ | | Assets | - | Entries tbl | | v1/... | | Form Markup | - | Payments tbl| +-------------+ +-------------+ - +-------------+ -``` - -## Plugin Bootstrap Flow - -### 1. Entry Point: sureforms.php - -The main plugin file defines all global constants and then requires `plugin-loader.php`: - -```php -define( 'SRFM_FILE', __FILE__ ); -define( 'SRFM_DIR', plugin_dir_path( SRFM_FILE ) ); -define( 'SRFM_URL', plugins_url( '/', SRFM_FILE ) ); -define( 'SRFM_VER', '2.5.1' ); -define( 'SRFM_FORMS_POST_TYPE', 'sureforms_form' ); -define( 'SRFM_ENTRIES', 'sureforms_entries' ); -define( 'SRFM_PAYMENTS', 'sureforms_payments' ); -require_once 'plugin-loader.php'; -``` - -### 2. Plugin Loader: plugin-loader.php - -The `SRFM\Plugin_Loader` class is the central bootstrap orchestrator: - -1. Loads the Action Scheduler library from `inc/lib/action-scheduler/action-scheduler.php` -2. Registers the PSR-4-style autoloader via `spl_autoload_register()` -3. Hooks into WordPress lifecycle events: - - `plugins_loaded` (default priority) -- `load_textdomain()` - - `plugins_loaded` (priority 99) -- `load_plugin()` (main class initialization) - - `init` -- `load_classes()` (block registration, admin, payments) - - `admin_init` -- `activation_redirect()` -4. Instantiates `Analytics` singleton immediately -5. Registers activation hook (`Activator::activate()`) -6. Registers deactivation hook (clears scheduled events) - -The `Plugin_Loader` fires the `srfm_core_loaded` action after first instantiation. - -### 3. Class Initialization Order - -**On `plugins_loaded` (priority 99) via `load_plugin()`:** - -| Order | Class | Responsibility | -|-------|-------|---------------| -| 1 | `Post_Types` | Registers `sureforms_form` CPT and all post meta | -| 2 | `Form_Submit` | Registers `/submit-form` and `/refresh-nonces` REST endpoints | -| 3 | `Gutenberg_Hooks` | Block editor integration hooks | -| 4 | `Frontend_Assets` | Frontend CSS/JS enqueue | -| 5 | `Helper` | Utility/helper functions | -| 6 | `Activator` | First-run setup and migrations | -| 7 | `Admin_Ajax` | WordPress AJAX handlers | -| 8 | `Forms_Data` | Form data queries and listing | -| 9 | `Export` | Import/export functionality | -| 10 | `Smart_Tags` | Template tag processing | -| 11 | `Generate_Form_Markup` | Form HTML rendering | -| 12 | `Create_New_Form` | New form creation flow with default meta | -| 13 | `Global_Settings` | Plugin-wide settings management | -| 14 | `Email_Summary` | Scheduled email digest reports | -| 15 | `Compliance_Settings` | GDPR/privacy per-form settings | -| 16 | `Events_Scheduler` | Cron-based scheduled events | -| 17 | `AI_Form_Builder` | AI-powered form generation | -| 18 | `Field_Mapping` | AI response to Gutenberg block mapping | -| 19 | `Background_Process` | Background task processing via Action Scheduler | -| 20 | `Page_Builders` | Third-party page builder integrations | -| 21 | `Rest_Api` | Admin REST endpoint registration (20+ routes) | -| 22 | `AI_Helper` | AI utility functions | -| 23 | `AI_Auth` | AI authentication and billing portal | -| 24 | `Updater` | Plugin update logic | -| 25 | `Onboarding` | First-run onboarding wizard | -| 26 | `DatabaseRegister` | Custom table creation/migration | -| 27 | `Form_Restriction` | Submission limits, scheduling, and entry caps | -| 28 | `Astra` | Astra theme compatibility layer | - -**On `init` via `load_classes()`:** - -| Class | Condition | Responsibility | -|-------|-----------|---------------| -| `Blocks\Register` | Always | Gutenberg block type registration | -| `Admin` | `is_admin()` only | Admin menu pages, assets, and screens | -| `Notice_Manager` | `is_admin()` only | Admin notice display and management | -| `Payments` | Always | Payment gateway setup (Stripe) | -| `Duplicate_Form` | Always | Form duplication handler | - -## Namespace and Autoloading - -### Namespace Structure - -The root namespace is `SRFM`. All classes follow `SRFM\{directory}\{ClassName}`: - -``` -SRFM\ -> plugin-loader.php -SRFM\Inc\Helper -> inc/helper.php -SRFM\Inc\Rest_Api -> inc/rest-api.php -SRFM\Inc\Form_Submit -> inc/form-submit.php -SRFM\Inc\Database\Base -> inc/database/base.php -SRFM\Inc\Database\Tables\Entries -> inc/database/tables/entries.php -SRFM\Inc\AI_Form_Builder\* -> inc/ai-form-builder/ -SRFM\Inc\Traits\Get_Instance -> inc/traits/get-instance.php -SRFM\Admin\Admin -> admin/admin.php -``` - -### Autoloader Logic - -The autoloader in `Plugin_Loader::autoload()`: - -1. Strips the root `SRFM\` namespace prefix -2. Converts `CamelCase` to `kebab-case` -3. Replaces underscores with hyphens -4. Replaces namespace separators with directory separators -5. Converts to lowercase, prepends `SRFM_DIR`, appends `.php` -6. Rejects any path containing `..` (directory traversal protection) - -## Directory Layout - -``` -sureforms/ -|-- sureforms.php # Plugin entry point, all SRFM_* constants -|-- plugin-loader.php # Autoloader, bootstrap, hook registration -|-- inc/ # Core PHP classes (SRFM\Inc namespace) -| |-- helper.php # Static utility methods -| |-- rest-api.php # REST API endpoint registration -| |-- form-submit.php # Public form submission handler -| |-- post-types.php # sureforms_form CPT and post meta -| |-- entries.php # Entry CRUD operations -| |-- blocks/ # Gutenberg block server-side rendering -| |-- database/ # Custom DB table management -| |-- fields/ # Form field type definitions and validation -| |-- payments/ # Payment gateway integrations (Stripe) -| |-- email/ # Email template rendering -| |-- ai-form-builder/ # AI-powered form generation -| |-- global-settings/ # Plugin-wide settings -| |-- single-form-settings/ # Per-form configuration -| |-- compatibility/ # Theme/plugin compatibility -| |-- page-builders/ # Page builder integrations -| |-- traits/ # Shared PHP traits (Get_Instance) -| |-- lib/ # Bundled third-party libraries (do not modify) -|-- src/ # JS/React source (compiled by wp-scripts) -| |-- admin/ # Admin dashboard React app -| |-- blocks/ # Gutenberg block editor components -| |-- components/ # Shared React components -| |-- store/ # WordPress @wordpress/data store -| |-- utils/ # JS utilities -|-- modules/ # Feature modules (Gutenberg, Quick Action Sidebar) -|-- admin/ # Admin PHP classes and assets -|-- sass/ # SASS source files (compiled to assets/css/) -|-- assets/ # Compiled and static assets -|-- templates/ # PHP templates (BladeOne engine) -|-- tests/ # PHPUnit, Playwright, and Docker setup -``` - -## Design Patterns - -### Singleton via Get_Instance Trait - -Nearly every class uses the `Get_Instance` trait from `inc/traits/get-instance.php`: - -```php -trait Get_Instance { - private static $instance = null; - public static function get_instance() { - if ( null === self::$instance ) { - self::$instance = new self(); - } - return self::$instance; - } -} -``` - -### Abstract Base Class (Database) - -The database layer uses `SRFM\Inc\Database\Base` providing: - -- Table creation via `dbDelta()` -- Schema versioning with integer versions stored in options -- Column migration (`maybe_add_new_columns()`, `maybe_rename_columns()`) -- CRUD wrappers (`use_insert()`, `use_update()`, `use_delete()`) -- Query builder with parameterized WHERE supporting `=`, `!=`, `>`, `<`, `LIKE`, `IN` -- In-memory result caching keyed by MD5 of SQL query -- Schema-driven JSON encoding/decoding for array columns - -### Hook-Based Initialization - -Classes register WordPress hooks in their constructors, invoked during singleton instantiation in `load_plugin()` or `load_classes()`. - -### Filter-Driven Extensibility - -| Filter | Purpose | -|--------|---------| -| `srfm_rest_api_endpoints` | Extend REST API routes | -| `srfm_register_post_meta` | Register additional form post meta | -| `srfm_form_submit_data` | Modify form data before processing | -| `srfm_entry_value` | Transform entry field values | -| `srfm_is_special_block` | Declare custom blocks as special | -| `srfm_handle_special_block` | Custom processing for special blocks | -| `srfm_form_confirmation_params` | Extend form confirmation settings | -| `srfm_normalize_csv_field_value` | Custom CSV export formatting | - -## Data Flow - -``` -1. FORM CREATION - Admin creates form in Gutenberg block editor - -> Saved as sureforms_form CPT (wp_posts) - -> Settings stored as post meta (_srfm_* keys) - -> Block content in post_content column - -2. FORM DISPLAY - Form embedded via shortcode, Gutenberg block, or Instant Form URL - -> Generate_Form_Markup::get_form_markup() renders HTML - -> Frontend_Assets enqueues CSS/JS - -> Submission nonce generated - -3. FORM SUBMISSION - User submits via JavaScript fetch() - -> POST /wp-json/sureforms/v1/submit-form - -> Nonce verification, field validation - -> CAPTCHA validation (reCAPTCHA, hCaptcha, Turnstile) - -> Honeypot spam check - -4. ENTRY STORAGE - -> JSON-encoded form data inserted into wp_srfm_entries - -> Email notifications sent - -> Payment processed if applicable (wp_srfm_payments) - -> Confirmation response returned - -5. POST-SUBMISSION - -> srfm_form_submit and srfm_after_submission_process actions fired -``` - -## Key Constants - -| Constant | Value | Purpose | -|----------|-------|---------| -| `SRFM_FILE` | `__FILE__` | Main plugin file path | -| `SRFM_DIR` | Plugin dir path | File system operations | -| `SRFM_URL` | Plugin URL | Asset URL generation | -| `SRFM_VER` | `2.5.1` | Version for cache busting | -| `SRFM_SLUG` | `srfm` | Short identifier slug | -| `SRFM_FORMS_POST_TYPE` | `sureforms_form` | Custom post type slug | -| `SRFM_ENTRIES` | `sureforms_entries` | Entries identifier | -| `SRFM_PAYMENTS` | `sureforms_payments` | Payments identifier | -| `SRFM_PRO_RECOMMENDED_VER` | `2.5.1` | Minimum Pro version | - -## Related Pages - -- [Database Schema](Database-Schema) -- Custom tables, post types, and meta keys -- [Environment Configuration](Environment-Configuration) -- Development setup, build tools, and linting -- [REST API Reference](REST-API-Reference) -- Full endpoint documentation -- [Form Submission Flow](Form-Submission-Flow) -- Detailed submission pipeline -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- All actions and filters diff --git a/docs/wiki/Block-Editor-Controls.md b/docs/wiki/Block-Editor-Controls.md deleted file mode 100644 index 7d5cd1e33..000000000 --- a/docs/wiki/Block-Editor-Controls.md +++ /dev/null @@ -1,109 +0,0 @@ -# Block Editor Controls - -SureForms provides custom inspector controls and utilities for the Gutenberg block editor experience. - -## Directory Structure - -``` -src/srfm-controls/ -|-- addCommonData.js # Inject common data into blocks -|-- generateCSS.js # Dynamic CSS generation for blocks -|-- block-icons.js # Block icon definitions -|-- fonts.js # Font management utilities -|-- maybeGetColorForVariable.js # CSS variable color resolution -|-- parseIcon.js # SVG icon parser -|-- renderIcon.js # Icon rendering component - -src/blocks-attributes/ -|-- getMetakeys.js # Form meta key retrieval -|-- getFormId.js # Get current form ID -|-- getBlocksDefaultAttributes.js # Block attribute defaults -``` - -## CSS Generation System - -`generateCSS.js` dynamically generates CSS for block styling: - -- Converts block attributes to CSS properties -- Supports responsive breakpoints (desktop, tablet, mobile) -- Generates inline styles injected into the page -- Handles CSS variable references - -This system allows each block instance to have unique styling without class conflicts. - -## Block Icons - -`block-icons.js` provides SVG icon components for all SureForms blocks, used in: - -- Block inserter panel -- Block toolbar -- Block settings sidebar - -Additional icon sets from Spectra are available via `spectra-icons-v*.php` files. - -Icon utilities: -- `parseIcon.js` -- Parses SVG string into React-renderable format -- `renderIcon.js` -- React component for rendering parsed icons - -## Common Data Injection - -`addCommonData.js` injects shared data into block components: - -- Form-level settings accessible to all child blocks -- Common attributes and defaults -- Shared configuration values - -## Meta Key Integration - -`getMetakeys.js` retrieves form post meta keys for use in block controls: - -- Provides meta key names for form settings -- Used by inspector controls to read/write form meta -- Integrates with `@wordpress/data` store - -## Block Attribute Defaults - -`getBlocksDefaultAttributes.js` provides default attribute values for all block types: - -- Ensures consistent defaults across block instances -- Used during block registration -- Prevents undefined attribute errors - -## Form ID Resolution - -`getFormId.js` resolves the current form's post ID: - -- Used by blocks to identify their parent form -- Required for meta key operations -- Supports nested block contexts - -## Field Preview - -`src/blocks/FieldsPreview.jsx` renders a preview of form fields in the block editor: - -- Shows a visual representation of the field in editor mode -- Responds to attribute changes in real-time -- Handles edit vs preview states - -## Font Management - -`fonts.js` manages font loading for form styling: - -- Font family selection in block settings -- Google Fonts integration -- Font weight and style options - -## Color Variable Resolution - -`maybeGetColorForVariable.js` resolves WordPress CSS custom properties: - -- Maps color slugs to CSS variable references -- Supports theme color palette integration -- Falls back to raw color values when variables are not available - -## Related Pages - -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block types and registration -- [State Management](State-Management) -- WordPress data store -- [Frontend Assets](Frontend-Assets) -- Editor asset loading -- [Form Fields Architecture](Form-Fields-Architecture) -- Field types diff --git a/docs/wiki/Changelog.md b/docs/wiki/Changelog.md deleted file mode 100644 index 058c89f45..000000000 --- a/docs/wiki/Changelog.md +++ /dev/null @@ -1,39 +0,0 @@ -# Changelog - -Recent changes to SureForms. For the full changelog, see [sureforms.com/whats-new](https://sureforms.com/whats-new/). - -## 2.5.1 - 16th February 2026 - -- **New:** Added option to change currency position (e.g., $100, 100$, $ 100, 100 $) -- **Fix:** Fixed the payment block amount not updating correctly with conditional logic - -## 2.5.0 - 2nd February 2026 - -- **Improvement:** Added translation support for the country list in the phone number field -- **Fix:** Fixed dropdown placeholder and GDPR label translation on the frontend - -## 2.4.0 - 20th January 2026 - -- **New:** Added form scheduling restrictions with start and end date/time -- **New:** Added Previous/Next navigation for single entry page -- **Improvement:** Improved multi-choice block settings UI -- **Improvement:** Updated Syria flag in the Phone field -- **Fix:** Fixed HTML block content corruption when duplicating forms -- **Fix:** Fixed multi-choice options being translated in entries -- **Fix:** Fixed reCAPTCHA v3 validation triggering multiple times on the same page -- **Fix:** Fixed Spectra button styles being affected by SureForms CSS - -## Version Bumping - -When preparing a release, update the version in: - -1. `sureforms.php` -- Plugin header `Version:` and `SRFM_VER` constant -2. `readme.txt` -- `Stable tag:` and changelog section -3. `package.json` -- `version` field - -See [Deployment Guide](Deployment-Guide) for the full release process. - -## Related Pages - -- [Deployment Guide](Deployment-Guide) -- Release workflow and CI/CD -- [Contributing Guide](Contributing-Guide) -- Development workflow diff --git a/docs/wiki/Contributing-Guide.md b/docs/wiki/Contributing-Guide.md deleted file mode 100644 index 441736c7e..000000000 --- a/docs/wiki/Contributing-Guide.md +++ /dev/null @@ -1,205 +0,0 @@ -# Contributing Guide - -Thank you for your interest in contributing to SureForms! This guide covers the development workflow, coding standards, and submission process. - -## Prerequisites - -- **PHP** 7.4+ -- **Node.js** 24.16.0 (managed via [Volta](https://volta.sh/)) -- **Composer** for PHP dependencies -- **Git** for version control -- **Docker** (optional, for wp-env test environment) - -## Getting Started - -1. Fork the repository on GitHub -2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/sureforms.git - cd sureforms - ``` -3. Install dependencies: - ```bash - composer install - npm install - ``` -4. Create a feature branch from `next-release`: - ```bash - git checkout next-release - git pull origin next-release - git checkout -b feature/your-feature-name - ``` - -## Git Workflow - -``` -feature/branch --> next-release --> master --> tag/release - (development) (production) -``` - -- **Feature branches** are created from `next-release` -- **Pull requests** target `next-release` -- **Hotfixes** may target `master` directly -- **Release merges** go from `next-release` into `master` - -## Development Workflow - -### Running the Dev Server - -```bash -npm start # Start wp-scripts dev server (hot reload) -``` - -### Building - -```bash -npm run build # Full production build (JS + SASS + minify) -npm run build:script # Build JS only -npm run build:sass # Build SASS only -``` - -### Linting - -Run linters before submitting a PR: - -```bash -# JavaScript -npm run lint-js # ESLint check -npm run lint-js:fix # Auto-fix JS issues - -# CSS -npm run lint-css # Stylelint check -npm run lint-css:fix # Auto-fix CSS issues - -# Formatting -npm run pretty # Prettier check -npm run pretty:fix # Auto-format - -# PHP -vendor/bin/phpcs # PHP CodeSniffer (WPCS) -vendor/bin/phpcbf # Auto-fix PHPCS issues -``` - -### Testing - -All tests should pass before submitting a PR: - -```bash -# PHP unit tests -vendor/bin/phpunit - -# JavaScript unit tests -npm run test:unit - -# E2E tests (requires wp-env) -npm run play:up # Start environment -npm run play:run # Run Playwright tests -``` - -See [Testing Guide](Testing-Guide) for detailed test setup. - -## Coding Standards - -### PHP Standards - -SureForms follows **WordPress Coding Standards (WPCS)** enforced via `phpcs.xml`: - -- **Escaping**: Use `esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()` for all output -- **Nonces**: Use `wp_nonce_field()` / `check_ajax_referer()` for form submissions and AJAX -- **Capabilities**: Always check `current_user_can()` before privileged operations -- **Sanitization**: Use `sanitize_text_field()`, `absint()`, `wp_kses()` for all input -- **Prefixes**: `SRFM_` for constants, `srfm_` for function names and handles -- **Text domain**: `sureforms` for all translatable strings -- **PHP compatibility**: 7.4+ (enforced via `PHPCompatibilityWP`) - -### JavaScript Standards - -- Follow `@wordpress/scripts` ESLint configuration -- Use `@wordpress/data` store for state management -- Use `@wordpress/api-fetch` for REST API calls -- Import WordPress dependencies from `@wordpress/*` packages -- Use Force UI (`@bsf/force-ui`) components in admin dashboard pages - -### CSS Standards - -- **Admin dashboard**: Tailwind CSS 3.x utility classes -- **Block frontend**: SASS files in `sass/` directory -- **Class prefix**: `srfm-` for all frontend CSS classes -- Do not edit generated CSS in `assets/css/` -- edit SASS source files instead - -## Pull Request Process - -### PR Checklist - -Before submitting your PR, verify: - -- [ ] Code follows the coding standards (PHP, JS, CSS) -- [ ] All linters pass without errors -- [ ] Tests pass (PHPUnit, Jest) -- [ ] New features include tests -- [ ] Translatable strings use the `sureforms` text domain -- [ ] No debug code, `console.log`, or `var_dump` statements -- [ ] No secrets or credentials committed -- [ ] Build completes without errors (`npm run build`) - -### PR Title Format - -PR titles are validated by CI. Use a clear, descriptive title that summarizes the change. - -### CI Checks - -Pull requests automatically run: - -1. **PHPUnit** -- PHP unit tests -2. **PHPStan** -- Static analysis -3. **PHP Insights** -- Code quality -4. **PR Title Validation** -- Title format check -5. **E2E Tests** -- When the PR is labeled with `e2e` - -All checks must pass before merge. - -### Code Review - -- PRs require at least one approving review -- Address all review comments -- Keep PRs focused on a single feature or fix -- Large changes should be discussed in an issue first - -## Directory Guide for Contributors - -| Directory | Purpose | Edit? | -|-----------|---------|-------| -| `src/` | JS/React source code | Yes | -| `sass/` | SASS stylesheets | Yes | -| `inc/` | PHP classes and logic | Yes | -| `tests/` | Test files | Yes | -| `build/` | Compiled JS output | No (generated) | -| `assets/css/` | Compiled CSS output | No (generated) | -| `assets/js/minified/` | Minified JS | No (generated) | -| `inc/lib/` | Bundled third-party libraries | No | -| `node_modules/` | NPM packages | No | -| `vendor/` | Composer packages | No | - -## Adding a New Gutenberg Block - -See [Gutenberg Blocks](Gutenberg-Blocks) for the full guide on adding blocks, including: - -- Block registration (server-side and client-side) -- `block.json` schema -- Editor and frontend rendering - -## Adding a New REST Endpoint - -See [REST API Reference](REST-API-Reference) for endpoint patterns and the registration system. - -## Reporting Issues - -- Use [GitHub Issues](https://github.com/brainstormforce/sureforms/issues) for bug reports and feature requests -- Security vulnerabilities should be reported through the [Bug Bounty Program](https://brainstormforce.com/bug-bounty-program/) - -## Related Pages - -- [Environment Configuration](Environment-Configuration) -- Development setup details -- [Architecture Overview](Architecture-Overview) -- Codebase structure -- [Testing Guide](Testing-Guide) -- Test framework details -- [Deployment Guide](Deployment-Guide) -- Release process diff --git a/docs/wiki/Data-Export-Import.md b/docs/wiki/Data-Export-Import.md deleted file mode 100644 index fc07271ae..000000000 --- a/docs/wiki/Data-Export-Import.md +++ /dev/null @@ -1,120 +0,0 @@ -# Data Export & Import - -SureForms supports exporting and importing forms and entries for backup, migration, and data analysis. - -## Form Export - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/export` - -**Parameters:** -- `form_ids` (required) -- Array of form IDs to export - -**Response:** JSON file containing form data including: -- Form post data (title, content, status) -- All form post meta (`_srfm_*` keys) -- Block content (Gutenberg blocks) - -### Export Format - -Forms are exported as a JSON structure containing all data needed to recreate the form on another site. - -## Form Import - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/import` - -**Parameters:** -- File upload containing exported JSON data - -**Process:** -1. Validates the uploaded JSON structure -2. Creates new `sureforms_form` posts -3. Restores all post meta keys -4. Assigns new post IDs (no ID conflicts) -5. Returns count of imported forms - -## Form Duplication - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/duplicate` - -**Parameters:** -- `form_id` (required) -- ID of the form to duplicate - -**Process:** - -Handled by `Duplicate_Form` class (`inc/duplicate-form.php`): - -1. Reads the original form post and all meta -2. Creates a new post with "(Copy)" appended to the title -3. Copies all `_srfm_*` post meta to the new form -4. Returns the new form ID - -## Entry Export - -### REST Endpoint - -`POST /wp-json/sureforms/v1/entries/export` - -**Parameters:** -- `entry_ids` -- Specific entry IDs to export (optional) -- `form_id` -- Export all entries for a specific form -- `status` -- Filter by status (all, read, unread) -- `search` -- Search filter -- `date_from` / `date_to` -- Date range filter - -**Response:** CSV file (or ZIP for large exports) - -### Export Format - -The `Export` class (`inc/export.php`) generates CSV with: -- One row per entry -- Columns for each form field -- Submission metadata (date, status, IP) -- Field values normalized via `srfm_normalize_csv_field_value` filter - -### CSV Field Value Normalization - -The `srfm_normalize_csv_field_value` filter allows customizing how field values appear in CSV exports: - -```php -add_filter( 'srfm_normalize_csv_field_value', function( $value, $field ) { - // Custom formatting for specific field types - return $value; -}, 10, 2 ); -``` - -## Form Lifecycle Management - -### REST Endpoint - -`POST /wp-json/sureforms/v1/forms/manage` - -**Parameters:** -- `form_ids` (required) -- Array of form IDs -- `action` (required) -- `trash`, `restore`, or `delete` - -| Action | Behavior | -|--------|----------| -| `trash` | Move forms to trash (recoverable) | -| `restore` | Restore trashed forms | -| `delete` | Permanently delete forms | - -## Create New Form - -The `Create_New_Form` class (`inc/create-new-form.php`) handles new form creation with default meta values: - -- Sets default styling options -- Configures default submit button text -- Initializes email notification defaults -- Applies default form confirmation settings - -## Related Pages - -- [REST API Reference](REST-API-Reference) -- All export/import endpoints -- [Database Schema](Database-Schema) -- Entry and form data structure -- [Admin Dashboard](Admin-Dashboard) -- UI for export/import operations diff --git a/docs/wiki/Database-Schema.md b/docs/wiki/Database-Schema.md deleted file mode 100644 index c0a47044b..000000000 --- a/docs/wiki/Database-Schema.md +++ /dev/null @@ -1,173 +0,0 @@ -# Database Schema - -SureForms uses two custom database tables alongside the WordPress `sureforms_form` custom post type. All custom tables use the `wp_srfm_` prefix. - -## Table Relationships - -``` -+-------------------+ +-------------------+ -| wp_posts | | wp_srfm_entries | -| (sureforms_form) |<---------| form_id | -| ID (PK) | | | ID (PK) | -| post_content | | | user_id | -| post_meta (_srfm)| | | form_data (JSON) | -+-------------------+ | | status | - | +--------+-----------+ - | | - | +--------v-----------+ - | | wp_srfm_payments | - +----| form_id | - | id (PK) | - | entry_id (FK) | - | transaction_id | - | total_amount | - +--------------------+ -``` - -## Custom Tables - -### wp_srfm_entries - -Stores all form submissions. Table version: 1. - -| Column | Type | Default | Description | -|--------|------|---------|-------------| -| `ID` | BIGINT(20) UNSIGNED | AUTO_INCREMENT | Primary key | -| `form_id` | BIGINT(20) UNSIGNED | -- | Reference to sureforms_form post ID | -| `user_id` | BIGINT(20) UNSIGNED | 0 | WordPress user ID (0 = anonymous) | -| `status` | VARCHAR(10) | `unread` | Entry status: `unread`, `read`, `trash` | -| `type` | VARCHAR(20) | -- | Form type: `quiz`, `standard`, etc. | -| `form_data` | LONGTEXT | `[]` | JSON-encoded submitted field values | -| `submission_info` | LONGTEXT | `[]` | JSON-encoded browser/device/IP metadata | -| `notes` | LONGTEXT | `[]` | JSON-encoded admin notes | -| `logs` | LONGTEXT | `[]` | JSON-encoded activity log entries | -| `extras` | LONGTEXT | `[]` | JSON-encoded misc additional data | -| `created_at` | TIMESTAMP | CURRENT_TIMESTAMP | Submission timestamp | -| `updated_at` | TIMESTAMP | CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | - -**Indexes:** -- `PRIMARY KEY (ID)` -- `INDEX idx_form_id (form_id)` -- `INDEX idx_user_id (user_id)` -- `INDEX idx_form_id_created_at_status (form_id, created_at, status)` -- composite index for list queries - -**Log Entry Structure:** -```json -{ - "title": "Form Submitted", - "messages": ["Email notification sent", "Entry stored"], - "timestamp": "2024-01-15 10:30:00" -} -``` - -### wp_srfm_payments - -Stores payment transaction records linked to form entries. Table version: 1. Added in v2.0.0. - -| Column | Type | Default | Description | -|--------|------|---------|-------------| -| `id` | BIGINT(20) UNSIGNED | AUTO_INCREMENT | Primary key | -| `form_id` | BIGINT(20) UNSIGNED | -- | Reference to sureforms_form post | -| `block_id` | VARCHAR(255) | `''` | Payment block identifier | -| `status` | VARCHAR(50) | `pending` | Payment status (see below) | -| `total_amount` | DECIMAL(26,8) | `0.00000000` | Total amount after discount | -| `refunded_amount` | DECIMAL(26,8) | `0.00000000` | Total refunded amount | -| `currency` | VARCHAR(10) | `''` | ISO 4217 currency code | -| `entry_id` | BIGINT(20) UNSIGNED | 0 | Reference to entries table | -| `gateway` | VARCHAR(20) | `''` | Payment gateway: `stripe` | -| `type` | VARCHAR(30) | `''` | Payment type: `payment`, `subscription`, `renewal` | -| `mode` | VARCHAR(20) | `''` | Mode: `test` or `live` | -| `transaction_id` | VARCHAR(50) | `''` | Gateway transaction ID | -| `customer_id` | VARCHAR(50) | `''` | Gateway customer ID | -| `subscription_id` | VARCHAR(50) | `''` | Subscription ID (if recurring) | -| `subscription_status` | VARCHAR(20) | `''` | Subscription status | -| `parent_subscription_id` | BIGINT(20) UNSIGNED | 0 | Parent subscription payment ID | -| `payment_data` | LONGTEXT | `[]` | JSON-encoded payment details (includes refunds) | -| `extra` | LONGTEXT | `[]` | JSON-encoded additional data | -| `log` | LONGTEXT | `[]` | JSON-encoded payment log | -| `created_at` | TIMESTAMP | CURRENT_TIMESTAMP | Creation timestamp | -| `updated_at` | TIMESTAMP | CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | -| `srfm_txn_id` | VARCHAR(100) | `''` | Custom transaction ID format | -| `customer_email` | VARCHAR(255) | `''` | Customer email | -| `customer_name` | VARCHAR(255) | `''` | Customer name | - -**Valid Payment Statuses:** -`pending`, `succeeded`, `failed`, `canceled`, `requires_action`, `requires_payment_method`, `processing`, `refunded`, `partially_refunded` - -**Valid Subscription Statuses:** -`active`, `canceled`, `past_due`, `unpaid`, `trialing`, `incomplete`, `incomplete_expired`, `paused` - -**Supported Currencies:** -USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, SEK, NZD, MXN, SGD, HKD, NOK, TRY, RUB, INR, BRL, ZAR, KRW - -## WordPress Post Type: sureforms_form - -Forms are stored as a custom post type (`sureforms_form`) in the standard `wp_posts` table. Block content is in `post_content`, and form configuration is stored as post meta with `_srfm_` prefixed keys. - -### Key Post Meta Keys - -| Meta Key | Type | Description | -|----------|------|-------------| -| `_srfm_forms_styling` | string | Form styling mode | -| `_srfm_color` | string | Primary form color | -| `_srfm_bg_color` | string | Background color | -| `_srfm_fontsize` | string | Font size | -| `_srfm_label_color` | string | Label text color | -| `_srfm_input_color` | string | Input text color | -| `_srfm_submit_button_text` | string | Submit button text | -| `_srfm_submit_alignment` | string | Button alignment | -| `_srfm_submit_width` | string | Button width | -| `_srfm_submit_width_backend` | string | Backend button width | -| `_srfm_is_inline_button` | boolean | Inline button mode | -| `_srfm_email_notification` | array | Email notification settings | -| `_srfm_form_confirmation` | array | Confirmation settings (message/redirect) | -| `_srfm_captcha` | string | CAPTCHA type (recaptcha_v2, v3, hcaptcha, turnstile) | -| `_srfm_honeypot` | boolean | Honeypot spam protection | -| `_srfm_is_page_break` | boolean | Multi-step form enabled | -| `_srfm_single_page_form_title` | boolean | Show title on instant form | -| `_srfm_instant_form_settings` | array | Instant form configuration | -| `_srfm_page_form_logo` | string | Form logo URL | -| `_srfm_submit_type` | string | Submission type | - -Post meta is extensible via the `srfm_register_post_meta` filter. - -## Database Base Class - -All custom tables extend `SRFM\Inc\Database\Base`, which provides: - -### Schema Versioning -- Each table has a `$table_version` integer -- Versions stored in `srfm_database_table_versions` option -- On version mismatch, `start_db_upgrade()` runs migrations (new columns, renames) - -### Query Builder -```php -// Simple equality -$results = $instance->get_results(['form_id' => 123]); - -// Complex conditions with operators -$results = $instance->get_results([ - [ - ['key' => 'status', 'compare' => '!=', 'value' => 'trash'], - ['key' => 'created_at', 'compare' => '>', 'value' => '2024-01-01'], - ] -]); - -// IN operator -$results = $instance->get_results([ - [['key' => 'ID', 'compare' => 'IN', 'value' => [1, 2, 3]]] -]); -``` - -### Caching -Results are cached in-memory using MD5 of the SQL query as the cache key. Cache is reset on insert, update, or delete operations. - -### Data Encoding -Array-type columns (defined in `get_schema()`) are automatically JSON-encoded on write and decoded on read. Number types are cast to integers. - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- Plugin bootstrap and class initialization -- [Payment Integration](Payment-Integration) -- Stripe payment flow -- [Form Submission Flow](Form-Submission-Flow) -- Entry creation pipeline -- [REST API Reference](REST-API-Reference) -- API endpoints for entries and payments diff --git a/docs/wiki/Deployment-Guide.md b/docs/wiki/Deployment-Guide.md deleted file mode 100644 index 1511856ec..000000000 --- a/docs/wiki/Deployment-Guide.md +++ /dev/null @@ -1,109 +0,0 @@ -# Deployment Guide - -## Release Process Overview - -SureForms follows a branch-based release workflow with automated CI/CD via GitHub Actions. - -## Git Workflow - -``` -feature/branch --> next-release --> master --> tag/release - (development) (production) -``` - -1. Feature branches are created from `next-release` -2. PRs target `next-release` for review and merge -3. `next-release` is merged to `master` for releases -4. Tags trigger release artifacts - -## Build Steps - -### Production Build - -```bash -# Full production build (JS + SASS + minify) -npm run build - -# Individual steps: -npm run build:script # Build JS via wp-scripts -npm run build:sass # Compile SASS via Grunt -``` - -The build process: -1. Webpack bundles JS/React source from `src/` to `assets/build/` -2. Grunt compiles SASS from `sass/` to `assets/css/` -3. Grunt minifies CSS to `assets/css/minified/` -4. Grunt minifies JS to `assets/js/minified/` -5. PostCSS applies Autoprefixer - -### i18n - -```bash -npm run makepot # Generate .pot translation file -``` - -## CI/CD Pipelines - -### GitHub Actions Workflows - -| Workflow | Trigger | Purpose | -|----------|---------|---------| -| `phpunit.yml` | Pull requests | Run PHP unit tests | -| `playwright.yml` | PR labeled `e2e` | Run E2E browser tests | -| `code-analysis.yml` | Pull requests | PHPStan static analysis | -| `insights.yml` | Pull requests | PHP Insights code quality | -| `pr-title.yml` | Pull requests | Validate PR title format | -| `push-to-deploy.yml` | Push to master | Deploy to production | -| `push-asset-readme-update.yml` | Push to master | Update readme/assets | -| `release-tag-draft.yml` | Tag push | Create draft GitHub release | -| `release-pr-template.yml` | Release PRs | Apply release PR template | -| `claude-code-review.yml` | Pull requests | AI-powered code review | - -### Test Pipeline - -Pull requests automatically run: -1. PHPUnit tests (PHP 8.2, WordPress trunk) -2. PHPStan static analysis -3. PHP Insights code quality -4. PR title validation -5. E2E tests (when labeled `e2e`) - -### Deployment Pipeline - -On push to `master`: -1. `push-to-deploy.yml` triggers deployment -2. `push-asset-readme-update.yml` updates WordPress.org assets - -### Release Pipeline - -1. Merge `next-release` into `master` -2. Create a version tag (e.g., `v2.5.1`) -3. `release-tag-draft.yml` creates a draft GitHub release -4. Review and publish the release - -## Pre-Deployment Checklist - -- [ ] All tests pass (PHPUnit, Playwright, Jest) -- [ ] PHPStan analysis clean -- [ ] PHPCS standards pass -- [ ] Production build completes without errors -- [ ] Version bumped in `sureforms.php` and `readme.txt` -- [ ] `SRFM_VER` constant updated -- [ ] `SRFM_PRO_RECOMMENDED_VER` checked -- [ ] Changelog updated in `readme.txt` -- [ ] Translation .pot file regenerated -- [ ] No debug code or console.log statements -- [ ] Database migrations tested (if any) - -## Version Bumping - -Update version in these files: -1. `sureforms.php` -- Plugin header `Version:` and `SRFM_VER` constant -2. `readme.txt` -- `Stable tag:` and changelog section -3. `package.json` -- `version` field - -## Related Pages - -- [Testing Guide](Testing-Guide) -- Test setup and execution -- [Contributing Guide](Contributing-Guide) -- Development workflow -- [Environment Configuration](Environment-Configuration) -- Build tools diff --git a/docs/wiki/Email-Notifications.md b/docs/wiki/Email-Notifications.md deleted file mode 100644 index 642751bcb..000000000 --- a/docs/wiki/Email-Notifications.md +++ /dev/null @@ -1,86 +0,0 @@ -# Email Notifications - -SureForms sends email notifications on form submission and supports scheduled email summary digests. - -## Email System Overview - -``` -Form Submission - -> Form_Submit::handle_form_submission() - -> Email_Template (inc/email/email-template.php) - -> Smart_Tags resolution - -> wp_mail() dispatch -``` - -## Email Template - -The `Email_Template` class (`inc/email/email-template.php`) handles: - -- HTML email template rendering -- Dynamic content insertion via smart tags -- Multiple notification recipients -- Customizable subject lines and body content -- Reply-to address configuration - -### Per-Form Email Settings - -Email notifications are configured per-form via the `_srfm_email_notification` post meta key. Each form can have: - -- **To** -- Recipient email addresses (supports multiple, comma-separated) -- **Subject** -- Email subject line (supports smart tags) -- **Body** -- Email content (supports smart tags and HTML) -- **Reply-To** -- Reply-to email address -- **CC/BCC** -- Additional recipients - -## Smart Tags - -The `Smart_Tags` class (`inc/smart-tags.php`) provides template variables that are resolved at email send time: - -| Smart Tag | Description | -|-----------|-------------| -| `{admin_email}` | WordPress admin email | -| `{site_title}` | WordPress site title | -| `{site_url}` | WordPress site URL | -| `{form_title}` | Name of the submitted form | -| `{form_id}` | ID of the submitted form | -| `{entry_id}` | ID of the created entry | -| `{all_fields}` | All submitted field values formatted | -| `{field:field_name}` | Specific field value by field name | -| `{user_ip}` | Submitter's IP address | -| `{user_agent}` | Submitter's browser user agent | -| `{submission_date}` | Date of submission | - -Smart tags are processed by replacing the tag placeholders with actual values from the form submission data. - -## Email Summary - -The `Email_Summary` class (`inc/global-settings/email-summary.php`) provides scheduled digest reports: - -- Configurable frequency (daily, weekly, monthly) -- Summary of form submissions over the period -- Entry counts per form -- Sent to configured admin email addresses -- Uses WordPress cron (`Events_Scheduler`) for scheduling - -## Customization - -### Modifying Email Content - -Email content is filtered through smart tag processing, allowing dynamic content: - -```php -// In per-form settings, the email body supports: -// - Smart tags: {form_title}, {all_fields}, etc. -// - HTML formatting -// - Custom text -``` - -### Extending Smart Tags - -Smart tags can be extended by hooking into the tag resolution process to add custom replacements. - -## Related Pages - -- [Form Submission Flow](Form-Submission-Flow) -- When emails are triggered -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- Email-related hooks -- [Architecture Overview](Architecture-Overview) -- Email_Template initialization diff --git a/docs/wiki/Environment-Configuration.md b/docs/wiki/Environment-Configuration.md deleted file mode 100644 index b4a89da2f..000000000 --- a/docs/wiki/Environment-Configuration.md +++ /dev/null @@ -1,197 +0,0 @@ -# Environment Configuration - -## Prerequisites - -| Requirement | Version | Notes | -|-------------|---------|-------| -| PHP | 7.4+ | WordPress minimum requirement | -| WordPress | 6.4+ | Gutenberg block editor required | -| Node.js | 24.16.0 | Managed via Volta (see `package.json`) | -| Composer | 2.x | PHP dependency management | -| npm | 11.x+ | Comes with Node 24 | - -## Plugin Constants - -Defined in `sureforms.php`: - -| Constant | Value | Purpose | -|----------|-------|---------| -| `SRFM_FILE` | `__FILE__` | Absolute path to main plugin file | -| `SRFM_BASENAME` | `sureforms/sureforms.php` | Plugin basename for hooks | -| `SRFM_DIR` | Plugin directory path | File system path operations | -| `SRFM_URL` | Plugin URL | Asset URL generation | -| `SRFM_VER` | `2.5.1` | Version for cache busting | -| `SRFM_SLUG` | `srfm` | Short identifier slug | -| `SRFM_FORMS_POST_TYPE` | `sureforms_form` | Custom post type slug | -| `SRFM_ENTRIES` | `sureforms_entries` | Entries identifier | -| `SRFM_PAYMENTS` | `sureforms_payments` | Payments identifier | -| `SRFM_WEBSITE` | `https://sureforms.com/` | Official website | -| `SRFM_AI_MIDDLEWARE` | `https://credits.startertemplates.com/sureforms/` | AI credits API | -| `SRFM_MIDDLEWARE_BASE_URL` | `https://api.sureforms.com/` | API middleware | -| `SRFM_BILLING_PORTAL` | `https://billing.sureforms.com/` | Billing portal | -| `SRFM_PRO_RECOMMENDED_VER` | `2.5.1` | Minimum recommended Pro version | -| `SRFM_SURETRIGGERS_INTEGRATION_BASE_URL` | `https://app.ottokit.com/` | OttoKit integration | - -## Development Environment Setup - -### Option 1: wp-env (Docker) - -```bash -# Start the WordPress environment -npm run play:up - -# Stop the environment -npm run play:stop - -# Clean environment data -npm run env:clean -``` - -Configuration is in `.wp-env.json`. Requires Docker to be installed. - -### Option 2: Local by Flywheel / MAMP / XAMPP - -1. Set up a local WordPress installation -2. Clone the repo into `wp-content/plugins/sureforms` -3. Run `composer install` for PHP dependencies -4. Run `npm install` for JS dependencies -5. Run `npm start` for development with hot reload - -## Build Tools - -### wp-scripts (JavaScript) - -Primary build tool for JS/React compilation via Webpack: - -```bash -npm start # Dev server with hot reload -npm run build:script # Production JS build -``` - -Entry points are defined in `webpack.config.js` and output to `assets/build/`. - -### Grunt (SASS / Minification) - -```bash -npm run build:sass # Compile SASS to CSS -npm run build # Full build (JS + SASS + minify) -``` - -The `Gruntfile.js` handles: -- SASS compilation (`sass/` -> `assets/css/`) -- CSS minification -- JS minification -- PostCSS with Autoprefixer - -### Tailwind CSS - -Used for admin dashboard styles. Configuration in `tailwind.config.js`: -- Content scanning paths: `src/admin/**/*.{js,jsx}` -- Plugin: `@tailwindcss/forms` -- Custom theme extensions for Force UI integration - -### PostCSS - -Configuration in `postcss.config.js`: -- Autoprefixer for browser compatibility -- cssnano for production minification - -## NPM Scripts Reference - -| Script | Command | Description | -|--------|---------|-------------| -| `start` | `wp-scripts start` | Dev server with hot reload | -| `build` | Full pipeline | JS + SASS + minify | -| `build:script` | `wp-scripts build` | Build JS only | -| `build:sass` | Grunt sass tasks | Build SASS only | -| `lint-js` | `wp-scripts lint-js` | ESLint check | -| `lint-js:fix` | `wp-scripts lint-js --fix` | Auto-fix JS lint | -| `lint-css` | `wp-scripts lint-style` | Stylelint check | -| `lint-css:fix` | `wp-scripts lint-style --fix` | Auto-fix CSS lint | -| `pretty` | `prettier --check` | Prettier check | -| `pretty:fix` | `prettier --write` | Prettier format | -| `test:unit` | `wp-scripts test-unit-js` | Jest unit tests | -| `play:up` | `wp-env start` | Start Docker env | -| `play:stop` | `wp-env stop` | Stop Docker env | -| `play:run` | `playwright test` | Run E2E tests | -| `play:run:interactive` | `playwright test --headed` | E2E tests (headed) | -| `env:clean` | `wp-env clean` | Clean Docker data | -| `makepot` | `wp i18n make-pot` | Generate .pot file | - -## Composer Scripts - -| Script | Description | -|--------|-------------| -| `vendor/bin/phpunit` | Run PHPUnit tests | -| `vendor/bin/phpcs` | PHP CodeSniffer (WPCS) | -| `vendor/bin/phpcbf` | Auto-fix PHPCS issues | -| `vendor/bin/phpstan` | PHPStan static analysis | - -## Linting Configuration - -### PHP (PHPCS) - -Configured via `phpcs.xml`: -- Ruleset: WordPress Coding Standards (WPCS) -- PHPCompatibility for PHP 7.4+ -- Text domain: `sureforms` -- Prefixes: `srfm`, `SRFM_` - -### JavaScript (ESLint) - -Uses `@wordpress/scripts` ESLint configuration which extends WordPress defaults. - -### CSS (Stylelint) - -WordPress Stylelint configuration via `@wordpress/scripts`. - -### Prettier - -Code formatting for JS/CSS files. - -## Volta Configuration - -Node version is pinned in `package.json`: - -```json -{ - "volta": { - "node": "24.16.0" - } -} -``` - -Install Volta to automatically use the correct Node version when entering the project directory. - -## Key Dependencies - -### PHP (Composer) - -| Package | Purpose | -|---------|---------| -| `yoast/phpunit-polyfills` | PHPUnit compatibility | -| `squizlabs/php_codesniffer` | Code style checking | -| `wp-coding-standards/wpcs` | WordPress standards | -| `phpcompatibility/php-compatibility` | PHP version checks | -| `phpstan/phpstan` | Static type analysis | -| `nunomaduro/phpinsights` | Code quality insights | - -### JavaScript (npm) - -| Package | Purpose | -|---------|---------| -| `@bsf/force-ui` | BSF internal UI component library | -| `@wordpress/scripts` | Build tooling | -| `@wordpress/api-fetch` | REST API client | -| `@tanstack/react-query` | Data fetching/caching | -| `react-router-dom` | Client-side routing | -| `tailwindcss` | Utility-first CSS | -| `@playwright/test` | E2E testing | -| `@wordpress/env` | Docker test environment | - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- Plugin structure and bootstrap -- [Testing Guide](Testing-Guide) -- Test setup and execution -- [Contributing Guide](Contributing-Guide) -- Coding standards and workflow -- [Frontend Assets](Frontend-Assets) -- Asset pipeline details diff --git a/docs/wiki/Form-Fields-Architecture.md b/docs/wiki/Form-Fields-Architecture.md deleted file mode 100644 index 285f4793c..000000000 --- a/docs/wiki/Form-Fields-Architecture.md +++ /dev/null @@ -1,137 +0,0 @@ -# Form Fields Architecture - -SureForms uses a layered architecture for form fields: Gutenberg blocks define the editor experience, field markup classes generate the frontend HTML, and the validation system ensures data integrity on submission. - -## Field Type Hierarchy - -``` -Fields\Base (inc/fields/base.php) - |-- Input_Markup (text, hidden) - |-- Email_Markup (email with validation) - |-- Textarea_Markup (multi-line text) - |-- Number_Markup (numeric with min/max) - |-- Phone_Markup (with country selector) - |-- URL_Markup (URL with validation) - |-- Dropdown_Markup (select options) - |-- Checkbox_Markup (single/multiple) - |-- Multichoice_Markup (radio/checkbox groups) - |-- Address_Markup (composite address field) - |-- GDPR_Markup (consent checkbox) - |-- Payment_Markup (Stripe payment field) - |-- Inlinebutton_Markup (inline buttons) -``` - -## Base Field Class - -`inc/fields/base.php` provides the foundation: - -- Common HTML wrapper generation -- Label rendering with required indicator -- Help text / description output -- Error message container -- CSS class assembly with `srfm-` prefix -- Conditional logic attribute output -- Accessibility attributes (aria-label, aria-required) - -## Markup Generation Pattern - -Each field markup class follows a consistent pattern: - -1. Receive block attributes from the Gutenberg block -2. Process and sanitize attribute values -3. Generate field-specific HTML using WordPress escaping functions -4. Wrap in the standard field container with label and error placeholder -5. Return the complete HTML string - -## Available Field Types - -### Input (input-markup.php) -Standard text input supporting `text`, `hidden`, and other HTML5 input types. Supports placeholder, default value, min/max length. - -### Email (email-markup.php) -Email input with built-in email format validation. Supports confirmation email field option. - -### Textarea (textarea-markup.php) -Multi-line text area with configurable rows and max length. - -### Number (number-markup.php) -Numeric input with min/max value constraints, step size, and format validation. - -### Phone (phone-markup.php) -Phone number input integrated with `intl-tel-input` library for country code selection. Uses `countries.json` for country data. - -### URL (url-markup.php) -URL input with format validation. - -### Dropdown (dropdown-markup.php) -Select dropdown with configurable options. Supports default selection and placeholder text. - -### Checkbox (checkbox-markup.php) -Single or multiple checkbox inputs with label positioning options. - -### Multi Choice (multichoice-markup.php) -Radio button or checkbox groups for multiple choice questions. Supports image choices and custom layouts. - -### Address (address-markup.php) -Composite field with subfields: street address, city, state/province, zip/postal code, and country. Each subfield is independently configurable. - -### GDPR (gdpr-markup.php) -GDPR consent checkbox with customizable compliance text. Integrates with form-level compliance settings. - -### Payment (payment-markup.php) -Payment collection field for Stripe integration. Renders the payment form elements and connects to the payment processing flow. - -## Validation System - -### Server-Side Validation (field-validation.php) - -`inc/field-validation.php` handles validation during form submission: - -- **Required field check** -- Ensures required fields have values -- **Type validation** -- Validates data types (email format, URL format, number ranges) -- **Unique value check** -- Optional unique validation against existing entries -- **Custom validation** -- Extensible via filters - -Common error messages are defined in `Helper::get_common_err_msg()`: -- `required`: "This field is required." -- `unique`: "Value needs to be unique." - -### Client-Side Validation - -Frontend JavaScript provides instant validation feedback before submission, matching server-side rules. - -## Field Rendering Pipeline - -``` -1. Block Editor (Gutenberg) - -> Block attributes saved in post_content - -2. Frontend Request - -> Generate_Form_Markup::get_form_markup() - -> Parses post_content blocks - -> For each block, calls its render callback - -3. Block Render Callback - -> Block class (inc/blocks/{type}/block.php) - -> Instantiates field markup class - -> Passes block attributes - -4. Field Markup Generation - -> Field markup class (inc/fields/{type}-markup.php) - -> Base class wraps with container, label, error placeholder - -> Field-specific HTML generated - -> Returns escaped HTML string - -5. Form Assembly - -> All field HTML concatenated inside
wrapper - -> Submit button appended - -> Nonce field added - -> Final form HTML returned -``` - -## Related Pages - -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block registration and editor components -- [Form Submission Flow](Form-Submission-Flow) -- How field data is validated and processed -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- Field-related filters -- [Payment Integration](Payment-Integration) -- Payment field details diff --git a/docs/wiki/Form-Submission-Flow.md b/docs/wiki/Form-Submission-Flow.md deleted file mode 100644 index 81a9e3806..000000000 --- a/docs/wiki/Form-Submission-Flow.md +++ /dev/null @@ -1,191 +0,0 @@ -# Form Submission Flow - -This page documents the end-to-end form submission process from frontend to entry storage. - -## Flow Diagram - -``` -+------------------+ +-------------------+ +------------------+ -| User fills form |---->| JavaScript fetch() |---->| REST API | -| in browser | | POST /submit-form | | Form_Submit | -+------------------+ +-------------------+ +--------+---------+ - | - +--------------------------------+ - | - +----------v-----------+ - | Permission Check | - | - X-WP-Submit-Nonce | - | - Form data exists | - +----------+-----------+ - | - +----------v-----------+ - | Form Restrictions | - | - Max entries check | - | - Date scheduling | - | - Login required | - +----------+-----------+ - | - +----------v-----------+ - | Field Validation | - | - Required fields | - | - Type validation | - | - Unique values | - +----------+-----------+ - | - +----------v-----------+ - | Security Checks | - | - CAPTCHA verify | - | - Honeypot check | - +----------+-----------+ - | - +----------v-----------+ - | Data Processing | - | - Sanitize fields | - | - Smart tag resolve | - | - Collect metadata | - +----------+-----------+ - | - +----------------+----------------+ - | | -+--------v--------+ +--------v--------+ -| Email Notify | | Entry Storage | -| Email_Template | | wp_srfm_entries | -+-----------------+ +--------+--------+ - | - +--------v--------+ - | Payment Process | - | (if applicable) | - | wp_srfm_payments| - +--------+--------+ - | - +--------v--------+ - | Confirmation | - | Response | - +-----------------+ -``` - -## Step-by-Step Process - -### 1. Frontend Submission - -The form is submitted via JavaScript `fetch()` to `POST /wp-json/sureforms/v1/submit-form` with: - -- Form field data as POST body -- `X-WP-Submit-Nonce` header for security (does not require logged-in user) -- Form ID to identify which form was submitted - -Nonces can be refreshed via `GET /refresh-nonces` (public, no auth required). - -### 2. Permission Check - -`Form_Submit::submit_form_permissions_check()`: - -- Verifies the `X-WP-Submit-Nonce` header -- Validates that form data and form ID are present -- Returns `WP_Error` on failure - -### 3. Form Restrictions - -`Form_Restriction` class checks: - -- **Maximum entries** -- Has the form reached its submission limit? -- **Date scheduling** -- Is the form within its active date range? -- **Login required** -- Does the form require a logged-in user? -- **Entry caps** -- Per-user submission limits - -### 4. Field Validation - -`Field_Validation` (`inc/field-validation.php`): - -- **Required fields** -- Ensures all required fields have non-empty values -- **Type validation** -- Email format, URL format, number ranges, phone format -- **Unique values** -- Checks existing entries for uniqueness if configured -- Returns validation errors that prevent submission - -### 5. Security Verification - -- **CAPTCHA** -- Validates reCAPTCHA v2/v3, hCaptcha, or Cloudflare Turnstile token -- **Honeypot** -- Checks the hidden honeypot field is empty (bots fill it) - -### 6. Data Processing - -- `apply_filters('srfm_form_submit_data', $data, $form_id)` -- allows modification -- Sanitizes each field value -- Resolves smart tags for email templates -- Collects submission metadata (IP, browser via Browser class, device info) -- `do_action('srfm_before_submission', $data, $form_id)` - -### 7. Email Notifications - -`Email_Template` sends configured email notifications: - -- Processes smart tags in subject and body -- Sends to configured recipients -- Handles CC/BCC -- See [Email Notifications](Email-Notifications) for details - -### 8. Entry Storage - -Checks GDPR compliance (`do_not_store_entries` setting). If storage is allowed: - -```php -Entries::add([ - 'form_id' => $form_id, - 'user_id' => get_current_user_id(), - 'form_data' => $sanitized_data, // JSON-encoded - 'submission_info' => $metadata, // JSON-encoded - 'logs' => $activity_logs, // JSON-encoded - 'status' => 'unread', - 'type' => $form_type, // 'standard', 'quiz', etc. -]); -``` - -### 9. Payment Processing - -If the form includes a payment block: - -- Stripe payment is processed -- Payment record created in `wp_srfm_payments` -- Payment ID linked to the entry -- See [Payment Integration](Payment-Integration) for details - -### 10. Post-Submission - -- `do_action('srfm_form_submit', $entry_id, $form_id, $form_data)` -- `do_action('srfm_after_submission_process', $entry_id, $form_id)` -- Returns confirmation response: - - **Message** -- Display a success message - - **Redirect** -- Redirect to a specified URL - -## Form Confirmation Types - -Configured via `_srfm_form_confirmation` post meta: - -| Type | Behavior | -|------|----------| -| Message | Display a success message on the same page | -| Redirect | Redirect to a specified URL after submission | - -Confirmation parameters are filterable via `srfm_form_confirmation_params`. - -## Error Handling - -Errors at any stage return a JSON error response to the frontend: - -```json -{ - "success": false, - "data": "Validation error message" -} -``` - -The frontend JavaScript displays errors inline next to the relevant fields. - -## Related Pages - -- [REST API Reference](REST-API-Reference) -- Submit endpoint details -- [Database Schema](Database-Schema) -- Entry and payment table structure -- [Email Notifications](Email-Notifications) -- Email dispatch details -- [Payment Integration](Payment-Integration) -- Payment processing -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- Submission hooks -- [Form Fields Architecture](Form-Fields-Architecture) -- Field validation diff --git a/docs/wiki/Frontend-Assets.md b/docs/wiki/Frontend-Assets.md deleted file mode 100644 index 99b4cca91..000000000 --- a/docs/wiki/Frontend-Assets.md +++ /dev/null @@ -1,122 +0,0 @@ -# Frontend Assets - -SureForms manages assets across three contexts: frontend (form display), admin (dashboard), and block editor. - -## Asset Loading Strategy - -### Frontend (Form Display) - -`inc/frontend-assets.php` (`Frontend_Assets` class) handles loading CSS/JS on pages containing SureForms forms: - -- Detects form blocks in page content -- Enqueues only necessary field-specific CSS/JS -- Loads Stripe JS SDK only when payment fields are present -- Enqueues `intl-tel-input` only when phone fields exist - -### Admin Dashboard - -Admin assets are enqueued only on SureForms admin pages: - -- React app bundle from `assets/build/` -- Tailwind CSS base styles from `src/admin/tw-base.scss` -- Localized data via `wp_localize_script()` - -### Block Editor - -`inc/gutenberg-hooks.php` (`Gutenberg_Hooks` class) loads editor assets: - -- Block editor JS/CSS from `assets/build/` -- Editor-specific stylesheets -- Block preview styles - -## Build Pipeline - -### wp-scripts (Webpack) - -Primary JS build tool. Configuration extends `@wordpress/scripts` defaults: - -```bash -npm start # Development with hot reload -npm run build:script # Production build -``` - -**Entry Points:** -- `src/blocks/blocks.js` -- Gutenberg block registration -- `src/admin/editor-scripts.js` -- Admin dashboard React app -- Additional entries per block type - -**Output:** `assets/build/` directory - -**Webpack Features:** -- Code splitting per entry point -- WordPress dependency extraction (`@wordpress/*` packages) -- Source maps in development -- Asset hashing for cache busting - -### Grunt (SASS / Minification) - -```bash -npm run build:sass # Compile SASS only -npm run build # Full build (JS + SASS + minify) -``` - -The `Gruntfile.js` tasks include: - -| Task | Description | -|------|-------------| -| `sass` | Compile SASS files from `sass/` to `assets/css/` | -| `postcss` | Run PostCSS with Autoprefixer | -| `cssmin` | Minify CSS to `assets/css/minified/` | -| `uglify` | Minify JS to `assets/js/minified/` | -| `copy` | Copy assets to distribution directories | - -### SASS Directory Structure - -``` -sass/ -|-- frontend block styles -|-- form field styles -|-- layout and grid styles -``` - -Compiled output: `assets/css/` (unminified) and `assets/css/minified/` - -### Tailwind CSS - -Used exclusively for admin dashboard pages: - -**Configuration (`tailwind.config.js`):** -- Content paths: `src/admin/**/*.{js,jsx}` -- Plugins: `@tailwindcss/forms` -- Custom theme extensions for Force UI compatibility -- `tailwind-merge` used for dynamic class composition - -**Base styles:** `src/admin/tw-base.scss` - -## CSS Naming Conventions - -| Context | Prefix | Example | -|---------|--------|---------| -| Frontend form styles | `srfm-` | `srfm-input-field`, `srfm-form-container` | -| Admin dashboard | Tailwind utilities | `flex items-center gap-2` | -| Block editor | `srfm-editor-` | `srfm-editor-preview` | - -## Asset Versioning - -All enqueued assets use `SRFM_VER` (currently `2.5.1`) for cache busting: - -```php -wp_enqueue_style( - 'srfm-frontend', - SRFM_URL . 'assets/css/minified/frontend.min.css', - [], - SRFM_VER -); -``` - -## Related Pages - -- [Admin Dashboard](Admin-Dashboard) -- React admin asset details -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block editor assets -- [Environment Configuration](Environment-Configuration) -- Build tool setup -- [Contributing Guide](Contributing-Guide) -- CSS/JS conventions diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md deleted file mode 100644 index a0f2ea908..000000000 --- a/docs/wiki/Getting-Started.md +++ /dev/null @@ -1,149 +0,0 @@ -# Getting Started - -This guide walks you through setting up a SureForms development environment from scratch. - -## Prerequisites - -| Tool | Version | Notes | -|------|---------|-------| -| PHP | 7.4+ | Required for plugin runtime | -| WordPress | 6.4+ | Required for block editor features | -| Node.js | 24.16.0 | Managed via Volta (auto-switches) | -| Composer | 2.x | PHP dependency manager | -| Git | 2.x | Version control | -| Docker | Latest | Optional, for wp-env test environment | - -### Installing Volta (Recommended) - -[Volta](https://volta.sh/) ensures you use the correct Node.js version automatically: - -```bash -curl https://get.volta.sh | bash -``` - -The `package.json` pins Node 24.16.0 via Volta config -- it activates automatically when you enter the project directory. - -## Initial Setup - -### 1. Clone the Repository - -```bash -git clone https://github.com/brainstormforce/sureforms.git -cd sureforms -``` - -### 2. Install Dependencies - -```bash -composer install # PHP dependencies -npm install # Node dependencies -``` - -### 3. Build the Plugin - -```bash -npm run build # Full production build (JS + SASS + minify) -``` - -This runs three steps: -1. `wp-scripts build` -- Bundles JS/React from `src/` to `build/` -2. SASS compilation -- Compiles `sass/` to `assets/css/` -3. `grunt minify` -- Minifies CSS and JS to `assets/css/minified/` and `assets/js/minified/` - -### 4. Start Development Server - -```bash -npm start # wp-scripts dev server with hot reload -``` - -This watches `src/` for changes and rebuilds JS automatically. For SASS changes, run `npm run build:sass` separately. - -## WordPress Environment - -### Option A: Local WordPress Installation - -1. Set up a local WordPress site (e.g., using Local, MAMP, or Docker) -2. Symlink or copy the plugin into `wp-content/plugins/sureforms/` -3. Activate the plugin from the WordPress admin - -### Option B: wp-env (Docker-Based) - -The project includes a `.wp-env.json` configuration for a Docker-based WordPress environment: - -```bash -npm run play:up # Start WordPress + MySQL containers -``` - -This provides: -- WordPress at `http://localhost:8888` (admin: `admin` / `password`) -- Test WordPress at `http://localhost:8889` -- Plugin auto-mounted and activated -- Consistent environment across developers - -Additional commands: -```bash -npm run play:stop # Stop containers -npm run env:clean # Reset all data -``` - -## Verifying Your Setup - -### Run Linters - -```bash -vendor/bin/phpcs # PHP CodeSniffer -npm run lint-js # ESLint -npm run lint-css # Stylelint -npm run pretty # Prettier -``` - -### Run Tests - -```bash -vendor/bin/phpunit # PHP unit tests -npm run test:unit # JavaScript unit tests -``` - -### Build Check - -```bash -npm run build # Should complete without errors -``` - -## Project Structure Overview - -``` -sureforms/ -├── sureforms.php # Plugin entry point -├── plugin-loader.php # Autoloader and bootstrap -├── inc/ # PHP classes (core logic) -├── src/ # JS/React source (compiled by wp-scripts) -│ ├── admin/ # Admin dashboard React app -│ ├── blocks/ # Gutenberg block components -│ ├── components/ # Shared React components -│ └── store/ # WordPress data store -├── sass/ # SASS source files -├── assets/ # Compiled assets (CSS, JS, images) -├── templates/ # PHP templates (BladeOne) -├── tests/ # Test files -├── modules/ # Feature modules -└── admin/ # Admin PHP + assets -``` - -See [Architecture Overview](Architecture-Overview) for a detailed breakdown. - -## Key Concepts - -- **Custom Post Type**: Forms are stored as `sureforms_form` posts with block content -- **Custom Database Tables**: Entries in `wp_srfm_entries`, payments in `wp_srfm_payments` -- **REST API**: All data operations use the `sureforms/v1` REST namespace -- **Block Editor**: Form fields are Gutenberg blocks registered via `block.json` -- **Admin SPA**: The dashboard is a React single-page application using Force UI components - -## Next Steps - -- [Architecture Overview](Architecture-Overview) -- Understand the codebase -- [Contributing Guide](Contributing-Guide) -- Development workflow and standards -- [Gutenberg Blocks](Gutenberg-Blocks) -- How form blocks work -- [REST API Reference](REST-API-Reference) -- Available API endpoints -- [Testing Guide](Testing-Guide) -- Running and writing tests diff --git a/docs/wiki/Gutenberg-Blocks.md b/docs/wiki/Gutenberg-Blocks.md deleted file mode 100644 index ed38cd9b3..000000000 --- a/docs/wiki/Gutenberg-Blocks.md +++ /dev/null @@ -1,129 +0,0 @@ -# Gutenberg Blocks - -SureForms provides 14+ Gutenberg block types for building forms in the WordPress block editor. - -## Block Types - -### Field Blocks - -| Block | Slug | Description | -|-------|------|-------------| -| Input | `srfm/input` | Text input (text, hidden, etc.) | -| Email | `srfm/email` | Email address input with validation | -| Textarea | `srfm/textarea` | Multi-line text area | -| Number | `srfm/number` | Numeric input with min/max | -| Phone | `srfm/phone` | Phone number with country selector (intl-tel-input) | -| URL | `srfm/url` | URL input with validation | -| Dropdown | `srfm/dropdown` | Select dropdown with options | -| Checkbox | `srfm/checkbox` | Single/multiple checkboxes | -| Multi Choice | `srfm/multichoice` | Radio/checkbox group | -| Address | `srfm/address` | Address field with subfields (street, city, state, zip, country) | -| GDPR | `srfm/gdpr` | GDPR consent checkbox | -| Payment | `srfm/payment` | Payment collection (Stripe integration) | -| Inline Button | `srfm/inlinebutton` | Inline form button | - -### Container Block - -| Block | Slug | Description | -|-------|------|-------------| -| SForm | `srfm/sform` | Main form wrapper block (required container for all field blocks) | - -## Block Registration - -### Server-Side (PHP) - -Blocks are registered in `inc/blocks/register.php` via the `Blocks\Register` class, initialized on the `init` hook: - -```php -// inc/blocks/register.php -register_block_type_from_metadata( - SRFM_DIR . 'inc/blocks/{block-type}/', - [ - 'render_callback' => [ $block_instance, 'render' ], - ] -); -``` - -Each block type has: -- `block.json` -- Block metadata, attributes, and category -- `block.php` -- Server-side rendering class extending `Blocks\Base` - -### Client-Side (JavaScript) - -Block registration happens in `src/blocks/blocks.js`: - -```javascript -// src/blocks/blocks.js -import { registerBlockType } from '@wordpress/blocks'; -// Block imports and registrations... -``` - -The `src/blocks/register-block.js` utility handles individual block registration. - -## Block Architecture - -### Base Class: `Blocks\Base` - -All block PHP classes extend `SRFM\Inc\Blocks\Base`, which provides: - -- `render()` -- Main render callback invoked by WordPress -- Access to block attributes from `block.json` -- Common rendering logic shared across field types -- Integration with field markup classes - -### Block to Field Mapping - -Each block delegates HTML generation to its corresponding field markup class in `inc/fields/`: - -``` -Block (inc/blocks/input/block.php) - -> Field Markup (inc/fields/input-markup.php) - -> Base Field (inc/fields/base.php) - -> HTML output -``` - -### block.json Structure - -Each block directory contains a `block.json` file defining: - -```json -{ - "apiVersion": 3, - "name": "srfm/input", - "title": "Input", - "category": "sureforms", - "attributes": { - "label": { "type": "string", "default": "Input" }, - "required": { "type": "boolean", "default": false }, - "placeholder": { "type": "string", "default": "" } - }, - "supports": { "html": false } -} -``` - -## Adding a New Block Type - -1. **Create block directory:** `inc/blocks/{new-type}/` -2. **Add `block.json`** with attributes, name, and category -3. **Create `block.php`** extending `Blocks\Base` with a `render()` method -4. **Create field markup:** `inc/fields/{new-type}-markup.php` extending `Fields\Base` -5. **Register in `Blocks\Register`** -- add to the blocks array -6. **Create editor component:** `src/blocks/{new-type}/` with `edit.js` and `index.js` -7. **Add to `src/blocks/blocks.js`** -- import and register the new block - -## The sform Container Block - -The `srfm/sform` block is the required wrapper for all form fields. It: - -- Renders the `` HTML element -- Handles form-level settings (styling, confirmation, CAPTCHA) -- Manages submission nonce generation -- Controls form layout and submit button -- Integrates with frontend JavaScript for submission handling - -## Related Pages - -- [Form Fields Architecture](Form-Fields-Architecture) -- Field markup and validation -- [Block Editor Controls](Block-Editor-Controls) -- Custom inspector controls -- [Architecture Overview](Architecture-Overview) -- Block registration in bootstrap -- [Frontend Assets](Frontend-Assets) -- Block editor asset loading diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md deleted file mode 100644 index 365d8af63..000000000 --- a/docs/wiki/Home.md +++ /dev/null @@ -1,58 +0,0 @@ -# SureForms Wiki - -Welcome to the SureForms developer wiki. This documentation covers the architecture, APIs, development workflows, and internals of the SureForms WordPress form builder plugin. - -**Version:** 2.5.1 | **WordPress:** 6.4+ | **PHP:** 7.4+ | **Node:** 24.16.0 - -## Quick Links - -- [Getting Started](Getting-Started) -- Set up your development environment -- [Architecture Overview](Architecture-Overview) -- Understand the codebase structure -- [Contributing Guide](Contributing-Guide) -- How to contribute - -## Architecture & Core - -| Page | Description | -|------|-------------| -| [Architecture Overview](Architecture-Overview) | Plugin bootstrap, namespaces, directory layout, design patterns | -| [Database Schema](Database-Schema) | Custom tables (`wp_srfm_entries`, `wp_srfm_payments`), post meta keys | -| [Environment Configuration](Environment-Configuration) | Constants, build tools, linting, development setup | -| [Form Submission Flow](Form-Submission-Flow) | End-to-end submission pipeline from frontend to storage | - -## WordPress / Backend - -| Page | Description | -|------|-------------| -| [Gutenberg Blocks](Gutenberg-Blocks) | Block types, registration, `block.json` structure | -| [Form Fields Architecture](Form-Fields-Architecture) | Field type hierarchy, validation, rendering pipeline | -| [WordPress Hooks Reference](WordPress-Hooks-Reference) | Custom actions, filters, and execution order | -| [Payment Integration](Payment-Integration) | Stripe setup, payment flow, webhooks, refunds | -| [Email Notifications](Email-Notifications) | Email templates, smart tags, notification settings | -| [AI Form Builder](AI-Form-Builder) | AI-powered form generation process | -| [Data Export & Import](Data-Export-Import) | Form/entry export, import, duplication | - -## Frontend / React - -| Page | Description | -|------|-------------| -| [Admin Dashboard](Admin-Dashboard) | React SPA architecture, routing, data fetching | -| [State Management](State-Management) | WordPress data store, actions, selectors | -| [Frontend Assets](Frontend-Assets) | Asset loading, build pipeline, SASS, Tailwind | -| [Block Editor Controls](Block-Editor-Controls) | Custom inspector controls, CSS generation, icons | - -## API - -| Page | Description | -|------|-------------| -| [REST API Reference](REST-API-Reference) | All endpoints with methods, parameters, authentication | - -## Development Workflow - -| Page | Description | -|------|-------------| -| [Getting Started](Getting-Started) | Prerequisites, installation, first build | -| [Contributing Guide](Contributing-Guide) | Git workflow, coding standards, PR process | -| [Testing Guide](Testing-Guide) | PHPUnit, Playwright, Jest setup and execution | -| [Deployment Guide](Deployment-Guide) | CI/CD pipelines, release process, version bumping | -| [Troubleshooting & FAQ](Troubleshooting-FAQ) | Common issues and debugging tips | -| [Changelog](Changelog) | Recent version history | diff --git a/docs/wiki/Payment-Integration.md b/docs/wiki/Payment-Integration.md deleted file mode 100644 index 28d985e6e..000000000 --- a/docs/wiki/Payment-Integration.md +++ /dev/null @@ -1,127 +0,0 @@ -# Payment Integration - -SureForms supports Stripe payment processing with one-time payments and recurring subscriptions. - -## Architecture Overview - -``` -inc/payments/ -|-- payments.php # Main payments orchestrator -|-- front-end.php # Frontend payment handling -|-- payment-helper.php # Shared payment utilities -|-- admin/ -| |-- admin-handler.php # Admin payment management UI -|-- stripe/ - |-- payments-settings.php # Stripe settings + REST routes - |-- stripe-helper.php # Stripe API utilities - |-- stripe-webhook.php # Webhook event handler - |-- admin-stripe-handler.php # Admin Stripe config -``` - -## Stripe Setup - -### Configuration - -Stripe settings are managed via the admin settings page and stored as WordPress options: - -1. Navigate to SureForms > Settings > Payments -2. Connect your Stripe account via OAuth flow -3. Configure test/live mode -4. Set default currency - -The `Payments_Settings` class registers REST routes for settings management and handles the Stripe OAuth callback via `admin_init`. - -### OAuth Flow - -1. Admin clicks "Connect Stripe" in settings -2. `intercept_stripe_callback()` on `admin_init` handles the OAuth redirect -3. Stripe credentials are securely stored in WordPress options - -## Payment Flow - -``` -1. FORM DISPLAY - - Payment block renders Stripe Elements - - Frontend_Assets enqueues Stripe JS SDK - - Payment form elements injected into the form - -2. FORM SUBMISSION - - User fills form + enters payment details - - Frontend JavaScript creates Stripe PaymentIntent - - Form data + payment token submitted to /submit-form - -3. SERVER PROCESSING - - Form_Submit validates form data - - Payment processing triggered - - Stripe API called to confirm payment - - Payment record created in wp_srfm_payments table - - Entry created in wp_srfm_entries table - - Payment ID linked to entry - -4. WEBHOOK HANDLING - - Stripe sends webhook events to /wp-json endpoint - - Stripe_Webhook validates signature - - Payment status updated in database - - Subscription events handled (renewal, cancellation, etc.) -``` - -## Payment Database Schema - -Payments are stored in the `wp_srfm_payments` custom table. See [Database Schema](Database-Schema) for full column definitions. - -### Payment Types - -| Type | Description | -|------|-------------| -| `payment` | One-time payment | -| `subscription` | Recurring subscription record | -| `renewal` | Subscription renewal payment | - -### Payment Statuses - -`pending`, `succeeded`, `failed`, `canceled`, `requires_action`, `requires_payment_method`, `processing`, `refunded`, `partially_refunded` - -### Subscription Statuses - -`active`, `canceled`, `past_due`, `unpaid`, `trialing`, `incomplete`, `incomplete_expired`, `paused` - -## Webhook Handling - -The `Stripe_Webhook` class (`inc/payments/stripe/stripe-webhook.php`) processes Stripe webhook events: - -- Validates webhook signature using Stripe signing secret -- Handles payment status updates (succeeded, failed, etc.) -- Processes subscription lifecycle events (renewal, cancellation) -- Creates renewal payment records for subscription billing cycles -- Updates payment and subscription statuses in the database - -## Refund Management - -The `Payments` table class provides refund tracking: - -- `add_refund_to_payment_data()` -- Record refund details in payment_data JSON -- `add_refund_amount()` -- Increment the refunded_amount column -- `get_refundable_amount()` -- Calculate remaining refundable balance -- `is_fully_refunded()` / `is_partially_refunded()` -- Status checks - -## Supported Currencies - -USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, SEK, NZD, MXN, SGD, HKD, NOK, TRY, RUB, INR, BRL, ZAR, KRW - -## Payment Modes - -| Mode | Description | -|------|-------------| -| `test` | Stripe test mode (test API keys) | -| `live` | Stripe live mode (production) | - -## Admin Payment Management - -The `Admin_Handler` class provides the admin interface for viewing and managing payments. Payment IDs in entry views are rendered as clickable links via the `srfm_entry_value` filter. - -## Related Pages - -- [Database Schema](Database-Schema) -- Payment table columns and relationships -- [Form Submission Flow](Form-Submission-Flow) -- Payment processing in submission pipeline -- [REST API Reference](REST-API-Reference) -- Payment-related endpoints -- [Gutenberg Blocks](Gutenberg-Blocks) -- Payment block type diff --git a/docs/wiki/REST-API-Reference.md b/docs/wiki/REST-API-Reference.md deleted file mode 100644 index d366470e5..000000000 --- a/docs/wiki/REST-API-Reference.md +++ /dev/null @@ -1,114 +0,0 @@ -# REST API Reference - -All endpoints are registered under the namespace `sureforms/v1` and accessible at `/wp-json/sureforms/v1/`. - -## Authentication - -### Admin Endpoints -All admin endpoints require the `manage_options` capability. Authentication is via WordPress REST API nonce (`X-WP-Nonce` header) or cookie-based auth. - -Permission check: `Helper::get_items_permissions_check()` verifies `current_user_can('manage_options')`. - -### Public Endpoints -The form submission endpoint uses a custom nonce (`X-WP-Submit-Nonce` header) that does not require a logged-in user. - -## Endpoints - -### AI Form Builder - -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/generate-form` | Generate a form using AI | -| POST | `/map-fields` | Map AI response to SureForms Gutenberg blocks | -| GET | `/initiate-auth` | Get auth URL for AI billing portal | -| POST | `/handle-access-key` | Decrypt and save AI access key | - -### Dashboard - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/entries-chart-data` | Get entries chart data for analytics | -| GET | `/form-data` | Get all forms data for dashboard | -| GET | `/plugin-status` | Check integration plugin install status | - -### Onboarding - -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/onboarding/set-status` | Mark onboarding as complete | -| GET | `/onboarding/get-status` | Get current onboarding status | - -### Entries - -| Method | Endpoint | Description | Parameters | -|--------|----------|-------------|------------| -| GET | `/entries/list` | Paginated entries listing | `form_id`, `status`, `search`, `date_from`, `date_to`, `orderby`, `order`, `per_page`, `page` | -| POST | `/entries/read-status` | Bulk mark entries read/unread | `entry_ids` (required), `action` (required: read/unread) | -| POST | `/entries/trash` | Bulk trash/restore entries | `entry_ids` (required), `action` (required: trash/restore) | -| POST | `/entries/delete` | Permanently delete entries | `entry_ids` (required) | -| POST | `/entries/export` | Export entries to CSV/ZIP | `entry_ids`, `form_id`, `status`, `search`, `date_from`, `date_to` | -| GET | `/entry/{id}/details` | Single entry with form data | `id` (required) | -| GET | `/entry/{id}/logs` | Entry activity logs | `id` (required), `page`, `per_page` | - -### Forms - -| Method | Endpoint | Description | Parameters | -|--------|----------|-------------|------------| -| GET | `/forms` | Paginated forms listing | `page`, `per_page`, `search`, `status`, `orderby`, `order`, `date_from`, `date_to` | -| POST | `/forms/export` | Export forms as JSON | `form_ids` (required) | -| POST | `/forms/import` | Import forms from JSON | File upload | -| POST | `/forms/manage` | Manage form lifecycle | `form_ids` (required), `action` (trash/restore/delete) | -| POST | `/forms/duplicate` | Duplicate a form | `form_id` (required) | - -### Form Submission (Public) - -| Method | Endpoint | Auth | Description | -|--------|----------|------|-------------| -| POST | `/submit-form` | Custom nonce | Public form submission | -| GET | `/refresh-nonces` | None | Refresh frontend nonces | - -## Extending the API - -Endpoints are extensible via the `srfm_rest_api_endpoints` filter: - -```php -add_filter( 'srfm_rest_api_endpoints', function( $endpoints ) { - $endpoints['custom/endpoint'] = [ - 'methods' => 'GET', - 'callback' => [ $this, 'handle_custom' ], - 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ], - ]; - return $endpoints; -} ); -``` - -## Error Responses - -Errors are returned via `wp_send_json_error()`: - -```json -{ - "success": false, - "data": "Error message string" -} -``` - -## Pagination - -List endpoints support pagination with consistent parameters: - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `page` | 1 | Current page number | -| `per_page` | 20 | Items per page | -| `orderby` | `created_at` | Sort column | -| `order` | `DESC` | Sort direction (ASC/DESC) | - -Response includes total counts for pagination UI. - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- API registration flow -- [Form Submission Flow](Form-Submission-Flow) -- Submit endpoint details -- [Data Export Import](Data-Export-Import) -- Export/import endpoints -- [Payment Integration](Payment-Integration) -- Payment API endpoints diff --git a/docs/wiki/State-Management.md b/docs/wiki/State-Management.md deleted file mode 100644 index 6cb4b072a..000000000 --- a/docs/wiki/State-Management.md +++ /dev/null @@ -1,109 +0,0 @@ -# State Management - -SureForms uses the `@wordpress/data` package to create a Redux-like data store for managing application state in the block editor. - -## Store Structure - -Located in `src/store/`: - -``` -src/store/ -|-- store.js # Store registration -|-- reducer.js # State reducers -|-- actions.js # Action creators -|-- selectors.js # State selectors -|-- constants.js # Store constants -|-- setInitialState.js # Initial state configuration -``` - -## Store Registration - -The store is registered with `@wordpress/data` in `store.js`: - -```javascript -import { createReduxStore, register } from '@wordpress/data'; -import reducer from './reducer'; -import * as actions from './actions'; -import * as selectors from './selectors'; - -const store = createReduxStore('sureforms', { - reducer, - actions, - selectors, -}); - -register(store); -``` - -The store namespace is `sureforms`. - -## Initial State - -`setInitialState.js` configures the initial state from server-side localized data (`wp_localize_script`), providing: - -- Form settings and metadata -- Block defaults -- User preferences -- Plugin configuration - -## Actions - -Actions are defined in `actions.js` and dispatched to modify the store state. They follow the WordPress data store pattern: - -```javascript -// Dispatching an action -import { dispatch } from '@wordpress/data'; -dispatch('sureforms').updateFormSettings({ key: 'value' }); -``` - -## Selectors - -Selectors in `selectors.js` extract specific data from the store state: - -```javascript -// Using a selector -import { select } from '@wordpress/data'; -const settings = select('sureforms').getFormSettings(); -``` - -## Component Integration - -Components connect to the store using WordPress data hooks: - -```javascript -import { useSelect, useDispatch } from '@wordpress/data'; - -function MyComponent() { - const formData = useSelect((select) => { - return select('sureforms').getFormSettings(); - }, []); - - const { updateFormSettings } = useDispatch('sureforms'); - - return ( - - ); -} -``` - -## Constants - -`constants.js` defines store-wide constants used across actions, reducers, and selectors to ensure consistency. - -## Usage Context - -The WordPress data store is primarily used in: - -- **Block editor** -- Form-level settings and block state -- **Inspector controls** -- Sidebar settings panels -- **Block attributes** -- Synchronized state between blocks - -For the admin dashboard pages, `@tanstack/react-query` is used instead for server state management. See [Admin Dashboard](Admin-Dashboard) for details. - -## Related Pages - -- [Admin Dashboard](Admin-Dashboard) -- React admin app architecture -- [Block Editor Controls](Block-Editor-Controls) -- Inspector controls using the store -- [Gutenberg Blocks](Gutenberg-Blocks) -- Block registration and attributes diff --git a/docs/wiki/Testing-Guide.md b/docs/wiki/Testing-Guide.md deleted file mode 100644 index ea129c084..000000000 --- a/docs/wiki/Testing-Guide.md +++ /dev/null @@ -1,172 +0,0 @@ -# Testing Guide - -SureForms uses three testing frameworks: PHPUnit for PHP unit tests, Playwright for end-to-end tests, and Jest for JavaScript unit tests. - -## Testing Strategy - -| Level | Framework | Location | Purpose | -|-------|-----------|----------|---------| -| Unit (PHP) | PHPUnit 9.x | `tests/unit/` | PHP class and function testing | -| Unit (JS) | Jest | Via `wp-scripts` | JavaScript utility testing | -| E2E | Playwright 1.48+ | `tests/play/` | Full browser-based workflow testing | - -## PHPUnit (PHP Unit Tests) - -### Setup - -1. Install PHP dependencies: - ```bash - composer install - ``` - -2. WordPress test environment must be available (via wp-env or local setup) - -### Running Tests - -```bash -# Run all PHP unit tests -vendor/bin/phpunit - -# Run a specific test file -vendor/bin/phpunit tests/unit/test-specific.php - -# Run tests with verbose output -vendor/bin/phpunit --verbose -``` - -### Test File Conventions - -- Location: `tests/unit/` -- File prefix: `test-` (e.g., `test-helper.php`) -- Class naming: `Test_ClassName` extending `WP_UnitTestCase` -- Method naming: `test_` prefix (e.g., `test_form_submission()`) - -### CI Configuration - -PHPUnit runs in GitHub Actions (`.github/workflows/phpunit.yml`): -- PHP: 8.2 -- WordPress: trunk -- MySQL: 5.7 -- Triggered on pull requests - -## Playwright (E2E Tests) - -### Prerequisites - -- Node.js 24.16.0 (Volta) -- Docker (for wp-env) - -### Setup - -```bash -# Install dependencies -npm install - -# Start the WordPress environment -npm run play:up - -# Build the plugin -npm run build - -# Install Playwright browsers -npx playwright install -``` - -### Running Tests - -```bash -# Run all E2E tests (headless) -npm run play:run - -# Run tests in headed mode (visible browser) -npm run play:run:interactive - -# Run a specific test file -npx playwright test tests/play/specific-test.spec.js -``` - -### Test File Conventions - -- Location: `tests/play/` -- File suffix: `.spec.js` -- Uses Playwright Test runner with `expect` assertions -- Page Object Model pattern for reusable interactions - -### CI Configuration - -Playwright runs in GitHub Actions (`.github/workflows/playwright.yml`): -- Triggered when PR is labeled with `e2e` -- Node: 18.15 -- Builds plugin and starts wp-env -- Generates HTML test report (30-day artifact retention) - -## Jest (JavaScript Unit Tests) - -### Running Tests - -```bash -npm run test:unit -``` - -Uses `@wordpress/scripts` Jest configuration with WordPress-specific transforms and module resolution. - -## Test Environment (wp-env) - -The Docker-based test environment is configured in `.wp-env.json`: - -```bash -# Start environment -npm run play:up - -# Stop environment -npm run play:stop - -# Clean all data -npm run env:clean -``` - -wp-env provides: -- WordPress installation with the plugin active -- MySQL database -- Consistent environment across developers -- Used by both Playwright E2E tests and local development - -## Writing New Tests - -### PHP Unit Test - -```php -class Test_My_Feature extends WP_UnitTestCase { - public function test_feature_works() { - // Arrange - $form_id = $this->factory->post->create([ - 'post_type' => 'sureforms_form', - ]); - - // Act - $result = some_function($form_id); - - // Assert - $this->assertEquals('expected', $result); - } -} -``` - -### Playwright E2E Test - -```javascript -const { test, expect } = require('@playwright/test'); - -test('form submission works', async ({ page }) => { - await page.goto('/form-page/'); - await page.fill('[name="email"]', 'test@example.com'); - await page.click('button[type="submit"]'); - await expect(page.locator('.success-message')).toBeVisible(); -}); -``` - -## Related Pages - -- [Environment Configuration](Environment-Configuration) -- Development setup -- [Contributing Guide](Contributing-Guide) -- Test requirements for PRs -- [Deployment Guide](Deployment-Guide) -- CI/CD test pipeline diff --git a/docs/wiki/Troubleshooting-FAQ.md b/docs/wiki/Troubleshooting-FAQ.md deleted file mode 100644 index 7afa4619a..000000000 --- a/docs/wiki/Troubleshooting-FAQ.md +++ /dev/null @@ -1,232 +0,0 @@ -# Troubleshooting & FAQ - -Common issues, debugging tips, and frequently asked questions for SureForms development. - -## Common Development Issues - -### Build Failures - -**Problem:** `npm run build` fails with errors. - -**Solutions:** -1. Ensure Node.js 24.16.0 is active (Volta manages this automatically via `package.json`): - ```bash - node -v # Should output v24.16.0 - ``` -2. Clear and reinstall dependencies: - ```bash - rm -rf node_modules - npm install - ``` -3. If SASS compilation fails, check that `sass/` source files have valid syntax -4. If Grunt minification fails, ensure Grunt CLI is available: - ```bash - npx grunt minify - ``` - -### wp-env Issues - -**Problem:** `npm run play:up` fails to start the Docker environment. - -**Solutions:** -1. Ensure Docker is installed and running -2. Clean the environment: - ```bash - npm run env:clean - ``` -3. Restart the environment: - ```bash - npm run play:stop - npm run play:up - ``` -4. If port conflicts occur, check for other services on ports 8888/8889 - -### PHPCS Errors - -**Problem:** `vendor/bin/phpcs` reports violations. - -**Solutions:** -1. Auto-fix what's possible: - ```bash - vendor/bin/phpcbf - ``` -2. Common issues: - - Missing text domain: Add `'sureforms'` to all translatable strings - - Unescaped output: Wrap output in `esc_html()`, `esc_attr()`, etc. - - Missing nonce verification: Add nonce checks for form/AJAX handlers - - Short array syntax: Use `[]` instead of `array()` (enforced by config) - -### PHPUnit Test Failures - -**Problem:** `vendor/bin/phpunit` tests fail. - -**Solutions:** -1. Ensure WordPress test environment is available (wp-env or local setup) -2. Check database connectivity for the test suite -3. Run a specific test to isolate the issue: - ```bash - vendor/bin/phpunit tests/unit/test-specific.php - ``` - -### JavaScript Lint Errors - -**Problem:** `npm run lint-js` reports errors. - -**Solutions:** -1. Auto-fix most issues: - ```bash - npm run lint-js:fix - ``` -2. Format code with Prettier: - ```bash - npm run pretty:fix - ``` -3. Common issues: - - Import ordering: WordPress dependencies should use `@wordpress/*` - - Unused variables: Remove or prefix with underscore - - Missing dependencies in `useEffect`: Add to dependency array - -## Form Submission Issues - -### Nonce Verification Fails - -**Cause:** The `X-WP-Submit-Nonce` has expired (default: 24-hour lifetime). - -**Debug:** -- Nonces can be refreshed via `GET /wp-json/sureforms/v1/refresh-nonces` -- Check that the nonce is being sent in the request header -- Caching plugins may cache the nonce -- exclude form pages from page cache - -### CAPTCHA Validation Fails - -**Cause:** CAPTCHA keys are misconfigured or the token is expired. - -**Debug:** -1. Verify CAPTCHA keys in SureForms global settings -2. Check browser console for CAPTCHA loading errors -3. Ensure the CAPTCHA provider (reCAPTCHA, hCaptcha, Turnstile) is correctly configured -4. reCAPTCHA v3 can trigger multiple times on pages with multiple forms - -### File Upload Errors - -**Debug:** -1. Check PHP `upload_max_filesize` and `post_max_size` in `php.ini` -2. Verify allowed file types in form field settings -3. Check file size against the configured maximum -4. Ensure the `wp-content/uploads/` directory is writable - -## Database Issues - -### Custom Tables Missing - -**Cause:** The `wp_srfm_entries` or `wp_srfm_payments` table was not created. - -**Debug:** -1. Deactivate and reactivate the plugin to trigger table creation -2. Check the `srfm_db_version` option in `wp_options` -3. Verify database user has `CREATE TABLE` permissions -4. Check for errors in `debug.log` during activation - -### Entry Data Not Saving - -**Debug:** -1. Check the `do_not_store_entries` form setting (GDPR compliance) -2. Verify the form submission response for error messages -3. Check `debug.log` for database insert errors -4. Ensure the entries table schema is up to date - -## REST API Issues - -### 401 Unauthorized on Admin Endpoints - -**Cause:** User is not authenticated or lacks required capabilities. - -**Debug:** -1. Admin endpoints require `manage_options` capability -2. Ensure the user is logged in with a valid session -3. Check that the REST API nonce is being sent (via `X-WP-Nonce` header) -4. WordPress cookie authentication must be active - -### 403 on Form Submission - -**Cause:** The submit nonce is invalid or form restrictions are blocking submission. - -**Debug:** -1. Check the `X-WP-Submit-Nonce` header value -2. Review form restriction settings (max entries, date scheduling, login required) -3. Check the `srfm_form_submit_permissions_check` filter - -## Performance Issues - -### Slow Admin Dashboard - -**Causes & Solutions:** -1. **Large number of entries:** Entries are paginated -- default page size is managed by REST API -2. **Unoptimized queries:** The entries table has indexes on `form_id`, `user_id`, and a composite index on `(form_id, created_at, status)` -3. **Asset loading:** Admin assets are conditionally loaded only on SureForms admin pages - -### Slow Form Rendering - -**Causes & Solutions:** -1. **Too many blocks:** Complex forms with many fields may benefit from multi-step layout -2. **External scripts:** CAPTCHA and payment scripts load conditionally based on form settings -3. **CSS generation:** Each block generates dynamic CSS -- review `generateCSS.js` for optimization - -## Debugging Tips - -### Enable WordPress Debug Mode - -Add to `wp-config.php`: -```php -define( 'WP_DEBUG', true ); -define( 'WP_DEBUG_LOG', true ); -define( 'WP_DEBUG_DISPLAY', false ); -define( 'SCRIPT_DEBUG', true ); -``` - -### Check REST API Responses - -Use the browser developer tools Network tab to inspect REST API calls: -- Filter by `sureforms/v1` to see plugin API requests -- Check response status codes and error messages - -### Inspect Form Data Store - -In the browser console on a form editor page: -```javascript -wp.data.select('starter-starter/starter-starter-forms-data').getFormSettings(); -``` - -## FAQ - -### What WordPress version is required? - -WordPress 6.4 or higher is required. - -### What PHP version is required? - -PHP 7.4 or higher. The `phpcs.xml` enforces PHP 7.4+ compatibility. - -### Can I modify bundled libraries? - -Files in `inc/lib/` are third-party libraries and should not be modified directly. If changes are needed, they should be managed upstream. - -### Where do I put new PHP classes? - -New PHP classes go in the `inc/` directory following the existing namespace structure (`SRFM\Inc\*`). The autoloader in `plugin-loader.php` handles class loading. - -### How do I add a new admin page? - -Admin pages are rendered by the React SPA in `src/admin/`. Add new routes using `react-router-dom` and register the corresponding menu items in PHP. See [Admin Dashboard](Admin-Dashboard) for details. - -### How do I add a new form field type? - -See [Form Fields Architecture](Form-Fields-Architecture) for the field type hierarchy and the process for adding new field types. - -## Related Pages - -- [Environment Configuration](Environment-Configuration) -- Development setup -- [Testing Guide](Testing-Guide) -- Running tests -- [Architecture Overview](Architecture-Overview) -- Codebase structure -- [REST API Reference](REST-API-Reference) -- API endpoints -- [Database Schema](Database-Schema) -- Table structure diff --git a/docs/wiki/Using-WPML-With-SureForms.md b/docs/wiki/Using-WPML-With-SureForms.md deleted file mode 100644 index 540b74aef..000000000 --- a/docs/wiki/Using-WPML-With-SureForms.md +++ /dev/null @@ -1,243 +0,0 @@ -# Using WPML With SureForms - -SureForms ships with native compatibility for **WPML**, the leading WordPress multilingual plugin. With WPML active, you can offer your forms in any number of languages without duplicating the form itself — one form serves every language, every submission is tagged with the visitor's language, and translators work from a single place. - -This guide walks you through everything you need to know as a site owner, content editor, or translator. - ---- - -## What you can do with WPML + SureForms - -Once both plugins are active and configured, SureForms automatically: - -- Translates **field labels, placeholders, help text, error messages** and **dropdown / multi-choice options** based on the visitor's language. -- Translates the **submit button** label. -- Translates the **success message** shown after a submission. -- Translates the **confirmation redirect content** (when "show same page" confirmation is used). -- Translates the **form-restriction notice** (e.g., "This form is closed"). -- Records the **submission language** alongside every entry, so you can see at a glance which language a lead came from. - -You keep one form. You collect entries from every language into the same list. Reports, integrations, and exports stay unified. - ---- - -## Before you start - -You will need: - -| Item | Notes | -|---|---| -| **WordPress 6.4 or higher** | SureForms minimum requirement. | -| **WPML Multilingual CMS** (4.5+) | Paid plugin from [wpml.org](https://wpml.org). | -| **WPML String Translation** add-on | Required to translate form labels, options, and messages. Bundled with most WPML licences. | -| **SureForms 2.5.1 or higher** | Native WPML support shipped in this release. | - -> **Polylang and other multilingual plugins:** SureForms is architected with a generic provider interface so other plugins can be supported in the future. WPML is the only multilingual plugin currently supported out of the box. - ---- - -## Step 1 — Install and configure WPML - -1. Install and activate the following plugins from your WordPress dashboard: - - **WPML Multilingual CMS** (`sitepress-multilingual-cms`) - - **WPML String Translation** - -2. Open **WPML → Languages** and choose at least two languages (for example, English as the default, plus German and French). - -3. Open **WPML → Settings → Translation Options** and confirm that **Posts** are set to *"Translatable — use translation"*. - -That's the only WPML setup that is required. SureForms takes care of the rest automatically. - ---- - -## Step 2 — Create your form (just once) - -Create a SureForms form exactly the way you always have: - -1. Go to **SureForms → Add New Form**. -2. Pick a template or start blank. -3. Add fields (text, email, dropdown, multi-choice, etc.) and configure their labels, placeholders, help text, options, and error messages in your default language. -4. Configure the submit button text, success message, and any other form settings. -5. Save the form. - -> **Important:** Do **not** try to duplicate the form for each language. SureForms intentionally registers the form post type as *non-translatable* in WPML. There is one canonical form post; WPML translates the strings on it. This keeps entries unified and avoids the maintenance nightmare of keeping multiple form copies in sync. - -When you save the form, SureForms automatically registers every translatable string with WPML under the `sureforms` text domain. - ---- - -## Step 3 — Translate your form strings - -1. Go to **WPML → String Translation**. -2. In the **"Select strings within domain"** dropdown at the top, choose `sureforms`. -3. You will see entries for every translatable piece of the form, grouped by form ID for easy scanning. For example, a form with ID `42` will show strings such as: - - - `form_42_submit_button` - - `form_42_confirmation_0_message` - - `form_42_notification_0_subject` - - `form_42_restriction_message` - - `form_42_block_{block-id}_label` - - `form_42_block_{block-id}_placeholder` - - `form_42_block_{block-id}_helpText` - - `form_42_block_{block-id}_errorMsg` - - `form_42_block_{block-id}_option_0_label` *(for dropdown / multi-choice options)* - -4. Click **"translations"** next to any string, enter the translated text for each language, and save. - -You can also use WPML's built-in **Translation Editor** workflow or send strings to a translation service from this screen — the standard WPML String Translation UI applies. - -> **Tip:** The per-form naming scheme (`form_{id}_...`) keeps every form's strings neatly grouped, so translators don't have to hunt across the entire site for a particular form's labels. - ---- - -## Step 4 — Display your form on translated pages - -SureForms forms are added to pages exactly the way they always have been — via the SureForms block or the `[sureforms id="..."]` shortcode. - -For the translated version of a form to render correctly, the **host page must also be translated in WPML**: - -1. Create the original page and embed your form on it. -2. In WPML, translate that page into each language. The form block / shortcode is carried over automatically. -3. Visit the translated URL (e.g., `/de/contact/`) — the form will render with all the labels, placeholders, options, and the submit button in German. - -If you instead use SureForms' **instant form** URL (`/form/{id}/`), append the language code to switch languages: - -``` -https://example.com/form/42/?lang=de -https://example.com/form/42/?lang=fr -``` - ---- - -## Step 5 — Verify everything is translating - -Open the translated form in the browser and confirm that the following are all in the target language: - -- ✅ Field labels -- ✅ Field placeholders -- ✅ Help text -- ✅ Inline error messages (e.g., "This field is required") -- ✅ Dropdown / multi-choice / checkbox option labels -- ✅ Submit button text -- ✅ Success / confirmation message after submission -- ✅ Form restriction notice (if the form is closed or capped) - -If anything appears in the original language: - -1. Check that the host page is translated in WPML. -2. Check that the specific string has a translation in **WPML → String Translation** (filter by domain `sureforms`). -3. Re-save the form once — this regenerates the string registry. - ---- - -## Step 6 — Collect entries and review them by language - -Submissions made on the German version of the page are stored against the **same form** as English submissions, with the submission language recorded on the entry row. - -1. Go to **SureForms → Entries**. -2. You will see a new **Language** column between *Date & Time* and *Actions*. -3. Each entry displays an uppercase language code: `EN`, `DE`, `FR`, etc. -4. Click the **Language** column header to sort entries by language. - -Older entries (and entries submitted on sites where WPML is not active) display `—`. - -> **Tip:** All your usual entry features — search, view details, delete, export — work as normal. Language is just an extra column for filtering and reporting. - ---- - -## Step 7 — Form restriction messages - -If you have configured form restrictions (a closing date or maximum entry count), the **restriction notice** shown to visitors is automatically translated: - -1. Open the form and enable restrictions (e.g., set a close date in the past, or set a max-entry limit). -2. Customise the restriction message in the form settings. -3. Translate `form_{id}_restriction_message` in **WPML → String Translation**. -4. Trigger the restriction (visit the form in a translated URL once it's closed) — visitors see the translated restriction notice. - ---- - -## What about email notifications? - -> **Note:** In the current release, email notifications (the message sent to your admin or to the visitor after a successful submission) are sent in the site's default language. Multilingual email notifications — where each email is composed in the visitor's submission language — are planned for a future SureForms release. - -The submission language is already recorded on the entry, so any custom integration that hooks into the SureForms submission flow can use it. - ---- - -## Graceful fallback when WPML is not active - -You can safely install and uninstall WPML at any time: - -- **WPML active**: forms translate based on the visitor's language. Entries are tagged. -- **WPML deactivated**: forms render in their default language as if WPML had never been installed. New entries record an empty language string. No errors, no broken pages. - -The plugin is designed so that the multilingual layer is completely silent when WPML is not active. - ---- - -## Troubleshooting - -**Translated strings don't appear in WPML String Translation** - -- Confirm that *WPML String Translation* is active (not just WPML core). -- Re-save your form once. The string registry runs on form save. -- Filter by domain `sureforms` in the WPML String Translation UI — strings from other plugins live in different domains. - -**Translated labels still render in the original language on `/de/...`** - -- Make sure the host page is translated in WPML and you are visiting the translated URL. -- Open WPML → String Translation, filter by `sureforms`, and confirm the specific strings actually have translations saved. -- Check that the page language switcher confirms you are on the German (or other target) version of the page. - -**The success message is in English even though I submitted in German** - -- Confirm `form_{id}_confirmation_0_message` has a German translation in WPML String Translation. -- Confirm the form is being submitted from a German page (`/de/...`), not from a query-string variant in some WPML modes. - -**Entries show `—` in the Language column** - -- Older entries submitted before WPML support shipped will not have a language stored. -- Entries created from the admin (e.g., test submissions, or imports) may not carry a language code. -- New front-end submissions on translated pages with WPML active will always carry a language code. - -**An entry's language code is wrong** - -- The visitor's language is determined by WPML based on the URL of the page they submitted from. If your WPML configuration uses query-string mode (`?lang=de`), make sure the parameter is on the page URL when the form is submitted. - ---- - -## Frequently asked questions - -**Do I need to duplicate the form for each language?** - -No. There is one canonical form post regardless of how many languages you support. WPML translates the strings on it. - -**Will entries from different languages mix in the same list?** - -Yes — by design. All entries from a given form, regardless of the visitor's language, belong to that one form. The Language column lets you filter and sort. - -**Can I export entries with the language?** - -The submission language is stored alongside each entry and is available to any integration that reads SureForms entries. A future SureForms release will include the language column in the standard CSV export header. - -**What languages are supported?** - -Any language WPML supports. SureForms records the standard WPML language code (`en`, `de`, `hi`, `zh-cn`, `pt-br`, etc.) — up to 20 characters, which covers the full WPML and BCP-47 locale-code range. - -**Does this work with Polylang or other multilingual plugins?** - -SureForms is built on a generic multilingual provider interface, so support for Polylang and other plugins is straightforward to add. WPML is the only multilingual plugin currently supported in the box. - ---- - -## Need help? - -If you run into anything not covered here, contact the SureForms support team at [sureforms.com/support](https://sureforms.com/support) with: - -1. Your SureForms version (Plugins → Installed Plugins → SureForms). -2. Your WPML version, including whether *WPML String Translation* is active. -3. Your form ID. -4. The specific string or behaviour that isn't translating. -5. A screenshot if possible. - -We're happy to help. diff --git a/docs/wiki/WordPress-Hooks-Reference.md b/docs/wiki/WordPress-Hooks-Reference.md deleted file mode 100644 index 1db16e372..000000000 --- a/docs/wiki/WordPress-Hooks-Reference.md +++ /dev/null @@ -1,152 +0,0 @@ -# WordPress Hooks Reference - -SureForms exposes custom actions and filters for extensibility, and hooks into WordPress core lifecycle events. - -## Custom Actions - -| Action | Parameters | Description | -|--------|-----------|-------------| -| `srfm_core_loaded` | -- | Fired after Plugin_Loader singleton is created | -| `srfm_before_submission` | `$form_data`, `$form_id` | Before form submission is processed | -| `srfm_form_submit` | `$entry_id`, `$form_id`, `$form_data` | After form submission is complete | -| `srfm_after_submission_process` | `$entry_id`, `$form_id` | After all post-submission processing | -| `srfm_before_delete_entry` | `$entry_id` | Before an entry is permanently deleted | - -## Custom Filters - -### REST API - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_rest_api_endpoints` | `$endpoints` | Add/modify REST API endpoints | - -### Form Data - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_form_submit_data` | `$form_data`, `$form_id` | Modify form data before processing | -| `srfm_entry_value` | `$value`, `$args` | Transform entry field values in responses | -| `srfm_normalize_csv_field_value` | `$value`, `$field` | Custom CSV export value formatting | -| `srfm_form_confirmation_params` | `$params`, `$form_id` | Modify confirmation settings | - -### Post Meta - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_register_post_meta` | `$meta_keys` | Register additional form post meta keys | - -### Blocks - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_is_special_block` | `$is_special`, `$block_name` | Declare a block as special (nested) | -| `srfm_handle_special_block` | `$result`, `$block` | Custom processing for special blocks | -| `srfm_extract_form_fields_field_name` | `$field_name`, `$block` | Modify generated field names | - -### Settings - -| Filter | Parameters | Description | -|--------|-----------|-------------| -| `srfm_global_settings_data` | `$settings` | Extend global settings data | -| `srfm_disable_nps_survey` | `$disable` | Disable NPS survey module | -| `srfm_enable_redirect_activation` | `$enable` | Control activation redirect behavior | - -## WordPress Core Hooks Used - -### plugins_loaded - -| Priority | Callback | Class | -|----------|----------|-------| -| default | `load_textdomain()` | Plugin_Loader | -| 99 | `load_plugin()` | Plugin_Loader | - -### init - -| Priority | Callback | Purpose | -|----------|----------|---------| -| 10 | `load_classes()` | Block registration, admin, payments | - -### rest_api_init - -| Callback | Class | Purpose | -|----------|-------|---------| -| `register_endpoints()` | Rest_Api | Register all admin REST endpoints | -| `register_routes()` | Form_Submit | Register public submission endpoints | -| `register_rest_routes()` | Payments_Settings | Register payment REST routes | - -### admin_init - -| Callback | Purpose | -|----------|---------| -| `activation_redirect()` | Redirect to onboarding after activation | -| `intercept_stripe_callback()` | Handle Stripe OAuth callback | - -### wp_enqueue_scripts - -| Callback | Class | Purpose | -|----------|-------|---------| -| Asset enqueue | Frontend_Assets | Load form CSS/JS on pages with forms | - -### enqueue_block_editor_assets - -| Callback | Class | Purpose | -|----------|-------|---------| -| Editor asset enqueue | Gutenberg_Hooks | Load block editor JS/CSS | - -## Hook Execution Order (Form Submission) - -``` -1. REST API receives POST /submit-form -2. Form_Submit::submit_form_permissions_check() - -> Nonce verification -3. Form_Submit::handle_form_submission() - -> apply_filters('srfm_form_submit_data', $data, $form_id) - -> Field validation - -> CAPTCHA verification - -> Honeypot check -4. do_action('srfm_before_submission', $data, $form_id) -5. Email notification sent -6. Entry stored in database -7. do_action('srfm_form_submit', $entry_id, $form_id, $data) -8. do_action('srfm_after_submission_process', $entry_id, $form_id) -9. Confirmation response returned -``` - -## Usage Examples - -### Adding a Custom REST Endpoint - -```php -add_filter( 'srfm_rest_api_endpoints', function( $endpoints ) { - $endpoints['my-custom/data'] = [ - 'methods' => 'GET', - 'callback' => 'my_custom_callback', - 'permission_callback' => [ \SRFM\Inc\Helper::class, 'get_items_permissions_check' ], - ]; - return $endpoints; -} ); -``` - -### Modifying Form Data Before Processing - -```php -add_filter( 'srfm_form_submit_data', function( $data, $form_id ) { - // Add custom processing - $data['custom_field'] = 'custom_value'; - return $data; -}, 10, 2 ); -``` - -### Running Code After Submission - -```php -add_action( 'srfm_form_submit', function( $entry_id, $form_id, $form_data ) { - // Send to external service, trigger automation, etc. -}, 10, 3 ); -``` - -## Related Pages - -- [Architecture Overview](Architecture-Overview) -- Plugin bootstrap and hook registration -- [REST API Reference](REST-API-Reference) -- Full endpoint documentation -- [Form Submission Flow](Form-Submission-Flow) -- Submission hook execution order diff --git a/docs/wiki/_Footer.md b/docs/wiki/_Footer.md deleted file mode 100644 index 0a54d2e24..000000000 --- a/docs/wiki/_Footer.md +++ /dev/null @@ -1,3 +0,0 @@ ---- - -**SureForms** by [Brainstorm Force](https://brainstormforce.com/) | [GitHub](https://github.com/brainstormforce/sureforms) | [Website](https://sureforms.com/) | [Support](https://support.brainstormforce.com/) diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md deleted file mode 100644 index 4fe3b09d1..000000000 --- a/docs/wiki/_Sidebar.md +++ /dev/null @@ -1,36 +0,0 @@ -**[Home](Home)** - -**Getting Started** -- [Getting Started](Getting-Started) -- [Environment Configuration](Environment-Configuration) - -**Architecture** -- [Architecture Overview](Architecture-Overview) -- [Database Schema](Database-Schema) -- [Form Submission Flow](Form-Submission-Flow) - -**WordPress / Backend** -- [Gutenberg Blocks](Gutenberg-Blocks) -- [Form Fields Architecture](Form-Fields-Architecture) -- [WordPress Hooks Reference](WordPress-Hooks-Reference) -- [Payment Integration](Payment-Integration) -- [Email Notifications](Email-Notifications) -- [AI Form Builder](AI-Form-Builder) -- [Data Export & Import](Data-Export-Import) -- [Using WPML With SureForms](Using-WPML-With-SureForms) - -**Frontend / React** -- [Admin Dashboard](Admin-Dashboard) -- [State Management](State-Management) -- [Frontend Assets](Frontend-Assets) -- [Block Editor Controls](Block-Editor-Controls) - -**API** -- [REST API Reference](REST-API-Reference) - -**Development** -- [Contributing Guide](Contributing-Guide) -- [Testing Guide](Testing-Guide) -- [Deployment Guide](Deployment-Guide) -- [Troubleshooting & FAQ](Troubleshooting-FAQ) -- [Changelog](Changelog) diff --git a/inc/abilities/CLAUDE.md b/inc/abilities/CLAUDE.md deleted file mode 100644 index 869f850ca..000000000 --- a/inc/abilities/CLAUDE.md +++ /dev/null @@ -1,66 +0,0 @@ -# Abilities — Development Guide - -## Adding a New Ability - -1. Create a class extending `Abstract_Ability` in the appropriate subdirectory: - - `forms/` — form CRUD operations - - `entries/` — entry/submission operations - - `embedding/` — shortcodes, block markup, rendering - - `settings/` — global settings read/write - - `analytics/` — form analytics and reporting - - `export/` — form and entry import/export -2. Register it in `Abilities_Registrar::register_abilities()` - -## Abstract_Ability Contract - -Every ability must implement: -- **Constructor** — set `$id`, `$label`, `$description`, `$capability` -- **`get_input_schema()`** — JSON Schema for input validation -- **`get_output_schema()`** — JSON Schema for output contract -- **`execute( $input )`** — business logic, return array or `WP_Error` -- **`get_annotations()`** — override if not default (`readonly`, `destructive`, `idempotent`) - -## Conventions - -- ID format: `sureforms/-` (e.g. `sureforms/list-forms`) -- Namespace: `SRFM\Inc\Abilities\\` -- Return `WP_Error` for failures, never throw exceptions -- Default capability: `edit_posts` — override per-ability where needed -- Destructive abilities must set `destructive: true` in annotations -- All classes need `@since x.x.x` tags -- Add PHPUnit tests in `tests/unit/inc/abilities/` for every new ability - -## Architecture - -``` -inc/abilities/ -├── abstract-ability.php # Base class — do not modify without updating all abilities -├── abilities-registrar.php # Singleton — registers category + all abilities -├── forms/ -│ ├── list-forms.php # readonly, idempotent -│ ├── create-form.php # uses Field_Mapping engine -│ ├── get-form.php # readonly, idempotent — parses Gutenberg blocks -│ ├── delete-form.php # destructive -│ ├── duplicate-form.php # delegates to \SRFM\Inc\Duplicate_Form -│ ├── update-form.php # write, idempotent — title/status/metadata -│ └── get-form-stats.php # readonly, idempotent — entry counts -├── entries/ -│ ├── list-entries.php # readonly, idempotent — paginated listing -│ ├── get-entry.php # readonly, idempotent — decoded form_data -│ ├── update-entry-status.php # write, idempotent — read/unread/trash/restore -│ └── delete-entry.php # destructive — permanent delete -├── embedding/ -│ └── get-shortcode.php # readonly, idempotent -├── settings/ -│ ├── get-global-settings.php # readonly, idempotent — all setting categories -│ └── update-global-settings.php # write, idempotent — delegates to Global_Settings -└── analytics/ - └── get-form-analytics.php # readonly, idempotent — submission date queries -``` - -## Extensibility - -- **Filter:** `srfm_register_abilities` — third-party plugins can add abilities (must extend `Abstract_Ability`) -- **Hooks:** `srfm_before_ability_execute` / `srfm_after_ability_execute` — fire around every execution -- **Collision guard:** Registrar checks `wp_has_ability()` before registering to avoid duplicates with zipwp-mcp -- **WP < 6.9:** Registrar bails in constructor — zero overhead when Abilities API is unavailable diff --git a/internal-docs/README.md b/internal-docs/README.md deleted file mode 100644 index 9a933cae0..000000000 --- a/internal-docs/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# SureForms Internal Documentation - -**Version:** 2.5.0 -**Last Updated:** February 2026 -**Target Audience:** Developers, AI Agents, Technical Team - ---- - -## Welcome - -This is the internal documentation for **SureForms Free** and **SureForms Pro** - WordPress form builder plugins used by 300,000+ websites. - -**SureForms Free:** AI-powered form builder with payments (Stripe), native WordPress blocks, 16+ fields -**SureForms Pro:** Advanced features - conversational forms, multi-step, conditional logic, PayPal, 24+ integrations - ---- - -## Quick Start - -### For New Developers (1 Hour) - -1. **Install locally:** - ```bash - cd /path/to/wordpress/wp-content/plugins - git clone https://github.com/brainstormforce/sureforms.git - git clone https://github.com/brainstormforce/sureforms-pro.git - cd sureforms && npm install && npm run build - cd ../sureforms-pro && npm install && npm run build - ``` - -2. **Activate plugins:** - - SureForms Free (required) - - SureForms Pro (optional, requires Free) - -3. **Create first form:** - - Dashboard → SureForms → Add New - - Try AI form builder or blank form - - Add fields from block inserter - - Publish and test submission - -4. **Read these docs:** - - [Architecture](architecture.md) - System design - - [Codebase Map](codebase-map.md) - Where is what - - [APIs](apis.md) - REST, AJAX, hooks - -### For AI Agents - -Start with [ai-agent-guide.md](ai-agent-guide.md) for codebase conventions, patterns, and agent-specific guidance. - ---- - -## Key Files & Directories - -| Path | Purpose | -|------|---------| -| `sureforms/inc/` | Core PHP logic | -| `sureforms/inc/blocks/` | Gutenberg block PHP | -| `sureforms/src/` | React components (admin UI) | -| `sureforms/inc/payments/` | Stripe payment processing | -| `sureforms/inc/ai-form-builder/` | AI form generation | -| `sureforms-pro/inc/business/` | Pro features (PayPal, registration, PDF) | -| `sureforms-pro/inc/pro/native-integrations/` | 24+ service integrations | - ---- - -## Build System - -**SureForms Free:** -```bash -npm run build # Full build (webpack + sass + grunt) -npm run start # Dev mode (watch) -npm run lint-js:fix # Fix JS linting -npm run makepot # Generate translation files -``` - -**SureForms Pro:** -```bash -npm run build # Full build -npm run package # Create distributable zip -``` - -Both use: -- **@wordpress/scripts** (webpack 5, Babel, ESLint) -- **Grunt** (minification, compression) -- **Sass** for CSS compilation - ---- - -## Database Tables - -Custom tables (via `inc/database/`): - -| Table | Purpose | -|-------|---------| -| `wp_sureforms_entries` | Form submissions | -| `wp_sureforms_payments` | Payment transactions (Stripe/PayPal) | -| `wp_sureforms_integrations` | Native integration credentials (encrypted) | -| `wp_sureforms_save_resume` | Draft form state (Pro) | - ---- - -## Testing - -**Unit Tests:** -```bash -composer test # PHPUnit -npm run test:unit # Jest (JS) -``` - -**E2E Tests:** -```bash -npm run play:up # Start wp-env -npm run play:run # Run Playwright tests -``` - -**Code Quality:** -```bash -composer lint # PHP_CodeSniffer -composer phpstan # Static analysis -npm run lint-js # ESLint -``` - ---- - -## Release Process - -**Free (WordPress.org):** -1. Update version in `sureforms.php` and `package.json` -2. Run `npm run build && grunt release` -3. Files excluded via `.distignore` -4. SVN commit to WordPress.org repo - -**Pro (Custom Distribution):** -1. Update version in `sureforms-pro.php` -2. Run `npm run build && npm run package` -3. Generates `sureforms-pro.zip` -4. Deploy to licensing server - ---- - -## Support & Resources - -- **GitHub (Free):** https://github.com/brainstormforce/sureforms -- **GitHub (Pro):** https://github.com/brainstormforce/sureforms-pro -- **Documentation:** https://sureforms.com/docs/ -- **Bug Bounty:** https://brainstormforce.com/bug-bounty-program/ -- **Support:** support@brainstormforce.com - ---- - -## Next Steps - -- **Product Vision:** [product-vision.md](product-vision.md) - Understand goals and personas -- **Architecture:** [architecture.md](architecture.md) - System design and data flow -- **Onboarding:** [onboarding.md](onboarding.md) - 1-hour, 1-day, 1-week learning paths - ---- - -**Maintained by:** SureForms Engineering Team -**Contact:** engineering@brainstormforce.com diff --git a/internal-docs/ai-agent-guide.md b/internal-docs/ai-agent-guide.md deleted file mode 100644 index 867f3c7ea..000000000 --- a/internal-docs/ai-agent-guide.md +++ /dev/null @@ -1,459 +0,0 @@ -# AI Agent Guide - -**Version:** 2.5.0 -**For:** AI Coding Agents & LLMs - ---- - -## Overview - -This guide helps AI agents understand SureForms codebase conventions, make safe changes, and avoid common pitfalls. - ---- - -## Core Principles - -1. **Security First:** Sanitize inputs, escape outputs, use prepared statements -2. **WordPress Standards:** Follow WP coding standards, use WP functions -3. **Backward Compatibility:** Don't break existing forms/entries -4. **Performance:** Avoid N+1 queries, cache when possible -5. **Testing:** Write tests, run existing tests before committing - ---- - -## File Modification Guidelines - -### ✅ Safe to Modify - -**Add features (low risk):** -- New blocks: `inc/blocks/new-block/` -- New integrations (Pro): `inc/pro/native-integrations/integrations/new-service/` -- New validation rules: Add to `inc/field-validation.php` -- New hooks: Add filters/actions (don't remove existing) - -**Extend existing:** -- Add sanitization functions to `inc/helper.php` -- Add validation methods to `inc/field-validation.php` -- Add utility functions to helper classes - -### ⚠️ Modify with Caution - -**Database schema changes:** -- Requires migration script -- Must handle existing data -- Version increments in `inc/database/tables/*.php` - -**Payment processing:** -- `inc/payments/stripe/stripe-webhook.php` -- `inc/business/payments/pay-pal/webhook-listener.php` -- **Risk:** Financial impact if broken - -**Form submission pipeline:** -- `inc/form-submit.php` -- **Risk:** Could break all forms - -### 🚫 High Risk - Avoid Unless Necessary - -**Core WordPress integration:** -- `plugin-loader.php` -- `sureforms.php` / `sureforms-pro.php` - -**Database base class:** -- `inc/database/base.php` (inherited by all tables) - -**Critical security:** -- Nonce verification logic -- Capability checks -- Encryption (Pro): `inc/pro/native-integrations/encryption.php` - ---- - -## Common Patterns - -### Adding a New Field Type - -1. **Create block:** - ``` - inc/blocks/my-field/block.php - src/blocks/my-field/ - ``` - -2. **Register block:** - ```php - // inc/blocks/my-field/block.php - namespace SRFM\Inc\Blocks\My_Field; - - class Block extends \SRFM\Inc\Blocks\Base { - public static function register() { - register_block_type( - 'sureforms/my-field', - [ - 'render_callback' => [__CLASS__, 'render'], - 'attributes' => [/* schema */], - ] - ); - } - - public static function render($attributes) { - // Return HTML - } - } - ``` - -3. **Add to block registry:** - ```php - // inc/blocks/register.php - My_Field\Block::register(); - ``` - -4. **Add sanitization:** - ```php - // inc/helper.php - public static function sanitize_by_field_type($type, $value) { - switch ($type) { - case 'my-field': - return sanitize_text_field($value); - } - } - ``` - -### Adding a REST Endpoint - -```php -// inc/rest-api.php or new file -public function register_routes() { - register_rest_route( - 'sureforms/v1', - '/my-endpoint', - [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_request'], - 'permission_callback' => [$this, 'check_permissions'], - ] - ); -} - -public function check_permissions() { - // CRITICAL: Always verify permissions - return current_user_can('manage_options'); -} - -public function handle_request($request) { - // CRITICAL: Always verify nonce - $nonce = $request->get_header('X-WP-Nonce'); - if (!wp_verify_nonce($nonce, 'wp_rest')) { - return new \WP_Error('invalid_nonce', 'Invalid nonce', ['status' => 403]); - } - - // Sanitize inputs - $data = $request->get_params(); - $clean_data = Helper::sanitize_array_recursively($data); - - // Process... - - return ['success' => true]; -} -``` - -### Adding an Integration (Pro) - -1. **Create integration folder:** - ``` - inc/pro/native-integrations/integrations/myservice/ - ├── config.json - └── actions/ - └── send-data.php - ``` - -2. **config.json:** - ```json - { - "name": "MyService", - "slug": "myservice", - "auth_method": "api_key", - "api_endpoint": "https://api.myservice.com/v1/", - "actions": [ - { - "name": "Send Data", - "slug": "send-data", - "endpoint": "contacts" - } - ] - } - ``` - -3. **actions/send-data.php:** - ```php - namespace SRFM_PRO\Inc\Pro\Native_Integrations\Integrations\Myservice\Actions; - - class Send_Data { - public function execute($integration_data, $form_data) { - $api_key = $integration_data['api_key']; - $response = wp_remote_post( - 'https://api.myservice.com/v1/contacts', - [ - 'headers' => ['Authorization' => 'Bearer ' . $api_key], - 'body' => json_encode($form_data), - ] - ); - - if (is_wp_error($response)) { - return ['success' => false, 'error' => $response->get_error_message()]; - } - - return ['success' => true, 'response' => wp_remote_retrieve_body($response)]; - } - } - ``` - ---- - -## Security Patterns - -### Input Sanitization - -```php -// Text -$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); - -// Email -$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); - -// Number -$id = absint( $_POST['id'] ?? 0 ); - -// Array (recursive) -$data = Helper::sanitize_array_recursively( $_POST['data'] ?? [] ); - -// Textarea -$message = Helper::sanitize_textarea( wp_unslash( $_POST['message'] ?? '' ) ); -``` - -### Output Escaping - -```php -// HTML content -echo esc_html( $user_name ); - -// Attributes -echo '
'; - -// URLs -echo ''; - -// Rich text (allows safe HTML) -echo wp_kses_post( $content ); -``` - -### Database Queries - -```php -global $wpdb; - -// ✅ Good - Prepared statement -$results = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}sureforms_entries WHERE form_id = %d AND status = %s", - $form_id, - $status - ) -); - -// ❌ Bad - SQL injection risk -$results = $wpdb->query("DELETE FROM {$wpdb->prefix}sureforms_entries WHERE id = $_POST[id]"); -``` - ---- - -## Testing Your Changes - -### Before Committing - -```bash -# 1. Lint PHP -composer lint - -# 2. Run PHP tests -composer test - -# 3. Lint JavaScript -npm run lint-js - -# 4. Run JS tests -npm run test:unit - -# 5. Build assets -npm run build - -# 6. Test in browser -# - Create form -# - Submit form -# - Check entries -# - Verify no console errors -``` - -### E2E Testing - -```bash -# Start environment -npm run play:up - -# Run tests -npm run play:run - -# Interactive mode (with browser) -npm run play:run:interactive -``` - ---- - -## Database Queries - Common Patterns - -### Get Entries - -```php -use SRFM\Inc\Database\Tables\Entries; - -// Get all for form -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', -]); - -// With pagination -$entries = Entries::get_all([ - 'where' => [['key' => 'form_id', 'value' => 123, 'compare' => '=']], - 'limit' => 20, - 'offset' => 40 // Page 3 (20 per page) -]); - -// Count entries -$count = Entries::get_count([ - 'where' => [['key' => 'status', 'value' => 'published', 'compare' => '=']] -]); -``` - -### Get Payments - -```php -use SRFM\Inc\Database\Tables\Payments; - -// Get completed payments -$payments = Payments::get_all([ - 'where' => [ - ['key' => 'status', 'value' => 'completed', 'compare' => '='] - ] -]); - -// Get payment by transaction ID -$payment = Payments::get_by_transaction_id('ch_xxxxx', 'stripe'); -``` - ---- - -## Hooks - When to Use - -### Modify Data Before Save - -```php -// Filter entry data before save -add_filter('sureforms_submit_form_data', function($data, $form_id) { - // Add custom field - $data['custom_timestamp'] = current_time('mysql'); - return $data; -}, 10, 2); -``` - -### Trigger External Action - -```php -// Action after entry save -add_action('sureforms_after_entry_save', function($entry_id, $form_id, $data) { - // Send to external CRM - send_to_crm($data); -}, 10, 3); -``` - -### Modify Email - -```php -// Filter email template -add_filter('sureforms_email_template', function($html, $entry_id) { - // Add custom header - return '
...
' . $html; -}, 10, 2); -``` - ---- - -## Debugging Tips - -### Enable Debug Mode - -```php -// wp-config.php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -``` - -### Check Debug Log - -```bash -tail -f wp-content/debug.log -``` - -### JavaScript Console - -```javascript -// Check for errors -// Open browser DevTools → Console - -// SureForms global -console.log(window.sureforms); -``` - -### Database Queries - -```php -// Install Query Monitor plugin -// Shows all queries, slow queries, errors -``` - ---- - -## Common Pitfalls to Avoid - -1. **Don't bypass sanitization:** "It's internal, it's safe" → Still sanitize -2. **Don't skip nonce checks:** Every AJAX/REST endpoint needs verification -3. **Don't use `$_POST` directly:** Always sanitize first -4. **Don't concatenate SQL:** Use `$wpdb->prepare()` -5. **Don't echo user data raw:** Use `esc_html()`, `esc_attr()`, etc. -6. **Don't modify database schema without migration:** Breaks existing sites -7. **Don't remove existing hooks:** Other plugins may depend on them -8. **Don't hardcode `wp_` prefix:** Use `$wpdb->prefix` - ---- - -## Getting Help - -**Documentation:** -- This folder: All internal docs -- WordPress Codex: https://codex.wordpress.org/ -- React Docs: https://react.dev/ - -**Code Search:** -- Use Glob/Grep tools to find patterns -- Example: `grep -r "sanitize_text_field" inc/` → See how sanitization is used - -**Ask Before:** -- Breaking changes -- Database schema changes -- Removing existing functionality - ---- - -**Next:** [Troubleshooting](troubleshooting.md) - Common problems and solutions diff --git a/internal-docs/apis.md b/internal-docs/apis.md deleted file mode 100644 index 0a7a16ec7..000000000 --- a/internal-docs/apis.md +++ /dev/null @@ -1,418 +0,0 @@ -# APIs - -Complete API reference for SureForms Free & Pro. - ---- - -## REST API Endpoints - -### Free (`/sureforms/v1/`) - -**Form Management:** -```http -POST /sureforms/v1/generate-form -GET /sureforms/v1/forms -GET /sureforms/v1/forms/{id} -DELETE /sureforms/v1/forms/{id} -``` - -**Form Submission:** -```http -POST /sureforms/v1/submit-form -``` -**Body:** `{ form_id, nonce, field_data... }` -**Response:** `{ success, message, entry_id }` - -**Entries:** -```http -GET /sureforms/v1/entries/list -GET /sureforms/v1/entries/{id} -DELETE /sureforms/v1/entries/{id} -POST /sureforms/v1/entries/export -``` - -**Payments:** -```http -GET /sureforms/v1/payments/list -GET /sureforms/v1/payments/{id} -``` - -**Stripe Webhooks:** -```http -POST /sureforms/webhook_test # Stripe webhooks -POST /sureforms/webhook_live -``` - -**Settings:** -```http -GET /sureforms/v1/settings -POST /sureforms/v1/settings/update -``` - -### Pro (`/sureforms-pro/v1/`) - -**OAuth:** -```http -POST /sureforms-pro/v1/oauth/authorize -POST /sureforms-pro/v1/oauth/callback -``` - -**PayPal Webhooks:** -```http -POST /sureforms-pro/paypal-test-webhook -POST /sureforms-pro/paypal-live-webhook -``` - -**User Registration:** -```http -POST /sureforms-pro/v1/login -POST /sureforms-pro/v1/lost-password -POST /sureforms-pro/v1/reset-password -``` - ---- - -## AJAX Handlers - -### Free (30 handlers) - -**Admin:** -```php -'srfm_admin_action' # Generic admin handler -'srfm_get_forms' # Fetch forms list -'srfm_delete_form' # Delete form -'srfm_duplicate_form' # Duplicate form -'srfm_export_entries' # Export CSV -``` - -**Payments (Public):** -```php -'wp_ajax_srfm_create_payment_intent' # Stripe payment -'wp_ajax_nopriv_srfm_create_payment_intent' -'wp_ajax_srfm_create_subscription_intent' # Stripe subscription -'wp_ajax_nopriv_srfm_create_subscription_intent' -``` - -**Admin Stripe:** -```php -'srfm_stripe_cancel_subscription' # Cancel subscription -'srfm_stripe_pause_subscription' # Pause subscription -``` - -**Admin Payments:** -```php -'srfm_create_payment_refund' # Create refund -``` - -**Form Validation (Public):** -```php -'wp_ajax_srfm_field_unique_validation' # Check unique fields -'wp_ajax_nopriv_srfm_field_unique_validation' -``` - -### Pro (22 handlers) - -**PayPal (Public):** -```php -'wp_ajax_srfm_pro_create_paypal_order' -'wp_ajax_nopriv_srfm_pro_create_paypal_order' -'wp_ajax_srfm_pro_create_paypal_subscription' -'wp_ajax_nopriv_srfm_pro_create_paypal_subscription' -``` - -**PayPal Admin:** -```php -'srfm_pro_paypal_cancel_subscription' -'srfm_pro_paypal_pause_subscription' -``` - -**PDF:** -```php -'srfm_pro_generate_pdf' # Generate PDF from entry -'srfm_pro_download_pdf' # Download PDF -``` - -**Licensing:** -```php -'srfm_pro_activate_license' -'srfm_pro_deactivate_license' -``` - -**Entries:** -```php -'sureforms_pro_entry_delete_file' # Delete uploaded file -``` - ---- - -## WordPress Hooks - -### Actions (Form Submission) - -```php -// Before entry save -do_action('sureforms_before_entry_save', $form_id, $entry_data); - -// After entry save -do_action('sureforms_after_entry_save', $entry_id, $form_id, $entry_data); - -// After payment completed -do_action('sureforms_payment_completed', $payment_id, $entry_id, $gateway); - -// After integration success -do_action('sureforms_integration_success', $integration_type, $response); - -// Before email send -do_action('sureforms_before_email_send', $to, $subject, $message); - -// After form submission (success) -do_action('sureforms_form_submitted', $entry_id, $form_id); -``` - -### Filters (Data Modification) - -```php -// Modify form data before save -$entry_data = apply_filters('sureforms_submit_form_data', $entry_data, $form_id); - -// Modify payment amount -$amount = apply_filters('sureforms_payment_amount', $amount, $form_id, $entry_data); - -// Modify email template -$html = apply_filters('sureforms_email_template', $html, $entry_id, $form_id); - -// Modify email subject -$subject = apply_filters('sureforms_email_subject', $subject, $form_id); - -// Modify email recipients -$to = apply_filters('sureforms_email_recipients', $to, $entry_id, $form_id); - -// Modify confirmation message -$message = apply_filters('sureforms_confirmation_message', $message, $entry_id); - -// Modify field validation -$is_valid = apply_filters('sureforms_validate_field', $is_valid, $field, $value); - -// Modify sanitized value -$value = apply_filters("sureforms_sanitize_{$field_type}", $value, $field); -``` - -### Integration Hooks (Pro) - -```php -// Before integration request -do_action('sureforms_pro_before_integration', $integration, $data); - -// After integration request -do_action('sureforms_pro_after_integration', $integration, $response); - -// Integration error -do_action('sureforms_pro_integration_error', $integration, $error); -``` - ---- - -## JavaScript APIs - -### Global Objects - -```javascript -// Form settings -window.sureforms = { - ajaxUrl: '/wp-admin/admin-ajax.php', - nonce: 'xxx', - formId: 123, - settings: {...} -}; - -// Block editor -window.sureformsBlocks = { - registerBlock: (name, settings) => {...}, - getBlock: (name) => {...} -}; -``` - -### Custom Events - -```javascript -// Form submitted -document.addEventListener('sureforms-form-submitted', (e) => { - console.log('Entry ID:', e.detail.entryId); -}); - -// Payment completed -document.addEventListener('sureforms-payment-completed', (e) => { - console.log('Payment ID:', e.detail.paymentId); -}); - -// Validation error -document.addEventListener('sureforms-validation-error', (e) => { - console.log('Field:', e.detail.field, 'Error:', e.detail.message); -}); -``` - ---- - -## Database APIs - -### Entry Operations - -```php -use SRFM\Inc\Database\Tables\Entries; - -// Get all entries for a form -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', - 'limit' => 20, - 'offset' => 0 -]); - -// Get single entry -$entry = Entries::get_by_id(456); - -// Insert entry -$entry_id = Entries::insert([ - 'form_id' => 123, - 'entry_data' => json_encode($data), - 'status' => 'published' -]); - -// Update entry -Entries::update(456, ['status' => 'trash']); - -// Delete entry -Entries::delete(456); -``` - -### Payment Operations - -```php -use SRFM\Inc\Database\Tables\Payments; - -// Get payments -$payments = Payments::get_all_main_payments([ - 'where' => [ - ['key' => 'status', 'value' => 'completed', 'compare' => '='] - ] -]); - -// Create payment -$payment_id = Payments::insert([ - 'entry_id' => 456, - 'transaction_id' => 'ch_xxx', - 'gateway' => 'stripe', - 'status' => 'pending', - 'total_amount' => 99.99, - 'currency' => 'USD' -]); - -// Update payment status -Payments::update($payment_id, ['status' => 'completed']); -``` - ---- - -## Helper APIs - -### Sanitization - -```php -use SRFM\Inc\Helper; - -// Sanitize by field type -$clean = Helper::sanitize_by_field_type('email', $value); -$clean = Helper::sanitize_by_field_type('textarea', $value); -$clean = Helper::sanitize_by_field_type('number', $value); - -// Email-specific -$email = Helper::sanitize_email_header($raw_email); - -// Recursive sanitization -$clean_array = Helper::sanitize_array_recursively($array); -``` - -### Validation - -```php -use SRFM\Inc\Field_Validation; - -// Validate field -$result = Field_Validation::validate_field($field_config, $value); -// Returns: ['valid' => true/false, 'message' => 'Error message'] - -// Validate entire form -$result = Field_Validation::validate_form_data($form_id, $data); -``` - -### Payment Helpers - -```php -use SRFM\Inc\Payments\Payment_Helper; - -// Validate amount against form config -$result = Payment_Helper::validate_payment_amount($amount, $currency, $form_id, $block_id); - -// Get currency -$currency = Payment_Helper::get_currency(); -``` - ---- - -## Encryption API (Pro) - -```php -use SRFM_PRO\Inc\Pro\Native_Integrations\Encryption; - -$enc = new Encryption(); - -// Encrypt sensitive data -$encrypted = $enc->encrypt('oauth_token_12345'); - -// Decrypt -$decrypted = $enc->decrypt($encrypted); - -// Storage: wp_sureforms_integrations.data column -``` - -**Algorithm:** AES-256-CTR -**Key:** `SRFM_ENCRYPTION_KEY` constant or `LOGGED_IN_KEY` - ---- - -## Integration Workflow API (Pro) - -```php -use SRFM_PRO\Inc\Pro\Native_Integrations\Services\Workflow_Processor; - -// Execute integration workflow -$processor = new Workflow_Processor(); -$result = $processor->execute_workflow($integration_config, $entry_data); - -// Returns: ['success' => true/false, 'response' => API response] -``` - ---- - -## Rate Limiting - -**Note:** No built-in rate limiting. Implementations should add: - -```php -// Example pattern (not in core) -$key = 'form_submit_' . $user_ip; -$attempts = get_transient($key) ?: 0; - -if ($attempts >= 5) { - wp_send_json_error('Too many submissions', 429); -} - -set_transient($key, $attempts + 1, 15 * MINUTE_IN_SECONDS); -``` - ---- - -**Next:** [Coding Standards](coding-standards.md) - PHP/JS conventions diff --git a/internal-docs/architecture.md b/internal-docs/architecture.md deleted file mode 100644 index 9e59e0b81..000000000 --- a/internal-docs/architecture.md +++ /dev/null @@ -1,329 +0,0 @@ -# Architecture - -**Version:** 2.5.0 - ---- - -## System Overview - -SureForms is a WordPress form builder with a **Free + Pro** plugin architecture: - -``` -WordPress Site -├── SureForms Free (Required) -│ ├── Core form functionality -│ ├── Stripe payments -│ ├── AI form builder -│ └── Entry management -└── SureForms Pro (Optional Extension) - ├── Advanced features (conditional logic, multi-step) - ├── PayPal payments - ├── User registration/login - └── 24+ native integrations -``` - ---- - -## High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ WordPress Admin │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ React Admin UI (src/) │ │ -│ │ - Form Builder (Gutenberg Blocks) │ │ -│ │ - Entry Dashboard │ │ -│ │ - Settings Panels │ │ -│ └──────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ REST API Layer │ -│ - /sureforms/v1/* (Free) │ -│ - /sureforms-pro/v1/* (Pro) │ -│ - AJAX handlers (wp_ajax_*) │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ Business Logic (inc/) │ -│ ┌──────────┬──────────┬──────────┬──────────────────┐ │ -│ │ Forms │ Payments │ AI │ Integrations │ │ -│ │ Submit │ Stripe │ Builder │ (Pro: 24+) │ │ -│ │ Validate │ PayPal │ Field │ Mailchimp, etc │ │ -│ │ Store │ Webhooks │ Mapping │ │ │ -│ └──────────┴──────────┴──────────┴──────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ Data Layer │ -│ ┌──────────────┬────────────────┬──────────────────┐ │ -│ │ WP Options │ Custom Tables │ Encrypted Data │ │ -│ │ Form configs │ Entries │ OAuth tokens │ │ -│ │ Settings │ Payments │ API keys │ │ -│ └──────────────┴────────────────┴──────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ↕ -┌─────────────────────────────────────────────────────────┐ -│ Frontend Rendering │ -│ - Gutenberg Blocks (PHP + JS) │ -│ - Form submission (AJAX) │ -│ - reCAPTCHA/Turnstile │ -│ - Real-time validation │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## Component Breakdown - -### 1. Block System (Gutenberg) - -**Free Blocks (16):** -- Input types: Text, Email, Number, Phone, URL, Textarea, Dropdown -- Special: Address, Checkbox, Multiple Choice, GDPR, Payment -- Layout: Separator, Heading, Image, Icon, Custom Button - -**Pro Blocks (8):** -- Input: Date Picker, Time Picker, Rating, Slider, Hidden, Password -- Advanced: File Upload, HTML, Page Break (multi-step) -- User Registration: Login, Register, Lost Password, Reset Password - -**Location:** `inc/blocks/*/block.php` and `src/blocks/*/` - -### 2. Form Submission Pipeline - -``` -User Submits Form - ↓ -1. Nonce Verification (inc/form-submit.php:91-95) - ↓ -2. Field Validation (inc/field-validation.php) - - Type validation (email, URL, number) - - Required fields - - Min/max constraints - - Custom regex - ↓ -3. Sanitization (inc/helper.php:258-274) - - Per-field type sanitization - - XSS prevention (wp_kses_post) - - SQL injection prevention - ↓ -4. CAPTCHA Validation (inc/form-submit.php:317-386) - - reCAPTCHA v2/v3 - - Cloudflare Turnstile - - hCaptcha - ↓ -5. Business Logic (Pro: Conditional Logic) - ↓ -6. Payment Processing (if enabled) - - Stripe: Create PaymentIntent - - PayPal: Create Order - ↓ -7. Entry Storage (inc/database/tables/entries.php) - ↓ -8. Integrations (Pro: Send to external services) - ↓ -9. Email Notifications (inc/email/email-template.php) - ↓ -10. Confirmation/Redirect -``` - -### 3. Payment Processing - -**Stripe (Free):** -- Location: `inc/payments/stripe/` -- Flow: - 1. Frontend creates PaymentIntent (AJAX) - 2. User completes payment on Stripe - 3. Webhook updates payment status - 4. Entry marked as paid - -**PayPal (Pro):** -- Location: `inc/business/payments/pay-pal/` -- Flow: - 1. Frontend creates Order via API - 2. User redirected to PayPal - 3. Webhook confirms payment - 4. Entry status updated - -**Database:** `wp_sureforms_payments` table - -### 4. AI Form Builder - -**Location:** `inc/ai-form-builder/` - -**Flow:** -``` -User Prompt ("Create a contact form") - ↓ -AI Middleware (SRFM_AI_MIDDLEWARE) - ↓ -Field Mapping (field-mapping.php) - ↓ -Gutenberg Block Generation - ↓ -Form Preview -``` - -**API:** `https://credits.startertemplates.com/sureforms/` - -### 5. Native Integrations (Pro) - -**Location:** `inc/pro/native-integrations/` - -**Supported Services (24+):** -- Email: Mailchimp, Brevo, Constant Contact -- CRM: HubSpot, Salesforce, Zoho -- Messaging: Telegram, Slack, Discord -- Marketing: FluentCRM, MailerPress -- Booking: LatePoint -- Automation: Zapier, OttoKit - -**Architecture:** -``` -Generic Provider Base Class - ↓ -Integration-Specific Classes -(inc/pro/native-integrations/integrations/*/actions/) - ↓ -OAuth Handler (oauth-handler.php) - ↓ -Encrypted Storage (encryption.php - AES-256-CTR) - ↓ -Workflow Processor (services/workflow-processor.php) -``` - -### 6. Database Schema - -**Custom Tables:** - -```sql --- Entries -wp_sureforms_entries -- id (INT, PK) -- form_id (INT) -- entry_data (LONGTEXT, JSON) -- created_at (DATETIME) -- updated_at (DATETIME) -- status (VARCHAR - 'published', 'trash') -- user_agent (TEXT) -- ip_address (VARCHAR) - --- Payments -wp_sureforms_payments -- id (INT, PK) -- entry_id (INT, FK) -- transaction_id (VARCHAR) -- gateway (ENUM 'stripe', 'paypal') -- status (ENUM 'pending', 'completed', 'failed', 'refunded') -- total_amount (DECIMAL) -- refunded_amount (DECIMAL) -- currency (VARCHAR) - --- Integrations (Pro) -wp_sureforms_integrations -- id (INT, PK) -- type (VARCHAR - 'mailchimp', 'hubspot', etc.) -- data (LONGTEXT, encrypted JSON) -- status (ENUM 'active', 'inactive') - --- Save & Resume (Pro) -wp_sureforms_save_resume -- id (INT, PK) -- form_id (INT) -- token (VARCHAR, unique) -- form_data (LONGTEXT, JSON) -- expires_at (DATETIME) -``` - ---- - -## Security Architecture - -**Input Validation:** -- Nonce verification on all AJAX/REST requests -- Sanitization via `sanitize_*` functions -- Type casting (absint, floatval) - -**Output Escaping:** -- `esc_html()`, `esc_attr()`, `esc_url()` throughout -- `wp_kses_post()` for rich text - -**SQL Security:** -- `$wpdb->prepare()` for all queries -- No direct SQL concatenation - -**Payment Security:** -- Webhook signature verification (Stripe, PayPal) -- Server-side amount validation -- PCI-DSS compliant (payments handled by Stripe/PayPal) - -**Encryption (Pro):** -- AES-256-CTR for OAuth tokens -- Key derivation: `SRFM_ENCRYPTION_KEY` constant or `LOGGED_IN_KEY` -- Storage: `wp_sureforms_integrations.data` (encrypted JSON) - ---- - -## Performance Optimizations - -1. **Lazy Loading:** React components loaded on-demand -2. **Asset Minification:** Grunt uglifies JS/CSS -3. **Database Indexing:** Indexes on `form_id`, `entry_id`, `status` -4. **Caching:** Transients for integration API calls -5. **Query Optimization:** `SELECT` only needed columns - ---- - -## Extensibility - -**Hooks (Filters):** -- `sureforms_submit_form_data` - Modify data before save -- `sureforms_email_template` - Customize email HTML -- `sureforms_payment_amount` - Modify payment amount - -**Hooks (Actions):** -- `sureforms_after_entry_save` - Trigger after entry created -- `sureforms_payment_completed` - After successful payment -- `sureforms_integration_success` - After integration sends data - -**Developer APIs:** -- REST: `/sureforms/v1/*` -- PHP: `SRFM\Inc\Helper` class -- JavaScript: `window.sureforms.*` globals - ---- - -## Technology Stack - -| Layer | Technologies | -|-------|-------------| -| Frontend (Admin) | React 18, @wordpress/components, TanStack Query | -| Frontend (Public) | Vanilla JS, DOMPurify, intl-tel-input | -| Backend | PHP 7.4+, WordPress 6.4+ | -| Build | webpack 5, Babel, Sass, Grunt | -| Testing | PHPUnit, Jest, Playwright | -| Database | MySQL 5.7+, MariaDB 10.3+ | - ---- - -## Deployment Architecture - -``` -Developer Machine - ↓ -Git Commit → GitHub Actions CI - ↓ -Automated Tests (PHPUnit, Playwright) - ↓ -Build Assets (npm run build) - ↓ -Free: WordPress.org SVN -Pro: Licensing Server (ZIP) - ↓ -End User WordPress Sites (300,000+) -``` - ---- - -**Next:** [Codebase Map](codebase-map.md) for detailed folder structure diff --git a/internal-docs/codebase-map.md b/internal-docs/codebase-map.md deleted file mode 100644 index 9d85000c2..000000000 --- a/internal-docs/codebase-map.md +++ /dev/null @@ -1,303 +0,0 @@ -# Codebase Map - -**Version:** 2.5.0 - -Complete folder-by-folder guide to both plugins. - ---- - -## SureForms Free (`sureforms/`) - -### Root Files - -| File | Purpose | -|------|---------| -| `sureforms.php` | Main plugin file, defines constants, loads `plugin-loader.php` | -| `plugin-loader.php` | Bootstraps plugin, registers hooks, loads dependencies | -| `composer.json` | PHP dependencies (nps-survey, bsf-analytics, astra-notices) | -| `package.json` | JS dependencies, build scripts | -| `Gruntfile.js` | Grunt tasks (minify, compress, release) | - -### `admin/` - WordPress Admin UI - -| File | Purpose | -|------|---------| -| `admin.php` | Admin page registration, AJAX handlers for admin actions | -| `analytics.php` | Usage analytics, event tracking | -| `notice-manager.php` | Admin notices, dismissible alerts | - -### `inc/` - Core PHP Logic - -#### `inc/blocks/` - Gutenberg Block Definitions (PHP) - -Each block has a `block.php` with: -- Block registration (`register_block_type`) -- Server-side rendering callback -- Attribute schema -- Enqueue scripts/styles - -**Blocks (16):** -- `address/`, `checkbox/`, `dropdown/`, `email/`, `gdpr/`, `inlinebutton/` -- `input/`, `multichoice/`, `number/`, `payment/`, `phone/`, `sform/`, `textarea/`, `url/` -- `base.php` - Base block class - -#### `inc/fields/` - Field Markup Generation - -Mirrors `blocks/` - generates HTML markup for each field type. - -**Pattern:** -```php -*-markup.php files: -- render_field() method -- Outputs escaped HTML -- Handles field attributes, validation states -``` - -#### `inc/payments/` - Payment Processing - -**Core Files:** -| File | Purpose | -|------|---------| -| `front-end.php` | Public AJAX handlers (`srfm_create_payment_intent`, `srfm_create_subscription_intent`) | -| `payment-helper.php` | Utility functions, amount validation, currency formatting | - -**`payments/stripe/`:** -| File | Purpose | -|------|---------| -| `stripe-webhook.php` | Webhook handler (charge, refund, subscription events) | -| `stripe-helper.php` | Stripe API wrapper, PaymentIntent creation | -| `admin-stripe-handler.php` | Admin AJAX for subscription management | -| `payments-settings.php` | Stripe connection settings, API keys | - -**`payments/admin/`:** -| File | Purpose | -|------|---------| -| `admin-handler.php` | Admin AJAX for refunds, payment management | - -#### `inc/ai-form-builder/` - AI Form Generation - -| File | Purpose | -|------|---------| -| `ai-form-builder.php` | Main AI form builder class, API integration | -| `ai-auth.php` | Middleware authentication | -| `ai-helper.php` | Helper functions, prompt processing | -| `field-mapping.php` | Maps AI response to Gutenberg blocks | - -**API:** `SRFM_AI_MIDDLEWARE` - `https://credits.startertemplates.com/sureforms/` - -#### `inc/database/` - Custom Database Tables - -| File | Purpose | -|------|---------| -| `base.php` | Abstract base class for all table classes (CRUD operations) | -| `register.php` | Table registration, schema creation | -| `tables/entries.php` | `wp_sureforms_entries` table (1,300+ lines) | -| `tables/payments.php` | `wp_sureforms_payments` table (1,200+ lines) | - -**Key Methods:** -- `get_all()`, `get_by_id()`, `insert()`, `update()`, `delete()` -- `get_all_main_payments()` - Payment queries with filtering - -#### Other `inc/` Files - -| File | Purpose | Lines | -|------|---------|-------| -| `form-submit.php` | Form submission handler (AJAX + REST) | 1,280 | -| `helper.php` | Utility functions (sanitization, escaping, validation) | 1,500+ | -| `field-validation.php` | Server-side field validation | 800+ | -| `rest-api.php` | REST API registration (`/sureforms/v1/*`) | 800 | -| `entries.php` | Entry management (list, view, export) | 1,000+ | -| `email/email-template.php` | Email notification templates | 400 | -| `frontend-assets.php` | Enqueue public scripts/styles | 300 | - -### `src/` - React Admin UI - -**Structure:** -``` -src/ -├── admin/ # Main admin app -│ ├── components/ # Reusable React components -│ ├── pages/ # Page-level components -│ └── index.js # Entry point -├── blocks/ # Block editor components -│ ├── address/ # Block-specific React -│ ├── email/ -│ └── ... -└── common/ # Shared utilities - ├── api.js # API client (wp.apiFetch) - └── utils.js # Helper functions -``` - -**Build:** webpack via `@wordpress/scripts` - ---- - -## SureForms Pro (`sureforms-pro/`) - -### Root Files - -| File | Purpose | -|------|---------| -| `sureforms-pro.php` | Main plugin file, requires Free plugin | -| `plugin-loader.php` | Bootstraps Pro features | - -### `inc/business/` - Pro Business Features - -#### `inc/business/payments/pay-pal/` - PayPal Integration - -| File | Purpose | Lines | -|------|---------|-------| -| `webhook-listener.php` | PayPal webhook handler (orders, refunds, subscriptions) | 1,173 | -| `frontend.php` | Public AJAX for PayPal orders, subscriptions | 1,100 | -| `api-payments.php` | PayPal API wrapper (create order, capture, refund) | 178 | -| `helper.php` | PayPal utilities, amount formatting | 520 | -| `settings.php` | PayPal connection settings | AJAX | - -#### `inc/business/user-registration/` - Login & Registration - -| File | Purpose | Lines | -|------|---------|-------| -| `init.php` | REST API registration, login/password endpoints | 600+ | -| `processor.php` | Registration processing, `wp_insert_user()` | 1,000+ | -| `login/block.php` | Login block definition | | -| `register/block.php` | Registration block | | -| `lost-password/block.php` | Lost password block | | -| `reset-password/block.php` | Reset password block | | - -**⚠️ Security Note:** Registration processor creates WordPress users from form submissions. - -#### `inc/business/pdf/` - PDF Generation - -| File | Purpose | -|------|---------| -| `pdf.php` | PDF generation API, AJAX handlers | -| `document.php` | PDF document builder (uses TCPDF or similar) | -| `utils.php` | PDF utilities | - -#### `inc/business/custom-app/` - Custom Application Builder - -| File | Purpose | -|------|---------| -| `load.php` | Custom post type forms, standalone applications | -| `utils.php` | App utilities | - -#### `inc/business/custom-post-type/` - Custom Post Type Integration - -Allows forms to create/update custom post types. - -### `inc/pro/native-integrations/` - 24+ Service Integrations - -**Core:** -| File | Purpose | Lines | -|------|---------|-------| -| `native-integrations.php` | Main integration manager | | -| `encryption.php` | AES-256-CTR encryption for OAuth tokens | 200 | -| `oauth-handler.php` | OAuth authorization flow | 400+ | -| `generic-provider.php` | Base provider class | | -| `integration-provider.php` | Provider interface | | - -**`inc/pro/native-integrations/integrations/`:** - -Each integration has its own folder: -``` -mailchimp/ -├── config.json # API endpoints, auth method -└── actions/ - ├── add-contact.php # Action: Add subscriber - └── remove-contact.php - -hubspot/ -├── config.json -└── actions/ - └── create-contact.php - -...24+ integrations -``` - -**Supported:** -- Email: Mailchimp, Brevo, Constant Contact -- CRM: HubSpot, Salesforce, Zoho -- Messaging: Telegram, Slack, Discord -- WP: FluentCRM, MailerPress, MailPoet -- Booking: LatePoint -- Automation: Zapier, OttoKit - -**Encryption:** OAuth tokens encrypted via `encryption.php` before storage in `wp_sureforms_integrations`. - -### `inc/pro/database/tables/` - Pro Database Tables - -| File | Purpose | -|------|---------| -| `integrations.php` | `wp_sureforms_integrations` table | -| `save-resume.php` | `wp_sureforms_save_resume` table (draft forms) | - -### `inc/extensions/` - Pro Extensions - -| File | Purpose | -|------|---------| -| `conditional-logic.php` | Show/hide fields based on conditions | -| `conditional-emails.php` | Conditional email notifications | -| `conditional-confirmations.php` | Conditional redirects | -| `field-validation.php` | Extended validation (file uploads, MIME types) | -| `entries-management.php` | Enhanced entry features (edit, delete files) | -| `page-break.php` | Multi-step form logic | - -### `inc/blocks/` - Pro Blocks (8) - -- `date-picker/`, `time-picker/`, `rating/`, `slider/` -- `upload/`, `hidden/`, `html/`, `page-break/` - ---- - -## Key Patterns & Conventions - -### Naming Conventions - -**Functions:** -- Public: `srfm_function_name()` -- Internal: `_srfm_internal_function()` - -**Classes:** -- Namespaced: `SRFM\Inc\ClassName` -- Pro: `SRFM_PRO\Inc\ClassName` - -**Hooks:** -- Actions: `sureforms_action_name` -- Filters: `sureforms_filter_name` - -**Database:** -- Tables: `wp_sureforms_table_name` -- Options: `sureforms_option_name` - -### File Organization - -``` -feature/ -├── block.php # Block registration -├── feature-markup.php # HTML generation -├── api-handler.php # AJAX/REST handlers -└── helper.php # Utilities -``` - -### REST API Convention - -**Free:** `/sureforms/v1/endpoint` -**Pro:** `/sureforms-pro/v1/endpoint` - ---- - -## Where to Find Things - -**Need to:** -- **Add a new field type?** → `inc/blocks/` + `inc/fields/` + `src/blocks/` -- **Modify form submission?** → `inc/form-submit.php` -- **Change email template?** → `inc/email/email-template.php` -- **Add payment gateway?** → `inc/payments/` (copy Stripe/PayPal pattern) -- **Add integration?** → `inc/pro/native-integrations/integrations/new-service/` -- **Debug entries?** → `inc/database/tables/entries.php` -- **Fix validation?** → `inc/field-validation.php` -- **Customize admin UI?** → `src/admin/` - ---- - -**Next:** [APIs](apis.md) - REST endpoints, AJAX handlers, hooks diff --git a/internal-docs/coding-standards.md b/internal-docs/coding-standards.md deleted file mode 100644 index 0b746674c..000000000 --- a/internal-docs/coding-standards.md +++ /dev/null @@ -1,312 +0,0 @@ -# Coding Standards - -**Version:** 2.5.0 - ---- - -## PHP Standards - -**WordPress Coding Standards:** https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/ - -### Code Style - -- **PSR-12 inspired** but follows WordPress conventions -- **Tabs for indentation** (not spaces) -- **Yoda conditions:** `if ( 'value' === $variable )` -- **Braces:** Required for all control structures - -```php -// ✅ Good -if ( $condition ) { - do_something(); -} - -// ❌ Bad -if ($condition) do_something(); -``` - -### Naming Conventions - -**Functions:** -```php -srfm_public_function() // Public functions -_srfm_private_function() // Internal (leading underscore) -``` - -**Classes:** -```php -namespace SRFM\Inc; -class Form_Submit { } // Underscores in class names - -namespace SRFM_PRO\Inc\Business; -class PayPal_Helper { } -``` - -**Variables:** -```php -$snake_case_variable = 'value'; // Lowercase with underscores -``` - -**Constants:** -```php -define( 'SRFM_CONSTANT', 'value' ); -``` - -### Security - -**Always:** -- Sanitize input: `sanitize_text_field()`, `absint()`, `sanitize_email()` -- Validate: Check types, ranges, allowed values -- Escape output: `esc_html()`, `esc_attr()`, `esc_url()` -- Use nonces: `wp_verify_nonce()` for AJAX/forms -- Use `$wpdb->prepare()` for SQL queries -- Check capabilities: `current_user_can()` - -**Example:** -```php -// Input -$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); - -// Database -$results = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}sureforms_entries WHERE form_id = %d", - $form_id - ) -); - -// Output -echo '
' . esc_html( $user_name ) . '
'; -``` - -### Documentation - -**PHPDoc blocks:** -```php -/** - * Short description. - * - * Longer description if needed. - * - * @since 2.5.0 - * - * @param int $form_id Form ID. - * @param array $data Entry data. - * @return int|false Entry ID on success, false on failure. - */ -function srfm_create_entry( $form_id, $data ) { - // Implementation -} -``` - ---- - -## JavaScript Standards - -**WordPress JavaScript Coding Standards:** https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/ - -### Code Style (ESLint) - -**Config:** `@wordpress/eslint-plugin` - -```javascript -// ✅ Good - ES6+, const/let, arrow functions -const handleSubmit = (event) => { - event.preventDefault(); - const formData = new FormData(event.target); - // ... -}; - -// ❌ Bad - var, function expressions -var handleSubmit = function(event) { - event.preventDefault(); -}; -``` - -### React Conventions - -**Functional components with hooks:** -```jsx -import { useState, useEffect } from '@wordpress/element'; - -function FormBuilder({ formId }) { - const [fields, setFields] = useState([]); - - useEffect(() => { - fetchFields(formId).then(setFields); - }, [formId]); - - return ( -
- {fields.map(field => )} -
- ); -} -``` - -**Naming:** -- Components: `PascalCase` -- Hooks: `useCamelCase` -- Functions: `camelCase` -- Constants: `UPPER_SNAKE_CASE` - ---- - -## CSS/SCSS Standards - -**BEM-inspired naming:** -```scss -.srfm-form { - &__field { - // Field styles - } - - &__field--error { - // Error state - } -} -``` - -**Prefix:** All classes start with `srfm-` to avoid conflicts. - ---- - -## Database Standards - -### Table Names - -```php -$wpdb->prefix . 'sureforms_entries' -$wpdb->prefix . 'sureforms_payments' -``` - -**Always use `$wpdb->prefix`**, never hardcode `wp_`. - -### Queries - -**Always use prepared statements:** -```php -// ✅ Good -$wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}sureforms_entries WHERE id = %d", - $entry_id - ) -); - -// ❌ Bad -$wpdb->query( "DELETE FROM wp_sureforms_entries WHERE id = $entry_id" ); -``` - ---- - -## Git Commit Messages - -**Format:** -``` -type(scope): brief description - -Longer explanation if needed. - -Co-Authored-By: Name -``` - -**Types:** -- `feat`: New feature -- `fix`: Bug fix -- `docs`: Documentation -- `style`: Formatting, no code change -- `refactor`: Code restructuring -- `test`: Adding tests -- `chore`: Build/tooling - -**Example:** -``` -feat(payments): add PayPal subscription support - -- Implement PayPal billing agreement API -- Add webhook handler for subscription events -- Update admin UI for PayPal subscriptions - -Co-Authored-By: Claude Sonnet 4.5 -``` - ---- - -## File Organization - -**One class per file:** -``` -inc/payments/stripe/stripe-helper.php → class Stripe_Helper -``` - -**Group related functionality:** -``` -inc/payments/ -├── payment-helper.php # Shared utilities -├── front-end.php # Public AJAX handlers -├── stripe/ -│ ├── stripe-helper.php -│ ├── stripe-webhook.php -│ └── admin-stripe-handler.php -└── admin/ - └── admin-handler.php -``` - ---- - -## Testing Standards - -**PHPUnit:** -```php -class Test_Form_Submit extends WP_UnitTestCase { - public function test_form_submission_saves_entry() { - $entry_id = srfm_submit_form( $data ); - $this->assertIsInt( $entry_id ); - } -} -``` - -**Playwright:** -```javascript -test('form submission works', async ({ page }) => { - await page.goto('/test-form/'); - await page.fill('[name="email"]', 'test@example.com'); - await page.click('button[type="submit"]'); - await expect(page.locator('.success-message')).toBeVisible(); -}); -``` - ---- - -## Code Review Checklist - -Before submitting PR: - -- [ ] Code follows WordPress/project standards -- [ ] All inputs sanitized -- [ ] All outputs escaped -- [ ] SQL queries use `prepare()` -- [ ] Nonces verified for AJAX/forms -- [ ] Capabilities checked for admin actions -- [ ] PHPDoc blocks added -- [ ] No PHP/JS errors in console -- [ ] Tested in latest WordPress version -- [ ] Tests pass: `composer test && npm run test:unit` -- [ ] Linting passes: `composer lint && npm run lint-js` - ---- - -**Automated Checks:** - -```bash -# PHP linting -composer lint # PHP_CodeSniffer - -# JS linting -npm run lint-js # ESLint - -# Auto-fix -composer format # phpcbf -npm run lint-js:fix # ESLint --fix -``` diff --git a/internal-docs/faq.md b/internal-docs/faq.md deleted file mode 100644 index 8e174c45f..000000000 --- a/internal-docs/faq.md +++ /dev/null @@ -1,1003 +0,0 @@ -# Frequently Asked Questions - -**Version:** 2.5.0 - ---- - -## General Questions - -### What is SureForms? - -SureForms is an AI-powered WordPress form builder that uses Gutenberg blocks. - -**Key features:** -- AI form generation (describe in plain language, get complete form) -- Built-in payments (Stripe in Free, PayPal in Pro) -- Mobile-first design -- Native WordPress (no proprietary builder) -- Custom database tables (not post meta) - -**Target users:** Website owners, designers, developers who need forms without complexity. - ---- - -### What's the difference between Free and Pro? - -**Free (sureforms):** -- Unlimited forms & submissions -- All 15+ field types -- Stripe payments (one-time & subscriptions) -- Email notifications -- Spam protection (reCAPTCHA, Honeypot) -- Form analytics -- CSV export - -**Pro (sureforms-pro):** -- **All Free features, plus:** -- PayPal payments -- 24+ native integrations (Mailchimp, HubSpot, Salesforce, etc.) -- Conditional logic -- Multi-step & conversational forms -- User registration & login -- PDF generation -- File upload fields -- Priority support (< 24hr response) - -**Pricing:** -- Free: $0 (WordPress.org) -- Pro: $99-$299/year (3 sites to unlimited) - ---- - -### Can I use SureForms on multiple sites? - -**Free:** Yes, unlimited sites. - -**Pro:** Depends on license: -- Essential ($99/year): 3 sites -- Plus ($199/year): 20 sites -- Agency ($299/year): Unlimited sites - -**License activation:** -- Enter license key in SureForms → Settings → License -- Deactivate from one site to move to another -- All sites must be owned by you (not for client sites unless Agency plan) - ---- - -### Is SureForms GDPR compliant? - -**Yes.** SureForms provides tools for GDPR compliance: - -**Features:** -- GDPR checkbox field (explicit consent) -- Data export (users can request their data) -- Data deletion (users can request deletion) -- No third-party data sharing (unless you enable integrations) -- Privacy policy link support - -**What you must do:** -1. Add GDPR checkbox to forms collecting personal data -2. Link to your privacy policy -3. Honor data export/deletion requests -4. Configure data retention policies - -**Data storage:** -- Submissions stored in your WordPress database -- No data sent to SureForms servers (AI calls go through middleware but don't store data) -- Payment data stored encrypted (PCI-compliant) - ---- - -### Does SureForms work with my theme? - -**Yes.** SureForms is designed to work with any properly-coded WordPress theme. - -**Tested with:** -- Astra -- GeneratePress -- Kadence -- Neve -- Blocksy -- Twenty Twenty-Three/Four -- Page builder themes (Elementor, Divi, Beaver Builder) - -**If you experience styling conflicts:** -1. Check [troubleshooting guide](troubleshooting.md#form-breaks-after-theme-update) -2. Use SureForms → Settings → Custom CSS to override -3. Contact support with theme name - ---- - -### Can I use SureForms with page builders? - -**Yes.** SureForms works with: -- **Gutenberg** (native, best experience) -- **Elementor** (via shortcode or WordPress widget) -- **Divi** (via shortcode or code module) -- **Beaver Builder** (via WordPress widget) -- **Bricks** (via shortcode or WordPress element) - -**How to add:** - -**Elementor:** -1. Add "Shortcode" widget -2. Paste: `[sureforms id="123"]` -3. Replace 123 with your form ID - -**Divi:** -1. Add "Code" module -2. Paste shortcode -3. Save - -**Beaver Builder:** -1. Add "WordPress Widget" module -2. Select "SureForms" widget -3. Choose form - ---- - -## Technical Questions - -### What are the system requirements? - -**Minimum:** -- WordPress 6.4+ -- PHP 7.4+ -- MySQL 5.6+ or MariaDB 10.1+ -- HTTPS (for payments) - -**Recommended:** -- WordPress 6.6+ -- PHP 8.1+ -- MySQL 8.0+ or MariaDB 10.6+ -- 256MB PHP memory limit -- Object caching (Redis or Memcached) - -**Browser support:** -- Chrome 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ - ---- - -### Does SureForms slow down my site? - -**No.** SureForms is optimized for performance: - -**Impact on page load:** -- Additional CSS: ~15KB (minified) -- Additional JS: ~25KB (minified, deferred) -- Total: ~40KB additional assets - -**Database queries:** -- Form render: 2-3 queries (cached) -- Form submission: 4-5 queries (optimized with indexes) -- No N+1 query issues - -**Performance features:** -- Lazy loading for non-critical assets -- Custom database tables (not post meta) -- Query optimization (indexed columns) -- Minified & concatenated assets -- CDN-friendly (versioned static files) - -**Benchmarks:** -- Lighthouse score: 95+ (with proper hosting) -- Time to Interactive: < 2 seconds on 3G -- Form submission latency: < 500ms - ---- - -### Can I export my data? - -**Yes.** Multiple export options: - -**Entries export:** -1. Go to SureForms → Entries -2. Select entries (or "Select All") -3. Click "Export to CSV" -4. Download file - -**Format:** CSV with all fields as columns - -**Form export:** -- Export form as JSON (Settings → Export) -- Import on another site (Settings → Import) - -**Database export:** -```bash -# Export all SureForms data -wp db export sureforms-backup.sql --tables=wp_sureforms_entries,wp_sureforms_payments,wp_sureforms_integrations -``` - ---- - -### How do I migrate forms to another site? - -**Method 1: Export/Import (Recommended)** - -**On old site:** -1. Go to SureForms → Forms -2. Hover over form → "Export" -3. Download JSON file - -**On new site:** -1. Install SureForms (same version) -2. Go to SureForms → Import -3. Upload JSON file -4. Map any dependencies (integrations, etc.) - -**Method 2: Database migration** - -```bash -# On old site -wp db export sureforms-data.sql --tables=wp_sureforms_entries,wp_sureforms_payments - -# Transfer file to new site - -# On new site -wp db import sureforms-data.sql - -# Update URLs in entries (if domain changed) -wp search-replace 'https://oldsite.com' 'https://newsite.com' wp_sureforms_entries -``` - ---- - -### Can I use SureForms offline / localhost? - -**Yes, with limitations:** - -**Works offline:** -- Form creation -- Form editing -- Local submissions (saved to database) - -**Requires internet:** -- AI form generation (external API) -- Payment processing (Stripe/PayPal API) -- Native integrations (Mailchimp, HubSpot, etc.) -- Google Fonts (if used) -- reCAPTCHA (if enabled) - -**Development setup:** -```bash -# Use Local by Flywheel or similar -# Or manual setup: -cd ~/Sites/my-wordpress-site -wp core download -wp core config --dbname=sureforms_dev --dbuser=root -wp core install --url=http://localhost:8080 --title="Dev Site" -wp plugin install sureforms --activate -``` - ---- - -## Payment Questions - -### Which payment gateways are supported? - -**Free plugin:** -- Stripe (one-time payments & subscriptions) - -**Pro plugin:** -- Stripe (one-time payments & subscriptions) -- PayPal (one-time payments & subscriptions) - -**Coming soon:** -- Razorpay (India) -- Mollie (Europe) - -**Not planned:** -- WooCommerce (use WooCommerce for full e-commerce) -- Square (low demand) -- Bitcoin/crypto (regulatory complexity) - ---- - -### How do I test payments before going live? - -**Stripe test mode:** - -1. Get test API keys: - - Go to https://dashboard.stripe.com/test/apikeys - - Copy "Publishable key" (pk_test_...) - - Copy "Secret key" (sk_test_...) - -2. In SureForms: - - Go to Settings → Payments → Stripe - - Enable "Test Mode" - - Paste test keys - - Save - -3. Test with card: - - Card number: `4242 4242 4242 4242` - - Expiry: Any future date (e.g., 12/28) - - CVC: Any 3 digits (e.g., 123) - -4. Verify in Stripe dashboard: - - Go to Payments → Test Mode - - See your test payment - -**PayPal sandbox (Pro):** - -1. Create sandbox account: - - Go to https://developer.paypal.com/ - - Sign in with PayPal account - - Create sandbox business & buyer accounts - -2. Get sandbox credentials: - - Apps & Credentials → Sandbox - - Copy Client ID & Secret - -3. In SureForms: - - Settings → Payments → PayPal - - Enable "Sandbox Mode" - - Paste credentials - - Save - -4. Test with sandbox buyer account - ---- - -### How are payment fees handled? - -**SureForms fees:** $0 (we charge nothing) - -**Gateway fees:** - -**Stripe:** -- Standard: 2.9% + $0.30 per transaction -- International cards: +1.5% -- Currency conversion: +1% -- (Varies by country, see Stripe pricing) - -**PayPal:** -- Standard: 2.9% + $0.30 per transaction -- International: +1.5% -- (Varies by country, see PayPal pricing) - -**Who pays fees:** -- Fees deducted from amount received -- Example: $100 charge → You receive ~$97 - -**Passing fees to customer:** -- Not built-in (would require payment gateway approval) -- Workaround: Add fee as separate amount field - ---- - -### Can I accept subscriptions? - -**Yes** (both Stripe and PayPal). - -**Setup:** - -1. Create form with Payment block -2. Payment block settings: - - Payment Type: ● Subscription - - Interval: Monthly/Yearly/Weekly - - Amount: [price] - -3. Customer pays once, charged automatically recurring - -**Management:** - -**Stripe:** -- View subscriptions: Stripe Dashboard → Subscriptions -- Cancel: Click subscription → "Cancel subscription" -- Refund: Not automatic, manual refund in Stripe - -**PayPal (Pro):** -- View subscriptions: PayPal Dashboard → Subscriptions -- Cancel: Click subscription → "Cancel" - -**Customer cancellation:** -- Customers can cancel in Stripe/PayPal customer portal -- You can provide link in confirmation email - ---- - -## AI & Form Generation Questions - -### How does AI form generation work? - -**Technical flow:** - -``` -User enters prompt ("job application form") - ↓ -SureForms sends to AI middleware - ↓ -Middleware calls GPT-4 API - ↓ -GPT-4 returns structured JSON (field types, labels, validations) - ↓ -Middleware sends back to SureForms - ↓ -SureForms creates Gutenberg blocks from JSON - ↓ -Form appears in editor, ready to customize -``` - -**What data is sent:** -- Your prompt ("create a contact form") -- Form context (if modifying existing form) -- No user data, no submissions, no PII - -**What data is stored:** -- None. AI responses are not logged. -- Form configuration stored in WordPress database only - -**Privacy:** -- AI calls routed through SureForms middleware (not direct to OpenAI) -- No data retention -- No training on your data - ---- - -### Can I use AI without internet? - -**No.** AI requires external API call to GPT-4. - -**Offline alternatives:** -- Create forms manually (no AI) -- Use templates (no AI needed) -- Import pre-made forms - ---- - -### What languages does AI support? - -**Prompts:** Any language GPT-4 supports (100+ languages) - -**Field labels:** Generated in same language as prompt - -**Example:** -- Prompt (Spanish): "formulario de contacto" -- Result: Fields with Spanish labels ("Nombre", "Correo electrónico", etc.) - -**Limitations:** -- Some languages better than others (English, Spanish, French, German excellent) -- Less common languages may have lower quality - ---- - -## Integration Questions (Pro) - -### What integrations are available? - -**24+ native integrations:** - -**Email Marketing:** -- Mailchimp -- Brevo (Sendinblue) -- ActiveCampaign -- ConvertKit -- MailerLite - -**CRM:** -- HubSpot -- Salesforce -- Zoho CRM -- Pipedrive - -**Communication:** -- Slack -- Telegram -- Discord (coming soon) - -**Productivity:** -- Google Sheets -- Airtable (coming soon) -- Notion (coming soon) - -**WordPress Plugins:** -- FluentCRM (native) -- OttoKit (SureTriggers) - -**Webhooks:** -- Custom webhooks (POST to any URL) - -**Not included:** -- Zapier (use webhooks instead) -- Make/Integromat (use webhooks) - ---- - -### How do I connect Mailchimp? - -**Setup (OAuth):** - -1. Go to SureForms → Settings → Integrations -2. Click "Mailchimp" → "Connect" -3. Redirected to Mailchimp → "Allow access" -4. Redirected back, connection confirmed - -**Use in form:** - -1. Edit form -2. Settings → Actions → "Add Action" -3. Select "Mailchimp" -4. Choose audience (list) -5. Map fields: - - Email field → Mailchimp EMAIL - - Name field → Mailchimp FNAME -6. Save - -**On form submission:** -- Contact added to Mailchimp list automatically -- Tags applied (if configured) -- Double opt-in email sent (if enabled in Mailchimp) - ---- - -### Can I send form data to custom webhook? - -**Yes** (Pro feature). - -**Setup:** - -1. Form settings → Actions → "Add Action" -2. Select "Webhook" -3. Configure: - - URL: `https://yourapi.com/endpoint` - - Method: POST (or GET, PUT) - - Headers: (optional, e.g., `Authorization: Bearer token`) -4. Save - -**Payload format:** - -```json -{ - "form_id": 123, - "entry_id": 456, - "fields": { - "email": "user@example.com", - "name": "John Doe", - "message": "Hello!" - }, - "meta": { - "submitted_at": "2026-02-12 10:30:00", - "user_ip": "192.168.1.1", - "user_agent": "Mozilla/5.0..." - } -} -``` - -**Retry logic:** -- Failed webhooks retried 3 times -- Exponential backoff (1s, 5s, 25s) -- After 3 failures, logged as error - ---- - -## Security & Privacy Questions - -### Is SureForms secure? - -**Yes.** Security is a core priority. - -**Security measures:** - -**Input sanitization:** -- All user input sanitized before storage -- WordPress functions: `sanitize_text_field()`, `sanitize_email()`, etc. -- Custom sanitization for complex fields - -**Output escaping:** -- All output escaped before rendering -- `esc_html()`, `esc_attr()`, `esc_url()` used throughout -- Prevents XSS attacks - -**SQL injection prevention:** -- All database queries use `$wpdb->prepare()` -- No direct SQL concatenation -- Custom database class with built-in protection - -**CSRF protection:** -- Nonce verification on all AJAX/REST endpoints -- `wp_verify_nonce()` checked before state-changing operations - -**Authorization:** -- Capability checks: `current_user_can()` -- Admin actions require `manage_options` capability -- User-specific data access controlled - -**Payment security:** -- PCI DSS compliant (no card storage) -- Stripe/PayPal handle sensitive data -- API credentials encrypted at rest (AES-256) - -**File uploads (Pro):** -- MIME type validation (server-side) -- File size limits enforced -- Path traversal prevention -- Disallowed file types: `.php`, `.js`, `.exe`, etc. - ---- - -### Where is my data stored? - -**Form submissions:** -- Your WordPress database -- Table: `wp_sureforms_entries` -- No data sent to SureForms servers - -**Payment data:** -- Stripe: Stored in Stripe (not in WordPress) -- PayPal: Stored in PayPal (not in WordPress) -- WordPress stores: Transaction ID, amount, status only - -**Integration credentials (Pro):** -- WordPress database -- Table: `wp_sureforms_integrations` -- Encrypted with AES-256 -- Encryption key in `wp-config.php` (recommended) - -**AI prompts:** -- Sent to AI middleware → OpenAI GPT-4 -- Not stored or logged -- Not used for training - ---- - -### Can users delete their data? - -**Yes** (GDPR requirement). - -**Manual deletion:** - -1. User requests deletion via email -2. Admin finds submissions by email -3. Delete from SureForms → Entries - -**Automatic (Pro):** -- Data retention policy: Auto-delete entries after X days -- Settings → Privacy → Data Retention -- Configure per form or globally - -**What gets deleted:** -- Entry data (all fields) -- Entry metadata (IP, user agent) -- File uploads (if any) - -**What's preserved:** -- Form configuration (for re-use) -- Analytics (anonymized counts only) - ---- - -## Troubleshooting Questions - -### My forms aren't showing on the frontend. Why? - -**Common causes:** - -**1. Form not published:** -- Check form status in SureForms → Forms -- Must be "Published" not "Draft" - -**2. Form not embedded:** -- Add SureForms block to page -- Select your form from dropdown -- Or use shortcode: `[sureforms id="123"]` - -**3. Theme conflict:** -- Try default theme: `wp theme activate twentytwentythree` -- If works → Theme issue, contact theme developer - -**4. JavaScript disabled:** -- Check browser console for errors -- Disable other plugins to find conflict - -**Debug:** -```bash -# Enable debugging -wp config set WP_DEBUG true --raw -wp config set WP_DEBUG_LOG true --raw - -# Check debug log -tail -f wp-content/debug.log -``` - -See [troubleshooting guide](troubleshooting.md) for more. - ---- - -### Form submissions aren't saving. What's wrong? - -**Check:** - -**1. Database tables exist:** -```bash -wp db query "SHOW TABLES LIKE '%sureforms%';" -``` - -Should show `wp_sureforms_entries` table. - -**2. Database permissions:** -```bash -wp db query "SHOW GRANTS;" -``` - -User must have INSERT privilege. - -**3. Nonce verification:** -- If using caching, exclude AJAX endpoints -- WP Super Cache: Exclude `/wp-admin/admin-ajax.php` - -**4. Debug log:** -```bash -tail -f wp-content/debug.log -# Submit form, watch for errors -``` - -See [troubleshooting: submissions not saving](troubleshooting.md#form-submissions-not-saving) - ---- - -### Email notifications aren't sending. Help! - -**Diagnose:** - -```bash -# Test WordPress mail function -wp eval "wp_mail('your@email.com', 'Test', 'Testing');" -``` - -If no email received → Server mail() disabled. - -**Fix:** - -**Install SMTP plugin:** -```bash -wp plugin install wp-mail-smtp --activate -``` - -**Configure with:** -- Gmail -- SendGrid -- Mailgun -- Amazon SES - -**Check spam folder:** -- Default WordPress sender: `wordpress@yourdomain.com` -- Often flagged as spam -- SMTP fixes this - -See [troubleshooting: emails not sending](troubleshooting.md#email-notifications-not-sending) - ---- - -## Customization Questions - -### Can I customize form styling? - -**Yes.** Multiple options: - -**Option 1: Settings panel (no code):** -1. Edit form -2. Settings sidebar → Design -3. Customize: - - Colors (background, text, buttons) - - Typography (fonts, sizes) - - Spacing (padding, margins) - - Border radius - -**Option 2: Custom CSS:** -1. SureForms → Settings → Custom CSS -2. Add your styles: -```css -.srfm-form { - background: #f9f9f9; - padding: 30px; - border-radius: 8px; -} - -.srfm-form input { - border: 2px solid #333; -} -``` - -**Option 3: Theme stylesheet:** - -Add to `style.css`: -```css -.srfm-form { /* Your styles */ } -``` - -**Option 4: Page builder:** - -Use page builder's CSS editor (if using Elementor/Divi). - ---- - -### Can I add custom fields? - -**Yes** (requires development). - -**Process:** - -1. Create block folder: `inc/blocks/my-field/` -2. Register block: `block.php` -3. Create React component: `src/blocks/my-field/edit.js` -4. Add sanitization: `inc/helper.php` -5. Add validation: `inc/field-validation.php` - -**Example:** See [onboarding guide](onboarding.md#step-2-create-block-structure) - -**Pre-built custom fields (Pro):** -- Star rating -- Signature -- File upload -- Date/time picker - ---- - -### Can I change the submit button text? - -**Yes.** - -**Per form:** -1. Edit form -2. Click submit button block -3. Settings sidebar → Button Text -4. Change text -5. Update form - -**Globally:** - -Add to `functions.php`: -```php -add_filter('sureforms_submit_button_text', function($text) { - return __('Send Message', 'sureforms'); -}); -``` - ---- - -## Business & Licensing Questions - -### Can I use SureForms on client sites? - -**Free:** Yes, unlimited sites. - -**Pro:** -- **Agency license required** ($299/year, unlimited sites) -- Essential ($99/year, 3 sites) - only for your own sites -- Plus ($199/year, 20 sites) - only for your own sites - -**License terms:** -- You must own the sites (not client-owned sites with Essential/Plus) -- Agency license allows client sites -- Each site needs separate activation - ---- - -### Can I white-label SureForms? - -**Not officially.** - -**What you can do:** -- Remove branding from admin (CSS) -- Custom email footer (replace "Powered by SureForms") -- Custom confirmation messages - -**What you can't do:** -- Remove copyright notices from code -- Rebrand and resell as your own product -- Remove attribution from free plugin - -**Enterprise white-label:** -- Contact sales for custom licensing -- Available for high-volume agencies - ---- - -### What's your refund policy? - -**14-day money-back guarantee** (Pro licenses) - -**Refund eligibility:** -- Purchased within last 14 days -- Tried to use but encountered issues -- Support unable to resolve - -**Not eligible:** -- After 14 days -- Changed mind (not technical issue) -- Purchased wrong license (can upgrade instead) - -**Process:** -1. Contact support: support@sureforms.com -2. Describe issue (we'll try to help first) -3. If unresolved, refund processed within 3-5 business days - ---- - -### How long do I get updates? - -**Free:** Lifetime updates. - -**Pro:** -- Updates: For duration of license (1 year) -- Renewals: Discounted renewal price after year 1 -- If license expires: Plugin continues working, no new updates - -**Version compatibility:** -- Major updates: Included -- Security updates: Prioritized -- Feature updates: Based on roadmap - ---- - -## Developer Questions - -### Is the code open source? - -**Free plugin:** Yes. -- License: GPL v2 or later -- GitHub: https://github.com/brainstormforce/sureforms - -**Pro plugin:** No. -- Proprietary license -- Source code visible but not redistributable - ---- - -### Can I contribute to SureForms? - -**Yes!** We welcome contributions. - -**How to contribute:** - -1. Fork repo: https://github.com/brainstormforce/sureforms -2. Create branch: `git checkout -b feat/my-feature` -3. Make changes -4. Run tests: `composer test && npm run test:unit` -5. Submit PR - -**Contribution guidelines:** -- Follow [coding standards](coding-standards.md) -- Add tests for new features -- Update docs if needed -- Sign off commits: `Co-Authored-By: Your Name ` - ---- - -### Where can I find the API documentation? - -**API docs:** [apis.md](apis.md) - -**Key resources:** -- REST endpoints: [apis.md#rest-api](apis.md#rest-api) -- Hooks & filters: [apis.md#hooks--filters](apis.md#hooks--filters) -- JavaScript APIs: [apis.md#javascript-apis](apis.md#javascript-apis) -- Database APIs: [apis.md#database-apis](apis.md#database-apis) - -**Code examples:** [ai-agent-guide.md](ai-agent-guide.md) - ---- - -## Need More Help? - -**Documentation:** -- [README.md](README.md) - Quick start -- [Troubleshooting](troubleshooting.md) - Common problems -- [Onboarding](onboarding.md) - Developer guide -- [Glossary](glossary.md) - Technical terms - -**Support:** -- **Free users:** WordPress.org forum, GitHub issues -- **Pro users:** https://support.brainstormforce.com/ (< 24hr response) - -**Community:** -- Facebook: https://www.facebook.com/groups/surecart -- GitHub Discussions: https://github.com/brainstormforce/sureforms/discussions - ---- - -**Next:** [Glossary](glossary.md) diff --git a/internal-docs/glossary.md b/internal-docs/glossary.md deleted file mode 100644 index 440e34b70..000000000 --- a/internal-docs/glossary.md +++ /dev/null @@ -1,723 +0,0 @@ -# Glossary - -**Version:** 2.5.0 - ---- - -## A - -### AJAX (Asynchronous JavaScript and XML) -Technique for updating parts of a page without full reload. Used in SureForms for form submissions, payment processing, and file uploads. - -**Example:** -```javascript -// AJAX request to submit form -wp.ajax.post('srfm_submit_form', { data: formData }); -``` - -### API (Application Programming Interface) -Set of functions and methods for interacting with software. SureForms provides REST API, database API, and JavaScript API. - -**Types in SureForms:** -- **REST API:** HTTP endpoints for external access -- **Database API:** Methods to query entries/payments -- **JavaScript API:** Frontend form manipulation - -### Attribute (Gutenberg) -Data stored with a block. In SureForms, attributes define field configuration (label, required, placeholder, etc.). - -**Example:** -```json -{ - "label": "Email", - "required": true, - "placeholder": "name@example.com" -} -``` - ---- - -## B - -### Block (Gutenberg) -Reusable component in WordPress editor. SureForms forms are composed of blocks (Email block, Textarea block, Submit button block, etc.). - -**Types:** -- **Static blocks:** Rendered client-side (React) -- **Dynamic blocks:** Rendered server-side (PHP) - -### Block Editor -WordPress's editing interface (formerly called "Gutenberg"). Native to WordPress 5.0+. - -**Why SureForms uses it:** -- Users already familiar -- No proprietary builder to learn -- Future-proof (WordPress core) - ---- - -## C - -### Capability -WordPress permission level. SureForms checks capabilities before allowing admin actions. - -**Common capabilities:** -- `manage_options` - Admin settings access -- `edit_posts` - Create forms (default) -- `read` - View forms (logged-in users) - -**Example:** -```php -if (current_user_can('manage_options')) { - // Allow admin action -} -``` - -### Conditional Logic (Pro) -Show or hide form fields based on user answers. Example: Show "Dietary restrictions" only if user selects "Yes" to "Will you attend?" - -**Types:** -- **Show when:** Display field if condition matches -- **Hide when:** Hide field if condition matches -- **AND/OR logic:** Multiple conditions combined - -### Conversational Form (Pro) -Form that displays one question at a time, like a chat conversation. Increases engagement and completion rates. - -**Benefits:** -- Less overwhelming than long forms -- Higher completion rate (3x vs single-page) -- Better mobile experience - -### CSRF (Cross-Site Request Forgery) -Attack where malicious site tricks user into submitting request to your site. SureForms prevents with nonce verification. - -**Example attack (prevented):** -```html - - - - - - -``` - -### CSV (Comma-Separated Values) -File format for spreadsheet data. SureForms can export form entries as CSV. - -**Example:** -``` -Name,Email,Message -John Doe,john@example.com,Hello! -Jane Smith,jane@example.com,Question about... -``` - ---- - -## D - -### Database Table -MySQL table storing structured data. SureForms uses 4 custom tables: -- `wp_sureforms_entries` - Form submissions -- `wp_sureforms_payments` - Payment records -- `wp_sureforms_integrations` - OAuth credentials -- `wp_sureforms_save_resume` - Draft submissions (Pro) - -**Why custom tables?** -- Faster queries (indexed columns) -- More efficient than post meta -- Easier to export - -### Dynamic Block -Gutenberg block rendered server-side (PHP). Most SureForms field blocks are dynamic for security (server-side validation). - -**Example:** -```php -register_block_type('sureforms/email', [ - 'render_callback' => 'render_email_field' -]); -``` - ---- - -## E - -### Encryption -Converting data to unreadable format. SureForms encrypts OAuth credentials and API keys. - -**Algorithm:** AES-256-CTR - -**Example:** -```php -$encrypted = Encryption::encrypt($api_key); -// Stored in database: "base64encodedIV.encryptedData" -``` - -### Entry -Single form submission. Stored in `wp_sureforms_entries` table. - -**Contains:** -- Form ID -- User ID (if logged in) -- Field values (JSON) -- Metadata (IP, user agent, timestamp) -- Status (published, spam, trash) - -### Escaping (Output Escaping) -Converting special characters to prevent XSS. SureForms escapes all user data before display. - -**Functions:** -- `esc_html()` - For HTML content -- `esc_attr()` - For HTML attributes -- `esc_url()` - For URLs -- `esc_js()` - For JavaScript strings - -**Example:** -```php -echo '
' . esc_html($user_input) . '
'; -``` - ---- - -## F - -### Field Type -Type of input field (email, text, number, etc.). SureForms has 15+ field types. - -**Common types:** -- Text, Email, URL, Textarea -- Number, Phone -- Multiple Choice, Checkbox, Dropdown -- Address, Upload (Pro) - -### Filter (WordPress Hook) -Function that modifies data. SureForms provides 30+ filters for customization. - -**Example:** -```php -add_filter('sureforms_email_subject', function($subject, $form_id) { - return "[Form $form_id] $subject"; -}, 10, 2); -``` - ---- - -## G - -### GDPR (General Data Protection Regulation) -European privacy law. SureForms provides GDPR-compliance features (consent checkbox, data export/deletion). - -**Requirements:** -- Explicit consent for data collection -- Right to access data (export) -- Right to deletion -- Privacy policy link - -### Gutenberg -WordPress's block editor (officially called "Block Editor"). Named after Johannes Gutenberg, inventor of printing press. - -**Why "Gutenberg"?** -- Revolutionized content creation (like printing press) -- Blocks are reusable components - ---- - -## H - -### Honeypot -Anti-spam technique using hidden field. Bots fill all fields (including hidden), revealing themselves. - -**How it works:** -```html - - -``` - -**SureForms logic:** -```php -if (!empty($_POST['srfm-honeypot'])) { - // Spam detected -} -``` - -### Hook (WordPress) -Point where developers can inject custom code. Two types: actions and filters. - -**SureForms hooks:** -- 50+ action hooks (e.g., `sureforms_after_entry_save`) -- 30+ filter hooks (e.g., `sureforms_email_template`) - ---- - -## I - -### Integration (Native Integration) -Connection to third-party service. SureForms Pro has 24+ native integrations (Mailchimp, HubSpot, etc.). - -**Types:** -- **OAuth:** User authorizes connection (Mailchimp, Google) -- **API Key:** User provides key from service -- **Webhook:** SureForms sends POST request to URL - -### Instant Form -SureForms feature allowing forms to be published with unique URL (no embedding needed). - -**Example URL:** -``` -https://yoursite.com/?srfm_form=abc123 -``` - -**Use cases:** -- Email signatures -- Social media links -- QR codes - ---- - -## J - -### JSON (JavaScript Object Notation) -Data format for structured information. SureForms stores entry data and block attributes as JSON. - -**Example entry data:** -```json -{ - "email": "john@example.com", - "name": "John Doe", - "message": "Hello!" -} -``` - ---- - -## L - -### Localization (l10n) -Adapting software for specific language/region. SureForms supports translation. - -**Files:** -- `languages/sureforms-{locale}.po` - Translation strings -- `languages/sureforms-{locale}.mo` - Compiled translations - -**Functions:** -```php -__('Email', 'sureforms'); // Translate string -_e('Submit', 'sureforms'); // Translate and echo -``` - ---- - -## M - -### Middleware -Intermediate server between client and API. SureForms uses middleware for AI form generation (client → middleware → OpenAI). - -**Why middleware?** -- Hides API keys from client -- Rate limiting -- Request logging -- Caching - -### Multi-Step Form (Pro) -Form split into multiple pages/steps. Improves completion rate by reducing cognitive load. - -**Example:** -``` -Step 1: Personal Information (name, email) -Step 2: Address Details -Step 3: Payment Information -``` - ---- - -## N - -### Namespace -PHP feature to organize code and prevent name conflicts. SureForms uses namespaces. - -**Example:** -```php -namespace SRFM\Inc\Database\Tables; - -class Entries { - // ... -} -``` - -### Native Integration -Integration built into SureForms (not requiring Zapier or external connector). - -**Examples:** -- Mailchimp (OAuth) -- HubSpot (OAuth) -- Slack (Webhook URL) - -### Nonce (Number Used Once) -Security token to verify request came from your site. SureForms verifies nonces on all AJAX/REST requests. - -**Example:** -```php -// Generate nonce -$nonce = wp_create_nonce('srfm_submit_form'); - -// Verify nonce -if (!wp_verify_nonce($_POST['nonce'], 'srfm_submit_form')) { - die('Invalid nonce'); -} -``` - ---- - -## O - -### OAuth (Open Authorization) -Protocol for secure authorization. Used by SureForms integrations (Mailchimp, HubSpot, etc.). - -**Flow:** -1. User clicks "Connect to Mailchimp" -2. Redirected to Mailchimp login -3. User approves access -4. Mailchimp redirects back with token -5. SureForms stores encrypted token - ---- - -## P - -### Payment Gateway -Service that processes credit card payments. SureForms supports Stripe and PayPal. - -**Comparison:** -- **Stripe:** 2.9% + $0.30, better for US/Europe -- **PayPal:** 2.9% + $0.30, more global recognition - -### Payment Intent (Stripe) -Stripe object representing payment attempt. Created before customer enters card details. - -**States:** -- `requires_payment_method` - Awaiting card -- `requires_confirmation` - Card entered, needs confirm -- `succeeded` - Payment successful -- `canceled` - Payment canceled - -### PCI DSS (Payment Card Industry Data Security Standard) -Security standard for handling credit card data. SureForms is PCI-compliant (never stores card data). - -**Compliance:** -- Card data sent directly to Stripe/PayPal -- SureForms only stores transaction ID -- No PAN (Primary Account Number) stored - -### Placeholder -Example text shown in empty input field. Disappears when user types. - -**Example:** -```html - -``` - -**Accessibility note:** Never use placeholder as label (screen readers may miss it). - -### Post Meta -WordPress method for storing additional data with posts. SureForms **doesn't** use post meta (uses custom tables instead). - -**Why not post meta?** -- Slow queries (no indexes) -- Hard to search -- Not optimized for large datasets - -### Post Type (Custom Post Type) -WordPress content type. SureForms forms are stored as custom post type `sureforms_form`. - -**Advantages:** -- Uses WordPress admin UI -- Revision history -- Trash/restore functionality - ---- - -## R - -### reCAPTCHA -Google anti-spam service. SureForms supports reCAPTCHA v2 and v3. - -**Versions:** -- **v2:** "I'm not a robot" checkbox -- **v3:** Invisible, scores user behavior (0-1) - -**Setup:** -1. Get API keys from Google reCAPTCHA admin -2. Add to SureForms → Settings → Security -3. Enable on forms - -### REST API -HTTP API for accessing SureForms data. Used by frontend, mobile apps, integrations. - -**Base URL:** `/wp-json/sureforms/v1/` - -**Example endpoints:** -- `POST /submit-form` - Submit form -- `GET /forms/{id}` - Get form data -- `POST /generate-form` - AI form generation - -### RTL (Right-to-Left) -Text direction for languages like Arabic, Hebrew. SureForms supports RTL. - -**CSS:** -```css -.srfm-field { - margin-inline-start: 10px; /* Not margin-left */ -} -``` - ---- - -## S - -### Sanitization (Input Sanitization) -Cleaning user input to prevent attacks. SureForms sanitizes all input before storage. - -**Functions:** -- `sanitize_text_field()` - Remove HTML/PHP -- `sanitize_email()` - Validate email format -- `sanitize_url()` - Validate URL format -- `absint()` - Force integer - -**Example:** -```php -$email = sanitize_email($_POST['email']); -$id = absint($_POST['id']); -``` - -### Schema (Database Schema) -Structure of database table (columns, types, indexes). - -**Example (wp_sureforms_entries):** -```sql -CREATE TABLE wp_sureforms_entries ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - form_id BIGINT UNSIGNED NOT NULL, - user_id BIGINT UNSIGNED, - entry_data LONGTEXT NOT NULL, - status VARCHAR(20) DEFAULT 'published', - created_at DATETIME NOT NULL, - INDEX idx_form_id (form_id), - INDEX idx_created_at (created_at) -); -``` - -### Shortcode -WordPress feature for embedding content. SureForms forms can be embedded via shortcode. - -**Syntax:** -``` -[sureforms id="123"] -``` - -**Parameters:** -- `id` - Form ID (required) -- `title` - Show title (true/false) -- `description` - Show description (true/false) - -### SQL Injection -Attack injecting malicious SQL into queries. SureForms prevents with `$wpdb->prepare()`. - -**Vulnerable code (DON'T DO THIS):** -```php -$id = $_GET['id']; -$wpdb->query("DELETE FROM table WHERE id = $id"); -// Attack: ?id=1 OR 1=1 (deletes all records!) -``` - -**Safe code:** -```php -$id = absint($_GET['id']); -$wpdb->query($wpdb->prepare("DELETE FROM table WHERE id = %d", $id)); -``` - -### Subscription -Recurring payment. SureForms supports subscriptions via Stripe and PayPal. - -**Intervals:** -- Daily -- Weekly -- Monthly -- Yearly - ---- - -## T - -### Template (Form Template) -Pre-built form configuration. SureForms provides 100+ templates (contact, registration, survey, etc.). - -**Structure:** -```json -{ - "name": "Contact Form", - "fields": [ - { "type": "text", "label": "Name" }, - { "type": "email", "label": "Email" }, - { "type": "textarea", "label": "Message" } - ] -} -``` - -### Transient -WordPress temporary cache. SureForms uses transients for AI rate limiting and API responses. - -**Example:** -```php -// Store for 1 hour -set_transient('srfm_ai_rate_limit_' . $user_id, true, HOUR_IN_SECONDS); - -// Check -if (get_transient('srfm_ai_rate_limit_' . $user_id)) { - // Rate limited -} -``` - ---- - -## U - -### User Agent -String identifying browser/device. SureForms logs user agent for spam detection. - -**Example:** -``` -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 -``` - ---- - -## V - -### Validation (Form Validation) -Checking if user input meets requirements. SureForms validates both client-side (JavaScript) and server-side (PHP). - -**Types:** -- **Required:** Field must not be empty -- **Format:** Email/URL must be valid -- **Range:** Number within min/max -- **Length:** String length constraints - -**Example:** -```php -if (empty($email) || !is_email($email)) { - $errors[] = 'Please enter a valid email'; -} -``` - ---- - -## W - -### Webhook -HTTP callback sent when event occurs. SureForms can send webhooks on form submission (Pro). - -**Flow:** -``` -User submits form - ↓ -SureForms processes submission - ↓ -Sends POST to webhook URL - ↓ -External service receives data -``` - -**Example payload:** -```json -{ - "event": "form_submit", - "form_id": 123, - "entry_id": 456, - "fields": { ... } -} -``` - -### WordPress Coding Standards (WPCS) -PHP coding style guide for WordPress. SureForms follows WPCS. - -**Key rules:** -- Tabs for indentation (not spaces) -- Yoda conditions: `if ( 'value' === $variable )` -- Braces required for all control structures -- Space after control keywords: `if (` not `if(` - -**Check compliance:** -```bash -composer lint -``` - ---- - -## X - -### XSS (Cross-Site Scripting) -Attack injecting malicious JavaScript into pages. SureForms prevents with output escaping. - -**Vulnerable code (DON'T DO THIS):** -```php -echo '
' . $_POST['name'] . '
'; -// Attack: -``` - -**Safe code:** -```php -echo '
' . esc_html($_POST['name']) . '
'; -// Output: <script>steal(document.cookie)</script> -``` - ---- - -## Z - -### Zapier -Third-party automation service. SureForms doesn't have native Zapier integration but supports webhooks (which Zapier can consume). - -**Alternative:** Use SureForms webhooks to trigger Zapier zaps. - ---- - -## Acronyms Quick Reference - -| Acronym | Full Term | Meaning | -|---------|-----------|---------| -| AJAX | Asynchronous JavaScript and XML | Update page without reload | -| API | Application Programming Interface | Methods to interact with software | -| CSRF | Cross-Site Request Forgery | Attack tricking users into unwanted actions | -| CSV | Comma-Separated Values | Spreadsheet file format | -| GDPR | General Data Protection Regulation | EU privacy law | -| JSON | JavaScript Object Notation | Data format | -| OAuth | Open Authorization | Secure authorization protocol | -| PCI DSS | Payment Card Industry Data Security Standard | Credit card security | -| REST | Representational State Transfer | Web API architecture | -| RTL | Right-to-Left | Text direction (Arabic, Hebrew) | -| SQL | Structured Query Language | Database query language | -| URL | Uniform Resource Locator | Web address | -| WCAG | Web Content Accessibility Guidelines | Accessibility standards | -| WPCS | WordPress Coding Standards | PHP coding style | -| XSS | Cross-Site Scripting | JavaScript injection attack | - ---- - -## Common Abbreviations - -| Abbr | Full Term | -|------|-----------| -| Pro | SureForms Pro (premium plugin) | -| CRM | Customer Relationship Management | -| UI | User Interface | -| UX | User Experience | -| CPT | Custom Post Type | -| DB | Database | -| WP | WordPress | -| PHP | PHP: Hypertext Preprocessor | -| JS | JavaScript | -| CSS | Cascading Style Sheets | - ---- - -**Next:** [Maintenance](maintenance.md) diff --git a/internal-docs/maintenance.md b/internal-docs/maintenance.md deleted file mode 100644 index 71c2f2604..000000000 --- a/internal-docs/maintenance.md +++ /dev/null @@ -1,842 +0,0 @@ -# Documentation Maintenance Guide - -**Version:** 2.5.0 - ---- - -## Purpose - -This guide explains how to keep SureForms internal documentation accurate and useful. - -**Goal:** Documentation should always reflect current reality. - -**Principle:** Update docs when code changes, not as afterthought. - ---- - -## When to Update Documentation - -### Always Update - -**1. New Feature Added** -- Update: `architecture.md`, `apis.md`, `product-vision.md` -- Add: Code examples to `ai-agent-guide.md` -- Update: Onboarding guide if workflow changes - -**2. API Changes** -- Update: `apis.md` (new endpoints, hooks, parameters) -- Update: Code examples in all docs referencing changed API -- Mark deprecated APIs in `apis.md` - -**3. Database Schema Changes** -- Update: `architecture.md` (database section) -- Update: `codebase-map.md` (new files) -- Update: Migration guide if schema change requires migration - -**4. Security Fix** -- Update: `ai-agent-guide.md` (add to common pitfalls) -- Update: `troubleshooting.md` if fix addresses known issue -- Add example of vulnerable pattern to avoid - -**5. Breaking Changes** -- Update: `README.md` (migration guide) -- Update: `onboarding.md` (new workflows) -- Update: `faq.md` (address upgrade questions) - ---- - -### Update if Needed - -**6. Bug Fixes** -- Update: `troubleshooting.md` if common issue -- Add: To FAQ if frequently asked - -**7. Performance Improvements** -- Update: `architecture.md` if architecture changed -- Update: Benchmarks in `README.md` - -**8. UI/UX Changes** -- Update: `ui-and-copy.md` (new patterns, copy) -- Update: `onboarding.md` (user journeys) - ---- - -### Don't Update - -**9. Code Refactoring (No Behavior Change)** -- No doc update unless internal architecture significantly changed - -**10. Minor Copy Changes** -- No doc update unless it changes UX patterns - -**11. Version Bumps** -- Update version number at top of each doc -- That's it (unless other changes) - ---- - -## How to Update Documentation - -### Step 1: Identify Affected Docs - -**Use this decision tree:** - -``` -Did you change... - -┌─ Database schema? -│ └─> Update: architecture.md, codebase-map.md -│ -┌─ REST API? -│ └─> Update: apis.md, ai-agent-guide.md (examples) -│ -┌─ AJAX handlers? -│ └─> Update: apis.md -│ -┌─ Hooks/filters? -│ └─> Update: apis.md, ai-agent-guide.md (patterns) -│ -┌─ User-facing UI? -│ └─> Update: ui-and-copy.md, onboarding.md -│ -┌─ Security patterns? -│ └─> Update: ai-agent-guide.md, coding-standards.md -│ -┌─ Build process? -│ └─> Update: README.md, onboarding.md -│ -└─ New file/folder? - └─> Update: codebase-map.md -``` - ---- - -### Step 2: Read Existing Doc - -**Before editing:** -1. Read entire document -2. Identify outdated sections -3. Check for consistency with your changes - -**Don't:** -- Blindly add new section without reading context -- Create duplicate information -- Use different terminology than existing docs - ---- - -### Step 3: Make Minimal Changes - -**Best practices:** - -**Do:** -- Update only what changed -- Keep existing structure -- Match existing tone and style -- Add code examples if helpful - -**Don't:** -- Rewrite entire document (unless necessary) -- Change unrelated sections -- Reorganize without discussion -- Remove information (mark deprecated instead) - ---- - -### Step 4: Verify Examples Still Work - -**All code examples must be tested:** - -**PHP examples:** -```bash -# Create test file -cat > /tmp/test-example.php << 'EOF' - [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'limit' => 20 -]); -``` - -**❌ Bad:** -```php -// Vague, generic -$data = SomeClass::get_stuff($args); -``` - ---- - -### Cross-Referencing - -**Internal links:** -```markdown -See [Architecture Guide](architecture.md) for details. -See [Database Schema](architecture.md#database-schema) for table structure. -``` - -**External links:** -```markdown -Read [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/). -``` - -**Code references:** -```markdown -See `inc/form-submit.php:88-119` for nonce verification. -``` - ---- - -### Code Block Guidelines - -**Always specify language:** - -````markdown -```php -// PHP code here -``` - -```javascript -// JavaScript here -``` - -```bash -# Shell commands here -``` - -```sql --- SQL queries here -``` -```` - -**Include comments:** -```php -// Good: Explain what code does -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'status', 'value' => 'published', 'compare' => '='] - ] -]); -``` - -**Show output if helpful:** -```bash -wp plugin list --status=active -# Output: -# sureforms active -# sureforms-pro active -``` - ---- - -## Deprecation Process - -**When removing features:** - -### Step 1: Mark as Deprecated - -**In code:** -```php -/** - * Old function. - * - * @deprecated 2.5.0 Use new_function() instead. - */ -function old_function() { - _deprecated_function(__FUNCTION__, '2.5.0', 'new_function'); - return new_function(); -} -``` - -**In docs:** -```markdown -## ~~Old Feature~~ (Deprecated) - -**Deprecated in:** 2.5.0 -**Removed in:** 3.0.0 -**Replacement:** [New Feature](#new-feature) - -This feature is deprecated and will be removed in version 3.0.0. -Use [New Feature](#new-feature) instead. - -~~Old documentation here...~~ -``` - ---- - -### Step 2: Update Migration Guide - -**In README.md:** - -```markdown -## Upgrading from 2.4.x to 2.5.0 - -### Breaking Changes - -**Old Feature Deprecated:** -- **What changed:** `old_function()` is now deprecated -- **Action required:** Replace with `new_function()` -- **Code example:** - ```php - // Old (deprecated) - old_function($data); - - // New (recommended) - new_function($data); - ``` -``` - ---- - -### Step 3: Remove After Major Version - -**On next major version (3.0.0):** -1. Remove deprecated code -2. Remove ~~strikethrough~~ docs -3. Update changelog - ---- - -## Changelog Management - -**Keep CHANGELOG.md updated:** - -### Format - -```markdown -# Changelog - -## [2.5.1] - 2026-02-15 - -### Added -- New REST endpoint: `/sureforms/v1/forms/{id}/duplicate` -- Support for custom date formats in email notifications - -### Changed -- Improved performance of entry queries (20% faster) -- Updated Stripe API to v2024-01-01 - -### Deprecated -- `old_function()` - Use `new_function()` instead - -### Fixed -- Bug where conditional logic didn't work with checkboxes -- Memory leak in AI form generation - -### Security -- Fixed XSS vulnerability in admin settings (CVE-2026-0001) - -## [2.5.0] - 2026-02-01 - -... -``` - -**Categories:** -- **Added:** New features -- **Changed:** Changes to existing features -- **Deprecated:** Features marked for removal -- **Removed:** Features removed -- **Fixed:** Bug fixes -- **Security:** Security fixes - ---- - -## Documentation Review Checklist - -**Before committing doc changes:** - -### Content -- [ ] Information is accurate (tested code examples) -- [ ] No outdated information (double-checked) -- [ ] Cross-references updated (links work) -- [ ] Version number updated -- [ ] Terminology consistent with other docs - -### Style -- [ ] Clear and concise (no fluff) -- [ ] Code examples formatted correctly -- [ ] Spelling and grammar correct -- [ ] Tone matches existing docs - -### Technical -- [ ] All code examples tested -- [ ] All commands verified (on dev environment) -- [ ] All links work (no 404s) -- [ ] File paths correct (checked in codebase) - -### Accessibility -- [ ] Headings in logical order (H1 → H2 → H3) -- [ ] Code blocks have language specified -- [ ] Alt text for images (if any) -- [ ] Tables have headers - ---- - -## Automation & Tools - -### Linting Documentation - -**Check Markdown syntax:** - -```bash -# Install markdownlint -npm install -g markdownlint-cli - -# Lint all docs -cd internal-docs/ -markdownlint *.md -``` - -**Common issues caught:** -- Inconsistent heading levels -- Trailing whitespace -- Missing blank lines around code blocks - ---- - -### Link Checking - -**Verify all links work:** - -```bash -# Install markdown-link-check -npm install -g markdown-link-check - -# Check links -markdown-link-check internal-docs/*.md -``` - -**Fix broken links:** -- Update to correct URL -- Remove if resource no longer exists -- Use Internet Archive if critical resource - ---- - -### Spell Checking - -**Use spell checker:** - -```bash -# Install aspell -brew install aspell # macOS -apt install aspell # Linux - -# Check spelling -aspell check internal-docs/README.md -``` - -**Custom dictionary:** - -Create `.aspell.en.pws`: -``` -personal_ws-1.1 en 50 -SureForms -WordPress -Gutenberg -Mailchimp -PayPal -``` - ---- - -## Documentation Templates - -### New Feature Documentation Template - -**When adding new feature:** - -```markdown -## [Feature Name] - -**Since:** [version] -**Type:** [Free/Pro] - -### Overview - -[2-3 sentence description of what feature does and why it exists] - -### Use Cases - -- [Use case 1] -- [Use case 2] -- [Use case 3] - -### How to Use - -**Step 1: [Action]** - -[Description] - -```php -// Code example -``` - -**Step 2: [Action]** - -[Description] - -### Configuration - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `option_name` | string | 'default' | What this does | - -### API Reference - -**REST Endpoint:** -``` -POST /sureforms/v1/feature -``` - -**Parameters:** -- `param1` (string, required) - Description -- `param2` (integer, optional) - Description - -**Response:** -```json -{ - "success": true, - "data": { ... } -} -``` - -### Examples - -**Example 1: [Use case]** -```php -// Code here -``` - -**Example 2: [Use case]** -```php -// Code here -``` - -### Troubleshooting - -**Issue:** [Common problem] -**Solution:** [How to fix] - -### Related - -- [Related feature 1](link) -- [Related feature 2](link) -``` - ---- - -### Bug Fix Documentation Template - -**When fixing bug:** - -**In troubleshooting.md:** - -```markdown -### [Issue Description] - -**Symptom:** [What user sees] - -**Diagnosis:** - -[How to identify the issue] - -```bash -# Commands to diagnose -``` - -**Fixes:** - -**Option 1: [Fix description]** -```php -// Code or command -``` - -**Option 2: [Alternative fix]** -```php -// Alternative code -``` - -**Prevents:** [What this prevents] - -**See also:** [Related issues] -``` - ---- - -## Review & Approval Process - -### Before Merging Docs - -**Self-review:** -1. Read your changes out loud -2. Test all code examples -3. Run linters -4. Check cross-references - -**Peer review:** -1. Request review from team member -2. Address feedback -3. Update based on comments - -**Final checks:** -1. Rebase on main branch -2. Verify no conflicts -3. One last proofread - ---- - -### Documentation Pull Request Template - -**PR description:** - -```markdown -## Documentation Update - -**Plugin Version:** 2.5.1 -**Docs Changed:** [List files] - -### Changes Made - -- [ ] Added documentation for [feature] -- [ ] Updated [section] in [file] -- [ ] Fixed typos in [file] - -### Verification - -- [x] All code examples tested -- [x] All links checked -- [x] Spelling checked -- [x] Cross-references updated -- [x] Version numbers updated - -### Related - -- Related PR: #123 -- Related Issue: #456 -``` - ---- - -## Documentation Metrics - -**Track documentation quality:** - -### Metrics to Monitor - -**Coverage:** -- % of features documented -- % of APIs documented -- % of code examples tested - -**Freshness:** -- Days since last update -- Number of outdated sections -- Deprecated content count - -**Quality:** -- Broken link count -- Spelling error count -- User-reported doc issues - -**Engagement:** -- Doc views (if analytics enabled) -- Time spent reading -- Bounce rate (if high, docs unclear) - ---- - -## Long-Term Maintenance - -### Quarterly Review - -**Every 3 months:** - -1. **Audit all docs:** - - Read each document start to finish - - Test all code examples - - Verify all links - - Update outdated info - -2. **User feedback:** - - Review GitHub issues tagged "documentation" - - Survey users about doc clarity - - Identify gaps in coverage - -3. **Reorganize if needed:** - - Move sections to better fit structure - - Split large docs if too long - - Merge duplicate information - ---- - -### Annual Review - -**Every year:** - -1. **Major cleanup:** - - Remove deprecated content - - Archive old versions - - Rewrite outdated sections - -2. **Structure evaluation:** - - Does structure still make sense? - - Should we add/remove files? - - Are cross-references clear? - -3. **Voice & tone:** - - Is tone consistent? - - Is it still beginner-friendly? - - Does it match product evolution? - ---- - -## Questions? - -**For documentation questions:** -- Create GitHub issue: `[Docs] Your question` -- Tag: `documentation` -- Assign: Documentation maintainer - -**For urgent doc bugs:** -- Ping in Slack: `#sureforms-docs` -- Include: File name, line number, issue description - ---- - -## Conclusion - -**Documentation is code.** - -Treat it with same care: -- Test before committing -- Review changes -- Keep it DRY (Don't Repeat Yourself) -- Refactor when needed - -**Good documentation:** -- Saves support time -- Accelerates onboarding -- Prevents bugs -- Shows we care - -**Thank you for maintaining SureForms docs!** 📚 - ---- - -**Version:** 2.5.0 -**Last Updated:** 2026-02-12 -**Maintainer:** SureForms Team diff --git a/internal-docs/onboarding.md b/internal-docs/onboarding.md deleted file mode 100644 index ab5b83ad3..000000000 --- a/internal-docs/onboarding.md +++ /dev/null @@ -1,1086 +0,0 @@ -# Developer Onboarding - -**Version:** 2.5.0 - ---- - -## Welcome to SureForms - -This guide helps you become productive quickly, whether you have 1 hour, 1 day, or 1 week. - -**What you're working on:** -- **SureForms Free:** AI-powered WordPress form builder (219 PHP files, 50+ blocks) -- **SureForms Pro:** Premium extension (206 PHP files, payments, integrations, advanced features) - -**Architecture:** WordPress + Gutenberg blocks + React 18 + Custom database - ---- - -## Prerequisites - -Before starting: -- [ ] WordPress 6.4+ installed locally -- [ ] Node.js 18+ and npm 8+ -- [ ] PHP 7.4+ with Composer -- [ ] Git configured -- [ ] IDE with PHP/JavaScript support -- [ ] Both plugins cloned from GitHub - -**Test environment:** -```bash -# If using Local by Flywheel or similar -http://localhost:10003/wp-admin/ -Username: admin -Password: admin -``` - ---- - -## 1-Hour Quick Start - -**Goal:** Make your first successful change - -### Step 1: Get Code Running (15 min) - -```bash -# Clone repositories (if not already) -cd /path/to/wp-content/plugins/ -git clone https://github.com/brainstormforce/sureforms.git -git clone https://github.com/brainstormforce/sureforms-pro.git - -# Install dependencies - SureForms Free -cd sureforms/ -npm install -composer install - -# Build assets -npm run build - -# Install dependencies - SureForms Pro -cd ../sureforms-pro/ -npm install -composer install -npm run build -``` - -**Verify installation:** -```bash -# In WordPress admin -wp plugin activate sureforms sureforms-pro -wp plugin list | grep sureforms -``` - -### Step 2: Create Your First Form (15 min) - -**Via WordPress admin:** -1. Go to **SureForms → Add New** -2. Click **Create with AI** or **Start from Blank** -3. Add fields: Email, Name, Message -4. Click **Publish** -5. Add to a page with the SureForms block - -**Via WP-CLI:** -```bash -# Create form programmatically -wp post create \ - --post_type=sureforms_form \ - --post_title="Test Contact Form" \ - --post_status=publish \ - --post_content='' -``` - -### Step 3: Make a Simple Change (20 min) - -**Task:** Change the submit button text - -**Files to modify:** -``` -src/blocks/form/edit.js ← React component -inc/blocks/form/block.php ← Server-side rendering -``` - -**Change 1: Edit the React component** - -Open `src/blocks/form/edit.js`: - -```javascript -// Find around line 45-50 -const { submitButtonText } = attributes; - -// Change default value -const defaultSubmitText = __('Send Message', 'sureforms'); // ← Change this -``` - -**Change 2: Rebuild** - -```bash -npm run build -``` - -**Verify:** Create new form, check if default button text changed. - -### Step 4: Debug Your Change (10 min) - -**Enable debugging:** - -Edit `wp-config.php`: -```php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -define('SCRIPT_DEBUG', true); // Loads unminified JS -``` - -**Check debug log:** -```bash -tail -f wp-content/debug.log -``` - -**Browser console:** -- Open DevTools → Console -- Look for JavaScript errors -- Check Network tab for AJAX failures - ---- - -## 1-Day Deep Dive - -**Goal:** Understand architecture and make meaningful contributions - -### Morning: Core Concepts (4 hours) - -#### Hour 1: Architecture Overview - -**Read these docs first:** -1. [README.md](README.md) - Project overview -2. [architecture.md](architecture.md) - System design -3. [codebase-map.md](codebase-map.md) - File structure - -**Key concepts to understand:** -- **Gutenberg blocks:** React components that render forms -- **Custom post type:** `sureforms_form` stores form configurations -- **Custom database tables:** `wp_sureforms_entries`, `wp_sureforms_payments` -- **REST API:** Handles form submissions, AI generation -- **AJAX handlers:** Payment processing, file uploads - -**Test your understanding:** -- Where is form submission data saved? (Answer: `wp_sureforms_entries` table) -- How does AI form generation work? (Answer: REST endpoint → external middleware → GPT API) -- What's the difference between Free and Pro? (Answer: Pro adds payments, integrations, conditional logic) - -#### Hour 2: Data Flow - -**Trace a form submission from start to finish:** - -``` -User fills form → Frontend validation → AJAX request - ↓ -inc/form-submit.php (Line 88-119: Nonce verification) - ↓ -Field validation (inc/field-validation.php) - ↓ -Sanitization (inc/helper.php::sanitize_by_field_type()) - ↓ -Database insert (inc/database/tables/entries.php::insert()) - ↓ -Email notification (inc/email/email-handler.php) - ↓ -Confirmation message (or redirect) -``` - -**Hands-on exercise:** -1. Enable MySQL query logging -2. Submit a test form -3. Check `wp_sureforms_entries` table -4. Verify entry data matches submission - -```bash -# View recent entries -wp db query "SELECT * FROM wp_sureforms_entries ORDER BY id DESC LIMIT 5;" -``` - -#### Hour 3: Payment Processing - -**Understand Stripe payment flow:** - -``` -User clicks "Pay" → Frontend creates payment intent - ↓ -AJAX: wp_ajax_nopriv_srfm_create_payment_intent - ↓ -inc/payments/front-end.php (Line 77-83: Create Stripe PaymentIntent) - ↓ -Stripe API processes payment - ↓ -Webhook received: sureforms/webhook_test - ↓ -inc/payments/stripe/stripe-webhook.php (Line 220-637: Handle events) - ↓ -Update wp_sureforms_payments table - ↓ -Send confirmation email -``` - -**Hands-on exercise:** -1. Set up Stripe test keys -2. Create a payment form -3. Use Stripe test card: `4242 4242 4242 4242` -4. Check `wp_sureforms_payments` table for payment record - -#### Hour 4: Pro Features - -**Explore key Pro additions:** - -**1. User Registration:** -- File: `inc/business/user-registration/processor.php` -- Creates WordPress user accounts from form submissions -- Handles login/password reset - -**2. Native Integrations:** -- Folder: `inc/pro/native-integrations/integrations/` -- 24+ services: Mailchimp, Brevo, HubSpot, Salesforce, etc. -- OAuth authentication with encrypted token storage - -**3. Conditional Logic:** -- Frontend: `src/pro/conditional-logic/` -- Show/hide fields based on user selections -- Real-time UI updates - -**Hands-on exercise:** -1. Create form with registration block -2. Submit form, verify user created: `wp user list` -3. Enable Mailchimp integration (test mode) -4. Submit form, check integration logs - -### Afternoon: Build Your First Feature (4 hours) - -**Challenge:** Add a new field type - -**Example: "Star Rating" field** - -#### Step 1: Plan (30 min) - -**Requirements:** -- Display 5 stars (clickable) -- Save selected rating (1-5) -- Validate: Required if configured -- Display in entries table - -**Files to create/modify:** -``` -src/blocks/star-rating/ ← New React component -inc/blocks/star-rating/block.php ← Server-side rendering -inc/helper.php ← Add sanitization -inc/field-validation.php ← Add validation -``` - -#### Step 2: Create Block Structure (1 hour) - -**Create block files:** - -```bash -# Create directories -mkdir -p src/blocks/star-rating -mkdir -p inc/blocks/star-rating - -# Copy from existing field (Phone is a good template) -cp -r src/blocks/phone/* src/blocks/star-rating/ -cp inc/blocks/phone/block.php inc/blocks/star-rating/ -``` - -**Edit `src/blocks/star-rating/block.json`:** - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 3, - "name": "sureforms/star-rating", - "title": "Star Rating", - "category": "sureforms", - "icon": "star-filled", - "description": "Display a star rating input", - "attributes": { - "label": { - "type": "string", - "default": "Rate your experience" - }, - "required": { - "type": "boolean", - "default": false - }, - "maxRating": { - "type": "number", - "default": 5 - } - }, - "supports": { - "html": false - } -} -``` - -**Edit `src/blocks/star-rating/edit.js`:** - -```javascript -import { __ } from '@wordpress/i18n'; -import { useBlockProps } from '@wordpress/block-editor'; -import { TextControl, ToggleControl, RangeControl } from '@wordpress/components'; - -export default function Edit({ attributes, setAttributes }) { - const { label, required, maxRating } = attributes; - - return ( -
- setAttributes({ label: value })} - /> - setAttributes({ required: value })} - /> - setAttributes({ maxRating: value })} - min={1} - max={10} - /> -
- {[...Array(maxRating)].map((_, i) => ( - - ))} -
-
- ); -} -``` - -#### Step 3: Server-Side Rendering (45 min) - -**Edit `inc/blocks/star-rating/block.php`:** - -```php - [__CLASS__, 'render'], - ] - ); - } - - public static function render($attributes) { - $label = esc_html($attributes['label'] ?? 'Rate your experience'); - $required = !empty($attributes['required']); - $max_rating = absint($attributes['maxRating'] ?? 5); - $field_name = 'srfm-star-rating-' . uniqid(); - - ob_start(); - ?> -
- -
- - - ★ - - -
- > -
- { - const stars = container.querySelectorAll('.srfm-star'); - const input = container.nextElementSibling; - const maxRating = parseInt(container.dataset.max); - - stars.forEach(star => { - star.addEventListener('click', function() { - const value = parseInt(this.dataset.value); - input.value = value; - - // Visual update - stars.forEach((s, index) => { - if (index < value) { - s.classList.add('selected'); - } else { - s.classList.remove('selected'); - } - }); - }); - - // Hover effect - star.addEventListener('mouseenter', function() { - const value = parseInt(this.dataset.value); - stars.forEach((s, index) => { - if (index < value) { - s.classList.add('hover'); - } else { - s.classList.remove('hover'); - } - }); - }); - }); - - container.addEventListener('mouseleave', function() { - stars.forEach(s => s.classList.remove('hover')); - }); - }); -}); -``` - -#### Step 5: Register Block (15 min) - -**Edit `inc/blocks/register.php`:** - -```php -// Add to existing blocks registration -Star_Rating\Block::register(); -``` - -**Edit `src/blocks/star-rating/index.js`:** - -```javascript -import { registerBlockType } from '@wordpress/blocks'; -import edit from './edit'; -import metadata from './block.json'; - -registerBlockType(metadata.name, { - edit, - save: () => null, // Server-side rendering -}); -``` - -#### Step 6: Add Sanitization & Validation (30 min) - -**Edit `inc/helper.php`:** - -Find `sanitize_by_field_type()` method and add: - -```php -case 'star-rating': - $value = absint($value); - // Ensure value is between 1 and max rating - return ($value >= 1 && $value <= 10) ? $value : 0; -``` - -**Edit `inc/field-validation.php`:** - -Find validation logic and add: - -```php -if ($field_type === 'star-rating' && $is_required && empty($value)) { - $errors[] = __('Please select a rating', 'sureforms'); -} -``` - -#### Step 7: Build & Test (30 min) - -```bash -# Build assets -npm run build - -# Test in browser -# 1. Create new form -# 2. Add "Star Rating" block -# 3. Configure settings (label, required, max stars) -# 4. Publish form -# 5. Submit test entry -# 6. Verify data saved correctly - -# Check database -wp db query "SELECT * FROM wp_sureforms_entries ORDER BY id DESC LIMIT 1;" -``` - ---- - -## 1-Week Mastery - -**Goal:** Expert-level understanding and complex feature development - -### Day 1: Architecture Deep Dive - -**Morning:** -- Complete 1-day onboarding -- Read all internal docs thoroughly -- Map data flow for all major features - -**Afternoon:** -- Trace payment processing end-to-end -- Understand webhook verification -- Study integration architecture - -### Day 2: Security & Best Practices - -**Focus areas:** -1. **Input sanitization:** Understand all 15+ sanitization methods -2. **Output escaping:** When to use `esc_html()`, `esc_attr()`, `esc_url()` -3. **SQL injection prevention:** Always use `$wpdb->prepare()` -4. **CSRF protection:** Nonce verification patterns -5. **Authorization:** `current_user_can()` checks - -**Hands-on exercises:** -- Review `inc/form-submit.php` (Lines 88-119: Security checks) -- Study `inc/helper.php` sanitization methods -- Find and fix intentional security bugs in test code - -**Read:** -- [coding-standards.md](coding-standards.md) -- [ai-agent-guide.md](ai-agent-guide.md) - Security section - -### Day 3: React & Gutenberg Blocks - -**Topics:** -1. **Block architecture:** How Gutenberg blocks work -2. **Attributes:** Data storage and retrieval -3. **Inspector controls:** Settings sidebar -4. **Block variations:** Different field configurations -5. **Dynamic blocks:** Server-side rendering - -**Build complex block:** -- **Multi-step form block** (if not using Pro feature) -- Add step navigation -- Save progress between steps -- Implement validation per step - -**Resources:** -- Official Gutenberg docs: https://developer.wordpress.org/block-editor/ -- Example: Study `src/blocks/phone/` for complex field - -### Day 4: Database & REST API - -**Morning: Database operations** - -**Study these classes:** -- `inc/database/base.php` - Base table class -- `inc/database/tables/entries.php` - Entries CRUD -- `inc/database/tables/payments.php` - Payment records - -**Practice:** -```php -// Create custom query -use SRFM\Inc\Database\Tables\Entries; - -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='], - ['key' => 'status', 'value' => 'published', 'compare' => '='] - ], - 'orderby' => 'created_at', - 'order' => 'DESC', - 'limit' => 20 -]); -``` - -**Afternoon: REST API development** - -**Create custom endpoint:** - -```php -// In new file: inc/rest-api-custom.php -namespace SRFM\Inc; - -class Rest_API_Custom { - public function register_routes() { - register_rest_route('sureforms/v1', '/custom-stats', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_stats'], - 'permission_callback' => [$this, 'check_permissions'] - ]); - } - - public function check_permissions() { - return current_user_can('manage_options'); - } - - public function get_stats($request) { - // Your logic here - return ['success' => true, 'data' => []]; - } -} -``` - -### Day 5: Pro Features & Integrations - -**Morning: Payment processing** - -**Study both gateways:** -- Stripe: `inc/payments/stripe/` -- PayPal (Pro): `inc/business/payments/pay-pal/` - -**Understand webhook handling:** -- Signature verification -- Event processing -- Database updates -- Error handling - -**Afternoon: Build custom integration** - -**Example: Slack integration** - -1. Create integration folder: -``` -inc/pro/native-integrations/integrations/slack/ -├── config.json -└── actions/ - └── send-message.php -``` - -2. Define config: -```json -{ - "name": "Slack", - "slug": "slack", - "auth_method": "webhook_url", - "actions": [ - { - "name": "Send Message", - "slug": "send-message", - "endpoint": "chat.postMessage" - } - ] -} -``` - -3. Implement action: -```php -format_message($form_data); - - $response = wp_remote_post($webhook_url, [ - 'body' => json_encode(['text' => $message]), - 'headers' => ['Content-Type' => 'application/json'] - ]); - - if (is_wp_error($response)) { - return ['success' => false, 'error' => $response->get_error_message()]; - } - - return ['success' => true]; - } - - private function format_message($form_data) { - // Format form data as Slack message - return "New form submission:\n" . print_r($form_data, true); - } -} -``` - ---- - -## Testing Your Changes - -### Local Testing - -**PHP Unit Tests:** -```bash -# Run all tests -composer test - -# Run specific test file -vendor/bin/phpunit tests/unit/test-helper.php - -# Run with coverage -composer test-coverage -``` - -**JavaScript Tests:** -```bash -# Run all JS tests -npm run test:unit - -# Watch mode -npm run test:unit:watch - -# Coverage -npm run test:unit:coverage -``` - -### E2E Testing (Playwright) - -```bash -# Start test environment -npm run play:up - -# Run E2E tests -npm run play:run - -# Interactive mode (see browser) -npm run play:run:interactive - -# Stop environment -npm run play:down -``` - -### Manual Testing Checklist - -Before submitting PR: -- [ ] Form creation works -- [ ] Form submission saves entry -- [ ] Email notifications sent -- [ ] Payment processing works (if touched) -- [ ] No JavaScript console errors -- [ ] No PHP errors in debug.log -- [ ] Works in latest WordPress version -- [ ] Works in Firefox, Chrome, Safari -- [ ] Mobile responsive -- [ ] Accessibility: keyboard navigation works - ---- - -## Code Review Process - -### Before Creating PR - -**Self-review:** -1. Run linters: -```bash -composer lint # PHP_CodeSniffer -npm run lint-js # ESLint -``` - -2. Auto-fix minor issues: -```bash -composer format # phpcbf -npm run lint-js:fix # ESLint --fix -``` - -3. Check for common issues: -- [ ] All inputs sanitized -- [ ] All outputs escaped -- [ ] SQL queries use `prepare()` -- [ ] Nonces verified for AJAX/forms -- [ ] Capabilities checked for admin actions -- [ ] No hardcoded strings (use `__()` for i18n) - -### Creating PR - -**Branch naming:** -```bash -git checkout -b feat/star-rating-field -git checkout -b fix/payment-webhook-error -git checkout -b refactor/database-queries -``` - -**Commit message format:** -``` -type(scope): brief description - -Longer explanation if needed. - -Co-Authored-By: Your Name -``` - -**Types:** feat, fix, docs, style, refactor, test, chore - -**Example:** -``` -feat(blocks): add star rating field - -- Add new star-rating block -- Implement frontend JavaScript -- Add sanitization and validation -- Update docs with new field type - -Co-Authored-By: Claude Sonnet 4.5 -``` - -### PR Checklist - -- [ ] Tests pass: `composer test && npm run test:unit` -- [ ] Linting passes: `composer lint && npm run lint-js` -- [ ] No console errors -- [ ] Database migrations documented (if any) -- [ ] Breaking changes documented -- [ ] Changelog updated -- [ ] Screenshots attached (for UI changes) - ---- - -## Common Tasks & Patterns - -### Add New AJAX Handler - -**Pattern:** -```php -// In controller file -public function register_ajax_actions() { - add_action('wp_ajax_srfm_my_action', [$this, 'handle_my_action']); - add_action('wp_ajax_nopriv_srfm_my_action', [$this, 'handle_my_action']); -} - -public function handle_my_action() { - // CRITICAL: Always verify nonce - check_ajax_referer('srfm_my_action_nonce', 'security'); - - // Check permissions if needed - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => 'Insufficient permissions']); - } - - // Sanitize inputs - $data = Helper::sanitize_array_recursively($_POST['data'] ?? []); - - // Process... - - wp_send_json_success(['result' => $result]); -} -``` - -### Add New REST Endpoint - -**Pattern:** -```php -// In inc/rest-api.php -public function register_routes() { - register_rest_route('sureforms/v1', '/my-endpoint', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_request'], - 'permission_callback' => [$this, 'check_permissions'] - ]); -} - -public function check_permissions() { - return current_user_can('manage_options'); -} - -public function handle_request($request) { - // Always verify nonce - $nonce = $request->get_header('X-WP-Nonce'); - if (!wp_verify_nonce($nonce, 'wp_rest')) { - return new \WP_Error('invalid_nonce', 'Invalid nonce', ['status' => 403]); - } - - // Get and sanitize parameters - $params = $request->get_params(); - $clean_params = Helper::sanitize_array_recursively($params); - - // Process... - - return ['success' => true, 'data' => $result]; -} -``` - -### Add WordPress Hook - -**Action hook (notify about event):** -```php -// Trigger action after entry save -do_action('sureforms_after_entry_save', $entry_id, $form_id, $entry_data); - -// Other plugins/themes can listen: -add_action('sureforms_after_entry_save', function($entry_id, $form_id, $data) { - // Custom logic -}, 10, 3); -``` - -**Filter hook (modify data):** -```php -// Allow filtering of email subject -$subject = apply_filters('sureforms_email_subject', $subject, $form_id, $entry_data); - -// Other plugins/themes can modify: -add_filter('sureforms_email_subject', function($subject, $form_id, $data) { - return "[Form $form_id] $subject"; -}, 10, 3); -``` - -### Database Operations - -**Insert entry:** -```php -use SRFM\Inc\Database\Tables\Entries; - -$entry_id = Entries::insert([ - 'form_id' => 123, - 'user_id' => get_current_user_id(), - 'status' => 'published', - 'entry_data' => wp_json_encode($form_data) -]); -``` - -**Update entry:** -```php -Entries::update($entry_id, [ - 'status' => 'spam' -]); -``` - -**Delete entry:** -```php -Entries::delete($entry_id); -``` - -**Query entries:** -```php -$entries = Entries::get_all([ - 'where' => [ - ['key' => 'form_id', 'value' => 123, 'compare' => '='] - ], - 'limit' => 20, - 'offset' => 0 -]); -``` - ---- - -## Debugging Tips - -### Enable Debug Mode - -**wp-config.php:** -```php -define('WP_DEBUG', true); -define('WP_DEBUG_LOG', true); -define('WP_DEBUG_DISPLAY', false); -define('SCRIPT_DEBUG', true); -define('SAVEQUERIES', true); -``` - -### Check Debug Log - -```bash -# Real-time log monitoring -tail -f wp-content/debug.log - -# Search for errors -grep "SureForms" wp-content/debug.log -``` - -### JavaScript Debugging - -**Browser console:** -```javascript -// Check global object -console.log(window.sureforms); - -// Check block attributes -wp.data.select('core/block-editor').getBlocks(); - -// Check form data -document.querySelector('form').addEventListener('submit', (e) => { - console.log('Form data:', new FormData(e.target)); -}); -``` - -### Database Queries - -**Install Query Monitor plugin:** -```bash -wp plugin install query-monitor --activate -``` - -**Check slow queries:** -- Visit any admin page -- Click "Query Monitor" in admin bar -- View "Queries" tab -- Sort by time -- Optimize slow queries - -**Manual query inspection:** -```php -// Add to code temporarily -global $wpdb; -$wpdb->show_errors(); -$wpdb->print_error(); -``` - -### AJAX Debugging - -**Check network tab:** -1. Open DevTools → Network -2. Filter: XHR -3. Submit form -4. Click request -5. Check Response tab for errors - -**Add debug output:** -```php -public function handle_ajax() { - error_log('AJAX data: ' . print_r($_POST, true)); - // ... rest of code -} -``` - ---- - -## Resources - -### Internal Documentation -- [README.md](README.md) - Quick start -- [architecture.md](architecture.md) - System design -- [codebase-map.md](codebase-map.md) - File structure -- [apis.md](apis.md) - API reference -- [coding-standards.md](coding-standards.md) - Code standards -- [ai-agent-guide.md](ai-agent-guide.md) - AI agent guidance -- [troubleshooting.md](troubleshooting.md) - Common problems - -### External Resources -- **WordPress Codex:** https://codex.wordpress.org/ -- **Block Editor Handbook:** https://developer.wordpress.org/block-editor/ -- **REST API Handbook:** https://developer.wordpress.org/rest-api/ -- **React Docs:** https://react.dev/ -- **WP-CLI:** https://wp-cli.org/ - -### Community -- **GitHub Issues:** Report bugs, request features -- **Facebook Group:** https://www.facebook.com/groups/surecart -- **Support:** https://support.brainstormforce.com/ - ---- - -## Next Steps - -After completing onboarding: - -1. **Pick a starter issue:** - - Look for "good first issue" label on GitHub - - Start with documentation or small bug fixes - - Work up to features - -2. **Read code:** - - Pick a feature you use - - Trace it from UI to database - - Understand every line - -3. **Write tests:** - - For code you touch - - Increases confidence - - Helps others understand your changes - -4. **Ask questions:** - - Don't hesitate to ask - - Better to ask than assume - - Document answers for others - -**Welcome to the team!** 🚀 diff --git a/internal-docs/product-vision.md b/internal-docs/product-vision.md deleted file mode 100644 index f41f1951c..000000000 --- a/internal-docs/product-vision.md +++ /dev/null @@ -1,688 +0,0 @@ -# Product Vision - -**Version:** 2.5.0 - ---- - -## Mission - -**Empower anyone to build beautiful, high-converting forms without code.** - -SureForms exists to solve a fundamental problem: creating forms on WordPress is unnecessarily complex, resulting in ugly forms that hurt conversion rates. - ---- - -## The Problem We Solve - -### Pain Points (Before SureForms) - -**1. Complexity** -- Learning curve: New interfaces, proprietary builders -- Time waste: Hours configuring simple contact forms -- Technical barriers: Requires developer for custom fields - -**2. Design Limitations** -- Generic templates that don't match site design -- Limited styling options without CSS knowledge -- Mobile responsiveness as afterthought - -**3. Low Engagement** -- Long, intimidating single-page forms -- No personalization or conditional logic -- High abandonment rates (avg 67% for long forms) - -**4. Spam & Security** -- Constant bot submissions -- Security vulnerabilities in popular plugins -- GDPR compliance challenges - -**5. Integration Friction** -- Payment processing requires multiple plugins -- Manual data export/import to CRMs -- Webhook setup requires developer knowledge - ---- - -## Our Solution - -### Core Differentiators - -**1. Native WordPress (Gutenberg)** -- **Why:** Users already know the interface -- **Benefit:** Zero learning curve, instant productivity -- **Technical:** React blocks, no proprietary builder - -**2. AI-Powered Form Building** -- **Innovation:** First AI form builder for WordPress -- **How:** Natural language → complete functional form in seconds -- **Examples:** - - "Create a job application form" → 12-field form with file upload - - "Simple contact form" → 3 fields, perfectly styled - - "Event RSVP with dietary restrictions" → Conditional logic auto-configured - -**3. Built-in Payments** -- **Why:** No WooCommerce, no add-ons required -- **Gateways:** Stripe (Free), PayPal (Pro) -- **Features:** One-time, subscriptions, custom amounts -- **Security:** PCI-compliant, encrypted credentials - -**4. Mobile-First Design** -- **Approach:** Responsive by default, not opt-in -- **Testing:** Every block tested on iOS/Android -- **Performance:** Fast load times on 3G networks - -**5. Engagement Features (Pro)** -- **Conversational Forms:** Chat-like, one question at a time -- **Multi-Step Forms:** Break long forms into digestible steps -- **Conditional Logic:** Show/hide based on answers -- **Result:** 3x higher completion rates vs single-page forms - ---- - -## Target Users - -### Primary Personas - -#### 1. Website Owner (40% of users) -**Profile:** -- Small business owner or solopreneur -- Limited technical skills -- DIY mentality -- Budget-conscious - -**Needs:** -- Contact forms, quote requests, bookings -- Easy setup (< 10 minutes) -- Professional appearance -- Spam protection - -**Pain Points:** -- Frustrated with complex form builders -- Can't afford developer -- Generic templates don't match brand - -**How SureForms Helps:** -- AI creates form in 30 seconds -- Instant Form feature (no embedding needed) -- Built-in anti-spam (reCAPTCHA, Honeypot) -- Modern, customizable design - ---- - -#### 2. WordPress Designer (30% of users) -**Profile:** -- Freelancer or agency designer -- Strong design skills, basic code knowledge -- Builds sites for clients -- Values aesthetics and UX - -**Needs:** -- Forms that match site design -- Custom styling without CSS -- Fast deployment -- Client-friendly interface - -**Pain Points:** -- Form plugins look "generic" -- CSS overrides are tedious -- Clients can't update forms themselves -- Other plugins don't use Gutenberg - -**How SureForms Helps:** -- Gutenberg-native (feels like page building) -- Extensive styling options in UI -- Client can edit without breaking design -- Block patterns for reusability - ---- - -#### 3. WordPress Developer (20% of users) -**Profile:** -- Full-stack developer -- Builds custom WordPress solutions -- Values clean code and extensibility -- Performance-conscious - -**Needs:** -- Developer-friendly APIs -- Hooks and filters -- Custom field types -- Database access -- Integration capabilities - -**Pain Points:** -- Other plugins have messy codebases -- Limited extensibility -- Poor documentation -- Performance issues (N+1 queries, bloat) - -**How SureForms Helps:** -- Clean, modern codebase (PSR-12-inspired) -- Extensive hooks: 50+ actions/filters -- Well-documented APIs -- Optimized queries (custom tables, not post meta) -- GitHub access for contributions - ---- - -#### 4. E-commerce Store Owner (10% of users) -**Profile:** -- Runs WooCommerce or custom store -- Needs payment forms (not full checkout) -- Sells services, memberships, donations -- Wants simplicity - -**Needs:** -- Accept payments without WooCommerce -- Subscription billing -- Custom pricing fields -- Receipt emails - -**Pain Points:** -- WooCommerce is overkill for simple payments -- Other payment forms require add-ons -- Can't customize payment flow -- Poor mobile checkout experience - -**How SureForms Helps:** -- Built-in Stripe/PayPal (no plugins) -- Subscription support included -- Custom amount fields (donations, tips) -- Mobile-optimized payment UI - ---- - -## Feature Philosophy - -### Design Principles - -**1. Simplicity Over Features** -- Don't add features just because competitors have them -- Every feature must solve real user problem -- Hide complexity behind smart defaults - -**Example:** -- ❌ Bad: 50 font options overwhelming users -- ✅ Good: 5 curated fonts + custom font option - -**2. Progressive Disclosure** -- Show basic options first -- Advanced settings collapsed by default -- Help text on hover, not always visible - -**Example:** -- Default form settings: 4 essential options -- Advanced panel: 15+ options (collapsed) - -**3. Convention Over Configuration** -- Smart defaults based on common use cases -- Zero config for 80% of users -- Power users can customize - -**Example:** -- Email notification auto-configured with sensible template -- User can override if needed - -**4. Performance First** -- Lazy load non-critical assets -- Minimize HTTP requests -- Database queries optimized (no N+1) -- CSS/JS minified and cached - -**Metrics:** -- Page load impact: < 50KB additional assets -- Time to Interactive: < 2 seconds on 3G -- Database queries: Max 5 per form render - -**5. Accessibility Built-In** -- WCAG 2.1 Level AA compliance -- Keyboard navigation -- Screen reader support -- Focus indicators - -**Testing:** -- Every block tested with NVDA/JAWS -- Keyboard-only testing required -- Color contrast checked - ---- - -## Free vs Pro Strategy - -### Free Plugin (Core Experience) - -**Philosophy:** Full-featured, not crippled trial - -**Included:** -- Unlimited forms -- Unlimited submissions -- All 15+ field types -- Email notifications -- Spam protection (reCAPTCHA, Honeypot) -- Stripe payments (one-time, subscriptions) -- Form analytics -- Export entries (CSV) -- GDPR compliance -- Instant Forms - -**Limitations:** -- No PayPal -- No native integrations (Mailchimp, etc.) -- No conditional logic -- No multi-step/conversational forms -- No user registration -- No PDF generation - -**Goal:** Provide genuine value, build trust, convert 5-10% to Pro - ---- - -### Pro Plugin (Power Features) - -**Philosophy:** Advanced features for serious users - -**Added Value:** -- **Payments:** PayPal (one-time, subscriptions) -- **Integrations:** 24+ native (Mailchimp, Brevo, HubSpot, Salesforce, etc.) -- **Logic:** Conditional show/hide, calculations -- **Forms:** Multi-step, conversational, save & resume -- **Users:** Registration, login, password reset -- **Output:** PDF generation from submissions -- **Advanced Fields:** Upload (images, files), signature, calculator -- **Priority Support:** < 24hr response time - -**Pricing Tiers:** -- **Essential:** $99/year (3 sites) -- **Plus:** $199/year (20 sites) -- **Agency:** $299/year (unlimited sites) - -**Conversion Strategy:** -- Free users see "Pro" badge on locked features -- No nag screens or popups -- Upgrade CTA in logical places (when user needs feature) -- 14-day money-back guarantee - ---- - -## Competitive Landscape - -### Direct Competitors - -**1. Gravity Forms** -- **Strengths:** Mature, extensive add-ons, trusted -- **Weaknesses:** Old UI, not Gutenberg-native, expensive -- **Our Advantage:** Modern interface, AI builder, lower cost - -**2. WPForms** -- **Strengths:** Beginner-friendly, drag-and-drop -- **Weaknesses:** Generic designs, limited styling -- **Our Advantage:** Better design flexibility, Gutenberg-native - -**3. Formidable Forms** -- **Strengths:** Advanced features, views/reporting -- **Weaknesses:** Steep learning curve, performance issues -- **Our Advantage:** Simpler, faster, AI-assisted - -**4. Fluent Forms** -- **Strengths:** Conversational forms, modern UI -- **Weaknesses:** Not Gutenberg-native, separate builder -- **Our Advantage:** Native Gutenberg, simpler UX - -**5. Contact Form 7** -- **Strengths:** Free, lightweight, popular -- **Weaknesses:** No UI, requires shortcodes, ugly default styles -- **Our Advantage:** Visual builder, beautiful defaults, AI - ---- - -### Market Positioning - -**SureForms:** The AI-powered Gutenberg form builder - -**Tagline:** "Beautiful forms without code" - -**Positioning Statement:** -> For WordPress users who want high-converting forms without complexity, SureForms is the AI-powered form builder that creates beautiful, mobile-first forms in seconds—unlike outdated plugins that require hours of configuration and result in generic-looking forms. - -**Why Users Choose SureForms:** -1. **Speed:** AI creates forms in 30 seconds vs 30 minutes -2. **Design:** Modern, mobile-first vs generic templates -3. **Ease:** Gutenberg-native vs proprietary builder -4. **Value:** Built-in payments vs add-ons required - ---- - -## Product Roadmap - -### Current Focus (2026 Q1) - -**Theme:** Stability & Performance - -**Priorities:** -1. Bug fixes from user reports -2. Performance optimization (lazy loading, query optimization) -3. Security hardening (code audit, penetration testing) -4. Documentation improvements -5. Accessibility compliance (WCAG 2.2 Level AA) - ---- - -### Near-Term (2026 Q2-Q3) - -**Theme:** Advanced Features & Integrations - -**Planned:** -1. **More Integrations:** - - Google Sheets (native, no Zapier) - - Airtable - - Notion - - Slack (native) - -2. **Field Types:** - - Star rating - - Matrix/grid (Pro) - - File upload enhancements (drag & drop, preview) - -3. **Conditional Logic Enhancements (Pro):** - - Show/hide blocks (not just fields) - - Calculation fields (price quotes, BMI calculators) - - Conditional email notifications - -4. **Analytics:** - - Conversion funnel visualization - - A/B testing (form variations) - - Heatmaps (where users drop off) - -5. **Templates:** - - 100+ pre-built form templates - - Industry-specific (real estate, healthcare, education) - - Import/export custom templates - ---- - -### Long-Term Vision (2027+) - -**Theme:** AI-Driven Personalization - -**Research Areas:** -1. **AI Form Optimization:** - - Auto-suggest field improvements based on completion rates - - Predictive text for common fields - - Smart field ordering (ML-driven) - -2. **Advanced Conversational Forms:** - - Voice input support - - Natural language processing for responses - - Branching logic based on sentiment - -3. **Global Expansion:** - - Multi-language support (WPML, Polylang) - - Currency localization - - Regional compliance (CCPA, PIPEDA, etc.) - -4. **Enterprise Features:** - - Team collaboration (roles, permissions) - - Advanced approval workflows - - Audit logs - - White-label options - -5. **Mobile App:** - - iOS/Android app for managing entries - - Push notifications for new submissions - - Offline form viewing - ---- - -## Success Metrics - -### North Star Metric -**Active Forms:** Number of forms receiving at least 1 submission per month - -**Why:** Indicates genuine usage, not just installs - -**Target:** 100,000 active forms by end of 2026 - ---- - -### Secondary Metrics - -**Growth:** -- New installations per month: 50,000+ -- Activation rate: 60% (user creates first form within 7 days) -- Retention: 80% still active after 30 days - -**Engagement:** -- Forms created per user: 3.5 average -- Submissions per form: 25/month average -- Feature adoption (Pro): 40% use conditional logic - -**Conversion:** -- Free → Pro conversion: 5-7% -- Trial → Paid: 25% -- Annual renewal rate: 85% - -**Quality:** -- Support ticket volume: < 2% of active users -- Bug reports: < 0.5% of installations -- 4.5+ star rating on WordPress.org - -**Performance:** -- Average page load impact: < 40KB -- Time to first form submission: < 5 minutes (from install) -- Support response time: < 12 hours - ---- - -## User Feedback Integration - -### How We Listen - -**1. Support Tickets** -- Every ticket tagged by topic -- Monthly review of common issues -- Feature requests tracked in GitHub - -**2. User Surveys** -- Annual user satisfaction survey -- Post-purchase survey (Pro users) -- Exit survey (churned Pro users) - -**3. Analytics** -- Feature usage tracking (opt-in) -- Error logging (anonymized) -- Performance metrics - -**4. Community** -- Facebook group discussions -- GitHub issues and discussions -- WordPress.org support forum - -**5. Direct Outreach** -- User interviews (quarterly) -- Beta tester program -- Power user advisory board - ---- - -### Decision Framework - -**Feature Requests Evaluation:** - -**Criteria:** -1. **Impact:** How many users need this? (1-10) -2. **Effort:** Development complexity? (1-10) -3. **Strategic Fit:** Aligns with vision? (Yes/No) -4. **Competitive:** Do competitors have it? (Yes/No) -5. **Revenue:** Drives conversions? (Yes/No) - -**Scoring:** -- Impact / Effort = Priority score -- Strategic Fit = multiplier (2x if yes) -- Build if score > 5 - -**Example:** -- Feature: "Drag & drop file upload" -- Impact: 8 (many requests) -- Effort: 4 (moderate complexity) -- Strategic Fit: Yes (better UX) -- Score: (8/4) × 2 = 4 → **Build later** - ---- - -## Brand & Voice - -### Brand Personality - -**Adjectives:** -- Approachable (not intimidating) -- Modern (not trendy) -- Reliable (not boring) -- Empowering (not condescending) - -**Tone:** -- Friendly but professional -- Clear over clever -- Helpful without being pushy -- Honest about limitations - ---- - -### Writing Guidelines - -**Do:** -- Use "you" (conversational) -- Short sentences -- Active voice -- Explain "why" not just "how" - -**Don't:** -- Jargon without explanation -- Marketing fluff ("revolutionary", "game-changing") -- Passive voice ("the form was created") -- Unnecessary exclamation marks!!!! - -**Examples:** - -❌ Bad: "SureForms revolutionizes form building with cutting-edge AI technology!" - -✅ Good: "SureForms uses AI to create forms in seconds. Describe what you need, and we'll build it." - ---- - -❌ Bad: "The form submission process has been optimized for maximum conversion potential." - -✅ Good: "We designed our forms to load fast and look great on mobile, so more people complete them." - ---- - -## Technical Vision - -### Architecture Goals - -**1. Performance** -- Custom database tables (not post meta) -- Lazy loading for non-critical assets -- Query optimization (no N+1) -- CDN-friendly (static assets versioned) - -**2. Scalability** -- Handle 100,000+ submissions per form -- Efficient database queries (indexed columns) -- Background processing for heavy tasks (webhooks, PDFs) -- Caching strategy (transients, object cache) - -**3. Security** -- Input sanitization (all user data) -- Output escaping (all rendered content) -- Nonce verification (all AJAX/REST) -- SQL injection prevention (prepared statements) -- Regular security audits - -**4. Extensibility** -- 50+ hooks (actions and filters) -- Clean, documented APIs -- Developer-friendly codebase -- Backward compatibility promise - -**5. Maintainability** -- WordPress coding standards (PHP_CodeSniffer) -- ESLint for JavaScript -- Automated testing (PHPUnit, Playwright) -- Clear documentation (inline and external) - ---- - -## Values & Principles - -**1. User Privacy** -- No tracking without consent -- GDPR compliance built-in -- Data portability (easy export) -- Clear privacy policy - -**2. Open Source (Free Plugin)** -- Public GitHub repository -- Accept community contributions -- Transparent roadmap -- Active maintenance - -**3. Quality Over Speed** -- Test thoroughly before release -- Fix bugs before adding features -- Code review required for all changes -- No "move fast and break things" - -**4. Accessibility** -- WCAG 2.1 Level AA minimum -- Keyboard navigation always -- Screen reader testing required -- Color contrast compliance - -**5. Sustainability** -- Reasonable pricing (not subscription trap) -- Long-term support commitment -- No vendor lock-in (data always exportable) -- Transparent upgrade policies - ---- - -## What We Won't Do - -**Out of Scope:** - -**1. Full CRM System** -- Why: Bloat, complexity, competing with specialists -- Alternative: Integrate with existing CRMs (HubSpot, Salesforce) - -**2. Email Marketing Platform** -- Why: Already solved by Mailchimp, Brevo, etc. -- Alternative: Native integrations with 24+ email platforms - -**3. Complete E-commerce Solution** -- Why: WooCommerce exists, does it well -- Alternative: Simple payment forms (our niche) - -**4. Website Builder** -- Why: Outside core competency (forms) -- Alternative: Work seamlessly with any page builder - -**5. Survey & Quiz Builder** -- Why: Different use case, different UX requirements -- Alternative: Forms work for simple surveys - ---- - -## Conclusion - -**Core Belief:** -Forms are critical touchpoints between businesses and customers. They should be beautiful, fast, and easy to create. - -**Our Promise:** -We'll keep building tools that make form creation accessible to everyone, without sacrificing power or flexibility. - -**For Developers:** -Your feedback shapes this product. Keep the issues and PRs coming. We're listening. - ---- - -**Next:** [UI & Copy Guidelines](ui-and-copy.md) diff --git a/internal-docs/troubleshooting.md b/internal-docs/troubleshooting.md deleted file mode 100644 index b7f256af5..000000000 --- a/internal-docs/troubleshooting.md +++ /dev/null @@ -1,1031 +0,0 @@ -# Troubleshooting Guide - -**Version:** 2.5.0 - ---- - -## Quick Diagnostic - -**Before diving deep, check these common issues:** - -```bash -# 1. WordPress & PHP versions -wp core version -php -v - -# 2. Plugin status -wp plugin list | grep sureforms - -# 3. Theme compatibility -wp theme list --status=active - -# 4. Recent errors -tail -50 wp-content/debug.log | grep -i "sureforms\|fatal\|error" - -# 5. Database tables exist -wp db query "SHOW TABLES LIKE '%sureforms%';" -``` - ---- - -## Installation & Activation Issues - -### Plugin Won't Activate - -**Symptom:** "Plugin activation failed" or white screen - -**Common Causes:** - -**1. PHP Version Too Old** -```bash -php -v -# Required: PHP 7.4+ -``` - -**Fix:** -```bash -# Update PHP (contact host if shared hosting) -# Or add to wp-config.php temporarily to see error: -define('WP_DEBUG', true); -define('WP_DEBUG_DISPLAY', true); -``` - -**2. WordPress Version Too Old** -```bash -wp core version -# Required: WordPress 6.4+ -``` - -**Fix:** -```bash -wp core update -``` - -**3. Conflicting Plugin** - -**Check for conflicts:** -```bash -# Deactivate all other plugins -wp plugin deactivate --all --exclude=sureforms,sureforms-pro - -# Try activating SureForms -wp plugin activate sureforms - -# Reactivate plugins one by one -wp plugin activate plugin-name -``` - -**Common conflicts:** -- Old caching plugins (W3 Total Cache < 2.0) -- Security plugins with aggressive rules -- Other form builders (namespace collisions) - -**4. Memory Limit Too Low** - -**Check current limit:** -```bash -wp eval 'echo WP_MEMORY_LIMIT;' -``` - -**Fix (wp-config.php):** -```php -define('WP_MEMORY_LIMIT', '256M'); -``` - ---- - -### Pro Plugin Shows "Base Plugin Required" - -**Symptom:** SureForms Pro won't activate - -**Diagnosis:** -```bash -wp plugin list | grep sureforms -# Ensure both sureforms AND sureforms-pro are installed -``` - -**Fix:** -1. Install SureForms Free first -2. Activate SureForms Free -3. Then activate SureForms Pro - -**Check version compatibility:** -```bash -# Free and Pro versions should match -# Both should be 2.5.0 (or same major.minor) -``` - ---- - -### Database Tables Not Created - -**Symptom:** Form submissions fail, entries not showing - -**Check tables exist:** -```bash -wp db query "SHOW TABLES LIKE '%sureforms%';" -``` - -**Expected output:** -``` -wp_sureforms_entries -wp_sureforms_payments -wp_sureforms_integrations -wp_sureforms_save_resume -``` - -**Fix: Force database creation** -```bash -# Deactivate plugin -wp plugin deactivate sureforms sureforms-pro - -# Delete plugin (backup first!) -wp plugin delete sureforms - -# Reinstall -wp plugin install sureforms --activate - -# Tables should be created on activation -``` - -**Manual creation (last resort):** - -Read schema from: -- `inc/database/tables/entries.php` -- `inc/database/tables/payments.php` - -Run CREATE TABLE statements manually. - ---- - -## Form Builder (Editor) Issues - -### Forms Won't Load in Editor - -**Symptom:** Blank screen or infinite loading spinner - -**Diagnosis:** - -**1. Check browser console:** -``` -Open DevTools → Console -Look for JavaScript errors -``` - -**Common errors:** - -**a) "wp.blockEditor is undefined"** -- **Cause:** Outdated WordPress version -- **Fix:** `wp core update` - -**b) "React version mismatch"** -- **Cause:** Another plugin using old React version -- **Fix:** Deactivate other plugins one by one to find conflict - -**c) "Uncaught SyntaxError"** -- **Cause:** JavaScript file corrupted or not minified correctly -- **Fix:** Rebuild assets: `npm run build` in plugin directory - -**2. Check network tab:** -``` -DevTools → Network -Filter: JS -Look for failed requests (red, 404, 500) -``` - -**Common failures:** -- `form-editor.js` (404) → Rebuild assets -- `chunk-vendors.js` (500) → Server misconfiguration - -**3. Enable SCRIPT_DEBUG:** - -In `wp-config.php`: -```php -define('SCRIPT_DEBUG', true); -``` - -This loads unminified JS, easier to debug. - ---- - -### Blocks Not Appearing in Inserter - -**Symptom:** SureForms blocks missing from block library - -**Diagnosis:** -```bash -# Check if blocks are registered -wp eval "print_r(get_option('srfm_blocks_registration'));" -``` - -**Fixes:** - -**1. Re-register blocks:** -```bash -wp plugin deactivate sureforms -wp plugin activate sureforms -``` - -**2. Clear block cache:** -```bash -wp cache flush -wp transient delete --all -``` - -**3. Check block.json files exist:** -```bash -find wp-content/plugins/sureforms/inc/blocks -name "block.json" -# Should return multiple files -``` - ---- - -### Form Preview Shows "Invalid Block" - -**Symptom:** Form renders as "This block contains unexpected or invalid content" - -**Cause:** Block HTML structure changed, saved form has old structure - -**Fix:** - -**Option 1: Update block (preserves data):** -1. Click "Attempt Block Recovery" -2. Verify content looks correct -3. Update form - -**Option 2: Clear and rebuild:** -1. Delete the invalid block -2. Add fresh block -3. Reconfigure settings - -**Prevention:** -- Always test after plugin updates -- Keep staging environment for testing - ---- - -## Form Submission Issues - -### Form Submissions Not Saving - -**Symptom:** User submits form, success message shows, but no entry in database - -**Diagnosis:** - -**1. Check debug log:** -```bash -tail -f wp-content/debug.log -# Submit form while watching log -``` - -**Common errors:** -- "Database insert failed" → Check table permissions -- "Nonce verification failed" → Caching issue (see below) -- "Call to undefined method" → PHP version or missing dependency - -**2. Check database directly:** -```bash -# Get latest entry -wp db query "SELECT * FROM wp_sureforms_entries ORDER BY id DESC LIMIT 1;" -``` - -**3. Test with minimal form:** -Create form with ONLY: -- Email field -- Submit button - -If this works, issue is with specific field type. - -**Fixes:** - -**Database Permissions:** -```bash -# Check MySQL user has INSERT privilege -wp db query "SHOW GRANTS;" -``` - -**Fix permissions (MySQL):** -```sql -GRANT INSERT, UPDATE, DELETE ON database_name.* TO 'wp_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -**Caching Interference:** - -Caching plugins cache nonce values, causing verification failures. - -**Fix:** -1. Exclude `/wp-admin/admin-ajax.php` from cache -2. Exclude REST API `/wp-json/` from cache -3. Or disable caching temporarily to test - ---- - -### Email Notifications Not Sending - -**Symptom:** Form submitted successfully, but no email received - -**Diagnosis:** - -**1. Check if emails are being sent at all:** -```bash -# Install WP Mail SMTP or similar -wp plugin install wp-mail-smtp --activate - -# Or test with simple command: -wp eval "wp_mail('your@email.com', 'Test', 'Testing SureForms');" -``` - -**2. Check SureForms email settings:** -```bash -# View form meta -wp post meta list | grep email -``` - -**3. Check spam folder** -- WordPress default `from` address: `wordpress@yourdomain.com` -- Often flagged as spam - -**Fixes:** - -**Configure SMTP:** - -Install WP Mail SMTP plugin: -```bash -wp plugin install wp-mail-smtp --activate -``` - -Configure with: -- Gmail -- SendGrid -- Mailgun -- Amazon SES - -**Check email template:** - -In form settings → Email Notification: -- Verify "To" address is correct -- Check "From" address is valid domain -- Test with simple subject/message - -**Server mail() function:** - -Some hosts disable PHP `mail()` function. - -Test: -```bash -php -r "mail('test@example.com', 'Test', 'Test message');" -``` - -If no email received, `mail()` is disabled. Use SMTP. - ---- - -### Form Validation Not Working - -**Symptom:** Form submits even with empty required fields - -**Causes:** - -**1. JavaScript disabled** (frontend validation skipped) -**2. Theme CSS hiding error messages** -**3. Custom JavaScript conflict** - -**Diagnosis:** - -**Check browser console:** -``` -Look for JavaScript errors -Check if srfm-validation.js loaded -``` - -**Test with default theme:** -```bash -wp theme activate twentytwentythree -# Submit form again -``` - -If works with default theme → Theme conflict. - -**Fixes:** - -**Theme conflict:** - -Add to theme's `functions.php`: -```php -add_action('wp_enqueue_scripts', function() { - // Ensure SureForms scripts load - wp_enqueue_script('srfm-frontend'); -}, 20); -``` - -**CSS hiding errors:** - -Check if theme has: -```css -.srfm-error { display: none !important; } -``` - -Remove or override. - ---- - -## Payment Processing Issues - -### Stripe Payments Failing - -**Symptom:** "Payment failed" error after entering card details - -**Diagnosis:** - -**1. Check Stripe API keys:** -```bash -# In WordPress admin: SureForms → Settings → Payments → Stripe -# Verify: -# - Using correct keys (test vs live) -# - Keys match Stripe dashboard -``` - -**2. Check Stripe webhook:** -```bash -# In Stripe Dashboard → Developers → Webhooks -# Verify webhook URL is: -https://yoursite.com/wp-json/sureforms/webhook_test - -# Check recent webhook deliveries for errors -``` - -**3. Check browser console:** -``` -DevTools → Console -Look for Stripe.js errors -``` - -**Common Errors:** - -**"Invalid API Key"** -- Using test key in live mode (or vice versa) -- API key revoked in Stripe dashboard -- Fix: Copy fresh keys from Stripe - -**"Payment Intent creation failed"** -- Amount is $0 or negative -- Currency mismatch -- Fix: Check form configuration, amount field - -**"Webhook signature verification failed"** -- Webhook secret incorrect -- Middleware issue -- Fix: Copy webhook signing secret from Stripe, update settings - -**Fixes:** - -**Test mode checklist:** -```bash -# 1. Use test API keys (starts with pk_test_ / sk_test_) -# 2. Test card: 4242 4242 4242 4242 -# 3. Any future expiry date -# 4. Any 3-digit CVC -``` - -**Live mode checklist:** -```bash -# 1. Use live API keys (starts with pk_live_ / sk_live_) -# 2. SSL certificate valid (https://) -# 3. Webhook verified in Stripe dashboard -# 4. Test with real card (refund immediately) -``` - ---- - -### PayPal Payments Failing (Pro) - -**Symptom:** Redirected to PayPal but payment doesn't process - -**Diagnosis:** - -**Check PayPal credentials:** -```bash -# SureForms → Settings → Payments → PayPal -# Verify: -# - Client ID matches PayPal dashboard -# - Secret matches -# - Using sandbox for testing, live for production -``` - -**Check webhook endpoint:** -```bash -# PayPal Dashboard → Apps & Credentials → Webhooks -# Webhook URL should be: -https://yoursite.com/wp-json/sureforms-pro/paypal-live-webhook -``` - -**Fixes:** - -**Test in sandbox mode first:** -1. Create PayPal sandbox account: https://developer.paypal.com/ -2. Use sandbox credentials in SureForms -3. Test with sandbox buyer account - -**Common issues:** -- **Wrong environment:** Using sandbox credentials in live mode -- **Webhook not subscribed:** Must subscribe to payment events in PayPal dashboard -- **SSL certificate:** PayPal requires valid HTTPS - ---- - -## Integration Issues (Pro) - -### Native Integration Not Connecting - -**Symptom:** "Connection failed" when adding integration (Mailchimp, HubSpot, etc.) - -**Diagnosis:** - -**1. Check OAuth redirect URL:** - -For OAuth integrations (Mailchimp, HubSpot, Salesforce): -``` -Redirect URL must be: -https://yoursite.com/wp-json/sureforms-pro/v1/oauth/callback -``` - -**2. Test API credentials:** - -For API key integrations (Brevo, etc.): -```bash -# Test API key directly -curl -X GET "https://api.brevo.com/v3/account" \ - -H "api-key: YOUR_API_KEY" -``` - -Should return account info, not error. - -**3. Check error logs:** -```bash -tail -f wp-content/debug.log | grep -i "integration\|oauth" -``` - -**Fixes:** - -**OAuth issues:** -- Ensure site uses HTTPS (required for OAuth) -- Whitelist redirect URL in service's developer console -- Check OAuth app has correct permissions/scopes - -**API key issues:** -- Regenerate key in service dashboard -- Copy entire key (no spaces, no quotes) -- Check key has required permissions (read/write) - ---- - -### Webhook Not Firing - -**Symptom:** Form submitted, but data not sent to integrated service - -**Diagnosis:** - -**Check webhook logs:** - -Install Query Monitor plugin: -```bash -wp plugin install query-monitor --activate -``` - -Submit form, check "HTTP API Calls" panel for webhook requests. - -**Manually trigger webhook:** -```bash -# Find integration ID -wp db query "SELECT * FROM wp_sureforms_integrations;" - -# Trigger webhook manually (developer test) -wp eval "do_action('sureforms_after_entry_save', 123, 456, []);" -``` - -**Fixes:** - -**Webhook URL validation:** - -In `inc/pro/integrations/webhooks.php`, ensure URL is valid: -- HTTPS only (no HTTP) -- No localhost (unless testing) -- Responds with 200 OK - -**Timeout issues:** - -Increase timeout in `wp-config.php`: -```php -define('WP_HTTP_BLOCK_EXTERNAL', false); -define('WP_ACCESSIBLE_HOSTS', 'api.mailchimp.com,api.hubspot.com'); -``` - ---- - -## Performance Issues - -### Form Editor Slow to Load - -**Symptom:** Takes 10+ seconds to load form in editor - -**Diagnosis:** - -**1. Check database size:** -```bash -wp db query "SELECT COUNT(*) FROM wp_sureforms_entries;" -# If > 100,000 entries, database may be slow -``` - -**2. Check server resources:** -```bash -# Memory usage -free -h - -# CPU usage -top - -# Disk I/O -iostat -``` - -**3. Profile with Query Monitor:** -```bash -wp plugin install query-monitor --activate -# Open form editor, check QM panel for slow queries -``` - -**Fixes:** - -**Optimize database:** -```bash -# Clean old entries (backup first!) -wp db query "DELETE FROM wp_sureforms_entries WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);" - -# Optimize tables -wp db optimize -``` - -**Increase PHP limits:** - -In `php.ini` or `.htaccess`: -``` -max_execution_time = 300 -memory_limit = 256M -``` - -**Enable object caching:** - -Install Redis or Memcached: -```bash -wp plugin install redis-cache --activate -wp redis enable -``` - ---- - -### Frontend Form Loads Slowly - -**Symptom:** Form takes 5+ seconds to appear on page - -**Diagnosis:** - -**Check asset loading:** -``` -DevTools → Network → Slow 3G simulation -Watch which assets are slow -``` - -**Common bottlenecks:** -- Google Fonts loading slowly -- Large CSS/JS files -- Unoptimized images in form - -**Fixes:** - -**Lazy load non-critical assets:** - -In `functions.php`: -```php -add_filter('script_loader_tag', function($tag, $handle) { - if ($handle === 'srfm-frontend') { - return str_replace(' src', ' defer src', $tag); - } - return $tag; -}, 10, 2); -``` - -**Use CDN for Google Fonts:** - -In form settings → Design → Typography: -- Limit to 1-2 font families -- Use system fonts for faster load (e.g., -apple-system) - -**Minify and combine assets:** -```bash -npm run build # Ensures assets are minified -``` - -**Enable caching:** - -Install caching plugin: -```bash -wp plugin install wp-super-cache --activate -``` - -Configure to cache pages with forms. - ---- - -## Block Compatibility Issues - -### Form Breaks After Theme Update - -**Symptom:** Form displays incorrectly or not at all after theme update - -**Diagnosis:** - -**Compare theme CSS:** - -Check if new theme has conflicting styles: -```css -/* Common conflicts */ -.srfm-form input { /* Theme override */ } -.srfm-field { /* Theme override */ } -``` - -**Test with default theme:** -```bash -wp theme activate twentytwentythree -``` - -If works → Theme issue. - -**Fixes:** - -**Add theme compatibility CSS:** - -In child theme `style.css`: -```css -/* Reset SureForms blocks */ -.srfm-form, -.srfm-field, -.srfm-form input, -.srfm-form textarea { - all: revert; -} - -/* Then apply minimal SureForms styles */ -``` - -**Use !important (last resort):** - -In SureForms settings → Custom CSS: -```css -.srfm-form input { - border: 1px solid #ccc !important; - padding: 10px !important; -} -``` - ---- - -### Conflicts with Page Builders - -**Symptom:** Forms don't work inside Elementor/Divi/Beaver Builder - -**Common Issues:** - -**1. JavaScript conflicts:** -- Page builder loads own jQuery version -- Conflicts with SureForms scripts - -**Fix:** -```php -// In functions.php -add_action('wp_enqueue_scripts', function() { - if (class_exists('Elementor\Plugin')) { - wp_dequeue_script('jquery'); - wp_enqueue_script('jquery'); - } -}, 100); -``` - -**2. CSS specificity:** -- Page builder CSS overrides SureForms - -**Fix:** - -Use SureForms settings → Custom CSS with higher specificity: -```css -.elementor-widget-container .srfm-form input { - /* Your styles */ -} -``` - ---- - -## Database & Query Issues - -### "Too many connections" Error - -**Symptom:** Site crashes during high form submission volume - -**Diagnosis:** -```bash -wp db query "SHOW STATUS LIKE 'max_used_connections';" -wp db query "SHOW VARIABLES LIKE 'max_connections';" -``` - -**Fixes:** - -**Increase max connections (MySQL):** - -In `my.cnf`: -``` -[mysqld] -max_connections = 500 -``` - -**Use persistent connections:** - -In `wp-config.php`: -```php -define('DB_CHARSET', 'utf8mb4'); -define('DB_COLLATE', ''); -define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_PERSISTENT); -``` - -**Add connection pooling:** - -Use ProxySQL or PgBouncer for connection pooling. - ---- - -### Slow Query: "SELECT * FROM wp_sureforms_entries" - -**Symptom:** Admin page very slow when viewing entries - -**Diagnosis:** -```bash -wp plugin install query-monitor --activate -# View entries page, check QM for slow queries -``` - -**Fixes:** - -**Add database indexes:** -```sql -ALTER TABLE wp_sureforms_entries -ADD INDEX idx_form_id (form_id), -ADD INDEX idx_created_at (created_at), -ADD INDEX idx_status (status); -``` - -**Limit entries displayed:** - -In admin, reduce entries per page from 100 to 20. - -**Paginate large result sets:** - -Ensure code uses `LIMIT` and `OFFSET`: -```php -$entries = Entries::get_all([ - 'limit' => 20, - 'offset' => ($page - 1) * 20 -]); -``` - ---- - -## WordPress.org Review Issues - -### Plugin Rejected for Security - -**Common reasons:** - -**1. Direct database calls without prepare():** -```php -// ❌ Bad -$wpdb->query("DELETE FROM table WHERE id = $id"); - -// ✅ Good -$wpdb->query($wpdb->prepare("DELETE FROM table WHERE id = %d", $id)); -``` - -**2. Unsanitized user input:** -```php -// ❌ Bad -echo $_POST['name']; - -// ✅ Good -echo esc_html(sanitize_text_field($_POST['name'] ?? '')); -``` - -**3. Missing nonce verification:** -```php -// ❌ Bad -if (isset($_POST['action'])) { do_action(); } - -// ✅ Good -if (isset($_POST['action']) && check_ajax_referer('my_action_nonce')) { - do_action(); -} -``` - -**Fix:** - -Run WordPress Coding Standards checker: -```bash -composer require --dev wp-coding-standards/wpcs -vendor/bin/phpcs --standard=WordPress inc/ -``` - -Fix all errors before resubmitting. - ---- - -## Getting Help - -### Before Asking for Help - -**Gather this information:** - -```bash -# 1. WordPress & PHP versions -wp core version -php -v - -# 2. Plugin version -wp plugin list | grep sureforms - -# 3. Active theme -wp theme list --status=active - -# 4. Other active plugins -wp plugin list --status=active - -# 5. Recent errors -tail -50 wp-content/debug.log - -# 6. Browser/OS (if frontend issue) -# Example: Chrome 120 on macOS 14 -``` - -### Where to Get Help - -**1. Documentation** (check first) -- This troubleshooting guide -- [FAQ](faq.md) -- [Architecture](architecture.md) - -**2. GitHub Issues** (bugs & feature requests) -- Search existing issues first -- Provide minimal reproduction steps -- Include system info from above - -**3. Support Portal** (Pro users) -- https://support.brainstormforce.com/ -- < 24hr response time for Pro users - -**4. Community** -- Facebook group: https://www.facebook.com/groups/surecart -- WordPress.org forum (Free only) - ---- - -## Still Stuck? - -If none of the above solutions work: - -**Create detailed bug report:** - -```markdown -**Environment:** -- WordPress: 6.4.2 -- PHP: 8.1 -- SureForms: 2.5.0 (Free/Pro) -- Theme: Astra 4.5.0 -- Browser: Chrome 120 - -**Steps to Reproduce:** -1. Create form with email field -2. Mark field as required -3. Submit form with empty email -4. [Describe unexpected behavior] - -**Expected:** Validation error shows -**Actual:** Form submits anyway - -**Debug Log:** -[Paste relevant errors from debug.log] - -**Screenshots:** -[Attach if visual issue] -``` - -Submit to: https://github.com/brainstormforce/sureforms/issues - ---- - -**Next:** [FAQ](faq.md) diff --git a/internal-docs/ui-and-copy.md b/internal-docs/ui-and-copy.md deleted file mode 100644 index e12a9a45a..000000000 --- a/internal-docs/ui-and-copy.md +++ /dev/null @@ -1,1077 +0,0 @@ -# UI & Copy Guidelines - -**Version:** 2.5.0 - ---- - -## User Experience Philosophy - -**Principle:** Every word and interaction should make the user feel capable, not confused. - -**User Mindset:** -- "I just want to create a form quickly" -- "I don't have time to read documentation" -- "I'm not a developer" - -**Our Responsibility:** -- Make obvious what to do next -- Explain why, not just how -- Never blame the user - ---- - -## User Journeys - -### Journey 1: First-Time User Creates Contact Form - -**Goal:** Create and publish first form in < 5 minutes - -**Steps:** - -**1. Plugin Activation (30 seconds)** - -``` -User activates plugin - ↓ -Redirect to welcome screen - ↓ -Show 2 options: - [Create with AI] [Start from Blank] -``` - -**Copy:** -``` -Welcome to SureForms! - -Let's create your first form. - -[Create with AI] ← Recommended for beginners - Let AI build your form from a simple description - -[Start from Blank] - Build your form block-by-block -``` - -**Design notes:** -- Large, friendly buttons -- "Recommended" badge on AI option -- No walls of text - ---- - -**2. AI Form Creation (2 minutes)** - -``` -User clicks "Create with AI" - ↓ -Modal appears with text input - ↓ -User types: "simple contact form" - ↓ -AI generates form (3-5 seconds) - ↓ -Form opens in editor, ready to publish -``` - -**Copy:** - -``` -Create Form with AI - -Describe the form you need: -┌─────────────────────────────────────┐ -│ Example: "job application with │ -│ file upload" or "event RSVP" │ -└─────────────────────────────────────┘ - -[Cancel] [Generate Form →] -``` - -**Loading state:** -``` -✨ Creating your form... - -AI is adding fields based on your description. -This usually takes 3-5 seconds. -``` - -**Success state:** -``` -✅ Your form is ready! - -We added: -• Name field -• Email field -• Message field -• Submit button - -You can add, remove, or rearrange fields below. - -[Publish Form] -``` - -**Design notes:** -- Loading spinner + encouraging message -- Success message lists what was created -- Clear next action (Publish) - ---- - -**3. Form Publishing (1 minute)** - -``` -User clicks "Publish" - ↓ -Gutenberg publish panel opens - ↓ -SureForms shows additional options -``` - -**Copy in publish panel:** - -``` -📋 Form Settings - -Where should submissions be sent? - -Email: [admin@yoursite.com ▼] - -After submission, show: -○ Confirmation message -○ Redirect to page -● Confirmation message (selected by default) - -Message: -┌─────────────────────────────────────┐ -│ Thank you! We'll respond within │ -│ 24 hours. │ -└─────────────────────────────────────┘ - -[Publish Form] -``` - -**Design notes:** -- Sensible defaults (confirmation message, admin email) -- Inline editing (no separate settings page) -- Jargon-free labels - ---- - -**4. Embedding Form (30 seconds)** - -``` -User wants to add form to page - ↓ -Edit page in Gutenberg - ↓ -Add SureForms block - ↓ -Select form from dropdown -``` - -**Copy in block inserter:** - -``` -SureForms - -Display a form on your page. - -📝 No form yet? Create one in SureForms > Add New -``` - -**Copy in block settings:** - -``` -Form - -Select a form: -[Contact Form ▼] [Create New] - -Display settings: -☑ Show form title -☑ Show form description -☐ Hide labels (show placeholders only) -``` - -**Design notes:** -- Help text guides to form creation if needed -- Checkbox labels explain what they do -- Preview updates in real-time - ---- - -### Journey 2: Pro User Sets Up Payment Form - -**Goal:** Create payment form and collect first payment - -**Complexity:** Higher (involves Stripe setup) - -**Steps:** - -**1. Payment Gateway Setup (5 minutes)** - -``` -User navigates to: -SureForms → Settings → Payments → Stripe -``` - -**Copy:** - -``` -Stripe Payments - -Accept credit card payments with Stripe. - -🔒 Secure: We never store card details. Payments are processed directly by Stripe. - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Test Mode (for testing) - -☑ Enable test mode - -Use these while testing: - Test card: 4242 4242 4242 4242 - Any future expiry, any CVC - -Publishable Key (starts with pk_test_) -┌─────────────────────────────────────┐ -│ │ -└─────────────────────────────────────┘ - -Secret Key (starts with sk_test_) -┌─────────────────────────────────────┐ -│ •••••••••••••••••••••••••••••••• │ -└─────────────────────────────────────┘ - -Where to get keys: -→ Stripe Dashboard > Developers > API Keys - https://dashboard.stripe.com/apikeys - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Live Mode (for real payments) - -⚠️ Only use after testing - -Publishable Key (starts with pk_live_) -┌─────────────────────────────────────┐ -│ │ -└─────────────────────────────────────┘ - -Secret Key (starts with sk_live_) -┌─────────────────────────────────────┐ -│ │ -└─────────────────────────────────────┘ - -[Save Settings] -``` - -**Design notes:** -- Test mode emphasized first -- Clear instructions where to get keys -- Security reassurance ("We never store") -- Direct link to Stripe dashboard - ---- - -**2. Creating Payment Form (3 minutes)** - -``` -User creates new form - ↓ -Adds "Payment" block - ↓ -Configures amount and currency -``` - -**Copy in Payment block settings:** - -``` -Payment Details - -Amount -○ Fixed amount - ┌──────┐ - │ 50 │ USD ▼ - └──────┘ - -● Let user choose amount - Min: [10] Max: [1000] USD ▼ - Default: [50] - -Payment Type -● One-time payment -○ Subscription - Interval: [Monthly ▼] - -Button Text -┌─────────────────────────────────────┐ -│ Pay Now │ -└─────────────────────────────────────┘ -``` - -**Design notes:** -- Radio buttons for mutually exclusive options -- Currency dropdown next to amount -- Clear labels (no "recurring" jargon, use "subscription") - ---- - -**3. Testing Payment (2 minutes)** - -**Copy on frontend form:** - -``` -Payment Information - -Amount: $50.00 - -Card Number -┌─────────────────────────────────────┐ -│ 1234 5678 9012 3456 │ [Card icon] -└─────────────────────────────────────┘ - -Expiry CVC -┌──────────┐ ┌─────┐ -│ MM / YY │ │ 123 │ -└──────────┘ └─────┘ - -🔒 Secure payment powered by Stripe - Your card details are encrypted and never stored. - -[Pay $50.00] -``` - -**Success message after payment:** - -``` -✅ Payment Successful! - -Receipt sent to: john@example.com - -Transaction ID: ch_1A2B3C4D5E6F - -[View Receipt] [Back to Home] -``` - -**Error message if payment fails:** - -``` -❌ Payment Failed - -Your card was declined. - -Common reasons: -• Insufficient funds -• Incorrect card number or expiry date -• Card requires 3D Secure verification - -Please try again or use a different card. - -[Try Again] -``` - -**Design notes:** -- Security reassurance prominent -- Success message includes receipt email and transaction ID -- Error message explains why and how to fix - ---- - -### Journey 3: Power User Builds Conditional Logic Form (Pro) - -**Goal:** Show/hide fields based on user selection - -**Example:** Event RSVP with dietary restrictions (only show if attending) - -**Steps:** - -**1. Creating Base Form (2 minutes)** - -``` -User adds fields: - - "Will you attend?" (Multiple Choice: Yes/No) - - "Dietary restrictions" (Dropdown) -``` - -**2. Adding Conditional Logic (3 minutes)** - -``` -User clicks "Dietary restrictions" block - ↓ -Opens block settings panel - ↓ -Enables conditional logic -``` - -**Copy in block settings:** - -``` -Conditional Logic - -Show this field only when certain conditions are met. - -☑ Enable conditional logic - -Show this field when: - -[Will you attend? ▼] [is ▼] [Yes ▼] - -[+ Add Condition] - -Logic: -● Show if ALL conditions match (AND) -○ Show if ANY condition matches (OR) -``` - -**Design notes:** -- Toggle to enable -- Dropdown-based condition builder (no code) -- Visual feedback: field grays out in editor preview when hidden - ---- - -**3. Testing Logic (1 minute)** - -**Copy in editor preview mode:** - -``` -💡 Preview Mode - -This is how your form will appear to users. - -Try selecting different options to see conditional logic in action. - -[Exit Preview] -``` - -**Behavior:** -- User selects "Yes" → Dietary field appears -- User selects "No" → Dietary field disappears - -**Design notes:** -- Clear indication of preview mode -- Real-time updates (no reload) - ---- - -## Microcopy Guidelines - -### Field Labels - -**Be concise and conversational** - -❌ Bad: "Please enter your electronic mail address" -✅ Good: "Email" - -❌ Bad: "Input telephone number (optional)" -✅ Good: "Phone (optional)" - -**Use sentence case, not title case** - -❌ Bad: "First Name" -✅ Good: "First name" - -❌ Bad: "Company Name (If Applicable)" -✅ Good: "Company name (if applicable)" - ---- - -### Help Text - -**Explain why or provide examples** - -**Email field:** -❌ Bad: "Enter email" -✅ Good: "We'll send a confirmation to this email" - -**Phone field:** -❌ Bad: "Phone number" -✅ Good: "We'll only call if there's an issue with your order" - -**File upload:** -❌ Bad: "Upload file" -✅ Good: "Upload your resume (PDF or DOCX, max 10MB)" - ---- - -### Error Messages - -**Be specific and actionable** - -**Email validation:** -❌ Bad: "Invalid email" -✅ Good: "Please enter a valid email (e.g., name@example.com)" - -**Required field:** -❌ Bad: "This field is required" -✅ Good: "Please enter your name" - -**File size:** -❌ Bad: "File too large" -✅ Good: "File must be under 10MB. Yours is 15MB. Try compressing it." - -**Payment failed:** -❌ Bad: "Transaction error" -✅ Good: "Your card was declined. Please check your card details or try a different card." - ---- - -### Success Messages - -**Be enthusiastic but not over-the-top** - -**Form submission:** -❌ Bad: "Form submitted" -✅ Good: "Thanks! We'll get back to you within 24 hours." - -**Payment successful:** -❌ Bad: "Payment processed successfully. Transaction ID: 1234." -✅ Good: "✅ Payment received! Receipt sent to your email." - -**Registration:** -❌ Bad: "Account created. Please log in." -✅ Good: "Welcome! Check your email to verify your account." - ---- - -### Button Labels - -**Use verbs that describe the action** - -❌ Bad: "Submit" -✅ Good: "Send Message" - -❌ Bad: "Click Here" -✅ Good: "Download Receipt" - -❌ Bad: "Proceed" -✅ Good: "Continue to Payment" - -**For payment buttons, include amount** - -❌ Bad: "Pay" -✅ Good: "Pay $50.00" - -❌ Bad: "Subscribe" -✅ Good: "Subscribe for $9.99/month" - ---- - -### Settings & Options - -**Explain consequences, not just features** - -**Email notification:** -❌ Bad: "Send email notification" -✅ Good: "Email me when someone submits this form" - -**Required field:** -❌ Bad: "Required" -✅ Good: "Make this field required" (checkbox label) - -**Conditional logic:** -❌ Bad: "Enable conditional logic" -✅ Good: "Show or hide this field based on other answers" - ---- - -## Visual Design Patterns - -### Empty States - -**When user has no forms yet:** - -``` -┌─────────────────────────────────────────┐ -│ │ -│ 📝 │ -│ │ -│ No forms yet │ -│ │ -│ Forms help you collect information │ -│ from your website visitors. │ -│ │ -│ [Create Your First Form] │ -│ │ -└─────────────────────────────────────────┘ -``` - -**Copy principles:** -- Icon relevant to context -- 1-sentence explanation -- Clear call-to-action button - ---- - -**When form has no submissions:** - -``` -┌─────────────────────────────────────────┐ -│ │ -│ 📭 │ -│ │ -│ No submissions yet │ -│ │ -│ Share this form to start collecting │ -│ responses. │ -│ │ -│ [Copy Form Link] [Embed on Page] │ -│ │ -└─────────────────────────────────────────┘ -``` - -**Design notes:** -- Actionable next steps -- Multiple options (link vs embed) - ---- - -### Loading States - -**Form submission in progress:** - -``` -┌─────────────────────────────────────┐ -│ ⏳ Sending... │ -│ │ -│ Please don't close this page. │ -└─────────────────────────────────────┘ -``` - -**AI form generation:** - -``` -┌─────────────────────────────────────┐ -│ ✨ Creating your form... │ -│ │ -│ [████████░░] 80% │ -│ │ -│ Adding email validation... │ -└─────────────────────────────────────┘ -``` - -**Design notes:** -- Spinner or progress indicator -- Explain what's happening -- Show progress if possible - ---- - -### Confirmation Dialogs - -**Deleting a form:** - -``` -┌─────────────────────────────────────┐ -│ Delete "Contact Form"? │ -│ │ -│ This will permanently delete: │ -│ • The form │ -│ • 47 submissions │ -│ • All settings │ -│ │ -│ This cannot be undone. │ -│ │ -│ [Cancel] [Delete Form] │ -└─────────────────────────────────────┘ -``` - -**Design notes:** -- Specific about what will be deleted -- Use red/destructive style for delete button -- Cancel button should be default (easier to accidentally click) - ---- - -### Tooltips & Hints - -**When to use:** -- Explaining technical terms -- Providing examples -- Showing keyboard shortcuts - -**Format:** - -``` -Field Name [?] - ↓ (on hover) -┌─────────────────────────────────────┐ -│ This is the internal name used in │ -│ the database. │ -│ │ -│ Example: "first_name" │ -└─────────────────────────────────────┘ -``` - -**Guidelines:** -- Keep under 2 sentences -- Provide example if helpful -- Don't repeat the label - ---- - -## Accessibility - -### Screen Reader Text - -**Form structure:** - -```html -
-
-

Personal Information

- -
-
-``` - -**Error announcements:** - -```html -
- Please fix 2 errors before submitting. -
- - - - Please enter a valid email address - -``` - ---- - -### Keyboard Navigation - -**Required interactions:** -- Tab through all fields -- Space to toggle checkboxes/radio buttons -- Enter to submit form -- Escape to close modals - -**Visual focus indicators:** - -```css -.srfm-field input:focus { - outline: 2px solid #0073aa; - outline-offset: 2px; -} -``` - -**Never:** -```css -:focus { - outline: none; /* ❌ Never remove focus outline */ -} -``` - ---- - -### Color Contrast - -**WCAG 2.1 Level AA Requirements:** -- Normal text: 4.5:1 minimum -- Large text (18pt+): 3:1 minimum -- UI components: 3:1 minimum - -**Test tools:** -- WebAIM Contrast Checker -- Chrome DevTools (Lighthouse) - -**Safe color combinations:** -- White text on dark backgrounds (#333 or darker) -- Dark text on light backgrounds (#F0F0F0 or lighter) -- Avoid gray text on gray backgrounds - ---- - -## Tone & Voice - -### Voice Characteristics - -**Friendly but professional** -- Use "we" and "you" -- Conversational, not formal -- Helpful, not condescending - -**Example:** -❌ Formal: "The system has detected an error in your input." -✅ Friendly: "Oops! We couldn't save that. Check for any errors above." - ---- - -**Clear over clever** -- Avoid puns and wordplay -- Be direct -- Technical accuracy over marketing fluff - -**Example:** -❌ Clever: "Houston, we have a problem!" -✅ Clear: "Something went wrong. Please try again." - ---- - -**Empowering, not blaming** -- Focus on solutions, not problems -- Use "we" for errors, "you" for successes - -**Example:** -❌ Blaming: "You entered an invalid email." -✅ Empowering: "Hmm, that email doesn't look quite right. Mind double-checking it?" - ---- - -### Writing for Different Contexts - -**First-time users:** -- More explanation -- Examples provided -- Encouraging tone - -**Example:** -``` -Welcome to SureForms! - -Creating your first form is easy. We'll walk you through each step. - -Let's start by choosing what kind of form you need. -``` - ---- - -**Power users:** -- Less explanation -- Assume knowledge -- Efficiency-focused - -**Example:** -``` -Advanced Settings - -Show field if: [condition builder] -Custom CSS class: [input] -``` - ---- - -**Error states:** -- Apologetic but not dramatic -- Specific about what went wrong -- Clear next steps - -**Example:** -``` -We couldn't save your changes. - -The connection to the server timed out. - -Please try again. If this keeps happening, check your internet connection. - -[Try Again] -``` - ---- - -**Success states:** -- Positive reinforcement -- What happens next -- Optional next action - -**Example:** -``` -✅ Form published! - -Your form is now live at: -https://yoursite.com/contact - -[View Form] [Create Another] -``` - ---- - -## Form Field Best Practices - -### Required vs Optional - -**Default:** Make fields optional - -**Require only when absolutely necessary:** -- Name (if sending personalized response) -- Email (if you need to contact them) -- Payment details (if collecting payment) - -**Label optional fields:** -``` -Phone (optional) -Company name (optional) -``` - -**Don't label required fields:** -❌ Bad: "Email (required)" -✅ Good: "Email" (with * indicator) - ---- - -### Field Order - -**Logical flow:** -1. Personal info (Name, Email) -2. Specific questions -3. Payment details (if applicable) -4. Submit button - -**Example contact form:** -``` -1. Name -2. Email -3. Subject -4. Message -5. [Submit] -``` - -**Example order form:** -``` -1. Name -2. Email -3. Product selection -4. Quantity -5. Payment details -6. [Complete Purchase] -``` - ---- - -### Placeholder Text - -**Use sparingly** - -Only for examples, not as labels: - -❌ Bad: -``` -Label: [empty] -Placeholder: "Enter your email" -``` - -✅ Good: -``` -Label: Email -Placeholder: "name@example.com" -``` - -**Never use placeholders as only label** (accessibility issue) - ---- - -### Multi-Step Forms (Pro) - -**Step indicators:** - -``` -Step 1 of 3: Your Information -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -●━━━━━━━━━━━━○━━━━━━━━━━━━━○ - -[Fields here] - -[Continue to Step 2 →] -``` - -**Navigation:** -- Show progress (Step 1 of 3) -- Visual progress bar -- Allow back navigation (not just forward) - -**Button labels:** -- Step 1: "Continue" (not "Next") -- Step 2: "Continue" -- Step 3: "Submit Form" (or "Complete Order") - ---- - -## Internationalization (i18n) - -### Text Translation - -**All user-facing strings must be translatable:** - -```php -// ✅ Good -__('Email', 'sureforms'); -_e('Submit Form', 'sureforms'); - -// ❌ Bad -echo 'Email'; // Hardcoded English -``` - -**Placeholders for dynamic content:** - -```php -// ✅ Good -sprintf(__('You have %d new submissions', 'sureforms'), $count); - -// ❌ Bad -echo "You have $count new submissions"; -``` - ---- - -### Date & Number Formatting - -**Use WordPress functions:** - -```php -// Dates -echo date_i18n(get_option('date_format'), $timestamp); - -// Numbers -echo number_format_i18n($number); - -// Currency -echo '$' . number_format_i18n($amount, 2); -``` - ---- - -### RTL Support - -**CSS for right-to-left languages:** - -```css -/* Use logical properties */ -.srfm-field { - margin-inline-start: 10px; /* Not margin-left */ - padding-inline-end: 20px; /* Not padding-right */ -} -``` - -**Test with RTL languages:** -- Arabic -- Hebrew -- Persian - ---- - -## Quality Checklist - -Before shipping UI copy: - -- [ ] Spell check passed -- [ ] Grammar correct -- [ ] Tone matches guidelines -- [ ] Actionable error messages -- [ ] Examples provided where helpful -- [ ] Accessibility: screen reader friendly -- [ ] Accessibility: color contrast ≥ 4.5:1 -- [ ] Keyboard navigation works -- [ ] i18n: All strings translatable -- [ ] RTL languages supported -- [ ] Tested with real users (if major UI) - ---- - -**Next:** [FAQ](faq.md) diff --git a/tests/play/specs/CLAUDE.md b/tests/play/specs/CLAUDE.md deleted file mode 100644 index 113b91abd..000000000 --- a/tests/play/specs/CLAUDE.md +++ /dev/null @@ -1,324 +0,0 @@ -# Playwright E2E Specs — Writing Guide - -Rules and patterns for writing reliable SureForms E2E tests. Each section is a lesson learned from real failures. - ---- - -## Auth: Never do a full form-login inside tests - -**Pattern:** `globalSetup` logs in once and saves `storageState.json`. All workers restore it. - -**Why:** Multiple workers calling the WordPress login form simultaneously causes nonce collisions — WordPress redirects to `?reauth=1`, breaking every test in those workers. - -**Correct:** Let `storageState` handle auth. The `loginAsAdmin` util now just navigates to `/wp-admin` and relies on the pre-loaded cookies. - -```js -// DO — storageState is already loaded by playwright.config.js -test.beforeEach(async ({ page }) => { - await loginAsAdmin(page); // navigates to /wp-admin, skips login form -}); - -// DON'T — never re-do a full form login inside specs -await page.goto('/wp-login.php'); -await page.fill('#user_login', 'admin'); -``` - ---- - -## Adding blocks: sidebar vs programmatic insertion - -**Only these blocks appear in the quick-action sidebar by default:** -`input`, `email`, `textarea`, `checkbox`, `number`, `inline-button`, `advanced-heading`, `payment` - -**All other blocks** (`phone`, `url`, `multi-choice`, `dropdown`, `gdpr`, `rating`, `date`, etc.) **must be inserted programmatically.** - -Use `addFieldBlock(page, 'slug')` from `formHelpers.js` — it automatically routes sidebar blocks via click and all other blocks via `insertBlock`. - -```js -// This handles routing automatically -await addFieldBlock(page, 'gdpr'); // programmatic -await addFieldBlock(page, 'input'); // sidebar click -await addFieldBlock(page, 'dropdown'); // programmatic -``` - -**Never** try to open the WP block inserter toolbar to add non-sidebar blocks — it is fragile and frequently breaks tests. - ---- - -## WP 6.9+ iframe editor: no `data-type`, use data store - -WP 6.9 renders the block editor inside an `iframe[name="editor-canvas"]` and **removed the `data-type` attribute** from block wrapper elements. - -**Never** use `page.locator('.wp-block[data-type="srfm/..."]')` — it will find nothing. - -To **select a block** (e.g. to open its settings panel), use `selectBlock(page, 'slug')` from `formHelpers.js`. It dispatches `selectBlock` via the Gutenberg data store: - -```js -const { selectBlock } = require('../utils/formHelpers'); - -await addFieldBlock(page, 'email'); -await selectBlock(page, 'email'); // selects the block via data store -await openBlockSettingsTab(page); // now the sidebar shows this block's settings -``` - -To **verify a block exists**, `addFieldBlock` already checks via `wp.data.select('core/block-editor').getBlocks()` — no need for a separate DOM assertion. - ---- - -## Field CSS selectors - -Always use the most specific class. Do not rely on element order (`.nth(N)`) unless you are certain only N elements exist. - -| Field | Frontend input selector | -|---|---| -| Single line input | `input.srfm-input-input` | -| Email | `input.srfm-input-email` | -| Email confirm | `input.srfm-input-email-confirm` — **NOT** `input.srfm-input-email` | -| Phone | `input.srfm-input-phone` | -| Number | `input.srfm-input-number` | -| URL | `input.srfm-input-url` | -| Textarea | `textarea.srfm-textarea-text` | -| Checkbox | `input[type="checkbox"]` inside `.srfm-checkbox-input-wrap` | -| Submit button | `#srfm-submit-btn` | -| Success box | `.srfm-success-box` | -| Error message | `.srfm-error-message` | - ---- - -## TomSelect dropdowns - -The SureForms dropdown field uses TomSelect. The native ``. Filling it simulates a bot: - -```js -await page.evaluate(() => { - const f = document.querySelector('[name="srfm-honeypot-field"]'); - if (f) f.value = 'spambot'; -}); -``` - -Server returns `wp_send_json_error` → `.srfm-success-box` must NOT appear. - -## Helpful utilities - -All utilities live in `tests/play/utils/`. - -| Utility | Purpose | -|---|---| -| `loginAsAdmin(page)` | Navigates to `/wp-admin`, restores auth from storageState | -| `createBlankForm(page)` | Creates a new blank SureForms form in the editor | -| `addFieldBlock(page, slug)` | Adds a field block (sidebar or programmatic) | -| `selectBlock(page, slug)` | Selects a block in the editor via the data store (iframe-safe) | -| `publishFormAndGetURL(page)` | Publishes the current form and returns its front-end URL | -| `openFormSettingsDialog(page, navItem)` | Opens Form Behavior dialog to a specific nav item | -| `setFormTitle(page, title)` | Sets the editor post title via `editPost` (avoids fragile UI title input) | -| `createWPPage(page, title, content)` | Creates a published WP page via REST API | - -When a new utility is needed, add it here rather than inlining complex selectors in spec files. diff --git a/tests/play/specs/TODO.md b/tests/play/specs/TODO.md deleted file mode 100644 index fd594df9a..000000000 --- a/tests/play/specs/TODO.md +++ /dev/null @@ -1,244 +0,0 @@ -# E2E Test Plan — SureForms Free Plugin - -Track progress for all Playwright test cases. -**Update this file every time a test is added or a test ID changes.** - -Last updated: 2026-03-16 -Branch convention: `test/e2e-p{N}-test-cases` - ---- - -## How to update this file - -- When you **add a test**, tick its checkbox and fill in the `Spec file` column. -- When you **add a new test ID** that wasn't listed here, add a row in the correct section. -- Fix the `Last updated` date above. - ---- - -## Coverage status key - -| Symbol | Meaning | -|---|---| -| ✅ | Implemented and passing | -| ⏭️ | Implemented but skipped (requires external keys / infra) | -| ❌ | Not yet implemented | -| 🔢 | Implemented but test ID needs renumbering | - ---- - -## Group 1 — Field types (basic submit) - -All tests live in `field-types.spec.js`. -Single-line input and email are covered in `form-creation-submission.spec.js` (not repeated here). - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 1.1 | Textarea — submit multi-line text | ✅ | field-types.spec.js | -| 1.2 | Number — submit a valid number | ✅ | field-types.spec.js | -| 1.3 | Phone — submit a valid phone number | ✅ | field-types.spec.js | -| 1.4 | URL — submit a valid URL | ✅ | field-types.spec.js | -| 1.5 | Checkbox — check and submit | ✅ | field-types.spec.js | -| 1.6 | Multi-choice (radio) — pick one option and submit | ✅ | field-types.spec.js | -| 1.7 | Multi-choice (multi-select) — pick multiple options and submit | ✅ | field-types.spec.js | -| 1.8 | Dropdown — select one option and submit | ✅ | field-types.spec.js | -| 1.9 | *(reserved — inline-button / advanced-heading behaviour TBD)* | ❌ | — | -| 1.10 | GDPR — check consent and submit | ✅ | field-types.spec.js | - ---- - -## Group 2 — Field validation - -All tests live in `field-validation.spec.js`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 2.1 | Email — invalid format shows error | ✅ | field-validation.spec.js | -| 2.2 | Email confirmation — mismatch shows error | ✅ | field-validation.spec.js | -| 2.3 | Email confirmation — matching emails submit successfully | ✅ | field-validation.spec.js | -| 2.4 | URL — invalid format shows error | ✅ | field-validation.spec.js | -| 2.5 | Number — value below minimum shows error | ✅ | field-validation.spec.js | -| 2.6 | Number — value above maximum shows error | ✅ | field-validation.spec.js | -| 2.7 | Required single-line input — submit empty shows error | ✅ | field-validation.spec.js | -| 2.8 | Required email — submit empty shows error | ✅ | field-validation.spec.js | -| 2.9 | Required phone — submit empty shows error | ✅ | field-validation.spec.js | -| 2.10 | Required textarea — submit empty shows error | ✅ | field-validation.spec.js | -| 2.11 | Required checkbox — submit unchecked shows error | ✅ | field-validation.spec.js | -| 2.12 | Required GDPR — submit without consent shows error | ✅ | field-validation.spec.js | -| 2.13 | Required dropdown — submit without selection shows error | ✅ | field-validation.spec.js | -| 2.14 | Required multi-choice — submit without selection shows error | ✅ | field-validation.spec.js | -| 2.15 | Character limit — input/textarea rejects text over max-chars | ❌ | — | - ---- - -## Group 3 — Form settings (confirmation behaviour) - -All tests live in `form-settings.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 3.1 | Redirect on submission — user lands on configured URL | ✅ | form-settings.spec.js | -| 3.2 | Custom success message — configured text shown after submit | ✅ | form-settings.spec.js | -| 3.3 | Store entries disabled — submission succeeds but no entry stored | ✅ | form-settings.spec.js | -| 3.4 | Page confirmation type — redirect to a configured WordPress page | ✅ | form-settings.spec.js | -| 3.5 | Store entries enabled (default) — entry appears in admin after submit | ✅ | form-settings.spec.js | -| 3.6 | Per-form GDPR compliance: do-not-store-entries enabled → no entry created | ✅ | form-settings.spec.js (covered by 3.3) | -| 3.7 | Per-form compliance: auto-delete setting saves and is visible in editor | ❌ | — | - ---- - -## Group 4 — Entries management - -All tests live in `entries.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 4.1 | Submitted entry appears in the admin entries list | ✅ | entries.spec.js | -| 4.2 | Entry detail contains the correct submitted field values | ✅ | entries.spec.js | -| 4.3 | Bulk delete removes selected entries | ✅ | entries.spec.js | -| 4.4 | CSV export downloads a file | ✅ | entries.spec.js | -| 4.5 | Mark entry as read / unread — status updates in the admin list | ✅ | entries.spec.js | - ---- - -## Group 5 — Form embedding - -All tests live in `form-embedding.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 5.1 | Shortcode-embedded form renders on a page | ✅ | form-embedding.spec.js | -| 5.2 | Shortcode-embedded form submits successfully | ✅ | form-embedding.spec.js | -| 5.3 | Gutenberg block-embedded form renders on a page | ✅ | form-embedding.spec.js | -| 5.4 | Gutenberg block-embedded form submits successfully | ✅ | form-embedding.spec.js | - ---- - -## Group 6 — Email notifications - -All tests live in `email-notifications.spec.js`. -All tests are currently skipped — require MailHog wired into the wp-env Docker setup. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 6.1 | Admin notification email is sent after form submission | ⏭️ | email-notifications.spec.js | -| 6.2 | Notification email body contains submitted field values | ⏭️ | email-notifications.spec.js | -| 6.3 | User confirmation email is sent to submitter's address | ❌ | — | - ---- - -## Group 7 — CAPTCHA - -All tests live in `captcha.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 7.2 | reCAPTCHA v2 — submit without solving is blocked | ⏭️ | captcha.spec.js | -| 7.3 | Cloudflare Turnstile — submit without solving is blocked | ❌ | — | -| 7.4 | hCaptcha — submit without solving is blocked | ❌ | — | -| 7.5 | reCAPTCHA v3 — submit with low score is blocked | ❌ | — | -| 7.7 | CAPTCHA disabled — form submits normally without CAPTCHA widget | ✅ | captcha.spec.js | - -> **Note:** 7.3–7.5 require external API keys; add them with `test.skip` pattern matching 7.2. - ---- - -## Group 8 — Payments (Stripe) - -All tests live in `payments.spec.js`. -All tests are currently skipped — require Stripe test keys. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 8.1 | Payment field renders the Stripe card widget | ⏭️ | payments.spec.js | -| 8.2 | Valid Stripe test card — payment succeeds and success message shown | ⏭️ | payments.spec.js | -| 8.3 | Declined Stripe test card — error message shown | ⏭️ | payments.spec.js | -| 8.6 | Successful payment creates an entry in the admin entries list | ⏭️ | payments.spec.js | - ---- - -## Group 9 — Global settings - -All tests live in `global-settings.spec.js`. - -| ID | Test case | Status | Spec file | -|-----|-----------|--------|-----------| -| 9.1 | Global settings page loads without a JS error | ✅ | global-settings.spec.js | -| 9.2 | All main settings tabs are clickable and stay on settings page | ✅ | global-settings.spec.js | - ---- - -## Group 10 — Spam protection - -All tests live in `spam-protection.spec.js`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 10.1 | Honeypot — bot fills hidden field → submission is blocked | ✅ | spam-protection.spec.js | -| 10.2 | Double-submit prevention — submit button disabled after first click | ✅ | spam-protection.spec.js | - ---- - -## Group 11 — Form restrictions - -All tests live in `form-restrictions.spec.js`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 11.1 | Entry limit — form shows restriction message after limit reached | ✅ | form-restrictions.spec.js | -| 11.2 | Scheduling — restriction message shown when schedule has ended | ✅ | form-restrictions.spec.js | -| 11.3 | Scheduling — restriction message shown when schedule has not started yet | ✅ | form-restrictions.spec.js | - ---- - -## Group 12 — Form lifecycle - -All tests live in `form-lifecycle.spec.js`. - -> **✅ Renaming done:** Comments in `form-lifecycle.spec.js` updated from `8.x` to `12.x`. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 12.1 | Duplicate form — copy appears in forms list with "(Copy)" suffix | ✅ | form-lifecycle.spec.js | -| 12.2 | Trash form — form disappears from All Forms view | ✅ | form-lifecycle.spec.js | -| 12.3 | Restore form — form reappears in All Forms view after restore from Trash | ✅ | form-lifecycle.spec.js | -| 12.4 | Delete permanently — form is fully removed and not restorable | ✅ | form-lifecycle.spec.js | - ---- - -## Group 13 — AI form generation - -All tests will live in `ai-form-builder.spec.js`. -Tests that require an API key should follow the `test.skip` pattern used in payments and CAPTCHA. - -| ID | Test case | Status | Spec file | -|------|-----------|--------|-----------| -| 13.1 | AI generation panel opens from the "Create Form" flow | ❌ | — | -| 13.2 | Generating a form with a prompt produces at least one block | ❌ | — | - ---- - -## Suggested implementation order - -Work these in priority order — top of each section first: - -### Next up (P1 — no external dependencies) - -- [x] **11.3** Scheduling not-started-yet *(done — form-restrictions.spec.js)* -- [x] **12.4** Delete permanently from trash *(done — form-lifecycle.spec.js)* -- [x] **2.7–2.10** Required validation for remaining field types *(done — field-validation.spec.js)* -- [x] **4.5** Entry read/unread status toggle *(done — entries.spec.js)* -- [x] **3.6** Per-form do-not-store-entries compliance *(already covered by test 3.3)* -- [ ] **2.15** Character limit — low E2E value (browser `maxlength` attribute, no server-side check); defer unless a custom error message is confirmed - -### Later (P2 — needs infra or API keys, use skip pattern) - -- [ ] **6.3** User confirmation email (needs MailHog) -- [ ] **7.3–7.5** Additional CAPTCHA types (need external keys) -- [ ] **13.1–13.2** AI form generation (needs AI API key) - -### Nice to have - -- [ ] **3.7** Auto-delete entries compliance setting -- [ ] **9.x** Global settings save (email From Name/From Email) -- [ ] **1.9** Inline button / advanced heading block behaviour

WM{KQ2U*>kGUbYwZj_n9M*Aum`XP9>(f; z@>|T`&^3B1B;Ppn>-KGELo3k(=v!=$b#{f1*)iyecPrY_Q|OYFcsraI=c4zAU}v0# z-Eli!gr~j}z6bPuhyCB4ipQwvh=eI*>-fMiG`ln3j|U67pX_U1@qB}Jccguhgchb!G?Ioo)D>IS1uA%KWYJ3oHz>CxQG5q5Yp)q^yFG^3y1?vC|)&?G#F4&;IZVXgaMZ^~DpA1bS{2Y!Y1vDs(AtFRR_pNlQ<<%3zN zf9(F(&)NSAcyP`aA#@eK%;Nvi%fW&bW68twQ+bmniz@)u|WzelqUh5M(W zOHv1oXghQlU4ot~Lt=S4+V8?c$c;gwg!>#CuKg0wcLOU$^O~~R((duaO)z!Y6S}jLkcs!P0 zLz-$gF>u9^D{}8?{S3@T>1D(J;O#S}FMw4Lp-{sT(S8Nh_2y6^tlJo{r@z&{a(Wa9>79OI~J01 z5}Gr&U_79Lu&Y{QC+-hLC-5*j!Q@&loZ$vEyEmZ`c>^8syV1|1N6`?U@NXD+ zSu|;@qepE+oR96%fxeAnaOi(oss9bf4s=)DO!q`0$$xq;ks81WtK3rl5|NM(6bG@A!RZ^Am1*J3lD$9h;Mt3>JqJP&<-5ZdnCSYD6z_a!C=aB+Hx z5~*LgjKh4&FXJ`X!X89j@E0#U6MCz!$4;|=g?2ezHA+BArL@HSuqaQ{S z(cN%6UW;ibl}P<|tl%X6|HnNk@1o*jtWv5(>i2Ua(C_sda0HesT_Ux=3-MCQJFo{< zDia1i9EVa~k3+EH$-zSOz2nw0OI!vFv19WE{?TO#!*8%mc84~)jE zso#bUpysI|lsDr9%ImQ`);=xd%4l>=bFnVwqmj55P3GlT7FVM=_7a+7JCj`a!Y=fM z-_d0H7yUkvSw46IT7MFD!Bg;F9Doh*2znx(QK3ZY9O;kd!VTCGpTwqk0A2fX6+^$t zPF$E|-ap+{iuCZiqA#MF%NB+B#987)TNe;A#>6X^zDfXcqe1}HlSM;b%uN+=1fzF^T8rmvo#9E*oUW`U;0Q%e*H0dUy z6S@hV$UW%5mSXC^|2@Qo4XsCK@FqH-chLbHKxgm;+VFSi0DnVgo_R*7FNY4GDmtJh zSRF4wJG>H|;4rkE+%uTJ7ll+fvSfU4F4m7m8M3FAH=bc49%7WX{8&ykoY?M7> zjQ1yun>3o46^_X+OyuR{jGV}*@bu7DcKVl*rdGTmcONMC|Ug9Kj|$q8jjD- z9haDrTgdyomy;NmlQ+6>46{nz7%?t4C$BJ(Jz_-8gu;pIJ7%m(lpZ%Nkvo1uenDZ* z$l^qej9DiX7uU)7Ab=$7JMX6dfI#F`#JGxQsf*rN(DWI<@$`WJaBq#n-IM z7@1x)V134@62yB_UeT8I8E0hXPs}OazdqxM%%YG~_aZ`!nlfL2i+)=51OeRoL}J?if4wJ6%HfWSoLj(1 zvt8j_-W@(MpByQg(k8R+DWfLk6lB{#e!<9`yyEq3GHa9E`TUfAlT_TUA-TFe_|6Tk4H<@cE<&Xp;awg^$ znnWQF!jy~WW@Q~NT|B2;)~^|DN8}e2" msgstr "Formato esperado para correos electrónicos: email@sureforms.com o John Doe " -#: admin/admin.php:1471 -#: admin/admin.php:1472 +#: admin/admin.php:1517 +#: admin/admin.php:1518 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -12173,7 +12179,7 @@ msgstr "¡Eleva tu SEO y asciende en los rankings de búsqueda sin esfuerzo!" msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress." msgstr "Aumenta la visibilidad de tu sitio web con una automatización SEO inteligente. Optimiza el contenido, rastrea el rendimiento de las palabras clave y obtén información procesable, todo dentro de WordPress." -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -15372,12 +15378,12 @@ msgid "Stripe Settings" msgstr "Configuración de Stripe" #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. -#: admin/admin.php:2281 +#: admin/admin.php:2335 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version." msgstr "SureForms %1$s requiere un mínimo de %2$s %3$s para funcionar correctamente. Por favor, actualice a la última versión." -#: admin/admin.php:2294 +#: admin/admin.php:2348 msgid "Update Now" msgstr "Actualizar ahora" @@ -15425,8 +15431,8 @@ msgstr "Posición del signo de moneda" msgid "Select the position of the currency symbol relative to the amount." msgstr "Seleccione la posición del símbolo de moneda en relación con la cantidad." -#: admin/admin.php:1414 -#: admin/admin.php:1415 +#: admin/admin.php:1460 +#: admin/admin.php:1461 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -15440,41 +15446,41 @@ msgstr "Seleccione la posición del símbolo de moneda en relación con la canti msgid "Learn" msgstr "Aprender" -#: admin/admin.php:2635 +#: admin/admin.php:2689 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!" msgstr "¡Increíble! SureForms está impulsando tus formularios y envíos - ¡sigamos creciendo juntos!" -#: admin/admin.php:2636 +#: admin/admin.php:2690 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?" msgstr "Si SureForms ha sido útil, ¿te importaría tomarte un momento para dejar una reseña de 5 estrellas en WordPress.org?" -#: admin/admin.php:2647 -#: admin/admin.php:2711 +#: admin/admin.php:2701 +#: admin/admin.php:2765 msgid "Maybe later" msgstr "Quizás más tarde" -#: admin/admin.php:2653 +#: admin/admin.php:2707 msgid "I already did" msgstr "Ya lo hice" -#: admin/admin.php:2702 +#: admin/admin.php:2756 msgid "SureForms is ready to power your forms — explore what's possible!" msgstr "SureForms está listo para potenciar tus formularios — ¡explora lo que es posible!" -#: admin/admin.php:2703 +#: admin/admin.php:2757 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard." msgstr "Administra tus formularios, realiza un seguimiento de las presentaciones y descubre funciones como el Creador de Formularios con IA, integraciones de pago y más desde el panel de SureForms." -#: admin/admin.php:2706 +#: admin/admin.php:2760 msgid "Go to Dashboard" msgstr "Ir al panel de control" -#: admin/admin.php:2717 +#: admin/admin.php:2771 msgid "I already know" msgstr "Ya lo sé" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2981 +#: admin/admin.php:3854 msgid "Invalid parameters." msgstr "Parámetros no válidos." @@ -15858,10 +15864,10 @@ msgid "This form is not yet available. Check back after the scheduled start time msgstr "Este formulario aún no está disponible. Vuelve a consultar después de la hora de inicio programada." #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -15884,48 +15890,48 @@ msgstr "Este formulario aún no está disponible. Vuelve a consultar después de msgid "Security verification failed. Please refresh the page and try again." msgstr "La verificación de seguridad falló. Por favor, actualice la página e inténtelo de nuevo." -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "No se encontraron los datos del formulario." -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "Falta el ID del formulario." -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "Lo siento, no tienes permiso para acceder a este recurso." -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "No tienes permiso para enviar este formulario." -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "Este formulario ya no está disponible." -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "Por favor, revise el formulario en busca de errores." -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "Tu envío fue marcado como spam. Por favor, inténtalo de nuevo." -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "No se puede enviar el formulario. Por favor, inténtelo de nuevo." -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "No se detectó ningún complemento SMTP. Por favor, configure un complemento SMTP para habilitar el envío de correos electrónicos." -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "El envío del correo electrónico falló por una razón desconocida." -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "No se enviaron correos electrónicos." @@ -16438,7 +16444,7 @@ msgid "Duplicate form?" msgstr "¿Formulario duplicado?" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:938 msgid "An error occurred while submitting your form. Please try again." msgstr "Se produjo un error al enviar su formulario. Por favor, inténtelo de nuevo." @@ -16815,28 +16821,28 @@ msgstr "Secreto del webhook no configurado." msgid "Invalid webhook signature." msgstr "Firma de webhook no válida." -#: admin/admin.php:1253 -#: admin/admin.php:1851 +#: admin/admin.php:1299 +#: admin/admin.php:1905 #: assets/build/formEditor.js:172 msgid "Quizzes" msgstr "Cuestionarios" -#: admin/admin.php:1252 +#: admin/admin.php:1298 msgid "Quiz Entries" msgstr "Entradas del cuestionario" -#: admin/admin.php:1255 -#: admin/admin.php:1288 -#: admin/admin.php:1321 +#: admin/admin.php:1301 +#: admin/admin.php:1334 +#: admin/admin.php:1367 #: assets/build/settings.js:172 msgid "New" msgstr "Nuevo" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "Formulario no válido." -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "Demasiadas solicitudes. Por favor, inténtelo de nuevo en breve." @@ -17345,9 +17351,9 @@ msgstr "Perfecto para educación, formación y trivia divertida" msgid "Select this to create a quiz with scored questions and graded results." msgstr "Selecciona esto para crear un cuestionario con preguntas puntuadas y resultados calificados." -#: admin/admin.php:1285 -#: admin/admin.php:1286 -#: admin/admin.php:1856 +#: admin/admin.php:1331 +#: admin/admin.php:1332 +#: admin/admin.php:1910 msgid "Survey Reports" msgstr "Informes de encuestas" @@ -17753,7 +17759,7 @@ msgid "Error creating SureForms Form." msgstr "Error al crear el formulario de SureForms." #. translators: %s represents the minimum number of characters required -#: inc/field-validation.php:426 +#: inc/field-validation.php:465 #: inc/translatable.php:94 #, php-format msgid "Please enter at least %s characters." @@ -17911,9 +17917,9 @@ msgstr "La respuesta de la IA estaba vacía. Por favor, refine su solicitud y vu msgid "Unable to build form fields from the AI response." msgstr "No se pueden construir los campos del formulario a partir de la respuesta de la IA." -#: admin/admin.php:1318 -#: admin/admin.php:1319 -#: admin/admin.php:1861 +#: admin/admin.php:1364 +#: admin/admin.php:1365 +#: admin/admin.php:1915 msgid "Partial Entries" msgstr "Entradas parciales" @@ -18784,23 +18790,23 @@ msgstr "No se pudo verificar el monto del pago para este formulario. Por favor, msgid "Cancellation is not supported for this payment gateway." msgstr "La cancelación no es compatible con esta pasarela de pago." -#: admin/admin.php:3039 +#: admin/admin.php:3203 msgid "SureForms AI Quick Draft" msgstr "SureForms AI Borrador Rápido" -#: admin/admin.php:3058 +#: admin/admin.php:3222 msgid "Describe the form and let SureForms AI generate it for you." msgstr "Describe el formulario y deja que SureForms AI lo genere por ti." -#: admin/admin.php:3061 +#: admin/admin.php:3225 msgid "Describe your form" msgstr "Describe tu formulario" -#: admin/admin.php:3068 +#: admin/admin.php:3232 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "Ejemplo: Crear un formulario de contacto con campos de nombre, correo electrónico, teléfono y mensaje." -#: admin/admin.php:3109 +#: admin/admin.php:3273 msgid "Redirecting..." msgstr "Redirigiendo..." @@ -18813,14 +18819,14 @@ msgid "The prompt is too long. Please shorten it and try again." msgstr "El mensaje es demasiado largo. Por favor, acórtalo e inténtalo de nuevo." #. translators: %s: maximum characters allowed before the @ symbol. -#: inc/field-validation.php:448 +#: inc/field-validation.php:487 #: inc/translatable.php:97 #, php-format msgid "The part before @ may not exceed %s characters." msgstr "La parte antes de @ no puede exceder %s caracteres." #. translators: %s: maximum characters allowed after the @ symbol. -#: inc/field-validation.php:454 +#: inc/field-validation.php:493 #: inc/translatable.php:100 #, php-format msgid "The part after @ may not exceed %s characters." @@ -18907,74 +18913,74 @@ msgstr "Lo siento, no tienes permiso para renderizar este formulario." msgid "This form requires a payment. Please complete the payment and submit again." msgstr "Este formulario requiere un pago. Por favor, complete el pago y envíe de nuevo." -#: admin/admin.php:552 +#: admin/admin.php:598 msgid "You are not allowed to update this prompt." msgstr "No tienes permiso para actualizar este mensaje." -#: admin/admin.php:595 +#: admin/admin.php:641 msgid "Finish setting up your form" msgstr "Termina de configurar tu formulario" -#: admin/admin.php:627 +#: admin/admin.php:673 msgid "Review or edit your form" msgstr "Revisa o edita tu formulario" -#: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:674 +#: admin/admin.php:4515 msgid "Edit form" msgstr "Editar formulario" -#: admin/admin.php:633 +#: admin/admin.php:679 msgid "Personalize the Thank You message" msgstr "Personaliza el mensaje de agradecimiento" -#: admin/admin.php:634 +#: admin/admin.php:680 msgid "Edit message" msgstr "Editar mensaje" -#: admin/admin.php:639 +#: admin/admin.php:685 msgid "Choose who gets notified of new replies" msgstr "Elige quién recibe notificaciones de nuevas respuestas" -#: admin/admin.php:640 +#: admin/admin.php:686 msgid "Set up email" msgstr "Configurar correo electrónico" -#: admin/admin.php:648 +#: admin/admin.php:694 msgid "your form" msgstr "tu formulario" #. translators: %s: form title. -#: admin/admin.php:651 +#: admin/admin.php:697 #, php-format msgid "Finish setting up %s" msgstr "Termina de configurar %s" #. translators: %s: form title. -#: admin/admin.php:659 +#: admin/admin.php:705 #, php-format msgid "View %s (opens in a new tab)" msgstr "Ver %s (se abre en una nueva pestaña)" -#: admin/admin.php:664 +#: admin/admin.php:710 msgid "Customize your form to get it ready for real submissions:" msgstr "Personaliza tu formulario para que esté listo para envíos reales:" -#: admin/admin.php:3896 +#: admin/admin.php:4504 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "Ya hemos creado este formulario para ti. Termina de personalizarlo para que esté listo para recibir envíos reales." #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4509 #, php-format msgid "Finish setting up “%s”" msgstr "Termina de configurar “%s”" -#: admin/admin.php:3914 +#: admin/admin.php:4522 msgid "Edit the Thank You message" msgstr "Edita el mensaje de agradecimiento" -#: admin/admin.php:3920 +#: admin/admin.php:4528 msgid "Set where replies go" msgstr "Configura dónde van las respuestas" @@ -19002,123 +19008,90 @@ msgstr "Por favor, elige un archivo de imagen." msgid "This form could not be submitted. Please refresh the page and try again." msgstr "Este formulario no pudo ser enviado. Por favor, actualiza la página e inténtalo de nuevo." -#: admin/admin.php:2341 -#: admin/admin.php:2430 +#: admin/admin.php:2395 +#: admin/admin.php:2484 msgid "Your SureForms database is up to date. New form entries will be saved as usual." msgstr "Su base de datos de SureForms está actualizada. Las nuevas entradas de formularios se guardarán como de costumbre." -#: admin/admin.php:2355 -#: admin/admin.php:2439 +#: admin/admin.php:2409 +#: admin/admin.php:2493 #: inc/rest-api.php:1447 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support." msgstr "SureForms no pudo terminar de actualizar la base de datos. Es posible que su alojamiento no permita a SureForms crear tablas de base de datos — por favor, contacte a su proveedor de alojamiento o al soporte de SureForms." -#: admin/admin.php:2358 +#: admin/admin.php:2412 msgid "Contact support" msgstr "Contactar soporte" -#: admin/admin.php:2378 +#: admin/admin.php:2432 msgid "Database update needed" msgstr "Actualización de base de datos necesaria" -#: admin/admin.php:2384 +#: admin/admin.php:2438 msgid "Fix now" msgstr "Arreglar ahora" -#: admin/admin.php:2453 +#: admin/admin.php:2507 msgid "SureForms — database update needed" msgstr "SureForms — se necesita actualización de base de datos" -#: admin/admin.php:2458 +#: admin/admin.php:2512 msgid "Update database" msgstr "Actualizar base de datos" -#: admin/admin.php:2478 +#: admin/admin.php:2532 msgid "You do not have permission to update the database." msgstr "No tiene permiso para actualizar la base de datos." -#: admin/admin.php:3437 +#: admin/admin.php:3669 msgid "You do not have permission to do this." msgstr "No tiene permiso para hacer esto." -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "%d envío de formulario consecutivo no pudo completarse." -msgstr[1] "" - -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." -msgstr "Los visitantes pueden no poder contactarlo, y esas entradas no fueron guardadas." - -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." -msgstr "Los envíos de formularios se están completando normalmente." - -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." -msgstr "No se encontró ningún plugin de caché que necesite configuración." - #. translators: %s: caching plugin name. -#: admin/admin.php:3551 +#: admin/admin.php:4209 msgid "%s may interfere with your forms." msgstr "%s puede interferir con sus formularios." -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." -msgstr "La optimización de caché y JavaScript puede servir una copia obsoleta de su formulario o cargar sus scripts fuera de orden." - -#: admin/admin.php:3555 +#: admin/admin.php:4185 +#: admin/admin.php:4213 msgid "Help Me Fix" msgstr "Ayúdame a arreglarlo" -#: admin/admin.php:3650 +#: admin/admin.php:4258 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "SureForms encontró sus entradas de formulario almacenadas bajo un prefijo de tabla de base de datos diferente. Reconectarlas toma un momento, y sus entradas existentes se mantendrán." -#: admin/admin.php:3653 +#: admin/admin.php:4261 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "SureForms necesita actualizar su base de datos antes de poder guardar nuevas entradas de formularios. Esto solo toma un momento y no cambiará sus formularios ni el contenido existente. Las entradas enviadas antes de ahora no pueden recuperarse desde aquí." #. translators: %s: site host. -#: admin/admin.php:4265 +#: admin/admin.php:5032 msgid "SureForms: form submissions are failing on %s" msgstr "SureForms: los envíos de formularios están fallando en %s" -#: admin/admin.php:4274 +#: admin/admin.php:4990 msgid "Debug log: no entries recorded." msgstr "Registro de depuración: no se registraron entradas." #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4995 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "Registro de depuración (más reciente %1$d de %2$d entradas)" -#: admin/admin.php:4288 -msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." -msgstr "Se dejaron fuera entradas más antiguas para mantener este correo dentro de la longitud que acepta un cliente de correo. El registro completo se puede descargar desde SureForms → Configuración → General." - -#: admin/admin.php:4318 +#: admin/admin.php:5158 msgid "Hello SureForms support," msgstr "Hola soporte de SureForms," -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." -msgstr[0] "SureForms ha registrado %d envío de formulario consecutivo que no pudo completarse." -msgstr[1] "" - -#: admin/admin.php:4332 +#: admin/admin.php:5182 msgid "Site details" msgstr "Detalles del sitio" -#: admin/admin.php:4335 +#: admin/admin.php:5185 msgid "not active" msgstr "no activo" -#: admin/admin.php:4338 +#: admin/admin.php:5188 msgid "none detected" msgstr "ninguno detectado" @@ -19126,7 +19099,7 @@ msgstr "ninguno detectado" msgid "No form submission failures have been recorded." msgstr "No se han registrado fallos en el envío de formularios." -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "No encontrado." @@ -19211,6 +19184,193 @@ msgstr "Vistas y Conversión de Formularios" msgid "Logs" msgstr "Registros" +#: admin/admin.php:2831 +msgid "Previous notice" +msgstr "Aviso anterior" + +#: admin/admin.php:2832 +msgid "Next notice" +msgstr "Aviso siguiente" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2834 +msgid "%1$d of %2$d" +msgstr "%1$d de %2$d" + +#: admin/admin.php:2891 +msgid "Nothing to report." +msgstr "Nada que reportar." + +#: admin/admin.php:4057 +#: assets/build/dashboard.js:172 +msgid "Details" +msgstr "Detalles" + +#: admin/admin.php:4058 +msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it." +msgstr "Lo que registramos sobre este problema. Cópielo en su solicitud de soporte para que podamos comenzar desde la causa en lugar de una descripción de ella." + +#: admin/admin.php:4059 +#: assets/build/dashboard.js:172 +msgid "Copy details" +msgstr "Copiar detalles" + +#: admin/admin.php:4068 +msgid "Copy the details first, so you have them to paste." +msgstr "Copie los detalles primero, para que los tenga para pegar." + +#: admin/admin.php:4072 +msgid "Copied. Contact Support is now available." +msgstr "Copiado. Contactar con Soporte ahora está disponible." + +#: admin/admin.php:4073 +msgid "Your browser would not let us copy. Select the text above and copy it by hand." +msgstr "Su navegador no nos permitió copiar. Seleccione el texto de arriba y cópielo manualmente." + +#: admin/admin.php:4076 +msgid "Recorded diagnostics" +msgstr "Diagnósticos registrados" + +#: admin/admin.php:4080 +msgid "Collecting the details…" +msgstr "Recopilando los detalles…" + +#: admin/admin.php:4081 +msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there." +msgstr "No pudimos recopilar los detalles. Contacte con Soporte y describa lo que sucedió, y lo tomaremos desde allí." + +#. translators: %s: form title. +#: admin/admin.php:4108 +msgid "We noticed a form submission failure on %s." +msgstr "Notamos un fallo en el envío del formulario en %s." + +#: admin/admin.php:4109 +msgid "We noticed a form submission failure." +msgstr "Notamos un fallo en el envío del formulario." + +#: admin/admin.php:4110 +msgid "Visitors may not be able to reach you, and their entries were not saved." +msgstr "Los visitantes pueden no poder contactarlo, y sus entradas no fueron guardadas." + +#. translators: %s: form title. +#: admin/admin.php:4115 +msgid "We noticed a notification failure on %s." +msgstr "Notamos un fallo en la notificación en %s." + +#: admin/admin.php:4116 +msgid "We noticed a notification failure." +msgstr "Notamos un fallo en la notificación." + +#: admin/admin.php:4117 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "La entrada fue guardada, pero no pudimos enviar el correo electrónico sobre ella. Pueden estar llegando nuevas entradas sin que usted lo sepa." + +#. translators: %s: form title. +#: admin/admin.php:4132 +msgid "We noticed an integration failure on %s." +msgstr "Notamos un fallo en la integración en %s." + +#: admin/admin.php:4133 +msgid "We noticed an integration failure." +msgstr "Notamos un fallo en la integración." + +#: admin/admin.php:4134 +msgid "The entry was saved, but we could not send it to a connected service." +msgstr "La entrada fue guardada, pero no pudimos enviarla a un servicio conectado." + +#: admin/admin.php:4158 +msgid "View details" +msgstr "Ver detalles" + +#: admin/admin.php:4212 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "El almacenamiento en caché puede mostrar a los visitantes una copia antigua de su formulario o cargar sus scripts en el orden incorrecto." + +#: admin/admin.php:5005 +msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General." +msgstr "Se dejaron fuera entradas más antiguas para mantener este extracto legible. El registro completo se puede descargar desde SureForms → Configuración → General." + +#. translators: %s: site host. +#: admin/admin.php:5034 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "SureForms ha registrado envíos de formularios en %s que no pudieron completarse." + +#. translators: %s: site host. +#: admin/admin.php:5038 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "SureForms: los correos electrónicos de notificación no se están enviando en %s" + +#. translators: %s: site host. +#: admin/admin.php:5040 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "SureForms guardó entradas en %s pero no pudo enviar los correos electrónicos de notificación para ellas." + +#. translators: %s: site host. +#: admin/admin.php:5044 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "SureForms: una integración no está recibiendo entradas en %s" + +#. translators: %s: site host. +#: admin/admin.php:5046 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "SureForms guardó entradas en %s pero no pudo pasarlas a un servicio conectado." + +#. translators: %s: site host. +#: admin/admin.php:5056 +msgid "SureForms: a problem with the forms on %s" +msgstr "SureForms: un problema con los formularios en %s" + +#. translators: %s: site host. +#: admin/admin.php:5058 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "SureForms ha registrado un problema con los formularios en %s." + +#. translators: %d: number of failed submissions. +#: admin/admin.php:5083 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." +msgstr[0] "SureForms ha registrado %d envío de formulario que no pudo completarse." +msgstr[1] "" + +#. translators: %d: number of failed notifications. +#: admin/admin.php:5095 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "SureForms guardó %d entrada pero no pudo enviar el correo electrónico de notificación para ella." +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:5107 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "SureForms guardó %d entrada pero no pudo pasarla a un servicio conectado." +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:5119 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "SureForms ha registrado %d problema con los formularios en este sitio." +msgstr[1] "" + +#. translators: %s: form title. +#: admin/admin.php:5169 +msgid "Form: %s" +msgstr "Formulario: %s" + +#: admin/admin.php:5189 +msgid "none recorded" +msgstr "ninguno registrado" + +#. translators: %s: date and time of the previous report, in the site's timezone. +#: admin/admin.php:5212 +msgid "Previously reported: %s" +msgstr "Reportado anteriormente: %s" + +#: assets/build/settings.js:172 +msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures." +msgstr "Las vistas y envíos por parte de cualquier persona que pueda editar el sitio normalmente se dejan fuera, por lo que probar sus propios formularios no cambia estas cifras." + #: inc/post-types.php:265 msgctxt "post type general name" msgid "Forms" diff --git a/languages/sureforms-fr_FR-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-fr_FR-51635fe6489fc8288d603fe596c755ca.json index 271ea9a57..9ea4b60ef 100644 --- a/languages/sureforms-fr_FR-51635fe6489fc8288d603fe596c755ca.json +++ b/languages/sureforms-fr_FR-51635fe6489fc8288d603fe596c755ca.json @@ -1 +1 @@ -{"translation-revision-date":"2024-12-13T13:12:49+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tableau de bord"],"Settings":["Param\u00e8tres"],"Entries":["Entr\u00e9es"],"Status":["Statut"],"Form":["Formulaire"],"Activated":["Activ\u00e9"],"Activate":["Activer"],"Address":["Adresse"],"Checkbox":["Case \u00e0 cocher"],"Dropdown":["Menu d\u00e9roulant"],"Email":["Email"],"Number":["Nombre"],"Phone":["T\u00e9l\u00e9phone"],"Textarea":["Zone de texte"],"Monday":["Lundi"],"Forms":["Formulaires"],"New Form Submission - %s":["Nouvelle soumission de formulaire - %s"],"GitHub":["GitHub"],"(no title)":["(pas de titre)"],"General":["G\u00e9n\u00e9ral"],"No tags available":["Aucune \u00e9tiquette disponible"],"Back":["Retour"],"Generic tags":["Balises g\u00e9n\u00e9riques"],"Other":["Autre"],"Confirm":["Confirmer"],"Cancel":["Annuler"],"Install":["Installer"],"Plugin Installation failed, Please try again later.":["L'installation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"Plugin activation failed, Please try again later.":["L'activation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"Integrations":["Int\u00e9grations"],"What's New?":["Quoi de neuf ?"],"Core":["C\u0153ur"],"Unlicensed":["Sans licence"],"Connecting\u2026":["Connexion\u2026"],"Install & Activate":["Installer et activer"],"Compliance Settings":["Param\u00e8tres de conformit\u00e9"],"Enable GDPR Compliance":["Activer la conformit\u00e9 RGPD"],"Never store entry data after form submission":["Ne jamais conserver les donn\u00e9es d'entr\u00e9e apr\u00e8s la soumission du formulaire"],"When enabled this form will never store Entries.":["Lorsqu'il est activ\u00e9, ce formulaire ne stockera jamais les entr\u00e9es."],"Automatically delete entries":["Supprimer automatiquement les entr\u00e9es"],"When enabled this form will automatically delete entries after a certain period of time.":["Lorsque cette option est activ\u00e9e, ce formulaire supprimera automatiquement les entr\u00e9es apr\u00e8s une certaine p\u00e9riode de temps."],"Entries older than the days set will be deleted automatically.":["Les entr\u00e9es plus anciennes que le nombre de jours d\u00e9fini seront supprim\u00e9es automatiquement."],"Visual":["Visuel"],"HTML":["HTML"],"All Data":["Toutes les donn\u00e9es"],"Add Shortcode":["Ajouter un shortcode"],"Form input tags":["Balises de saisie de formulaire"],"Comma separated values are also accepted.":["Les valeurs s\u00e9par\u00e9es par des virgules sont \u00e9galement accept\u00e9es."],"Email Notification":["Notification par e-mail"],"Name":["Nom"],"Send Email To":["Envoyer un e-mail \u00e0"],"Subject":["Sujet"],"CC":["CC"],"BCC":["Cci"],"Reply To":["R\u00e9pondre \u00e0"],"Add Key":["Ajouter une cl\u00e9"],"Add Value":["Ajouter de la valeur"],"Add":["Ajouter"],"Confirmation Message":["Message de confirmation"],"After Form Submission":["Apr\u00e8s la soumission du formulaire"],"Hide Form":["Masquer le formulaire"],"Reset Form":["R\u00e9initialiser le formulaire"],"Custom URL":["URL personnalis\u00e9e"],"Add Query Parameters":["Ajouter des param\u00e8tres de requ\u00eate"],"Select if you want to add key-value pairs for form fields to include in query parameters":["S\u00e9lectionnez si vous souhaitez ajouter des paires cl\u00e9-valeur pour les champs de formulaire \u00e0 inclure dans les param\u00e8tres de requ\u00eate"],"Query Parameters":["Param\u00e8tres de requ\u00eate"],"Success Message":["Message de r\u00e9ussite"],"Redirect":["Rediriger"],"Redirect to":["Rediriger vers"],"Page":["Page"],"Form Confirmation":["Confirmation du formulaire"],"Confirmation Type":["Type de confirmation"],"Clear":["Clair"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invisible"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validations"],"Spam Protection":["Protection contre le spam"],"If this option is turned on, the user's IP address will be saved with the form data":["Si cette option est activ\u00e9e, l'adresse IP de l'utilisateur sera enregistr\u00e9e avec les donn\u00e9es du formulaire"],"Enable Honeypot Security":["Activer la s\u00e9curit\u00e9 Honeypot"],"Enable Honeypot Security for better spam protection":["Activez la s\u00e9curit\u00e9 Honeypot pour une meilleure protection contre le spam"],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s repr\u00e9sente le nombre minimum de s\u00e9lections n\u00e9cessaires. Par exemple : \u00ab Un minimum de 2 s\u00e9lections est requis. \u00bb"],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s repr\u00e9sente le nombre maximum de s\u00e9lections autoris\u00e9es. Par exemple : \u00ab Maximum 4 s\u00e9lections autoris\u00e9es. \u00bb"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s repr\u00e9sente le nombre minimum de choix n\u00e9cessaires. Par exemple : \u00ab Au moins 1 s\u00e9lection est requise. \u00bb"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s repr\u00e9sente le nombre maximum de choix autoris\u00e9s. Par exemple : \u00ab Maximum de 3 s\u00e9lections autoris\u00e9es. \u00bb"]," Error Message":["Message d'erreur"],"Email Summaries":["R\u00e9sum\u00e9s d'e-mails"],"Tuesday":["Mardi"],"Wednesday":["Mercredi"],"Thursday":["Jeudi"],"Friday":["Vendredi"],"Saturday":["Samedi"],"Sunday":["Dimanche"],"Schedule Reports":["Programmer les rapports"],"Auto":["Auto"],"Light":["Lumi\u00e8re"],"Dark":["Sombre"],"Turnstile":["Tourniquet"],"Get Keys":["Obtenir les cl\u00e9s"],"Documentation":["Documentation"],"Site Key":["Cl\u00e9 du site"],"Secret Key":["Cl\u00e9 secr\u00e8te"],"Cloudflare Turnstile":["Tourniquet Cloudflare"],"Appearance Mode":["Mode d'apparence"],"Text":["Texte"],"Test Email":["Email de test"],"IP Logging":["Journalisation IP"],"Honeypot":["Pot de miel"],"Confirmation Email Mismatch Message":["Message de non-correspondance de l'email de confirmation"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s repr\u00e9sente la valeur d'entr\u00e9e minimale. Par exemple : \"La valeur minimale est 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s repr\u00e9sente la valeur d'entr\u00e9e maximale. Par exemple : \"La valeur maximale est 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Connectez-vous avec OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'adresse e-mail 'De' actuelle ne correspond pas au nom de domaine de votre site web (%1$s). Cela peut entra\u00eener le blocage ou le marquage de vos e-mails de notification comme spam. Alternativement, essayez d'utiliser une adresse De qui correspond au nom de domaine de votre site web (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'adresse e-mail 'De' actuelle ne correspond pas au nom de domaine de votre site web (%s). Cela peut entra\u00eener le blocage de vos e-mails de notification ou leur classement en tant que spam."],"We strongly recommend that you install the free ":["Nous vous recommandons vivement d'installer le gratuit"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin ! L'assistant de configuration facilite la correction de vos e-mails."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativement, essayez d'utiliser une adresse d'exp\u00e9diteur qui correspond au domaine de votre site web (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Veuillez entrer une adresse e-mail valide. Vos notifications ne seront pas envoy\u00e9es si le champ n'est pas correctement rempli."],"From Name":["De la part de Nom"],"From Email":["De l'email"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'adresse e-mail actuelle de l'exp\u00e9diteur peut ne pas correspondre au nom de domaine de votre site web (%1$s). Cela peut entra\u00eener le blocage ou le marquage comme spam de vos e-mails de notification. Alternativement, essayez d'utiliser une adresse de l'exp\u00e9diteur qui correspond au nom de domaine de votre site web (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'adresse e-mail actuelle 'De' peut ne pas correspondre au nom de domaine de votre site web (%s). Cela peut entra\u00eener le blocage de vos e-mails de notification ou leur marquage comme spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Pr\u00eat \u00e0 aller au-del\u00e0 du plan gratuit ?"],"Upgrade now":["Mettez \u00e0 niveau maintenant"],"and unlock the full power of SureForms!":["et d\u00e9bloquez toute la puissance de SureForms !"],"Upgrade SureForms":["Mettre \u00e0 niveau SureForms"],"Upgrade Now":["Mettez \u00e0 niveau maintenant"],"Entries older than the selected days will be deleted.":["Les entr\u00e9es plus anciennes que les jours s\u00e9lectionn\u00e9s seront supprim\u00e9es."],"Entries Time Period":["P\u00e9riode des entr\u00e9es"],"Notifications can use only one From Email so please enter a single address.":["Les notifications ne peuvent utiliser qu'une seule adresse e-mail d'exp\u00e9diteur, veuillez donc entrer une seule adresse."],"Select Page to redirect":["S\u00e9lectionnez la page \u00e0 rediriger"],"Search for a page":["Rechercher une page"],"Select a page":["S\u00e9lectionnez une page"],"Form Validation":["Validation de formulaire"],"Required Error Messages":["Messages d'erreur requis"],"Other Error Messages":["Autres messages d'erreur"],"Input Field Unique":["Champ de saisie unique"],"Email Field Unique":["Champ de courriel unique"],"Invalid URL":["URL invalide"],"Phone Field Unique":["Champ de t\u00e9l\u00e9phone unique"],"Invalid Field Number Block":["Bloc de num\u00e9ro de champ invalide"],"Invalid Email":["Email invalide"],"Number Minimum Value":["Valeur minimale du nombre"],"Number Maximum Value":["Valeur maximale du nombre"],"Dropdown Minimum Selections":["S\u00e9lections minimales du menu d\u00e9roulant"],"Dropdown Maximum Selections":["S\u00e9lections Maximales du Menu D\u00e9roulant"],"Multiple Choice Minimum Selections":["S\u00e9lections minimales \u00e0 choix multiple"],"Multiple Choice Maximum Selections":["S\u00e9lections maximales \u00e0 choix multiple"],"Input Field":["Champ de saisie"],"Email Field":["Champ de courriel"],"URL Field":["Champ URL"],"Phone Field":["Champ T\u00e9l\u00e9phone"],"Textarea Field":["Champ de zone de texte"],"Checkbox Field":["Champ de case \u00e0 cocher"],"Dropdown Field":["Champ d\u00e9roulant"],"Multiple Choice Field":["Champ \u00e0 choix multiple"],"Address Field":["Champ d'adresse"],"Number Field":["Champ num\u00e9rique"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Pour activer la fonctionnalit\u00e9 reCAPTCHA sur vos SureForms, veuillez activer l'option reCAPTCHA dans les param\u00e8tres de vos blocs et s\u00e9lectionner la version. Ajoutez ici la cl\u00e9 secr\u00e8te et la cl\u00e9 de site de Google reCAPTCHA. reCAPTCHA sera ajout\u00e9 \u00e0 votre page sur le front-end."],"Enter your %s here":["Entrez votre %s ici"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Pour activer hCAPTCHA, veuillez ajouter votre cl\u00e9 de site et votre cl\u00e9 secr\u00e8te. Configurez ces param\u00e8tres dans le formulaire individuel."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Pour activer Cloudflare Turnstile, veuillez ajouter votre cl\u00e9 de site et votre cl\u00e9 secr\u00e8te. Configurez ces param\u00e8tres dans le formulaire individuel."],"Save":["Sauvegarder"],"Anonymous Analytics":["Analytique Anonyme"],"Learn More":["En savoir plus"],"Admin Notification":["Notification d'administration"],"Enable Admin Notification":["Activer la notification administrateur"],"Admin notifications keep you informed about new form entries since your last visit.":["Les notifications administratives vous informent des nouvelles entr\u00e9es de formulaire depuis votre derni\u00e8re visite."],"Skip":["Passer"],"Continue":["Continuer"],"Maximum Number of Entries":["Nombre maximum d'entr\u00e9es"],"Maximum Entries":["Entr\u00e9es maximales"],"Response Description After Maximum Entries":["Description de la r\u00e9ponse apr\u00e8s le nombre maximum d'entr\u00e9es"],"Get Started":["Commencer"],"Integration":["Int\u00e9gration"],"Connect Native Integrations with SureForms":["Connectez les int\u00e9grations natives avec SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["D\u00e9bloquez des int\u00e9grations puissantes dans le plan Premium pour automatiser vos flux de travail et connecter SureForms directement \u00e0 vos outils pr\u00e9f\u00e9r\u00e9s."],"Send form submissions straight to CRMs, email, and marketing platforms":["Envoyez les soumissions de formulaires directement aux CRM, par e-mail et aux plateformes de marketing"],"Automate repetitive tasks with seamless data syncing":["Automatisez les t\u00e2ches r\u00e9p\u00e9titives avec une synchronisation des donn\u00e9es transparente"],"Access exclusive native integrations for faster workflows":["Acc\u00e9dez \u00e0 des int\u00e9grations natives exclusives pour des flux de travail plus rapides"],"Expected format for emails - email@sureforms.com or John Doe ":["Format attendu pour les e-mails - email@sureforms.com ou John Doe "],"Payments":["Paiements"],"Stripe account disconnected successfully.":["Compte Stripe d\u00e9connect\u00e9 avec succ\u00e8s."],"Failed to create webhook.":["\u00c9chec de la cr\u00e9ation du webhook."],"Failed to connect to Stripe.":["\u00c9chec de la connexion \u00e0 Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Base de connaissances"],"What\u2019s New":["Quoi de neuf"],"out of":["hors de"],"No entries found":["Aucune entr\u00e9e trouv\u00e9e"],"delete":["supprimer"],"Please type \"%s\" in the input box":["Veuillez taper \"%s\" dans la bo\u00eete de saisie"],"To confirm, type \"%s\" in the box below:":["Pour confirmer, tapez \"%s\" dans la case ci-dessous :"],"Type \"%s\"":["Tapez \"%s\""],"Go to OttoKit Settings":["Acc\u00e9dez aux param\u00e8tres OttoKit"],"USD - US Dollar":["USD - Dollar am\u00e9ricain"],"Payment Mode":["Mode de paiement"],"Test Mode":["Mode Test"],"Live Mode":["Mode en direct"],"Action":["Action"],"General Settings":["Param\u00e8tres g\u00e9n\u00e9raux"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Configurez les r\u00e9sum\u00e9s d'e-mails, les alertes administratives et les pr\u00e9f\u00e9rences de donn\u00e9es pour g\u00e9rer vos formulaires en toute simplicit\u00e9."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personnalisez les messages d'erreur par d\u00e9faut affich\u00e9s lorsque les utilisateurs soumettent des entr\u00e9es de formulaire invalides ou incompl\u00e8tes."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Activez la protection contre le spam pour vos formulaires en utilisant des services CAPTCHA ou une s\u00e9curit\u00e9 de type honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Connectez et g\u00e9rez vos passerelles de paiement pour accepter en toute s\u00e9curit\u00e9 les transactions via vos formulaires."],"1% transaction and payment gateway fees apply.":["Des frais de transaction et de passerelle de paiement de 1 % s'appliquent."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Des frais de transaction et de passerelle de paiement de 2,9 % s'appliquent. Activez la licence pour r\u00e9duire les frais de transaction."],"2.9% transaction and payment gateway fees apply.":["Des frais de transaction et de passerelle de paiement de 2,9 % s'appliquent."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Veuillez visiter %1$s, supprimer un webhook inutilis\u00e9, puis cliquer ci-dessous pour r\u00e9essayer."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms n'a pas pu cr\u00e9er un webhook car votre compte Stripe a \u00e9puis\u00e9 ses emplacements gratuits. Les webhooks sont n\u00e9cessaires pour recevoir des mises \u00e0 jour sur les paiements."],"Stripe Dashboard":["Tableau de bord Stripe"],"Creating\u2026":["Cr\u00e9ation\u2026"],"Create Webhook":["Cr\u00e9er un Webhook"],"Successfully connected to Stripe!":["Connexion r\u00e9ussie \u00e0 Stripe !"],"Invalid response from server. Please try again.":["R\u00e9ponse invalide du serveur. Veuillez r\u00e9essayer."],"Failed to disconnect Stripe account.":["\u00c9chec de la d\u00e9connexion du compte Stripe."],"Webhook created successfully!":["Webhook cr\u00e9\u00e9 avec succ\u00e8s !"],"Select Currency":["S\u00e9lectionner la devise"],"Select the default currency for payment forms.":["S\u00e9lectionnez la devise par d\u00e9faut pour les formulaires de paiement."],"Connection Status":["Statut de la connexion"],"Disconnect Stripe Account":["D\u00e9connecter le compte Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["\u00cates-vous s\u00fbr de vouloir d\u00e9connecter votre compte Stripe ? Cela arr\u00eatera tous les paiements actifs, abonnements et transactions de formulaire li\u00e9s \u00e0 ce compte."],"Disconnect":["D\u00e9connecter"],"Disconnecting\u2026":["D\u00e9connexion\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook connect\u00e9 avec succ\u00e8s, tous les \u00e9v\u00e9nements Stripe sont suivis."],"Connect your Stripe account to start accepting payments through your forms.":["Connectez votre compte Stripe pour commencer \u00e0 accepter les paiements via vos formulaires."],"Connect to Stripe":["Connectez-vous \u00e0 Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Connectez-vous en toute s\u00e9curit\u00e9 \u00e0 Stripe en quelques clics pour commencer \u00e0 accepter les paiements !"],"Set the total number of submissions allowed for this form.":["D\u00e9finissez le nombre total de soumissions autoris\u00e9es pour ce formulaire."],"Payment Methods":["M\u00e9thodes de paiement"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Le mode test vous permet de traiter les paiements sans frais r\u00e9els. Passez en mode Live pour des transactions r\u00e9elles."],"General Payment Settings":["Param\u00e8tres g\u00e9n\u00e9raux de paiement"],"These settings apply to all payment gateways.":["Ces param\u00e8tres s'appliquent \u00e0 toutes les passerelles de paiement."],"Stripe Settings":["Param\u00e8tres Stripe"],"Left ($100)":["Gauche (100 $)"],"Right (100$)":["D'accord (100$)"],"Left Space ($ 100)":["Espace gauche (100 $)"],"Right Space (100 $)":["Espace droit (100 $)"],"Currency Sign Position":["Position du signe mon\u00e9taire"],"Select the position of the currency symbol relative to the amount.":["S\u00e9lectionnez la position du symbole mon\u00e9taire par rapport au montant."],"Learn":["Apprendre"],"Enable email summaries":["Activer les r\u00e9sum\u00e9s par e-mail"],"Enable IP logging":["Activer la journalisation IP"],"Turn on Admin Notification from here.":["Activez la notification d'administration \u00e0 partir d'ici."],"New":["Nouveau"],"Send entries to 100+ popular apps.":["Envoyez des entr\u00e9es \u00e0 plus de 100 applications populaires."],"Build automated workflows that run instantly.":["Cr\u00e9ez des flux de travail automatis\u00e9s qui s'ex\u00e9cutent instantan\u00e9ment."],"Create custom app integrations using our Custom App feature.":["Cr\u00e9ez des int\u00e9grations d'applications personnalis\u00e9es en utilisant notre fonctionnalit\u00e9 d'application personnalis\u00e9e."],"Keep your tools in sync automatically.":["Gardez vos outils synchronis\u00e9s automatiquement."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Cela installera et activera OttoKit sur votre site WordPress pour activer les fonctionnalit\u00e9s d'automatisation."],"Automate Your Forms with OttoKit":["Automatisez vos formulaires avec OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Chaque soumission de formulaire devrait d\u00e9clencher quelque chose \u2014 une alerte Slack, un lead CRM, un e-mail de suivi, ou une nouvelle ligne dans Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configurer les autorisations du client IA et les param\u00e8tres du serveur MCP."],"View documentation":["Voir la documentation"],"Copy to clipboard":["Copier dans le presse-papiers"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) ou %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Claude Code"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (projet) ou ~\/.claude.json (global)"],"Cursor":["Curseur"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (projet) ou settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml ou config.json"],"Your client's MCP configuration file":["Le fichier de configuration MCP de votre client"],"Connect Your AI Client":["Connectez votre client IA"],"AI Client":["Client IA"],"Create an Application Password \u2014 ":["Cr\u00e9er un mot de passe d'application \u2014"],"Open Application Passwords":["Ouvrir les mots de passe de l'application"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Ou utilisez cette commande CLI pour ajouter le serveur rapidement (vous devrez toujours d\u00e9finir les variables d'environnement) :"],"Copy the JSON config below into: ":["Copiez la configuration JSON ci-dessous dans :"],"Replace \"your-application-password\" with the password from Step 1.":["Remplacez \"your-application-password\" par le mot de passe de l'\u00e9tape 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 l'endpoint MCP de votre site. WP_API_USERNAME \u2014 votre nom d'utilisateur WordPress. WP_API_PASSWORD \u2014 le mot de passe de l'application que vous avez g\u00e9n\u00e9r\u00e9."],"View setup docs":["Voir les documents de configuration"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Le plugin MCP Adapter est install\u00e9 mais pas actif. Activez-le pour configurer les param\u00e8tres MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Le plugin MCP Adapter est n\u00e9cessaire pour connecter les clients IA \u00e0 vos formulaires. T\u00e9l\u00e9chargez-le et installez-le depuis GitHub, puis activez-le."],"Download the latest release from":["T\u00e9l\u00e9chargez la derni\u00e8re version depuis"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installez le plugin via Extensions > Ajouter une nouvelle extension > T\u00e9l\u00e9verser une extension."],"Activate the MCP Adapter plugin.":["Activez le plugin d'adaptateur MCP."],"Activating\u2026":["Activation\u2026"],"Activate MCP Adapter":["Activer l'adaptateur MCP"],"Download MCP Adapter":["T\u00e9l\u00e9charger l'adaptateur MCP"],"Experimental":["Exp\u00e9rimental"],"Enable Abilities":["Activer les capacit\u00e9s"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Enregistrez les capacit\u00e9s de SureForms avec l'API des capacit\u00e9s de WordPress. Lorsqu'elle est activ\u00e9e, les clients IA peuvent lister, lire, cr\u00e9er, modifier et supprimer vos formulaires et entr\u00e9es. Lorsqu'elle est d\u00e9sactiv\u00e9e, aucune capacit\u00e9 n'est enregistr\u00e9e et les clients IA ne peuvent effectuer aucune action sur vos formulaires."],"Abilities API \u2014 Edit":["API des capacit\u00e9s \u2014 Modifier"],"Enable Edit Abilities":["Activer les capacit\u00e9s d'\u00e9dition"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Lorsqu'elle est activ\u00e9e, les clients IA peuvent cr\u00e9er de nouveaux formulaires, mettre \u00e0 jour les titres, les champs et les param\u00e8tres des formulaires, dupliquer des formulaires et modifier les statuts des entr\u00e9es. Lorsqu'elle est d\u00e9sactiv\u00e9e, ces capacit\u00e9s sont d\u00e9senregistr\u00e9es et les clients IA peuvent uniquement lire vos donn\u00e9es."],"Abilities API \u2014 Delete":["API des capacit\u00e9s \u2014 Supprimer"],"Enable Delete Abilities":["Activer les capacit\u00e9s de suppression"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Lorsqu'ils sont activ\u00e9s, les clients IA peuvent supprimer d\u00e9finitivement des formulaires et des entr\u00e9es. Les donn\u00e9es supprim\u00e9es ne peuvent pas \u00eatre r\u00e9cup\u00e9r\u00e9es. Lorsqu'ils sont d\u00e9sactiv\u00e9s, les capacit\u00e9s de suppression sont d\u00e9senregistr\u00e9es et les clients IA ne peuvent supprimer aucune donn\u00e9e."],"MCP Server":["Serveur MCP"],"Enable MCP Server":["Activer le serveur MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Cr\u00e9e un point de terminaison SureForms MCP d\u00e9di\u00e9 auquel les clients IA comme Claude peuvent se connecter. Lorsqu'il est d\u00e9sactiv\u00e9, le point de terminaison est supprim\u00e9 et les clients IA externes ne peuvent pas d\u00e9couvrir ou appeler les capacit\u00e9s de SureForms."],"Learn more":["En savoir plus"],"MCP Adapter Required":["Adaptateur MCP requis"],"Heading 1":["Titre 1"],"Heading 2":["Titre 2"],"Heading 3":["Titre 3"],"Heading 4":["Titre 4"],"Heading 5":["Titre 5"],"Heading 6":["Titre 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configurez la cl\u00e9 API Google Maps pour la saisie semi-automatique des adresses et l'aper\u00e7u de la carte."],"Help shape the future of SureForms":["Aidez \u00e0 fa\u00e7onner l'avenir de SureForms"],"Enable Google Address Autocomplete":["Activer la saisie semi-automatique d'adresse Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Passez au plan d'affaires SureForms pour ajouter la saisie semi-automatique d'adresses aliment\u00e9e par Google avec un aper\u00e7u de la carte interactive \u00e0 vos formulaires."],"Auto-suggest addresses as users type for faster, error-free submissions":["Sugg\u00e9rer automatiquement des adresses au fur et \u00e0 mesure que les utilisateurs tapent pour des soumissions plus rapides et sans erreur"],"Show an interactive map preview with draggable pin for precise locations":["Afficher un aper\u00e7u de carte interactif avec un rep\u00e8re d\u00e9pla\u00e7able pour des emplacements pr\u00e9cis"],"Automatically populate address fields like city, state, and postal code":["Remplir automatiquement les champs d'adresse tels que la ville, l'\u00e9tat et le code postal"],"This form is now closed as we've received all the entries.":["Ce formulaire est maintenant ferm\u00e9 car nous avons re\u00e7u toutes les inscriptions."],"Thank you for contacting us! We will be in touch with you shortly.":["Merci de nous avoir contact\u00e9s ! Nous vous contacterons sous peu."],"Saving\u2026":["Enregistrement\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Lorsqu'il est activ\u00e9, ce formulaire ne stockera pas l'IP de l'utilisateur, le nom du navigateur et le nom de l'appareil dans les entr\u00e9es."],"Form data":["Donn\u00e9es du formulaire"],"Unsaved changes":["Modifications non enregistr\u00e9es"],"Keep editing":["Continuez \u00e0 \u00e9diter"],"Global Defaults":["Param\u00e8tres par d\u00e9faut globaux"],"Form Restrictions":["Restrictions de formulaire"],"Configure default settings that apply to newly created forms.":["Configurer les param\u00e8tres par d\u00e9faut qui s'appliquent aux formulaires nouvellement cr\u00e9\u00e9s."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Collectez des informations non sensibles de votre site web, telles que la version PHP et les fonctionnalit\u00e9s utilis\u00e9es, pour nous aider \u00e0 corriger les bugs plus rapidement, \u00e0 prendre des d\u00e9cisions plus \u00e9clair\u00e9es et \u00e0 d\u00e9velopper des fonctionnalit\u00e9s qui comptent r\u00e9ellement pour vous."],"Failed to load pages. Please refresh and try again.":["\u00c9chec du chargement des pages. Veuillez actualiser et r\u00e9essayer."],"Failed to load settings. Please refresh and try again.":["\u00c9chec du chargement des param\u00e8tres. Veuillez actualiser et r\u00e9essayer."],"Form Validation fields cannot be left blank.":["Les champs de validation de formulaire ne peuvent pas \u00eatre laiss\u00e9s vides."],"Recipient email is required when email summaries are enabled.":["L'adresse e-mail du destinataire est requise lorsque les r\u00e9sum\u00e9s par e-mail sont activ\u00e9s."],"Please enter a valid recipient email.":["Veuillez entrer une adresse e-mail de destinataire valide."],"Settings saved.":["Param\u00e8tres enregistr\u00e9s."],"Failed to save settings.":["\u00c9chec de l'enregistrement des param\u00e8tres."],"Some settings failed to save. Please retry.":["Certains param\u00e8tres n'ont pas pu \u00eatre enregistr\u00e9s. Veuillez r\u00e9essayer."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Certains champs contiennent des modifications non enregistr\u00e9es. Abandonnez-les pour continuer, ou restez pour enregistrer vos modifications."],"Discard & switch":["Annuler et changer"],"Import":["Importer"],"Migration":["Migration"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importer des formulaires depuis Contact Form 7, WPForms, Gravity Forms et d'autres plugins dans SureForms."],"Could not generate the import preview.":["Impossible de g\u00e9n\u00e9rer l'aper\u00e7u de l'importation."],"Import failed. Please try again or check your error logs.":["\u00c9chec de l'importation. Veuillez r\u00e9essayer ou v\u00e9rifier vos journaux d'erreurs."],"Go back":["Retourne"],"Update & import":["Mise \u00e0 jour et importation"],"Confirm & import":["Confirmer et importer"],"Form #%s":["Formulaire n\u00b0%s"],"%d field will import":["%d champ sera import\u00e9"],"Some fields can't be migrated yet":["Certains champs ne peuvent pas encore \u00eatre migr\u00e9s"],"These fields will be skipped - add them manually after the form is created:":["Ces champs seront ignor\u00e9s - ajoutez-les manuellement apr\u00e8s la cr\u00e9ation du formulaire :"],"Some forms could not be parsed":["Certains formulaires n'ont pas pu \u00eatre analys\u00e9s"],"Update existing":["Mettre \u00e0 jour l'existant"],"Create a new copy":["Cr\u00e9er une nouvelle copie"],"Could not load forms for this source.":["Impossible de charger les formulaires pour cette source."],"(untitled form)":["(formulaire sans titre)"],"Previously imported":["Import\u00e9 pr\u00e9c\u00e9demment"],"Open existing SureForms form in a new tab":["Ouvrir le formulaire SureForms existant dans un nouvel onglet"],"On re-import":["Lors de la r\u00e9importation"],"No forms found in this plugin.":["Aucun formulaire trouv\u00e9 dans ce plugin."],"%1$d of %2$d form selected":["%1$d sur %2$d formulaire s\u00e9lectionn\u00e9"],"Preview update":["Aper\u00e7u de la mise \u00e0 jour"],"Preview import":["Aper\u00e7u de l'importation"],"Migration complete":["Migration termin\u00e9e"],"Nothing was imported":["Rien n'a \u00e9t\u00e9 import\u00e9"],"%d form was imported into SureForms.":["%d formulaire a \u00e9t\u00e9 import\u00e9 dans SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Aucun des formulaires s\u00e9lectionn\u00e9s n'a pu \u00eatre migr\u00e9. Voir les avertissements ci-dessous pour plus de d\u00e9tails."],"Imported forms":["Formulaires import\u00e9s"],"Edit in SureForms":["Modifier dans SureForms"],"Some fields were skipped during import":["Certains champs ont \u00e9t\u00e9 ignor\u00e9s lors de l'importation"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Ajoutez-les manuellement dans l'\u00e9diteur de formulaire - ils n'ont pas encore d'\u00e9quivalent SureForms :"],"Some forms failed to import":["Certains formulaires n'ont pas pu \u00eatre import\u00e9s"],"Import more forms":["Importer plus de formulaires"],"Could not load the list of importable plugins.":["Impossible de charger la liste des plugins importables."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Aucun plugin de formulaire pris en charge n'est actif sur ce site. Activez-en un (comme Contact Form 7) avec au moins un formulaire pour l'importer dans SureForms."],"Plugin":["Plugin"],"%d form":["%d formulaire"],"No forms":["Pas de formulaires"],"Multiple choice":["Choix multiple"],"Consent":["Consentement"],"The image uploaded but could not be inserted. Please try again.":["L'image a \u00e9t\u00e9 t\u00e9l\u00e9charg\u00e9e mais n'a pas pu \u00eatre ins\u00e9r\u00e9e. Veuillez r\u00e9essayer."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["L'image n'a pas pu \u00eatre t\u00e9l\u00e9charg\u00e9e. Elle est peut-\u00eatre trop grande, ou vous n'avez pas la permission de t\u00e9l\u00e9charger des fichiers."],"Please choose an image file.":["Veuillez choisir un fichier image."],"Show views and conversion rate":["Afficher les vues et le taux de conversion"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Ajoute les colonnes Vues et Taux de conversion \u00e0 la liste des formulaires. Une vue est compt\u00e9e une fois par visite de page lorsque le formulaire appara\u00eet \u00e0 l'\u00e9cran, et le taux de conversion est la part de ces vues qui se sont termin\u00e9es par une soumission. Le comptage commence la premi\u00e8re fois que vous l'activez, donc les soumissions re\u00e7ues avant cela ne sont pas compt\u00e9es dans le taux. Le d\u00e9sactiver par la suite ne fait que masquer les colonnes \u2014 le comptage continue, donc les chiffres sont \u00e0 jour si vous le r\u00e9activez."],"Log cleared.":["Journal effac\u00e9."],"Could not clear the log.":["Impossible d'effacer le journal."],"Enable logs":["Activer les journaux"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Enregistre les \u00e9checs de soumission de formulaire signal\u00e9s par le navigateur, afin que vous puissiez envoyer le journal au support au lieu de lire la console. Rien n'est \u00e9crit tant que vos formulaires fonctionnent, et les valeurs soumises ne sont jamais stock\u00e9es."],"Download log":["T\u00e9l\u00e9charger le journal"],"Confirm delete":["Confirmer la suppression"],"Log is full \u2014 download and clear it to keep recording.":["Le journal est plein \u2014 t\u00e9l\u00e9chargez-le et effacez-le pour continuer l'enregistrement."],"Form Views & Conversion":["Vues de formulaire et conversion"],"Logs":["Journaux"],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}} \ No newline at end of file +{"translation-revision-date":"2024-12-13T13:12:49+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tableau de bord"],"Settings":["Param\u00e8tres"],"Entries":["Entr\u00e9es"],"Status":["Statut"],"Form":["Formulaire"],"Activated":["Activ\u00e9"],"Activate":["Activer"],"Address":["Adresse"],"Checkbox":["Case \u00e0 cocher"],"Dropdown":["Menu d\u00e9roulant"],"Email":["Email"],"Number":["Nombre"],"Phone":["T\u00e9l\u00e9phone"],"Textarea":["Zone de texte"],"Monday":["Lundi"],"Forms":["Formulaires"],"New Form Submission - %s":["Nouvelle soumission de formulaire - %s"],"GitHub":["GitHub"],"(no title)":["(pas de titre)"],"General":["G\u00e9n\u00e9ral"],"No tags available":["Aucune \u00e9tiquette disponible"],"Back":["Retour"],"Generic tags":["Balises g\u00e9n\u00e9riques"],"Other":["Autre"],"Confirm":["Confirmer"],"Cancel":["Annuler"],"Install":["Installer"],"Plugin Installation failed, Please try again later.":["L'installation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"Plugin activation failed, Please try again later.":["L'activation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"Integrations":["Int\u00e9grations"],"What's New?":["Quoi de neuf ?"],"Core":["C\u0153ur"],"Unlicensed":["Sans licence"],"Connecting\u2026":["Connexion\u2026"],"Install & Activate":["Installer et activer"],"Compliance Settings":["Param\u00e8tres de conformit\u00e9"],"Enable GDPR Compliance":["Activer la conformit\u00e9 RGPD"],"Never store entry data after form submission":["Ne jamais conserver les donn\u00e9es d'entr\u00e9e apr\u00e8s la soumission du formulaire"],"When enabled this form will never store Entries.":["Lorsqu'il est activ\u00e9, ce formulaire ne stockera jamais les entr\u00e9es."],"Automatically delete entries":["Supprimer automatiquement les entr\u00e9es"],"When enabled this form will automatically delete entries after a certain period of time.":["Lorsque cette option est activ\u00e9e, ce formulaire supprimera automatiquement les entr\u00e9es apr\u00e8s une certaine p\u00e9riode de temps."],"Entries older than the days set will be deleted automatically.":["Les entr\u00e9es plus anciennes que le nombre de jours d\u00e9fini seront supprim\u00e9es automatiquement."],"Visual":["Visuel"],"HTML":["HTML"],"All Data":["Toutes les donn\u00e9es"],"Add Shortcode":["Ajouter un shortcode"],"Form input tags":["Balises de saisie de formulaire"],"Comma separated values are also accepted.":["Les valeurs s\u00e9par\u00e9es par des virgules sont \u00e9galement accept\u00e9es."],"Email Notification":["Notification par e-mail"],"Name":["Nom"],"Send Email To":["Envoyer un e-mail \u00e0"],"Subject":["Sujet"],"CC":["CC"],"BCC":["Cci"],"Reply To":["R\u00e9pondre \u00e0"],"Add Key":["Ajouter une cl\u00e9"],"Add Value":["Ajouter de la valeur"],"Add":["Ajouter"],"Confirmation Message":["Message de confirmation"],"After Form Submission":["Apr\u00e8s la soumission du formulaire"],"Hide Form":["Masquer le formulaire"],"Reset Form":["R\u00e9initialiser le formulaire"],"Custom URL":["URL personnalis\u00e9e"],"Add Query Parameters":["Ajouter des param\u00e8tres de requ\u00eate"],"Select if you want to add key-value pairs for form fields to include in query parameters":["S\u00e9lectionnez si vous souhaitez ajouter des paires cl\u00e9-valeur pour les champs de formulaire \u00e0 inclure dans les param\u00e8tres de requ\u00eate"],"Query Parameters":["Param\u00e8tres de requ\u00eate"],"Success Message":["Message de r\u00e9ussite"],"Redirect":["Rediriger"],"Redirect to":["Rediriger vers"],"Page":["Page"],"Form Confirmation":["Confirmation du formulaire"],"Confirmation Type":["Type de confirmation"],"Clear":["Clair"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invisible"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Validations"],"Spam Protection":["Protection contre le spam"],"If this option is turned on, the user's IP address will be saved with the form data":["Si cette option est activ\u00e9e, l'adresse IP de l'utilisateur sera enregistr\u00e9e avec les donn\u00e9es du formulaire"],"Enable Honeypot Security":["Activer la s\u00e9curit\u00e9 Honeypot"],"Enable Honeypot Security for better spam protection":["Activez la s\u00e9curit\u00e9 Honeypot pour une meilleure protection contre le spam"],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s repr\u00e9sente le nombre minimum de s\u00e9lections n\u00e9cessaires. Par exemple : \u00ab Un minimum de 2 s\u00e9lections est requis. \u00bb"],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s repr\u00e9sente le nombre maximum de s\u00e9lections autoris\u00e9es. Par exemple : \u00ab Maximum 4 s\u00e9lections autoris\u00e9es. \u00bb"],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s repr\u00e9sente le nombre minimum de choix n\u00e9cessaires. Par exemple : \u00ab Au moins 1 s\u00e9lection est requise. \u00bb"],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s repr\u00e9sente le nombre maximum de choix autoris\u00e9s. Par exemple : \u00ab Maximum de 3 s\u00e9lections autoris\u00e9es. \u00bb"]," Error Message":["Message d'erreur"],"Email Summaries":["R\u00e9sum\u00e9s d'e-mails"],"Tuesday":["Mardi"],"Wednesday":["Mercredi"],"Thursday":["Jeudi"],"Friday":["Vendredi"],"Saturday":["Samedi"],"Sunday":["Dimanche"],"Schedule Reports":["Programmer les rapports"],"Auto":["Auto"],"Light":["Lumi\u00e8re"],"Dark":["Sombre"],"Turnstile":["Tourniquet"],"Get Keys":["Obtenir les cl\u00e9s"],"Documentation":["Documentation"],"Site Key":["Cl\u00e9 du site"],"Secret Key":["Cl\u00e9 secr\u00e8te"],"Cloudflare Turnstile":["Tourniquet Cloudflare"],"Appearance Mode":["Mode d'apparence"],"Text":["Texte"],"Test Email":["Email de test"],"IP Logging":["Journalisation IP"],"Honeypot":["Pot de miel"],"Confirmation Email Mismatch Message":["Message de non-correspondance de l'email de confirmation"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s repr\u00e9sente la valeur d'entr\u00e9e minimale. Par exemple : \"La valeur minimale est 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s repr\u00e9sente la valeur d'entr\u00e9e maximale. Par exemple : \"La valeur maximale est 100.\""],"OttoKit":["OttoKit"],"Connect with OttoKit":["Connectez-vous avec OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'adresse e-mail 'De' actuelle ne correspond pas au nom de domaine de votre site web (%1$s). Cela peut entra\u00eener le blocage ou le marquage de vos e-mails de notification comme spam. Alternativement, essayez d'utiliser une adresse De qui correspond au nom de domaine de votre site web (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'adresse e-mail 'De' actuelle ne correspond pas au nom de domaine de votre site web (%s). Cela peut entra\u00eener le blocage de vos e-mails de notification ou leur classement en tant que spam."],"We strongly recommend that you install the free ":["Nous vous recommandons vivement d'installer le gratuit"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin ! L'assistant de configuration facilite la correction de vos e-mails."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["Alternativement, essayez d'utiliser une adresse d'exp\u00e9diteur qui correspond au domaine de votre site web (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Veuillez entrer une adresse e-mail valide. Vos notifications ne seront pas envoy\u00e9es si le champ n'est pas correctement rempli."],"From Name":["De la part de Nom"],"From Email":["De l'email"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'adresse e-mail actuelle de l'exp\u00e9diteur peut ne pas correspondre au nom de domaine de votre site web (%1$s). Cela peut entra\u00eener le blocage ou le marquage comme spam de vos e-mails de notification. Alternativement, essayez d'utiliser une adresse de l'exp\u00e9diteur qui correspond au nom de domaine de votre site web (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'adresse e-mail actuelle 'De' peut ne pas correspondre au nom de domaine de votre site web (%s). Cela peut entra\u00eener le blocage de vos e-mails de notification ou leur marquage comme spam."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Pr\u00eat \u00e0 aller au-del\u00e0 du plan gratuit ?"],"Upgrade now":["Mettez \u00e0 niveau maintenant"],"and unlock the full power of SureForms!":["et d\u00e9bloquez toute la puissance de SureForms !"],"Upgrade SureForms":["Mettre \u00e0 niveau SureForms"],"Upgrade Now":["Mettez \u00e0 niveau maintenant"],"Entries older than the selected days will be deleted.":["Les entr\u00e9es plus anciennes que les jours s\u00e9lectionn\u00e9s seront supprim\u00e9es."],"Entries Time Period":["P\u00e9riode des entr\u00e9es"],"Notifications can use only one From Email so please enter a single address.":["Les notifications ne peuvent utiliser qu'une seule adresse e-mail d'exp\u00e9diteur, veuillez donc entrer une seule adresse."],"Select Page to redirect":["S\u00e9lectionnez la page \u00e0 rediriger"],"Search for a page":["Rechercher une page"],"Select a page":["S\u00e9lectionnez une page"],"Form Validation":["Validation de formulaire"],"Required Error Messages":["Messages d'erreur requis"],"Other Error Messages":["Autres messages d'erreur"],"Input Field Unique":["Champ de saisie unique"],"Email Field Unique":["Champ de courriel unique"],"Invalid URL":["URL invalide"],"Phone Field Unique":["Champ de t\u00e9l\u00e9phone unique"],"Invalid Field Number Block":["Bloc de num\u00e9ro de champ invalide"],"Invalid Email":["Email invalide"],"Number Minimum Value":["Valeur minimale du nombre"],"Number Maximum Value":["Valeur maximale du nombre"],"Dropdown Minimum Selections":["S\u00e9lections minimales du menu d\u00e9roulant"],"Dropdown Maximum Selections":["S\u00e9lections Maximales du Menu D\u00e9roulant"],"Multiple Choice Minimum Selections":["S\u00e9lections minimales \u00e0 choix multiple"],"Multiple Choice Maximum Selections":["S\u00e9lections maximales \u00e0 choix multiple"],"Input Field":["Champ de saisie"],"Email Field":["Champ de courriel"],"URL Field":["Champ URL"],"Phone Field":["Champ T\u00e9l\u00e9phone"],"Textarea Field":["Champ de zone de texte"],"Checkbox Field":["Champ de case \u00e0 cocher"],"Dropdown Field":["Champ d\u00e9roulant"],"Multiple Choice Field":["Champ \u00e0 choix multiple"],"Address Field":["Champ d'adresse"],"Number Field":["Champ num\u00e9rique"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Pour activer la fonctionnalit\u00e9 reCAPTCHA sur vos SureForms, veuillez activer l'option reCAPTCHA dans les param\u00e8tres de vos blocs et s\u00e9lectionner la version. Ajoutez ici la cl\u00e9 secr\u00e8te et la cl\u00e9 de site de Google reCAPTCHA. reCAPTCHA sera ajout\u00e9 \u00e0 votre page sur le front-end."],"Enter your %s here":["Entrez votre %s ici"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Pour activer hCAPTCHA, veuillez ajouter votre cl\u00e9 de site et votre cl\u00e9 secr\u00e8te. Configurez ces param\u00e8tres dans le formulaire individuel."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Pour activer Cloudflare Turnstile, veuillez ajouter votre cl\u00e9 de site et votre cl\u00e9 secr\u00e8te. Configurez ces param\u00e8tres dans le formulaire individuel."],"Save":["Sauvegarder"],"Anonymous Analytics":["Analytique Anonyme"],"Learn More":["En savoir plus"],"Admin Notification":["Notification d'administration"],"Enable Admin Notification":["Activer la notification administrateur"],"Admin notifications keep you informed about new form entries since your last visit.":["Les notifications administratives vous informent des nouvelles entr\u00e9es de formulaire depuis votre derni\u00e8re visite."],"Skip":["Passer"],"Continue":["Continuer"],"Maximum Number of Entries":["Nombre maximum d'entr\u00e9es"],"Maximum Entries":["Entr\u00e9es maximales"],"Response Description After Maximum Entries":["Description de la r\u00e9ponse apr\u00e8s le nombre maximum d'entr\u00e9es"],"Get Started":["Commencer"],"Integration":["Int\u00e9gration"],"Connect Native Integrations with SureForms":["Connectez les int\u00e9grations natives avec SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["D\u00e9bloquez des int\u00e9grations puissantes dans le plan Premium pour automatiser vos flux de travail et connecter SureForms directement \u00e0 vos outils pr\u00e9f\u00e9r\u00e9s."],"Send form submissions straight to CRMs, email, and marketing platforms":["Envoyez les soumissions de formulaires directement aux CRM, par e-mail et aux plateformes de marketing"],"Automate repetitive tasks with seamless data syncing":["Automatisez les t\u00e2ches r\u00e9p\u00e9titives avec une synchronisation des donn\u00e9es transparente"],"Access exclusive native integrations for faster workflows":["Acc\u00e9dez \u00e0 des int\u00e9grations natives exclusives pour des flux de travail plus rapides"],"Expected format for emails - email@sureforms.com or John Doe ":["Format attendu pour les e-mails - email@sureforms.com ou John Doe "],"Payments":["Paiements"],"Stripe account disconnected successfully.":["Compte Stripe d\u00e9connect\u00e9 avec succ\u00e8s."],"Failed to create webhook.":["\u00c9chec de la cr\u00e9ation du webhook."],"Failed to connect to Stripe.":["\u00c9chec de la connexion \u00e0 Stripe."],"Webhook":["Webhook"],"Knowledge Base":["Base de connaissances"],"What\u2019s New":["Quoi de neuf"],"out of":["hors de"],"No entries found":["Aucune entr\u00e9e trouv\u00e9e"],"delete":["supprimer"],"Please type \"%s\" in the input box":["Veuillez taper \"%s\" dans la bo\u00eete de saisie"],"To confirm, type \"%s\" in the box below:":["Pour confirmer, tapez \"%s\" dans la case ci-dessous :"],"Type \"%s\"":["Tapez \"%s\""],"Go to OttoKit Settings":["Acc\u00e9dez aux param\u00e8tres OttoKit"],"USD - US Dollar":["USD - Dollar am\u00e9ricain"],"Payment Mode":["Mode de paiement"],"Test Mode":["Mode Test"],"Live Mode":["Mode en direct"],"Action":["Action"],"General Settings":["Param\u00e8tres g\u00e9n\u00e9raux"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Configurez les r\u00e9sum\u00e9s d'e-mails, les alertes administratives et les pr\u00e9f\u00e9rences de donn\u00e9es pour g\u00e9rer vos formulaires en toute simplicit\u00e9."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personnalisez les messages d'erreur par d\u00e9faut affich\u00e9s lorsque les utilisateurs soumettent des entr\u00e9es de formulaire invalides ou incompl\u00e8tes."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Activez la protection contre le spam pour vos formulaires en utilisant des services CAPTCHA ou une s\u00e9curit\u00e9 de type honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Connectez et g\u00e9rez vos passerelles de paiement pour accepter en toute s\u00e9curit\u00e9 les transactions via vos formulaires."],"1% transaction and payment gateway fees apply.":["Des frais de transaction et de passerelle de paiement de 1 % s'appliquent."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Des frais de transaction et de passerelle de paiement de 2,9 % s'appliquent. Activez la licence pour r\u00e9duire les frais de transaction."],"2.9% transaction and payment gateway fees apply.":["Des frais de transaction et de passerelle de paiement de 2,9 % s'appliquent."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Veuillez visiter %1$s, supprimer un webhook inutilis\u00e9, puis cliquer ci-dessous pour r\u00e9essayer."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms n'a pas pu cr\u00e9er un webhook car votre compte Stripe a \u00e9puis\u00e9 ses emplacements gratuits. Les webhooks sont n\u00e9cessaires pour recevoir des mises \u00e0 jour sur les paiements."],"Stripe Dashboard":["Tableau de bord Stripe"],"Creating\u2026":["Cr\u00e9ation\u2026"],"Create Webhook":["Cr\u00e9er un Webhook"],"Successfully connected to Stripe!":["Connexion r\u00e9ussie \u00e0 Stripe !"],"Invalid response from server. Please try again.":["R\u00e9ponse invalide du serveur. Veuillez r\u00e9essayer."],"Failed to disconnect Stripe account.":["\u00c9chec de la d\u00e9connexion du compte Stripe."],"Webhook created successfully!":["Webhook cr\u00e9\u00e9 avec succ\u00e8s !"],"Select Currency":["S\u00e9lectionner la devise"],"Select the default currency for payment forms.":["S\u00e9lectionnez la devise par d\u00e9faut pour les formulaires de paiement."],"Connection Status":["Statut de la connexion"],"Disconnect Stripe Account":["D\u00e9connecter le compte Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["\u00cates-vous s\u00fbr de vouloir d\u00e9connecter votre compte Stripe ? Cela arr\u00eatera tous les paiements actifs, abonnements et transactions de formulaire li\u00e9s \u00e0 ce compte."],"Disconnect":["D\u00e9connecter"],"Disconnecting\u2026":["D\u00e9connexion\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook connect\u00e9 avec succ\u00e8s, tous les \u00e9v\u00e9nements Stripe sont suivis."],"Connect your Stripe account to start accepting payments through your forms.":["Connectez votre compte Stripe pour commencer \u00e0 accepter les paiements via vos formulaires."],"Connect to Stripe":["Connectez-vous \u00e0 Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Connectez-vous en toute s\u00e9curit\u00e9 \u00e0 Stripe en quelques clics pour commencer \u00e0 accepter les paiements !"],"Set the total number of submissions allowed for this form.":["D\u00e9finissez le nombre total de soumissions autoris\u00e9es pour ce formulaire."],"Payment Methods":["M\u00e9thodes de paiement"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["Le mode test vous permet de traiter les paiements sans frais r\u00e9els. Passez en mode Live pour des transactions r\u00e9elles."],"General Payment Settings":["Param\u00e8tres g\u00e9n\u00e9raux de paiement"],"These settings apply to all payment gateways.":["Ces param\u00e8tres s'appliquent \u00e0 toutes les passerelles de paiement."],"Stripe Settings":["Param\u00e8tres Stripe"],"Left ($100)":["Gauche (100 $)"],"Right (100$)":["D'accord (100$)"],"Left Space ($ 100)":["Espace gauche (100 $)"],"Right Space (100 $)":["Espace droit (100 $)"],"Currency Sign Position":["Position du signe mon\u00e9taire"],"Select the position of the currency symbol relative to the amount.":["S\u00e9lectionnez la position du symbole mon\u00e9taire par rapport au montant."],"Learn":["Apprendre"],"Enable email summaries":["Activer les r\u00e9sum\u00e9s par e-mail"],"Enable IP logging":["Activer la journalisation IP"],"Turn on Admin Notification from here.":["Activez la notification d'administration \u00e0 partir d'ici."],"New":["Nouveau"],"Send entries to 100+ popular apps.":["Envoyez des entr\u00e9es \u00e0 plus de 100 applications populaires."],"Build automated workflows that run instantly.":["Cr\u00e9ez des flux de travail automatis\u00e9s qui s'ex\u00e9cutent instantan\u00e9ment."],"Create custom app integrations using our Custom App feature.":["Cr\u00e9ez des int\u00e9grations d'applications personnalis\u00e9es en utilisant notre fonctionnalit\u00e9 d'application personnalis\u00e9e."],"Keep your tools in sync automatically.":["Gardez vos outils synchronis\u00e9s automatiquement."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Cela installera et activera OttoKit sur votre site WordPress pour activer les fonctionnalit\u00e9s d'automatisation."],"Automate Your Forms with OttoKit":["Automatisez vos formulaires avec OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Chaque soumission de formulaire devrait d\u00e9clencher quelque chose \u2014 une alerte Slack, un lead CRM, un e-mail de suivi, ou une nouvelle ligne dans Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configurer les autorisations du client IA et les param\u00e8tres du serveur MCP."],"View documentation":["Voir la documentation"],"Copy to clipboard":["Copier dans le presse-papiers"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) ou %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Claude Code"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (projet) ou ~\/.claude.json (global)"],"Cursor":["Curseur"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (projet) ou settings.json > mcp.servers (global)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml ou config.json"],"Your client's MCP configuration file":["Le fichier de configuration MCP de votre client"],"Connect Your AI Client":["Connectez votre client IA"],"AI Client":["Client IA"],"Create an Application Password \u2014 ":["Cr\u00e9er un mot de passe d'application \u2014"],"Open Application Passwords":["Ouvrir les mots de passe de l'application"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Ou utilisez cette commande CLI pour ajouter le serveur rapidement (vous devrez toujours d\u00e9finir les variables d'environnement) :"],"Copy the JSON config below into: ":["Copiez la configuration JSON ci-dessous dans :"],"Replace \"your-application-password\" with the password from Step 1.":["Remplacez \"your-application-password\" par le mot de passe de l'\u00e9tape 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 l'endpoint MCP de votre site. WP_API_USERNAME \u2014 votre nom d'utilisateur WordPress. WP_API_PASSWORD \u2014 le mot de passe de l'application que vous avez g\u00e9n\u00e9r\u00e9."],"View setup docs":["Voir les documents de configuration"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Le plugin MCP Adapter est install\u00e9 mais pas actif. Activez-le pour configurer les param\u00e8tres MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Le plugin MCP Adapter est n\u00e9cessaire pour connecter les clients IA \u00e0 vos formulaires. T\u00e9l\u00e9chargez-le et installez-le depuis GitHub, puis activez-le."],"Download the latest release from":["T\u00e9l\u00e9chargez la derni\u00e8re version depuis"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installez le plugin via Extensions > Ajouter une nouvelle extension > T\u00e9l\u00e9verser une extension."],"Activate the MCP Adapter plugin.":["Activez le plugin d'adaptateur MCP."],"Activating\u2026":["Activation\u2026"],"Activate MCP Adapter":["Activer l'adaptateur MCP"],"Download MCP Adapter":["T\u00e9l\u00e9charger l'adaptateur MCP"],"Experimental":["Exp\u00e9rimental"],"Enable Abilities":["Activer les capacit\u00e9s"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Enregistrez les capacit\u00e9s de SureForms avec l'API des capacit\u00e9s de WordPress. Lorsqu'elle est activ\u00e9e, les clients IA peuvent lister, lire, cr\u00e9er, modifier et supprimer vos formulaires et entr\u00e9es. Lorsqu'elle est d\u00e9sactiv\u00e9e, aucune capacit\u00e9 n'est enregistr\u00e9e et les clients IA ne peuvent effectuer aucune action sur vos formulaires."],"Abilities API \u2014 Edit":["API des capacit\u00e9s \u2014 Modifier"],"Enable Edit Abilities":["Activer les capacit\u00e9s d'\u00e9dition"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Lorsqu'elle est activ\u00e9e, les clients IA peuvent cr\u00e9er de nouveaux formulaires, mettre \u00e0 jour les titres, les champs et les param\u00e8tres des formulaires, dupliquer des formulaires et modifier les statuts des entr\u00e9es. Lorsqu'elle est d\u00e9sactiv\u00e9e, ces capacit\u00e9s sont d\u00e9senregistr\u00e9es et les clients IA peuvent uniquement lire vos donn\u00e9es."],"Abilities API \u2014 Delete":["API des capacit\u00e9s \u2014 Supprimer"],"Enable Delete Abilities":["Activer les capacit\u00e9s de suppression"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Lorsqu'ils sont activ\u00e9s, les clients IA peuvent supprimer d\u00e9finitivement des formulaires et des entr\u00e9es. Les donn\u00e9es supprim\u00e9es ne peuvent pas \u00eatre r\u00e9cup\u00e9r\u00e9es. Lorsqu'ils sont d\u00e9sactiv\u00e9s, les capacit\u00e9s de suppression sont d\u00e9senregistr\u00e9es et les clients IA ne peuvent supprimer aucune donn\u00e9e."],"MCP Server":["Serveur MCP"],"Enable MCP Server":["Activer le serveur MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Cr\u00e9e un point de terminaison SureForms MCP d\u00e9di\u00e9 auquel les clients IA comme Claude peuvent se connecter. Lorsqu'il est d\u00e9sactiv\u00e9, le point de terminaison est supprim\u00e9 et les clients IA externes ne peuvent pas d\u00e9couvrir ou appeler les capacit\u00e9s de SureForms."],"Learn more":["En savoir plus"],"MCP Adapter Required":["Adaptateur MCP requis"],"Heading 1":["Titre 1"],"Heading 2":["Titre 2"],"Heading 3":["Titre 3"],"Heading 4":["Titre 4"],"Heading 5":["Titre 5"],"Heading 6":["Titre 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configurez la cl\u00e9 API Google Maps pour la saisie semi-automatique des adresses et l'aper\u00e7u de la carte."],"Help shape the future of SureForms":["Aidez \u00e0 fa\u00e7onner l'avenir de SureForms"],"Enable Google Address Autocomplete":["Activer la saisie semi-automatique d'adresse Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Passez au plan d'affaires SureForms pour ajouter la saisie semi-automatique d'adresses aliment\u00e9e par Google avec un aper\u00e7u de la carte interactive \u00e0 vos formulaires."],"Auto-suggest addresses as users type for faster, error-free submissions":["Sugg\u00e9rer automatiquement des adresses au fur et \u00e0 mesure que les utilisateurs tapent pour des soumissions plus rapides et sans erreur"],"Show an interactive map preview with draggable pin for precise locations":["Afficher un aper\u00e7u de carte interactif avec un rep\u00e8re d\u00e9pla\u00e7able pour des emplacements pr\u00e9cis"],"Automatically populate address fields like city, state, and postal code":["Remplir automatiquement les champs d'adresse tels que la ville, l'\u00e9tat et le code postal"],"This form is now closed as we've received all the entries.":["Ce formulaire est maintenant ferm\u00e9 car nous avons re\u00e7u toutes les inscriptions."],"Thank you for contacting us! We will be in touch with you shortly.":["Merci de nous avoir contact\u00e9s ! Nous vous contacterons sous peu."],"Saving\u2026":["Enregistrement\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Lorsqu'il est activ\u00e9, ce formulaire ne stockera pas l'IP de l'utilisateur, le nom du navigateur et le nom de l'appareil dans les entr\u00e9es."],"Form data":["Donn\u00e9es du formulaire"],"Unsaved changes":["Modifications non enregistr\u00e9es"],"Keep editing":["Continuez \u00e0 \u00e9diter"],"Global Defaults":["Param\u00e8tres par d\u00e9faut globaux"],"Form Restrictions":["Restrictions de formulaire"],"Configure default settings that apply to newly created forms.":["Configurer les param\u00e8tres par d\u00e9faut qui s'appliquent aux formulaires nouvellement cr\u00e9\u00e9s."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Collectez des informations non sensibles de votre site web, telles que la version PHP et les fonctionnalit\u00e9s utilis\u00e9es, pour nous aider \u00e0 corriger les bugs plus rapidement, \u00e0 prendre des d\u00e9cisions plus \u00e9clair\u00e9es et \u00e0 d\u00e9velopper des fonctionnalit\u00e9s qui comptent r\u00e9ellement pour vous."],"Failed to load pages. Please refresh and try again.":["\u00c9chec du chargement des pages. Veuillez actualiser et r\u00e9essayer."],"Failed to load settings. Please refresh and try again.":["\u00c9chec du chargement des param\u00e8tres. Veuillez actualiser et r\u00e9essayer."],"Form Validation fields cannot be left blank.":["Les champs de validation de formulaire ne peuvent pas \u00eatre laiss\u00e9s vides."],"Recipient email is required when email summaries are enabled.":["L'adresse e-mail du destinataire est requise lorsque les r\u00e9sum\u00e9s par e-mail sont activ\u00e9s."],"Please enter a valid recipient email.":["Veuillez entrer une adresse e-mail de destinataire valide."],"Settings saved.":["Param\u00e8tres enregistr\u00e9s."],"Failed to save settings.":["\u00c9chec de l'enregistrement des param\u00e8tres."],"Some settings failed to save. Please retry.":["Certains param\u00e8tres n'ont pas pu \u00eatre enregistr\u00e9s. Veuillez r\u00e9essayer."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Certains champs contiennent des modifications non enregistr\u00e9es. Abandonnez-les pour continuer, ou restez pour enregistrer vos modifications."],"Discard & switch":["Annuler et changer"],"Import":["Importer"],"Migration":["Migration"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importer des formulaires depuis Contact Form 7, WPForms, Gravity Forms et d'autres plugins dans SureForms."],"Could not generate the import preview.":["Impossible de g\u00e9n\u00e9rer l'aper\u00e7u de l'importation."],"Import failed. Please try again or check your error logs.":["\u00c9chec de l'importation. Veuillez r\u00e9essayer ou v\u00e9rifier vos journaux d'erreurs."],"Go back":["Retourne"],"Update & import":["Mise \u00e0 jour et importation"],"Confirm & import":["Confirmer et importer"],"Form #%s":["Formulaire n\u00b0%s"],"%d field will import":["%d champ sera import\u00e9"],"Some fields can't be migrated yet":["Certains champs ne peuvent pas encore \u00eatre migr\u00e9s"],"These fields will be skipped - add them manually after the form is created:":["Ces champs seront ignor\u00e9s - ajoutez-les manuellement apr\u00e8s la cr\u00e9ation du formulaire :"],"Some forms could not be parsed":["Certains formulaires n'ont pas pu \u00eatre analys\u00e9s"],"Update existing":["Mettre \u00e0 jour l'existant"],"Create a new copy":["Cr\u00e9er une nouvelle copie"],"Could not load forms for this source.":["Impossible de charger les formulaires pour cette source."],"(untitled form)":["(formulaire sans titre)"],"Previously imported":["Import\u00e9 pr\u00e9c\u00e9demment"],"Open existing SureForms form in a new tab":["Ouvrir le formulaire SureForms existant dans un nouvel onglet"],"On re-import":["Lors de la r\u00e9importation"],"No forms found in this plugin.":["Aucun formulaire trouv\u00e9 dans ce plugin."],"%1$d of %2$d form selected":["%1$d sur %2$d formulaire s\u00e9lectionn\u00e9"],"Preview update":["Aper\u00e7u de la mise \u00e0 jour"],"Preview import":["Aper\u00e7u de l'importation"],"Migration complete":["Migration termin\u00e9e"],"Nothing was imported":["Rien n'a \u00e9t\u00e9 import\u00e9"],"%d form was imported into SureForms.":["%d formulaire a \u00e9t\u00e9 import\u00e9 dans SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Aucun des formulaires s\u00e9lectionn\u00e9s n'a pu \u00eatre migr\u00e9. Voir les avertissements ci-dessous pour plus de d\u00e9tails."],"Imported forms":["Formulaires import\u00e9s"],"Edit in SureForms":["Modifier dans SureForms"],"Some fields were skipped during import":["Certains champs ont \u00e9t\u00e9 ignor\u00e9s lors de l'importation"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Ajoutez-les manuellement dans l'\u00e9diteur de formulaire - ils n'ont pas encore d'\u00e9quivalent SureForms :"],"Some forms failed to import":["Certains formulaires n'ont pas pu \u00eatre import\u00e9s"],"Import more forms":["Importer plus de formulaires"],"Could not load the list of importable plugins.":["Impossible de charger la liste des plugins importables."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Aucun plugin de formulaire pris en charge n'est actif sur ce site. Activez-en un (comme Contact Form 7) avec au moins un formulaire pour l'importer dans SureForms."],"Plugin":["Plugin"],"%d form":["%d formulaire"],"No forms":["Pas de formulaires"],"Multiple choice":["Choix multiple"],"Consent":["Consentement"],"The image uploaded but could not be inserted. Please try again.":["L'image a \u00e9t\u00e9 t\u00e9l\u00e9charg\u00e9e mais n'a pas pu \u00eatre ins\u00e9r\u00e9e. Veuillez r\u00e9essayer."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["L'image n'a pas pu \u00eatre t\u00e9l\u00e9charg\u00e9e. Elle est peut-\u00eatre trop grande, ou vous n'avez pas la permission de t\u00e9l\u00e9charger des fichiers."],"Please choose an image file.":["Veuillez choisir un fichier image."],"Show views and conversion rate":["Afficher les vues et le taux de conversion"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Ajoute les colonnes Vues et Taux de conversion \u00e0 la liste des formulaires. Une vue est compt\u00e9e une fois par visite de page lorsque le formulaire appara\u00eet \u00e0 l'\u00e9cran, et le taux de conversion est la part de ces vues qui se sont termin\u00e9es par une soumission. Le comptage commence la premi\u00e8re fois que vous l'activez, donc les soumissions re\u00e7ues avant cela ne sont pas compt\u00e9es dans le taux. Le d\u00e9sactiver par la suite ne fait que masquer les colonnes \u2014 le comptage continue, donc les chiffres sont \u00e0 jour si vous le r\u00e9activez."],"Log cleared.":["Journal effac\u00e9."],"Could not clear the log.":["Impossible d'effacer le journal."],"Enable logs":["Activer les journaux"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Enregistre les \u00e9checs de soumission de formulaire signal\u00e9s par le navigateur, afin que vous puissiez envoyer le journal au support au lieu de lire la console. Rien n'est \u00e9crit tant que vos formulaires fonctionnent, et les valeurs soumises ne sont jamais stock\u00e9es."],"Download log":["T\u00e9l\u00e9charger le journal"],"Confirm delete":["Confirmer la suppression"],"Log is full \u2014 download and clear it to keep recording.":["Le journal est plein \u2014 t\u00e9l\u00e9chargez-le et effacez-le pour continuer l'enregistrement."],"Form Views & Conversion":["Vues de formulaire et conversion"],"Logs":["Journaux"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Les vues et soumissions par toute personne pouvant \u00e9diter le site sont normalement omises, afin que tester vos propres formulaires ne modifie pas ces chiffres."],"Quill heading picker: default paragraph style\u0004Normal":["Normal"]}}} \ No newline at end of file diff --git a/languages/sureforms-fr_FR-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-fr_FR-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json index aa35e80ab..a271b150a 100644 --- a/languages/sureforms-fr_FR-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json +++ b/languages/sureforms-fr_FR-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json @@ -1 +1 @@ -{"translation-revision-date":"2024-12-13T13:12:49+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tableau de bord"],"Settings":["Param\u00e8tres"],"Entries":["Entr\u00e9es"],"Activated":["Activ\u00e9"],"Advanced Fields":["Champs avanc\u00e9s"],"Select Form":["S\u00e9lectionner le formulaire"],"Create New Form":["Cr\u00e9er un nouveau formulaire"],"Forms":["Formulaires"],"Copy":["Copier"],"Business":["Affaires"],"Next":["Suivant"],"Back":["Retour"],"Cancel":["Annuler"],"This is where your form views will appear":["C'est ici que vos vues de formulaire appara\u00eetront"],"Install":["Installer"],"Plugin Installation failed, Please try again later.":["L'installation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"Plugin activation failed, Please try again later.":["L'activation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"What's New?":["Quoi de neuf ?"],"Core":["C\u0153ur"],"Unlicensed":["Sans licence"],"Upgrade":["Mise \u00e0 niveau"],"Webhooks":["Webhooks"],"Install & Activate":["Installer et activer"],"Conditional Logic":["Logique conditionnelle"],"Premium":["Premium"],"Welcome to SureForms!":["Bienvenue chez SureForms !"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms est un plugin WordPress qui permet aux utilisateurs de cr\u00e9er de magnifiques formulaires gr\u00e2ce \u00e0 une interface de glisser-d\u00e9poser, sans avoir besoin de coder. Il s'int\u00e8gre avec l'\u00e9diteur de blocs WordPress."],"Read Full Guide":["Lire le guide complet"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms : Formulaires WordPress personnalis\u00e9s SIMPLIFI\u00c9S"],"No Date":["Aucune date"],"Invalid Date":["Date invalide"],"Ready to go beyond free plan?":["Pr\u00eat \u00e0 aller au-del\u00e0 du plan gratuit ?"],"Upgrade now":["Mettez \u00e0 niveau maintenant"],"and unlock the full power of SureForms!":["et d\u00e9bloquez toute la puissance de SureForms !"],"Upgrade SureForms":["Mettre \u00e0 niveau SureForms"],"Open Support Ticket":["Ouvrir un ticket de support"],"Help Center":["Centre d'aide"],"Join our Community on Facebook":["Rejoignez notre communaut\u00e9 sur Facebook"],"Leave Us a Review":["Laissez-nous un avis"],"Quick Access":["Acc\u00e8s rapide"],"Upgrade Now":["Mettez \u00e0 niveau maintenant"],"Clear Filters":["Effacer les filtres"],"Unnamed Form":["Formulaire sans nom"],"Forms Overview":["Aper\u00e7u des formulaires"],"Clear Form Filters":["Effacer les filtres du formulaire"],"Clear Date Filters":["Effacer les filtres de date"],"Select Date Range":["S\u00e9lectionner la plage de dates"],"Apply":["Appliquer"],"Please wait for the data to load":["Veuillez attendre le chargement des donn\u00e9es"],"No entries to display":["Aucune entr\u00e9e \u00e0 afficher"],"Once you create a form and start receiving submissions, the data will appear here.":["Une fois que vous cr\u00e9ez un formulaire et commencez \u00e0 recevoir des soumissions, les donn\u00e9es appara\u00eetront ici."],"Free":["Gratuit"],"All Forms":["Tous les formulaires"],"Guided Setup":["Configuration guid\u00e9e"],"Exit Guided Setup":["Quitter l'installation guid\u00e9e"],"Skip":["Passer"],"Build beautiful forms visually":["Cr\u00e9ez de beaux formulaires visuellement"],"Works perfectly on mobile":["Fonctionne parfaitement sur mobile"],"Easy to connect with automation tools":["Facile \u00e0 connecter avec des outils d'automatisation"],"Welcome to SureForms":["Bienvenue chez SureForms"],"Smart, Quick and Powerful Forms.":["Formulaires intelligents, rapides et puissants."],"Let's Get Started":["Commen\u00e7ons"],"Build up to 10 forms using AI":["Cr\u00e9ez jusqu'\u00e0 10 formulaires en utilisant l'IA"],"A secure and private connection":["Une connexion s\u00e9curis\u00e9e et priv\u00e9e"],"Smart form drafts based on your input":["Brouillons de formulaires intelligents bas\u00e9s sur votre saisie"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Commencer \u00e0 partir d'un formulaire vierge n'est pas toujours facile. Notre IA peut vous aider en cr\u00e9ant un brouillon de formulaire bas\u00e9 sur ce que vous essayez de faire \u2014 vous faisant gagner du temps et vous donnant une direction claire."],"To do this, you'll need to connect your account.":["Pour ce faire, vous devrez connecter votre compte."],"Let AI Help You Build Smarter, Faster Forms":["Laissez l'IA vous aider \u00e0 cr\u00e9er des formulaires plus intelligents et plus rapides"],"Here's what that gives you:":["Voici ce que cela vous donne :"],"Continue":["Continuer"],"Connect":["Connecter"],"Works smoothly with forms made using SureForms":["Fonctionne parfaitement avec les formulaires cr\u00e9\u00e9s \u00e0 l'aide de SureForms"],"Helps your emails reach the inbox instead of spam":["Aide vos e-mails \u00e0 atteindre la bo\u00eete de r\u00e9ception au lieu du spam"],"Setup is straightforward, even if you're not technical":["L'installation est simple, m\u00eame si vous n'\u00eates pas technique"],"Lightweight and easy to use without adding clutter":["L\u00e9ger et facile \u00e0 utiliser sans ajouter de d\u00e9sordre"],"Make Sure Your Emails Get Delivered":["Assurez-vous que vos e-mails soient livr\u00e9s"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["La plupart des sites WordPress ont du mal \u00e0 envoyer des e-mails de mani\u00e8re fiable, ce qui signifie que les soumissions de formulaires de votre site pourraient ne pas atteindre votre bo\u00eete de r\u00e9ception \u2014 ou finir dans les spams."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail est un plugin SMTP simple qui aide \u00e0 s'assurer que vos e-mails sont effectivement livr\u00e9s."],"What you will get:":["Ce que vous obtiendrez :"],"Install SureMail":["Installez SureMail"],"AI Form Generation":["G\u00e9n\u00e9ration de formulaires par IA"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Fatigu\u00e9 de cr\u00e9er des formulaires manuellement ? Laissez l'IA faire le travail pour vous. D\u00e9crivez simplement et notre IA cr\u00e9era votre formulaire parfait en quelques secondes."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["D\u00e9composez les formulaires complexes en \u00e9tapes simples, r\u00e9duisant ainsi la surcharge et augmentant les taux de compl\u00e9tion. Guidez les utilisateurs en douceur tout au long du processus"],"Conditional Fields":["Champs conditionnels"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Afficher ou masquer les champs en fonction des r\u00e9ponses des utilisateurs. Posez les bonnes questions et affichez uniquement ce qui est n\u00e9cessaire pour garder les formulaires clairs et pertinents."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Am\u00e9liorez vos formulaires avec des champs avanc\u00e9s tels que le t\u00e9l\u00e9chargement de plusieurs fichiers, les champs de notation et les s\u00e9lecteurs de date et d'heure pour collecter des donn\u00e9es plus riches et flexibles."],"Conversational Forms":["Formes conversationnelles"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Cr\u00e9ez des formulaires qui ressemblent \u00e0 une conversation. Une question \u00e0 la fois maintient l'engagement des utilisateurs et facilite la compl\u00e9tion du formulaire."],"Digital Signatures":["Signatures num\u00e9riques"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Collectez des signatures num\u00e9riques l\u00e9galement contraignantes directement dans vos formulaires pour les accords, les approbations et les contrats."],"Calculators":["Calculatrices"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Ajoutez des calculateurs interactifs \u00e0 vos formulaires pour des estimations, devis et calculs instantan\u00e9s pour vos utilisateurs."],"User Registration and Login":["Inscription et Connexion Utilisateur"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Permettez aux visiteurs de s'inscrire et de se connecter \u00e0 votre site. Utile pour les sites de membres, de communaut\u00e9 ou tout site n\u00e9cessitant un acc\u00e8s utilisateur."],"PDF Generation Made Simple":["G\u00e9n\u00e9ration de PDF simplifi\u00e9e"],"Custom App":["Application personnalis\u00e9e"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Collectez des donn\u00e9es, envoyez-les \u00e0 des applications externes pour traitement, et affichez les r\u00e9sultats instantan\u00e9ment \u2014 le tout int\u00e9gr\u00e9 de mani\u00e8re transparente pour cr\u00e9er des exp\u00e9riences utilisateur dynamiques et interactives."],"Select Your Features":["S\u00e9lectionnez vos fonctionnalit\u00e9s"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Obtenez plus de contr\u00f4le, des flux de travail plus rapides et une personnalisation plus pouss\u00e9e \u2014 tout est con\u00e7u pour vous aider \u00e0 cr\u00e9er de meilleurs sites web avec moins d'effort."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Les fonctionnalit\u00e9s s\u00e9lectionn\u00e9es n\u00e9cessitent %1$s - utilisez le code %2$s pour obtenir 10 % de r\u00e9duction sur n'importe quel plan."],"Copied":["Copi\u00e9"],"Style your form to better match your site's design":["Adaptez le style de votre formulaire pour mieux correspondre au design de votre site"],"Add spam protection to block common bot submissions":["Ajoutez une protection anti-spam pour bloquer les soumissions courantes de bots"],"Get weekly email reports with a summary of form activity":["Recevez des rapports hebdomadaires par e-mail avec un r\u00e9sum\u00e9 de l'activit\u00e9 du formulaire"],"You're All Set! \ud83d\ude80":["Tout est pr\u00eat ! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Utilisez notre g\u00e9n\u00e9rateur de formulaires IA pour commencer rapidement, ou cr\u00e9ez votre formulaire \u00e0 partir de z\u00e9ro si vous savez d\u00e9j\u00e0 ce dont vous avez besoin. Vos formulaires sont pr\u00eats \u00e0 \u00eatre cr\u00e9\u00e9s, partag\u00e9s et connect\u00e9s avec les visiteurs de votre site."],"Final Touches That Make a Difference:":["Touches finales qui font la diff\u00e9rence :"],"Build Your First Form":["Cr\u00e9ez votre premier formulaire"],"File Uploads":["T\u00e9l\u00e9versements de fichiers"],"Signature & Rating":["Signature et \u00e9valuation"],"Calculation Forms":["Formulaires de calcul"],"And Much More\u2026":["Et bien plus encore\u2026"],"Upgrade to Pro":["Passer \u00e0 Pro"],"Unlock Premium Features":["D\u00e9bloquez les fonctionnalit\u00e9s Premium"],"Build Better Forms with SureForms":["Cr\u00e9ez de meilleurs formulaires avec SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Ajoutez des champs avanc\u00e9s, des mises en page conversationnelles et une logique intelligente pour cr\u00e9er des formulaires qui engagent les utilisateurs et capturent de meilleures donn\u00e9es."],"SureForms Video Thumbnail":["Miniature vid\u00e9o SureForms"],"Payments":["Paiements"],"Knowledge Base":["Base de connaissances"],"What\u2019s New":["Quoi de neuf"],"Importing\u2026":["Importation\u2026"],"Learn":["Apprendre"],"Unable to complete action. Please try again.":["Impossible de terminer l'action. Veuillez r\u00e9essayer."],"Supercharge Your Workflow":["Boostez votre flux de travail"],"Spam protection included":["Protection contre le spam incluse"],"Multistep Forms":["Formulaires \u00e0 \u00e9tapes multiples"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Envoyez instantan\u00e9ment les entr\u00e9es de formulaire \u00e0 tout syst\u00e8me ou point de terminaison externe pour alimenter des flux de travail avanc\u00e9s."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Transformez automatiquement les entr\u00e9es de formulaire en PDF propres et pr\u00eats \u00e0 t\u00e9l\u00e9charger. Parfait pour les archives, le partage, l'archivage ou pour garder les choses organis\u00e9es."],"Set up confirmation messages and email notifications for each entry":["Configurer les messages de confirmation et les notifications par e-mail pour chaque entr\u00e9e"],"Something went wrong. Please try again.":["Un probl\u00e8me est survenu. Veuillez r\u00e9essayer."],"Payment Forms":["Formulaires de paiement"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Collectez les paiements directement via vos formulaires. Acceptez les paiements uniques et r\u00e9currents en toute simplicit\u00e9."],"SureForms %s":["SureForms %s"],"First name is required.":["Le pr\u00e9nom est requis."],"Please enter a valid email address.":["Veuillez entrer une adresse e-mail valide."],"Email address is required.":["L'adresse e-mail est requise."],"This is required.":["Ceci est requis."],"Okay, just one last step\u2026":["D'accord, juste une derni\u00e8re \u00e9tape\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Aidez-nous \u00e0 personnaliser votre exp\u00e9rience SureForms en partageant quelques informations sur vous."],"First Name":["Pr\u00e9nom"],"Enter your first name":["Entrez votre pr\u00e9nom"],"Last Name":["Nom de famille"],"Enter your last name":["Entrez votre nom de famille"],"Email Address":["Adresse e-mail"],"Enter your email address":["Entrez votre adresse e-mail"],"Privacy Policy":["Politique de confidentialit\u00e9"],"Finish":["Terminer"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Restez inform\u00e9 et contribuez \u00e0 fa\u00e7onner SureForms ! Recevez des mises \u00e0 jour des fonctionnalit\u00e9s et aidez-nous \u00e0 am\u00e9liorer SureForms en partageant comment vous utilisez le plugin. Politique de confidentialit\u00e9<\/a>."],"%d form ready":["%d formulaire pr\u00eat"],"%d already imported":["%d d\u00e9j\u00e0 import\u00e9"],"(unnamed source)":["(source sans nom)"],"All forms already imported":["Tous les formulaires d\u00e9j\u00e0 import\u00e9s"],"Import forms":["Importer des formulaires"],"Import %d form":["Importer %d formulaire"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Une erreur s'est produite lors de l'importation. Vous pouvez r\u00e9essayer, passer ou ouvrir Param\u00e8tres \u2192 Migration."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Aucun autre plugin de formulaire d\u00e9tect\u00e9. Vous pouvez importer \u00e0 tout moment depuis Param\u00e8tres \u2192 Migration."],"Forms imported":["Formulaires import\u00e9s"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formulaire de %2$s est maintenant dans SureForms, pr\u00eat \u00e0 \u00eatre publi\u00e9, styl\u00e9 et connect\u00e9."],"%d form imported":["%d formulaire import\u00e9"],"%d form could not be imported":["%d formulaire n'a pas pu \u00eatre import\u00e9"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d type de champ n'\u00e9tait pas pris en charge. Vous pouvez le reconstruire manuellement dans SureForms."],"Bring your existing forms with you":["Apportez vos formulaires existants avec vous"],"We detected forms in another plugin. Pick one to import into SureForms.":["Nous avons d\u00e9tect\u00e9 des formulaires dans un autre plugin. Choisissez-en un \u00e0 importer dans SureForms."],"Choose a form plugin to import from":["Choisissez un plugin de formulaire \u00e0 importer"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importer plus d'un plugin ? Vous pouvez importer des sources suppl\u00e9mentaires plus tard depuis Param\u00e8tres \u2192 Migration."],"Import did not complete":["L'importation n'a pas \u00e9t\u00e9 compl\u00e9t\u00e9e"],"I'll do this later":["Je le ferai plus tard"],"Retry":["R\u00e9essayer"],"Form Checks":["V\u00e9rifications de formulaire"],"Collapse form checks":["R\u00e9duire les v\u00e9rifications de formulaire"],"Expand form checks":["D\u00e9velopper les v\u00e9rifications de formulaire"],"Ignore":["Ignorer"],"Working\u2026":["En cours\u2026"]}}} \ No newline at end of file +{"translation-revision-date":"2024-12-13T13:12:49+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Tableau de bord"],"Settings":["Param\u00e8tres"],"Entries":["Entr\u00e9es"],"Activated":["Activ\u00e9"],"Advanced Fields":["Champs avanc\u00e9s"],"Select Form":["S\u00e9lectionner le formulaire"],"Create New Form":["Cr\u00e9er un nouveau formulaire"],"Forms":["Formulaires"],"Copy":["Copier"],"Business":["Affaires"],"Next":["Suivant"],"Back":["Retour"],"Close":["Fermer"],"Cancel":["Annuler"],"This is where your form views will appear":["C'est ici que vos vues de formulaire appara\u00eetront"],"Install":["Installer"],"Plugin Installation failed, Please try again later.":["L'installation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"Plugin activation failed, Please try again later.":["L'activation du plugin a \u00e9chou\u00e9, veuillez r\u00e9essayer plus tard."],"What's New?":["Quoi de neuf ?"],"Core":["C\u0153ur"],"Unlicensed":["Sans licence"],"Upgrade":["Mise \u00e0 niveau"],"Webhooks":["Webhooks"],"Install & Activate":["Installer et activer"],"Contact Support":["Contacter le support"],"Conditional Logic":["Logique conditionnelle"],"Premium":["Premium"],"Welcome to SureForms!":["Bienvenue chez SureForms !"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms est un plugin WordPress qui permet aux utilisateurs de cr\u00e9er de magnifiques formulaires gr\u00e2ce \u00e0 une interface de glisser-d\u00e9poser, sans avoir besoin de coder. Il s'int\u00e8gre avec l'\u00e9diteur de blocs WordPress."],"Read Full Guide":["Lire le guide complet"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms : Formulaires WordPress personnalis\u00e9s SIMPLIFI\u00c9S"],"No Date":["Aucune date"],"Invalid Date":["Date invalide"],"Ready to go beyond free plan?":["Pr\u00eat \u00e0 aller au-del\u00e0 du plan gratuit ?"],"Upgrade now":["Mettez \u00e0 niveau maintenant"],"and unlock the full power of SureForms!":["et d\u00e9bloquez toute la puissance de SureForms !"],"Upgrade SureForms":["Mettre \u00e0 niveau SureForms"],"Open Support Ticket":["Ouvrir un ticket de support"],"Help Center":["Centre d'aide"],"Join our Community on Facebook":["Rejoignez notre communaut\u00e9 sur Facebook"],"Leave Us a Review":["Laissez-nous un avis"],"Quick Access":["Acc\u00e8s rapide"],"Upgrade Now":["Mettez \u00e0 niveau maintenant"],"Clear Filters":["Effacer les filtres"],"Unnamed Form":["Formulaire sans nom"],"Forms Overview":["Aper\u00e7u des formulaires"],"Clear Form Filters":["Effacer les filtres du formulaire"],"Clear Date Filters":["Effacer les filtres de date"],"Select Date Range":["S\u00e9lectionner la plage de dates"],"Apply":["Appliquer"],"Please wait for the data to load":["Veuillez attendre le chargement des donn\u00e9es"],"No entries to display":["Aucune entr\u00e9e \u00e0 afficher"],"Once you create a form and start receiving submissions, the data will appear here.":["Une fois que vous cr\u00e9ez un formulaire et commencez \u00e0 recevoir des soumissions, les donn\u00e9es appara\u00eetront ici."],"Free":["Gratuit"],"All Forms":["Tous les formulaires"],"Guided Setup":["Configuration guid\u00e9e"],"Exit Guided Setup":["Quitter l'installation guid\u00e9e"],"Skip":["Passer"],"Build beautiful forms visually":["Cr\u00e9ez de beaux formulaires visuellement"],"Works perfectly on mobile":["Fonctionne parfaitement sur mobile"],"Easy to connect with automation tools":["Facile \u00e0 connecter avec des outils d'automatisation"],"Welcome to SureForms":["Bienvenue chez SureForms"],"Smart, Quick and Powerful Forms.":["Formulaires intelligents, rapides et puissants."],"Let's Get Started":["Commen\u00e7ons"],"Build up to 10 forms using AI":["Cr\u00e9ez jusqu'\u00e0 10 formulaires en utilisant l'IA"],"A secure and private connection":["Une connexion s\u00e9curis\u00e9e et priv\u00e9e"],"Smart form drafts based on your input":["Brouillons de formulaires intelligents bas\u00e9s sur votre saisie"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Commencer \u00e0 partir d'un formulaire vierge n'est pas toujours facile. Notre IA peut vous aider en cr\u00e9ant un brouillon de formulaire bas\u00e9 sur ce que vous essayez de faire \u2014 vous faisant gagner du temps et vous donnant une direction claire."],"To do this, you'll need to connect your account.":["Pour ce faire, vous devrez connecter votre compte."],"Let AI Help You Build Smarter, Faster Forms":["Laissez l'IA vous aider \u00e0 cr\u00e9er des formulaires plus intelligents et plus rapides"],"Here's what that gives you:":["Voici ce que cela vous donne :"],"Continue":["Continuer"],"Connect":["Connecter"],"Works smoothly with forms made using SureForms":["Fonctionne parfaitement avec les formulaires cr\u00e9\u00e9s \u00e0 l'aide de SureForms"],"Helps your emails reach the inbox instead of spam":["Aide vos e-mails \u00e0 atteindre la bo\u00eete de r\u00e9ception au lieu du spam"],"Setup is straightforward, even if you're not technical":["L'installation est simple, m\u00eame si vous n'\u00eates pas technique"],"Lightweight and easy to use without adding clutter":["L\u00e9ger et facile \u00e0 utiliser sans ajouter de d\u00e9sordre"],"Make Sure Your Emails Get Delivered":["Assurez-vous que vos e-mails soient livr\u00e9s"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["La plupart des sites WordPress ont du mal \u00e0 envoyer des e-mails de mani\u00e8re fiable, ce qui signifie que les soumissions de formulaires de votre site pourraient ne pas atteindre votre bo\u00eete de r\u00e9ception \u2014 ou finir dans les spams."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail est un plugin SMTP simple qui aide \u00e0 s'assurer que vos e-mails sont effectivement livr\u00e9s."],"What you will get:":["Ce que vous obtiendrez :"],"Install SureMail":["Installez SureMail"],"AI Form Generation":["G\u00e9n\u00e9ration de formulaires par IA"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Fatigu\u00e9 de cr\u00e9er des formulaires manuellement ? Laissez l'IA faire le travail pour vous. D\u00e9crivez simplement et notre IA cr\u00e9era votre formulaire parfait en quelques secondes."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["D\u00e9composez les formulaires complexes en \u00e9tapes simples, r\u00e9duisant ainsi la surcharge et augmentant les taux de compl\u00e9tion. Guidez les utilisateurs en douceur tout au long du processus"],"Conditional Fields":["Champs conditionnels"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Afficher ou masquer les champs en fonction des r\u00e9ponses des utilisateurs. Posez les bonnes questions et affichez uniquement ce qui est n\u00e9cessaire pour garder les formulaires clairs et pertinents."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Am\u00e9liorez vos formulaires avec des champs avanc\u00e9s tels que le t\u00e9l\u00e9chargement de plusieurs fichiers, les champs de notation et les s\u00e9lecteurs de date et d'heure pour collecter des donn\u00e9es plus riches et flexibles."],"Conversational Forms":["Formes conversationnelles"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Cr\u00e9ez des formulaires qui ressemblent \u00e0 une conversation. Une question \u00e0 la fois maintient l'engagement des utilisateurs et facilite la compl\u00e9tion du formulaire."],"Digital Signatures":["Signatures num\u00e9riques"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Collectez des signatures num\u00e9riques l\u00e9galement contraignantes directement dans vos formulaires pour les accords, les approbations et les contrats."],"Calculators":["Calculatrices"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Ajoutez des calculateurs interactifs \u00e0 vos formulaires pour des estimations, devis et calculs instantan\u00e9s pour vos utilisateurs."],"User Registration and Login":["Inscription et Connexion Utilisateur"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Permettez aux visiteurs de s'inscrire et de se connecter \u00e0 votre site. Utile pour les sites de membres, de communaut\u00e9 ou tout site n\u00e9cessitant un acc\u00e8s utilisateur."],"PDF Generation Made Simple":["G\u00e9n\u00e9ration de PDF simplifi\u00e9e"],"Custom App":["Application personnalis\u00e9e"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Collectez des donn\u00e9es, envoyez-les \u00e0 des applications externes pour traitement, et affichez les r\u00e9sultats instantan\u00e9ment \u2014 le tout int\u00e9gr\u00e9 de mani\u00e8re transparente pour cr\u00e9er des exp\u00e9riences utilisateur dynamiques et interactives."],"Select Your Features":["S\u00e9lectionnez vos fonctionnalit\u00e9s"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Obtenez plus de contr\u00f4le, des flux de travail plus rapides et une personnalisation plus pouss\u00e9e \u2014 tout est con\u00e7u pour vous aider \u00e0 cr\u00e9er de meilleurs sites web avec moins d'effort."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Les fonctionnalit\u00e9s s\u00e9lectionn\u00e9es n\u00e9cessitent %1$s - utilisez le code %2$s pour obtenir 10 % de r\u00e9duction sur n'importe quel plan."],"Copied":["Copi\u00e9"],"Style your form to better match your site's design":["Adaptez le style de votre formulaire pour mieux correspondre au design de votre site"],"Add spam protection to block common bot submissions":["Ajoutez une protection anti-spam pour bloquer les soumissions courantes de bots"],"Get weekly email reports with a summary of form activity":["Recevez des rapports hebdomadaires par e-mail avec un r\u00e9sum\u00e9 de l'activit\u00e9 du formulaire"],"You're All Set! \ud83d\ude80":["Tout est pr\u00eat ! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Utilisez notre g\u00e9n\u00e9rateur de formulaires IA pour commencer rapidement, ou cr\u00e9ez votre formulaire \u00e0 partir de z\u00e9ro si vous savez d\u00e9j\u00e0 ce dont vous avez besoin. Vos formulaires sont pr\u00eats \u00e0 \u00eatre cr\u00e9\u00e9s, partag\u00e9s et connect\u00e9s avec les visiteurs de votre site."],"Final Touches That Make a Difference:":["Touches finales qui font la diff\u00e9rence :"],"Build Your First Form":["Cr\u00e9ez votre premier formulaire"],"File Uploads":["T\u00e9l\u00e9versements de fichiers"],"Signature & Rating":["Signature et \u00e9valuation"],"Calculation Forms":["Formulaires de calcul"],"And Much More\u2026":["Et bien plus encore\u2026"],"Upgrade to Pro":["Passer \u00e0 Pro"],"Unlock Premium Features":["D\u00e9bloquez les fonctionnalit\u00e9s Premium"],"Build Better Forms with SureForms":["Cr\u00e9ez de meilleurs formulaires avec SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Ajoutez des champs avanc\u00e9s, des mises en page conversationnelles et une logique intelligente pour cr\u00e9er des formulaires qui engagent les utilisateurs et capturent de meilleures donn\u00e9es."],"SureForms Video Thumbnail":["Miniature vid\u00e9o SureForms"],"Payments":["Paiements"],"Knowledge Base":["Base de connaissances"],"What\u2019s New":["Quoi de neuf"],"Importing\u2026":["Importation\u2026"],"Learn":["Apprendre"],"Unable to complete action. Please try again.":["Impossible de terminer l'action. Veuillez r\u00e9essayer."],"Supercharge Your Workflow":["Boostez votre flux de travail"],"Spam protection included":["Protection contre le spam incluse"],"Multistep Forms":["Formulaires \u00e0 \u00e9tapes multiples"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Envoyez instantan\u00e9ment les entr\u00e9es de formulaire \u00e0 tout syst\u00e8me ou point de terminaison externe pour alimenter des flux de travail avanc\u00e9s."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Transformez automatiquement les entr\u00e9es de formulaire en PDF propres et pr\u00eats \u00e0 t\u00e9l\u00e9charger. Parfait pour les archives, le partage, l'archivage ou pour garder les choses organis\u00e9es."],"Set up confirmation messages and email notifications for each entry":["Configurer les messages de confirmation et les notifications par e-mail pour chaque entr\u00e9e"],"Something went wrong. Please try again.":["Un probl\u00e8me est survenu. Veuillez r\u00e9essayer."],"Payment Forms":["Formulaires de paiement"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Collectez les paiements directement via vos formulaires. Acceptez les paiements uniques et r\u00e9currents en toute simplicit\u00e9."],"SureForms %s":["SureForms %s"],"First name is required.":["Le pr\u00e9nom est requis."],"Please enter a valid email address.":["Veuillez entrer une adresse e-mail valide."],"Email address is required.":["L'adresse e-mail est requise."],"This is required.":["Ceci est requis."],"Okay, just one last step\u2026":["D'accord, juste une derni\u00e8re \u00e9tape\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Aidez-nous \u00e0 personnaliser votre exp\u00e9rience SureForms en partageant quelques informations sur vous."],"First Name":["Pr\u00e9nom"],"Enter your first name":["Entrez votre pr\u00e9nom"],"Last Name":["Nom de famille"],"Enter your last name":["Entrez votre nom de famille"],"Email Address":["Adresse e-mail"],"Enter your email address":["Entrez votre adresse e-mail"],"Privacy Policy":["Politique de confidentialit\u00e9"],"Finish":["Terminer"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Restez inform\u00e9 et contribuez \u00e0 fa\u00e7onner SureForms ! Recevez des mises \u00e0 jour des fonctionnalit\u00e9s et aidez-nous \u00e0 am\u00e9liorer SureForms en partageant comment vous utilisez le plugin. Politique de confidentialit\u00e9<\/a>."],"%d form ready":["%d formulaire pr\u00eat"],"%d already imported":["%d d\u00e9j\u00e0 import\u00e9"],"(unnamed source)":["(source sans nom)"],"All forms already imported":["Tous les formulaires d\u00e9j\u00e0 import\u00e9s"],"Import forms":["Importer des formulaires"],"Import %d form":["Importer %d formulaire"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Une erreur s'est produite lors de l'importation. Vous pouvez r\u00e9essayer, passer ou ouvrir Param\u00e8tres \u2192 Migration."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Aucun autre plugin de formulaire d\u00e9tect\u00e9. Vous pouvez importer \u00e0 tout moment depuis Param\u00e8tres \u2192 Migration."],"Forms imported":["Formulaires import\u00e9s"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["%1$d formulaire de %2$s est maintenant dans SureForms, pr\u00eat \u00e0 \u00eatre publi\u00e9, styl\u00e9 et connect\u00e9."],"%d form imported":["%d formulaire import\u00e9"],"%d form could not be imported":["%d formulaire n'a pas pu \u00eatre import\u00e9"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d type de champ n'\u00e9tait pas pris en charge. Vous pouvez le reconstruire manuellement dans SureForms."],"Bring your existing forms with you":["Apportez vos formulaires existants avec vous"],"We detected forms in another plugin. Pick one to import into SureForms.":["Nous avons d\u00e9tect\u00e9 des formulaires dans un autre plugin. Choisissez-en un \u00e0 importer dans SureForms."],"Choose a form plugin to import from":["Choisissez un plugin de formulaire \u00e0 importer"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importer plus d'un plugin ? Vous pouvez importer des sources suppl\u00e9mentaires plus tard depuis Param\u00e8tres \u2192 Migration."],"Import did not complete":["L'importation n'a pas \u00e9t\u00e9 compl\u00e9t\u00e9e"],"I'll do this later":["Je le ferai plus tard"],"Retry":["R\u00e9essayer"],"Form Checks":["V\u00e9rifications de formulaire"],"Collapse form checks":["R\u00e9duire les v\u00e9rifications de formulaire"],"Expand form checks":["D\u00e9velopper les v\u00e9rifications de formulaire"],"Ignore":["Ignorer"],"Working\u2026":["En cours\u2026"],"Details":["D\u00e9tails"],"Copy details":["Copier les d\u00e9tails"]}}} \ No newline at end of file diff --git a/languages/sureforms-fr_FR.mo b/languages/sureforms-fr_FR.mo index d4226a08db0597d54e0de73610af081219f182c0..05722a01352795227563e8f4e8a6ca87c2059b41 100644 GIT binary patch delta 76515 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 D1*1A2 delta 72819 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" msgstr "Format attendu pour les e-mails - email@sureforms.com ou John Doe " -#: admin/admin.php:1471 -#: admin/admin.php:1472 +#: admin/admin.php:1517 +#: admin/admin.php:1518 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -12176,7 +12182,7 @@ msgstr "Élevez votre SEO et grimpez dans les classements de recherche sans effo msgid "Boost your website's visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress." msgstr "Boostez la visibilité de votre site web avec une automatisation SEO intelligente. Optimisez le contenu, suivez la performance des mots-clés et obtenez des insights exploitables, le tout dans WordPress." -#: inc/form-submit.php:129 +#: inc/form-submit.php:174 #: inc/form-views.php:205 #: inc/payments/admin/admin-handler.php:99 #: inc/payments/admin/admin-handler.php:183 @@ -15375,12 +15381,12 @@ msgid "Stripe Settings" msgstr "Paramètres Stripe" #. translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. -#: admin/admin.php:2281 +#: admin/admin.php:2335 #, php-format msgid "SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version." msgstr "SureForms %1$s nécessite au minimum %2$s %3$s pour fonctionner correctement. Veuillez mettre à jour vers la dernière version." -#: admin/admin.php:2294 +#: admin/admin.php:2348 msgid "Update Now" msgstr "Mettez à jour maintenant" @@ -15428,8 +15434,8 @@ msgstr "Position du signe monétaire" msgid "Select the position of the currency symbol relative to the amount." msgstr "Sélectionnez la position du symbole monétaire par rapport au montant." -#: admin/admin.php:1414 -#: admin/admin.php:1415 +#: admin/admin.php:1460 +#: admin/admin.php:1461 #: assets/build/dashboard.js:172 #: assets/build/entries.js:172 #: assets/build/forms.js:172 @@ -15443,41 +15449,41 @@ msgstr "Sélectionnez la position du symbole monétaire par rapport au montant." msgid "Learn" msgstr "Apprendre" -#: admin/admin.php:2635 +#: admin/admin.php:2689 msgid "Amazing! SureForms is powering your forms and submissions - let's keep growing together!" msgstr "Incroyable ! SureForms alimente vos formulaires et soumissions - continuons à grandir ensemble !" -#: admin/admin.php:2636 +#: admin/admin.php:2690 msgid "If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?" msgstr "Si SureForms vous a été utile, pourriez-vous prendre un moment pour laisser un avis 5 étoiles sur WordPress.org ?" -#: admin/admin.php:2647 -#: admin/admin.php:2711 +#: admin/admin.php:2701 +#: admin/admin.php:2765 msgid "Maybe later" msgstr "Peut-être plus tard" -#: admin/admin.php:2653 +#: admin/admin.php:2707 msgid "I already did" msgstr "Je l'ai déjà fait" -#: admin/admin.php:2702 +#: admin/admin.php:2756 msgid "SureForms is ready to power your forms — explore what's possible!" msgstr "SureForms est prêt à dynamiser vos formulaires — explorez ce qui est possible !" -#: admin/admin.php:2703 +#: admin/admin.php:2757 msgid "Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard." msgstr "Gérez vos formulaires, suivez les soumissions et découvrez des fonctionnalités telles que le constructeur de formulaires IA, les intégrations de paiement et bien plus encore depuis le tableau de bord SureForms." -#: admin/admin.php:2706 +#: admin/admin.php:2760 msgid "Go to Dashboard" msgstr "Aller au tableau de bord" -#: admin/admin.php:2717 +#: admin/admin.php:2771 msgid "I already know" msgstr "Je sais déjà" -#: admin/admin.php:2833 -#: admin/admin.php:3608 +#: admin/admin.php:2981 +#: admin/admin.php:3854 msgid "Invalid parameters." msgstr "Paramètres invalides." @@ -15861,10 +15867,10 @@ msgid "This form is not yet available. Check back after the scheduled start time msgstr "Ce formulaire n'est pas encore disponible. Revenez après l'heure de début prévue." #: inc/admin/html-form-detector.php:268 -#: inc/form-submit.php:197 -#: inc/form-submit.php:503 -#: inc/form-submit.php:548 -#: inc/form-submit.php:1149 +#: inc/form-submit.php:247 +#: inc/form-submit.php:553 +#: inc/form-submit.php:598 +#: inc/form-submit.php:1257 #: inc/migrator/bootstrap.php:357 #: inc/payments/front-end.php:79 #: inc/payments/front-end.php:265 @@ -15887,48 +15893,48 @@ msgstr "Ce formulaire n'est pas encore disponible. Revenez après l'heure de dé msgid "Security verification failed. Please refresh the page and try again." msgstr "La vérification de sécurité a échoué. Veuillez actualiser la page et réessayer." -#: inc/form-submit.php:582 +#: inc/form-submit.php:632 msgid "Form data was not found." msgstr "Les données du formulaire n'ont pas été trouvées." -#: inc/form-submit.php:336 +#: inc/form-submit.php:386 msgid "Form ID is missing." msgstr "L'identifiant du formulaire est manquant." -#: inc/form-submit.php:213 +#: inc/form-submit.php:263 msgid "Sorry, you do not have permission to access this resource." msgstr "Désolé, vous n'avez pas la permission d'accéder à cette ressource." -#: inc/form-submit.php:368 +#: inc/form-submit.php:418 msgid "You do not have permission to submit this form." msgstr "Vous n'avez pas la permission de soumettre ce formulaire." -#: inc/form-submit.php:378 +#: inc/form-submit.php:428 msgid "This form is no longer available." msgstr "Ce formulaire n'est plus disponible." -#: inc/form-submit.php:396 +#: inc/form-submit.php:446 msgid "Please check the form for errors." msgstr "Veuillez vérifier le formulaire pour les erreurs." -#: inc/form-submit.php:523 -#: inc/form-submit.php:564 +#: inc/form-submit.php:573 +#: inc/form-submit.php:614 msgid "Your submission was flagged as spam. Please try again." msgstr "Votre soumission a été signalée comme spam. Veuillez réessayer." -#: inc/form-submit.php:818 +#: inc/form-submit.php:868 msgid "Unable to submit form. Please try again." msgstr "Impossible de soumettre le formulaire. Veuillez réessayer." -#: inc/form-submit.php:1070 +#: inc/form-submit.php:1137 msgid "No SMTP plugin detected. Please configure an SMTP plugin to enable email sending." msgstr "Aucun plugin SMTP détecté. Veuillez configurer un plugin SMTP pour activer l'envoi d'e-mails." -#: inc/form-submit.php:1071 +#: inc/form-submit.php:1138 msgid "Email sending failed for an unknown reason." msgstr "L'envoi de l'email a échoué pour une raison inconnue." -#: inc/form-submit.php:1124 +#: inc/form-submit.php:1201 msgid "No emails were sent." msgstr "Aucun e-mail n'a été envoyé." @@ -16441,7 +16447,7 @@ msgid "Duplicate form?" msgstr "Dupliquer le formulaire ?" #: assets/build/formSubmit.js:2 -#: assets/js/unminified/form-submit.js:917 +#: assets/js/unminified/form-submit.js:938 msgid "An error occurred while submitting your form. Please try again." msgstr "Une erreur s'est produite lors de la soumission de votre formulaire. Veuillez réessayer." @@ -16818,28 +16824,28 @@ msgstr "Secret du webhook non configuré." msgid "Invalid webhook signature." msgstr "Signature de webhook invalide." -#: admin/admin.php:1253 -#: admin/admin.php:1851 +#: admin/admin.php:1299 +#: admin/admin.php:1905 #: assets/build/formEditor.js:172 msgid "Quizzes" msgstr "Quiz" -#: admin/admin.php:1252 +#: admin/admin.php:1298 msgid "Quiz Entries" msgstr "Participations au quiz" -#: admin/admin.php:1255 -#: admin/admin.php:1288 -#: admin/admin.php:1321 +#: admin/admin.php:1301 +#: admin/admin.php:1334 +#: admin/admin.php:1367 #: assets/build/settings.js:172 msgid "New" msgstr "Nouveau" -#: inc/form-submit.php:1158 +#: inc/form-submit.php:1266 msgid "Invalid form." msgstr "Formulaire invalide." -#: inc/form-submit.php:1163 +#: inc/form-submit.php:1271 msgid "Too many requests. Please try again shortly." msgstr "Trop de demandes. Veuillez réessayer sous peu." @@ -17348,9 +17354,9 @@ msgstr "Parfait pour l'éducation, la formation et les quiz amusants" msgid "Select this to create a quiz with scored questions and graded results." msgstr "Sélectionnez ceci pour créer un quiz avec des questions notées et des résultats évalués." -#: admin/admin.php:1285 -#: admin/admin.php:1286 -#: admin/admin.php:1856 +#: admin/admin.php:1331 +#: admin/admin.php:1332 +#: admin/admin.php:1910 msgid "Survey Reports" msgstr "Rapports d'enquête" @@ -17756,7 +17762,7 @@ msgid "Error creating SureForms Form." msgstr "Erreur lors de la création du formulaire SureForms." #. translators: %s represents the minimum number of characters required -#: inc/field-validation.php:426 +#: inc/field-validation.php:465 #: inc/translatable.php:94 #, php-format msgid "Please enter at least %s characters." @@ -17914,9 +17920,9 @@ msgstr "La réponse de l'IA était vide. Veuillez affiner votre invite et réess msgid "Unable to build form fields from the AI response." msgstr "Impossible de créer les champs de formulaire à partir de la réponse de l'IA." -#: admin/admin.php:1318 -#: admin/admin.php:1319 -#: admin/admin.php:1861 +#: admin/admin.php:1364 +#: admin/admin.php:1365 +#: admin/admin.php:1915 msgid "Partial Entries" msgstr "Entrées partielles" @@ -18787,23 +18793,23 @@ msgstr "Le montant du paiement n'a pas pu être vérifié pour ce formulaire. Ve msgid "Cancellation is not supported for this payment gateway." msgstr "L'annulation n'est pas prise en charge pour cette passerelle de paiement." -#: admin/admin.php:3039 +#: admin/admin.php:3203 msgid "SureForms AI Quick Draft" msgstr "SureForms AI Brouillon Rapide" -#: admin/admin.php:3058 +#: admin/admin.php:3222 msgid "Describe the form and let SureForms AI generate it for you." msgstr "Décrivez le formulaire et laissez SureForms AI le générer pour vous." -#: admin/admin.php:3061 +#: admin/admin.php:3225 msgid "Describe your form" msgstr "Décrivez votre formulaire" -#: admin/admin.php:3068 +#: admin/admin.php:3232 msgid "Example: Create a contact form with name, email, phone, and message fields." msgstr "Exemple : Créez un formulaire de contact avec des champs pour le nom, l'email, le téléphone et le message." -#: admin/admin.php:3109 +#: admin/admin.php:3273 msgid "Redirecting..." msgstr "Redirection..." @@ -18816,14 +18822,14 @@ msgid "The prompt is too long. Please shorten it and try again." msgstr "L'invite est trop longue. Veuillez la raccourcir et réessayer." #. translators: %s: maximum characters allowed before the @ symbol. -#: inc/field-validation.php:448 +#: inc/field-validation.php:487 #: inc/translatable.php:97 #, php-format msgid "The part before @ may not exceed %s characters." msgstr "La partie avant @ ne peut pas dépasser %s caractères." #. translators: %s: maximum characters allowed after the @ symbol. -#: inc/field-validation.php:454 +#: inc/field-validation.php:493 #: inc/translatable.php:100 #, php-format msgid "The part after @ may not exceed %s characters." @@ -18910,74 +18916,74 @@ msgstr "Désolé, vous n'êtes pas autorisé à afficher ce formulaire." msgid "This form requires a payment. Please complete the payment and submit again." msgstr "Ce formulaire nécessite un paiement. Veuillez effectuer le paiement et soumettre à nouveau." -#: admin/admin.php:552 +#: admin/admin.php:598 msgid "You are not allowed to update this prompt." msgstr "Vous n'êtes pas autorisé à mettre à jour cette invite." -#: admin/admin.php:595 +#: admin/admin.php:641 msgid "Finish setting up your form" msgstr "Terminez la configuration de votre formulaire" -#: admin/admin.php:627 +#: admin/admin.php:673 msgid "Review or edit your form" msgstr "Révisez ou modifiez votre formulaire" -#: admin/admin.php:628 -#: admin/admin.php:3907 +#: admin/admin.php:674 +#: admin/admin.php:4515 msgid "Edit form" msgstr "Modifier le formulaire" -#: admin/admin.php:633 +#: admin/admin.php:679 msgid "Personalize the Thank You message" msgstr "Personnalisez le message de remerciement" -#: admin/admin.php:634 +#: admin/admin.php:680 msgid "Edit message" msgstr "Modifier le message" -#: admin/admin.php:639 +#: admin/admin.php:685 msgid "Choose who gets notified of new replies" msgstr "Choisissez qui sera notifié des nouvelles réponses" -#: admin/admin.php:640 +#: admin/admin.php:686 msgid "Set up email" msgstr "Configurer l'email" -#: admin/admin.php:648 +#: admin/admin.php:694 msgid "your form" msgstr "votre formulaire" #. translators: %s: form title. -#: admin/admin.php:651 +#: admin/admin.php:697 #, php-format msgid "Finish setting up %s" msgstr "Terminez la configuration de %s" #. translators: %s: form title. -#: admin/admin.php:659 +#: admin/admin.php:705 #, php-format msgid "View %s (opens in a new tab)" msgstr "Voir %s (s'ouvre dans un nouvel onglet)" -#: admin/admin.php:664 +#: admin/admin.php:710 msgid "Customize your form to get it ready for real submissions:" msgstr "Personnalisez votre formulaire pour le préparer à recevoir de vraies soumissions :" -#: admin/admin.php:3896 +#: admin/admin.php:4504 msgid "We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions." msgstr "Nous avons déjà créé ce formulaire pour vous. Terminez de le personnaliser pour qu'il soit prêt à collecter de vraies soumissions." #. translators: %s: form name. -#: admin/admin.php:3901 +#: admin/admin.php:4509 #, php-format msgid "Finish setting up “%s”" msgstr "Terminez la configuration de « %s »" -#: admin/admin.php:3914 +#: admin/admin.php:4522 msgid "Edit the Thank You message" msgstr "Modifier le message de remerciement" -#: admin/admin.php:3920 +#: admin/admin.php:4528 msgid "Set where replies go" msgstr "Définir où vont les réponses" @@ -19005,123 +19011,90 @@ msgstr "Veuillez choisir un fichier image." msgid "This form could not be submitted. Please refresh the page and try again." msgstr "Ce formulaire n'a pas pu être soumis. Veuillez rafraîchir la page et réessayer." -#: admin/admin.php:2341 -#: admin/admin.php:2430 +#: admin/admin.php:2395 +#: admin/admin.php:2484 msgid "Your SureForms database is up to date. New form entries will be saved as usual." msgstr "Votre base de données SureForms est à jour. Les nouvelles entrées de formulaire seront enregistrées comme d'habitude." -#: admin/admin.php:2355 -#: admin/admin.php:2439 +#: admin/admin.php:2409 +#: admin/admin.php:2493 #: inc/rest-api.php:1447 msgid "SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support." msgstr "SureForms n'a pas pu terminer la mise à jour de la base de données. Votre hébergement peut ne pas permettre à SureForms de créer des tables de base de données — veuillez contacter votre fournisseur d'hébergement ou le support de SureForms." -#: admin/admin.php:2358 +#: admin/admin.php:2412 msgid "Contact support" msgstr "Contacter le support" -#: admin/admin.php:2378 +#: admin/admin.php:2432 msgid "Database update needed" msgstr "Mise à jour de la base de données nécessaire" -#: admin/admin.php:2384 +#: admin/admin.php:2438 msgid "Fix now" msgstr "Corriger maintenant" -#: admin/admin.php:2453 +#: admin/admin.php:2507 msgid "SureForms — database update needed" msgstr "SureForms — mise à jour de la base de données nécessaire" -#: admin/admin.php:2458 +#: admin/admin.php:2512 msgid "Update database" msgstr "Mettre à jour la base de données" -#: admin/admin.php:2478 +#: admin/admin.php:2532 msgid "You do not have permission to update the database." msgstr "Vous n'avez pas la permission de mettre à jour la base de données." -#: admin/admin.php:3437 +#: admin/admin.php:3669 msgid "You do not have permission to do this." msgstr "Vous n'avez pas la permission de faire cela." -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:3507 -msgid "%d form submission in a row could not be completed." -msgid_plural "%d form submissions in a row could not be completed." -msgstr[0] "%d soumission de formulaire d'affilée n'a pas pu être complétée." -msgstr[1] "" - -#: admin/admin.php:3515 -msgid "Visitors may be unable to reach you, and those entries were not saved." -msgstr "Les visiteurs peuvent être incapables de vous joindre, et ces entrées n'ont pas été enregistrées." - -#: admin/admin.php:3525 -msgid "Form submissions are completing normally." -msgstr "Les soumissions de formulaire se terminent normalement." - -#: admin/admin.php:3539 -msgid "No caching plugin that needs configuring was found." -msgstr "Aucun plugin de mise en cache nécessitant une configuration n'a été trouvé." - #. translators: %s: caching plugin name. -#: admin/admin.php:3551 +#: admin/admin.php:4209 msgid "%s may interfere with your forms." msgstr "%s peut interférer avec vos formulaires." -#: admin/admin.php:3554 -msgid "Caching and JavaScript optimisation can serve a stale copy of your form or load its scripts out of order." -msgstr "La mise en cache et l'optimisation JavaScript peuvent servir une copie obsolète de votre formulaire ou charger ses scripts dans le désordre." - -#: admin/admin.php:3555 +#: admin/admin.php:4185 +#: admin/admin.php:4213 msgid "Help Me Fix" msgstr "Aidez-moi à corriger" -#: admin/admin.php:3650 +#: admin/admin.php:4258 msgid "SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept." msgstr "SureForms a trouvé vos entrées de formulaire stockées sous un préfixe de table de base de données différent. Les reconnecter prend un moment, et vos entrées existantes seront conservées." -#: admin/admin.php:3653 +#: admin/admin.php:4261 msgid "SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here." msgstr "SureForms doit mettre à jour votre base de données avant de pouvoir enregistrer de nouvelles entrées de formulaire. Cela ne prend qu'un moment et ne changera pas vos formulaires ou le contenu existant. Les entrées soumises avant maintenant ne peuvent pas être récupérées d'ici." #. translators: %s: site host. -#: admin/admin.php:4265 +#: admin/admin.php:5032 msgid "SureForms: form submissions are failing on %s" msgstr "SureForms : les soumissions de formulaire échouent sur %s" -#: admin/admin.php:4274 +#: admin/admin.php:4990 msgid "Debug log: no entries recorded." msgstr "Journal de débogage : aucune entrée enregistrée." #. translators: 1: entries shown, 2: entries recorded. -#: admin/admin.php:4278 +#: admin/admin.php:4995 msgid "Debug log (most recent %1$d of %2$d entries)" msgstr "Journal de débogage (les %1$d plus récentes de %2$d entrées)" -#: admin/admin.php:4288 -msgid "Older entries were left out to keep this email within the length a mail client accepts. The full log can be downloaded from SureForms → Settings → General." -msgstr "Les entrées plus anciennes ont été omises pour que cet e-mail reste dans la longueur acceptée par un client de messagerie. Le journal complet peut être téléchargé depuis SureForms → Paramètres → Général." - -#: admin/admin.php:4318 +#: admin/admin.php:5158 msgid "Hello SureForms support," msgstr "Bonjour support SureForms," -#. translators: %d: number of consecutive failed submissions. -#: admin/admin.php:4322 -msgid "SureForms has recorded %d form submission in a row that could not be completed." -msgid_plural "SureForms has recorded %d form submissions in a row that could not be completed." -msgstr[0] "SureForms a enregistré %d soumission de formulaire d'affilée qui n'a pas pu être complétée." -msgstr[1] "" - -#: admin/admin.php:4332 +#: admin/admin.php:5182 msgid "Site details" msgstr "Détails du site" -#: admin/admin.php:4335 +#: admin/admin.php:5185 msgid "not active" msgstr "non actif" -#: admin/admin.php:4338 +#: admin/admin.php:5188 msgid "none detected" msgstr "aucun détecté" @@ -19129,7 +19102,7 @@ msgstr "aucun détecté" msgid "No form submission failures have been recorded." msgstr "Aucun échec de soumission de formulaire n'a été enregistré." -#: inc/form-submit.php:118 +#: inc/form-submit.php:163 msgid "Not found." msgstr "Non trouvé." @@ -19214,6 +19187,193 @@ msgstr "Vues de formulaire et conversion" msgid "Logs" msgstr "Journaux" +#: admin/admin.php:2831 +msgid "Previous notice" +msgstr "Avis précédent" + +#: admin/admin.php:2832 +msgid "Next notice" +msgstr "Avis suivant" + +#. translators: 1: current position, 2: total notices. +#: admin/admin.php:2834 +msgid "%1$d of %2$d" +msgstr "%1$d sur %2$d" + +#: admin/admin.php:2891 +msgid "Nothing to report." +msgstr "Rien à signaler." + +#: admin/admin.php:4057 +#: assets/build/dashboard.js:172 +msgid "Details" +msgstr "Détails" + +#: admin/admin.php:4058 +msgid "What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it." +msgstr "Ce que nous avons enregistré à propos de ce problème. Copiez-le dans votre demande de support afin que nous puissions partir de la cause plutôt que d'une description de celle-ci." + +#: admin/admin.php:4059 +#: assets/build/dashboard.js:172 +msgid "Copy details" +msgstr "Copier les détails" + +#: admin/admin.php:4068 +msgid "Copy the details first, so you have them to paste." +msgstr "Copiez d'abord les détails, afin de les avoir à coller." + +#: admin/admin.php:4072 +msgid "Copied. Contact Support is now available." +msgstr "Copié. Contacter le support est maintenant disponible." + +#: admin/admin.php:4073 +msgid "Your browser would not let us copy. Select the text above and copy it by hand." +msgstr "Votre navigateur ne nous a pas permis de copier. Sélectionnez le texte ci-dessus et copiez-le à la main." + +#: admin/admin.php:4076 +msgid "Recorded diagnostics" +msgstr "Diagnostics enregistrés" + +#: admin/admin.php:4080 +msgid "Collecting the details…" +msgstr "Collecte des détails…" + +#: admin/admin.php:4081 +msgid "We could not collect the details. Contact Support and describe what happened, and we will take it from there." +msgstr "Nous n'avons pas pu collecter les détails. Contactez le support et décrivez ce qui s'est passé, et nous prendrons le relais à partir de là." + +#. translators: %s: form title. +#: admin/admin.php:4108 +msgid "We noticed a form submission failure on %s." +msgstr "Nous avons remarqué un échec de soumission de formulaire sur %s." + +#: admin/admin.php:4109 +msgid "We noticed a form submission failure." +msgstr "Nous avons remarqué un échec de soumission de formulaire." + +#: admin/admin.php:4110 +msgid "Visitors may not be able to reach you, and their entries were not saved." +msgstr "Les visiteurs peuvent ne pas être en mesure de vous joindre, et leurs entrées n'ont pas été enregistrées." + +#. translators: %s: form title. +#: admin/admin.php:4115 +msgid "We noticed a notification failure on %s." +msgstr "Nous avons remarqué un échec de notification sur %s." + +#: admin/admin.php:4116 +msgid "We noticed a notification failure." +msgstr "Nous avons remarqué un échec de notification." + +#: admin/admin.php:4117 +msgid "The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing." +msgstr "L'entrée a été enregistrée, mais nous n'avons pas pu envoyer l'email à ce sujet. De nouvelles entrées peuvent arriver sans que vous le sachiez." + +#. translators: %s: form title. +#: admin/admin.php:4132 +msgid "We noticed an integration failure on %s." +msgstr "Nous avons remarqué un échec d'intégration sur %s." + +#: admin/admin.php:4133 +msgid "We noticed an integration failure." +msgstr "Nous avons remarqué un échec d'intégration." + +#: admin/admin.php:4134 +msgid "The entry was saved, but we could not send it to a connected service." +msgstr "L'entrée a été enregistrée, mais nous n'avons pas pu l'envoyer à un service connecté." + +#: admin/admin.php:4158 +msgid "View details" +msgstr "Voir les détails" + +#: admin/admin.php:4212 +msgid "Caching can show visitors an old copy of your form, or load its scripts in the wrong order." +msgstr "La mise en cache peut montrer aux visiteurs une ancienne copie de votre formulaire, ou charger ses scripts dans le mauvais ordre." + +#: admin/admin.php:5005 +msgid "Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General." +msgstr "Les entrées plus anciennes ont été omises pour garder cet extrait lisible. Le journal complet peut être téléchargé depuis SureForms → Paramètres → Général." + +#. translators: %s: site host. +#: admin/admin.php:5034 +msgid "SureForms has recorded form submissions on %s that could not be completed." +msgstr "SureForms a enregistré des soumissions de formulaire sur %s qui n'ont pas pu être complétées." + +#. translators: %s: site host. +#: admin/admin.php:5038 +msgid "SureForms: notification emails are not being sent on %s" +msgstr "SureForms : les emails de notification ne sont pas envoyés sur %s" + +#. translators: %s: site host. +#: admin/admin.php:5040 +msgid "SureForms saved entries on %s but could not send the notification emails for them." +msgstr "SureForms a enregistré des entrées sur %s mais n'a pas pu envoyer les emails de notification pour celles-ci." + +#. translators: %s: site host. +#: admin/admin.php:5044 +msgid "SureForms: an integration is not receiving entries on %s" +msgstr "SureForms : une intégration ne reçoit pas d'entrées sur %s" + +#. translators: %s: site host. +#: admin/admin.php:5046 +msgid "SureForms saved entries on %s but could not pass them to a connected service." +msgstr "SureForms a enregistré des entrées sur %s mais n'a pas pu les transmettre à un service connecté." + +#. translators: %s: site host. +#: admin/admin.php:5056 +msgid "SureForms: a problem with the forms on %s" +msgstr "SureForms : un problème avec les formulaires sur %s" + +#. translators: %s: site host. +#: admin/admin.php:5058 +msgid "SureForms has recorded a problem with the forms on %s." +msgstr "SureForms a enregistré un problème avec les formulaires sur %s." + +#. translators: %d: number of failed submissions. +#: admin/admin.php:5083 +msgid "SureForms has recorded %d form submission that could not be completed." +msgid_plural "SureForms has recorded %d form submissions that could not be completed." +msgstr[0] "SureForms a enregistré %d soumission de formulaire qui n'a pas pu être complétée." +msgstr[1] "" + +#. translators: %d: number of failed notifications. +#: admin/admin.php:5095 +msgid "SureForms saved %d entry but could not send the notification email for it." +msgid_plural "SureForms saved %d entries but could not send the notification emails for them." +msgstr[0] "SureForms a enregistré %d entrée mais n'a pas pu envoyer l'email de notification pour celle-ci." +msgstr[1] "" + +#. translators: %d: number of failed integration hand-offs. +#: admin/admin.php:5107 +msgid "SureForms saved %d entry but could not pass it to a connected service." +msgid_plural "SureForms saved %d entries but could not pass them to a connected service." +msgstr[0] "SureForms a enregistré %d entrée mais n'a pas pu la transmettre à un service connecté." +msgstr[1] "" + +#. translators: %d: number of recorded problems. +#: admin/admin.php:5119 +msgid "SureForms has recorded %d problem with the forms on this site." +msgid_plural "SureForms has recorded %d problems with the forms on this site." +msgstr[0] "SureForms a enregistré %d problème avec les formulaires sur ce site." +msgstr[1] "" + +#. translators: %s: form title. +#: admin/admin.php:5169 +msgid "Form: %s" +msgstr "Formulaire : %s" + +#: admin/admin.php:5189 +msgid "none recorded" +msgstr "aucun enregistré" + +#. translators: %s: date and time of the previous report, in the site's timezone. +#: admin/admin.php:5212 +msgid "Previously reported: %s" +msgstr "Déjà signalé : %s" + +#: assets/build/settings.js:172 +msgid "Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures." +msgstr "Les vues et soumissions par toute personne pouvant éditer le site sont normalement omises, afin que tester vos propres formulaires ne modifie pas ces chiffres." + #: inc/post-types.php:265 msgctxt "post type general name" msgid "Forms" diff --git a/languages/sureforms-it_IT-51635fe6489fc8288d603fe596c755ca.json b/languages/sureforms-it_IT-51635fe6489fc8288d603fe596c755ca.json index 199fdd162..65f72aef6 100644 --- a/languages/sureforms-it_IT-51635fe6489fc8288d603fe596c755ca.json +++ b/languages/sureforms-it_IT-51635fe6489fc8288d603fe596c755ca.json @@ -1 +1 @@ -{"translation-revision-date":"2025-01-01T06:16:29+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Cruscotto"],"Settings":["Impostazioni"],"Entries":["Voci"],"Status":["Stato"],"Form":["Modulo"],"Activated":["Attivato"],"Activate":["Attiva"],"Address":["Indirizzo"],"Checkbox":["Casella di controllo"],"Dropdown":["Menu a tendina"],"Email":["Email"],"Number":["Numero"],"Phone":["Telefono"],"Textarea":["Area di testo"],"Monday":["Luned\u00ec"],"Forms":["Moduli"],"New Form Submission - %s":["Nuova presentazione del modulo - %s"],"GitHub":["GitHub"],"(no title)":["(nessun titolo)"],"General":["Generale"],"No tags available":["Nessun tag disponibile"],"Back":["Indietro"],"Generic tags":["Tag generici"],"Other":["Altro"],"Confirm":["Conferma"],"Cancel":["Annulla"],"Install":["Installa"],"Plugin Installation failed, Please try again later.":["Installazione del plugin fallita, per favore riprova pi\u00f9 tardi."],"Plugin activation failed, Please try again later.":["Attivazione del plugin fallita, riprova pi\u00f9 tardi."],"Integrations":["Integrazioni"],"What's New?":["Novit\u00e0?"],"Core":["Nucleo"],"Unlicensed":["Senza licenza"],"Connecting\u2026":["Connessione in corso\u2026"],"Install & Activate":["Installa e attiva"],"Compliance Settings":["Impostazioni di conformit\u00e0"],"Enable GDPR Compliance":["Abilita la conformit\u00e0 GDPR"],"Never store entry data after form submission":["Non memorizzare mai i dati di ingresso dopo l'invio del modulo"],"When enabled this form will never store Entries.":["Quando abilitato, questo modulo non memorizzer\u00e0 mai le voci."],"Automatically delete entries":["Elimina automaticamente le voci"],"When enabled this form will automatically delete entries after a certain period of time.":["Quando abilitato, questo modulo eliminer\u00e0 automaticamente le voci dopo un certo periodo di tempo."],"Entries older than the days set will be deleted automatically.":["Le voci pi\u00f9 vecchie dei giorni impostati verranno eliminate automaticamente."],"Visual":["Visivo"],"HTML":["HTML"],"All Data":["Tutti i dati"],"Add Shortcode":["Aggiungi shortcode"],"Form input tags":["Tag di input del modulo"],"Comma separated values are also accepted.":["Sono accettati anche i valori separati da virgola."],"Email Notification":["Notifica email"],"Name":["Nome"],"Send Email To":["Invia email a"],"Subject":["Oggetto"],"CC":["CC"],"BCC":["Ccn"],"Reply To":["Rispondi a"],"Add Key":["Aggiungi chiave"],"Add Value":["Aggiungi valore"],"Add":["Aggiungi"],"Confirmation Message":["Messaggio di conferma"],"After Form Submission":["Dopo l'invio del modulo"],"Hide Form":["Nascondi modulo"],"Reset Form":["Reimposta modulo"],"Custom URL":["URL personalizzato"],"Add Query Parameters":["Aggiungi parametri di query"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Seleziona se desideri aggiungere coppie chiave-valore per i campi del modulo da includere nei parametri di query"],"Query Parameters":["Parametri di query"],"Success Message":["Messaggio di successo"],"Redirect":["Reindirizza"],"Redirect to":["Reindirizza a"],"Page":["Pagina"],"Form Confirmation":["Conferma del modulo"],"Confirmation Type":["Tipo di conferma"],"Clear":["Chiaro"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invisibile"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Convalide"],"Spam Protection":["Protezione antispam"],"Email Summaries":["Riepiloghi email"],"Tuesday":["Marted\u00ec"],"Wednesday":["Mercoled\u00ec"],"Thursday":["Gioved\u00ec"],"Friday":["Venerd\u00ec"],"Saturday":["Sabato"],"Sunday":["Domenica"],"Test Email":["Email di prova"],"Schedule Reports":["Programma rapporti"],"IP Logging":["Registrazione IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Se questa opzione \u00e8 attivata, l'indirizzo IP dell'utente verr\u00e0 salvato con i dati del modulo"],"Confirmation Email Mismatch Message":["Messaggio di mancata corrispondenza dell'email di conferma"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s rappresenta il valore minimo di input. Ad esempio: \"Il valore minimo \u00e8 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s rappresenta il valore massimo di input. Ad esempio: \"Il valore massimo \u00e8 100.\""],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s rappresenta le selezioni minime necessarie. Ad esempio: \"Sono necessarie almeno 2 selezioni.\""],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s rappresenta il numero massimo di selezioni consentite. Ad esempio: \"Sono consentite al massimo 4 selezioni.\""],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s rappresenta le scelte minime necessarie. Ad esempio: \"\u00c8 richiesta almeno 1 selezione.\""],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s rappresenta il numero massimo di scelte consentite. Ad esempio: \"Sono consentite un massimo di 3 selezioni.\""]," Error Message":["Messaggio di errore"],"Auto":["Auto"],"Light":["Luce"],"Dark":["Scuro"],"Turnstile":["Tornello"],"Honeypot":["Trappola"],"Get Keys":["Ottieni chiavi"],"Documentation":["Documentazione"],"Site Key":["Chiave del sito"],"Secret Key":["Chiave segreta"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Modalit\u00e0 Aspetto"],"Enable Honeypot Security":["Abilita la Sicurezza Honeypot"],"Enable Honeypot Security for better spam protection":["Abilita la Sicurezza Honeypot per una migliore protezione dallo spam"],"Text":["Testo"],"OttoKit":["OttoKit"],"Connect with OttoKit":["Connettiti con OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'attuale indirizzo 'Da Email' non corrisponde al nome di dominio del tuo sito web (%1$s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica. In alternativa, prova a utilizzare un indirizzo Da che corrisponda al dominio del tuo sito web (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'indirizzo email 'Da' attuale non corrisponde al nome di dominio del tuo sito web (%s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica."],"We strongly recommend that you install the free ":["Consigliamo vivamente di installare il gratuito"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin! Il Wizard di configurazione rende facile correggere le tue email."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["In alternativa, prova a utilizzare un indirizzo mittente che corrisponda al dominio del tuo sito web (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Per favore, inserisci un indirizzo email valido. Le tue notifiche non verranno inviate se il campo non \u00e8 compilato correttamente."],"From Name":["Da Nome"],"From Email":["Da Email"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'attuale indirizzo 'Da Email' potrebbe non corrispondere al nome di dominio del tuo sito web (%1$s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica. In alternativa, prova a utilizzare un indirizzo Da che corrisponda al dominio del tuo sito web (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'attuale indirizzo 'Da Email' potrebbe non corrispondere al nome di dominio del tuo sito web (%s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Pronto a superare il piano gratuito?"],"Upgrade now":["Aggiorna ora"],"and unlock the full power of SureForms!":["e sblocca tutta la potenza di SureForms!"],"Upgrade SureForms":["Aggiorna SureForms"],"Upgrade Now":["Aggiorna ora"],"Entries older than the selected days will be deleted.":["Le voci pi\u00f9 vecchie dei giorni selezionati verranno eliminate."],"Entries Time Period":["Periodo di tempo delle voci"],"Notifications can use only one From Email so please enter a single address.":["Le notifiche possono utilizzare un solo indirizzo email del mittente, quindi inserisci un solo indirizzo."],"Select Page to redirect":["Seleziona la pagina a cui reindirizzare"],"Search for a page":["Cerca una pagina"],"Select a page":["Seleziona una pagina"],"Form Validation":["Validazione del modulo"],"Required Error Messages":["Messaggi di errore richiesti"],"Other Error Messages":["Altri messaggi di errore"],"Input Field Unique":["Campo di input univoco"],"Email Field Unique":["Campo email univoco"],"Invalid URL":["URL non valido"],"Phone Field Unique":["Campo Telefono Unico"],"Invalid Field Number Block":["Blocco Numero Campo Non Valido"],"Invalid Email":["Email non valido"],"Number Minimum Value":["Valore Minimo Numero"],"Number Maximum Value":["Valore Massimo Numero"],"Dropdown Minimum Selections":["Selezioni Minime del Menu a Tendina"],"Dropdown Maximum Selections":["Selezioni massime del menu a tendina"],"Multiple Choice Minimum Selections":["Selezioni Minime a Scelta Multipla"],"Multiple Choice Maximum Selections":["Selezioni massime a scelta multipla"],"Input Field":["Campo di input"],"Email Field":["Campo Email"],"URL Field":["Campo URL"],"Phone Field":["Campo Telefono"],"Textarea Field":["Campo di testo"],"Checkbox Field":["Campo di selezione"],"Dropdown Field":["Campo a discesa"],"Multiple Choice Field":["Campo a scelta multipla"],"Address Field":["Campo Indirizzo"],"Number Field":["Campo numerico"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Per abilitare la funzione reCAPTCHA su SureForms, attiva l'opzione reCAPTCHA nelle impostazioni dei tuoi blocchi e seleziona la versione. Aggiungi qui la chiave segreta e la chiave del sito di Google reCAPTCHA. reCAPTCHA verr\u00e0 aggiunto alla tua pagina nel front-end."],"Enter your %s here":["Inserisci qui il tuo %s"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Per abilitare hCAPTCHA, aggiungi la tua chiave del sito e la chiave segreta. Configura queste impostazioni all'interno del modulo individuale."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Per abilitare Cloudflare Turnstile, aggiungi la tua chiave del sito e la chiave segreta. Configura queste impostazioni all'interno del modulo individuale."],"Save":["Salva"],"Anonymous Analytics":["Analisi Anonime"],"Learn More":["Scopri di pi\u00f9"],"Admin Notification":["Notifica dell'amministratore"],"Enable Admin Notification":["Abilita la Notifica Amministratore"],"Admin notifications keep you informed about new form entries since your last visit.":["Le notifiche dell'amministratore ti tengono informato sui nuovi invii di moduli dalla tua ultima visita."],"Skip":["Salta"],"Continue":["Continua"],"Maximum Number of Entries":["Numero massimo di voci"],"Maximum Entries":["Voci massime"],"Response Description After Maximum Entries":["Descrizione della risposta dopo il numero massimo di voci"],"Get Started":["Inizia"],"Integration":["Integrazione"],"Connect Native Integrations with SureForms":["Collega le integrazioni native con SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Sblocca potenti integrazioni nel piano Premium per automatizzare i tuoi flussi di lavoro e collegare SureForms direttamente con i tuoi strumenti preferiti."],"Send form submissions straight to CRMs, email, and marketing platforms":["Invia le sottomissioni dei moduli direttamente ai CRM, email e piattaforme di marketing"],"Automate repetitive tasks with seamless data syncing":["Automatizza le attivit\u00e0 ripetitive con una sincronizzazione dei dati senza interruzioni"],"Access exclusive native integrations for faster workflows":["Accedi a integrazioni native esclusive per flussi di lavoro pi\u00f9 veloci"],"Expected format for emails - email@sureforms.com or John Doe ":["Formato previsto per le email - email@sureforms.com o John Doe "],"Payments":["Pagamenti"],"Stripe account disconnected successfully.":["Account Stripe disconnesso con successo."],"Failed to create webhook.":["Impossibile creare il webhook."],"Failed to connect to Stripe.":["Connessione a Stripe non riuscita."],"Webhook":["Webhook"],"Knowledge Base":["Base di conoscenza"],"What\u2019s New":["Novit\u00e0"],"out of":["fuori da"],"No entries found":["Nessuna voce trovata"],"delete":["elimina"],"Please type \"%s\" in the input box":["Per favore digita \"%s\" nella casella di input"],"To confirm, type \"%s\" in the box below:":["Per confermare, digita \"%s\" nella casella sottostante:"],"Type \"%s\"":["Digita \"%s\""],"Go to OttoKit Settings":["Vai alle Impostazioni di OttoKit"],"USD - US Dollar":["USD - Dollaro statunitense"],"Payment Mode":["Modalit\u00e0 di pagamento"],"Test Mode":["Modalit\u00e0 di test"],"Live Mode":["Modalit\u00e0 Live"],"Action":["Azione"],"General Settings":["Impostazioni generali"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Imposta i riepiloghi delle email, gli avvisi amministrativi e le preferenze sui dati per gestire i tuoi moduli con facilit\u00e0."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personalizza i messaggi di errore predefiniti mostrati quando gli utenti inviano voci di modulo non valide o incomplete."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Abilita la protezione antispam per i tuoi moduli utilizzando servizi CAPTCHA o la sicurezza honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Collega e gestisci i tuoi gateway di pagamento per accettare transazioni in modo sicuro attraverso i tuoi moduli."],"1% transaction and payment gateway fees apply.":["Si applicano commissioni di transazione e gateway di pagamento dell'1%."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Si applicano commissioni di transazione e gateway di pagamento del 2,9%. Attiva la licenza per ridurre le commissioni di transazione."],"2.9% transaction and payment gateway fees apply.":["Si applicano commissioni di transazione e gateway di pagamento del 2,9%."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Si prega di visitare %1$s, eliminare un webhook non utilizzato, quindi fare clic qui sotto per riprovare."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms non \u00e8 riuscito a creare un webhook perch\u00e9 il tuo account Stripe ha esaurito gli slot gratuiti. I webhook sono necessari per ricevere aggiornamenti sui pagamenti."],"Stripe Dashboard":["Dashboard di Stripe"],"Creating\u2026":["Creando\u2026"],"Create Webhook":["Crea Webhook"],"Successfully connected to Stripe!":["Connessione a Stripe riuscita!"],"Invalid response from server. Please try again.":["Risposta non valida dal server. Per favore riprova."],"Failed to disconnect Stripe account.":["Impossibile disconnettere l'account Stripe."],"Webhook created successfully!":["Webhook creato con successo!"],"Select Currency":["Seleziona valuta"],"Select the default currency for payment forms.":["Seleziona la valuta predefinita per i moduli di pagamento."],"Connection Status":["Stato della connessione"],"Disconnect Stripe Account":["Disconnetti l'account Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Sei sicuro di voler scollegare il tuo account Stripe? Questo interromper\u00e0 tutti i pagamenti attivi, gli abbonamenti e le transazioni dei moduli collegati a questo account."],"Disconnect":["Disconnetti"],"Disconnecting\u2026":["Disconnessione in corso\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook collegato con successo, tutti gli eventi di Stripe vengono tracciati."],"Connect your Stripe account to start accepting payments through your forms.":["Collega il tuo account Stripe per iniziare ad accettare pagamenti tramite i tuoi moduli."],"Connect to Stripe":["Connetti a Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Collegati in modo sicuro a Stripe con pochi clic per iniziare ad accettare pagamenti!"],"Set the total number of submissions allowed for this form.":["Imposta il numero totale di invii consentiti per questo modulo."],"Payment Methods":["Metodi di pagamento"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["La modalit\u00e0 di test ti consente di elaborare pagamenti senza addebiti reali. Passa alla modalit\u00e0 Live per le transazioni effettive."],"General Payment Settings":["Impostazioni generali di pagamento"],"These settings apply to all payment gateways.":["Queste impostazioni si applicano a tutti i gateway di pagamento."],"Stripe Settings":["Impostazioni di Stripe"],"Left ($100)":["Sinistra ($100)"],"Right (100$)":["Destra (100$)"],"Left Space ($ 100)":["Spazio Sinistro ($ 100)"],"Right Space (100 $)":["Spazio destro (100 $)"],"Currency Sign Position":["Posizione del simbolo di valuta"],"Select the position of the currency symbol relative to the amount.":["Seleziona la posizione del simbolo della valuta rispetto all'importo."],"Learn":["Impara"],"Enable email summaries":["Abilita i riepiloghi email"],"Enable IP logging":["Abilita la registrazione IP"],"Turn on Admin Notification from here.":["Attiva la notifica amministratore da qui."],"New":["Nuovo"],"Send entries to 100+ popular apps.":["Invia voci a oltre 100 app popolari."],"Build automated workflows that run instantly.":["Crea flussi di lavoro automatizzati che vengono eseguiti istantaneamente."],"Create custom app integrations using our Custom App feature.":["Crea integrazioni di app personalizzate utilizzando la nostra funzione App personalizzata."],"Keep your tools in sync automatically.":["Mantieni i tuoi strumenti sincronizzati automaticamente."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Questo installer\u00e0 e attiver\u00e0 OttoKit sul tuo sito WordPress per abilitare le funzionalit\u00e0 di automazione."],"Automate Your Forms with OttoKit":["Automatizza i tuoi moduli con OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Ogni invio di modulo dovrebbe attivare qualcosa: un avviso Slack, un lead CRM, un'email di follow-up o una nuova riga in Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configura le autorizzazioni del client AI e le impostazioni del server MCP."],"View documentation":["Visualizza la documentazione"],"Copy to clipboard":["Copia negli appunti"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) o %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Codice Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (progetto) o ~\/.claude.json (globale)"],"Cursor":["Cursore"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (progetto) o settings.json > mcp.servers (globale)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml o config.json"],"Your client's MCP configuration file":["Il file di configurazione MCP del tuo cliente"],"Connect Your AI Client":["Collega il tuo client AI"],"AI Client":["Cliente AI"],"Create an Application Password \u2014 ":["Crea una Password per l'Applicazione \u2014"],"Open Application Passwords":["Apri Password Applicazioni"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Oppure usa questo comando CLI per aggiungere rapidamente il server (dovrai comunque impostare le variabili d'ambiente):"],"Copy the JSON config below into: ":["Copia la configurazione JSON qui sotto in:"],"Replace \"your-application-password\" with the password from Step 1.":["Sostituisci \"your-application-password\" con la password del Passo 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 l'endpoint MCP del tuo sito. WP_API_USERNAME \u2014 il tuo nome utente WordPress. WP_API_PASSWORD \u2014 la password dell'applicazione che hai generato."],"View setup docs":["Visualizza i documenti di configurazione"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Il plugin MCP Adapter \u00e8 installato ma non attivo. Attivalo per configurare le impostazioni MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Il plugin MCP Adapter \u00e8 necessario per connettere i client AI ai tuoi moduli. Scaricalo e installalo da GitHub, quindi attivalo."],"Download the latest release from":["Scarica l'ultima versione da"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installa il plugin tramite Plugin > Aggiungi nuovo plugin > Carica plugin."],"Activate the MCP Adapter plugin.":["Attiva il plugin dell'adattatore MCP."],"Activating\u2026":["Attivazione in corso\u2026"],"Activate MCP Adapter":["Attiva l'adattatore MCP"],"Download MCP Adapter":["Scarica l'adattatore MCP"],"Experimental":["Sperimentale"],"Enable Abilities":["Abilita Abilit\u00e0"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registra le abilit\u00e0 di SureForms con l'API Abilities di WordPress. Quando \u00e8 abilitato, i client AI possono elencare, leggere, creare, modificare e eliminare i tuoi moduli e le voci. Quando \u00e8 disabilitato, nessuna abilit\u00e0 \u00e8 registrata e i client AI non possono eseguire alcuna azione sui tuoi moduli."],"Abilities API \u2014 Edit":["API delle abilit\u00e0 \u2014 Modifica"],"Enable Edit Abilities":["Abilita le capacit\u00e0 di modifica"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Quando abilitato, i client AI possono creare nuovi moduli, aggiornare i titoli dei moduli, i campi e le impostazioni, duplicare i moduli e modificare gli stati delle voci. Quando disabilitato, queste capacit\u00e0 vengono annullate e i client AI possono solo leggere i tuoi dati."],"Abilities API \u2014 Delete":["API delle abilit\u00e0 \u2014 Elimina"],"Enable Delete Abilities":["Abilita Elimina Abilit\u00e0"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Quando abilitato, i client AI possono eliminare permanentemente moduli e voci. I dati eliminati non possono essere recuperati. Quando disabilitato, le capacit\u00e0 di eliminazione vengono annullate e i client AI non possono rimuovere alcun dato."],"MCP Server":["Server MCP"],"Enable MCP Server":["Abilita server MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Crea un endpoint SureForms MCP dedicato a cui i clienti AI come Claude possono connettersi. Quando \u00e8 disabilitato, l'endpoint viene rimosso e i clienti AI esterni non possono scoprire o chiamare alcuna funzionalit\u00e0 di SureForms."],"Learn more":["Scopri di pi\u00f9"],"MCP Adapter Required":["Adattatore MCP richiesto"],"Heading 1":["Intestazione 1"],"Heading 2":["Intestazione 2"],"Heading 3":["Intestazione 3"],"Heading 4":["Intestazione 4"],"Heading 5":["Intestazione 5"],"Heading 6":["Intestazione 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configura la chiave API di Google Maps per il completamento automatico degli indirizzi e l'anteprima della mappa."],"Help shape the future of SureForms":["Aiuta a plasmare il futuro di SureForms"],"Enable Google Address Autocomplete":["Abilita il completamento automatico degli indirizzi di Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Passa al piano SureForms Business per aggiungere il completamento automatico degli indirizzi con tecnologia Google e l'anteprima della mappa interattiva ai tuoi moduli."],"Auto-suggest addresses as users type for faster, error-free submissions":["Suggerisci automaticamente gli indirizzi mentre gli utenti digitano per invii pi\u00f9 rapidi e senza errori"],"Show an interactive map preview with draggable pin for precise locations":["Mostra un'anteprima della mappa interattiva con un segnaposto trascinabile per posizioni precise"],"Automatically populate address fields like city, state, and postal code":["Compila automaticamente i campi dell'indirizzo come citt\u00e0, stato e codice postale"],"This form is now closed as we've received all the entries.":["Questo modulo \u00e8 ora chiuso poich\u00e9 abbiamo ricevuto tutte le iscrizioni."],"Thank you for contacting us! We will be in touch with you shortly.":["Grazie per averci contattato! Saremo in contatto con te a breve."],"Saving\u2026":["Salvataggio in corso\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Quando abilitato, questo modulo non memorizzer\u00e0 l'IP dell'utente, il nome del browser e il nome del dispositivo nelle voci."],"Form data":["Dati del modulo"],"Unsaved changes":["Modifiche non salvate"],"Keep editing":["Continua a modificare"],"Global Defaults":["Impostazioni predefinite globali"],"Form Restrictions":["Restrizioni del modulo"],"Configure default settings that apply to newly created forms.":["Configura le impostazioni predefinite che si applicano ai moduli appena creati."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Raccogli informazioni non sensibili dal tuo sito web, come la versione PHP e le funzionalit\u00e0 utilizzate, per aiutarci a correggere i bug pi\u00f9 velocemente, prendere decisioni pi\u00f9 intelligenti e sviluppare funzionalit\u00e0 che contano davvero per te."],"Failed to load pages. Please refresh and try again.":["Caricamento delle pagine non riuscito. Si prega di aggiornare e riprovare."],"Failed to load settings. Please refresh and try again.":["Caricamento delle impostazioni non riuscito. Si prega di aggiornare e riprovare."],"Form Validation fields cannot be left blank.":["I campi di convalida del modulo non possono essere lasciati vuoti."],"Recipient email is required when email summaries are enabled.":["L'email del destinatario \u00e8 necessaria quando i riepiloghi email sono abilitati."],"Please enter a valid recipient email.":["Si prega di inserire un'email del destinatario valida."],"Settings saved.":["Impostazioni salvate."],"Failed to save settings.":["Impossibile salvare le impostazioni."],"Some settings failed to save. Please retry.":["Alcune impostazioni non sono state salvate. Riprova."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Alcuni campi hanno modifiche non salvate. Scartale per continuare, oppure rimani per salvare le tue modifiche."],"Discard & switch":["Scarta e cambia"],"Import":["Importa"],"Migration":["Migrazione"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importa moduli da Contact Form 7, WPForms, Gravity Forms e altri plugin in SureForms."],"Could not generate the import preview.":["Impossibile generare l'anteprima dell'importazione."],"Import failed. Please try again or check your error logs.":["Importazione non riuscita. Riprova o controlla i tuoi registri degli errori."],"Go back":["Torna indietro"],"Update & import":["Aggiorna e importa"],"Confirm & import":["Conferma e importa"],"Form #%s":["Modulo n.%s"],"%d field will import":["%d campo verr\u00e0 importato"],"Some fields can't be migrated yet":["Alcuni campi non possono essere ancora migrati"],"These fields will be skipped - add them manually after the form is created:":["Questi campi verranno saltati - aggiungili manualmente dopo che il modulo \u00e8 stato creato:"],"Some forms could not be parsed":["Alcuni moduli non possono essere analizzati"],"Update existing":["Aggiorna esistente"],"Create a new copy":["Crea una nuova copia"],"Could not load forms for this source.":["Impossibile caricare i moduli per questa fonte."],"(untitled form)":["(modulo senza titolo)"],"Previously imported":["Importato in precedenza"],"Open existing SureForms form in a new tab":["Apri il modulo SureForms esistente in una nuova scheda"],"On re-import":["Al momento della reimportazione"],"No forms found in this plugin.":["Nessun modulo trovato in questo plugin."],"%1$d of %2$d form selected":["%1$d di %2$d modulo selezionato"],"Preview update":["Anteprima aggiornamento"],"Preview import":["Anteprima importazione"],"Migration complete":["Migrazione completata"],"Nothing was imported":["Non \u00e8 stato importato nulla"],"%d form was imported into SureForms.":["%d modulo \u00e8 stato importato in SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Nessuno dei moduli selezionati pu\u00f2 essere migrato. Vedi gli avvisi qui sotto per i dettagli."],"Imported forms":["Moduli importati"],"Edit in SureForms":["Modifica in SureForms"],"Some fields were skipped during import":["Alcuni campi sono stati saltati durante l'importazione"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Aggiungi questi manualmente nell'editor di moduli - non hanno ancora un equivalente SureForms:"],"Some forms failed to import":["Alcuni moduli non sono stati importati"],"Import more forms":["Importa pi\u00f9 moduli"],"Could not load the list of importable plugins.":["Impossibile caricare l'elenco dei plugin importabili."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Non ci sono plugin di moduli supportati attivi su questo sito. Attiva uno (come Contact Form 7) con almeno un modulo per importarlo in SureForms."],"Plugin":["Plugin"],"%d form":["%d modulo"],"No forms":["Nessun modulo"],"Multiple choice":["Scelta multipla"],"Consent":["Consenso"],"The image uploaded but could not be inserted. Please try again.":["L'immagine \u00e8 stata caricata ma non \u00e8 stato possibile inserirla. Per favore riprova."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["Non \u00e8 stato possibile caricare l'immagine. Potrebbe essere troppo grande o potresti non avere il permesso di caricare file."],"Please choose an image file.":["Per favore scegli un file immagine."],"Show views and conversion rate":["Mostra visualizzazioni e tasso di conversione"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Aggiunge le colonne Visualizzazioni e Tasso di Conversione all'elenco dei Moduli. Una visualizzazione viene conteggiata una volta per visita alla pagina quando il modulo appare sullo schermo, e il tasso di conversione \u00e8 la quota di quelle visualizzazioni che si sono concluse con un invio. Il conteggio inizia la prima volta che lo attivi, quindi gli invii ricevuti prima non vengono conteggiati nel tasso. Disattivarlo successivamente nasconde solo le colonne \u2014 il conteggio continua, quindi le cifre sono aggiornate se lo riattivi."],"Log cleared.":["Registro cancellato."],"Could not clear the log.":["Impossibile cancellare il registro."],"Enable logs":["Abilita registri"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registra i fallimenti di invio dei moduli segnalati dal browser, cos\u00ec puoi inviare il registro al supporto invece di leggere la console. Nulla viene scritto mentre i tuoi moduli funzionano, e i valori inviati non vengono mai memorizzati."],"Download log":["Scarica registro"],"Confirm delete":["Conferma eliminazione"],"Log is full \u2014 download and clear it to keep recording.":["Il registro \u00e8 pieno \u2014 scaricalo e cancellalo per continuare a registrare."],"Form Views & Conversion":["Visualizzazioni e Conversione Moduli"],"Logs":["Registri"],"Quill heading picker: default paragraph style\u0004Normal":["Normale"]}}} \ No newline at end of file +{"translation-revision-date":"2025-01-01T06:16:29+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/settings.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Cruscotto"],"Settings":["Impostazioni"],"Entries":["Voci"],"Status":["Stato"],"Form":["Modulo"],"Activated":["Attivato"],"Activate":["Attiva"],"Address":["Indirizzo"],"Checkbox":["Casella di controllo"],"Dropdown":["Menu a tendina"],"Email":["Email"],"Number":["Numero"],"Phone":["Telefono"],"Textarea":["Area di testo"],"Monday":["Luned\u00ec"],"Forms":["Moduli"],"New Form Submission - %s":["Nuova presentazione del modulo - %s"],"GitHub":["GitHub"],"(no title)":["(nessun titolo)"],"General":["Generale"],"No tags available":["Nessun tag disponibile"],"Back":["Indietro"],"Generic tags":["Tag generici"],"Other":["Altro"],"Confirm":["Conferma"],"Cancel":["Annulla"],"Install":["Installa"],"Plugin Installation failed, Please try again later.":["Installazione del plugin fallita, per favore riprova pi\u00f9 tardi."],"Plugin activation failed, Please try again later.":["Attivazione del plugin fallita, riprova pi\u00f9 tardi."],"Integrations":["Integrazioni"],"What's New?":["Novit\u00e0?"],"Core":["Nucleo"],"Unlicensed":["Senza licenza"],"Connecting\u2026":["Connessione in corso\u2026"],"Install & Activate":["Installa e attiva"],"Compliance Settings":["Impostazioni di conformit\u00e0"],"Enable GDPR Compliance":["Abilita la conformit\u00e0 GDPR"],"Never store entry data after form submission":["Non memorizzare mai i dati di ingresso dopo l'invio del modulo"],"When enabled this form will never store Entries.":["Quando abilitato, questo modulo non memorizzer\u00e0 mai le voci."],"Automatically delete entries":["Elimina automaticamente le voci"],"When enabled this form will automatically delete entries after a certain period of time.":["Quando abilitato, questo modulo eliminer\u00e0 automaticamente le voci dopo un certo periodo di tempo."],"Entries older than the days set will be deleted automatically.":["Le voci pi\u00f9 vecchie dei giorni impostati verranno eliminate automaticamente."],"Visual":["Visivo"],"HTML":["HTML"],"All Data":["Tutti i dati"],"Add Shortcode":["Aggiungi shortcode"],"Form input tags":["Tag di input del modulo"],"Comma separated values are also accepted.":["Sono accettati anche i valori separati da virgola."],"Email Notification":["Notifica email"],"Name":["Nome"],"Send Email To":["Invia email a"],"Subject":["Oggetto"],"CC":["CC"],"BCC":["Ccn"],"Reply To":["Rispondi a"],"Add Key":["Aggiungi chiave"],"Add Value":["Aggiungi valore"],"Add":["Aggiungi"],"Confirmation Message":["Messaggio di conferma"],"After Form Submission":["Dopo l'invio del modulo"],"Hide Form":["Nascondi modulo"],"Reset Form":["Reimposta modulo"],"Custom URL":["URL personalizzato"],"Add Query Parameters":["Aggiungi parametri di query"],"Select if you want to add key-value pairs for form fields to include in query parameters":["Seleziona se desideri aggiungere coppie chiave-valore per i campi del modulo da includere nei parametri di query"],"Query Parameters":["Parametri di query"],"Success Message":["Messaggio di successo"],"Redirect":["Reindirizza"],"Redirect to":["Reindirizza a"],"Page":["Pagina"],"Form Confirmation":["Conferma del modulo"],"Confirmation Type":["Tipo di conferma"],"Clear":["Chiaro"],"Google reCAPTCHA":["Google reCAPTCHA"],"hCaptcha":["hCaptcha"],"reCAPTCHA v2 Invisible":["reCAPTCHA v2 Invisibile"],"reCAPTCHA v3":["reCAPTCHA v3"],"URL":["URL"],"Validations":["Convalide"],"Spam Protection":["Protezione antispam"],"Email Summaries":["Riepiloghi email"],"Tuesday":["Marted\u00ec"],"Wednesday":["Mercoled\u00ec"],"Thursday":["Gioved\u00ec"],"Friday":["Venerd\u00ec"],"Saturday":["Sabato"],"Sunday":["Domenica"],"Test Email":["Email di prova"],"Schedule Reports":["Programma rapporti"],"IP Logging":["Registrazione IP"],"If this option is turned on, the user's IP address will be saved with the form data":["Se questa opzione \u00e8 attivata, l'indirizzo IP dell'utente verr\u00e0 salvato con i dati del modulo"],"Confirmation Email Mismatch Message":["Messaggio di mancata corrispondenza dell'email di conferma"],"%s represents the minimum input value. For example: \"Minimum value is 10.\"":["%s rappresenta il valore minimo di input. Ad esempio: \"Il valore minimo \u00e8 10.\""],"%s represents the maximum input value. For example: \"Maximum value is 100.\"":["%s rappresenta il valore massimo di input. Ad esempio: \"Il valore massimo \u00e8 100.\""],"%s represents the minimum selections needed. For example: \u201cMinimum 2 selections are required.\u201d":["%s rappresenta le selezioni minime necessarie. Ad esempio: \"Sono necessarie almeno 2 selezioni.\""],"%s represents the maximum selections allowed. For example: \u201cMaximum 4 selections are allowed.\u201d":["%s rappresenta il numero massimo di selezioni consentite. Ad esempio: \"Sono consentite al massimo 4 selezioni.\""],"%s represents the minimum choices needed. For example: \u201cMinimum 1 selection is required.\u201d":["%s rappresenta le scelte minime necessarie. Ad esempio: \"\u00c8 richiesta almeno 1 selezione.\""],"%s represents the maximum choices allowed. For example: \u201cMaximum 3 selections are allowed.\u201d":["%s rappresenta il numero massimo di scelte consentite. Ad esempio: \"Sono consentite un massimo di 3 selezioni.\""]," Error Message":["Messaggio di errore"],"Auto":["Auto"],"Light":["Luce"],"Dark":["Scuro"],"Turnstile":["Tornello"],"Honeypot":["Trappola"],"Get Keys":["Ottieni chiavi"],"Documentation":["Documentazione"],"Site Key":["Chiave del sito"],"Secret Key":["Chiave segreta"],"Cloudflare Turnstile":["Cloudflare Turnstile"],"Appearance Mode":["Modalit\u00e0 Aspetto"],"Enable Honeypot Security":["Abilita la Sicurezza Honeypot"],"Enable Honeypot Security for better spam protection":["Abilita la Sicurezza Honeypot per una migliore protezione dallo spam"],"Text":["Testo"],"OttoKit":["OttoKit"],"Connect with OttoKit":["Connettiti con OttoKit"],"The current 'From Email' address does not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'attuale indirizzo 'Da Email' non corrisponde al nome di dominio del tuo sito web (%1$s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica. In alternativa, prova a utilizzare un indirizzo Da che corrisponda al dominio del tuo sito web (admin@%2$s)."],"The current 'From Email' address does not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'indirizzo email 'Da' attuale non corrisponde al nome di dominio del tuo sito web (%s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica."],"We strongly recommend that you install the free ":["Consigliamo vivamente di installare il gratuito"]," plugin! The Setup Wizard makes it easy to fix your emails. ":["plugin! Il Wizard di configurazione rende facile correggere le tue email."]," Alternately, try using a From Address that matches your website domain (admin@%s).":["In alternativa, prova a utilizzare un indirizzo mittente che corrisponda al dominio del tuo sito web (admin@%s)."],"Please enter a valid email address. Your notifications won't be sent if the field is not filled in correctly.":["Per favore, inserisci un indirizzo email valido. Le tue notifiche non verranno inviate se il campo non \u00e8 compilato correttamente."],"From Name":["Da Nome"],"From Email":["Da Email"],"The current 'From Email' address may not match your website domain name (%1$s). This can cause your notification emails to be blocked or marked as spam. Alternately, try using a From Address that matches your website domain (admin@%2$s).":["L'attuale indirizzo 'Da Email' potrebbe non corrispondere al nome di dominio del tuo sito web (%1$s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica. In alternativa, prova a utilizzare un indirizzo Da che corrisponda al dominio del tuo sito web (admin@%2$s)."],"The current 'From Email' address may not match your website domain name (%s). This can cause your notification emails to be blocked or marked as spam. ":["L'attuale indirizzo 'Da Email' potrebbe non corrispondere al nome di dominio del tuo sito web (%s). Questo pu\u00f2 causare il blocco o la classificazione come spam delle tue email di notifica."],"reCAPTCHA":["reCAPTCHA"],"Ready to go beyond free plan?":["Pronto a superare il piano gratuito?"],"Upgrade now":["Aggiorna ora"],"and unlock the full power of SureForms!":["e sblocca tutta la potenza di SureForms!"],"Upgrade SureForms":["Aggiorna SureForms"],"Upgrade Now":["Aggiorna ora"],"Entries older than the selected days will be deleted.":["Le voci pi\u00f9 vecchie dei giorni selezionati verranno eliminate."],"Entries Time Period":["Periodo di tempo delle voci"],"Notifications can use only one From Email so please enter a single address.":["Le notifiche possono utilizzare un solo indirizzo email del mittente, quindi inserisci un solo indirizzo."],"Select Page to redirect":["Seleziona la pagina a cui reindirizzare"],"Search for a page":["Cerca una pagina"],"Select a page":["Seleziona una pagina"],"Form Validation":["Validazione del modulo"],"Required Error Messages":["Messaggi di errore richiesti"],"Other Error Messages":["Altri messaggi di errore"],"Input Field Unique":["Campo di input univoco"],"Email Field Unique":["Campo email univoco"],"Invalid URL":["URL non valido"],"Phone Field Unique":["Campo Telefono Unico"],"Invalid Field Number Block":["Blocco Numero Campo Non Valido"],"Invalid Email":["Email non valido"],"Number Minimum Value":["Valore Minimo Numero"],"Number Maximum Value":["Valore Massimo Numero"],"Dropdown Minimum Selections":["Selezioni Minime del Menu a Tendina"],"Dropdown Maximum Selections":["Selezioni massime del menu a tendina"],"Multiple Choice Minimum Selections":["Selezioni Minime a Scelta Multipla"],"Multiple Choice Maximum Selections":["Selezioni massime a scelta multipla"],"Input Field":["Campo di input"],"Email Field":["Campo Email"],"URL Field":["Campo URL"],"Phone Field":["Campo Telefono"],"Textarea Field":["Campo di testo"],"Checkbox Field":["Campo di selezione"],"Dropdown Field":["Campo a discesa"],"Multiple Choice Field":["Campo a scelta multipla"],"Address Field":["Campo Indirizzo"],"Number Field":["Campo numerico"],"reCAPTCHA v2":["reCAPTCHA v2"],"To enable reCAPTCHA feature on your SureForms Please enable reCAPTCHA option on your blocks setting and select version. Add google reCAPTCHA secret and site key here. reCAPTCHA will be added to your page on front-end.":["Per abilitare la funzione reCAPTCHA su SureForms, attiva l'opzione reCAPTCHA nelle impostazioni dei tuoi blocchi e seleziona la versione. Aggiungi qui la chiave segreta e la chiave del sito di Google reCAPTCHA. reCAPTCHA verr\u00e0 aggiunto alla tua pagina nel front-end."],"Enter your %s here":["Inserisci qui il tuo %s"],"To enable hCAPTCHA, please add your site key and secret key. Configure these settings within the individual form.":["Per abilitare hCAPTCHA, aggiungi la tua chiave del sito e la chiave segreta. Configura queste impostazioni all'interno del modulo individuale."],"To enable Cloudflare Turnstile, please add your site key and secret key. Configure these settings within the individual form.":["Per abilitare Cloudflare Turnstile, aggiungi la tua chiave del sito e la chiave segreta. Configura queste impostazioni all'interno del modulo individuale."],"Save":["Salva"],"Anonymous Analytics":["Analisi Anonime"],"Learn More":["Scopri di pi\u00f9"],"Admin Notification":["Notifica dell'amministratore"],"Enable Admin Notification":["Abilita la Notifica Amministratore"],"Admin notifications keep you informed about new form entries since your last visit.":["Le notifiche dell'amministratore ti tengono informato sui nuovi invii di moduli dalla tua ultima visita."],"Skip":["Salta"],"Continue":["Continua"],"Maximum Number of Entries":["Numero massimo di voci"],"Maximum Entries":["Voci massime"],"Response Description After Maximum Entries":["Descrizione della risposta dopo il numero massimo di voci"],"Get Started":["Inizia"],"Integration":["Integrazione"],"Connect Native Integrations with SureForms":["Collega le integrazioni native con SureForms"],"Unlock powerful integrations in the Premium plan to automate your workflows and connect SureForms directly with your favourite tools.":["Sblocca potenti integrazioni nel piano Premium per automatizzare i tuoi flussi di lavoro e collegare SureForms direttamente con i tuoi strumenti preferiti."],"Send form submissions straight to CRMs, email, and marketing platforms":["Invia le sottomissioni dei moduli direttamente ai CRM, email e piattaforme di marketing"],"Automate repetitive tasks with seamless data syncing":["Automatizza le attivit\u00e0 ripetitive con una sincronizzazione dei dati senza interruzioni"],"Access exclusive native integrations for faster workflows":["Accedi a integrazioni native esclusive per flussi di lavoro pi\u00f9 veloci"],"Expected format for emails - email@sureforms.com or John Doe ":["Formato previsto per le email - email@sureforms.com o John Doe "],"Payments":["Pagamenti"],"Stripe account disconnected successfully.":["Account Stripe disconnesso con successo."],"Failed to create webhook.":["Impossibile creare il webhook."],"Failed to connect to Stripe.":["Connessione a Stripe non riuscita."],"Webhook":["Webhook"],"Knowledge Base":["Base di conoscenza"],"What\u2019s New":["Novit\u00e0"],"out of":["fuori da"],"No entries found":["Nessuna voce trovata"],"delete":["elimina"],"Please type \"%s\" in the input box":["Per favore digita \"%s\" nella casella di input"],"To confirm, type \"%s\" in the box below:":["Per confermare, digita \"%s\" nella casella sottostante:"],"Type \"%s\"":["Digita \"%s\""],"Go to OttoKit Settings":["Vai alle Impostazioni di OttoKit"],"USD - US Dollar":["USD - Dollaro statunitense"],"Payment Mode":["Modalit\u00e0 di pagamento"],"Test Mode":["Modalit\u00e0 di test"],"Live Mode":["Modalit\u00e0 Live"],"Action":["Azione"],"General Settings":["Impostazioni generali"],"Set up email summaries, admin alerts, and data preferences to manage your forms with ease.":["Imposta i riepiloghi delle email, gli avvisi amministrativi e le preferenze sui dati per gestire i tuoi moduli con facilit\u00e0."],"Customize default error messages shown when users submit invalid or incomplete form entries.":["Personalizza i messaggi di errore predefiniti mostrati quando gli utenti inviano voci di modulo non valide o incomplete."],"Enable spam protection for your forms using CAPTCHA services or honeypot security.":["Abilita la protezione antispam per i tuoi moduli utilizzando servizi CAPTCHA o la sicurezza honeypot."],"Connect and manage your payment gateways to securely accept transactions through your forms.":["Collega e gestisci i tuoi gateway di pagamento per accettare transazioni in modo sicuro attraverso i tuoi moduli."],"1% transaction and payment gateway fees apply.":["Si applicano commissioni di transazione e gateway di pagamento dell'1%."],"2.9% transaction and payment gateway fees apply. Activate license to reduce transaction fees.":["Si applicano commissioni di transazione e gateway di pagamento del 2,9%. Attiva la licenza per ridurre le commissioni di transazione."],"2.9% transaction and payment gateway fees apply.":["Si applicano commissioni di transazione e gateway di pagamento del 2,9%."],"Please visit %1$s, delete an unused webhook, then click below to retry.":["Si prega di visitare %1$s, eliminare un webhook non utilizzato, quindi fare clic qui sotto per riprovare."],"SureForms could not create a webhook because your Stripe account has run out of free slots. Webhooks are needed to receive updates about payments.":["SureForms non \u00e8 riuscito a creare un webhook perch\u00e9 il tuo account Stripe ha esaurito gli slot gratuiti. I webhook sono necessari per ricevere aggiornamenti sui pagamenti."],"Stripe Dashboard":["Dashboard di Stripe"],"Creating\u2026":["Creando\u2026"],"Create Webhook":["Crea Webhook"],"Successfully connected to Stripe!":["Connessione a Stripe riuscita!"],"Invalid response from server. Please try again.":["Risposta non valida dal server. Per favore riprova."],"Failed to disconnect Stripe account.":["Impossibile disconnettere l'account Stripe."],"Webhook created successfully!":["Webhook creato con successo!"],"Select Currency":["Seleziona valuta"],"Select the default currency for payment forms.":["Seleziona la valuta predefinita per i moduli di pagamento."],"Connection Status":["Stato della connessione"],"Disconnect Stripe Account":["Disconnetti l'account Stripe"],"Are you sure you want to disconnect your Stripe account? This will stop all active payments, subscriptions, and form transactions connected to this account.":["Sei sicuro di voler scollegare il tuo account Stripe? Questo interromper\u00e0 tutti i pagamenti attivi, gli abbonamenti e le transazioni dei moduli collegati a questo account."],"Disconnect":["Disconnetti"],"Disconnecting\u2026":["Disconnessione in corso\u2026"],"Webhook successfully connected, all Stripe events are being tracked.":["Webhook collegato con successo, tutti gli eventi di Stripe vengono tracciati."],"Connect your Stripe account to start accepting payments through your forms.":["Collega il tuo account Stripe per iniziare ad accettare pagamenti tramite i tuoi moduli."],"Connect to Stripe":["Connetti a Stripe"],"Securely connect to Stripe with just a few clicks to begin accepting payments! ":["Collegati in modo sicuro a Stripe con pochi clic per iniziare ad accettare pagamenti!"],"Set the total number of submissions allowed for this form.":["Imposta il numero totale di invii consentiti per questo modulo."],"Payment Methods":["Metodi di pagamento"],"Test mode allows you to process payments without real charges. Switch to Live mode for actual transactions.":["La modalit\u00e0 di test ti consente di elaborare pagamenti senza addebiti reali. Passa alla modalit\u00e0 Live per le transazioni effettive."],"General Payment Settings":["Impostazioni generali di pagamento"],"These settings apply to all payment gateways.":["Queste impostazioni si applicano a tutti i gateway di pagamento."],"Stripe Settings":["Impostazioni di Stripe"],"Left ($100)":["Sinistra ($100)"],"Right (100$)":["Destra (100$)"],"Left Space ($ 100)":["Spazio Sinistro ($ 100)"],"Right Space (100 $)":["Spazio destro (100 $)"],"Currency Sign Position":["Posizione del simbolo di valuta"],"Select the position of the currency symbol relative to the amount.":["Seleziona la posizione del simbolo della valuta rispetto all'importo."],"Learn":["Impara"],"Enable email summaries":["Abilita i riepiloghi email"],"Enable IP logging":["Abilita la registrazione IP"],"Turn on Admin Notification from here.":["Attiva la notifica amministratore da qui."],"New":["Nuovo"],"Send entries to 100+ popular apps.":["Invia voci a oltre 100 app popolari."],"Build automated workflows that run instantly.":["Crea flussi di lavoro automatizzati che vengono eseguiti istantaneamente."],"Create custom app integrations using our Custom App feature.":["Crea integrazioni di app personalizzate utilizzando la nostra funzione App personalizzata."],"Keep your tools in sync automatically.":["Mantieni i tuoi strumenti sincronizzati automaticamente."],"This will install and activate OttoKit on your WordPress site to enable automation features.":["Questo installer\u00e0 e attiver\u00e0 OttoKit sul tuo sito WordPress per abilitare le funzionalit\u00e0 di automazione."],"Automate Your Forms with OttoKit":["Automatizza i tuoi moduli con OttoKit"],"Every form submission should trigger something \u2014 a Slack alert, a CRM lead, a follow-up email, or a new row in Google Sheets.":["Ogni invio di modulo dovrebbe attivare qualcosa: un avviso Slack, un lead CRM, un'email di follow-up o una nuova riga in Google Sheets."],"MCP":["MCP"],"Configure AI client permissions and MCP server settings.":["Configura le autorizzazioni del client AI e le impostazioni del server MCP."],"View documentation":["Visualizza la documentazione"],"Copy to clipboard":["Copia negli appunti"],"Claude Desktop":["Claude Desktop"],"~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)":["~\/Library\/Application Support\/Claude\/claude_desktop_config.json (macOS) o %APPDATA%\\Claude\\claude_desktop_config.json (Windows)"],"Claude Code":["Codice Claude"],".mcp.json (project) or ~\/.claude.json (global)":[".mcp.json (progetto) o ~\/.claude.json (globale)"],"Cursor":["Cursore"],"~\/.cursor\/mcp.json":["~\/.cursor\/mcp.json"],"VS Code (Copilot)":["VS Code (Copilot)"],".vscode\/mcp.json (project) or settings.json > mcp.servers (global)":[".vscode\/mcp.json (progetto) o settings.json > mcp.servers (globale)"],"~\/.continue\/config.yaml or config.json":["~\/.continue\/config.yaml o config.json"],"Your client's MCP configuration file":["Il file di configurazione MCP del tuo cliente"],"Connect Your AI Client":["Collega il tuo client AI"],"AI Client":["Cliente AI"],"Create an Application Password \u2014 ":["Crea una Password per l'Applicazione \u2014"],"Open Application Passwords":["Apri Password Applicazioni"],"Or use this CLI command to add the server quickly (you will still need to set the environment variables):":["Oppure usa questo comando CLI per aggiungere rapidamente il server (dovrai comunque impostare le variabili d'ambiente):"],"Copy the JSON config below into: ":["Copia la configurazione JSON qui sotto in:"],"Replace \"your-application-password\" with the password from Step 1.":["Sostituisci \"your-application-password\" con la password del Passo 1."],"WP_API_URL \u2014 your site's MCP endpoint. WP_API_USERNAME \u2014 your WordPress username. WP_API_PASSWORD \u2014 the application password you generated.":["WP_API_URL \u2014 l'endpoint MCP del tuo sito. WP_API_USERNAME \u2014 il tuo nome utente WordPress. WP_API_PASSWORD \u2014 la password dell'applicazione che hai generato."],"View setup docs":["Visualizza i documenti di configurazione"],"The MCP Adapter plugin is installed but not active. Activate it to configure MCP settings.":["Il plugin MCP Adapter \u00e8 installato ma non attivo. Attivalo per configurare le impostazioni MCP."],"The MCP Adapter plugin is required to connect AI clients to your forms. Download and install it from GitHub, then activate it.":["Il plugin MCP Adapter \u00e8 necessario per connettere i client AI ai tuoi moduli. Scaricalo e installalo da GitHub, quindi attivalo."],"Download the latest release from":["Scarica l'ultima versione da"],"Install the plugin via Plugins > Add New Plugin > Upload Plugin.":["Installa il plugin tramite Plugin > Aggiungi nuovo plugin > Carica plugin."],"Activate the MCP Adapter plugin.":["Attiva il plugin dell'adattatore MCP."],"Activating\u2026":["Attivazione in corso\u2026"],"Activate MCP Adapter":["Attiva l'adattatore MCP"],"Download MCP Adapter":["Scarica l'adattatore MCP"],"Experimental":["Sperimentale"],"Enable Abilities":["Abilita Abilit\u00e0"],"Register SureForms abilities with the WordPress Abilities API. When enabled, AI clients can list, read, create, edit, and delete your forms and entries. When disabled, no abilities are registered and AI clients cannot perform any actions on your forms.":["Registra le abilit\u00e0 di SureForms con l'API Abilities di WordPress. Quando \u00e8 abilitato, i client AI possono elencare, leggere, creare, modificare e eliminare i tuoi moduli e le voci. Quando \u00e8 disabilitato, nessuna abilit\u00e0 \u00e8 registrata e i client AI non possono eseguire alcuna azione sui tuoi moduli."],"Abilities API \u2014 Edit":["API delle abilit\u00e0 \u2014 Modifica"],"Enable Edit Abilities":["Abilita le capacit\u00e0 di modifica"],"When enabled, AI clients can create new forms, update form titles, fields, and settings, duplicate forms, and modify entry statuses. When disabled, these abilities are unregistered and AI clients can only read your data.":["Quando abilitato, i client AI possono creare nuovi moduli, aggiornare i titoli dei moduli, i campi e le impostazioni, duplicare i moduli e modificare gli stati delle voci. Quando disabilitato, queste capacit\u00e0 vengono annullate e i client AI possono solo leggere i tuoi dati."],"Abilities API \u2014 Delete":["API delle abilit\u00e0 \u2014 Elimina"],"Enable Delete Abilities":["Abilita Elimina Abilit\u00e0"],"When enabled, AI clients can permanently delete forms and entries. Deleted data cannot be recovered. When disabled, delete abilities are unregistered and AI clients cannot remove any data.":["Quando abilitato, i client AI possono eliminare permanentemente moduli e voci. I dati eliminati non possono essere recuperati. Quando disabilitato, le capacit\u00e0 di eliminazione vengono annullate e i client AI non possono rimuovere alcun dato."],"MCP Server":["Server MCP"],"Enable MCP Server":["Abilita server MCP"],"Creates a dedicated SureForms MCP endpoint that AI clients like Claude can connect to. When disabled, the endpoint is removed and external AI clients cannot discover or call any SureForms abilities.":["Crea un endpoint SureForms MCP dedicato a cui i clienti AI come Claude possono connettersi. Quando \u00e8 disabilitato, l'endpoint viene rimosso e i clienti AI esterni non possono scoprire o chiamare alcuna funzionalit\u00e0 di SureForms."],"Learn more":["Scopri di pi\u00f9"],"MCP Adapter Required":["Adattatore MCP richiesto"],"Heading 1":["Intestazione 1"],"Heading 2":["Intestazione 2"],"Heading 3":["Intestazione 3"],"Heading 4":["Intestazione 4"],"Heading 5":["Intestazione 5"],"Heading 6":["Intestazione 6"],"Google Maps":["Google Maps"],"Configure Google Maps API key for address autocomplete and map preview.":["Configura la chiave API di Google Maps per il completamento automatico degli indirizzi e l'anteprima della mappa."],"Help shape the future of SureForms":["Aiuta a plasmare il futuro di SureForms"],"Enable Google Address Autocomplete":["Abilita il completamento automatico degli indirizzi di Google"],"Upgrade to the SureForms Business Plan to add Google-powered address autocomplete with interactive map preview to your forms.":["Passa al piano SureForms Business per aggiungere il completamento automatico degli indirizzi con tecnologia Google e l'anteprima della mappa interattiva ai tuoi moduli."],"Auto-suggest addresses as users type for faster, error-free submissions":["Suggerisci automaticamente gli indirizzi mentre gli utenti digitano per invii pi\u00f9 rapidi e senza errori"],"Show an interactive map preview with draggable pin for precise locations":["Mostra un'anteprima della mappa interattiva con un segnaposto trascinabile per posizioni precise"],"Automatically populate address fields like city, state, and postal code":["Compila automaticamente i campi dell'indirizzo come citt\u00e0, stato e codice postale"],"This form is now closed as we've received all the entries.":["Questo modulo \u00e8 ora chiuso poich\u00e9 abbiamo ricevuto tutte le iscrizioni."],"Thank you for contacting us! We will be in touch with you shortly.":["Grazie per averci contattato! Saremo in contatto con te a breve."],"Saving\u2026":["Salvataggio in corso\u2026"],"When enabled this form will not store User IP, Browser Name and the Device Name in the Entries.":["Quando abilitato, questo modulo non memorizzer\u00e0 l'IP dell'utente, il nome del browser e il nome del dispositivo nelle voci."],"Form data":["Dati del modulo"],"Unsaved changes":["Modifiche non salvate"],"Keep editing":["Continua a modificare"],"Global Defaults":["Impostazioni predefinite globali"],"Form Restrictions":["Restrizioni del modulo"],"Configure default settings that apply to newly created forms.":["Configura le impostazioni predefinite che si applicano ai moduli appena creati."],"Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. ":["Raccogli informazioni non sensibili dal tuo sito web, come la versione PHP e le funzionalit\u00e0 utilizzate, per aiutarci a correggere i bug pi\u00f9 velocemente, prendere decisioni pi\u00f9 intelligenti e sviluppare funzionalit\u00e0 che contano davvero per te."],"Failed to load pages. Please refresh and try again.":["Caricamento delle pagine non riuscito. Si prega di aggiornare e riprovare."],"Failed to load settings. Please refresh and try again.":["Caricamento delle impostazioni non riuscito. Si prega di aggiornare e riprovare."],"Form Validation fields cannot be left blank.":["I campi di convalida del modulo non possono essere lasciati vuoti."],"Recipient email is required when email summaries are enabled.":["L'email del destinatario \u00e8 necessaria quando i riepiloghi email sono abilitati."],"Please enter a valid recipient email.":["Si prega di inserire un'email del destinatario valida."],"Settings saved.":["Impostazioni salvate."],"Failed to save settings.":["Impossibile salvare le impostazioni."],"Some settings failed to save. Please retry.":["Alcune impostazioni non sono state salvate. Riprova."],"Some fields have unsaved changes. Discard them to continue, or stay to save your edits.":["Alcuni campi hanno modifiche non salvate. Scartale per continuare, oppure rimani per salvare le tue modifiche."],"Discard & switch":["Scarta e cambia"],"Import":["Importa"],"Migration":["Migrazione"],"Import forms from Contact Form 7, WPForms, Gravity Forms, and other plugins into SureForms.":["Importa moduli da Contact Form 7, WPForms, Gravity Forms e altri plugin in SureForms."],"Could not generate the import preview.":["Impossibile generare l'anteprima dell'importazione."],"Import failed. Please try again or check your error logs.":["Importazione non riuscita. Riprova o controlla i tuoi registri degli errori."],"Go back":["Torna indietro"],"Update & import":["Aggiorna e importa"],"Confirm & import":["Conferma e importa"],"Form #%s":["Modulo n.%s"],"%d field will import":["%d campo verr\u00e0 importato"],"Some fields can't be migrated yet":["Alcuni campi non possono essere ancora migrati"],"These fields will be skipped - add them manually after the form is created:":["Questi campi verranno saltati - aggiungili manualmente dopo che il modulo \u00e8 stato creato:"],"Some forms could not be parsed":["Alcuni moduli non possono essere analizzati"],"Update existing":["Aggiorna esistente"],"Create a new copy":["Crea una nuova copia"],"Could not load forms for this source.":["Impossibile caricare i moduli per questa fonte."],"(untitled form)":["(modulo senza titolo)"],"Previously imported":["Importato in precedenza"],"Open existing SureForms form in a new tab":["Apri il modulo SureForms esistente in una nuova scheda"],"On re-import":["Al momento della reimportazione"],"No forms found in this plugin.":["Nessun modulo trovato in questo plugin."],"%1$d of %2$d form selected":["%1$d di %2$d modulo selezionato"],"Preview update":["Anteprima aggiornamento"],"Preview import":["Anteprima importazione"],"Migration complete":["Migrazione completata"],"Nothing was imported":["Non \u00e8 stato importato nulla"],"%d form was imported into SureForms.":["%d modulo \u00e8 stato importato in SureForms."],"None of the selected forms could be migrated. See the warnings below for details.":["Nessuno dei moduli selezionati pu\u00f2 essere migrato. Vedi gli avvisi qui sotto per i dettagli."],"Imported forms":["Moduli importati"],"Edit in SureForms":["Modifica in SureForms"],"Some fields were skipped during import":["Alcuni campi sono stati saltati durante l'importazione"],"Add these manually in the form editor - they have no SureForms equivalent yet:":["Aggiungi questi manualmente nell'editor di moduli - non hanno ancora un equivalente SureForms:"],"Some forms failed to import":["Alcuni moduli non sono stati importati"],"Import more forms":["Importa pi\u00f9 moduli"],"Could not load the list of importable plugins.":["Impossibile caricare l'elenco dei plugin importabili."],"No supported form plugins are active on this site. Activate one (such as Contact Form 7) with at least one form to import it into SureForms.":["Non ci sono plugin di moduli supportati attivi su questo sito. Attiva uno (come Contact Form 7) con almeno un modulo per importarlo in SureForms."],"Plugin":["Plugin"],"%d form":["%d modulo"],"No forms":["Nessun modulo"],"Multiple choice":["Scelta multipla"],"Consent":["Consenso"],"The image uploaded but could not be inserted. Please try again.":["L'immagine \u00e8 stata caricata ma non \u00e8 stato possibile inserirla. Per favore riprova."],"The image could not be uploaded. It may be too large, or you may not have permission to upload files.":["Non \u00e8 stato possibile caricare l'immagine. Potrebbe essere troppo grande o potresti non avere il permesso di caricare file."],"Please choose an image file.":["Per favore scegli un file immagine."],"Show views and conversion rate":["Mostra visualizzazioni e tasso di conversione"],"Adds the Views and Conversion Rate columns to the Forms list. A view is counted once per page visit when the form appears on screen, and the conversion rate is the share of those views that ended in a submission. Counting starts the first time you turn this on, so submissions received before then are not counted towards the rate. Turning it off afterwards only hides the columns \u2014 counting continues, so the figures are up to date if you switch it back on.":["Aggiunge le colonne Visualizzazioni e Tasso di Conversione all'elenco dei Moduli. Una visualizzazione viene conteggiata una volta per visita alla pagina quando il modulo appare sullo schermo, e il tasso di conversione \u00e8 la quota di quelle visualizzazioni che si sono concluse con un invio. Il conteggio inizia la prima volta che lo attivi, quindi gli invii ricevuti prima non vengono conteggiati nel tasso. Disattivarlo successivamente nasconde solo le colonne \u2014 il conteggio continua, quindi le cifre sono aggiornate se lo riattivi."],"Log cleared.":["Registro cancellato."],"Could not clear the log.":["Impossibile cancellare il registro."],"Enable logs":["Abilita registri"],"Records form submission failures reported by the browser, so you can send the log to support instead of reading the console. Nothing is written while your forms are working, and submitted values are never stored.":["Registra i fallimenti di invio dei moduli segnalati dal browser, cos\u00ec puoi inviare il registro al supporto invece di leggere la console. Nulla viene scritto mentre i tuoi moduli funzionano, e i valori inviati non vengono mai memorizzati."],"Download log":["Scarica registro"],"Confirm delete":["Conferma eliminazione"],"Log is full \u2014 download and clear it to keep recording.":["Il registro \u00e8 pieno \u2014 scaricalo e cancellalo per continuare a registrare."],"Form Views & Conversion":["Visualizzazioni e Conversione Moduli"],"Logs":["Registri"],"Views and submissions by anyone who can edit the site are normally left out, so testing your own forms does not change these figures.":["Le visualizzazioni e gli invii da parte di chiunque possa modificare il sito sono normalmente omessi, quindi testare i propri moduli non modifica queste cifre."],"Quill heading picker: default paragraph style\u0004Normal":["Normale"]}}} \ No newline at end of file diff --git a/languages/sureforms-it_IT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json b/languages/sureforms-it_IT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json index 9eef0678f..129468c65 100644 --- a/languages/sureforms-it_IT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json +++ b/languages/sureforms-it_IT-a8e4a9e4a048ac2cb1c6e28cb6b9f3a1.json @@ -1 +1 @@ -{"translation-revision-date":"2025-01-01T06:16:29+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Cruscotto"],"Settings":["Impostazioni"],"Entries":["Voci"],"Activated":["Attivato"],"Advanced Fields":["Campi Avanzati"],"Select Form":["Seleziona modulo"],"Create New Form":["Crea nuovo modulo"],"Forms":["Moduli"],"Copy":["Copia"],"Business":["Affari"],"Next":["Avanti"],"Back":["Indietro"],"Cancel":["Annulla"],"This is where your form views will appear":["Qui \u00e8 dove appariranno le visualizzazioni del tuo modulo"],"Install":["Installa"],"Plugin Installation failed, Please try again later.":["Installazione del plugin fallita, per favore riprova pi\u00f9 tardi."],"Plugin activation failed, Please try again later.":["Attivazione del plugin fallita, riprova pi\u00f9 tardi."],"What's New?":["Novit\u00e0?"],"Core":["Nucleo"],"Unlicensed":["Senza licenza"],"Upgrade":["Aggiornamento"],"Webhooks":["Webhooks"],"Install & Activate":["Installa e attiva"],"Conditional Logic":["Logica Condizionale"],"Premium":["Premium"],"Welcome to SureForms!":["Benvenuto su SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms \u00e8 un plugin per WordPress che consente agli utenti di creare moduli dall'aspetto elegante tramite un'interfaccia drag-and-drop, senza bisogno di programmare. Si integra con l'editor a blocchi di WordPress."],"Read Full Guide":["Leggi la guida completa"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Moduli personalizzati per WordPress RESI SEMPLICI"],"No Date":["Nessuna data"],"Invalid Date":["Data non valida"],"Ready to go beyond free plan?":["Pronto a superare il piano gratuito?"],"Upgrade now":["Aggiorna ora"],"and unlock the full power of SureForms!":["e sblocca tutta la potenza di SureForms!"],"Upgrade SureForms":["Aggiorna SureForms"],"Open Support Ticket":["Apri un ticket di supporto"],"Help Center":["Centro assistenza"],"Join our Community on Facebook":["Unisciti alla nostra community su Facebook"],"Leave Us a Review":["Lasciaci una recensione"],"Quick Access":["Accesso rapido"],"Upgrade Now":["Aggiorna ora"],"Clear Filters":["Cancella filtri"],"Unnamed Form":["Modulo senza nome"],"Forms Overview":["Panoramica dei moduli"],"Clear Form Filters":["Cancella i filtri del modulo"],"Clear Date Filters":["Cancella i filtri di data"],"Select Date Range":["Seleziona intervallo di date"],"Apply":["Applica"],"Please wait for the data to load":["Per favore, attendi che i dati vengano caricati"],"No entries to display":["Nessuna voce da visualizzare"],"Once you create a form and start receiving submissions, the data will appear here.":["Una volta che crei un modulo e inizi a ricevere invii, i dati appariranno qui."],"Free":["Gratis"],"All Forms":["Tutti i moduli"],"Guided Setup":["Configurazione guidata"],"Exit Guided Setup":["Esci dalla configurazione guidata"],"Skip":["Salta"],"Build beautiful forms visually":["Crea moduli belli visivamente"],"Works perfectly on mobile":["Funziona perfettamente su mobile"],"Easy to connect with automation tools":["Facile da collegare con strumenti di automazione"],"Welcome to SureForms":["Benvenuto su SureForms"],"Smart, Quick and Powerful Forms.":["Moduli intelligenti, rapidi e potenti."],"Let's Get Started":["Iniziamo"],"Build up to 10 forms using AI":["Costruisci fino a 10 moduli utilizzando l'IA"],"A secure and private connection":["Una connessione sicura e privata"],"Smart form drafts based on your input":["Bozze di moduli intelligenti basate sul tuo input"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Iniziare da un modulo vuoto non \u00e8 sempre facile. La nostra IA pu\u00f2 aiutarti creando un modulo di bozza basato su ci\u00f2 che stai cercando di fare, risparmiando tempo e fornendoti una direzione chiara."],"To do this, you'll need to connect your account.":["Per fare ci\u00f2, dovrai collegare il tuo account."],"Let AI Help You Build Smarter, Faster Forms":["Lascia che l'IA ti aiuti a creare moduli pi\u00f9 intelligenti e veloci"],"Here's what that gives you:":["Ecco cosa ti d\u00e0:"],"Continue":["Continua"],"Connect":["Connetti"],"Works smoothly with forms made using SureForms":["Funziona senza problemi con i moduli creati utilizzando SureForms"],"Helps your emails reach the inbox instead of spam":["Aiuta le tue email a raggiungere la casella di posta invece della cartella spam"],"Setup is straightforward, even if you're not technical":["L'installazione \u00e8 semplice, anche se non sei esperto di tecnologia"],"Lightweight and easy to use without adding clutter":["Leggero e facile da usare senza aggiungere disordine"],"Make Sure Your Emails Get Delivered":["Assicurati che le tue email vengano consegnate"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["La maggior parte dei siti WordPress fatica a inviare email in modo affidabile, il che significa che le invii dei moduli dal tuo sito potrebbero non raggiungere la tua casella di posta \u2014 o finire nello spam."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail \u00e8 un semplice plugin SMTP che aiuta a garantire che le tue email vengano effettivamente consegnate."],"What you will get:":["Cosa otterrai:"],"Install SureMail":["Installa SureMail"],"AI Form Generation":["Generazione di moduli AI"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Stanco di creare moduli manualmente? Lascia che l'IA faccia il lavoro per te. Basta descrivere e la nostra IA creer\u00e0 il tuo modulo perfetto in pochi secondi."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Scomponi moduli complessi in passaggi semplici, riducendo il sovraccarico e aumentando i tassi di completamento. Guida gli utenti senza intoppi attraverso il processo"],"Conditional Fields":["Campi Condizionali"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Mostra o nascondi i campi in base alle risposte degli utenti. Fai le domande giuste e visualizza solo ci\u00f2 che \u00e8 necessario per mantenere i moduli puliti e pertinenti."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Migliora i tuoi moduli con campi avanzati come il caricamento di pi\u00f9 file, campi di valutazione e selettori di data e ora per raccogliere dati pi\u00f9 ricchi e flessibili."],"Conversational Forms":["Forme Conversazionali"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Crea moduli che sembrano una conversazione. Una domanda alla volta mantiene gli utenti coinvolti e rende facile il completamento del modulo."],"Digital Signatures":["Firme digitali"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Raccogli firme digitali legalmente vincolanti direttamente nei tuoi moduli per accordi, approvazioni e contratti."],"Calculators":["Calcolatrici"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Aggiungi calcolatori interattivi ai tuoi moduli per stime, preventivi e calcoli istantanei per i tuoi utenti."],"User Registration and Login":["Registrazione e accesso utente"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Consenti ai visitatori di registrarsi e accedere al tuo sito. Utile per siti di abbonamento, comunit\u00e0 o qualsiasi sito che richieda l'accesso degli utenti."],"PDF Generation Made Simple":["Generazione PDF resa semplice"],"Custom App":["App personalizzata"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Raccogli dati, inviali ad applicazioni esterne per l'elaborazione e visualizza i risultati istantaneamente, tutto perfettamente integrato per creare esperienze utente dinamiche e interattive."],"Select Your Features":["Seleziona le tue funzionalit\u00e0"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Ottieni pi\u00f9 controllo, flussi di lavoro pi\u00f9 rapidi e una personalizzazione pi\u00f9 profonda, tutto progettato per aiutarti a creare siti web migliori con meno sforzo."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Le funzionalit\u00e0 selezionate richiedono %1$s - usa il codice %2$s per ottenere il 10% di sconto su qualsiasi piano."],"Copied":["Copiato"],"Style your form to better match your site's design":["Stilizza il tuo modulo per adattarlo meglio al design del tuo sito"],"Add spam protection to block common bot submissions":["Aggiungi protezione antispam per bloccare le comuni sottomissioni di bot"],"Get weekly email reports with a summary of form activity":["Ricevi rapporti settimanali via email con un riepilogo dell'attivit\u00e0 del modulo"],"You're All Set! \ud83d\ude80":["Tutto pronto! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Usa il nostro generatore di moduli AI per iniziare rapidamente, oppure crea il tuo modulo da zero se sai gi\u00e0 cosa ti serve. I tuoi moduli sono pronti per essere creati, condivisi e collegati con i visitatori del tuo sito."],"Final Touches That Make a Difference:":["Ritocchi finali che fanno la differenza:"],"Build Your First Form":["Crea il tuo primo modulo"],"File Uploads":["Caricamenti di file"],"Signature & Rating":["Firma e Valutazione"],"Calculation Forms":["Moduli di Calcolo"],"And Much More\u2026":["E molto altro ancora\u2026"],"Upgrade to Pro":["Passa a Pro"],"Unlock Premium Features":["Sblocca le funzionalit\u00e0 premium"],"Build Better Forms with SureForms":["Costruisci moduli migliori con SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Aggiungi campi avanzati, layout conversazionali e logica intelligente per creare moduli che coinvolgano gli utenti e catturino dati migliori."],"SureForms Video Thumbnail":["Miniatura video di SureForms"],"Payments":["Pagamenti"],"Knowledge Base":["Base di conoscenza"],"What\u2019s New":["Novit\u00e0"],"Importing\u2026":["Importazione in corso\u2026"],"Learn":["Impara"],"Unable to complete action. Please try again.":["Impossibile completare l'azione. Per favore riprova."],"Supercharge Your Workflow":["Potenzia il tuo flusso di lavoro"],"Spam protection included":["Protezione antispam inclusa"],"Multistep Forms":["Moduli a pi\u00f9 fasi"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Invia le voci del modulo istantaneamente a qualsiasi sistema o endpoint esterno per alimentare flussi di lavoro avanzati."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Trasforma automaticamente le voci del modulo in PDF puliti e pronti per il download. Perfetto per registri, condivisione, archiviazione o per mantenere tutto organizzato."],"Set up confirmation messages and email notifications for each entry":["Imposta i messaggi di conferma e le notifiche email per ogni voce"],"Something went wrong. Please try again.":["Qualcosa \u00e8 andato storto. Per favore riprova."],"Payment Forms":["Moduli di pagamento"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Raccogli pagamenti direttamente attraverso i tuoi moduli. Accetta pagamenti una tantum e ricorrenti senza problemi."],"SureForms %s":["SureForms %s"],"First name is required.":["Il nome \u00e8 obbligatorio."],"Please enter a valid email address.":["Per favore inserisci un indirizzo email valido."],"Email address is required.":["L'indirizzo email \u00e8 obbligatorio."],"This is required.":["Questo \u00e8 necessario."],"Okay, just one last step\u2026":["Va bene, solo un ultimo passo\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Aiutaci a personalizzare la tua esperienza con SureForms condividendo alcune informazioni su di te."],"First Name":["Nome"],"Enter your first name":["Inserisci il tuo nome"],"Last Name":["Cognome"],"Enter your last name":["Inserisci il tuo cognome"],"Email Address":["Indirizzo email"],"Enter your email address":["Inserisci il tuo indirizzo email"],"Privacy Policy":["Informativa sulla privacy"],"Finish":["Finire"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Rimani aggiornato e contribuisci a plasmare SureForms! Ricevi aggiornamenti sulle funzionalit\u00e0 e aiutaci a migliorare SureForms condividendo come utilizzi il plugin. Informativa sulla privacy<\/a>."],"%d form ready":["%d modulo pronto"],"%d already imported":["%d gi\u00e0 importati"],"(unnamed source)":["(fonte senza nome)"],"All forms already imported":["Tutti i moduli gi\u00e0 importati"],"Import forms":["Importa moduli"],"Import %d form":["Importa %d modulo"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Si \u00e8 verificato un errore durante l'importazione. Puoi riprovare, saltare o aprire Impostazioni \u2192 Migrazione."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Nessun altro plugin di moduli rilevato. Puoi importare in qualsiasi momento da Impostazioni \u2192 Migrazione."],"Forms imported":["Moduli importati"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["Il modulo %1$d da %2$s \u00e8 ora in SureForms, pronto per essere pubblicato, stilizzato e connesso."],"%d form imported":["%d modulo importato"],"%d form could not be imported":["%d modulo non pu\u00f2 essere importato"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d tipo di campo non era supportato. Puoi ricostruirlo manualmente all'interno di SureForms."],"Bring your existing forms with you":["Porta con te i tuoi moduli esistenti"],"We detected forms in another plugin. Pick one to import into SureForms.":["Abbiamo rilevato moduli in un altro plugin. Scegli uno da importare in SureForms."],"Choose a form plugin to import from":["Scegli un plugin di modulo da cui importare"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importare pi\u00f9 di un plugin? Puoi importare ulteriori fonti successivamente da Impostazioni \u2192 Migrazione."],"Import did not complete":["L'importazione non \u00e8 stata completata"],"I'll do this later":["Lo far\u00f2 pi\u00f9 tardi"],"Retry":["Riprova"],"Form Checks":["Controlli del modulo"],"Collapse form checks":["Comprimi i controlli del modulo"],"Expand form checks":["Espandi i controlli del modulo"],"Ignore":["Ignora"],"Working\u2026":["Lavorando\u2026"]}}} \ No newline at end of file +{"translation-revision-date":"2025-01-01T06:16:29+00:00","generator":"WP-CLI\/2.12.0","source":"assets\/build\/dashboard.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Dashboard":["Cruscotto"],"Settings":["Impostazioni"],"Entries":["Voci"],"Activated":["Attivato"],"Advanced Fields":["Campi Avanzati"],"Select Form":["Seleziona modulo"],"Create New Form":["Crea nuovo modulo"],"Forms":["Moduli"],"Copy":["Copia"],"Business":["Affari"],"Next":["Avanti"],"Back":["Indietro"],"Close":["Chiudi"],"Cancel":["Annulla"],"This is where your form views will appear":["Qui \u00e8 dove appariranno le visualizzazioni del tuo modulo"],"Install":["Installa"],"Plugin Installation failed, Please try again later.":["Installazione del plugin fallita, per favore riprova pi\u00f9 tardi."],"Plugin activation failed, Please try again later.":["Attivazione del plugin fallita, riprova pi\u00f9 tardi."],"What's New?":["Novit\u00e0?"],"Core":["Nucleo"],"Unlicensed":["Senza licenza"],"Upgrade":["Aggiornamento"],"Webhooks":["Webhooks"],"Install & Activate":["Installa e attiva"],"Contact Support":["Contatta il supporto"],"Conditional Logic":["Logica Condizionale"],"Premium":["Premium"],"Welcome to SureForms!":["Benvenuto su SureForms!"],"SureForms is a WordPress plugin that enables users to create beautiful looking forms through a drag-and-drop interface, without needing to code. It integrates with the WordPress block editor.":["SureForms \u00e8 un plugin per WordPress che consente agli utenti di creare moduli dall'aspetto elegante tramite un'interfaccia drag-and-drop, senza bisogno di programmare. Si integra con l'editor a blocchi di WordPress."],"Read Full Guide":["Leggi la guida completa"],"SureForms: Custom WordPress Forms MADE SIMPLE":["SureForms: Moduli personalizzati per WordPress RESI SEMPLICI"],"No Date":["Nessuna data"],"Invalid Date":["Data non valida"],"Ready to go beyond free plan?":["Pronto a superare il piano gratuito?"],"Upgrade now":["Aggiorna ora"],"and unlock the full power of SureForms!":["e sblocca tutta la potenza di SureForms!"],"Upgrade SureForms":["Aggiorna SureForms"],"Open Support Ticket":["Apri un ticket di supporto"],"Help Center":["Centro assistenza"],"Join our Community on Facebook":["Unisciti alla nostra community su Facebook"],"Leave Us a Review":["Lasciaci una recensione"],"Quick Access":["Accesso rapido"],"Upgrade Now":["Aggiorna ora"],"Clear Filters":["Cancella filtri"],"Unnamed Form":["Modulo senza nome"],"Forms Overview":["Panoramica dei moduli"],"Clear Form Filters":["Cancella i filtri del modulo"],"Clear Date Filters":["Cancella i filtri di data"],"Select Date Range":["Seleziona intervallo di date"],"Apply":["Applica"],"Please wait for the data to load":["Per favore, attendi che i dati vengano caricati"],"No entries to display":["Nessuna voce da visualizzare"],"Once you create a form and start receiving submissions, the data will appear here.":["Una volta che crei un modulo e inizi a ricevere invii, i dati appariranno qui."],"Free":["Gratis"],"All Forms":["Tutti i moduli"],"Guided Setup":["Configurazione guidata"],"Exit Guided Setup":["Esci dalla configurazione guidata"],"Skip":["Salta"],"Build beautiful forms visually":["Crea moduli belli visivamente"],"Works perfectly on mobile":["Funziona perfettamente su mobile"],"Easy to connect with automation tools":["Facile da collegare con strumenti di automazione"],"Welcome to SureForms":["Benvenuto su SureForms"],"Smart, Quick and Powerful Forms.":["Moduli intelligenti, rapidi e potenti."],"Let's Get Started":["Iniziamo"],"Build up to 10 forms using AI":["Costruisci fino a 10 moduli utilizzando l'IA"],"A secure and private connection":["Una connessione sicura e privata"],"Smart form drafts based on your input":["Bozze di moduli intelligenti basate sul tuo input"],"Starting from a blank form isn't always easy. Our AI can help by creating a draft form based on what you're trying to do \u2014 saving you time and giving you a clear direction.":["Iniziare da un modulo vuoto non \u00e8 sempre facile. La nostra IA pu\u00f2 aiutarti creando un modulo di bozza basato su ci\u00f2 che stai cercando di fare, risparmiando tempo e fornendoti una direzione chiara."],"To do this, you'll need to connect your account.":["Per fare ci\u00f2, dovrai collegare il tuo account."],"Let AI Help You Build Smarter, Faster Forms":["Lascia che l'IA ti aiuti a creare moduli pi\u00f9 intelligenti e veloci"],"Here's what that gives you:":["Ecco cosa ti d\u00e0:"],"Continue":["Continua"],"Connect":["Connetti"],"Works smoothly with forms made using SureForms":["Funziona senza problemi con i moduli creati utilizzando SureForms"],"Helps your emails reach the inbox instead of spam":["Aiuta le tue email a raggiungere la casella di posta invece della cartella spam"],"Setup is straightforward, even if you're not technical":["L'installazione \u00e8 semplice, anche se non sei esperto di tecnologia"],"Lightweight and easy to use without adding clutter":["Leggero e facile da usare senza aggiungere disordine"],"Make Sure Your Emails Get Delivered":["Assicurati che le tue email vengano consegnate"],"Most WordPress sites struggle to send emails reliably, which means form submissions from your site might not reach your inbox \u2014 or end up in spam.":["La maggior parte dei siti WordPress fatica a inviare email in modo affidabile, il che significa che le invii dei moduli dal tuo sito potrebbero non raggiungere la tua casella di posta \u2014 o finire nello spam."],"SureMail is a simple SMTP plugin that helps make sure your emails actually get delivered.":["SureMail \u00e8 un semplice plugin SMTP che aiuta a garantire che le tue email vengano effettivamente consegnate."],"What you will get:":["Cosa otterrai:"],"Install SureMail":["Installa SureMail"],"AI Form Generation":["Generazione di moduli AI"],"Tired of building forms manually? Let AI do the work for you. Just describe and our AI will create your perfect form in seconds.":["Stanco di creare moduli manualmente? Lascia che l'IA faccia il lavoro per te. Basta descrivere e la nostra IA creer\u00e0 il tuo modulo perfetto in pochi secondi."],"Break complex forms into simple steps, reducing overwhelm and boosting completion rates. Guide users smoothly through the process":["Scomponi moduli complessi in passaggi semplici, riducendo il sovraccarico e aumentando i tassi di completamento. Guida gli utenti senza intoppi attraverso il processo"],"Conditional Fields":["Campi Condizionali"],"Show or hide fields based on user answers. Ask the right questions and display only what's needed to keep forms clean and relevant.":["Mostra o nascondi i campi in base alle risposte degli utenti. Fai le domande giuste e visualizza solo ci\u00f2 che \u00e8 necessario per mantenere i moduli puliti e pertinenti."],"Enhance your forms with advanced fields like multi-file upload, rating fields, and date & time pickers to collect richer, flexible data.":["Migliora i tuoi moduli con campi avanzati come il caricamento di pi\u00f9 file, campi di valutazione e selettori di data e ora per raccogliere dati pi\u00f9 ricchi e flessibili."],"Conversational Forms":["Forme Conversazionali"],"Create forms that feel like a conversation. One question at a time keeps users engaged and makes form completion easy.":["Crea moduli che sembrano una conversazione. Una domanda alla volta mantiene gli utenti coinvolti e rende facile il completamento del modulo."],"Digital Signatures":["Firme digitali"],"Collect legally binding digital signatures directly in your forms for agreements, approvals, and contracts.":["Raccogli firme digitali legalmente vincolanti direttamente nei tuoi moduli per accordi, approvazioni e contratti."],"Calculators":["Calcolatrici"],"Add interactive calculators to your forms for instant estimates, quotes, and calculations for your users.":["Aggiungi calcolatori interattivi ai tuoi moduli per stime, preventivi e calcoli istantanei per i tuoi utenti."],"User Registration and Login":["Registrazione e accesso utente"],"Allow visitors to register and log in to your site. Useful for membership, community, or any site that needs user access.":["Consenti ai visitatori di registrarsi e accedere al tuo sito. Utile per siti di abbonamento, comunit\u00e0 o qualsiasi sito che richieda l'accesso degli utenti."],"PDF Generation Made Simple":["Generazione PDF resa semplice"],"Custom App":["App personalizzata"],"Collect data, send it to external applications for processing, and display results instantly \u2014 all seamlessly integrated to create dynamic, interactive user experiences.":["Raccogli dati, inviali ad applicazioni esterne per l'elaborazione e visualizza i risultati istantaneamente, tutto perfettamente integrato per creare esperienze utente dinamiche e interattive."],"Select Your Features":["Seleziona le tue funzionalit\u00e0"],"Get more control, faster workflows, and deeper customization \u2014 all designed to help you build better websites with less effort.":["Ottieni pi\u00f9 controllo, flussi di lavoro pi\u00f9 rapidi e una personalizzazione pi\u00f9 profonda, tutto progettato per aiutarti a creare siti web migliori con meno sforzo."],"Selected features require %1$s - use code %2$s to get 10% off on any plan.":["Le funzionalit\u00e0 selezionate richiedono %1$s - usa il codice %2$s per ottenere il 10% di sconto su qualsiasi piano."],"Copied":["Copiato"],"Style your form to better match your site's design":["Stilizza il tuo modulo per adattarlo meglio al design del tuo sito"],"Add spam protection to block common bot submissions":["Aggiungi protezione antispam per bloccare le comuni sottomissioni di bot"],"Get weekly email reports with a summary of form activity":["Ricevi rapporti settimanali via email con un riepilogo dell'attivit\u00e0 del modulo"],"You're All Set! \ud83d\ude80":["Tutto pronto! \ud83d\ude80"],"Use our AI form builder to get started quickly, or build your form from scratch if you already know what you need. Your forms are ready to create, share, and connect with your site visitors.":["Usa il nostro generatore di moduli AI per iniziare rapidamente, oppure crea il tuo modulo da zero se sai gi\u00e0 cosa ti serve. I tuoi moduli sono pronti per essere creati, condivisi e collegati con i visitatori del tuo sito."],"Final Touches That Make a Difference:":["Ritocchi finali che fanno la differenza:"],"Build Your First Form":["Crea il tuo primo modulo"],"File Uploads":["Caricamenti di file"],"Signature & Rating":["Firma e Valutazione"],"Calculation Forms":["Moduli di Calcolo"],"And Much More\u2026":["E molto altro ancora\u2026"],"Upgrade to Pro":["Passa a Pro"],"Unlock Premium Features":["Sblocca le funzionalit\u00e0 premium"],"Build Better Forms with SureForms":["Costruisci moduli migliori con SureForms"],"Add advanced fields, conversational layouts, and smart logic to create forms that engage users and capture better data.":["Aggiungi campi avanzati, layout conversazionali e logica intelligente per creare moduli che coinvolgano gli utenti e catturino dati migliori."],"SureForms Video Thumbnail":["Miniatura video di SureForms"],"Payments":["Pagamenti"],"Knowledge Base":["Base di conoscenza"],"What\u2019s New":["Novit\u00e0"],"Importing\u2026":["Importazione in corso\u2026"],"Learn":["Impara"],"Unable to complete action. Please try again.":["Impossibile completare l'azione. Per favore riprova."],"Supercharge Your Workflow":["Potenzia il tuo flusso di lavoro"],"Spam protection included":["Protezione antispam inclusa"],"Multistep Forms":["Moduli a pi\u00f9 fasi"],"Send form entries instantly to any external system or endpoint to power advanced workflows.":["Invia le voci del modulo istantaneamente a qualsiasi sistema o endpoint esterno per alimentare flussi di lavoro avanzati."],"Automatically turn form entries into clean, ready-to-download PDFs. Perfect for records, sharing, archiving, or keeping things organized.":["Trasforma automaticamente le voci del modulo in PDF puliti e pronti per il download. Perfetto per registri, condivisione, archiviazione o per mantenere tutto organizzato."],"Set up confirmation messages and email notifications for each entry":["Imposta i messaggi di conferma e le notifiche email per ogni voce"],"Something went wrong. Please try again.":["Qualcosa \u00e8 andato storto. Per favore riprova."],"Payment Forms":["Moduli di pagamento"],"Collect payments directly through your forms. Accept one-time and recurring payments seamlessly.":["Raccogli pagamenti direttamente attraverso i tuoi moduli. Accetta pagamenti una tantum e ricorrenti senza problemi."],"SureForms %s":["SureForms %s"],"First name is required.":["Il nome \u00e8 obbligatorio."],"Please enter a valid email address.":["Per favore inserisci un indirizzo email valido."],"Email address is required.":["L'indirizzo email \u00e8 obbligatorio."],"This is required.":["Questo \u00e8 necessario."],"Okay, just one last step\u2026":["Va bene, solo un ultimo passo\u2026"],"Help us tailor your SureForms experience by sharing a bit about yourself.":["Aiutaci a personalizzare la tua esperienza con SureForms condividendo alcune informazioni su di te."],"First Name":["Nome"],"Enter your first name":["Inserisci il tuo nome"],"Last Name":["Cognome"],"Enter your last name":["Inserisci il tuo cognome"],"Email Address":["Indirizzo email"],"Enter your email address":["Inserisci il tuo indirizzo email"],"Privacy Policy":["Informativa sulla privacy"],"Finish":["Finire"],"Stay in the loop and help shape SureForms! Get feature updates, and help us in betterment of SureForms by sharing how you use the plugin. Privacy Policy<\/a>.":["Rimani aggiornato e contribuisci a plasmare SureForms! Ricevi aggiornamenti sulle funzionalit\u00e0 e aiutaci a migliorare SureForms condividendo come utilizzi il plugin. Informativa sulla privacy<\/a>."],"%d form ready":["%d modulo pronto"],"%d already imported":["%d gi\u00e0 importati"],"(unnamed source)":["(fonte senza nome)"],"All forms already imported":["Tutti i moduli gi\u00e0 importati"],"Import forms":["Importa moduli"],"Import %d form":["Importa %d modulo"],"Something went wrong while importing. You can retry, skip, or open Settings \u2192 Migration.":["Si \u00e8 verificato un errore durante l'importazione. Puoi riprovare, saltare o aprire Impostazioni \u2192 Migrazione."],"No other form plugins detected. You can import any time from Settings \u2192 Migration.":["Nessun altro plugin di moduli rilevato. Puoi importare in qualsiasi momento da Impostazioni \u2192 Migrazione."],"Forms imported":["Moduli importati"],"%1$d form from %2$s is now in SureForms, ready to publish, style, and connect.":["Il modulo %1$d da %2$s \u00e8 ora in SureForms, pronto per essere pubblicato, stilizzato e connesso."],"%d form imported":["%d modulo importato"],"%d form could not be imported":["%d modulo non pu\u00f2 essere importato"],"%d field type was unsupported. You can rebuild it manually inside SureForms.":["%d tipo di campo non era supportato. Puoi ricostruirlo manualmente all'interno di SureForms."],"Bring your existing forms with you":["Porta con te i tuoi moduli esistenti"],"We detected forms in another plugin. Pick one to import into SureForms.":["Abbiamo rilevato moduli in un altro plugin. Scegli uno da importare in SureForms."],"Choose a form plugin to import from":["Scegli un plugin di modulo da cui importare"],"Importing more than one plugin? You can import additional sources later from Settings \u2192 Migration.":["Importare pi\u00f9 di un plugin? Puoi importare ulteriori fonti successivamente da Impostazioni \u2192 Migrazione."],"Import did not complete":["L'importazione non \u00e8 stata completata"],"I'll do this later":["Lo far\u00f2 pi\u00f9 tardi"],"Retry":["Riprova"],"Form Checks":["Controlli del modulo"],"Collapse form checks":["Comprimi i controlli del modulo"],"Expand form checks":["Espandi i controlli del modulo"],"Ignore":["Ignora"],"Working\u2026":["Lavorando\u2026"],"Details":["Dettagli"],"Copy details":["Copia dettagli"]}}} \ No newline at end of file diff --git a/languages/sureforms-it_IT.mo b/languages/sureforms-it_IT.mo index e7040428e0a9c1db05cff3a33719a734536c5018..2068e8c36e8f7d0f6c264b2430a034a37bb914df 100644 GIT binary patch delta 76500 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