Builder max min character Limit for localized fields

In Builder.io in our page and section model for our custom component we want to add a max and min character limit, that we have achieved but we have several locales, how to acheive that for the various locales in our builder registry .
We are not able to get the particular locale for which we need to check.

please see the title field in the following custom component. I am searching for locale in options and context but dont get it

{

      name: 'title',

      type: 'string',

      friendlyName: 'Title',

      helperText: 'Overlay Card: Main heading text',

      onChange: (options) => {

      

        console.log('locale',options.get('locale'))

          console.log('options',options);

        const typeTitle = options.get('title');

        const titleObj =

          typeof typeTitle === 'string'

            ? typeTitle

            : typeTitle && typeof typeTitle.get === 'function'

              ? typeTitle.get('en-CA') || typeTitle.get('Default') || ''

              : '';

        if (titleObj.length > 40) {

          alert('Title size should not Exceed 40 characters');

          typeof typeTitle === 'string'?

           options.set('title', options.get('title').substring(0, 40)):

           '';

        }

      },

    },

Hello @dipanjanbhattacharya ,

The issue was that the locale wasn’t directly accessible through the standard options or context in the Builder registry. However, the following implementation successfully retrieves the current locale from the Builder state context and applies both minimum and maximum character limits per locale.

Here’s the tested code snippet that works well:

Builder.registerComponent(Heading, {
  name: "Heading",
  inputs: [
    {
      name: "text",
      type: "string",
      defaultValue: "Add a headline",
      localized: true,
      helperText: "Supports per-locale copy with a 10-120 character range.",
      onChange: (options) => {
        const currentLocale =
          (options.builder &&
            options.builder.state &&
            options.builder.state.context &&
            options.builder.state.context.locale) ||
          options.locale;
        const value = options.get("text") as
          | string
          | Record<string, unknown>
          | undefined
          | null;
        const minLength = 4;
        const maxLength = 12;
        const globalScope =
          typeof globalThis === "object" && globalThis !== null
            ? (globalThis as typeof globalThis & {
                alert?: (value: string) => void;
              })
            : undefined;
        const showAlert = (message: string) => {
          if (typeof globalScope?.alert === "function") {
            globalScope.alert(message);
            return;
          }

          if (typeof alert === "function") {
            alert(message);
          }
        };

        if (typeof value === "string") {
          if (value.length > maxLength) {
            options.set("text", value.slice(0, maxLength));
            showAlert(`Maximum length of ${maxLength} reached${currentLocale ? ` for locale ${currentLocale}` : ""}.`);
          }

          if (value.length !== 0 && value.length < minLength) {
            showAlert(
              `${currentLocale ? `Locale ${currentLocale}` : "Heading text"} must have at least ${minLength} characters.`,
            );
          }
          return;
        }

        if (!value || typeof value !== "object") {
          return;
        }

        const mapCandidate = value as unknown as Map<string, unknown>;
        const isMapLike =
          typeof mapCandidate?.forEach === "function" &&
          typeof mapCandidate?.entries === "function";

        const entries = isMapLike
          ? Array.from(mapCandidate.entries())
          : Object.entries(value as Record<string, unknown>);

        let didModify = false;

        entries.forEach(([locale, localeValue]) => {
          if (locale.startsWith("@") || typeof localeValue !== "string") {
            return;
          }

          if (localeValue.length > maxLength) {
            const truncated = localeValue.slice(0, maxLength);
            if (isMapLike) {
              mapCandidate.set(locale, truncated);
            } else {
              (value as Record<string, unknown>)[locale] = truncated;
            }
            didModify = true;
            showAlert(`Maximum length of ${maxLength} reached for locale ${locale}.`);
          }

          if (localeValue.length !== 0 && localeValue.length < minLength) {
            showAlert(`Locale ${locale} must have at least ${minLength} characters.`);
          }
        });

        if (!isMapLike && didModify) {
          options.set("text", { ...(value as Record<string, unknown>) });
        }
      },
    },
    {
      name: "level",
      type: "string",
      enum: ["h1", "h2", "h3", "h4", "h5", "h6"],
      defaultValue: "h2",
    },
    {
      name: "align",
      type: "string",
      enum: ["left", "center", "right"],
      defaultValue: "left",
    },
  ],
});

Thanks,

Alert seems a bad UX. Can we set a red border with a custom message especially for a rich text field

@manish-sharma

Hi @manish-sharma we used the code and made it a function, and it is working fine but I am facing a challenge with a component

Builder.registerComponent(EditorialGridBuilder, {
  name: 'EditorialGrid',
  inputs: [
    {
      name: 'editorialType',
      type: 'string',
      defaultValue: 'Story',
      enum: ['Story', 'EditorialCardUsp'],
      required: true,
    },
    {
      name: 'EditorialCardProps',
      type: 'object',
      subFields: [
        {
          name: 'title',
          type: 'string',
          defaultValue: 'Stories from Ama',
          required: false,
        },
        {
          name: 'description',
          type: 'richText',
          required: false,
        },
        {
          name: 'buttonText',
          type: 'string',
          defaultValue: 'See all stories',
          required: false,
          helperText: 'Text for the button. Leave empty to hide the button.',
        },
        // Button URL fields - using helpers
        ...urlFieldGroup({
          urlTypeFieldName: 'buttonUrlType',
          manualFieldName: 'buttonManualUrl',
          internalFieldName: 'buttonInternalUrl',
        }),
        {
          name: 'cards',
          type: 'list',
          subFields: [
            imageField({
              fieldName: 'Image',
              required: true,
              allowedTypes: ['.jpg', '.jpeg', '.png', '.webp'],
              includeAltText: true,
              altTextDefault: 'Editorial image',
            }),
            {
              name: 'title',
              type: 'string',
              required: false,
              helperText: 'length should be between 40-90 characters',
              onChange: characterValidationHandler({
                //minLength: 40,
                maxLength: 90,
                fieldName: 'title',
                fieldType: 'string',
              }),

              // onChange: (options) => {
              //   const editorialType = options.get('editorialType');
              //   const titleValue = options.get('title');
              //   if (editorialType === 'Story') {
              //     return characterValidationHandler({
              //       //minLength: 40,
              //       maxLength: 90,
              //       fieldName: 'title',
              //       fieldType: 'string',
              //     })(titleValue);
              //   }
              //   if (editorialType === 'EditorialCardUsp') {
              //     return characterValidationHandler({
              //       //minLength: 5,
              //       maxLength: 22,
              //       fieldName: 'title',
              //       fieldType: 'string',
              //     })(titleValue);
              //   }
              //   return true;
              // },
            },
            {
              name: 'description',
              type: 'richText',
              required: true,
              helperText: 'length should be between 400-500 characters',
              onChange: characterValidationHandler({
                //minLength: 400,
                maxLength: 500,
                fieldName: 'description',
                fieldType: 'richText',
              }),
            },
            {
              name: 'tags',
              type: 'string',
              defaultValue: '',
              required: false,
              helperText: 'Comma-separated list of tags (e.g., "Insider guides, Life Onboard")',
            },
            ...urlFieldGroup({
              manualFieldName: 'manualUrl',
              internalFieldName: 'cardurl',
              manualHelperText:
                'Enter a full URL (e.g., https://...) or internal path (/stories/article-1).',
            }),
          ],
        },
      ],
      defaultValue: EditorialCardData,
    },
  ],
});

As you can see we are using a field “name: ‘editorialType’,” , and based on this showing different UI variant of a component.
In this component title field we want the limits to be different based on “editorialType” but unable to do so cause no if else is working even the editorialType is not accessible within this block we can’t get its value the function “characterValidationHandler” is the function implementation for your provided code which takes the max length value. can you help us with that please.

Hi @manish-sharma ,

Can you please help on this issue?