Localizing Global Form Validation Error Messages

SanfordWhiteman
Level 10 - Community Moderator
Level 10 - Community Moderator

2023-03-15-20_59_17--VanEck-Prod--Admin

 

Global Form Validation Rules a.k.a. server-enforced banned email domains — came out last year for Marketo forms.

 

They’re a step up from JavaScript-driven domain validation: server-side rules can’t be bypassed by hackers, and they also hide your list of “interesting” domains from end users. (That is, if a person is blocked, they  can’t see the other domains you block all at once.[1])

 

But they have one downside that custom JS validation doesn’t: they only officially support a single language. So if you have this Error Message text:

2023-03-15-20_58_17--VanEck-Prod--Admin

 

That's what everybody sees, regardless of other translations at the form level:

image-8

 

So let's do something fun to enable additional language support.

 

The form error payload

You wouldn't have any reason to know this, but when there's a submission error (that is, an error after sending data to Marketo, not validation errors that occur entirely within JS) the Forms 2.0 library passes around a JS object shaped like this:[2]

{
  "mktoResponse": {
    "for": "mktoFormMessage0",
    "error": true,
    "data": {
      "errorCode": "400",
      "errorType": "invalid",
      "errorFields": [
        "Email"
      ],
      "invalidInputMsg": "Sorry, no free/seemingly free addresses allowed.",
      "error": true
    }
  }
}

Note the source of the message can be either:

(a) a cross-domain postMessage, when using the embed code, or
(b) a direct jQuery ajax response, when on a Marketo LP

 

Making the error payload multilingual

First order of business is packing multiple languages into the string. This is quite easy using JSON:

2023-03-15-20_59_17--VanEck-Prod--Admin

 

Now, the generated error object will look like this:

{
  "mktoResponse": {
    "for": "mktoFormMessage0",
    "error": true,
    "data": {
      "errorCode": "400",
      "errorType": "invalid",
      "errorFields": [
        "Email"
      ],
      "invalidInputMsg": "{\"en\":\"Sorry, no free/seemingly free addresses allowed.\",\"de\":\"Entschuldigung, E-Mail nicht erlaubt\"}",
      "error": true
    }
  }
}

We haven’t done the special stuff yet. Just laying the groundwork. So for now, the whole JSON string is shown:

2023-03-15-23_25_16-CodePen---MktoForms2-__-Customize-Global-Rules-error-message---Mozilla-Firefox

 

Get the code

Next, we add custom JS to narrow down to just the current language:

2023-03-15-23_24_25-CodePen---MktoForms2-__-Customize-Global-Rules-error-message---Mozilla-Firefox

 

Include the handleGlobalRulesJSONErrors function before the embed code or named mktoForm. (In the <head> is fine. Usually, custom form behaviors go after the embed code, but in this case we need to get in before Forms 2.0’s native message listener.)

   /**
    * Customize server-emitted Marketo Forms 2.0 error messages 
    * @author Sanford Whiteman
    * @version v1.0 2023-03-30
    * @copyright © 2023 Sanford Whiteman
    * @license Hippocratic 3.0: This license must appear with all reproductions of this software.
    *
    */
   function handleGlobalRulesJSONErrors(options){

      // error message relay and direct ajax error have different depth
      function getMarketoFlavoredError(obj, mktoField) {
         if (obj && obj.error) {
            let data = obj.data || obj;
            if (data && data.errorFields && data.errorFields.includes(mktoField)) {
               return data;
            }
         }
      }

      // enhance showErrorMessage by also reeenabling form
      function showMarketoError(mktoForm, errorElemName, message, buttonHTML) {
         const jqForm = mktoForm.getFormElem(),
               jqSubmitButton = jqForm.find("button[type='submit']"),
               jqInput = jqForm.find("[name='" + errorElemName + "']");

         jqSubmitButton.removeAttr("disabled");
         jqSubmitButton.html(buttonHTML || jqSubmitButton.attr("data-original-html") || "Submit");
         mktoForm.showErrorMessage(message, jqInput);
      }

      function getInvalidInputTranslations(errorData) {
         try {
            return JSON.parse(errorData.invalidInputMsg);
         } catch (e) {
            throw "Marketo-flavored event detected, but error message text could not be expanded as JSON.";
         }
      }

      // utility fn to allow for async and/or later lib load
      function onMktoFormsLibReady(callback) {
         let reenter = !callback ? onMktoFormsLibReady : onMktoFormsLibReady.bind(this, callback);

         if ( typeof MktoForms2 != "object" && !onMktoFormsLibReady.handling ) {
            document.addEventListener("load", reenter, true);
            onMktoFormsLibReady.handling = true;
         } else if ( typeof MktoForms2 == "object" ) {
            if( !onMktoFormsLibReady.done ) {
               document.removeEventListener("load", reenter, true);
               onMktoFormsLibReady.done = true;
               window.dispatchEvent(new Event("FormsPlus.MktoForms2Ready"));
            }
            callback && callback();            
         }
      }

      // for form embed
      function handleEmbeddedFormElementError(originalEvent) {
         if (originalEvent.origin != options.formOrigin || !originalEvent.isTrusted) return;

         let eventData;
         try {
            eventData = JSON.parse(originalEvent.data);
         } catch (e) {
            return;
         }

         let mktoErrorData = getMarketoFlavoredError(eventData.mktoResponse, "Email");
         if (mktoErrorData) {
            const firstForm = MktoForms2.allForms()[0];
            const messageTranslations = options.translateMap || getInvalidInputTranslations(mktoErrorData);
            showMarketoError(firstForm, "Email", messageTranslations[options.errorContext]);
            originalEvent.stopImmediatePropagation();
         }
      }

      // for named form on Marketo LP
      function handleNamedFormElementError(e) {
         MktoForms2.$.ajaxPrefilter("json", function(ajaxOptions, originaAjaxOptions, jqXHR) {
            const originalErrorCb = ajaxOptions.error;

            ajaxOptions.error = function(jqXHR, jqStatusText, httpStatusText) {
               let mktoErrorData = getMarketoFlavoredError(jqXHR.responseJSON, "Email");
               if (mktoErrorData) {
                  const firstForm = MktoForms2.allForms()[0];
                  const messageTranslations = options.translateMap || getInvalidInputTranslations(mktoErrorData);
                  showMarketoError(firstForm, "Email", messageTranslations[options.errorContext]);
               } else {
                  originalErrorCb.apply(this, arguments);
               }
            };
         });
      };

      // cache original button look, since we can't access the descriptor
      function cacheButtonText(){
         MktoForms2.whenReady(function(readyForm){
            const jqForm = readyForm.getFormElem(),
                  jqSubmitButton = jqForm.find("button[type='submit']");

            jqSubmitButton.attr("data-original-html", jqSubmitButton.html());
         });
      }

      // setup form for manual error management
      onMktoFormsLibReady(cacheButtonText);

      // bind 2 different error listeners, one for Marketo LPs and one for 3rd-party embeds
      onMktoFormsLibReady(handleNamedFormElementError);
      window.addEventListener("message", handleEmbeddedFormElementError);      

   }

 

Use the code

Pass 2 required options to handleGlobalRulesJSONErrors:

  • errorContext: the current language
  • formOrigin: the form source (embed code <script src> or LP URL[3])
 handleGlobalRulesJSONErrors({ 
   errorContext: "de"
   formOrigin: "https://pages.example.com"
 });

That’s it! Now the message will switch based on errorContext.

 

If you have a lot of translations, use a separate map

This solution is made tricky-slick by packing JSON into the existing Error Message box. The max length of that value is 255 bytes, so it’s possible you’ll run out of room if you support many languages (especially ones with wider characters).

 

In that case, you can pass a third option translateMap to the function, shaped like this:

{
    "en" : "English message",
    "de" : "German message",
    "jp" : "Japanese message"
    /* etc… */
}
 
NOTES

[1] There’s a way to keep blocked domains secret even when using JavaScript validation, though I haven’t had a chance to blog about it.

[2] On Marketo LPs, the shape is slightly different from what’s shown. But the code covers both cases.

[3] Technically the formOrigin is only used by the embed code, but always include it for portability.

4233
11
11 Comments
Steven_Vanderb3
Marketo Employee

One day I hope to be able to productize this...thanks Sanford for the great post.

SanfordWhiteman
Level 10 - Community Moderator

Thanks Steven! Officially changing my title to Senior Reverse Engineer after this one.

 

Also made a fascinating discovery about postMessage while working on it. Unrelated to Marketo. But I'll PM you a link about it once I write it up (not here).

Crystal_Pacheco
Level 4

Love this!

Arjun
Level 1

Thank you for the in-depth write up Sanford. 

I am trying to implement this code on our forms, but can't seem to get the custom errors to fire. Any ideas please? It is still firing the message set up in Marketo as opposed to the translateMap. Also tried to change it on Marketo and use a JSON string but then the whole string is displayed. Can't seem to figure out where I'm going wrong.

 

https://codepen.io/gillyinthemist/pen/oNaZLLd

I have set up a simple codepen with a test form, and trying to get it to use the German translation as in your write up. 

 

Any help would be greatly appreciated!

Thanks

Arjun

 

 

SanfordWhiteman
Level 10 - Community Moderator

I’d have to know what domains you’re blocking to test it.

Arjun
Level 1

Hi Sanford!

Thank you for replying.

Apologies I got your name confused with Steven 🙂 

 

Yes that would help wouldn't it. I have been testing with gmail and hotmail.com emails which throws the error.

 

Thanks,

Arjun 

SanfordWhiteman
Level 10 - Community Moderator

The origin has to be an actual origin (which includes the protocol):

 formOrigin: "https://pages.soldo.com",
Jack_Meyenberg
Level 1

This is extremely helpful and absolutely brilliant. Thank you so much Sanford. You are the best!

Babu_Chikati
Level 3

Hi Stanford, I have done following steps to display localization error message for gmail.com validation

  • Added handleGlobalRulesJSONErrors code at the beginning of <head> section of LP template
  • Called handleGlobalRulesJSONErrors({errorContext: "de",formOrigin: "http://mydomain.lp.com"}); right after code  definition
  • Added a From custom validation rule and error message as {"en" : "English message","de" : "German message","jp" : "Japanese message"}

When try to submit the from with test@gmail.com  JSON string is displaying instead of localization message.

Can you please help me to fix the issue.

Thanks for your help!!

 

SanfordWhiteman
Level 10 - Community Moderator

You’d need to link to your actual page, this isn’t enough info.