and keeps "Form Builder"
* highlighted in the sidebar (the hook suffix still resolves under that
* parent), without adding a visible, redundant submenu entry.
*/
class Form_Builder_Builder_Page {
const PAGE_SLUG = 'adminoptim-form-builder';
const PARENT_SLUG = 'edit.php?post_type=' . Form_Builder::ADMIN_POST_TYPE;
/**
* @var Form_Builder_Forms
*/
private $forms;
/**
* The hook suffix add_submenu_page() returns for this page, needed to
* scope admin_enqueue_scripts() even though the menu entry itself is
* removed right after registration.
*
* @var string
*/
private $page_hook = '';
/**
* Constructor.
*
* @param Form_Builder_Forms $forms Forms service instance.
*/
public function __construct( Form_Builder_Forms $forms ) {
$this->forms = $forms;
add_action( 'admin_menu', [ $this, 'register_page' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'load-post.php', [ $this, 'maybe_redirect_edit_screen' ] );
add_action( 'load-post-new.php', [ $this, 'maybe_redirect_new_screen' ] );
add_filter( 'get_edit_post_link', [ $this, 'filter_edit_post_link' ], 10, 2 );
}
/**
* Register the hidden builder page under the CPT's parent slug.
*
* @return void
*/
public function register_page() {
$this->page_hook = (string) add_submenu_page(
self::PARENT_SLUG,
__( 'Edit Form', 'admin-optimizer' ),
__( 'Edit Form', 'admin-optimizer' ),
Form_Builder::management_capability(),
self::PAGE_SLUG,
[ $this, 'render_page' ]
);
remove_submenu_page( self::PARENT_SLUG, self::PAGE_SLUG );
/*
* Because the submenu entry is removed immediately above,
* get_admin_page_title() can no longer find it in $submenu, so
* wp-admin's own admin-header.php ends up with a null $title
* (triggering a strip_tags(null) deprecation notice on PHP 8.1+).
* Set the global explicitly, early enough (load-$hook fires before
* admin-header.php is required) to pre-empt that lookup.
*/
add_action( 'load-' . $this->page_hook, [ $this, 'set_admin_title' ] );
/*
* The "Add New" flow (no form_id) creates a draft and redirects to
* attach it. That redirect must happen here, on load-$hook — which
* fires before admin-header.php requires/prints anything — not from
* inside render_page(), which WordPress only calls after
* admin-header.php has already sent headers and output the page's
*
(styles included). A wp_safe_redirect() from render_page()
* would reliably warn "headers already sent" and leave the page
* blank, since enqueued admin styles always print before any
* submenu page callback runs.
*/
add_action( 'load-' . $this->page_hook, [ $this, 'maybe_create_draft_and_redirect' ] );
}
/**
* Create a new draft and redirect to it when the builder page is visited
* with no form_id (the "Add New" flow) — see the comment in
* register_page() for why this must run on load-$hook.
*
* @return void
*/
public function maybe_create_draft_and_redirect() {
if ( ! Form_Builder::can_manage_forms() ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing, no state change.
$form_id = isset( $_GET['form_id'] ) ? absint( wp_unslash( $_GET['form_id'] ) ) : 0;
if ( $form_id ) {
return;
}
$this->create_draft_and_redirect();
}
/**
* Explicitly set the admin page title global — see the comment in
* register_page() for why this is necessary for a hidden submenu page.
*
* @return void
*/
public function set_admin_title() {
$GLOBALS['title'] = __( 'Edit Form', 'admin-optimizer' );
}
/**
* Redirect the CPT's native post.php edit screen to the builder page.
*
* @return void
*/
public function maybe_redirect_edit_screen() {
global $typenow;
if ( Form_Builder::ADMIN_POST_TYPE !== $typenow || ! Form_Builder::can_manage_forms() ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing, no state change.
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
if ( '' !== $action && 'edit' !== $action ) {
// Anything other than a plain edit view — delete, trash, untrash,
// restore from autosave, etc. — must reach post.php's own
// handling untouched. Row actions like "Delete Permanently" link
// here with action=delete, and hijacking that to the builder page
// silently turns every such action into a no-op edit-screen visit.
return;
}
$form_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect target, no state change.
if ( ! $form_id ) {
return;
}
wp_safe_redirect( $this->builder_url( $form_id ) );
exit;
}
/**
* Redirect the CPT's native post-new.php screen to the builder page,
* which creates the new draft itself (see maybe_create_draft()).
*
* @return void
*/
public function maybe_redirect_new_screen() {
global $typenow;
if ( Form_Builder::ADMIN_POST_TYPE !== $typenow || ! Form_Builder::can_manage_forms() ) {
return;
}
wp_safe_redirect( $this->builder_url() );
exit;
}
/**
* Point every get_edit_post_link() call for a form at the builder page
* instead of post.php — covers the CPT list table's own title/edit link,
* the entries list table and single-entry view's form links, and the AI
* generator's `editUrl`, with no changes needed in any of those files.
*
* @param string $link Default edit link.
* @param int $post_id Post ID.
*
* @return string
*/
public function filter_edit_post_link( $link, $post_id ) {
$post = get_post( $post_id );
if ( ! $post || Form_Builder::ADMIN_POST_TYPE !== $post->post_type ) {
return $link;
}
return $this->builder_url( (int) $post->ID );
}
/**
* Build the builder page's URL, with or without a form ID.
*
* @param int $form_id Form post ID, or `0` for the "create a new form" entry point.
*
* @return string
*/
private function builder_url( $form_id = 0 ) {
$url = admin_url( 'admin.php?page=' . self::PAGE_SLUG );
return $form_id ? add_query_arg( [ 'form_id' => absint( $form_id ) ], $url ) : $url;
}
/**
* Render the builder page: create a draft and self-redirect when no
* form_id is present (the "Add New" entry point), otherwise render the
* page shell the JS app mounts into.
*
* @return void
*/
public function render_page() {
if ( ! Form_Builder::can_manage_forms() ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'admin-optimizer' ) );
}
$form_id = isset( $_GET['form_id'] ) ? absint( wp_unslash( $_GET['form_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing, no state change.
if ( ! $form_id ) {
$this->create_draft_and_redirect();
return;
}
$form = get_post( $form_id );
if ( ! $form || ! Form_Builder::can_edit_form( $form->ID ) ) {
wp_die( esc_html__( 'This form could not be found.', 'admin-optimizer' ) );
}
$is_pro_active = Module_Registry::is_pro_active();
?>
ID ) ) : ?>
ID, Form_Builder_Forms::get_form_settings( $form->ID ) ); ?>
Form_Builder::ADMIN_POST_TYPE,
'post_title' => __( 'Untitled form', 'admin-optimizer' ),
'post_status' => 'draft',
'post_author' => get_current_user_id(),
],
true
);
if ( is_wp_error( $post_id ) || ! $post_id ) {
wp_die( esc_html__( 'The form could not be created.', 'admin-optimizer' ) );
}
wp_safe_redirect( $this->builder_url( (int) $post_id ) );
exit;
}
/**
* Enqueue the builder app's assets and bootstrap it with JSON, only on
* this page (matched by hook suffix, since the menu entry itself is
* removed and so is never the current-screen-by-slug in the usual way).
*
* @param string $hook_suffix Current admin page hook suffix.
*
* @return void
*/
public function enqueue_assets( $hook_suffix ) {
if ( $hook_suffix !== $this->page_hook || ! Form_Builder::can_manage_forms() ) {
return;
}
$form_id = isset( $_GET['form_id'] ) ? absint( wp_unslash( $_GET['form_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only bootstrap data, no state change.
if ( ! $form_id ) {
return;
}
$form = get_post( $form_id );
if ( ! $form || ! Form_Builder::can_edit_form( $form->ID ) ) {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_style( 'dashicons' );
wp_enqueue_style(
'adminoptimizer-form-builder-settings',
Form_Builder::MODULE_URL . 'assets/css/form-builder-settings.min.css',
[ 'wp-color-picker', 'dashicons' ],
ADMINOPTIMIZER_VERSION
);
wp_enqueue_script(
'adminoptimizer-form-builder-settings',
Form_Builder::MODULE_URL . 'assets/js/form-builder-settings.min.js',
[ 'wp-color-picker' ],
ADMINOPTIMIZER_VERSION,
true
);
wp_localize_script(
'adminoptimizer-form-builder-settings',
'aofbBuilder',
$this->bootstrap_data( $form )
);
/**
* Fires at the end of the builder page's asset enqueue. Pro attaches
* preset data/assets and inline-appends its JS onto this script
* handle here.
*/
do_action( 'adminoptim_form_builder_enqueue_settings_scripts' );
}
/**
* Build the JSON payload the builder app boots from.
*
* @param \WP_Post $form Form post.
*
* @return array
*/
private function bootstrap_data( \WP_Post $form ) {
$is_pro_active = Module_Registry::is_pro_active();
return [
'form' => [
'id' => (int) $form->ID,
'title' => (string) $form->post_title,
'status' => (string) $form->post_status,
'fields' => Form_Builder_Forms::get_form_fields( $form->ID ),
'settings' => Form_Builder_Forms::get_form_settings( $form->ID ),
],
'fieldTypes' => $this->forms->get_supported_field_types(),
'fieldCapabilities' => $this->forms->get_supported_field_capabilities(),
'defaultSettings' => Form_Builder_Forms::get_default_form_settings(),
'hasSmtpProfiles' => $is_pro_active && $this->forms->has_smtp_profile_choices(),
'smtpProfiles' => $is_pro_active ? $this->forms->get_smtp_profile_choices() : [],
'fileTypeOptions' => $is_pro_active ? $this->file_type_options() : [],
'emailFields' => $this->forms->get_email_field_choices( Form_Builder_Forms::get_form_fields( $form->ID ) ),
'isPro' => $is_pro_active,
'shortcode' => sprintf( '[ao_form id="%d"]', (int) $form->ID ),
'formsListUrl' => admin_url( 'edit.php?post_type=' . Form_Builder::ADMIN_POST_TYPE ),
'formBuilderSettingsUrl' => current_user_can( 'manage_options' ) ? admin_url( 'edit.php?post_type=' . Form_Builder::ADMIN_POST_TYPE . '&page=adminoptim-form-settings' ) : '',
'entriesUrl' => add_query_arg(
[
'post_type' => Form_Builder::ADMIN_POST_TYPE,
'page' => 'adminoptim-form-entries',
'form_id' => (int) $form->ID,
],
admin_url( 'edit.php' )
),
'upgradeUrl' => esc_url_raw( ao_get_upgrade_url( 'form-builder-template-lock' ) ),
'restUrl' => esc_url_raw( rest_url( Form_Builder_Rest::REST_NAMESPACE . '/form-builder/' . (int) $form->ID ) ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'untitledLabel' => __( 'Untitled field', 'admin-optimizer' ),
'i18n' => $this->i18n_strings(),
'templates' => Form_Builder_Templates::get_registered_templates(),
'lockedTemplates' => $is_pro_active ? [] : Form_Builder_Templates::get_locked_template_teasers(),
'templateCategories' => Form_Builder_Templates::get_categories(),
'inertSettings' => Form_Builder_Forms::get_inert_settings_labels( $form->ID ),
'integrationsInert' => Form_Builder_Forms::has_inert_integrations( $form->ID ),
'customCssInert' => Form_Builder_Forms::has_inert_custom_css( $form->ID ),
];
}
/**
* The upload types this site permits, grouped for the File field's picker.
* Each item lists the extensions WordPress treats as one type (jpg, jpeg, jpe).
*
* @return array>}>
*/
private function file_type_options() {
$labels = [
'image' => __( 'Images', 'admin-optimizer' ),
'document' => __( 'Documents', 'admin-optimizer' ),
'spreadsheet' => __( 'Spreadsheets', 'admin-optimizer' ),
'interactive' => __( 'Presentations', 'admin-optimizer' ),
'audio' => __( 'Audio', 'admin-optimizer' ),
'video' => __( 'Video', 'admin-optimizer' ),
'text' => __( 'Text', 'admin-optimizer' ),
'archive' => __( 'Archives', 'admin-optimizer' ),
'other' => __( 'Other', 'admin-optimizer' ),
];
$items = array_fill_keys( array_keys( $labels ), [] );
foreach ( get_allowed_mime_types() as $extensions => $mime_type ) {
$extensions = explode( '|', strtolower( (string) $extensions ) );
// Admins hold unfiltered_html and see these, but WordPress refuses
// them for the visitors who actually submit the form.
if ( array_intersect( $extensions, [ 'htm', 'html', 'js' ] ) ) {
continue;
}
$items[ $this->file_type_group( $extensions, (string) $mime_type, $labels ) ][] = $extensions;
}
$groups = [];
foreach ( $items as $group => $group_items ) {
if ( ! $group_items ) {
continue;
}
usort(
$group_items,
static function ( $a, $b ) {
return strcmp( $a[0], $b[0] );
}
);
$groups[] = [
'id' => $group,
'label' => $labels[ $group ],
'items' => $group_items,
];
}
return $groups;
}
/**
* Pick the picker group for one WordPress file type.
*
* wp_ext2type() knows xls but not xla, and none of the Office templates, so
* every extension is tried before falling back to the MIME type.
*
* @param array $extensions Extensions of this file type.
* @param string $mime_type MIME type of this file type.
* @param array $groups Known groups, keyed by group.
*
* @return string
*/
private function file_type_group( array $extensions, $mime_type, array $groups ) {
foreach ( $extensions as $extension ) {
$group = (string) wp_ext2type( $extension );
if ( isset( $groups[ $group ] ) ) {
return $group;
}
}
$office_groups = [
'wordprocessingml' => 'document',
'spreadsheetml' => 'spreadsheet',
'ms-excel' => 'spreadsheet',
'presentationml' => 'interactive',
'ms-powerpoint' => 'interactive',
];
foreach ( $office_groups as $needle => $group ) {
if ( false !== strpos( $mime_type, $needle ) ) {
return $group;
}
}
$group = strtok( $mime_type, '/' );
return isset( $groups[ $group ] ) ? $group : 'other';
}
/**
* UI strings the JS app needs, kept in one place so every string is
* translatable through this module's text domain.
*
* @return array
*/
private function i18n_strings() {
return [
'formsLabel' => __( 'Forms', 'admin-optimizer' ),
'publishedLabel' => __( 'Published', 'admin-optimizer' ),
'draftLabel' => __( 'Draft', 'admin-optimizer' ),
'unsavedChanges' => __( 'Unsaved changes', 'admin-optimizer' ),
'previewLabel' => __( 'Preview', 'admin-optimizer' ),
'saveLabel' => __( 'Save', 'admin-optimizer' ),
'savedLabel' => __( 'Saved', 'admin-optimizer' ),
'savingLabel' => __( 'Saving…', 'admin-optimizer' ),
'updateLabel' => __( 'Update', 'admin-optimizer' ),
'saveDraftLabel' => __( 'Save draft', 'admin-optimizer' ),
'copyLabel' => __( 'Copy', 'admin-optimizer' ),
'viewEntries' => __( 'View entries', 'admin-optimizer' ),
'switchToDraft' => __( 'Switch to draft', 'admin-optimizer' ),
'allForms' => __( 'All forms', 'admin-optimizer' ),
'formUpdated' => __( 'Form updated.', 'admin-optimizer' ),
'switchedToDraft' => __( 'Switched to draft.', 'admin-optimizer' ),
'untitledFormTitle' => __( 'Untitled form', 'admin-optimizer' ),
'templatesTitle' => __( 'Templates', 'admin-optimizer' ),
'templatesDesc' => __( 'Start from a ready-made form, then change anything in the Builder.', 'admin-optimizer' ),
'searchTemplates' => __( 'Search templates', 'admin-optimizer' ),
'allCategory' => __( 'All', 'admin-optimizer' ),
'blankFormTitle' => __( 'Blank form', 'admin-optimizer' ),
'blankFormDesc' => __( 'Start from scratch', 'admin-optimizer' ),
'generateAiTitle' => __( 'Generate with AI', 'admin-optimizer' ),
'generateAiDesc' => __( 'Describe your form and we build it', 'admin-optimizer' ),
'noTemplatesFound' => __( 'No templates match that search.', 'admin-optimizer' ),
/* translators: %d: number of fields in a template. */
'fieldsCountLabel' => __( '%d fields', 'admin-optimizer' ),
/* translators: %s: template name. */
'useTemplateReplace' => __( 'Use the "%s" template?', 'admin-optimizer' ),
'useTemplateBlank' => __( 'Start a blank form?', 'admin-optimizer' ),
/* translators: %d: number of fields already in the form. */
'templateHasFields' => __( 'Your form already has %d field(s). You can keep them and add the template\'s fields, or replace them. You can undo this right after.', 'admin-optimizer' ),
'templateBlankWarning' => __( 'Starting blank removes your current fields. You can undo this right after.', 'admin-optimizer' ),
'addToMyFields' => __( 'Add to my fields', 'admin-optimizer' ),
'replaceMyFields' => __( 'Replace my fields', 'admin-optimizer' ),
'removeMyFields' => __( 'Remove my fields', 'admin-optimizer' ),
/* translators: %s: template name. */
'templateApplied' => __( '"%s" applied.', 'admin-optimizer' ),
/* translators: %s: template name. */
'templateAdded' => __( '"%s" added to your form.', 'admin-optimizer' ),
'startedBlank' => __( 'Started a blank form.', 'admin-optimizer' ),
'undone' => __( 'Undone.', 'admin-optimizer' ),
'browseTemplates' => __( 'Browse templates', 'admin-optimizer' ),
'aiNotPro' => __( 'Generate-with-AI is part of Admin Optimizer Pro.', 'admin-optimizer' ),
'copyShortcode' => __( 'Copy shortcode', 'admin-optimizer' ),
'shortcodeCopied' => __( 'Shortcode copied.', 'admin-optimizer' ),
'titlePlaceholder' => __( 'Enter form title', 'admin-optimizer' ),
'presetsLabel' => __( 'Presets', 'admin-optimizer' ),
'presetsHint' => __( 'Start from a ready-made form', 'admin-optimizer' ),
'proTag' => __( 'Pro', 'admin-optimizer' ),
'upgradeCta' => __( 'See what Pro adds', 'admin-optimizer' ),
'upgradeToProPrefix' => __( 'Upgrade to the', 'admin-optimizer' ),
'upgradeToProLinkText' => __( 'Pro version', 'admin-optimizer' ),
'upgradeToProSuffix' => __( 'to access these features', 'admin-optimizer' ),
'tabBuilder' => __( 'Builder', 'admin-optimizer' ),
'tabSettings' => __( 'Settings', 'admin-optimizer' ),
'tabStyle' => __( 'Style', 'admin-optimizer' ),
'tabEmails' => __( 'Emails', 'admin-optimizer' ),
'tabAuto' => __( 'Autoresponder', 'admin-optimizer' ),
'fieldsHeading' => __( 'Fields', 'admin-optimizer' ),
'searchFields' => __( 'Search fields', 'admin-optimizer' ),
'canvasHeading' => __( 'Form Editor', 'admin-optimizer' ),
'canvasSubtitle' => __( 'Create the form that visitors will see on the frontend.', 'admin-optimizer' ),
'emptyCanvasTitle' => __( 'Your form has no fields yet.', 'admin-optimizer' ),
'emptyCanvasBody' => __( 'Pick a template, or add your first field.', 'admin-optimizer' ),
'addTextField' => __( 'Add a text field', 'admin-optimizer' ),
'addField' => __( 'Add field', 'admin-optimizer' ),
'moveUp' => __( 'Move up', 'admin-optimizer' ),
'moveDown' => __( 'Move down', 'admin-optimizer' ),
'removeField' => __( 'Remove field', 'admin-optimizer' ),
'removeThisField' => __( 'Remove this field', 'admin-optimizer' ),
'fieldRemoved' => __( 'Field removed.', 'admin-optimizer' ),
'undo' => __( 'Undo', 'admin-optimizer' ),
'requiredLabel' => __( 'Required', 'admin-optimizer' ),
'pinnedHeading' => __( 'Pinned to the end', 'admin-optimizer' ),
'pinnedHint' => __( 'reCAPTCHA and Turnstile always run last. Add one from the Anti-spam group.', 'admin-optimizer' ),
'buttonLabelField' => __( 'Button label', 'admin-optimizer' ),
'widthField' => __( 'Width', 'admin-optimizer' ),
'widthFull' => __( 'Fill available space', 'admin-optimizer' ),
'widthThreeQuarter' => __( 'Three quarters', 'admin-optimizer' ),
'widthHalf' => __( 'Half', 'admin-optimizer' ),
'widthQuarter' => __( 'Quarter', 'admin-optimizer' ),
'noSubmitWarning' => __( 'This form has no submit button.', 'admin-optimizer' ),
'submitAlreadyAdded' => __( 'This form already has a submit button.', 'admin-optimizer' ),
'rowInRowError' => __( 'A Row cannot contain another Row.', 'admin-optimizer' ),
'conditionalLogic' => __( 'Conditional logic', 'admin-optimizer' ),
'showWhen' => __( 'Only show this field when', 'admin-optimizer' ),
'matchAll' => __( 'ALL of the following rules match', 'admin-optimizer' ),
'matchAny' => __( 'ANY of the following rules match', 'admin-optimizer' ),
'addRule' => __( 'Add rule', 'admin-optimizer' ),
'conditionHint' => __( 'A rule can only use a field that has no conditional logic of its own.', 'admin-optimizer' ),
'inactiveConditions' => __( 'These rules are saved but not applied on your site without Pro.', 'admin-optimizer' ),
'saveError' => __( 'Fix the problems above to save.', 'admin-optimizer' ),
'leaveTitle' => __( 'Leave without saving?', 'admin-optimizer' ),
'leaveBody' => __( 'You have unsaved changes to this form.', 'admin-optimizer' ),
'keepEditing' => __( 'Keep editing', 'admin-optimizer' ),
'leaveAnyway' => __( 'Leave without saving', 'admin-optimizer' ),
'upgradeTitle' => __( 'Pro feature', 'admin-optimizer' ),
'upgradeBody' => __( 'Presets, star ratings, conditional logic, redirects, submission limits, and access rules are part of Admin Optimizer Pro.', 'admin-optimizer' ),
'notNow' => __( 'Not now', 'admin-optimizer' ),
'seeProPlans' => __( 'See Pro plans', 'admin-optimizer' ),
'confirmReplaceTitle' => __( 'Replace your current fields?', 'admin-optimizer' ),
'confirmReplaceButton' => __( 'Replace fields', 'admin-optimizer' ),
'cancel' => __( 'Cancel', 'admin-optimizer' ),
'close' => __( 'Close', 'admin-optimizer' ),
'savedAndPublished' => __( 'Form saved and published.', 'admin-optimizer' ),
'draftSaved' => __( 'Draft saved.', 'admin-optimizer' ),
'previewUnsavedNote' => __( 'This preview includes your unsaved changes.', 'admin-optimizer' ),
'previewSavedNote' => __( 'This is your saved form.', 'admin-optimizer' ),
'inertNotice' => __( 'Saved but not enforced. Your form is open to everyone until Pro is active.', 'admin-optimizer' ),
'accessSectionLabel' => __( 'Access and limits', 'admin-optimizer' ),
'settingsSectionTitle' => __( 'Submission Settings', 'admin-optimizer' ),
'settingsSectionDesc' => __( 'Choose what happens when someone submits this form.', 'admin-optimizer' ),
'styleSectionTitle' => __( 'Form Style', 'admin-optimizer' ),
'styleSectionDesc' => __( 'Colors and styling for this form only.', 'admin-optimizer' ),
'emailsSectionTitle' => __( 'Notification Email', 'admin-optimizer' ),
'emailsSectionDesc' => __( 'Sent to you each time this form is submitted.', 'admin-optimizer' ),
'autoSectionDesc' => __( 'Send an automatic thank-you email right after someone submits.', 'admin-optimizer' ),
'onLabel' => __( 'On', 'admin-optimizer' ),
'offLabel' => __( 'Off', 'admin-optimizer' ),
'deliveryLabel' => __( 'What should happen to submissions?', 'admin-optimizer' ),
'deliveryBoth' => __( 'Save entries and email me', 'admin-optimizer' ),
'deliveryEmail' => __( 'Email me only', 'admin-optimizer' ),
'deliveryStore' => __( 'Save entries only', 'admin-optimizer' ),
'successMessageLabel' => __( 'Success message', 'admin-optimizer' ),
'successBehaviorLabel' => __( 'Show success message', 'admin-optimizer' ),
'successBehaviorMessage' => __( 'After the last form field', 'admin-optimizer' ),
'successBehaviorReplace' => __( 'Replace form with success message', 'admin-optimizer' ),
'redirectLabel' => __( 'Redirect after submit', 'admin-optimizer' ),
'whoCanSubmit' => __( 'Who can submit', 'admin-optimizer' ),
'everyone' => __( 'Everyone', 'admin-optimizer' ),
'loggedInUsers' => __( 'Logged-in users', 'admin-optimizer' ),
'specificRoles' => __( 'Specific roles', 'admin-optimizer' ),
'maxEntries' => __( 'Maximum entries', 'admin-optimizer' ),
'closeOn' => __( 'Close form on', 'admin-optimizer' ),
'colorsHeading' => __( 'Style and Color', 'admin-optimizer' ),
'clearAll' => __( 'Clear all', 'admin-optimizer' ),
'primaryColor' => __( 'Primary color', 'admin-optimizer' ),
'textColor' => __( 'Text color', 'admin-optimizer' ),
'inputBgColor' => __( 'Input background', 'admin-optimizer' ),
'borderColor' => __( 'Input border', 'admin-optimizer' ),
'borderRadius' => __( 'Border radius (px)', 'admin-optimizer' ),
'previewHeading' => __( 'How it looks', 'admin-optimizer' ),
'notifyHeading' => __( 'Notification to you', 'admin-optimizer' ),
'mailEnable' => __( 'Email me when someone submits', 'admin-optimizer' ),
'mailStoreOnlyNote' => __( 'Submissions are set to save entries only, so no notification is sent. Change this under Settings.', 'admin-optimizer' ),
'sendTo' => __( 'Send to', 'admin-optimizer' ),
'smtpProfile' => __( 'Send using SMTP profile', 'admin-optimizer' ),
'subjectLabel' => __( 'Subject', 'admin-optimizer' ),
'messageLabel' => __( 'Message', 'admin-optimizer' ),
'insertValue' => __( 'Insert a value from the form:', 'admin-optimizer' ),
'autoHeading' => __( 'Confirmation to the submitter', 'admin-optimizer' ),
'autoEnable' => __( 'Send a confirmation email to the submitter', 'admin-optimizer' ),
'autoOffHint' => __( 'Turn this on to configure the confirmation email.', 'admin-optimizer' ),
'sendConfirmationTo' => __( 'Send confirmation to', 'admin-optimizer' ),
'firstEmailField' => __( 'First Email field submitted', 'admin-optimizer' ),
'sendAsHtml' => __( 'Send this message as HTML email', 'admin-optimizer' ),
'basicField' => __( 'Field', 'admin-optimizer' ),
'choiceField' => __( 'Choice', 'admin-optimizer' ),
'advancedField' => __( 'Advanced', 'admin-optimizer' ),
'layoutField' => __( 'Layout', 'admin-optimizer' ),
'layoutKind' => __( 'Layout field', 'admin-optimizer' ),
'choiceKind' => __( 'Choice field', 'admin-optimizer' ),
'displayKind' => __( 'Display field', 'admin-optimizer' ),
'antispamKind' => __( 'Anti-spam field', 'admin-optimizer' ),
'inputKind' => __( 'Input field', 'admin-optimizer' ),
'antispamField' => __( 'Anti-spam', 'admin-optimizer' ),
'labelField' => __( 'Label', 'admin-optimizer' ),
'nameField' => __( 'Name', 'admin-optimizer' ),
'placeholderField' => __( 'Placeholder', 'admin-optimizer' ),
'defaultValueField' => __( 'Default value', 'admin-optimizer' ),
'helpTextField' => __( 'Help text', 'admin-optimizer' ),
'requiredField' => __( 'Required field', 'admin-optimizer' ),
'hideLabelField' => __( 'Hide label (kept for screen readers)', 'admin-optimizer' ),
'allowPrepopulationField' => __( 'Allow value override', 'admin-optimizer' ),
'allowPrepopulationHint' => __( 'Allow this field’s initial value to be overridden through shortcode attributes or Gutenberg Form Block settings.', 'admin-optimizer' ),
'choicesField' => __( 'Choices', 'admin-optimizer' ),
'choicesHint' => __( 'One per line. Use value|Label to set them separately.', 'admin-optimizer' ),
'columnsField' => __( 'Columns', 'admin-optimizer' ),
'fileTypesField' => __( 'Allowed file types', 'admin-optimizer' ),
'fileTypesPlaceholder' => __( 'Type to search, e.g. pdf', 'admin-optimizer' ),
'fileTypesHint' => __( 'Leave empty to allow every file type this site permits.', 'admin-optimizer' ),
'fileTypesNoMatch' => __( 'No matching file types. Only types this site permits are listed.', 'admin-optimizer' ),
'fileTypesSelectAll' => __( 'Select all', 'admin-optimizer' ),
'fileTypesClearAll' => __( 'Clear all', 'admin-optimizer' ),
/* translators: %s: file type, e.g. pdf. */
'fileTypesRemove' => _x( 'Remove %s', 'file type chip', 'admin-optimizer' ),
'fileTypesUnlisted' => __( 'Saved earlier, but not in the list of types this site permits.', 'admin-optimizer' ),
'fileSizeField' => __( 'Max size (MB)', 'admin-optimizer' ),
'imageMinWidth' => __( 'Min width (px)', 'admin-optimizer' ),
'imageMaxWidth' => __( 'Max width (px)', 'admin-optimizer' ),
'imageMinHeight' => __( 'Min height (px)', 'admin-optimizer' ),
'imageMaxHeight' => __( 'Max height (px)', 'admin-optimizer' ),
'imageDimensionsHint' => __( 'For an exact size, enter the same minimum and maximum.', 'admin-optimizer' ),
'fileUploadNote' => __( 'Files are saved to the Uploads folder by default.', 'admin-optimizer' ),
'noFieldsToPreview' => __( 'Add fields to see them here.', 'admin-optimizer' ),
'selectPlaceholder' => __( 'Select…', 'admin-optimizer' ),
];
}
}