| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // <if expr="is_ios"> |
| import 'chrome://resources/js/ios/web_ui.js'; |
| // </if> |
| |
| import '/strings.m.js'; |
| import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js'; |
| |
| import {addWebUiListener} from 'chrome://resources/js/cr.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; |
| import {$} from 'chrome://resources/js/util.js'; |
| |
| const detectionLogs = []; |
| |
| /** |
| * Initializes UI and sends a message to the browser for |
| * initialization. |
| */ |
| function initialize() { |
| addMessageHandlers(); |
| const tabbox = document.querySelector('cr-tab-box'); |
| tabbox.hidden = false; |
| chrome.send('requestInfo'); |
| |
| const button = $('detection-logs-dump'); |
| button.addEventListener('click', onDetectionLogsDump); |
| |
| const tabpanelNodeList = document.querySelectorAll('div[slot=\'panel\']'); |
| const tabpanels = Array.prototype.slice.call(tabpanelNodeList, 0); |
| const tabpanelIds = tabpanels.map(function(tab) { |
| return tab.id; |
| }); |
| |
| tabbox.addEventListener('selected-index-change', e => { |
| const tabpanel = tabpanels[e.detail]; |
| const hash = tabpanel.id.match(/(?:^tabpanel-)(.+)/)[1]; |
| window.location.hash = hash; |
| }); |
| |
| const activateTabByHash = function() { |
| let hash = window.location.hash; |
| |
| // Remove the first character '#'. |
| hash = hash.substring(1); |
| |
| const id = 'tabpanel-' + hash; |
| const index = tabpanelIds.indexOf(id); |
| if (index === -1) { |
| return; |
| } |
| tabbox.setAttribute('selected-index', `${index}`); |
| }; |
| |
| window.onhashchange = activateTabByHash; |
| activateTabByHash(); |
| } |
| |
| /* |
| * Creates a button to dismiss an item. |
| * |
| * @param {Function} func Callback called when the button is clicked. |
| */ |
| function createDismissingButton(func) { |
| const button = document.createElement('button'); |
| button.textContent = 'X'; |
| button.classList.add('dismissing'); |
| button.addEventListener('click', function(e) { |
| e.preventDefault(); |
| func(); |
| }, false); |
| return button; |
| } |
| |
| /** |
| * Creates a new LI element with a button to dismiss the item. |
| * |
| * @param {string} text The label of the LI element. |
| * @param {Function} func Callback called when the button is clicked. |
| */ |
| function createLIWithDismissingButton(text, func) { |
| const span = document.createElement('span'); |
| span.textContent = text; |
| |
| const li = document.createElement('li'); |
| li.appendChild(span); |
| li.appendChild(createDismissingButton(func)); |
| return li; |
| } |
| |
| /** |
| * Formats the language name to a human-readable text. For example, if |
| * |langCode| is 'en', this may return 'en (English)'. |
| * |
| * @param {string} langCode ISO 639 language code. |
| * @return {string} The formatted string. |
| */ |
| function formatLanguageCode(langCode) { |
| const key = 'language-' + langCode; |
| if (loadTimeData.valueExists(key)) { |
| const langName = loadTimeData.getString(key); |
| return langCode + ' (' + langName + ')'; |
| } |
| |
| return langCode; |
| } |
| |
| /** |
| * Formats the error type to a human-readable text. |
| * |
| * @param {string} error Translation error type from the browser. |
| * @return {string} The formatted string. |
| */ |
| function formatTranslateErrorsType(error) { |
| // This list is from chrome/common/translate/translate_errors.h. |
| // If this header file is updated, the below list also should be updated. |
| const errorStrs = { |
| 0: 'None', |
| 1: 'Network', |
| 2: 'Initialization Error', |
| 3: 'Unknown Language', |
| 4: 'Unsupported Language', |
| 5: 'Identical Languages', |
| 6: 'Translation Error', |
| 7: 'Translation Timeout', |
| 8: 'Unexpected Script Error', |
| 9: 'Bad Origin', |
| 10: 'Script Load Error', |
| }; |
| |
| if (error < 0 || errorStrs.length <= error) { |
| console.error('Invalid error code:', error); |
| return 'Invalid Error Code'; |
| } |
| return errorStrs[error]; |
| } |
| |
| /** |
| * @return {TrustedHTML|string} Empty TrustedHTML or empty string based on |
| * Trusted Types support. |
| */ |
| function emptyHTML() { |
| if (window.trustedTypes) { |
| return trustedTypes.emptyHTML; |
| } else { |
| return ''; |
| } |
| } |
| |
| /** |
| * Handles the message of 'prefsUpdated' from the browser. |
| * |
| * @param {Object} detail the object which represents pref values. |
| */ |
| function onPrefsUpdated(detail) { |
| let ul; |
| |
| ul = document.querySelector('#prefs-blocked-languages ul'); |
| ul.innerHTML = emptyHTML(); |
| |
| if ('translate_blocked_languages' in detail) { |
| const langs = detail['translate_blocked_languages']; |
| |
| langs.forEach(function(langCode) { |
| const text = formatLanguageCode(langCode); |
| |
| const li = createLIWithDismissingButton(text, function() { |
| chrome.send('removePrefItem', ['blocked_languages', langCode]); |
| }); |
| ul.appendChild(li); |
| }); |
| } |
| |
| ul = document.querySelector('#prefs-site-blocklist ul'); |
| ul.innerHTML = emptyHTML(); |
| |
| if ('translate_site_blocklist' in detail) { |
| const sites = detail['translate_site_blocklist']; |
| |
| sites.forEach(function(site) { |
| const li = createLIWithDismissingButton(site, function() { |
| chrome.send('removePrefItem', ['site_blocklist', site]); |
| }); |
| ul.appendChild(li); |
| }); |
| } |
| |
| ul = document.querySelector('#prefs-allowlists ul'); |
| ul.innerHTML = emptyHTML(); |
| |
| if ('translate_allowlists' in detail) { |
| const pairs = detail['translate_allowlists']; |
| |
| Object.keys(pairs).forEach(function(fromLangCode) { |
| const toLangCode = pairs[fromLangCode]; |
| const text = formatLanguageCode(fromLangCode) + ' \u2192 ' + |
| formatLanguageCode(toLangCode); |
| |
| const li = createLIWithDismissingButton(text, function() { |
| chrome.send('removePrefItem', ['allowlists', fromLangCode, toLangCode]); |
| }); |
| ul.appendChild(li); |
| }); |
| } |
| |
| if ('translate_recent_target' in detail) { |
| const recentTarget = detail['translate_recent_target']; |
| |
| const p = $('recent-override'); |
| |
| p.innerHTML = emptyHTML(); |
| |
| appendTextFieldWithButton(p, recentTarget, function(value) { |
| chrome.send('setRecentTargetLanguage', [value]); |
| }); |
| } |
| |
| const p = document.querySelector('#prefs-dump p'); |
| const content = JSON.stringify(detail, null, 2); |
| p.textContent = content; |
| } |
| |
| /** |
| * Handles the message of 'supportedLanguagesUpdated' from the browser. |
| * |
| * @param {Object} details the object which represents the supported |
| * languages by the Translate server. |
| */ |
| function onSupportedLanguagesUpdated(details) { |
| const span = |
| $('prefs-supported-languages-last-updated').querySelector('span'); |
| span.textContent = formatDate(new Date(details['last_updated'])); |
| |
| const ul = $('prefs-supported-languages-languages'); |
| ul.innerHTML = emptyHTML(); |
| const languages = details['languages']; |
| for (let i = 0; i < languages.length; i++) { |
| const language = languages[i]; |
| const li = document.createElement('li'); |
| |
| const text = formatLanguageCode(language); |
| li.innerText = text; |
| |
| ul.appendChild(li); |
| } |
| } |
| |
| /** |
| * Handles the message of 'countryUpdated' from the browser. |
| * |
| * @param {Object} details the object containing the country |
| * information. |
| */ |
| function onCountryUpdated(details) { |
| const p = $('country-override'); |
| |
| p.innerHTML = emptyHTML(); |
| |
| const country = details['country'] || ''; |
| const h2 = $('override-variations-country'); |
| h2.title = |
| ('Changing this value will override the permanent country stored ' + |
| 'by variations. The overridden country is not automatically ' + |
| 'updated when Chrome is updated and overrides all variations ' + |
| 'country settings. After clicking clear button or the overridden ' + |
| 'country is not set, the text box shows the country used by ' + |
| 'permanent consistency studies. The value that this is overriding ' + |
| 'gets automatically updated with a new value received from the ' + |
| 'variations server when Chrome is updated.'); |
| |
| // Add the button to override the country. Note: The country-override |
| // component is re-created on country update, so there is no issue of this |
| // being called multiple times. |
| appendTextFieldWithButton(p, country, function(value) { |
| chrome.send('overrideCountry', [value]); |
| }); |
| |
| appendClearButton( |
| p, 'overridden' in details && details['overridden'], function() { |
| chrome.send('overrideCountry', ['']); |
| }); |
| |
| if ('update' in details && details['update']) { |
| const div1 = document.createElement('div'); |
| div1.textContent = 'Permanent stored country updated.'; |
| const div2 = document.createElement('div'); |
| div2.textContent = |
| ('You will need to restart your browser ' + |
| 'for the changes to take effect.'); |
| p.appendChild(div1); |
| p.appendChild(div2); |
| } |
| } |
| |
| /** |
| * Adds '0's to |number| as a string. |width| is length of the string |
| * including '0's. |
| * |
| * @param {string} number The number to be converted into a string. |
| * @param {number} width The width of the returned string. |
| * @return {string} The formatted string. |
| */ |
| function padWithZeros(number, width) { |
| const numberStr = number.toString(); |
| const restWidth = width - numberStr.length; |
| if (restWidth <= 0) { |
| return numberStr; |
| } |
| |
| return Array(restWidth + 1).join('0') + numberStr; |
| } |
| |
| /** |
| * Formats |date| as a Date object into a string. The format is like |
| * '2006-01-02 15:04:05'. |
| * |
| * @param {Date} date Date to be formatted. |
| * @return {string} The formatted string. |
| */ |
| function formatDate(date) { |
| const year = date.getFullYear(); |
| const month = date.getMonth() + 1; |
| const day = date.getDate(); |
| const hour = date.getHours(); |
| const minute = date.getMinutes(); |
| const second = date.getSeconds(); |
| |
| const yearStr = padWithZeros(year, 4); |
| const monthStr = padWithZeros(month, 2); |
| const dayStr = padWithZeros(day, 2); |
| const hourStr = padWithZeros(hour, 2); |
| const minuteStr = padWithZeros(minute, 2); |
| const secondStr = padWithZeros(second, 2); |
| |
| const str = yearStr + '-' + monthStr + '-' + dayStr + ' ' + hourStr + ':' + |
| minuteStr + ':' + secondStr; |
| |
| return str; |
| } |
| |
| /** |
| * Appends a new TD element to the specified element. |
| * |
| * @param {string} parent The element to which a new TD element is appended. |
| * @param {string} content The text content of the element. |
| * @param {string} className The class name of the element. |
| */ |
| function appendTD(parent, content, className) { |
| const td = document.createElement('td'); |
| td.textContent = content; |
| td.className = className; |
| parent.appendChild(td); |
| } |
| |
| function appendBooleanTD(parent, value, className) { |
| const td = document.createElement('td'); |
| td.textContent = value; |
| td.className = className; |
| td.bgColor = value ? '#3cba54' : '#db3236'; |
| parent.appendChild(td); |
| } |
| |
| /** |
| * Handles the message of 'languageDetectionInfoAdded' from the |
| * browser. |
| * |
| * @param {Object} detail The object which represents the logs. |
| */ |
| function onLanguageDetectionInfoAdded(detail) { |
| detectionLogs.push(detail); |
| |
| const tr = document.createElement('tr'); |
| |
| // If language detection was skipped, do not populate related details. |
| const hasRunLangDetection = JSON.parse(detail['has_run_lang_detection']); |
| const cldLang = hasRunLangDetection ? |
| formatLanguageCode(detail['model_detected_language']) : |
| 'No page content - language detection skipped'; |
| const modelVersion = |
| hasRunLangDetection ? detail['detection_model_version'] : ''; |
| const reliabilityScore = |
| hasRunLangDetection ? detail['model_reliability_score'].toFixed(2) : ''; |
| const isReliable = hasRunLangDetection ? detail['is_model_reliable'] : ''; |
| |
| appendTD(tr, formatDate(new Date(detail['time'])), 'detection-logs-time'); |
| appendTD(tr, detail['url'], 'detection-logs-url'); |
| appendTD( |
| tr, formatLanguageCode(detail['content_language']), |
| 'detection-logs-content-language'); |
| appendTD( |
| tr, formatLanguageCode(detail['html_root_language']), |
| 'detection-logs-html-root-language'); |
| appendTD(tr, cldLang, 'detection-logs-cld-language'); |
| appendTD(tr, modelVersion, 'detection-logs-detection-model-version'); |
| appendTD(tr, reliabilityScore, 'detection-logs-model-reliability'); |
| appendTD(tr, isReliable, 'detection-logs-is-cld-reliable'); |
| appendTD(tr, detail['has_notranslate'], 'detection-logs-has-notranslate'); |
| appendTD( |
| tr, formatLanguageCode(detail['adopted_language']), |
| 'detection-logs-adopted-language'); |
| appendTD(tr, formatLanguageCode(detail['content']), 'detection-logs-content'); |
| |
| // TD (and TR) can't use the CSS property 'max-height', so DIV |
| // in the content is needed. |
| const contentTD = tr.querySelector('.detection-logs-content'); |
| const div = document.createElement('div'); |
| div.textContent = contentTD.textContent; |
| contentTD.textContent = ''; |
| contentTD.appendChild(div); |
| |
| const tabpanel = $('tabpanel-detection-logs'); |
| const tbody = tabpanel.getElementsByTagName('tbody')[0]; |
| tbody.appendChild(tr); |
| } |
| |
| /** |
| * Handles the message of 'translateErrorDetailsAdded' from the |
| * browser. |
| * |
| * @param {Object} details The object which represents the logs. |
| */ |
| function onTranslateErrorDetailsAdded(details) { |
| const tr = document.createElement('tr'); |
| |
| appendTD(tr, formatDate(new Date(details['time'])), 'error-logs-time'); |
| appendTD(tr, details['url'], 'error-logs-url'); |
| appendTD( |
| tr, details['error'] + ': ' + formatTranslateErrorsType(details['error']), |
| 'error-logs-error'); |
| |
| const tabpanel = $('tabpanel-error-logs'); |
| const tbody = tabpanel.getElementsByTagName('tbody')[0]; |
| tbody.appendChild(tr); |
| } |
| |
| /** |
| * Handles the message of 'translateInitDetailsAdded' from the |
| * browser. |
| * |
| * @param {Object} details The object which represents the logs. |
| */ |
| function onTranslateInitDetailsAdded(details) { |
| const tr = document.createElement('tr'); |
| |
| appendTD(tr, formatDate(new Date(details['time'])), 'init-logs-time'); |
| appendTD(tr, details['url'], 'init-logs-url'); |
| |
| appendTD(tr, details['page_language_code'], 'init-logs-page-language-code'); |
| appendTD(tr, details['target_lang'], 'init-logs-target-lang'); |
| |
| appendBooleanTD(tr, details['ui_shown'], 'init-logs-ui-shown'); |
| |
| appendBooleanTD( |
| tr, details['can_auto_translate'], 'init-logs-can-auto-translate'); |
| appendBooleanTD(tr, details['can_show_ui'], 'init-logs-can-show-ui'); |
| appendBooleanTD( |
| tr, details['can_auto_href_translate'], |
| 'init-logs-can-auto-href-translate'); |
| appendBooleanTD( |
| tr, details['can_show_href_translate_ui'], |
| 'init-logs-can-show-href-translate-ui'); |
| appendBooleanTD( |
| tr, details['can_show_predefined_language_translate_ui'], |
| 'init-logs-can-show-predefined-language-translate-ui'); |
| appendBooleanTD( |
| tr, details['should_suppress_from_ranker'], |
| 'init-logs-should-suppress-from-ranker'); |
| appendBooleanTD( |
| tr, details['is_triggering_possible'], |
| 'init-logs-is-triggering-possible'); |
| appendBooleanTD( |
| tr, details['should_auto_translate'], 'init-logs-should-auto-translate'); |
| appendBooleanTD(tr, details['should_show_ui'], 'init-logs-should-show-ui'); |
| |
| appendTD( |
| tr, details['auto_translate_target'], 'init-logs-auto-translate-target'); |
| appendTD( |
| tr, details['href_translate_target'], 'init-logs-href-translate-target'); |
| appendTD( |
| tr, details['predefined_translate_target'], |
| 'init-logs-predefined-translate-target'); |
| |
| const tabpanel = $('tabpanel-init-logs'); |
| const tbody = tabpanel.getElementsByTagName('tbody')[0]; |
| tbody.appendChild(tr); |
| } |
| |
| |
| /** |
| * Handles the message of 'translateEventDetailsAdded' from the browser. |
| * |
| * @param {Object} details The object which contains event information. |
| */ |
| function onTranslateEventDetailsAdded(details) { |
| const tr = document.createElement('tr'); |
| appendTD(tr, formatDate(new Date(details['time'])), 'event-logs-time'); |
| appendTD( |
| tr, details['filename'] + ': ' + details['line'], 'event-logs-place'); |
| appendTD(tr, details['message'], 'event-logs-message'); |
| |
| const tbody = $('tabpanel-event-logs').getElementsByTagName('tbody')[0]; |
| tbody.appendChild(tr); |
| } |
| |
| /** |
| * Appends an <input type="text" /> and a button to `elt`, and sets the value |
| * of the <input> to `value`. When the button is clicked, |
| * `buttonClickCallback` is called with the value of the <input> field. |
| * |
| * @param {HTMLElement} elt Container element to append to. |
| * @param {string} value Initial value of the <input> element. |
| * @param {Function} buttonClickCallback Function to call when the button is |
| * clicked. |
| */ |
| function appendTextFieldWithButton(elt, value, buttonClickCallback) { |
| const input = document.createElement('input'); |
| input.type = 'text'; |
| input.value = value; |
| |
| const button = document.createElement('button'); |
| button.textContent = 'update'; |
| button.addEventListener('click', function() { |
| buttonClickCallback(input.value); |
| }, false); |
| |
| elt.appendChild(input); |
| elt.appendChild(document.createElement('br')); |
| elt.appendChild(button); |
| } |
| |
| /** |
| * Appends a clear button to `elt`. When the button is clicked, |
| * `clearCallback` is called. |
| * |
| * @param {HTMLElement} elt Container element to append to. |
| * @param {boolean} enabled Whether the button is clickable. |
| * @param {Function} clearCallback Function to call when the button is |
| * clicked. |
| */ |
| function appendClearButton(elt, enabled, clearCallback) { |
| const button = document.createElement('button'); |
| button.textContent = 'clear'; |
| button.style.marginLeft = '10px'; |
| button.disabled = !enabled; |
| if (enabled) { |
| button.addEventListener('click', clearCallback, false); |
| } else { |
| // Used for tooltip when the button is unclickable. |
| button.title = 'No pref values to clear.'; |
| } |
| |
| elt.appendChild(button); |
| } |
| |
| function addMessageHandlers() { |
| addWebUiListener('languageDetectionInfoAdded', onLanguageDetectionInfoAdded); |
| addWebUiListener('prefsUpdated', onPrefsUpdated); |
| addWebUiListener('supportedLanguagesUpdated', onSupportedLanguagesUpdated); |
| addWebUiListener('countryUpdated', onCountryUpdated); |
| addWebUiListener('translateErrorDetailsAdded', onTranslateErrorDetailsAdded); |
| addWebUiListener('translateEventDetailsAdded', onTranslateEventDetailsAdded); |
| addWebUiListener('translateInitDetailsAdded', onTranslateInitDetailsAdded); |
| } |
| |
| /** |
| * The callback of button#detetion-logs-dump. |
| */ |
| function onDetectionLogsDump() { |
| const data = JSON.stringify(detectionLogs); |
| const blob = new Blob([data], {'type': 'text/json'}); |
| const url = URL.createObjectURL(blob); |
| const filename = 'translate_internals_detect_logs_dump.json'; |
| |
| const a = document.createElement('a'); |
| a.setAttribute('href', url); |
| a.setAttribute('download', filename); |
| |
| const event = document.createEvent('MouseEvent'); |
| event.initMouseEvent( |
| 'click', true, true, window, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null); |
| a.dispatchEvent(event); |
| } |
| |
| document.addEventListener('DOMContentLoaded', initialize); |