Skip to content Skip to sidebar

This is the technical support forum for WPML - the multilingual WordPress plugin.

Everyone can read, but only WPML clients can post here. WPML team is replying on the forum 6 days per week, 22 hours per day.

This topic contains 10 replies, has 1 voice.

Last updated by andreasH-126 17 hours, 20 minutes ago.

Assisted by: Andreas W..

Author Posts
September 19, 2025 at 2:29 pm #17417096

andreasH-126

Background of the issue:
I am trying to submit a WPForms form that includes repeater fields (Repeater Addon enabled). The form works when one repeater row is used, but when two or more are submitted, the form fails silently, and I get a 500 Internal Server Error via admin-ajax.php. Link to a page where the issue can be seen: hidden link. I fixed the issue by updating Notifications.php to: - Use clone-aware key resolution for field lookups (43_2 falls back to 43) - Add null checks to avoid passing null into FP functional helpers - Improve restoreFieldLabelsToDefaultLanguage(), getChoiceMap(), and restoreConditionalLabels() to support repeater fields safely.

Symptoms:
A 500 error caused by WPML’s WPForms email integration crashing when handling repeater clone field keys, such as "43_2". These keys are not present in the $formPost['fields'] or $formData['fields'] arrays, so lookups like $fields['43_2'] fail. PHP Fatal error: Uncaught InvalidArgumentException: target should be an object with map method or an array in /wpml/fp/core/Fns.php:143. This breaks email notifications completely when more than one repeater row is present.

Questions:
Can WPML officially patch the WPMLFormsHooksWpFormsNotifications class to handle repeater clone keys?
Is there a hook to intercept and normalize $fields before translation logic is applied?
Let me know if you want the complete code for the fix.

September 19, 2025 at 2:32 pm #17417189

andreasH-126

Temporary login link hidden link

September 19, 2025 at 2:33 pm #17417193

andreasH-126

Bug free WPForms Multilingual code for Notifications.php:

<?php

namespace WPML\Forms\Hooks\WpForms;

use WPForms_Conditional_Logic_Fields;
use WPML\Forms\Helpers\WpForms\Field;
use WPML\Forms\Hooks\Base;
use WPML\Forms\WpForms\SmartTag;
use WPML\FP\Fns;
use WPML\FP\Obj;
use WPML\FP\Lst;
use function WPML\FP\pipe;

class Notifications extends Base {

const EMAIL_HTML_CONTEXT = 'email-html';

/** Adds hooks. */
public function addHooks() {
add_filter( 'wpforms_process_before_form_data', [ $this, 'applyConfirmationTranslations' ] );
add_filter( 'wpforms_emails_send_email_data', [ $this, 'applyEmailTranslations' ], 10, 2 );
add_action( 'wpforms_email_send_after', [ $this, 'restoreLanguage' ] );
add_filter( 'wpml_user_language', [ $this, 'getLanguageForEmail' ], 10, 2 );
add_filter( 'wpforms_entry_email_data', [ $this, 'restoreFieldLabelsToDefaultLanguage' ], 10, 3 );

add_filter( 'wpforms_html_field_value', [ $this, 'restoreRawValuesForHtmlEmail' ], 10, 4 );
add_filter( 'wpforms_plaintext_field_value', [ $this, 'restoreRawValuesForNotifications' ], 10, 2 );

// These are only required in the 'Pro' version.
if (
class_exists( WPForms_Conditional_Logic_Fields::class )
&& has_filter( 'wpforms_entry_email_process', [ WPForms_Conditional_Logic_Fields::instance(), 'process_notification_conditionals' ] )
) {
remove_filter( 'wpforms_entry_email_process', [ WPForms_Conditional_Logic_Fields::instance(), 'process_notification_conditionals' ], 10 );
add_filter( 'wpforms_entry_email_process', [ $this, 'processNotificationConditionals' ], 10, 4 );
}
}

/**
* @param string $value
* @param array $field
* @param array $formData
* @param string $context
*
* @return string
*/
public function restoreRawValuesForHtmlEmail( string $value, array $field, array $formData, string $context ) : string {
if ( self::EMAIL_HTML_CONTEXT === $context ) {
return $this->restoreRawValuesForNotifications( $value, $field );
}

return $value;
}

/**
* @param string $value
* @param array $field
*
* @return string
*/
public function restoreRawValuesForNotifications( string $value, array $field ) : string {
if (
in_array( $field['type'], [ 'radio', 'select', 'checkbox' ], true )
&& ! Field::isDynamic( $field )
) {
return Obj::propOr( $value, 'value_raw', $field );
}

return $value;
}

/**
* Restores field labels to default language.
*
* Clone-aware and null-safe for repeater keys like "43_2".
*
* @param array $fields The form fields.
* @param array $entry The form entry.
* @param array $formData The form data.
*
* @return array
*/
public function restoreFieldLabelsToDefaultLanguage( $fields, $entry, $formData ) {

$formPost = wpforms()->get( 'form' )->get(
$formData['id'],
[ 'content_only' => true ]
);

$formPostFields = $formPost['fields'] ?? [];
$translatedFields = $formData['fields'] ?? [];
$entryFields = is_array( $entry ) ? ( $entry['fields'] ?? [] ) : [];

foreach ( $fields as $key => &$field ) {

// Resolve structures using a clone-aware helper.
$formPostField = $this->getFieldByCloneAwareKey( $formPostFields, $key ) ?: [];
$translatedField = $this->getFieldByCloneAwareKey( $translatedFields, $key ) ?: [];
$entryField = $this->getFieldByCloneAwareKey( $entryFields, $key );

// Label: prefer original form-post label when available.
if ( is_array( $formPostField ) && isset( $formPostField['label'] ) ) {
$field['name'] = $formPostField['label'];
}

// Value: only translate/format when we have a submitted entry field structure.
if ( null !== $entryField && is_array( $entryField ) ) {
$field['value'] = $this->getFieldValue(
$field,
$entryField,
$formPostField,
$translatedField
);
}
}

return $fields;
}

/**
* Applies translations to email data.
*
* @param array $data Email data.
* @param \WPForms_WP_Emails $emails WPForms email object.
*
* @return mixed
*/
public function applyEmailTranslations( $data, $emails ) {
$package = $this->newPackage( $this->getId( $emails->form_data ) );

$email = is_array( $data['to'] ) ? reset( $data['to'] ) : $data['to'];
do_action( 'wpml_switch_language_for_email', $email );

$dataKeys = [ 'subject', 'message' ];
$formPost = wpforms()->get( 'form' )->get(
$emails->form_data['id'],
[ 'content_only' => true ]
);

if ( $this->notEmpty( 'settings', $formPost ) ) {

$formPost['settings'] = $package->translateFormSettings( $formPost['settings'] );
}

$current_notification = $formPost['settings']['notifications'][ $emails->notification_id ];

$setData = function( $data, $key ) use ( $current_notification ) {
if ( ! empty( $current_notification[ $key ] ) ) {
$data[ $key ] = $current_notification[ $key ];
}

return $data;
};

foreach ( $dataKeys as $key ) {
$data = $setData( $data, $key );
}

$data = SmartTag::process( $data, $formPost, $dataKeys );

$translated = [ 'notifications' => [ $emails->notification_id => $data ] ];
foreach ( $emails->fields as &$field ) {
$field['name'] = $package->translateString( $field['name'], strval( $this->getId( $field ) ), 'label' );
}

return $translated['notifications'][ $emails->notification_id ];
}

/**
* Restores current language.
*
* @codeCoverageIgnore
*/
public function restoreLanguage() {
do_action( 'wpml_restore_language_from_email' );
}

/**
* Applies form confirmations translations.
*
* @param array $formData Form data.
*
* @return array
*/
public function applyConfirmationTranslations( $formData ) {

$package = $this->newPackage( $this->getId( $formData ) );

if (
$this->notEmpty( 'settings', $formData )
&& $this->notEmpty( 'confirmations', $formData['settings'] )
) {
$formData['settings'] = $package->translateFormSettings( $formData['settings'] );

foreach ( $formData['settings']['confirmations'] as &$confirmation ) {

$confirmation = SmartTag::process( $confirmation, $formData );
}
}

return $formData;
}

/**
* Returns language to use for translation based on email.
* Will use the language of the post if non site user.
*
* @param string $language Language detected.
* @param string $email The user email.
*
* @return string
*/
public function getLanguageForEmail( $language, $email ) {
$user = get_user_by( 'email', $email );
if ( isset( $user->ID ) ) {
return $language;
}
return $this->getCurrentFormLanguage();
}

/**
* @return string
*/
private function getCurrentFormLanguage() {
return apply_filters( 'wpml_current_language', '' );
}

/**
* Resolve a field by key, accepting clone keys like "43_2".
* Falls back to base "43" if the suffixed key is not present.
*
* @param array $array
* @param string|int $key
* @return mixed|null
*/
private function getFieldByCloneAwareKey( array $array, $key ) {
if ( array_key_exists( $key, $array ) ) {
return $array[ $key ];
}
if ( is_string( $key ) && preg_match( '/^(\d+)_\d+$/', $key, $m ) ) {
$base = $m[1];
if ( array_key_exists( $base, $array ) ) {
return $array[ $base ];
}
}
return null;
}

/**
* @param array $field Field in default language.
* @param array|string $entryField
* @param array $originalField
* @param array $translatedField
* @return string
*/
private function getFieldValue( $field, $entryField, $originalField, $translatedField ) {
$getField = Obj::path( Fns::__, $field );
$getOriginalField = Obj::path( Fns::__, $originalField );

switch ( $field['type'] ) {
case 'select':
case 'radio':
case 'checkbox':
$choicesMap = $this->getChoiceMap( $originalField, $translatedField );

if ( is_array( $entryField ) && $choicesMap ) {
$value = '';
foreach ( $entryField as $key => $val ) {
$value .= PHP_EOL . Obj::propOr( $entryField[ $key ], $val, $choicesMap );
}
return $value;
}
return Obj::propOr( $field['value'], $field['value'], $choicesMap );

case 'likert_scale':
$value = '';
if ( isset( $field['value_raw'] ) && is_array( $field['value_raw'] ) ) {
foreach ( $field['value_raw'] as $key => $val ) {
$value .= PHP_EOL . $getOriginalField( [ 'rows', $key ] ) . ':' . PHP_EOL
. $getOriginalField( [ 'columns', $val ] );
}
}
return $value;

case 'payment-multiple':
case 'payment-select':
return $getOriginalField( [ 'choices', $field['value_raw'], 'label' ] )
. ' - ' . $getField( [ 'currency' ] )
. ' ' . $getField( [ 'amount' ] );

case 'payment-checkbox':
$value = '';
$choiceIds = explode( ',', (string) $field['value_raw'] );
foreach ( $choiceIds as $key ) {
$value .= PHP_EOL . $getOriginalField( [ 'choices', $key, 'label' ] )
. ' - ' . $getField( [ 'currency' ] )
. ' ' . $getOriginalField( [ 'choices', $key, 'value' ] );
}
return $value;

default:
return $field['value'];
}
}

/**
* @param array $originalField
* @param array $translatedField
*
* @return array
*/
private function getChoiceMap( $originalField, $translatedField ) {
if ( ! is_array( $originalField ) || ! is_array( $translatedField ) ) {
return [];
}

if ( Obj::prop( 'dynamic_choices', $originalField ) ) {
return [];
}

$getChoices = pipe( Obj::path( [ 'choices' ] ), Lst::pluck( 'label' ) );

$originalChoices = $getChoices( $originalField );
$translatedChoices = $getChoices( $translatedField );

if ( ! is_array( $originalChoices ) || ! is_array( $translatedChoices ) ) {
return [];
}

$choicesMap = [];
$max = min( count( $originalChoices ), count( $translatedChoices ) );

for ( $i = 0; $i < $max; $i++ ) {
$choicesMap[ $translatedChoices[ $i ] ] = $originalChoices[ $i ];
}

return $choicesMap;
}

/**
* Restore labels in form data before processing conditional logic for form entry notifications.
*
* Clone-aware for repeater submissions.
*
* @param bool $process Whether to process the logic or not.
* @param array $fields List of submitted fields.
* @param array $form_data Form data and settings.
* @param int $id Notification ID.
*
* @return bool
*/
public function processNotificationConditionals( $process, $fields, $form_data, $id ) {
$conditionalLogicFields = WPForms_Conditional_Logic_Fields::instance();

$form_data = $this->restoreConditionalLabels( $form_data, $fields );

return $conditionalLogicFields->process_notification_conditionals( $process, $fields, $form_data, $id );
}

/**
* @param array $form_data Form data and settings.
* @param array $fields List of submitted fields.
*
* @return array
*/
private function restoreConditionalLabels( $form_data, $fields ) {
if ( empty( $form_data['fields'] ) || ! is_array( $form_data['fields'] ) ) {
return $form_data;
}

foreach ( $form_data['fields'] as $key => &$field ) {
if ( empty( $field['choices'] ) || ! is_array( $field['choices'] ) ) {
continue;
}

// Prefer exact key, else find the first clone "{$key}_N".
$submitted = $fields[ $key ] ?? null;
if ( null === $submitted ) {
foreach ( $fields as $k => $f ) {
if ( is_string( $k ) && preg_match( '/^' . preg_quote( (string) $key, '/' ) . '_\d+$/', $k ) ) {
$submitted = $f;
break;
}
}
}

if ( ! is_array( $submitted ) || ! isset( $submitted['value_raw'], $submitted['value'] ) ) {
continue;
}

foreach ( $field['choices'] as &$choice ) {
if ( isset( $choice['label'] ) && $choice['label'] === $submitted['value_raw'] ) {
$choice['label'] = $submitted['value'];
}
}
}

return $form_data;
}

}

September 19, 2025 at 2:51 pm #17417216

Andreas W.
WPML Supporter since 12/2018

Languages: English (English ) Spanish (Español ) German (Deutsch )

Timezone: America/Lima (GMT-05:00)

Hello,

The numeric values are, by default, not translatable on the Advanced Translation Editor.

You might need to search for these values when translating the form by using the text search field on the top right of the Advanced Translation Editor.

It might even be the case that you need to use the following XML configuration at WPML > Settings > Custom XML Configuration to make the numeric values translatable:
https://wpml.org/documentation/support/language-configuration-files/numeric-value-translation/

After adding this config, you will need to edit the original form, save it, then create a new translation job for the form on the WPML Translation Dashboard, translate the form, and make sure to translate these numeric values.

Please give this a try and let me know if this will not work out.

Best regards
Andreas

September 22, 2025 at 6:16 am #17419927

andreasH-126

Hi,

I can test this, but I have this setup on +30 sites and they have 5-10 forms on each, so doing this for about 250 forms, and teaching about 10 web editors to do it each time they create a new form, that is not feasable. Makeing a fix for the repeater fields in the WPForms Multilingual plugin seems like a more reasonable way forward. I don't want to "hack" the plugin each time there is a new official update either.

September 23, 2025 at 6:58 am #17423580

Andreas W.
WPML Supporter since 12/2018

Languages: English (English ) Spanish (Español ) German (Deutsch )

Timezone: America/Lima (GMT-05:00)

I understand, but could you please confirm first if my suggestion solves the issue?

This is basically not a plugin hack; it is only a limitation on the Advanced Translation Editor, as it is designed to translate text only and will hide any numeric values and non-visual elements by default. Such values are usually copied over in the background and remain identical in all languages. This design is required to avoid automatic translation leading to unexpected results.

September 24, 2025 at 8:53 am #17428080

andreasH-126

OK, but just to be sure: This is not a translated page, this is an original page in the default language (Swedish). So no translated fields are being involved here. But when I deactivate (or "patch") WPForms Multilingual, the error disappears.

Also, the numbers in the repeater fields are created dynamically in WPForms, when a user adds more fields in a form. I don't create (or translate) field 1_1, 1_2, 1_3 etc. There can be an infite number of repeater fields with an infinite number of numbers.

September 24, 2025 at 3:03 pm #17429633

Andreas W.
WPML Supporter since 12/2018

Languages: English (English ) Spanish (Español ) German (Deutsch )

Timezone: America/Lima (GMT-05:00)

I have created a new test site with a form incl. two repeater fields, but I can not confirm any error on form submission. Could you please test using the latest version of WPForms?

If this does not solve the issue, could you please try to replicate the issue on the following test site?

One-Click-Login:
hidden link

If you can't replicate the issue there, please allow me admin access to your site for further revision.

I would like to request temporary access (wp-admin and FTP) to the website to investigate the issue further.

The required fields are located below the comments section when you log in to leave the next reply. The information you provide is private, meaning only you and I can see and access it.

IMPORTANT
Please be sure to back up your website and database before granting us access.
If you can't see the "wp-admin / FTP" fields, your post and website credentials will be set to "PUBLIC." DO NOT publish the data unless you see the required wp-admin / FTP fields.

I may need to install a plugin called "All In One WP Migration" to create a copy of the website so I can investigate the issue further.

However, I would also be very grateful if you could provide a staging site or a copy of the website from your server for this purpose.

If you have any questions about creating such a staging site, you can consult your hosting provider. Please note that WPML must also be registered on this staging site at https://wpml.org/account/websites/.

If you are unable to provide such a copy of the website for testing, please let me know on this ticket.

The private reply form looks like this:
hidden link

Click "I still need assistance" the next time you reply.

Video:
hidden link

Please note that we are required to request this information individually on each ticket. We are not permitted to access any credentials that were not specifically submitted on this ticket using the private response form.

September 30, 2025 at 7:04 am #17442717

andreasH-126

OK, I'll test it and come back.

September 30, 2025 at 7:17 am #17442818

andreasH-126

I could replicate the bug on the first try at hidden link

As a frontend user, fill in the fields and than click "Lägg till" to add at least one extra set of repeater fields. Than try to submit the form.

This triggers an error that prevents the form from being sent. Deactivate WPForms Multilingual and the bug goes away.

I've specified exactly what's wrong in the code and how to fix it in this ticket.

Screenshot 2025-09-30 at 09.11.44.jpg
October 1, 2025 at 3:35 pm #17449457

Andreas W.
WPML Supporter since 12/2018

Languages: English (English ) Spanish (Español ) German (Deutsch )

Timezone: America/Lima (GMT-05:00)

We have published an errata for the reported issue:
https://wpml.org/errata/wpforms-incorrect-field-labels-and-email-notification-issue-with-repeater-fields/

Please take note of the following workaround for this issue:

Open the file .../wp-content/plugins/wpml-wpforms/classes/Hooks/WpForms/Notifications.php

Look for line 92

Replace:

foreach ( $fields as $key => &$field ) {
    $field['name'] = $formPostFields[ $key ]['label'];
    $entryFields   = Obj::propOr( [], 'fields', $entry );
    if ( array_key_exists( $key, $entryFields ) ) {
        $field['value'] = $this->getFieldValue( $field, $entry['fields'][ $key ], $formPostFields[ $key ], $translatedFields[ $key ] );
    }
}

With:

foreach ( $fields as $key => &$field ) {
    $key = strpos( $key, '_' ) !== false ? substr( $key, 0, strpos( $key, '_' ) ) : $key;
    $field['name'] = $formPostFields[ $key ]['label'];
    $entryFields   = Obj::propOr( [], 'fields', $entry );
    if ( array_key_exists( $key, $entryFields ) ) {
        $field['value'] = $this->getFieldValue( $field, $entry['fields'][ $key ], $formPostFields[ $key ], $translatedFields[ $key ] );
    }
}

Please give this a try. It solved the issue in the provided sandbox.

This issue will be solved in the next update for "WP Forms Multilingual 0.5.0".

October 9, 2025 at 6:40 am #17469507

andreasH-126

Awesome, thanks a lot for picking this up!