blob: fac52dcf9043476eb128a4bd6c7bf5b044336b02 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/translate/core/browser/translate_manager.h"
#include <map>
#include <memory>
#include <string_view>
#include <tuple>
#include <utility>
#include "base/command_line.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/language/core/browser/language_model.h"
#include "components/language/core/common/language_experiments.h"
#include "components/language/core/common/language_util.h"
#include "components/language/core/common/locale_util.h"
#include "components/language_detection/core/constants.h"
#include "components/prefs/pref_service.h"
#include "components/translate/core/browser/language_state.h"
#include "components/translate/core/browser/page_translated_details.h"
#include "components/translate/core/browser/translate_browser_metrics.h"
#include "components/translate/core/browser/translate_client.h"
#include "components/translate/core/browser/translate_download_manager.h"
#include "components/translate/core/browser/translate_driver.h"
#include "components/translate/core/browser/translate_error_details.h"
#include "components/translate/core/browser/translate_init_details.h"
#include "components/translate/core/browser/translate_language_list.h"
#include "components/translate/core/browser/translate_metrics_logger.h"
#include "components/translate/core/browser/translate_metrics_logger_impl.h"
#include "components/translate/core/browser/translate_prefs.h"
#include "components/translate/core/browser/translate_ranker.h"
#include "components/translate/core/browser/translate_script.h"
#include "components/translate/core/browser/translate_trigger_decision.h"
#include "components/translate/core/browser/translate_url_util.h"
#include "components/translate/core/common/language_detection_details.h"
#include "components/translate/core/common/translate_switches.h"
#include "components/translate/core/common/translate_util.h"
#include "components/variations/variations_associated_data.h"
#include "google_apis/google_api_keys.h"
#include "net/base/mime_util.h"
#include "net/base/network_change_notifier.h"
#include "net/base/url_util.h"
#include "net/http/http_status_code.h"
#include "third_party/metrics_proto/translate_event.pb.h"
namespace translate {
namespace {
// Callbacks for translate errors.
TranslateManager::TranslateErrorCallbackList* g_error_callback_list_ = nullptr;
// Callbacks for translate initializations.
TranslateManager::TranslateInitCallbackList* g_init_callback_list_ = nullptr;
// Callbacks for language detection.
TranslateManager::LanguageDetectedCallbackList* g_detection_callback_list_ =
nullptr;
} // namespace
TranslateManager::~TranslateManager() = default;
// static
base::CallbackListSubscription TranslateManager::RegisterTranslateErrorCallback(
const TranslateManager::TranslateErrorCallback& callback) {
if (!g_error_callback_list_)
g_error_callback_list_ = new TranslateErrorCallbackList;
return g_error_callback_list_->Add(callback);
}
// static
base::CallbackListSubscription TranslateManager::RegisterTranslateInitCallback(
const TranslateManager::TranslateInitCallback& callback) {
if (!g_init_callback_list_)
g_init_callback_list_ = new TranslateInitCallbackList;
return g_init_callback_list_->Add(callback);
}
// static
base::CallbackListSubscription
TranslateManager::RegisterLanguageDetectedCallback(
const TranslateManager::LanguageDetectedCallback& callback) {
if (!g_detection_callback_list_)
g_detection_callback_list_ = new LanguageDetectedCallbackList;
return g_detection_callback_list_->Add(callback);
}
TranslateManager::TranslateManager(TranslateClient* translate_client,
TranslateRanker* translate_ranker,
language::LanguageModel* language_model)
: page_seq_no_(0),
translate_client_(translate_client),
translate_driver_(translate_client_->GetTranslateDriver()),
translate_ranker_(translate_ranker),
language_model_(language_model),
null_translate_metrics_logger_(
std::make_unique<NullTranslateMetricsLogger>()),
language_state_(translate_driver_),
translate_event_(std::make_unique<metrics::TranslateEventProto>()) {}
base::WeakPtr<TranslateManager> TranslateManager::GetWeakPtr() {
return weak_method_factory_.GetWeakPtr();
}
void TranslateManager::InitiateTranslation(const std::string& page_lang) {
std::unique_ptr<TranslatePrefs> translate_prefs(
translate_client_->GetTranslatePrefs());
std::string page_language_code =
TranslateDownloadManager::GetLanguageCode(page_lang);
TranslateBrowserMetrics::TargetLanguageOrigin target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kUninitialized;
std::string target_lang =
GetTargetLanguage(translate_prefs.get(), language_model_,
page_language_code, target_language_origin);
// TODO(crbug.com/40610937): The ranker event shouldn't be a global on this
// object. It should instead be passed around to code that uses it.
InitTranslateEvent(page_language_code, target_lang, *translate_prefs);
// Logs the initial source and target languages, as well as whether the
// initial source language is blocked (i.e. on the never translate list).
GetActiveTranslateMetricsLogger()->LogInitialSourceLanguage(
page_language_code,
translate_prefs->IsBlockedLanguage(page_language_code));
GetActiveTranslateMetricsLogger()->LogTargetLanguage(target_lang,
target_language_origin);
const TranslateTriggerDecision& decision = ComputePossibleOutcomes(
translate_prefs.get(), page_language_code, target_lang);
bool ui_shown = MaterializeDecision(decision, translate_prefs.get(),
page_language_code, target_lang);
MaybeShowOmniboxIcon(decision);
NotifyTranslateInit(page_language_code, target_lang, decision, ui_shown);
RecordDecisionMetrics(decision, page_language_code, ui_shown);
RecordDecisionRankerEvent(decision, translate_prefs.get(), page_language_code,
target_lang);
// Mark the current state as the initial state now that we are done
// initializing Translate.
GetActiveTranslateMetricsLogger()->LogInitialState();
}
bool TranslateManager::CanManuallyTranslate(bool menuLogging) {
bool can_translate = true;
if (net::NetworkChangeNotifier::IsOffline()) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kNetworkOffline);
can_translate = false;
}
if (!ignore_missing_key_for_testing_ &&
!::google_apis::HasAPIKeyConfigured()) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kApiKeysMissing);
can_translate = false;
}
// not supported MIME type pages currently cannot be translated.
// See bug: 217945, 1208340.
if (!IsMimeTypeSupported(translate_driver_->GetContentsMimeType())) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kMIMETypeUnsupported);
can_translate = false;
}
if (!translate_client_->IsTranslatableURL(
translate_driver_->GetVisibleURL())) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kURLNotTranslatable);
can_translate = false;
}
const std::string source_language = language_state_.source_language();
// The source language is empty when language detection has not finished
// running. In this case, Android queues the translation and waits until the
// source language has been determined. Android is the only platform that
// supports manual translation in this case.
#if !BUILDFLAG(IS_ANDROID)
if (source_language.empty()) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kSourceLangUnknown);
can_translate = false;
}
#endif
std::unique_ptr<TranslatePrefs> translate_prefs(
translate_client_->GetTranslatePrefs());
if (!translate_prefs->IsTranslateAllowedByPolicy()) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kNotAllowedByPolicy);
can_translate = false;
}
const std::string target_lang = GetTargetLanguage(
translate_prefs.get(), language_model_,
TranslateDownloadManager::GetLanguageCode(source_language));
if (target_lang.empty()) {
if (!menuLogging)
return false;
TranslateBrowserMetrics::ReportMenuTranslationUnavailableReason(
TranslateBrowserMetrics::MenuTranslationUnavailableReason::
kTargetLangUnknown);
can_translate = false;
}
if (menuLogging)
UMA_HISTOGRAM_BOOLEAN("Translate.MenuTranslation.IsAvailable",
can_translate);
return can_translate;
}
bool TranslateManager::CanPartiallyTranslateTargetLanguage() {
std::unique_ptr<TranslatePrefs> translate_prefs(
translate_client_->GetTranslatePrefs());
const std::string& source_language = language_state_.source_language();
const std::string target_lang = GetTargetLanguage(
translate_prefs.get(), language_model_,
TranslateDownloadManager::GetLanguageCode(source_language));
if (target_lang.empty()) {
return false;
}
return TranslateLanguageList::IsSupportedPartialTranslateLanguage(
target_lang);
}
bool TranslateManager::IsMimeTypeSupported(const std::string& mime_type) {
if (net::MatchesMimeType("image/*", mime_type))
return false;
if (mime_type == "multipart/related")
return false;
return true;
}
void TranslateManager::ShowTranslateUI(std::optional<std::string> source_code,
std::optional<std::string> target_code,
bool auto_translate,
bool triggered_from_menu) {
// If a translation is in progress, do nothing.
if (language_state_.translation_pending()) {
return;
}
std::unique_ptr<TranslatePrefs> translate_prefs(
translate_client_->GetTranslatePrefs());
// Get language codes.
std::string converted_source_code =
source_code.has_value() ? source_code.value()
: TranslateDownloadManager::GetLanguageCode(
language_state_.source_language());
language::ToTranslateLanguageSynonym(&converted_source_code);
const std::string converted_target_code =
target_code.has_value()
? target_code.value()
: GetTargetLanguageForDisplay(translate_prefs.get(), language_model_);
bool is_translated =
language_state_.IsPageTranslated() &&
converted_target_code == language_state_.current_language();
language_state_.SetTranslateEnabled(true);
const TranslateStep step = is_translated ? TRANSLATE_STEP_AFTER_TRANSLATE
: TRANSLATE_STEP_BEFORE_TRANSLATE;
// Translate the page if it has not been translated and manual translate
// should trigger translation automatically. Otherwise, only show the infobar.
if (auto_translate && !is_translated) {
TranslatePage(
converted_source_code, converted_target_code, triggered_from_menu,
GetActiveTranslateMetricsLogger()->GetNextManualTranslationType(
triggered_from_menu));
return;
}
translate_client_->ShowTranslateUI(
step, converted_source_code, converted_target_code, TranslateErrors::NONE,
triggered_from_menu);
}
void TranslateManager::ShowTranslateUI(bool auto_translate,
bool triggered_from_menu) {
ShowTranslateUI(std::nullopt, std::nullopt, auto_translate,
triggered_from_menu);
}
void TranslateManager::TranslatePage(const std::string& original_source_lang,
const std::string& target_lang,
bool triggered_from_menu,
TranslationType translation_type) {
const GURL& page_url = translate_driver_->GetVisibleURL();
language_state_.SetTranslationType(translation_type);
// TODO(crbug.com/40898004): Very rarely, users can reach a state where this
// Translate code is called on a page ineligible for translation. It is
// unclear how this state is reached, but the crash rate is very low
// (<0.1CPM) and this state is not breaking for the user. This crash rate
// should be monitored and investigated if it increases.
if (!translate_client_->IsTranslatableURL(page_url)) {
base::debug::DumpWithoutCrashing();
return;
}
translate_driver_->PrepareToTranslatePage(page_seq_no_, original_source_lang,
target_lang, triggered_from_menu);
// If the source language matches the UI language, it means the translation
// prompt is being forced by an experiment. Report this so the count of how
// often it happens can be decremented (meaning the user didn't decline or
// ignore the prompt).
if (original_source_lang ==
TranslateDownloadManager::GetLanguageCode(
TranslateDownloadManager::GetInstance()->application_locale())) {
translate_client_->GetTranslatePrefs()
->ReportAcceptedAfterForceTriggerOnEnglishPages();
}
// If the target language isn't in the chrome://settings/languages list, add
// it there. This way, it's obvious to the user that Chrome is remembering
// their choice, they can remove it from the list, and they'll send that
// language in the Accept-Language header, giving servers a chance to serve
// them pages in that language.
AddTargetLanguageToAcceptLanguages(target_lang);
// Translation can be kicked by context menu against unsupported languages.
// Unsupported language strings should be replaced with
// kUnknownLanguageCode in order to send a translation request with enabling
// server side auto language detection.
std::string source_lang(original_source_lang);
if (!TranslateDownloadManager::IsSupportedLanguage(source_lang))
source_lang = std::string(language_detection::kUnknownLanguageCode);
// Capture the translate event if we were triggered from the menu.
if (triggered_from_menu) {
RecordTranslateEvent(
metrics::TranslateEventProto::USER_CONTEXT_MENU_TRANSLATE);
}
if (source_lang == target_lang) {
// If the languages are the same, try the translation using the unknown
// language code. The source and target languages should only be equal if
// the translation was manually triggered by the user. Rather than show
// them the error, we should attempt to send the page for translation. For
// page with multiple languages we often detect same language, but the
// Translation service is able to translate the various languages using it's
// own language detection.
source_lang = language_detection::kUnknownLanguageCode;
}
// Trigger the "translating now" UI.
translate_client_->ShowTranslateUI(
translate::TRANSLATE_STEP_TRANSLATING, source_lang, target_lang,
TranslateErrors::NONE, triggered_from_menu);
GetActiveTranslateMetricsLogger()->LogTranslationStarted(translation_type);
TranslateScript* script = TranslateDownloadManager::GetInstance()->script();
DCHECK(script != nullptr);
const std::string& script_data = script->data();
if (!script_data.empty()) {
DoTranslatePage(script_data, source_lang, target_lang);
return;
}
// The script is not available yet. Queue that request and query for the
// script. Once it is downloaded we'll do the translate.
TranslateScript::RequestCallback callback =
base::BindOnce(&TranslateManager::OnTranslateScriptFetchComplete,
GetWeakPtr(), source_lang, target_lang);
script->Request(std::move(callback), translate_driver_->IsIncognito());
}
void TranslateManager::RevertTranslation() {
// Do nothing if the page is not translated.
if (!GetLanguageState()->IsPageTranslated()) {
return;
}
// Capture the revert event in the translate metrics
RecordTranslateEvent(metrics::TranslateEventProto::USER_REVERT);
// Revert the translation.
translate_driver_->RevertTranslation(page_seq_no_);
language_state_.SetCurrentLanguage(language_state_.source_language());
GetActiveTranslateMetricsLogger()->LogReversion();
}
void TranslateManager::DoTranslatePage(const std::string& translate_script,
const std::string& source_lang,
const std::string& target_lang) {
language_state_.set_translation_pending(true);
translate_driver_->TranslatePage(page_seq_no_, translate_script, source_lang,
target_lang);
}
// Notifies |g_error_callback_list_| of translate errors.
void TranslateManager::NotifyTranslateError(TranslateErrors error_type) {
if (!g_error_callback_list_ || error_type == TranslateErrors::NONE ||
translate_driver_->IsIncognito()) {
return;
}
TranslateErrorDetails error_details;
error_details.time = base::Time::Now();
error_details.url = translate_driver_->GetLastCommittedURL();
error_details.error = error_type;
g_error_callback_list_->Notify(error_details);
}
void TranslateManager::NotifyTranslateInit(std::string page_language_code,
std::string target_lang,
TranslateTriggerDecision decision,
bool ui_shown) {
if (!g_init_callback_list_ || translate_driver_->IsIncognito())
return;
TranslateInitDetails details;
details.time = base::Time::Now();
details.url = translate_driver_->GetLastCommittedURL();
details.page_language_code = page_language_code;
details.target_lang = target_lang;
details.decision = decision;
details.ui_shown = ui_shown;
g_init_callback_list_->Notify(details);
}
void TranslateManager::NotifyLanguageDetected(
const translate::LanguageDetectionDetails& details) {
if (!g_detection_callback_list_)
return;
g_detection_callback_list_->Notify(details);
}
void TranslateManager::PageTranslated(const std::string& source_lang,
const std::string& target_lang,
TranslateErrors error_type) {
if (error_type == TranslateErrors::NONE) {
// The user could have updated the source language before translating, so
// update the language state with both source and current.
language_state_.SetSourceLanguage(source_lang);
language_state_.SetCurrentLanguage(target_lang);
}
language_state_.set_translation_pending(false);
language_state_.set_translation_error(error_type != TranslateErrors::NONE);
if ((error_type == TranslateErrors::NONE) &&
source_lang != language_detection::kUnknownLanguageCode &&
!TranslateDownloadManager::IsSupportedLanguage(source_lang)) {
error_type = TranslateErrors::UNSUPPORTED_LANGUAGE;
}
// Currently we only want to log any error happens during the translation
// script initialization phase such as translation script failed because of
// CSP issues (crbug.com/738277).
// Note: NotifyTranslateError and ShowTranslateUI will not log the errors.
if (error_type == TranslateErrors::INITIALIZATION_ERROR)
RecordTranslateEvent(metrics::TranslateEventProto::INITIALIZATION_ERROR);
translate_client_->ShowTranslateUI(translate::TRANSLATE_STEP_AFTER_TRANSLATE,
source_lang, target_lang, error_type,
false);
language_state_.SetTranslationType(TranslationType::kUninitialized);
NotifyTranslateError(error_type);
GetActiveTranslateMetricsLogger()->LogTranslationFinished(
error_type == TranslateErrors::NONE, error_type);
}
void TranslateManager::OnTranslateScriptFetchComplete(
const std::string& source_lang,
const std::string& target_lang,
bool success) {
if (!translate_driver_->HasCurrentPage())
return;
if (success) {
// Translate the page.
TranslateScript* translate_script =
TranslateDownloadManager::GetInstance()->script();
DCHECK(translate_script);
DoTranslatePage(translate_script->data(), source_lang, target_lang);
} else {
translate_client_->ShowTranslateUI(
translate::TRANSLATE_STEP_TRANSLATE_ERROR, source_lang, target_lang,
TranslateErrors::NETWORK, false);
NotifyTranslateError(TranslateErrors::NETWORK);
GetActiveTranslateMetricsLogger()->LogTranslationFinished(
false, TranslateErrors::NETWORK);
}
}
std::string TranslateManager::GetTargetLanguageForDisplay(
TranslatePrefs* prefs,
language::LanguageModel* language_model) {
// If the page is translated, always return the current target language. This
// ensures that reshowing the UI on a translated page maintains the correct
// target language that the page is currently translated into.
if (language_state_.IsPageTranslated())
return language_state_.current_language();
return GetTargetLanguage(prefs, language_model,
TranslateDownloadManager::GetLanguageCode(
language_state_.source_language()));
}
// static
std::string TranslateManager::GetTargetLanguage(
TranslatePrefs* prefs,
language::LanguageModel* language_model,
const std::string source_lang_code,
TranslateBrowserMetrics::TargetLanguageOrigin& target_language_origin) {
DCHECK(prefs);
// For callers with source language we want to check their auto translate
// preference.
// We don't enable auto translate feature in incognito profiles so this will
// always be empty for incognito.
if (source_lang_code != language_detection::kUnknownLanguageCode) {
std::string auto_translate_language =
translate::TranslateManager::GetAutoTargetLanguage(source_lang_code,
prefs);
if (!auto_translate_language.empty() &&
TranslateDownloadManager::IsSupportedLanguage(
auto_translate_language)) {
target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kAutoTranslate;
return auto_translate_language;
}
}
const std::string& recent_target = prefs->GetRecentTargetLanguage();
// If we've recorded the most recent target language, use that.
if (base::FeatureList::IsEnabled(kTranslateRecentTarget) &&
!recent_target.empty() &&
TranslateDownloadManager::IsSupportedLanguage(recent_target)) {
target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kRecentTarget;
return recent_target;
}
if (language_model) {
std::vector<std::string> language_codes;
for (const auto& lang : language_model->GetLanguages()) {
std::string lang_code =
TranslateDownloadManager::GetLanguageCode(lang.lang_code);
language::ToTranslateLanguageSynonym(&lang_code);
if (TranslateDownloadManager::IsSupportedLanguage(lang_code))
language_codes.push_back(lang_code);
}
// If forcing triggering on English, skip English as the target language if
// possible by moving it to the end of |language_codes|.
if (prefs->ShouldForceTriggerTranslateOnEnglishPages() &&
source_lang_code == "en") {
std::stable_partition(
language_codes.begin(), language_codes.end(),
[&](const auto& lang) { return lang.compare("en") != 0; });
}
// Use the first language from the model that translate supports.
if (!language_codes.empty()) {
target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kLanguageModel;
return language_codes[0];
}
}
// Get the browser's user interface language.
std::string ui_language = TranslateDownloadManager::GetLanguageCode(
TranslateDownloadManager::GetInstance()->application_locale());
// Map 'he', 'nb', 'fil' back to 'iw', 'no', 'tl'
language::ToTranslateLanguageSynonym(&ui_language);
if (TranslateDownloadManager::IsSupportedLanguage(ui_language)) {
target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kApplicationUI;
return ui_language;
}
// Get the first supported language on the Accept Languages list.
std::vector<std::string> accept_languages_list;
prefs->GetLanguageList(&accept_languages_list);
for (const auto& lang : accept_languages_list) {
std::string lang_code = TranslateDownloadManager::GetLanguageCode(lang);
if (TranslateDownloadManager::IsSupportedLanguage(lang_code)) {
target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kAcceptLanguages;
return lang_code;
}
}
// If there isn't a target language determined by the above logic, default to
// English. Otherwise the user can get stuck not being able to translate. See
// https://crbug.com/1041387.
target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kDefaultEnglish;
return std::string("en");
}
// static
std::string TranslateManager::GetTargetLanguage(
TranslatePrefs* prefs,
language::LanguageModel* language_model,
const std::string source_lang_code) {
TranslateBrowserMetrics::TargetLanguageOrigin target_language_origin =
TranslateBrowserMetrics::TargetLanguageOrigin::kUninitialized;
return GetTargetLanguage(prefs, language_model, source_lang_code,
target_language_origin);
}
// static
std::string TranslateManager::GetAutoTargetLanguage(
const std::string& source_language,
TranslatePrefs* translate_prefs) {
std::string auto_target_lang;
if (translate_prefs->ShouldAutoTranslate(source_language,
&auto_target_lang)) {
// We need to confirm that the saved target language is still supported.
// Also, GetLanguageCode will take care of removing country code if any.
auto_target_lang =
TranslateDownloadManager::GetLanguageCode(auto_target_lang);
if (TranslateDownloadManager::IsSupportedLanguage(auto_target_lang))
return auto_target_lang;
}
return std::string();
}
LanguageState* TranslateManager::GetLanguageState() {
return &language_state_;
}
bool TranslateManager::ignore_missing_key_for_testing_ = false;
// static
void TranslateManager::SetIgnoreMissingKeyForTesting(bool ignore) {
ignore_missing_key_for_testing_ = ignore;
}
// static
bool TranslateManager::IsAvailable(const TranslatePrefs* prefs) {
// These conditions mirror the conditions in InitiateTranslation.
return (ignore_missing_key_for_testing_ ||
::google_apis::HasAPIKeyConfigured()) &&
prefs->IsOfferTranslateEnabled();
}
void TranslateManager::InitTranslateEvent(const std::string& src_lang,
const std::string& dst_lang,
const TranslatePrefs& prefs) {
translate_event_->Clear();
translate_event_->set_source_language(src_lang);
translate_event_->set_target_language(dst_lang);
translate_event_->set_country(prefs.GetCountry());
translate_event_->set_accept_count(
prefs.GetTranslationAcceptedCount(src_lang));
translate_event_->set_decline_count(
prefs.GetTranslationDeniedCount(src_lang));
translate_event_->set_ignore_count(
prefs.GetTranslationIgnoredCount(src_lang));
translate_event_->set_ranker_response(
metrics::TranslateEventProto::NOT_QUERIED);
translate_event_->set_event_type(metrics::TranslateEventProto::UNKNOWN);
// TODO(rogerm): Populate the language list.
}
void TranslateManager::RecordTranslateEvent(int event_type) {
translate_ranker_->RecordTranslateEvent(
event_type, translate_driver_->GetUkmSourceId(), translate_event_.get());
}
bool TranslateManager::ShouldOverrideMatchesPreviousLanguageDecision() {
return translate_ranker_->ShouldOverrideMatchesPreviousLanguageDecision(
translate_driver_->GetUkmSourceId(), translate_event_.get());
}
bool TranslateManager::ShouldSuppressBubbleUI(
const std::string& target_language) {
// Suppress the UI if the user navigates to a page with the same language as
// the previous page, unless the page was loaded from a link click with
// hrefTranslate attached that matches the target language, since in that case
// the site might have a good reason to show the translate UI regardless of
// the source language of the previous page. In the new UI, continue offering
// translation after the user navigates to another page.
DCHECK(!target_language.empty());
if (language_state_.href_translate() == target_language ||
language_state_.HasLanguageChanged() ||
ShouldOverrideMatchesPreviousLanguageDecision()) {
return false;
}
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledMatchesPreviousLanguage);
return true;
}
void TranslateManager::AddTargetLanguageToAcceptLanguages(
const std::string& target_language_code) {
auto prefs = translate_client_->GetTranslatePrefs();
std::vector<std::string> languages;
prefs->GetLanguageList(&languages);
std::string_view target_language, tail;
// |target_language_code| should satisfy BCP47 and consist of a language code
// and an optional region code joined by an hyphen.
std::tie(target_language, tail) =
language::SplitIntoMainAndTail(target_language_code);
// Don't add the target language if it's redundant with another already in the
// list.
if (tail.empty()) {
for (const auto& language : languages) {
if (language::ExtractBaseLanguage(language) == target_language)
return;
}
} else {
for (const auto& language : languages) {
if (language == target_language_code)
return;
}
}
// Only add the target language if it's not an automatic target (such as when
// translation happens because of an hrefTranslate navigation).
if (language_state_.AutoTranslateTo() != target_language_code &&
language_state_.href_translate() != target_language_code) {
prefs->AddToLanguageList(target_language_code, /*force_blocked=*/false);
}
}
const TranslateTriggerDecision TranslateManager::ComputePossibleOutcomes(
TranslatePrefs* translate_prefs,
const std::string& page_language_code,
const std::string& target_lang) {
// This function looks at a bunch of signals and determines which of three
// outcomes should be selected:
// 1. Auto-translate the page
// 2. Show translate UI
// 3. Do nothing
// This is achieved by passing the |decision| object to the different Filter*
// functions, which will mark certain outcomes as undesirable. This |decision|
// object is then used to trigger the correct behavior, and finally record
// corresponding metrics in InitiateTranslation.
TranslateTriggerDecision decision;
FilterIsTranslatePossible(&decision, translate_prefs, page_language_code,
target_lang);
// Querying the ranker now, but not exiting immediately so that we may log
// other potential suppression reasons.
if (!translate_ranker_->ShouldOfferTranslation(
translate_event_.get(), GetActiveTranslateMetricsLogger())) {
decision.SuppressFromRanker();
}
FilterForUserPrefs(&decision, translate_prefs, page_language_code);
if (decision.should_suppress_from_ranker()) {
// Delay logging this until after FilterForUserPrefs because TriggerDecision
// values from FilterForUserPrefs have higher priority.
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledByRanker);
}
FilterAutoTranslate(&decision, translate_prefs, page_language_code);
FilterForHrefTranslate(&decision, translate_prefs, page_language_code);
FilterForPredefinedTarget(&decision, translate_prefs, page_language_code);
return decision;
}
void TranslateManager::FilterIsTranslatePossible(
TranslateTriggerDecision* decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code,
const std::string& target_lang) {
// Short-circuit out if not in a state where initiating translation makes
// sense (this method may be called multiple times for a given page).
if (!language_state_.page_level_translation_criteria_met() ||
language_state_.translation_pending() ||
language_state_.translation_declined() ||
language_state_.IsPageTranslated()) {
decision->PreventAllTriggering();
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledDoesntNeedTranslation);
}
// Also, skip if the connection is currently offline - initiation doesn't make
// sense there, either.
if (net::NetworkChangeNotifier::IsOffline()) {
decision->PreventAllTriggering();
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledOffline);
}
if (!ignore_missing_key_for_testing_ &&
!::google_apis::HasAPIKeyConfigured()) {
// Without an API key, translate won't work, so don't offer to translate in
// the first place. Leave kOfferTranslateEnabled on, though, because that
// settings syncs and we don't want to turn off translate everywhere else.
decision->PreventAllTriggering();
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledMissingAPIKey);
}
// not supported MIME type pages currently cannot be translated.
// See bug: 217945, 1208340.
if (!IsMimeTypeSupported(translate_driver_->GetContentsMimeType())) {
decision->PreventAllTriggering();
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledMIMETypeNotSupported);
}
// Don't translate any Chrome specific page, e.g., New Tab Page, Download,
// History, and so on.
const GURL& page_url = translate_driver_->GetVisibleURL();
if (!translate_client_->IsTranslatableURL(page_url)) {
decision->PreventAllTriggering();
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledURLNotSupported);
}
if (!translate_prefs->IsOfferTranslateEnabled()) {
decision->PreventAllTriggering();
decision->ranker_events.push_back(
metrics::TranslateEventProto::DISABLED_BY_PREF);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledNeverOfferTranslations);
}
// Don't translate similar languages (ex: en-US to en).
if (page_language_code == target_lang) {
// This doesn't prevent *all* possible translate outcomes because some could
// use a different target language, making this condition only relevant to
// regular auto-translate/show UI.
decision->PreventAutoTranslate();
decision->PreventShowingUI();
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledSimilarLanguages);
}
// Nothing to do if either the language Chrome is in or the language of
// the page is not supported by the translation server.
if (target_lang.empty() ||
!TranslateDownloadManager::IsSupportedLanguage(page_language_code)) {
// This doesn't prevent *all* possible translate outcomes because some could
// use a different target language, making this condition only relevant to
// regular auto-translate/show UI.
decision->PreventAutoTranslate();
decision->PreventShowingUI();
decision->ranker_events.push_back(
metrics::TranslateEventProto::UNSUPPORTED_LANGUAGE);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledUnsupportedLanguage);
}
}
void TranslateManager::FilterAutoTranslate(
TranslateTriggerDecision* decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code) {
// Determine whether auto-translate is required, and if so for which target
// language.
std::string always_translate_target =
GetAutoTargetLanguage(page_language_code, translate_prefs);
std::string link_auto_translate_target = language_state_.AutoTranslateTo();
if (!translate_driver_->IsIncognito() && !always_translate_target.empty()) {
// If the user has previously selected "always translate" for this language
// we automatically translate. Note that in incognito mode we disable that
// feature; the user will get an infobar, so they can control whether the
// page's text is sent to the translate server.
decision->auto_translate_target = always_translate_target;
decision->ranker_events.push_back(
metrics::TranslateEventProto::AUTO_TRANSLATION_BY_PREF);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kAutomaticTranslationByPref);
} else if (!link_auto_translate_target.empty()) {
// This page was navigated through a click from a translated page.
decision->auto_translate_target = link_auto_translate_target;
decision->ranker_events.push_back(
metrics::TranslateEventProto::AUTO_TRANSLATION_BY_LINK);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kAutomaticTranslationByLink);
}
if (decision->auto_translate_target.empty()) {
decision->PreventAutoTranslate();
}
}
void TranslateManager::FilterForUserPrefs(
TranslateTriggerDecision* decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code) {
// Don't translate any user blocklisted languages.
if (!translate_prefs->CanTranslateLanguage(page_language_code)) {
decision->SetIsInLanguageBlocklist();
decision->PreventAutoTranslate();
decision->PreventShowingUI();
// Disable showing the translate UI for a predefined target language,
// although note that auto translation to a predefined target language is
// still allowed to happen, since that auto-translation overrides the user's
// language blocklist.
decision->PreventShowingPredefinedLanguageTranslateUI();
decision->ranker_events.push_back(
metrics::TranslateEventProto::LANGUAGE_DISABLED_BY_USER_CONFIG);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledNeverTranslateLanguage);
}
// Don't translate any user blocklisted URLs.
const GURL& page_url = translate_driver_->GetVisibleURL();
if (translate_prefs->IsSiteOnNeverPromptList(page_url.HostNoBrackets())) {
decision->SetIsInSiteBlocklist();
decision->PreventAutoTranslate();
decision->PreventShowingUI();
// The site blocklist isn't overridden for hrefTranslate.
decision->PreventShowingHrefTranslateUI();
decision->PreventAutoHrefTranslate();
// The site blocklist isn't overridden for predefined target languages.
decision->PreventShowingPredefinedLanguageTranslateUI();
decision->PreventPredefinedLanguageAutoTranslate();
decision->ranker_events.push_back(
metrics::TranslateEventProto::URL_DISABLED_BY_USER_CONFIG);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kDisabledNeverTranslateSite);
}
}
void TranslateManager::FilterForHrefTranslate(
TranslateTriggerDecision* decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code) {
if (!language_state_.navigation_from_google()) {
decision->PreventAutoHrefTranslate();
}
decision->href_translate_source = page_language_code;
decision->href_translate_target = language_state_.href_translate();
if (language_state_.navigation_from_google()) {
GetActiveTranslateMetricsLogger()->SetHasHrefTranslateTarget(
!decision->href_translate_target.empty());
}
// Can't honor hrefTranslate if there's no specified target or the target
// language isn't supported.
if (decision->href_translate_target.empty() ||
!TranslateDownloadManager::IsSupportedLanguage(
decision->href_translate_target)) {
decision->PreventAutoHrefTranslate();
decision->PreventShowingHrefTranslateUI();
}
if (!TranslateDownloadManager::IsSupportedLanguage(page_language_code) ||
page_language_code == decision->href_translate_target) {
if (language_state_.navigation_from_google()) {
// If hrefTranslate is present but the page language is unsupported,
// unknown, or seems to match the hrefTranslate target language, then as a
// last ditch effort assume that language detection was incorrect and send
// "und" as the source language to make the translate service attempt to
// detect the language as it processes the page content.
decision->href_translate_source =
language_detection::kUnknownLanguageCode;
} else {
decision->PreventAutoHrefTranslate();
decision->PreventShowingHrefTranslateUI();
}
}
}
void TranslateManager::FilterForPredefinedTarget(
TranslateTriggerDecision* decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code) {
decision->predefined_translate_source = page_language_code;
decision->predefined_translate_target =
language_state_.GetPredefinedTargetLanguage();
if (!language_state_.should_auto_translate_to_predefined_target_language()) {
decision->PreventPredefinedLanguageAutoTranslate();
}
if (decision->predefined_translate_target.empty() ||
!TranslateDownloadManager::IsSupportedLanguage(
decision->predefined_translate_target)) {
decision->PreventShowingPredefinedLanguageTranslateUI();
decision->PreventPredefinedLanguageAutoTranslate();
return;
}
if (!TranslateDownloadManager::IsSupportedLanguage(
decision->predefined_translate_source) ||
decision->predefined_translate_source ==
decision->href_translate_target) {
// If a predefined auto-translate target language is present but the page
// language is unsupported, unknown, or seems to match this target language,
// then as a last ditch effort assume that language detection was incorrect
// and send "und" as the source language to make the translate service
// attempt to detect the language as it processes the page content.
decision->predefined_translate_source =
language_detection::kUnknownLanguageCode;
}
}
void TranslateManager::MaybeShowOmniboxIcon(
const TranslateTriggerDecision& decision) {
if (decision.IsTriggeringPossible()) {
// Show the omnibox icon if any translate trigger is possible.
language_state_.SetTranslateEnabled(true);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kShowIcon);
}
}
bool TranslateManager::MaterializeDecision(
const TranslateTriggerDecision& decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code,
const std::string target_lang) {
// Auto-translating always happens if it's still possible here.
if (decision.can_auto_translate()) {
TranslatePage(page_language_code, decision.auto_translate_target, false,
GetLanguageState()->InTranslateNavigation()
? TranslationType::kAutomaticTranslationByLink
: TranslationType::kAutomaticTranslationByPref);
return true;
}
if (decision.can_auto_href_translate()) {
TranslatePage(decision.href_translate_source,
decision.href_translate_target, false,
TranslationType::kAutomaticTranslationByHref);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kAutomaticTranslationByHref);
return true;
}
if (decision.can_auto_translate_for_predefined_language()) {
TranslatePage(decision.predefined_translate_source,
decision.predefined_translate_target, false,
TranslationType::kAutomaticTranslationToPredefinedTarget);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kAutomaticTranslationToPredefinedTarget);
return true;
}
// Auto-translate didn't happen, so check if the UI should be shown. It must
// not be suppressed by preference, system state, or the Ranker.
// Will be true if we've decided to show the infobar/bubble UI to the user.
bool did_show_ui = false;
// Check whether the target language is predefined. If it is predefined
// trigger Translate UI even if it would not otherwise be triggered
// or would be triggered with another language.
if (decision.can_show_predefined_language_translate_ui()) {
did_show_ui = translate_client_->ShowTranslateUI(
translate::TRANSLATE_STEP_BEFORE_TRANSLATE, page_language_code,
decision.predefined_translate_target, TranslateErrors::NONE, false);
}
if (!did_show_ui && decision.ShouldShowUI()) {
// If the source language matches the UI language, it means the translation
// prompt is being forced by an experiment. Report this so the count of how
// often it happens can be tracked to suppress the experiment as necessary.
if (page_language_code ==
TranslateDownloadManager::GetLanguageCode(
TranslateDownloadManager::GetInstance()->application_locale())) {
translate_prefs->ReportForceTriggerOnEnglishPages();
}
// Prompts the user if they want the page translated.
did_show_ui = translate_client_->ShowTranslateUI(
translate::TRANSLATE_STEP_BEFORE_TRANSLATE, page_language_code,
target_lang, TranslateErrors::NONE, false);
}
// Auto-translate didn't happen, and the UI wasn't shown so consider the
// hrefTranslate attribute if it was present on the originating link.
if (!did_show_ui && decision.can_show_href_translate_ui()) {
did_show_ui = translate_client_->ShowTranslateUI(
translate::TRANSLATE_STEP_BEFORE_TRANSLATE,
decision.href_translate_source, decision.href_translate_target,
TranslateErrors::NONE, false);
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kShowUIFromHref);
}
if (did_show_ui) {
GetActiveTranslateMetricsLogger()->LogTriggerDecision(
TriggerDecision::kShowUI);
}
return did_show_ui;
}
void TranslateManager::RecordDecisionMetrics(
const TranslateTriggerDecision& decision,
const std::string& page_language_code,
bool ui_shown) {
// For Google navigations, the hrefTranslate hint may trigger a translation
// automatically. Record metrics if there is navigation from Google and a
// |decision.href_translate_target|.
if (language_state_.navigation_from_google() &&
!decision.href_translate_target.empty()) {
if (decision.can_auto_translate() || decision.can_auto_href_translate()) {
if (decision.can_auto_translate() &&
decision.auto_translate_target != decision.href_translate_target) {
TranslateBrowserMetrics::ReportTranslateHrefHintStatus(
TranslateBrowserMetrics::HrefTranslateStatus::
kAutoTranslatedDifferentTargetLanguage);
} else {
TranslateBrowserMetrics::ReportTranslateHrefHintStatus(
TranslateBrowserMetrics::HrefTranslateStatus::kAutoTranslated);
}
} else if (decision.can_show_href_translate_ui()) {
TranslateBrowserMetrics::ReportTranslateHrefHintStatus(
TranslateBrowserMetrics::HrefTranslateStatus::
kUiShownNotAutoTranslated);
} else {
TranslateBrowserMetrics::ReportTranslateHrefHintStatus(
TranslateBrowserMetrics::HrefTranslateStatus::
kNoUiShownNotAutoTranslated);
}
}
if (!decision.can_auto_translate() && !decision.can_auto_href_translate() &&
(decision.can_auto_translate_for_predefined_language() ||
decision.can_show_predefined_language_translate_ui())) {
return;
}
}
void TranslateManager::RecordDecisionRankerEvent(
const TranslateTriggerDecision& decision,
TranslatePrefs* translate_prefs,
const std::string& page_language_code,
const std::string& target_lang) {
if (!decision.auto_translate_target.empty()) {
translate_event_->set_modified_target_language(
decision.auto_translate_target);
}
if (!decision.ranker_events.empty()) {
auto event = decision.ranker_events[0];
RecordTranslateEvent(event);
}
// Finally, if the decision was to show UI and ranker suppressed it, log that.
if (!decision.can_auto_translate() && decision.can_show_ui() &&
decision.should_suppress_from_ranker()) {
RecordTranslateEvent(metrics::TranslateEventProto::DISABLED_BY_RANKER);
}
}
void TranslateManager::SetPredefinedTargetLanguage(
const std::string& language_code,
bool should_auto_translate) {
language_state_.SetPredefinedTargetLanguage(language_code,
should_auto_translate);
}
TranslateMetricsLogger* TranslateManager::GetActiveTranslateMetricsLogger() {
// If |active_translate_metrics_logger_| is not null, return that. Otherwise
// return |null_translate_metrics_logger_|. This way the callee doesn't have
// to check if the returned value is null.
return active_translate_metrics_logger_
? active_translate_metrics_logger_.get()
: null_translate_metrics_logger_.get();
}
void TranslateManager::RegisterTranslateMetricsLogger(
base::WeakPtr<TranslateMetricsLogger> translate_metrics_logger) {
active_translate_metrics_logger_ = translate_metrics_logger;
}
} // namespace translate