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.