chromium/chrome/browser/resources/lens/overlay/translate_button.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '//resources/cr_elements/cr_button/cr_button.js';

import type {CrButtonElement} from '//resources/cr_elements/cr_button/cr_button.js';
import {assert, assertInstanceof} from '//resources/js/assert.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import type {DomRepeat} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {BrowserProxy} from './browser_proxy.js';
import {BrowserProxyImpl} from './browser_proxy.js';
import {LanguageBrowserProxyImpl} from './language_browser_proxy.js';
import type {LanguageBrowserProxy} from './language_browser_proxy.js';
import {getTemplate} from './translate_button.html.js';

// The language codes that are supported to be translated by the server.
const SUPPORTED_TRANSLATION_LANGUAGES = new Set([
  'af',  'sq',  'am',    'ar',    'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg',
  'ca',  'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'nl', 'en', 'eo',
  'et',  'fi',  'fr',    'fy',    'gl', 'ka', 'de', 'el', 'gu', 'ht', 'ha',
  'haw', 'hi',  'hmn',   'hu',    'is', 'ig', 'id', 'ga', 'it', 'iw', 'ja',
  'jv',  'kn',  'kk',    'km',    'rw', 'ko', 'ku', 'ky', 'lo', 'la', 'lv',
  'lt',  'lb',  'mk',    'mg',    'ms', 'ml', 'mt', 'mi', 'mr', 'mn', 'my',
  'ne',  'no',  'ny',    'or',    'ps', 'fa', 'pl', 'pt', 'pa', 'ro', 'ru',
  'sm',  'gd',  'sr',    'st',    'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es',
  'su',  'sw',  'sv',    'tl',    'tg', 'ta', 'tt', 'te', 'th', 'tr', 'tk',
  'uk',  'ur',  'ug',    'uz',    'vi', 'cy', 'xh', 'yi', 'yo', 'zu',
]);

export interface TranslateState {
  translateModeEnabled: boolean;
  targetLanguage: string;
}

export interface TranslateButtonElement {
  $: {
    languagePicker: HTMLDivElement,
    sourceAutoDetectButton: CrButtonElement,
    sourceLanguageButton: CrButtonElement,
    sourceLanguagePickerContainer: DomRepeat,
    sourceLanguagePickerMenu: HTMLDivElement,
    targetLanguageButton: CrButtonElement,
    targetLanguagePickerContainer: DomRepeat,
    targetLanguagePickerMenu: HTMLDivElement,
    translateButton: CrButtonElement,
  };
}

export class TranslateButtonElement extends PolymerElement {
  static get is() {
    return 'translate-button';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      isTranslateModeEnabled: {
        type: Boolean,
        reflectToAttribute: true,
      },
      shouldShowStarsIcon: {
        type: Boolean,
        computed: 'computeShouldShowStarsIcon(sourceLanguage)',
        reflectToAttribute: true,
      },
      sourceLanguage: Object,
      sourceLanguageMenuVisible: {
        type: Boolean,
        reflectToAttribute: true,
      },
      targetLanguage: Object,
      targetLanguageMenuVisible: {
        type: Boolean,
        reflectToAttribute: true,
      },
    };
  }

  // Whether the translate mode on the lens overlay has been enabled.
  private isTranslateModeEnabled: boolean = false;
  // Whether the stars icon is visible on the source language button.
  private shouldShowStarsIcon: boolean;
  // The currently selected source language to translate to. If null, we should
  // auto detect the language.
  private sourceLanguage: chrome.languageSettingsPrivate.Language|null = null;
  // The currently selected target language to translate to.
  private targetLanguage: chrome.languageSettingsPrivate.Language;
  // Whether the source language menu picker is visible.
  private sourceLanguageMenuVisible: boolean = false;
  // Whether the target language menu picker is visible.
  private targetLanguageMenuVisible: boolean = false;
  // The list of target languages provided by the chrome API.
  private translateLanguageList: chrome.languageSettingsPrivate.Language[];
  // A browser proxy for communicating with the C++ Lens overlay controller.
  private browserProxy: BrowserProxy = BrowserProxyImpl.getInstance();
  // A browser proxy for fetching the language settings from the Chrome API.
  private languageBrowserProxy: LanguageBrowserProxy =
      LanguageBrowserProxyImpl.getInstance();

  override connectedCallback() {
    super.connectedCallback();
    this.languageBrowserProxy.getLanguageList().then(
        this.onLanguageListRetrieved.bind(this));
  }

  private onLanguageListRetrieved(
      languageList: chrome.languageSettingsPrivate.Language[]) {
    this.translateLanguageList = languageList.filter((language) => {
      return SUPPORTED_TRANSLATION_LANGUAGES.has(language.code);
    });

    // After receiving the language list, get the default translate target
    // language. This needs to happen after fetching the language list so we can
    //  use the list to fetch the language's display name.
    this.languageBrowserProxy.getTranslateTargetLanguage().then(
        this.onTargetLanguageRetrieved.bind(this));
  }

  private onTargetLanguageRetrieved(languageCode: string) {
    const defaultLanguage = this.translateLanguageList.find(
        language => language.code === languageCode);
    assert(defaultLanguage);
    this.targetLanguage = defaultLanguage;
  }

  private onAutoDetectMenuItemClick() {
    this.sourceLanguage = null;
    this.hideLanguagePickerMenus();
  }

  private onSourceLanguageButtonClick() {
    this.sourceLanguageMenuVisible = !this.sourceLanguageMenuVisible;
    this.targetLanguageMenuVisible = false;
  }

  private onTargetLanguageButtonClick() {
    this.targetLanguageMenuVisible = !this.targetLanguageMenuVisible;
    this.sourceLanguageMenuVisible = false;
  }

  private onSourceLanguageMenuItemClick(event: PointerEvent) {
    assertInstanceof(event.target, HTMLElement);
    const newSourceLanguage =
        this.$.sourceLanguagePickerContainer.itemForElement(event.target);
    this.sourceLanguage = newSourceLanguage;
    this.hideLanguagePickerMenus();
    this.maybeIssueTranslateRequest();
  }

  private onTargetLanguageMenuItemClick(event: PointerEvent) {
    assertInstanceof(event.target, HTMLElement);
    const newTargetLanguage =
        this.$.targetLanguagePickerContainer.itemForElement(event.target);
    this.targetLanguage = newTargetLanguage;
    this.hideLanguagePickerMenus();
    this.maybeIssueTranslateRequest();
    // Dispatch event to let other components know the overlay translate mode
    // state.
    this.dispatchEvent(new CustomEvent('translate-mode-state-changed', {
      bubbles: true,
      composed: true,
      detail: {
        translateModeEnabled: this.isTranslateModeEnabled,
        targetLanguage: this.targetLanguage.code,
      },
    }));
  }

  private onTranslateButtonClick() {
    // Toggle translate mode on button click.
    this.isTranslateModeEnabled = !this.isTranslateModeEnabled;
    this.maybeIssueTranslateRequest();
    // Dispatch event to let other components know the overlay translate mode
    // state.
    this.dispatchEvent(new CustomEvent('translate-mode-state-changed', {
      bubbles: true,
      composed: true,
      detail: {
        translateModeEnabled: this.isTranslateModeEnabled,
        targetLanguage: this.targetLanguage.code,
      },
    }));
  }

  private maybeIssueTranslateRequest() {
    if (this.isTranslateModeEnabled) {
      this.browserProxy.handler.issueTranslateFullPageRequest(
          this.sourceLanguage ? this.sourceLanguage.code : 'auto',
          this.targetLanguage.code);
    }
  }

  private hideLanguagePickerMenus() {
    this.targetLanguageMenuVisible = false;
    this.sourceLanguageMenuVisible = false;
  }

  private getSourceLanguageDisplayName(): string {
    if (this.sourceLanguage) {
      return this.sourceLanguage.displayName;
    }

    return loadTimeData.getString('autoDetect');
  }

  private computeShouldShowStarsIcon(): boolean {
    return this.sourceLanguage === null;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'translate-button': TranslateButtonElement;
  }
}

customElements.define(TranslateButtonElement.is, TranslateButtonElement);