Parsing text with a translation function

I’m working on a plugin. I have two basic types of options that developers can use

toggleButtonHTML: '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">{language}</button>', 
//toggleButtonHTML: '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">Translate<span class="fsStyleSROnly"> this page</span></button>',

They can only pick one or the other. The commented out option is fully taken care of. In other words, I have my plugin properly discerning the text of the button + any hidden spans (fsStyleSROnly is a screen-reader only class - so it hides text visually). Translation is perfect. This use case is if the design has a button called “Translate” and it opens a dropdown of languages you can click to translate.

The other design pattern is what I’m trying to focus on. There may be situations where instead of “Translate” as a button, it shows the active language the user is on. So if you loaded up the page to English, you could show the 2 character code “EN” (which the data point will be {lang}) or “English” which should generate if {language} is used. I have a few problems. E.g. I have this function which I was using to replace {lang} and {language} BUT this function alone isn’t enough because upon initial rendering, it will replace {lang} and {language} which means future translations cannot translate it properly. E.g. maybe it translates English to “Ingles” instead of “Espanol”. So this function needed to be updated to check for spans or the {lang}/{language}

getUpdatedToggleButtonHTML() {
    const { toggleButtonHTML } = this;

    // Replace {lang} and {language} with the appropriate values
    const updatedHTML = toggleButtonHTML
      .replace(/{lang}/g, Weglot.getCurrentLang())
      .replace(/{language}/g, () => {
        const currentLang = Weglot.getCurrentLang();
        return Weglot.getLanguageName(currentLang);
      });

    return updatedHTML;
  }

So the new attempt

getUpdatedToggleButtonHTML() {
  const { toggleButtonHTML } = this;
  const currentLang = Weglot.getCurrentLang();
  const translatedLanguageName = Weglot.getLanguageName(currentLang);

  // Check for the existence of {lang} or {language}
  const hasLangOrLanguage = /{lang}|{language}/.test(toggleButtonHTML);

  // Replace either {lang}/{language} or the spans with the appropriate values
  let updatedHTML = toggleButtonHTML;
  if (hasLangOrLanguage) {
    updatedHTML = updatedHTML
      .replace(/{lang}/g, `<span data-translation-type="lang">${currentLang}</span>`)
      .replace(/{language}/g, `<span data-translation-type="language">${translatedLanguageName}</span>`);
  }

  return updatedHTML;
}

This new code has 2 problems though: the toggleButton actually won’t click to open my dropdown menu. I see in inspector that something is happening to the attributes, but even if I manually open the dropdown (disabling some CSS), the text of the button stays as “english”

The only other function which is relevant, is addToggleButton() which runs if you’ve elected to output the toggleButton (I have it as a boolean). So, function gets called, addToggleButton runs, and off to the races…The accessibility function is irrelevant IMO. It does stuff like arrow support, escape, attributes work, etc

addToggleButton() {
    const toggleButton = $(this.target).prepend(this.getUpdatedToggleButtonHTML()).find('.fs-weglot-toggle');
    const dropdownList = $(this.target).find(".weglot-list");

    // Accessibility function
    this.toggleButtonAccessibility(toggleButton, dropdownList);

    // Store the original text and inner span text separately
    const originalButtonText = toggleButton.contents().filter(function () {
      return this.nodeType === 3; // Text node
    }).text().trim();

    const originalInnerSpanText = toggleButton.find("span.fsStyleSROnly").text().trim();

    // Update the toggle button text when the language changes
    Weglot.on("languageChanged", (newLang, prevLang) => {
      // Strip the inner span if it exists
      const innerSpan = toggleButton.find("span.fsStyleSROnly");
      if (innerSpan.length) {
        this.translateText(innerSpan.text(), newLang, (translatedText) => {
          innerSpan.text(`${translatedText}`);
        });
      }

      // Translate the button text using the stored original text
      this.translateText(originalButtonText, newLang, (translatedText) => {
        toggleButton.contents().filter(function () {
          return this.nodeType === 3; // Remove existing text nodes
        }).remove();

        // Insert the translated text as a new text node
        toggleButton.prepend(document.createTextNode(`${translatedText}`));

        // Translate the original inner span text
        this.translateText(originalInnerSpanText, newLang, (translatedInnerSpanText) => {
          // Remove existing inner span
          innerSpan.remove();

          // Append the translated inner span after translation only if it's not empty
          if (translatedInnerSpanText) {
            toggleButton.append(` <span class="fsStyleSROnly">${translatedInnerSpanText}</span>`);
          }
        });
        
      });
    });

    // Close dropdown when clicking outside
    $(document).on("click", (event) => {
      const isDropdownClick = dropdownList.is(event.target) || dropdownList.has(event.target).length > 0;
      const isToggleButtonClick = toggleButton.is(event.target);

      if (!isDropdownClick && !isToggleButtonClick) {
        dropdownList.removeClass("weglot-list-open");
        toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
      }
    });
  }

How can I get this working so that if {lang} (“EN”) or {language} (“English”) is used, it works? In my head, if toggleButtonHTML has either of those, it should replace it with a with some sort of data-language-type attribute which we can then use to say "hey, this should not translate normally, like to Ingles, but rather update it with the new {lang} or {language} (I have functions which can get me this info - I just need to detect it)

Thank you.

I am sure confused. Your title indicates you have a question about parsing text but the question seems to be about HTML.

If it were me then I would reduce the sample down to the minimum to reproduce the problem. You are much more likely to get help then. I have done it; I am not being unreasonable. Sometimes I have solved a problem myself that way.

2 Likes

So i’m confused. What is this in these contexts? The button? The innertext? A static string?
It cant be an element, you can’t call replace on an element, and you’re trying to prepend a string, which doesnt… really make sense…because it’s not text, you’re trying to replace the innerHTML, not insert extra stuff into the button…

1 Like

Let me back up and just post the whole program :slight_smile: . I’m calling the plugin as such

const weglotInstance = new FSWeglot(
      document.querySelector(".fsEmbed.fs-weglot-container > .fsElementContent"),
      {
        defaultTemplate: '<li role="none"><a href="#Weglot-{lang}" lang="{lang}" role="option"><span>{language}</span></a></li>',// Should only need to do small modifications here, like adding an inner <span>, for example.
        toggleButtonHTML: '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">{language}</button>', //If using a span to hide text, please use the fsStyleSROnly class as shown in this example.
        // toggleButtonHTML: '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">Translate<span class="fsStyleSROnly"> this page</span></button>', //If using a span to hide text, please use the fsStyleSROnly class as shown in this example.
      },
      function () {
        // Your callback logic here
        console.log($(".fsEmbed.fs-weglot-container")[0].innerHTML);
        console.log("callback'd");
      }
    ).init();

In here, you can see the two options for toggleButtonHTML that I need to support. Below is my program.

class FSWeglot {

  constructor(target, options, callback) {
    this.target = target;
    this.options = options;
    this.defaultTemplate = options.defaultTemplate || '<li role="none"><a href="#Weglot-{lang}" lang="{lang}">{language}</a></li>';
    this.renderedTemplate = [];
    this.callback = callback;
    this.toggleButtonOutput = options.toggleButtonOutput !== undefined ? options.toggleButtonOutput : true;
    this.toggleButtonHTML = options.toggleButtonHTML || '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">Translate<span class="fsStyleSROnly"> this page</span></button>';
  }

  handleLanguageChange(language) {
    const toggleButton = $(this.target).find("button"),
          dropdownList = $(this.target).find(".weglot-list");

    Weglot.switchTo(language.code);

    dropdownList.removeClass("weglot-list-open");
    toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
  }

  createLanguageItem(language) {
    const listItem = this.createLanguageTemplate(language),
          anchor = listItem.find("a");

    anchor.on("click", (e) => {
      e.preventDefault();
      this.handleLanguageChange(language);
    });

    return listItem;
  }

  createLanguageTemplate(language) {
    const template = this.defaultTemplate
      .replaceAll("{lang}", language)
      .replaceAll("{language}", Weglot.getLanguageName(language));

    return $(template);
  }

  availableLanguages() {
    const availableLanguages = Weglot.getAvailableLanguages(),
          list =$('<ul role="listbox" id="weglot-list" class="weglot-list"/>');
    if(!$(this.target).find(list).length) {
      $(this.target).append(list);
    }

    availableLanguages.forEach((language) => {
      list.append(this.createLanguageItem(language));
    });
  }

  render(template) {
    $(this.target).append(template.join(''));
  }

  init() {
    Weglot.on("initialized", () => {
      this.availableLanguages();
      this.render(this.renderedTemplate);

      if (this.toggleButtonOutput) {
        this.addToggleButton();
      }

      this.callback();
    });
  }

  addToggleButton() {
    const toggleButton = $(this.target).prepend(this.getUpdatedToggleButtonHTML()).find('.fs-weglot-toggle');
    const dropdownList = $(this.target).find(".weglot-list");

    // Accessibility function
    this.toggleButtonAccessibility(toggleButton, dropdownList);

    // Store the original text and inner span text separately
    const originalButtonText = toggleButton.contents().filter(function () {
      return this.nodeType === 3; // Text node
    }).text().trim();

    const originalInnerSpanText = toggleButton.find("span.fsStyleSROnly").text().trim();

    // Update the toggle button text when the language changes
    Weglot.on("languageChanged", (newLang, prevLang) => {
      console.log("languageChanged"+newLang,prevLang);
      // Strip the inner span if it exists
      const innerSpan = toggleButton.find("span.fsStyleSROnly");
      if (innerSpan.length) {
        this.translateText(innerSpan.text(), newLang, (translatedText) => {
          innerSpan.text(`${translatedText}`);
        });
      }

      // Translate the button text using the stored original text
      this.translateText(originalButtonText, newLang, (translatedText) => {
        toggleButton.contents().filter(function () {
          return this.nodeType === 3; // Remove existing text nodes
        }).remove();

        // Insert the translated text as a new text node
        toggleButton.prepend(document.createTextNode(`${translatedText}`));

        // Translate the original inner span text
        this.translateText(originalInnerSpanText, newLang, (translatedInnerSpanText) => {
          // Remove existing inner span
          innerSpan.remove();

          // Append the translated inner span after translation only if it's not empty
          if (translatedInnerSpanText) {
            toggleButton.append(` <span class="fsStyleSROnly">${translatedInnerSpanText}</span>`);
          }
        });
        
      });
    });

    // Close dropdown when clicking outside
    $(document).on("click", (event) => {
      const isDropdownClick = dropdownList.is(event.target) || dropdownList.has(event.target).length > 0;
      const isToggleButtonClick = toggleButton.is(event.target);

      if (!isDropdownClick && !isToggleButtonClick) {
        dropdownList.removeClass("weglot-list-open");
        toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
      }
    });
  }

  getUpdatedToggleButtonHTML() {
    const { toggleButtonHTML } = this;

    // Replace {lang} and {language} with the appropriate values
    const updatedHTML = toggleButtonHTML
      .replace(/{lang}/g, Weglot.getCurrentLang())
      .replace(/{language}/g, () => {
        const currentLang = Weglot.getCurrentLang();
        return Weglot.getLanguageName(currentLang);
      });

    return updatedHTML;
  }

  translateText(text, toLang, callback) {
    Weglot.translate(
      {
        words: [{ t: 1, w: text }],
        languageTo: toLang,
      },
      (data) => {
        if(data) {
          callback(data[0]);
        } else {
          console.error("Translation was not successful");
          callack(text);
        }
      }
    );
  }

  toggleButtonAccessibility(toggleButton, dropdownList) {
    toggleButton.on("click", function () {
      console.log(toggleButton);
      dropdownList.toggleClass("weglot-list-open");
      $(this).toggleClass("active");
      toggleButton.attr('aria-expanded', (index, attr) => attr === 'true' ? 'false' : 'true');
    });

    $(this.target).on("keydown", ".weglot-list a", function (e) {
      const anchors = dropdownList.find("a:visible"),
            currentIndex = anchors.index(this);

      switch (e.which) {
        case 13: // Enter
          e.preventDefault();
          Weglot.switchTo(anchors.eq(currentIndex).attr("lang"));
          dropdownList.removeClass("weglot-list-open");
          toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
          break;
        case 27: // Escape
          dropdownList.removeClass("weglot-list-open");
          toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
          break;
        case 38: // Up arrow
          e.preventDefault();
          if (currentIndex > 0) {
            anchors.eq(currentIndex - 1).focus();
          }
          break;
        case 40: // Down arrow
          e.preventDefault();
          if (currentIndex < anchors.length - 1) {
            anchors.eq(currentIndex + 1).focus();
          }
          break;
      }
    });
  }

}

export { FSWeglot };

I’m passing a string of HTML and replacing {lang} and {language} with values that I’m fetching from an API.

but isnt this at that point…the object? You cant call .replace on an object. That or this is some rather obscure and difficult to follow rescoping…

EDIT: Apparently it’s the latter. That… is REALLY poor readability…

1 Like
.replace/{language}/g, Weglot.getLanguageName(Weglot.getCurrentLang())

(I’m gonna avoid having return statements inside nested functions…)

I mean… if you can guarantee 1) the short form is 2 characters long, and 2) there’s no two-letter full named language… you could just… read the current length, and return the appropriate string?

if {the button’s current label text}.Length == 2, button.text = {lang}, elseif button.text != “Translate”, {language}.
then replace?

It shouldnt show up, because the button should skip rendering the template string…

1 Like

That is a bit of a risky solution. I cannot guarentee that the text will always say “Translate”. For example, maybe there’s no visible text (Translate) to the user. Maybe it’s all hidden within the fsStyleSROnly (screenreader only). Plus I cannot guarentee each {lang} is only 2 characters long.

Is identifying {lang} and replacing it with a SPAN and a data attribute (e.g. data-weglot=“lang”) and if that span with the data attribute exists, use getUpdatedToggleButtonHTML to replace that newly created span?

That’s the sort of solution in my head which seems pretty solid but I can’t get it to work.

So how are you identifying {lang}?
Why not just set the attribute on the button to begin with, rather than creating and replacing elements within elements?

(And this line of thought then goes to… “so why aren’t you just using i18n?”)

Well, in post #1, I gave an example of how I’m trying to replace {lang} with the span, but my problem with that example was that there seem to be some issues when I replace the with a . My togglebutton seems to break. It no longer opens up the dropdown. When clicking the button, I see attributes highlighting indicating they are changing, but the values do not change.

Next issue is that In inspector, if I remove the open/close CSS of the dropdown so I can click a language, the toggleButton does not translate at all. E.g. if I open it up in English and click spanish, the text does not adjust at all.

If these two issues could somehow get resolved, I’d be cooking. For reference, here is that function I am talking about.

etUpdatedToggleButtonHTML() {
  const { toggleButtonHTML } = this;
  const currentLang = Weglot.getCurrentLang();
  const translatedLanguageName = Weglot.getLanguageName(currentLang);

  // Check for the existence of {lang} or {language}
  const hasLangOrLanguage = /{lang}|{language}/.test(toggleButtonHTML);

  // Replace either {lang}/{language} or the spans with the appropriate values
  let updatedHTML = toggleButtonHTML;
  if (hasLangOrLanguage) {
    updatedHTML = updatedHTML
      .replace(/{lang}/g, `<span data-translation-type="lang">${currentLang}</span>`)
      .replace(/{language}/g, `<span data-translation-type="language">${translatedLanguageName}</span>`);
  }

  return updatedHTML;
}

Ease of use for the plugin. If I can just have developers use {lang}, then they don’t need to remember to do some data attribute somewhere. Less room for error. What if a developer put it in the wrong spot like an inner span instead of on the button tag? I could wipe my hands and point out incorrect usage, but I’m trying to make this as flexible as possible.

Just wanted to follow up here. I managed to get it working

        // toggleButtonHTML: '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">{language}</button>', //If using a span to hide text, please use the fsStyleSROnly class as shown in this example.

Developers can use this now and it works. It detects the existence of {lang} or {language}, and if found, it sets this.translationType and a data attribute. Here’s the full program.

class FSWeglot {

  constructor(target, options, callback) {
    this.target = target;
    this.options = options;
    this.defaultTemplate = options.defaultTemplate || '<li role="none"><a href="#Weglot-{lang}" lang="{lang}">{language}</a></li>';
    this.renderedTemplate = [];
    this.callback = callback;
    this.toggleButtonOutput = options.toggleButtonOutput !== undefined ? options.toggleButtonOutput : true;
    this.toggleButtonHTML = options.toggleButtonHTML || '<button aria-controls="weglot-list" aria-expanded="false" aria-haspopup="listbox" role="combobox" class="fs-weglot-toggle">Translate<span class="fsStyleSROnly"> this page</span></button>';
    this.translationType = null;
  }

  handleLanguageChange(language) {
    const toggleButton = $(this.target).find("button"),
          dropdownList = $(this.target).find(".weglot-list");

    Weglot.switchTo(language.code);

    dropdownList.removeClass("weglot-list-open");
    toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
  }

  createLanguageItem(language) {
    const listItem = this.createLanguageTemplate(language),
          anchor = listItem.find("a");

    anchor.on("click", (e) => {
      e.preventDefault();
      this.handleLanguageChange(language);
    });

    return listItem;
  }

  createLanguageTemplate(language) {
    const template = this.defaultTemplate
      .replaceAll("{lang}", language)
      .replaceAll("{language}", Weglot.getLanguageName(language));

    return $(template);
  }

  availableLanguages() {
    const availableLanguages = Weglot.getAvailableLanguages(),
          list =$('<ul role="listbox" id="weglot-list" class="weglot-list"/>');
    if(!$(this.target).find(list).length) {
      $(this.target).append(list);
    }

    availableLanguages.forEach((language) => {
      list.append(this.createLanguageItem(language));
    });
  }

  render(template) {
    $(this.target).append(template.join(''));
  }

  init() {
    Weglot.on("initialized", () => {
      this.availableLanguages();
      this.render(this.renderedTemplate);

      if (this.toggleButtonOutput) {
        this.addToggleButton();
      }

      this.callback();
    });
  }

  addToggleButton() {
    const toggleButton = $(this.target).prepend(this.getUpdatedToggleButtonHTML()).find('.fs-weglot-toggle');
    const dropdownList = $(this.target).find(".weglot-list");

    // Accessibility function
    this.toggleButtonAccessibility(toggleButton, dropdownList);

    // Store the original text and inner span text separately
    const originalButtonText = toggleButton.contents().filter(function () {
      return this.nodeType === 3; // Text node
    }).text().trim();

    const originalInnerSpanText = toggleButton.find("span.fsStyleSROnly").text().trim();

    // Update the toggle button text when the language changes
    Weglot.on("languageChanged", (newLang, prevLang) => {

      // Strip the inner span if it exists
      const innerSpan = toggleButton.find("span.fsStyleSROnly");
      if (innerSpan.length) {
        this.translateText(innerSpan.text(), newLang, (translatedText) => {
          innerSpan.text(`${translatedText}`);
        });
      }

      if(this.translationType) {
        this.updateButtonText(toggleButton);
      } else {
        // Translate the button text using the stored original text
        this.translateText(originalButtonText, newLang, (translatedText) => {
          toggleButton.contents().filter(function () {
            return this.nodeType === 3; // Remove existing text nodes
          }).remove();

          // Insert the translated text as a new text node
          toggleButton.prepend(document.createTextNode(`${translatedText}`));

          // Translate the original inner span text
          this.translateText(originalInnerSpanText, newLang, (translatedInnerSpanText) => {
            // Remove existing inner span
            innerSpan.remove();

            // Append the translated inner span after translation only if it's not empty
            if (translatedInnerSpanText) {
              toggleButton.append(` <span class="fsStyleSROnly">${translatedInnerSpanText}</span>`);
            }
          });
          
        });
      }
    });

    // Close dropdown when clicking outside
    $(document).on("click", (event) => {
      const isDropdownClick = dropdownList.is(event.target) || dropdownList.has(event.target).length > 0;
      const isToggleButtonClick = toggleButton.is(event.target);

      if (!isDropdownClick && !isToggleButtonClick) {
        dropdownList.removeClass("weglot-list-open");
        toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
      }
    });
  }

  updateButtonText(button) {
    const translatedText = Weglot.getCurrentLang(); // Default to language code
    if (this.translationType === 'language') {
      // Set button text to Weglot function for language
      button.text(Weglot.getLanguageName(Weglot.getCurrentLang()));
    } else if (this.translationType === 'lang') {
      // Set button text to Weglot function for lang
      button.text(Weglot.getCurrentLang());
    }
  }

  getUpdatedToggleButtonHTML() {
    const { toggleButtonHTML, translationType } = this;
    let updatedHTML = toggleButtonHTML;

    if (/{lang}/.test(this.toggleButtonHTML)) {
      this.translationType = 'lang';
      updatedHTML = updatedHTML.replace(/<button/, `<button data-translation-type="lang"`);
    } else if (/{language}/.test(this.toggleButtonHTML)) {
      this.translationType = 'language';
      updatedHTML = updatedHTML.replace(/<button/, `<button data-translation-type="language"`);
    }
    
    // Replace {lang} and {language} with the appropriate values
    updatedHTML = updatedHTML
      .replace(/{lang}/g, Weglot.getCurrentLang())
      .replace(/{language}/g, () => Weglot.getLanguageName(Weglot.getCurrentLang()));

    // Add data-translation-type attribute if translationType is set
    if (this.translationType) {
      const translatedText = Weglot.getCurrentLang(); // Default to language code
      if (this.translationType === 'language') {
        // Set button text to Weglot function for language
        $(updatedHTML).find('button').text(Weglot.getLanguageName(Weglot.getCurrentLang()));
      } else if (this.translationType === 'lang') {
        // Set button text to Weglot function for lang
        $(updatedHTML).find('button').text(Weglot.getCurrentLang());
      }
    }
    
    return updatedHTML;
  }

  translateText(text, toLang, callback) {
    Weglot.translate(
      {
        words: [{ t: 1, w: text }],
        languageTo: toLang,
      },
      (data) => {
        if(data) {
          callback(data[0]);
        } else {
          console.error("Translation was not successful");
          callack(text);
        }
      }
    );
  }

  toggleButtonAccessibility(toggleButton, dropdownList) {
    toggleButton.on("click", function () {
      dropdownList.toggleClass("weglot-list-open");
      $(this).toggleClass("active");
      toggleButton.attr('aria-expanded', (index, attr) => attr === 'true' ? 'false' : 'true');
    });

    $(this.target).on("keydown", ".weglot-list a", function (e) {
      const anchors = dropdownList.find("a:visible"),
            currentIndex = anchors.index(this);

      switch (e.which) {
        case 13: // Enter
          e.preventDefault();
          Weglot.switchTo(anchors.eq(currentIndex).attr("lang"));
          dropdownList.removeClass("weglot-list-open");
          toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
          break;
        case 27: // Escape
          dropdownList.removeClass("weglot-list-open");
          toggleButton.removeClass("active").attr('aria-expanded', 'false').focus();
          break;
        case 38: // Up arrow
          e.preventDefault();
          if (currentIndex > 0) {
            anchors.eq(currentIndex - 1).focus();
          }
          break;
        case 40: // Down arrow
          e.preventDefault();
          if (currentIndex < anchors.length - 1) {
            anchors.eq(currentIndex + 1).focus();
          }
          break;
      }
    });
  }

}

export { FSWeglot };
2 Likes