// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/translate/translate_manager.h" #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/memory/singleton.h" #include "base/metrics/histogram.h" #include "base/string_split.h" #include "base/string_util.h" #include "chrome/browser/autofill/autofill_manager.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/tab_contents/language_state.h" #include "chrome/browser/tab_contents/tab_util.h" #include "chrome/browser/tabs/tab_strip_model.h" #include "chrome/browser/translate/page_translated_details.h" #include "chrome/browser/translate/translate_infobar_delegate.h" #include "chrome/browser/translate/translate_tab_helper.h" #include "chrome/browser/translate/translate_prefs.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/pref_names.h" #include "chrome/common/render_messages.h" #include "chrome/common/translate_errors.h" #include "chrome/common/url_constants.h" #include "content/browser/renderer_host/render_process_host.h" #include "content/browser/renderer_host/render_view_host.h" #include "content/browser/tab_contents/navigation_controller.h" #include "content/browser/tab_contents/navigation_entry.h" #include "content/browser/tab_contents/tab_contents.h" #include "content/common/notification_details.h" #include "content/common/notification_service.h" #include "content/common/notification_source.h" #include "content/common/notification_type.h" #include "grit/browser_resources.h" #include "net/base/escape.h" #include "net/url_request/url_request_status.h" #include "ui/base/resource/resource_bundle.h" namespace { // Mapping from a locale name to a language code name. // Locale names not included are translated as is. struct LocaleToCLDLanguage { const char* locale_language; // Language Chrome locale is in. const char* cld_language; // Language the CLD reports. }; LocaleToCLDLanguage kLocaleToCLDLanguages[] = { { "en-GB", "en" }, { "en-US", "en" }, { "es-419", "es" }, { "pt-BR", "pt" }, { "pt-PT", "pt" }, }; // The list of languages the Google translation server supports. // For information, here is the list of languages that Chrome can be run in // but that the translation server does not support: // am Amharic // bn Bengali // gu Gujarati // kn Kannada // ml Malayalam // mr Marathi // ta Tamil // te Telugu const char* kSupportedLanguages[] = { "af", // Afrikaans "az", // Azerbaijani "sq", // Albanian "ar", // Arabic "hy", // Armenian "eu", // Basque "be", // Belarusian "bg", // Bulgarian "ca", // Catalan "zh-CN", // Chinese (Simplified) "zh-TW", // Chinese (Traditional) "hr", // Croatian "cs", // Czech "da", // Danish "nl", // Dutch "en", // English "et", // Estonian "fi", // Finnish "fil", // Filipino "fr", // French "gl", // Galician "de", // German "el", // Greek "ht", // Haitian Creole "he", // Hebrew "hi", // Hindi "hu", // Hungarian "is", // Icelandic "id", // Indonesian "it", // Italian "ga", // Irish "ja", // Japanese "ka", // Georgian "ko", // Korean "lv", // Latvian "lt", // Lithuanian "mk", // Macedonian "ms", // Malay "mt", // Maltese "nb", // Norwegian "fa", // Persian "pl", // Polish "pt", // Portuguese "ro", // Romanian "ru", // Russian "sr", // Serbian "sk", // Slovak "sl", // Slovenian "es", // Spanish "sw", // Swahili "sv", // Swedish "th", // Thai "tr", // Turkish "uk", // Ukrainian "ur", // Urdu "vi", // Vietnamese "cy", // Welsh "yi", // Yiddish }; const char* const kTranslateScriptURL = "http://translate.google.com/translate_a/element.js?" "cb=cr.googleTranslate.onTranslateElementLoad"; const char* const kTranslateScriptHeader = "Google-Translate-Element-Mode: library"; const char* const kReportLanguageDetectionErrorURL = "http://translate.google.com/translate_error"; const int kTranslateScriptExpirationDelayMS = 24 * 60 * 60 * 1000; // 1 day. } // namespace // static base::LazyInstance > TranslateManager::supported_languages_(base::LINKER_INITIALIZED); TranslateManager::~TranslateManager() { } // static TranslateManager* TranslateManager::GetInstance() { return Singleton::get(); } // static bool TranslateManager::IsTranslatableURL(const GURL& url) { // A URLs is translatable unless it is one of the following: // - an internal URL (chrome:// and others) // - the devtools (which is considered UI) // - an FTP page (as FTP pages tend to have long lists of filenames that may // confuse the CLD) return !url.SchemeIs(chrome::kChromeUIScheme) && !url.SchemeIs(chrome::kChromeDevToolsScheme) && !url.SchemeIs(chrome::kFtpScheme); } // static void TranslateManager::GetSupportedLanguages( std::vector* languages) { DCHECK(languages && languages->empty()); for (size_t i = 0; i < arraysize(kSupportedLanguages); ++i) languages->push_back(kSupportedLanguages[i]); } // static std::string TranslateManager::GetLanguageCode( const std::string& chrome_locale) { for (size_t i = 0; i < arraysize(kLocaleToCLDLanguages); ++i) { if (chrome_locale == kLocaleToCLDLanguages[i].locale_language) return kLocaleToCLDLanguages[i].cld_language; } return chrome_locale; } // static bool TranslateManager::IsSupportedLanguage(const std::string& page_language) { if (supported_languages_.Pointer()->empty()) { for (size_t i = 0; i < arraysize(kSupportedLanguages); ++i) supported_languages_.Pointer()->insert(kSupportedLanguages[i]); } return supported_languages_.Pointer()->find(page_language) != supported_languages_.Pointer()->end(); } void TranslateManager::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { switch (type.value) { case NotificationType::NAV_ENTRY_COMMITTED: { NavigationController* controller = Source(source).ptr(); NavigationController::LoadCommittedDetails* load_details = Details(details).ptr(); NavigationEntry* entry = controller->GetActiveEntry(); if (!entry) { NOTREACHED(); return; } TabContentsWrapper* wrapper = TabContentsWrapper::GetCurrentWrapperForContents( controller->tab_contents()); if (!wrapper || !wrapper->translate_tab_helper()) return; TranslateTabHelper* helper = wrapper->translate_tab_helper(); if (!load_details->is_main_frame && helper->language_state().translation_declined()) { // Some sites (such as Google map) may trigger sub-frame navigations // when the user interacts with the page. We don't want to show a new // infobar if the user already dismissed one in that case. return; } if (entry->transition_type() != PageTransition::RELOAD && load_details->type != NavigationType::SAME_PAGE) { return; } // When doing a page reload, we don't get a TAB_LANGUAGE_DETERMINED // notification. So we need to explictly initiate the translation. // Note that we delay it as the TranslateManager gets this notification // before the TabContents and the TabContents processing might remove the // current infobars. Since InitTranslation might add an infobar, it must // be done after that. MessageLoop::current()->PostTask(FROM_HERE, method_factory_.NewRunnableMethod( &TranslateManager::InitiateTranslationPosted, controller->tab_contents()->render_view_host()->process()->id(), controller->tab_contents()->render_view_host()->routing_id(), helper->language_state().original_language())); break; } case NotificationType::TAB_LANGUAGE_DETERMINED: { TabContents* tab = Source(source).ptr(); // We may get this notifications multiple times. Make sure to translate // only once. TabContentsWrapper* wrapper = TabContentsWrapper::GetCurrentWrapperForContents(tab); LanguageState& language_state = wrapper->translate_tab_helper()->language_state(); if (language_state.page_translatable() && !language_state.translation_pending() && !language_state.translation_declined() && !language_state.IsPageTranslated()) { std::string language = *(Details(details).ptr()); InitiateTranslation(tab, language); } break; } case NotificationType::PAGE_TRANSLATED: { // Only add translate infobar if it doesn't exist; if it already exists, // just update the state, the actual infobar would have received the same // notification and update the visual display accordingly. TabContents* tab = Source(source).ptr(); PageTranslatedDetails* page_translated_details = Details(details).ptr(); PageTranslated(tab, page_translated_details); break; } case NotificationType::PROFILE_DESTROYED: { Profile* profile = Source(source).ptr(); notification_registrar_.Remove(this, NotificationType::PROFILE_DESTROYED, source); size_t count = accept_languages_.erase(profile->GetPrefs()); // We should know about this profile since we are listening for // notifications on it. DCHECK(count > 0); pref_change_registrar_.Remove(prefs::kAcceptLanguages, this); break; } case NotificationType::PREF_CHANGED: { DCHECK(*Details(details).ptr() == prefs::kAcceptLanguages); PrefService* prefs = Source(source).ptr(); InitAcceptLanguages(prefs); break; } default: NOTREACHED(); } } void TranslateManager::OnURLFetchComplete(const URLFetcher* source, const GURL& url, const net::URLRequestStatus& status, int response_code, const ResponseCookies& cookies, const std::string& data) { scoped_ptr delete_ptr(source); DCHECK(translate_script_request_pending_); translate_script_request_pending_ = false; bool error = (status.status() != net::URLRequestStatus::SUCCESS || response_code != 200); if (!error) { base::StringPiece str = ResourceBundle::GetSharedInstance(). GetRawDataResource(IDR_TRANSLATE_JS); DCHECK(translate_script_.empty()); str.CopyToString(&translate_script_); translate_script_ += "\n" + data; // We'll expire the cached script after some time, to make sure long running // browsers still get fixes that might get pushed with newer scripts. MessageLoop::current()->PostDelayedTask(FROM_HERE, method_factory_.NewRunnableMethod( &TranslateManager::ClearTranslateScript), translate_script_expiration_delay_); } // Process any pending requests. std::vector::const_iterator iter; for (iter = pending_requests_.begin(); iter != pending_requests_.end(); ++iter) { const PendingRequest& request = *iter; TabContents* tab = tab_util::GetTabContentsByID(request.render_process_id, request.render_view_id); if (!tab) { // The tab went away while we were retrieving the script. continue; } NavigationEntry* entry = tab->controller().GetActiveEntry(); if (!entry || entry->page_id() != request.page_id) { // We navigated away from the page the translation was triggered on. continue; } if (error) { ShowInfoBar(tab, TranslateInfoBarDelegate::CreateErrorDelegate( TranslateErrors::NETWORK, tab, request.source_lang, request.target_lang)); } else { // Translate the page. DoTranslatePage(tab, translate_script_, request.source_lang, request.target_lang); } } pending_requests_.clear(); } // static bool TranslateManager::IsShowingTranslateInfobar(TabContents* tab) { return GetTranslateInfoBarDelegate(tab) != NULL; } TranslateManager::TranslateManager() : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)), translate_script_expiration_delay_(kTranslateScriptExpirationDelayMS), translate_script_request_pending_(false) { notification_registrar_.Add(this, NotificationType::NAV_ENTRY_COMMITTED, NotificationService::AllSources()); notification_registrar_.Add(this, NotificationType::TAB_LANGUAGE_DETERMINED, NotificationService::AllSources()); notification_registrar_.Add(this, NotificationType::PAGE_TRANSLATED, NotificationService::AllSources()); } void TranslateManager::InitiateTranslation(TabContents* tab, const std::string& page_lang) { PrefService* prefs = tab->profile()->GetOriginalProfile()->GetPrefs(); if (!prefs->GetBoolean(prefs::kEnableTranslate)) return; pref_change_registrar_.Init(prefs); // Allow disabling of translate from the command line to assist with // automated browser testing. if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableTranslate)) return; NavigationEntry* entry = tab->controller().GetActiveEntry(); if (!entry) { // This can happen for popups created with window.open(""). return; } // If there is already a translate infobar showing, don't show another one. if (GetTranslateInfoBarDelegate(tab)) return; std::string target_lang = GetTargetLanguage(); // 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() || !IsSupportedLanguage(page_lang)) { return; } // We don't want to translate: // - any Chrome specific page (New Tab Page, Download, History... pages). // - similar languages (ex: en-US to en). // - any user black-listed URLs or user selected language combination. // - any language the user configured as accepted languages. if (!IsTranslatableURL(entry->url()) || page_lang == target_lang || !TranslatePrefs::CanTranslate(prefs, page_lang, entry->url()) || IsAcceptLanguage(tab, page_lang)) { return; } // 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. std::string auto_target_lang; if (!tab->profile()->IsOffTheRecord() && TranslatePrefs::ShouldAutoTranslate(prefs, page_lang, &auto_target_lang)) { TranslatePage(tab, page_lang, auto_target_lang); return; } TranslateTabHelper* helper = TabContentsWrapper::GetCurrentWrapperForContents( tab)->translate_tab_helper(); std::string auto_translate_to = helper->language_state().AutoTranslateTo(); if (!auto_translate_to.empty()) { // This page was navigated through a click from a translated page. TranslatePage(tab, page_lang, auto_translate_to); return; } // Prompts the user if he/she wants the page translated. tab->AddInfoBar(TranslateInfoBarDelegate::CreateDelegate( TranslateInfoBarDelegate::BEFORE_TRANSLATE, tab, page_lang, target_lang)); } void TranslateManager::InitiateTranslationPosted( int process_id, int render_id, const std::string& page_lang) { // The tab might have been closed. TabContents* tab = tab_util::GetTabContentsByID(process_id, render_id); if (!tab) return; TranslateTabHelper* helper = TabContentsWrapper::GetCurrentWrapperForContents( tab)->translate_tab_helper(); if (helper->language_state().translation_pending()) return; InitiateTranslation(tab, page_lang); } void TranslateManager::TranslatePage(TabContents* tab_contents, const std::string& source_lang, const std::string& target_lang) { NavigationEntry* entry = tab_contents->controller().GetActiveEntry(); if (!entry) { NOTREACHED(); return; } TranslateInfoBarDelegate* infobar = TranslateInfoBarDelegate::CreateDelegate( TranslateInfoBarDelegate::TRANSLATING, tab_contents, source_lang, target_lang); if (!infobar) { // This means the source or target languages are not supported, which should // not happen as we won't show a translate infobar or have the translate // context menu activated in such cases. NOTREACHED(); return; } ShowInfoBar(tab_contents, infobar); if (!translate_script_.empty()) { DoTranslatePage(tab_contents, translate_script_, 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. RenderViewHost* rvh = tab_contents->render_view_host(); PendingRequest request; request.render_process_id = rvh->process()->id(); request.render_view_id = rvh->routing_id(); request.page_id = entry->page_id(); request.source_lang = source_lang; request.target_lang = target_lang; pending_requests_.push_back(request); RequestTranslateScript(); } void TranslateManager::RevertTranslation(TabContents* tab_contents) { NavigationEntry* entry = tab_contents->controller().GetActiveEntry(); if (!entry) { NOTREACHED(); return; } tab_contents->render_view_host()->Send(new ViewMsg_RevertTranslation( tab_contents->render_view_host()->routing_id(), entry->page_id())); TranslateTabHelper* helper = TabContentsWrapper::GetCurrentWrapperForContents( tab_contents)->translate_tab_helper(); helper->language_state().set_current_language( helper->language_state().original_language()); } void TranslateManager::ReportLanguageDetectionError(TabContents* tab_contents) { UMA_HISTOGRAM_COUNTS("Translate.ReportLanguageDetectionError", 1); GURL page_url = tab_contents->controller().GetActiveEntry()->url(); std::string report_error_url(kReportLanguageDetectionErrorURL); report_error_url += "?client=cr&action=langidc&u="; report_error_url += EscapeUrlEncodedData(page_url.spec()); report_error_url += "&sl="; TranslateTabHelper* helper = TabContentsWrapper::GetCurrentWrapperForContents( tab_contents)->translate_tab_helper(); report_error_url += helper->language_state().original_language(); report_error_url += "&hl="; report_error_url += GetLanguageCode(g_browser_process->GetApplicationLocale()); // Open that URL in a new tab so that the user can tell us more. Browser* browser = BrowserList::GetLastActive(); if (!browser) { NOTREACHED(); return; } browser->AddSelectedTabWithURL(GURL(report_error_url), PageTransition::AUTO_BOOKMARK); } void TranslateManager::DoTranslatePage(TabContents* tab, const std::string& translate_script, const std::string& source_lang, const std::string& target_lang) { NavigationEntry* entry = tab->controller().GetActiveEntry(); if (!entry) { NOTREACHED(); return; } TabContentsWrapper* wrapper = TabContentsWrapper::GetCurrentWrapperForContents(tab); wrapper->translate_tab_helper()->language_state().set_translation_pending( true); tab->render_view_host()->Send(new ViewMsg_TranslatePage( tab->render_view_host()->routing_id(), entry->page_id(), translate_script, source_lang, target_lang)); // Ideally we'd have a better way to uniquely identify form control elements, // but we don't have that yet. So before start translation, we clear the // current form and re-parse it in AutofillManager first to get the new // labels. if (wrapper) wrapper->autofill_manager()->Reset(); } void TranslateManager::PageTranslated(TabContents* tab, PageTranslatedDetails* details) { // Create the new infobar to display. TranslateInfoBarDelegate* infobar; if (details->error_type != TranslateErrors::NONE) { infobar = TranslateInfoBarDelegate::CreateErrorDelegate(details->error_type, tab, details->source_language, details->target_language); } else if (!IsSupportedLanguage(details->source_language)) { // TODO(jcivelli): http://crbug.com/9390 We should change the "after // translate" infobar to support unknown as the original // language. UMA_HISTOGRAM_COUNTS("Translate.ServerReportedUnsupportedLanguage", 1); infobar = TranslateInfoBarDelegate::CreateErrorDelegate( TranslateErrors::UNSUPPORTED_LANGUAGE, tab, details->source_language, details->target_language); } else { infobar = TranslateInfoBarDelegate::CreateDelegate( TranslateInfoBarDelegate::AFTER_TRANSLATE, tab, details->source_language, details->target_language); } ShowInfoBar(tab, infobar); } bool TranslateManager::IsAcceptLanguage(TabContents* tab, const std::string& language) { PrefService* pref_service = tab->profile()->GetOriginalProfile()->GetPrefs(); PrefServiceLanguagesMap::const_iterator iter = accept_languages_.find(pref_service); if (iter == accept_languages_.end()) { InitAcceptLanguages(pref_service); // Listen for this profile going away, in which case we would need to clear // the accepted languages for the profile. notification_registrar_.Add(this, NotificationType::PROFILE_DESTROYED, Source(tab->profile())); // Also start listening for changes in the accept languages. pref_change_registrar_.Add(prefs::kAcceptLanguages, this); iter = accept_languages_.find(pref_service); } return iter->second.count(language) != 0; } void TranslateManager::InitAcceptLanguages(PrefService* prefs) { // We have been asked for this profile, build the languages. std::string accept_langs_str = prefs->GetString(prefs::kAcceptLanguages); std::vector accept_langs_list; LanguageSet accept_langs_set; base::SplitString(accept_langs_str, ',', &accept_langs_list); std::vector::const_iterator iter; std::string ui_lang = GetLanguageCode(g_browser_process->GetApplicationLocale()); bool is_ui_english = StartsWithASCII(ui_lang, "en-", false); for (iter = accept_langs_list.begin(); iter != accept_langs_list.end(); ++iter) { // Get rid of the locale extension if any (ex: en-US -> en), but for Chinese // for which the CLD reports zh-CN and zh-TW. std::string accept_lang(*iter); size_t index = iter->find("-"); if (index != std::string::npos && *iter != "zh-CN" && *iter != "zh-TW") accept_lang = iter->substr(0, index); // Special-case English until we resolve bug 36182 properly. // Add English only if the UI language is not English. This will annoy // users of non-English Chrome who can comprehend English until English is // black-listed. // TODO(jungshik): Once we determine that it's safe to remove English from // the default Accept-Language values for most locales, remove this // special-casing. if (accept_lang != "en" || is_ui_english) accept_langs_set.insert(accept_lang); } accept_languages_[prefs] = accept_langs_set; } void TranslateManager::RequestTranslateScript() { if (translate_script_request_pending_) return; translate_script_request_pending_ = true; URLFetcher* fetcher = URLFetcher::Create(0, GURL(kTranslateScriptURL), URLFetcher::GET, this); fetcher->set_request_context(Profile::GetDefaultRequestContext()); fetcher->set_extra_request_headers(kTranslateScriptHeader); fetcher->Start(); } void TranslateManager::ShowInfoBar(TabContents* tab, TranslateInfoBarDelegate* infobar) { TranslateInfoBarDelegate* old_infobar = GetTranslateInfoBarDelegate(tab); infobar->UpdateBackgroundAnimation(old_infobar); if (old_infobar) { // There already is a translate infobar, simply replace it. tab->ReplaceInfoBar(old_infobar, infobar); } else { tab->AddInfoBar(infobar); } } // static std::string TranslateManager::GetTargetLanguage() { std::string target_lang = GetLanguageCode(g_browser_process->GetApplicationLocale()); return IsSupportedLanguage(target_lang) ? target_lang : std::string(); } // static TranslateInfoBarDelegate* TranslateManager::GetTranslateInfoBarDelegate( TabContents* tab) { for (size_t i = 0; i < tab->infobar_count(); ++i) { TranslateInfoBarDelegate* delegate = tab->GetInfoBarDelegateAt(i)->AsTranslateInfoBarDelegate(); if (delegate) return delegate; } return NULL; }