PageSourceSearch

https://saspeakup.com/Scripts/Translation/translation.js

js saspeakup.com collected 2026-09-28 05:58:58 UTC 56,353 bytes, 1,624 lines download raw bytes

1window.translationModule = (function ($) {
2  var translating = false;
3  var ASSET_TYPES = {
4    DEFAULT: 0,
5    POLL: 1,
6    POLL_OPTION: 2,
7    SUB_POLL: 3,
8    PROJECT_NAME: 4,
9    PROJECT_DESCRIPTION: 5,
10    FREEFORM: 6,
11    POLL_DESCRIPTION: 7,
12    POLL_FORMATTEDTEXT: 8,
13    COMMENT_BOX_PLACEHOLDER: 11,
14    SPECIAL_POLL_TYPE_PLACEHOLDER: 12,
15    STEP_LABEL: 13,
16    POLL_ACTION_BUTTON_PRIMARY: 14,
17    POLL_ACTION_BUTTON_SECONDARY: 15,
18    CHANGE_RESPONSE_TEXT: 16,
19    COMMENT_BUTTON: 17,
20    SURVEY_COMPLETION: 18
21  };
22  var inMemoryTranslationItems = null;
23  var timesWeveDoneTranslation = 0;
24  var config = {};
25  function translate(customerId, projectId, language) {
26    config.customerId = customerId;
27    config.projectId = projectId;
28    config.language = language;
29
30    if (timesWeveDoneTranslation === 0 || timesWeveDoneTranslation > 20)
31      getTranslations(customerId, projectId, language);
32    else applyAllTranslations();
33
34    timesWeveDoneTranslation++;
35  }
36
37  function getTranslations(customerId, projectId, language) {
38    $.ajax({
39      url: "/Translation/GetTranslationsForPoll",
40      data: {
41        customerId: customerId,
42        projectId: projectId,
43        language: language
44      },
45      method: "GET"
46    }).done(function (data) {
47      inMemoryTranslationItems = data;
48      timesWeveDoneTranslation = 0;
49      applyAllTranslations();
50    });
51  }
52
53  function applyAllTranslations() {
54    inMemoryTranslationItems &&
55      inMemoryTranslationItems.forEach(function (translation) {
56        applySingleTranslation(translation);
57      });
58    updatePlaceHolders();
59  }
60
61  function updatePlaceHolders() {
62    // Find all placeholders
63    var inputsWithPlaceholder = document.querySelectorAll(
64      "input[placeholder], textarea[placeholder]"
65    );
66
67    if (inputsWithPlaceholder.length) {
68      var $hiddenDiv = $("<div>")
69        .addClass("translate-hidden-div")
70        .css("position", "absolute")
71        .css("left", "-1000px")
72        .css("top", "-500px")
73        .css("height", "200px");
74
75      createPlaceholderDivs();
76      updateAllPlaceholdersToHiddenDivValues();
77      $("body").append($hiddenDiv);
78
79      function createPlaceholderDivs() {
80        $(inputsWithPlaceholder).each(function (idx, input) {
81          var $input = $(input);
82          if (
83            $input.data("hidden-placeholder-id") != null ||
84            $input.hasClass("notranslate")
85          )
86            return;
87
88          var guid = makeId(7);
89          $(input).data("hidden-placeholder-id", guid);
90
91          var $translateMe = $(
92            '<div id="' + guid + '">' + input.placeholder + "</div>"
93          );
94          $hiddenDiv.append($translateMe);
95        });
96      }
97      function updateAllPlaceholdersToHiddenDivValues() {
98        $(inputsWithPlaceholder).each(function (idx, input) {
99          if ($(input).hasClass("notranslate")) return;
100
101          var guid = $(input).data("hidden-placeholder-id");
102          if (guid == null || guid.length == 0) return;
103          var translatedTxt = $("#" + guid).text();
104          if (input.placeholder != translatedTxt)
105            input.placeholder = translatedTxt;
106        });
107      }
108      function makeId(length) {
109        var result = "";
110        var characters =
111          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
112        var charactersLength = characters.length;
113        for (var i = 0; i < length; i++) {
114          result += characters.charAt(
115            Math.floor(Math.random() * charactersLength)
116          );
117        }
118        return result;
119      }
120    }
121  }
122
123  function undoMostRecentTranslation() {
124    $.post("/Project/UndoTranslate", { projectId: window.cz.projectId }).done(
125      function () {
126        location.reload();
127      }
128    );
129  }
130
131  function saveTranslation(translationForm) {
132    $.ajax({
133      url: "/Translation/Create",
134      data: $("#createTranslation").serialize(),
135      method: "POST"
136    }).done(function (translation) {
137      applySingleTranslation(translation);
138
139      $("#translationTxt").val("");
140      $("#translationModal").modal("hide");
141
142      addClickToTranslateListenerForNextClick();
143    });
144  }
145
146  function addClickToTranslateListenerForNextClick() {
147    document.addEventListener("click", translationModule.generateCssSelector, {
148      capture: true,
149      once: true,
150      passive: false
151    });
152  }
153  function createTranslation() {
154    return new Translation();
155  }
156
157  function generateSelector(event) {
158    if (!translating) {
159      translating = true;
160
161      $("#customTranslation")
162        .text("Stop Translating")
163        .removeClass("btn-success")
164        .addClass("btn-danger stop-translating");
165
166      document.addEventListener(
167        "click",
168        translationModule.generateCssSelector,
169        { capture: true, once: true, passive: false }
170      );
171    } else {
172      stopCustomTranslation(event);
173    }
174  }
175
176  function stopCustomTranslation(rtnData) {
177    translating = false;
178
179    $("#translationTxt").val("");
180    $("#translationModal").modal("hide");
181    $("#customTranslation")
182      .html('<i class="fas fa-mouse-pointer"></i> Click To Translate')
183      .removeClass("btn-danger stop-translating")
184      .addClass("btn-success");
185
186    if (rtnData) {
187      $(rtnData.path).html(rtnData.translationTxt.replace(/\n/g, "<br/>"));
188    }
189  }
190
191  function generateCssSelector(event) {
192    event.stopPropagation();
193    var $target = $(event.target);
194
195    // Check if it has a translation already.
196    if (
197      $target.hasClass("notranslate") ||
198      $target.closest(".notranslate").length > 0
199    ) {
200      // Disable "last mile translation" tool (click to translate) for header buttons
201      if ($target.closest("#custom-translation-header").length) {
202        translationModule.stopCustomTranslation();
203        return;
204      }
205      if (
206        !$target.hasClass("click-to-translate") ||
207        !$target.closest(".notranslate").hasClass("click-to-translate")
208      ) {
209        alert(
210          `This item's translation is managed by the Translation Form Editor. Please click 'Translation Form Editor' at the top of the screen.`
211        );
212        addClickToTranslateListenerForNextClick();
213        return;
214      }
215    }
216    // Check if the clicked element has more than one child
217    if ($target.children().length > 1) {
218      alert(
219        "It appears you've clicked on an item that contains multiple elements. Please try clicking on plain text or an item with fewer components."
220      );
221      return; // Exit the function if too complex
222    }
223    var assetId = -1;
224    var assetType = -1;
225    var custom_options = {
226      class_blacklist: ["goog-text-highlight", "notranslate"]
227    };
228    var selector = new CssSelectorGenerator(custom_options).getSelector(
229      event.target
230    );
231
232    // Google Translate adds a class which messes up the generated selector.
233    // Add the class to the ignored array.
234
235    if (
236      $target.parents(".data-entry-header").length ||
237      $target.hasClass("push-full") ||
238      $target.hasClass("container") ||
239      $target.hasClass("padder-v") ||
240      $target.hasClass("footer") ||
241      $target.hasClass("multi-step-container") ||
242      $target.hasClass("cd-multi-steps")
243    ) {
244      addClickToTranslateListenerForNextClick();
245      return;
246    }
247
248    if ($target.parents(".poll-body").length) {
249      //this is a poll, might be an option
250      var pollId = $target
251        .parents(".poll-body")
252        .siblings(".poll-content-div")
253        .data("poll");
254      if (
255        $target.hasClass("option-name") ||
256        $target.parents(".option-name").length
257      ) {
258        //this is an option, store it as such
259        var optionId = $target.hasClass("option-name")
260          ? $target.data("option-id")
261          : $target.parents(".option-name").data("option-id");
262        assetId = optionId;
263        assetType = ASSET_TYPES.POLL_OPTION; //See translation.cs assettypeenum
264      } else if ($target.parents(".matrixList").length) {
265        //this is a matrix sub poll
266        //this is a matrix subpoll prompt!
267        var subPollId = $target
268          .parents(".matrixList")
269          .first()
270          .data("sub-poll-id");
271        assetId = subPollId;
272        assetType = ASSET_TYPES.SUB_POLL;
273      } else if (
274        $target.hasClass("poll-formatted-text") ||
275        $target.parents(".poll-formatted-text").length
276      ) {
277        assetType = ASSET_TYPES.POLL_FORMATTEDTEXT;
278        assetId = pollId;
279      } else if (
280        $target.hasClass("poll-header") ||
281        $target.parents(".poll-header").length
282      ) {
283        assetType = ASSET_TYPES.POLL_DESCRIPTION;
284        assetId = pollId;
285      } else {
286        //this is part of a poll but not an option or subpoll, so we're just add the poll-specific selector as a parent
287        selector = new CssSelectorGenerator(custom_options).getSelector(
288          event.target
289        );
290        assetId = pollId;
291        assetType = 1;
292      }
293    } else if (
294      $target.attr("id") === "project-description" ||
295      $target.parents("#project-description").length
296    ) {
297      assetType = ASSET_TYPES.PROJECT_DESCRIPTION;
298      assetId = window.cz.projectId;
299    } else if (
300      $target.attr("id") === "project-name" ||
301      $target.parents("#project-name").length
302    ) {
303      assetType = ASSET_TYPES.PROJECT_NAME;
304      assetId = window.cz.projectId;
305    } else if (
306      $target.hasClass("freeform") ||
307      $target.parents(".freeform").length
308    ) {
309      assetType = ASSET_TYPES.FREEFORM;
310      assetId = $target.hasClass("freeform")
311        ? $target.data("freeform-id")
312        : $target.parents(".freeform").first().data("freeform-id");
313    } else if ($target.parents(".multi-step-container").length) {
314      assetType = ASSET_TYPES.STEP_LABEL;
315      assetId = $target.hasClass("surveyProgress")
316        ? $target.data("step-id")
317        : $target.parents(".surveyProgress ").first().data("step-id");
318    } else if ($target.parents(".public-header").length) {
319      selector = ".public-header " + selector;
320    } else if (
321      $target.parents("#footer").length &&
322      selector.indexOf("#footer") < 0
323    ) {
324      selector = "#footer " + selector;
325    } else if ($target.hasClass("finishSurveyBtn")) {
326      assetType = ASSET_TYPES.DEFAULT;
327      assetId = 0;
328      selector = ".finishSurveyBtn";
329    } else if ($target.hasClass("step-continue-button")) {
330      assetType = ASSET_TYPES.DEFAULT;
331      assetId = 0;
332      selector = ".step-continue-button";
333    }
334
335    var $translationModal = $("#translationModal");
336
337    $translationModal.find("input[name='assetId']").val(assetId);
338    $translationModal.find("input[name='assetType']").val(assetType);
339
340    $("#generatedSelector").text(selector);
341    $("#path").val(selector);
342    $("#translationTxt").val(event.target.innerText);
343    $("#translationModal").modal({ backdrop: "static", keyboard: false });
344  }
345  function _getTranslationHtml(translation, wrapInNoTranslateClass) {
346    var html = translation.translationTxt;
347    if (translation.translationTxt.indexOf("<") == -1) {
348      html = html.replace(/\n/g, "<br/>");
349    }
350    if (!wrapInNoTranslateClass) {
351      return html;
352    }
353
354    var $span = $("<span></span>");
355    $span.addClass("notranslate click-to-translate");
356    $span.html(html);
357
358    return $span[0].outerHTML;
359  }
360
361  function _getTranslationText(translation) {
362    return translation.translationTxt;
363  }
364  function applySingleTranslation(translation) {
365    var $toTranslate = null;
366    switch (translation.assetType) {
367      case ASSET_TYPES.SUB_POLL:
368        $toTranslate = $(
369          ".matrixList[data-sub-poll-id=" +
370            translation.assetId +
371            "] .matrixQuestion"
372        );
373        _replaceTextOrHtml();
374        break;
375      case ASSET_TYPES.POLL_OPTION:
376        $toTranslate = $(
377          ".option-name[data-option-id=" + translation.assetId + "]"
378        );
379        _replaceTextOrHtml(true);
380        $toTranslate = $(
381          ".option-name[data-parent-option-id=" + translation.assetId + "]"
382        );
383        _replaceTextOrHtml(true);
384        break;
385      case ASSET_TYPES.POLL:
386        if (translation.path.indexOf("#pluginDiv") === -1)
387          translation.path =
388            "#pluginDiv-poll-" + translation.assetId + " " + translation.path;
389        $toTranslate = $(translation.path);
390        _replaceTextOrHtml();
391        break;
392      case ASSET_TYPES.PROJECT_NAME:
393        break;
394      case ASSET_TYPES.PROJECT_DESCRIPTION:
395        //$toTranslate = $("#project-description");
396        //_replaceTextOrHtml(true);
397        break;
398      case ASSET_TYPES.FREEFORM:
399        //$toTranslate = $(".freeform[data-freeform-id=\"" + translation.assetId + "\"] .freeform-inner-text");
400        //_replaceTextOrHtml(true);
401        break;
402      case ASSET_TYPES.POLL_DESCRIPTION:
403        //$toTranslate = $("#pluginDiv-poll-" + translation.assetId + " .poll-header");
404        //_replaceTextOrHtml(true);
405        break;
406      case ASSET_TYPES.POLL_FORMATTEDTEXT:
407        //$toTranslate = $("#pluginDiv-poll-" + translation.assetId + " .poll-formatted-text p");
408        //_replaceTextOrHtml();
409        break;
410      case ASSET_TYPES.COMMENT_BOX_PLACEHOLDER:
411        $("#pluginDiv-poll-" + translation.assetId + " .comment-entry-box")
412          .attr("placeholder", _getTranslationText(translation))
413          .addClass("notranslate");
414        break;
415      case ASSET_TYPES.SPECIAL_POLL_TYPE_PLACEHOLDER:
416        $(
417          "#pluginDiv-poll-" +
418            translation.assetId +
419            " .locationPoll-searchAddress"
420        )
421          .attr("placeholder", _getTranslationText(translation))
422          .addClass("notranslate");
423        break;
424      case ASSET_TYPES.STEP_LABEL:
425        //translated server-side
426        break;
427      case ASSET_TYPES.SURVEY_COMPLETION:
428        $toTranslate = $(".survey-progress-complete");
429        _replaceTextOrHtml();
430        break;
431      default:
432        if (translation.path.length) {
433          var $toTranslate = $(translation.path);
434          switch (translation.path) {
435            case ".finishSurveyBtn":
436              $toTranslate.data(
437                "finish-survey-translated",
438                _getTranslationHtml(translation)
439              );
440              break;
441            case ".step-continue-button":
442              if ($toTranslate.hasClass("finishSurveyBtn")) {
443                if ($toTranslate.data("finish-survey-translated") == null) {
444                  //if this data is not present, the button may not have a translation, give it back to google
445                  $toTranslate.removeClass("notranslate");
446                }
447                return;
448              }
449          }
450          _replaceTextOrHtml();
451        }
452        break;
453    }
454
455    function storePreTranslationHtml() {
456      if ($toTranslate) mostRecentlyReplacedHtml = $toTranslate.html();
457    }
458    function _replaceTextOrHtml(forceHtml) {
459      if ($toTranslate == null || $toTranslate.length == 0) return;
460
461      storePreTranslationHtml();
462      if (forceHtml == null) {
463        if ($toTranslate.find("a").length) {
464          $toTranslate
465            .find("a")
466            .text(_getTranslationHtml(translation))
467            .addClass("notranslate");
468        } else if ($toTranslate.find("input").length) {
469          $toTranslate
470            .find("span")
471            .text(_getTranslationHtml(translation))
472            .addClass("notranslate");
473        } else {
474          $toTranslate.html(_getTranslationHtml(translation, true));
475        }
476      } else if (
477        $toTranslate.hasClass("option-name") &&
478        $toTranslate.find(".option-other-form").length
479      ) {
480        // This is an option that already has an "add other" form built out. Do not override, as we've probably already translated it.
481        return;
482      } else {
483        $toTranslate.html(_getTranslationHtml(translation, true));
484      }
485    }
486  }
487
488  function throttle(fn, threshhold, scope) {
489    threshhold || (threshhold = 1000);
490    var last, deferTimer;
491    return function () {
492      var context = scope || this;
493
494      var now = +new Date(),
495        args = arguments;
496      if (last && now < last + threshhold) {
497        clearTimeout(deferTimer);
498        deferTimer = setTimeout(function () {
499          last = now;
500          fn.apply(context, args);
501        }, threshhold);
502      } else {
503        last = now;
504        fn.apply(context, args);
505      }
506    };
507  }
508
509  function waitForEditorReady(editor, textareaId, $spinner, timeout = 10000) {
510    return new Promise((resolve, reject) => {
511      let attempts = 0;
512      const intervalTime = 500; // Check every 500ms
513      const maxAttempts = timeout / intervalTime;
514      const interval = setInterval(() => {
515        attempts++;
516        // Check if editor has a status property and if it's set to 'ready'
517        if (editor && editor.status && editor.status === "ready") {
518          clearInterval(interval);
519          resolve();
520        }
521        if (attempts >= maxAttempts) {
522          clearInterval(interval);
523          reject(
524            new Error(
525              "Timeout waiting for CKEditor to become ready for " + textareaId
526            )
527          );
528        }
529      }, intervalTime);
530    });
531  }
532
533  function loadEditors() {
534    // AB#9952 / WASP#756. This file is loaded on EVERY page that renders
535    // _PartialTranslationListenerScripts -- which includes every public engagement hub and
536    // portal page -- and the IIFE at the bottom calls loadEditors() on document ready.
537    // Fetching the editor before checking for something to edit therefore pulled the full
538    // CKEditor 4 build into every ANONYMOUS page view: ~1.3 MB of script nobody could use,
539    // and a self-reporting `CKEDITOR.version` that LA County's Tenable scan fingerprints as
540    // "CKEditor < 4.25.0-LTS Multiples Cross-Site Scripting" on a public portal URL.
541    //
542    // The translation editors only ever exist inside .translation-item containers, which are
543    // rendered by the translation admin UI. No containers means no editor is possible, so
544    // there is nothing to load. Checking first is what keeps the bundle off public pages.
545    if (!$(".translation-item textarea").length) {
546      return;
547    }
548
549    ScriptLoader.loadScriptArray(ScriptLoader.scriptArrays.imageUpload);
550
551    ScriptLoader.loadScriptArray(
552      ScriptLoader.scriptArrays.ckEditor,
553      function () {
554        // Re-queried rather than reused from the guard above: the guard only answers "is an
555        // editor possible on this page", and the translation UI can add rows between the
556        // ready handler and the bundle finishing, so the callback must see the live DOM.
557        var $textareas = $(".translation-item textarea");
558        let editorLoadingPromises = [];
559
560        $textareas.each(function () {
561          var textareaId = this.id;
562          var $container = $(this).closest(".translation-item");
563          var $spinner = $container.find(".spinner");
564
565          if (!$(this).is(":visible")) {
566            return;
567          }
568
569          if ($spinner.length) {
570            $spinner.show();
571          }
572
573          let promise = new Promise(function (resolve, reject) {
574            try {
575              var editor = initCkEditor(textareaId);
576              if (!editor) {
577                return reject(
578                  new Error("Failed to initialize CKEditor for " + textareaId)
579                );
580              }
581
582              var resolved = false;
583
584              if (editor.on) {
585                editor.on("instanceReady", function () {
586                  if (!resolved) {
587                    resolved = true;
588                    if ($spinner.length) {
589                      $spinner.hide();
590                    }
591                    resolve(textareaId);
592                  }
593                });
594              }
595
596              waitForEditorReady(editor, textareaId, $spinner)
597                .then(function () {
598                  if (!resolved) {
599                    resolved = true;
600                    if ($spinner.length) {
601                      $spinner.hide();
602                    }
603                    resolve(textareaId);
604                  }
605                })
606                .catch(function (error) {
607                  if (!resolved) {
608                    resolved = true;
609                    if ($spinner.length) {
610                      $spinner.hide();
611                    }
612                    reject(error);
613                  }
614                });
615            } catch (error) {
616              if ($spinner.length) {
617                $spinner.hide();
618              }
619              reject(error);
620            }
621          });
622
623          editorLoadingPromises.push(promise);
624        });
625
626        Promise.allSettled(editorLoadingPromises).then(function (results) {
627          let errors = 0;
628
629          results.forEach(function (result, index) {
630            let id = $textareas.eq(index).attr("id") || "unknown";
631            if (result.status !== "fulfilled") {
632              errors++;
633            }
634          });
635
636          if (errors > 0) {
637            let fullErrorMessage = `${errors} errors occurred while initializing editors. Please close the window try again.`;
638            TOOLS.notify(fullErrorMessage, "danger");
639          }
640
641          $(
642            "[data-dismiss='modal'], .save-translation-form-fill, .create-form"
643          ).prop("disabled", false);
644        });
645      }
646    );
647  }
648  function initCkEditor(id, isInline, configExtra) {
649    configExtra = configExtra || {};
650
651    if (
652      CKEDITOR &&
653      CKEDITOR.instances[id] &&
654      CKEDITOR.instances[id].status != "unloaded"
655    ) {
656      if (id === "projectDescriptionEntry") return;
657      //CKEDITOR.instances[id].destroy();
658      CKEDITOR.remove(CKEDITOR.instances[id]);
659    }
660
661    window.ckPlaceholder = "Click here to edit this text block";
662    var config = {
663      filebrowserBrowseUrl: "filebrowserBrowseUrl",
664      on: {
665        change: function () {
666          var data = getCkEditorInnerText(this);
667          configExtra.change && configExtra.change(this);
668          if (this.element && this.element.$) {
669            $(this.element.$).trigger("change");
670          }
671        },
672        focus: function () {},
673        blur: function () {}
674      }
675    };
676    try {
677      if (isInline) {
678        return CKEDITOR.inline(id, config);
679      } else {
680        return CKEDITOR.replace(id, config);
681      }
682    } catch (e) {
683      if (e.indexOf("already attached")) return;
684
685      setTimeout(function () {
686        initCkEditor(id, isInline, configExtra);
687      }, 500);
688    }
689  }
690  function getCkEditorInnerText(editor) {
691    return editor.element.$.innerHTML.trim();
692  }
693
694  function generatePublicTranslateLink() {
695    var formWindow = window.open(
696      "",
697      "_blank",
698      "width=" + screen.width + ",height=" + screen.height
699    );
700    if (formWindow) {
701      formWindow.document.write(getLoadingHtml());
702      formWindow.focus();
703    } else {
704      console.error("Popup blocked. Please allow popups for this site.");
705    }
706
707    let data = {
708      projectId: config.projectId,
709      customerId: config.customerId,
710      queryString: window.location.search.substring(1),
711      language: this.config.language
712    };
713
714    $.ajax({
715      url: "/Project/GeneratePublicTranslateLink",
716      type: "POST",
717      data: data
718    })
719      .done((link) => {
720        window.location.href = link;
721        openFormFillWindow(link, formWindow);
722      })
723      .fail((err) => {
724        if (window.TOOLS) {
725          TOOLS.notify(
726            "An error occurred while creating your translation link. Please try again later.",
727            "danger"
728          );
729        }
730      });
731  }
732
733  function openFormFillWindow(url, formWindow) {
734    if (!formWindow) {
735      let newFormWindow = window.open(
736        "",
737        "_blank",
738        "width=" + screen.width + ",height=" + screen.height
739      );
740      if (!newFormWindow) {
741        console.error("Popup blocked. Please allow popups for this site.");
742        return;
743      }
744      newFormWindow.document.write(getLoadingHtml());
745      formWindow = newFormWindow;
746    }
747
748    if (!url) {
749      if (
750        !config ||
751        !config.projectId ||
752        !config.customerId ||
753        !config.language
754      ) {
755        console.error("Config properties are missing.");
756        return;
757      }
758
759      url = "";
760    }
761
762    let extractedHash = "";
763    if (url && url != "") {
764      let parts = url.split("#");
765      if (parts.length > 1) {
766        extractedHash = parts[1];
767      }
768    } else {
769      extractedHash = window.location.hash.substring(1);
770    }
771
772    let getFormUrl =
773      "/Translation/TranslationForm?projectId=" +
774      encodeURIComponent(config.projectId) +
775      "&customerId=" +
776      encodeURIComponent(config.customerId) +
777      "&language=" +
778      encodeURIComponent(config.language) +
779      "&hash=" +
780      encodeURIComponent(extractedHash);
781
782    formWindow.location.href = getFormUrl;
783    formWindow.focus();
784  }
785
786  function populateDropdownLangSelector() {
787    $("a.lang-item").each(function () {
788      var url = window.location.href;
789      var lang = $(this).data("translate-to");
790      var queryString = getUpdatedParamsString(window.location, "lang", lang);
791      var href = url;
792      if (url.indexOf("#") > 0) {
793        var cleanUrl = url.substring(0, url.indexOf("#"));
794        href =
795          cleanUrl.split("?")[0] + "?" + queryString + window.location.hash;
796      } else {
797        href = url.split("?")[0] + "?" + queryString;
798      }
799      $(this).attr("href", href);
800    });
801  }
802
803  function saveFormFillModal() {
804    var postData = $.extend({}, config, { translations: [] });
805    postData.translations = mapAllTranslationItems();
806
807    postData.__RequestVerificationToken = $(
808      '#translation-form input[name="__RequestVerificationToken"]'
809    ).val();
810
811    $.post("/Translation/SaveTranslationFormFill", postData)
812      .done(() => {
813        if (window.TOOLS) {
814          TOOLS.notify("Saved successfully", "success");
815        }
816      })
817      .fail(() => {
818        if (window.TOOLS) {
819          TOOLS.notify(
820            "An error occurred while saving your translation. Please try logging in as an admin, then re-saving your changes.",
821            "danger"
822          );
823        }
824      });
825  }
826
827  var allowedFields = [
828    "Id",
829    "AssetId",
830    "AssetType",
831    "Path",
832    "ProjectId",
833    "CustomerId",
834    "Language",
835    "TranslationTxt"
836  ];
837  function mapTranslationItem($container) {
838    var item = {};
839    $container.find("input, textarea, select").each(function () {
840      var $el = $(this);
841      var name = $el.attr("name");
842      if (name && allowedFields.indexOf(name) !== -1) {
843        item[name] = $el.val();
844      }
845    });
846    return item;
847  }
848
849  function mapAllTranslationItems() {
850    var translations = [];
851    $(".translation-item").each(function (index, translationItem) {
852      var $translationItem = $(translationItem);
853
854      var $textarea = $translationItem.find("textarea");
855      if ($textarea.length) {
856        var txtId = $textarea.attr("id");
857        if (
858          CKEDITOR &&
859          CKEDITOR.instances[txtId] &&
860          CKEDITOR.instances[txtId].status !== "unloaded"
861        ) {
862          var val = CKEDITOR.instances[txtId].getData();
863          $textarea.val(val);
864        }
865      }
866
867      var mappedItem = mapTranslationItem($translationItem);
868
869      translations.push(mappedItem);
870    });
871    return translations;
872  }
873
874  function getLoadingHtml() {
875    return `
876        <!DOCTYPE html>
877        <html>
878          <head>
879            <title>Loading Translation Form...</title>
880            <style>
881              body {
882                margin: 0;
883                padding: 0;
884                background: #f5f5f5;
885                font-family: Arial, sans-serif;
886                text-align: center;
887                color: #333;
888              }
889              .container {
890                padding: 50px;
891              }
892              .loader {
893                margin: 20px auto;
894                border: 8px solid #e0e0e0; /* Light grey */
895                border-top: 8px solid #3498db; /* Blue */
896                border-radius: 50%;
897                width: 60px;
898                height: 60px;
899                animation: spin 1s linear infinite;
900              }
901              @keyframes spin {
902                0% { transform: rotate(0deg); }
903                100% { transform: rotate(360deg); }
904              }
905              p {
906                font-size: 18px;
907                margin-top: 20px;
908              }
909            </style>
910          </head>
911          <body>
912            <div class="container">
913              <div class="loader"></div>
914              <p>Loading Translation Form...</p>
915            </div>
916          </body>
917        </html>
918    `;
919  }
920
921  (function () {
922    let $projectTitleSection = $(".project-title").first();
923
924    if ($projectTitleSection.length) {
925      config.projectId = $projectTitleSection
926        .find('input[name="ProjectId"]')
927        .val();
928      config.customerId = $projectTitleSection
929        .find('input[name="CustomerId"]')
930        .val();
931      config.language = $projectTitleSection
932        .find('input[name="Language"]')
933        .val();
934    }
935
936    $(function () {
937      loadEditors();
938      translateInitCallbackLanguageExists();
939    });
940  })();
941
942  return {
943    createTranslation: createTranslation,
944    generateSelector: generateSelector,
945    generateCssSelector: generateCssSelector,
946    stopCustomTranslation: stopCustomTranslation,
947    saveTranslation: saveTranslation,
948    translate: translate,
949    throttle: throttle,
950    loadEditors: loadEditors,
951    generatePublicTranslateLink: generatePublicTranslateLink,
952    undoMostRecentTranslation: undoMostRecentTranslation,
953    openFormFillWindow: openFormFillWindow,
954    saveFormFillModal: saveFormFillModal,
955    populateDropdownLangSelector: populateDropdownLangSelector,
956    setupGoogleTranslateListener: setupGoogleTranslateListener,
957    updateSignupWidgetLang: updateSignupWidgetLang,
958    inMemoryTranslationItems: function () {
959      return inMemoryTranslationItems;
960    },
961    config: config
962  };
963})(jQuery);
964
965$(document).ready(function () {
966  translationModule.populateDropdownLangSelector();
967});
968$(document).on("click", ".undo-translation", function () {
969  translationModule.undoMostRecentTranslation();
970});
971$(document).on("click", ".save-translation-form-fill", function (e) {
972  e.preventDefault();
973  translationModule.saveFormFillModal();
974});
975$(document).on("click", ".open-translation-form-fill", function () {
976  translationModule.openFormFillWindow();
977});
978$(document).on("click", ".stop-translating", function () {
979  translationModule.stopCustomTranslation();
980});
981$(document).on("click", "#customTranslation", function () {
982  translationModule.generateSelector();
983});
984$(document).on("click", "#stopTranslation", function () {
985  translationModule.stopCustomTranslation();
986});
987$(document).on("click", "#continueTranslation", function () {
988  translationModule.saveTranslation();
989});
990// --- #7321: guarded reveal of the Google Translate widget -------------------
991// The widget container is only ever populated by Google's element.js. When that
992// script is blocked (privacy/ad-block extension, network policy, CSP) or simply
993// slow, the container stays EMPTY. Revealing it while hiding the toggle strands
994// the user: the button is gone and the empty container renders as a 
994small blank
995// box that does nothing, recoverable only by refreshing the page.
996
997// Some pages render more than one #google_translate_element (duplicate ids).
998// querySelectorAll is deliberate: unlike getElementById it returns EVERY match,
999// while still using the engine's id lookup rather than a full-document attribute
1000// scan. Do NOT "simplify" this back to $("#google_translate_element") -- jQuery
1001// would silently operate on only the first container.
1002function translateWidgetContainers() {
1003  return $(document.querySelectorAll("#google_translate_element"));
1004}
1005
1006// "Ready" means at least one container has actually been populated. Keying
1007// readiness solely on Google's internal .goog-te-combo class would turn any
1008// future change to their widget markup into a silent global outage that wrongly
1009// blames the visitor's ad blocker, so accept any injected child as evidence the
1010// widget rendered. Google's TranslateElement resolves its host with
1011// document.getElementById and therefore populates only the FIRST container, so
1012// this is also the single source of "which container may we reveal" -- showing
1013// the empty copies would put blank boxes on screen, exactly the #7321 symptom.
1014function populatedTranslateContainers($containers) {
1015  return ($containers || translateWidgetContainers()).filter(function () {
1016    var $c = $(this);
1017    return $c.children().length > 0 || $c.find(".goog-te-combo").length > 0;
1018  });
1019}
1020
1021// Readiness is defined once, above: the widget is ready as soon as any container
1022// has been populated.
1023function isTranslateWidgetReady($containers) {
1024  return populatedTranslateContainers($containers).length > 0;
1025}
1026
1027function removeTranslateUnavailableNotice() {
1028  $(".translate-unavailable-notice").stop(true, true).remove();
1029}
1030
1031function showTranslateUnavailableNotice() {
1032  if ($(".translate-unavailable-notice").length) return; // idempotent
1033  var $toggle = $(".language-toggle").filter(":visible").first();
1034  if (!$toggle.length) $toggle = $(".language-toggle").first();
1035  if (!$toggle.length) return;
1036  // Insert the live region EMPTY, then fill it on a later tick. Screen readers
1037  // generally only announce mutations to a region that was already in the
1038  // accessibility tree; one injected with its text intact is commonly silent
1039  // (WCAG 4.1.3 Status Messages). Styling lives in Content/Civic-Front.css --
1040  // a muted utility class would fail contrast over the dark hub hero banner.
1041  var $notice = $("<div>")
1042    .addClass("translate-unavailable-notice")
1043    .attr("role", "status")
1044    .attr("aria-live", "polite");
1045  $toggle.after($notice);
1046  setTimeout(function () {
1047    $notice.text("Translation isn't available right now. Please try again.");
1048  }, 100);
1049  setTimeout(function () {
1050    $notice.fadeOut(400, function () {
1051      $(this).remove();
1052    });
1053  }, 10000);
1054}
1055
1056/**
1057 * Put the toggle back after a reveal that could not complete.
1058 *
1059 * stop(true, false) is required: several views still reveal the container with
1060 * .fadeIn(), and a bare .hide() would be overwritten when that 400ms animation
1061 * finishes, letting the empty box reappear.
1062 *
1063 * Focus is only restored when the user's own activation failed (focusToggle),
1064 * and even then only if the user hasn't already moved on (see below).
1065 */
1066function restoreTranslateToggle($containers, focusToggle) {
1067  var $c = $containers || translateWidgetContainers();
1068  $c.stop(true, false).hide();
1069  var $toggles = $(".language-toggle").show().attr("aria-expanded", "false");
1070  if (!focusToggle) return;
1071  // The reveal can wait ~6s before failing; by then the user may have tabbed
1072  // away. Yanking focus back to the toggle seconds after activation is its own
1073  // accessibility failure, so only refocus when focus is nowhere meaningful
1074  // (null / <body>) or still inside one of the translate containers.
1075  var active = document.activeElement;
1076  var focusIsIdle =
1077    !active ||
1078    active === document.body ||
1079    $c.filter(function () {
1080      return this === active || $.contains(this, active);
1081    }).length > 0;
1082  if (!focusIsIdle) return;
1083  var $first = $toggles.filter(":visible").first();
1084  if ($first.length && $first[0] !== active) $first.trigger("focus");
1085}
1086
1087var translateRevealInFlight = false;
1088
1089/**
1090 * Swap the toggle for the Google Translate widget, but only once the widget has
1091 * actually initialized. Polls so a slow load still succeeds, and never hides the
1092 * toggle unless there is a real control to replace it with.
1093 */
1094function revealTranslateWidget() {
1095  if (translateRevealInFlight) return; // ignore repeat activations
1096  translateRevealInFlight = true;
1097
1098  var $containers = translateWidgetContainers();
1099  var attempts = 0;
1100  var maxAttempts = 60; // ~6s: a cold Google CDN fetch can easily exceed 2s
1101
1102  (function poll() {
1103    if (isTranslateWidgetReady($containers)) {
1104      removeTranslateUnavailableNotice(); // never leave a stale failure message
1105      // Reset every copy, then reveal only the one Google populated.
1106      $containers.stop(true, false).hide();
1107      populatedTranslateContainers($containers).show();
1108      $(".language-toggle").hide().attr("aria-expanded", "true");
1109      var $select = $containers.find(".goog-te-combo").first();
1110      if ($select.length) $select.trigger("focus");
1111      translateRevealInFlight = false;
1112      return;
1113    }
1114    if (window.__piTranslateScriptFailed === true) {
1115      // The element.js <script> tag sets this flag onerror, so a blocked or
1116      // failed script fails in milliseconds instead of making the visitor wait
1117      // out the ~6s poll. Pages that never set the flag still fall back to the
1118      // timeout below.
1119      restoreTranslateToggle($containers, true);
1120      showTranslateUnavailableNotice();
1121      translateRevealInFlight = false;
1122      return;
1123    }
1124    if (++attempts >= maxAttempts) {
1125      // Never strand the user (#7321): keep the button, keep the empty box hidden.
1126      restoreTranslateToggle($containers, true);
1127      showTranslateUnavailableNotice(); // only here -- we actually waited it out
1128      translateRevealInFlight = false;
1129      return;
1130    }
1131    setTimeout(poll, 100);
1132  })();
1133}
1134
1135if (typeof translationModule !== "undefined" && translationModule) {
1136  translationModule.revealTranslateWidget = revealTranslateWidget;
1137  translationModule.isTranslateWidgetReady = isTranslateWidgetReady;
1138}
1139
1140// click only: every .language-toggle in the app is a native <button>, which
1141// already fires click on Enter/Space. Binding keydown as well would auto-repeat
1142// (~30Hz) while the key is held and stack poll chains, and would put a delegated
1143// keydown matcher on document for every keystroke on the page.
1144// Bind once. translation.js is included from several views/layouts, so a page
1145// that both loads it directly and inherits a layout rendering the listener
1146// partial would otherwise double-bind and run two reveal machines per click.
1147if (!window.__piTranslateToggleBound) {
1148  window.__piTranslateToggleBound = true;
1149
1150  $(document).on("click", ".language-toggle", function (e) {
1151    e.preventDefault();
1152    revealTranslateWidget();
1153  });
1154}
1155
1156/**
1157 * Enforces focus within a modal when the modal is shown. This function is an event handler
1158 * for the 'shown.bs.modal' event. It adds another event listener to the document to manage
1159 * focus within the modal. It checks if the focus is outside the modal and, if so, brings it
1160 * back to the modal. This function ensures that the focus remains trapped inside the modal
1161 * for better accessibility and user experience.
1162 *
1163 * @param {Event} event - The event object associated with the 'shown.bs.modal' event.
1164 */
1165//function enforceModalFocus(e) {
1166//  let modal = e.target;
1167//  if (modal.classList.contains("modal")) {
1168//    document.addEventListener(
1169//      "focusin",
1170//      function (focusEvent) {
1171//        if (!modal.contains(focusEvent.target)) {
1172//          // Focus is outside the modal, enforce focus back to the modal
1173//          modal.focus();
1174//        }
1175//      },
1176//      true
1177//    );
1178//  }
1179//}
1180
1181/**
1182 * Removes the focus enforcement from the modal when it is hidden. This function is an event
1183 * handler for the 'hidden.bs.modal' event. It removes the 'focusin' event listener from the
1184 * document that was added by enforceModalFocus. This cleanup prevents potential memory leaks
1185 * and ensures that the focus behavior is only active when the modal is visible.
1186 *
1187 * @param {Event} event - The event object associated with the 'hidden.bs.modal' event.
1188 */
1189function removeModalFocus(e) {
1190  let modal = e.target;
1191  if (modal.classList.contains("modal")) {
1192    // Cleanup: remove the event listener when the modal is closed
1193    document.removeEventListener("focusin", enforceModalFocus, true);
1194  }
1195}
1196
1197$(document).on("click", ".edit-banner-image-id", function () {
1198  var $btn = $(this);
1199  window.mediaLibrary.$modal.modal("show");
1200  window.mediaLibrary.imageSelectedCallback = function (imageData) {
1201    var $imgWrapper = $btn.closest("div").find(".image-wrapper");
1202    $btn.closest("div").find('[name="TranslationTxt"]').val(imageData.imageId);
1203    var $img = $imgWrapper.find("img");
1204    if ($img.length == 0) {
1205      $img = $("<img>");
1206      $imgWrapper.append($img);
1207    }
1208    $img.attr(
1209      "src",
1210      "/img/" + imageData.imageId + "_200_400." + imageData.extension
1211    );
1212  };
1213});
1214
1215function googleTranslateElementInit(config) {
1216  // Prefer an explicit config passed in, otherwise try to derive one from
1217  // window.translationConfig.  If that is still missing, fall back to
1218  // values placed on window.modelData (added by several server-side views)
1219  // or to the query-string "lang" parameter.
1220  var derivedConfig = window.translationConfig || {};
1221
1222  // Pull values from modelData when translationConfig is not present or incomplete
1223  if (
1224    (!derivedConfig.selectedLang || !derivedConfig.availableLangs) &&
1225    window.modelData
1226  ) {
1227    if (!derivedConfig.selectedLang && window.modelData.lang) {
1228      derivedConfig.selectedLang = window.modelData.lang;
1229    }
1230    if (
1231      !derivedConfig.availableLangs &&
1232      window.modelData.customer &&
1233      window.modelData.customer.googleTranslateLanguages
1234    ) {
1235      derivedConfig.availableLangs =
1236        window.modelData.customer.googleTranslateLanguages;
1237    }
1238  }
1239
1240  // Still no selectedLang?  Try the URL query-string.
1241  if (!derivedConfig.selectedLang) {
1242    var qsLang = new URLSearchParams(window.location.search).get("lang");
1243    if (qsLang) {
1244      derivedConfig.selectedLang = qsLang;
1245    }
1246  }
1247
1248  // Store back to the global so the rest of the module can access it.
1249  window.translationConfig = derivedConfig;
1250
1251  if (derivedConfig.selectedLang) {
1252    updateSignupWidgetLang(derivedConfig.selectedLang);
1253  }
1254
1255  // Instantiate the Google Translate widget so it's available when the user clicks
1256  // the language toggle, even if no lang param was specified.
1257  //
1258  // #7321: idempotent by design. Several views load Google's element.js more than
1259  // once (e.g. a page's own tag plus the translation listener partial), and each
1260  // load fires this callback. Constructing TranslateElement per call stacks a fresh
1261  // widget into #google_translate_element -- the reported "duplicate/triplicate
1262  // Select Language dropdowns". Build it once and reuse it.
1263  if (!window.translateEl) {
1264    window.translateEl = new google.translate.TranslateElement(
1265      {
1266        pageLanguage: "en",
1267        includedLanguages: derivedConfig.availableLangs || "",
1268        layout: google.translate.TranslateElement.InlineLayout.HORIZONTAL
1269      },
1270      "google_translate_element"
1271    );
1272  }
1273
1274  // Always sync html[lang] with the active translation language so assistive
1275  // technologies announce content in the correct language even when the page
1276  // starts in English and the user picks a language manually from the widget.
1277  (function syncHtmlLang() {
1278    var htmlEl = document.documentElement;
1279    var originalLang = htmlEl.getAttribute("lang") || "en";
1280    new MutationObserver(function (mutations) {
1281      for (var i = 0; i < mutations.length; i++) {
1282        if (mutations[i].attributeName === "class") {
1283          var isTranslating =
1284            htmlEl.classList.contains("translated-ltr") ||
1285            htmlEl.classList.contains("translated-rtl");
1286          if (isTranslating) {
1287            setTimeout(function () {
1288              var lang = detectCurrentGoogleTranslateLanguage();
1289              if (lang) {
1290                htmlEl.setAttribute("lang", lang);
1291              }
1292            }, 500);
1293          } else {
1294            htmlEl.setAttribute("lang", originalLang);
1295          }
1296          break;
1297        }
1298      }
1299    }).observe(htmlEl, { attributes: true });
1300  })();
1301
1302  // Always attach the dropdown listener so manual language picks update html[lang]
1303  // even on pages that loaded without a pre-set lang param.
1304  setupGoogleTranslateDropdownListener();
1305
1306  // Automatically trigger translation only when a non-English language is specified.
1307  // This handles the case where lang comes from URL query string.
1308  if (derivedConfig.selectedLang && derivedConfig.selectedLang !== "en") {
1309    if (typeof translateInitCallbackLanguageExists === "function") {
1310      translateInitCallbackLanguageExists();
1311    }
1312  }
1313}
1314
1315function toggleGoogleTranslateSelectClick() {
1316  var $select = $(".goog-te-combo");
1317  if (!$select.length) return;
1318
1319  // Figure out which language we are switching to.
1320  var lang =
1321    (window.translationConfig && window.translationConfig.selectedLang) ||
1322    (window.modelData && window.modelData.lang);
1323  if (!lang) return;
1324
1325  // Trigger the Google dropdown to apply the language programmatically.
1326  $select.click();
1327  $select.val(lang);
1328  // Manually dispatch the change event so Google picks it up
1329  $select[0].dispatchEvent(createNewEvent("change"));
1330  $select[0].click();
1331}
1332
1333function translateInitCallbackLanguageExists() {
1334  // Track if user has manually turned off translation
1335  window.userDisabledTranslation = false;
1336
1337  // Set up observer to detect changes to HTML classes (when translation is toggled)
1338  var htmlElement = document.documentElement;
1339  var originalLang = htmlElement.getAttribute("lang") || "en";
1340  var wasTranslating = false;
1341
1342  var observer = new MutationObserver(function (mutations) {
1343    mutations.forEach(function (mutation) {
1344      if (mutation.attributeName === "class") {
1345        var isCurrentlyTranslating =
1346          htmlElement.classList.contains("translated-ltr") ||
1347          htmlElement.classList.contains("translated-rtl");
1348
1349        // If translation was on and now it's off, user likely disabled it manually
1350        if (wasTranslating && !isCurrentlyTranslating) {
1351          window.userDisabledTranslation = true;
1352          clearInterval(window.translationCheckInterval);
1353          htmlElement.setAttribute("lang", originalLang);
1354        }
1355
1356        // If translation state changed (either on or off), update signup widgets
1357        if (wasTranslating !== isCurrentlyTranslating) {
1358          if (isCurrentlyTranslating) {
1359            // Google Translate is now active, detect the current language
1360            setTimeout(function () {
1361              var currentLang = detectCurrentGoogleTranslateLanguage();
1362              if (currentLang) {
1363                updateSignupWidgetLang(currentLang);
1364                htmlElement.setAttribute("lang", currentLang);
1365              }
1366            }, 500); // Give Google Translate a moment to update the dropdown
1367          }
1368        }
1369
1370        wasTranslating = isCurrentlyTranslating;
1371      }
1372    });
1373  });
1374
1375  // Start observing HTML element for class changes
1376  observer.observe(htmlElement, { attributes: true });
1377
1378  // Initial attempt to translate when translate element is ready
1379  $.when(window.translateEl).done(function () {
1380    setTimeout(function () {
1381      toggleGoogleTranslateSelectClick();
1382    }, 500);
1383  });
1384
1385  // Only start the retry interval when there is a language to apply.
1386  // Without this guard the interval fires every 2s on English-default pages
1387  // (where toggleGoogleTranslateSelectClick returns early), never clears,
1388  // and creates a permanent background timer.
1389  var selectedLang =
1390    (window.translationConfig && window.translationConfig.selectedLang) ||
1391    (window.modelData && window.modelData.lang);
1392
1393  if (selectedLang) {
1394    window.translationCheckInterval = setInterval(function () {
1395      var isTranslating =
1396        $("html").hasClass("translated-ltr") ||
1397        $("html").hasClass("translated-rtl");
1398
1399      // If manually disabled or already translating, stop the interval
1400      if (window.userDisabledTranslation || isTranslating) {
1401        clearInterval(window.translationCheckInterval);
1402      } else {
1403        // Only try to re-translate if not manually disabled
1404        toggleGoogleTranslateSelectClick();
1405      }
1406    }, 2000);
1407
1408    // Safety valve: clear the interval after 30s regardless, to avoid a
1409    // permanent timer if Google Translate never activates (e.g. blocked by CSP).
1410    setTimeout(function () {
1411      clearInterval(window.translationCheckInterval);
1412    }, 30000);
1413  }
1414
1415  // Set up direct listener for Google Translate dropdown changes
1416  setupGoogleTranslateDropdownListener();
1417}
1418
1419function createNewEvent(eventName) {
1420  var event;
1421  if (typeof Event === "function") {
1422    event = new Event(eventName);
1423  } else {
1424    event = document.createEvent("Event");
1425    event.initEvent(eventName, true, true);
1426  }
1427  return event;
1428}
1429
1430function detectCurrentGoogleTranslateLanguage() {
1431  // Try to get language from Google Translate dropdown
1432  var dropdown = document.querySelector(".goog-te-combo");
1433  if (dropdown && dropdown.value) {
1434    return dropdown.value;
1435  }
1436
1437  // Fallback: check window.translationConfig.selectedLang
1438  if (window.translationConfig && window.translationConfig.selectedLang) {
1439    return window.translationConfig.selectedLang;
1440  }
1441
1442  // Fallback: check window.modelData.lang
1443  if (window.modelData && window.modelData.lang) {
1444    return window.modelData.lang;
1445  }
1446
1447  // Fallback: check URL query string
1448  var urlParams = new URLSearchParams(window.location.search);
1449  return urlParams.get("lang");
1450}
1451
1452function setupGoogleTranslateDropdownListener() {
1453  // Function to setup listener on the Google Translate dropdown
1454  function attachDropdownListener() {
1455    var dropdown = document.querySelector(".goog-te-combo");
1456    if (dropdown && !dropdown.hasAttribute("cityzen-listener")) {
1457      dropdown.setAttribute("cityzen-listener", "true");
1458      dropdown.addEventListener("change", function () {
1459        var selectedLang = this.value;
1460        if (selectedLang) {
1461          updateSignupWidgetLang(selectedLang);
1462          document.documentElement.setAttribute("lang", selectedLang);
1463        }
1464      });
1465    }
1466  }
1467
1468  // Try to attach listener immediately
1469  attachDropdownListener();
1470
1471  // Set up periodic check for dropdown in case it's created later
1472  var dropdownCheckInterval = setInterval(function () {
1473    var dropdown = document.querySelector(".goog-te-combo");
1474    if (dropdown && !dropdown.hasAttribute("cityzen-listener")) {
1475      attachDropdownListener();
1476      clearInterval(dropdownCheckInterval);
1477    }
1478  }, 1000);
1479
1480  // Clear the interval after 30 seconds to avoid infinite polling
1481  setTimeout(function () {
1482    clearInterval(dropdownCheckInterval);
1483  }, 30000);
1484}
1485
1486// Helper function to replace deprecated DOMSubtreeModified event
1487function setupGoogleTranslateListener(elementId, callbackFn) {
1488  // Find the element to observe
1489  var targetElement = document.getElementById(elementId);
1490  if (!targetElement) {
1491    // If element doesn't exist, create a fallback interval
1492    var checkExistsInterval = setInterval(function () {
1493      targetElement = document.getElementById(elementId);
1494      if (targetElement) {
1495        clearInterval(checkExistsInterval);
1496        setupObserver(targetElement);
1497      }
1498    }, 500);
1499    return;
1500  }
1501
1502  setupObserver(targetElement);
1503
1504  function setupObserver(element) {
1505    // Create observer to watch for content changes
1506    var observer = new MutationObserver(function (mutations) {
1507      // When content changes, execute the callback
1508      observer.disconnect(); // Stop observing to prevent multiple callbacks
1509      if (typeof callbackFn === "function") {
1510        callbackFn();
1511      }
1512      if (window.translationConfig && window.translationConfig.selectedLang) {
1513        updateSignupWidgetLang(window.translationConfig.selectedLang);
1514      }
1515    });
1516
1517    // Start observing the target element for text or child changes
1518    observer.observe(element, {
1519      childList: true,
1520      characterData: true,
1521      subtree: true
1522    });
1523  }
1524}
1525
1526function getUpdatedParamsString(uri, key, value) {
1527  key = encodeURIComponent(key);
1528  value = encodeURIComponent(value);
1529
1530  // kvp looks like ['key1=value1', 'key2=value2', ...]
1531  var kvp = uri.search.substr(1).split("&");
1532  var i = 0;
1533
1534  for (; i < kvp.length; i++) {
1535    if (kvp[i].startsWith(key + "=")) {
1536      var pair = kvp[i].split("=");
1537      pair[1] = value;
1538      kvp[i] = pair.join("=");
1539      break;
1540    }
1541  }
1542
1543  if (i >= kvp.length) {
1544    kvp[kvp.length] = [key, value].join("=");
1545  }
1546
1547  // can return this or...
1548  var params = kvp.join("&");
1549  return params;
1550}
1551
1552function updateSignupWidgetLang(lang) {
1553  document.querySelectorAll("iframe[src*='Signup']").forEach(function (iframe) {
1554    try {
1555      var url = new URL(iframe.src, window.location.origin);
1556      url.searchParams.set("language", lang);
1557      if (iframe.src !== url.href) {
1558        iframe.src = url.href;
1559      }
1560    } catch (e) {
1561      // ignore invalid URLs
1562    }
1563  });
1564}
1565
1566//document.addEventListener('DOMSubtreeModified', function (e) {
1567//    if(e.target.tagName === 'HTML' && window.google) {
1568//        if(e.target.className.match('translated')) {
1569//            // page has been translated
1570//            window.googleTranslateIsRunning = true;
1571//         } else {
1572//            // page has been translated and translation was canceled
1573//            window.googleTranslateIsRunning = false;
1574//        }
1575//   }
1576//}, true);
1577
1578// Google Translate injects its "suggest a translation" voting form into our document.
1579// Those inputs ship with no labels, and because they land in our DOM rather than an
1580// iframe they are reported against us. We cannot change what Google emits, but we can
1581// name the fields once they appear.
1582(function nameGoogleTranslateVotingInputs() {
1583  var LABELS = {
1584    "goog-gt-votingInputSrcLang": "Translation source language",
1585    "goog-gt-votingInputTrgLang": "Translation target language",
1586    "goog-gt-votingInputSrcText": "Original text",
1587    "goog-gt-votingInputTrgText": "Translated text",
1588    "goog-gt-votingInputVote": "Translation feedback"
1589  };
1590  var observer = null;
1591
1592  function apply() {
1593    var missing = 0;
1594    Object.keys(LABELS).forEach(function (id) {
1595      var el = document.getElementById(id);
1596      if (!el) {
1597        missing++;
1598        return;
1599      }
1600      if (!el.getAttribute("aria-label")) {
1601        el.setAttribute("aria-label", LABELS[id]);
1602      }
1603    });
1604    // Google injects these once; stop watching as soon as they are all named.
1605    if (missing === 0 && observer) {
1606      observer.disconnect();
1607      observer = null;
1608    }
1609  }
1610
1611  function start() {
1612    apply();
1613    if (!observer && window.MutationObserver && document.body) {
1614      observer = new MutationObserver(apply);
1615      observer.observe(document.body, { childList: true, subtree: true });
1616    }
1617  }
1618
1619  if (document.readyState === "loading") {
1620    document.addEventListener("DOMContentLoaded", start);
1621  } else {
1622    start();
1623  }
1624})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.