translate_manager.cc revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/translate/translate_manager.h"
6
7#include "base/bind.h"
8#include "base/command_line.h"
9#include "base/compiler_specific.h"
10#include "base/json/json_reader.h"
11#include "base/memory/singleton.h"
12#include "base/message_loop.h"
13#include "base/metrics/histogram.h"
14#include "base/prefs/pref_service.h"
15#include "base/string_util.h"
16#include "base/stringprintf.h"
17#include "base/strings/string_split.h"
18#include "base/values.h"
19#include "chrome/browser/browser_process.h"
20#include "chrome/browser/infobars/infobar_service.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/browser/tab_contents/language_state.h"
23#include "chrome/browser/tab_contents/tab_util.h"
24#include "chrome/browser/translate/page_translated_details.h"
25#include "chrome/browser/translate/translate_infobar_delegate.h"
26#include "chrome/browser/translate/translate_prefs.h"
27#include "chrome/browser/translate/translate_tab_helper.h"
28#include "chrome/browser/ui/browser.h"
29#include "chrome/browser/ui/browser_finder.h"
30#include "chrome/browser/ui/browser_tabstrip.h"
31#include "chrome/browser/ui/tabs/tab_strip_model.h"
32#include "chrome/common/chrome_notification_types.h"
33#include "chrome/common/chrome_switches.h"
34#include "chrome/common/pref_names.h"
35#include "chrome/common/render_messages.h"
36#include "chrome/common/translate_errors.h"
37#include "chrome/common/url_constants.h"
38#include "content/public/browser/navigation_controller.h"
39#include "content/public/browser/navigation_details.h"
40#include "content/public/browser/navigation_entry.h"
41#include "content/public/browser/notification_details.h"
42#include "content/public/browser/notification_service.h"
43#include "content/public/browser/notification_source.h"
44#include "content/public/browser/notification_types.h"
45#include "content/public/browser/render_process_host.h"
46#include "content/public/browser/render_view_host.h"
47#include "content/public/browser/web_contents.h"
48#include "google_apis/google_api_keys.h"
49#include "grit/browser_resources.h"
50#include "net/base/escape.h"
51#include "net/base/load_flags.h"
52#include "net/base/url_util.h"
53#include "net/http/http_status_code.h"
54#include "net/url_request/url_fetcher.h"
55#include "net/url_request/url_request_status.h"
56#include "ui/base/resource/resource_bundle.h"
57
58#ifdef FILE_MANAGER_EXTENSION
59#include "chrome/browser/chromeos/extensions/file_manager/file_manager_util.h"
60#include "extensions/common/constants.h"
61#endif
62
63using content::NavigationController;
64using content::NavigationEntry;
65using content::WebContents;
66
67namespace {
68
69// The default list of languages the Google translation server supports.
70// We use this list until we receive the list that the server exposes.
71// For information, here is the list of languages that Chrome can be run in
72// but that the translation server does not support:
73// am Amharic
74// bn Bengali
75// gu Gujarati
76// kn Kannada
77// ml Malayalam
78// mr Marathi
79// ta Tamil
80// te Telugu
81const char* const kDefaultSupportedLanguages[] = {
82    "af",     // Afrikaans
83    "sq",     // Albanian
84    "ar",     // Arabic
85    "be",     // Belarusian
86    "bg",     // Bulgarian
87    "ca",     // Catalan
88    "zh-CN",  // Chinese (Simplified)
89    "zh-TW",  // Chinese (Traditional)
90    "hr",     // Croatian
91    "cs",     // Czech
92    "da",     // Danish
93    "nl",     // Dutch
94    "en",     // English
95    "eo",     // Esperanto
96    "et",     // Estonian
97    "tl",     // Filipino
98    "fi",     // Finnish
99    "fr",     // French
100    "gl",     // Galician
101    "de",     // German
102    "el",     // Greek
103    "ht",     // Haitian Creole
104    "iw",     // Hebrew
105    "hi",     // Hindi
106    "hu",     // Hungarian
107    "is",     // Icelandic
108    "id",     // Indonesian
109    "ga",     // Irish
110    "it",     // Italian
111    "ja",     // Japanese
112    "ko",     // Korean
113    "lv",     // Latvian
114    "lt",     // Lithuanian
115    "mk",     // Macedonian
116    "ms",     // Malay
117    "mt",     // Maltese
118    "no",     // Norwegian
119    "fa",     // Persian
120    "pl",     // Polish
121    "pt",     // Portuguese
122    "ro",     // Romanian
123    "ru",     // Russian
124    "sr",     // Serbian
125    "sk",     // Slovak
126    "sl",     // Slovenian
127    "es",     // Spanish
128    "sw",     // Swahili
129    "sv",     // Swedish
130    "th",     // Thai
131    "tr",     // Turkish
132    "uk",     // Ukrainian
133    "vi",     // Vietnamese
134    "cy",     // Welsh
135    "yi",     // Yiddish
136};
137
138const char kTranslateScriptURL[] =
139    "https://translate.google.com/translate_a/element.js";
140const char kTranslateScriptHeader[] = "Google-Translate-Element-Mode: library";
141const char kReportLanguageDetectionErrorURL[] =
142    "https://translate.google.com/translate_error?client=cr&action=langidc";
143const char kLanguageListFetchURL[] =
144    "https://translate.googleapis.com/translate_a/l?client=chrome&cb=sl";
145
146// Used in kTranslateScriptURL to request supporting languages list including
147// "alpha languages".
148const char kAlphaLanguageQueryName[] = "alpha";
149const char kAlphaLanguageQueryValue[] = "1";
150
151// Used in all translate URLs to specify API Key.
152const char kApiKeyName[] = "key";
153
154// Used in kTranslateScriptURL to specify a callback function name.
155const char kCallbackQueryName[] = "cb";
156const char kCallbackQueryValue[] =
157    "cr.googleTranslate.onTranslateElementLoad";
158
159// Used in kTranslateScriptURL and kLanguageListFetchURL to specify the
160// application locale.
161const char kHostLocaleQueryName[] = "hl";
162
163// Used in kReportLanguageDetectionErrorURL to specify the original page
164// language.
165const char kSourceLanguageQueryName[] = "sl";
166
167// Used in kReportLanguageDetectionErrorURL to specify the page URL.
168const char kUrlQueryName[] = "u";
169
170const int kMaxRetryLanguageListFetch = 5;
171const int kTranslateScriptExpirationDelayDays = 1;
172
173GURL AddApiKeyToUrl(const GURL& url) {
174  return net::AppendQueryParameter(url, kApiKeyName, google_apis::GetAPIKey());
175}
176
177GURL AddHostLocaleToUrl(const GURL& url) {
178  return net::AppendQueryParameter(
179      url,
180      kHostLocaleQueryName,
181      TranslateManager::GetLanguageCode(
182          g_browser_process->GetApplicationLocale()));
183}
184
185}  // namespace
186
187// This must be kept in sync with the &cb= value in the kLanguageListFetchURL.
188const char TranslateManager::kLanguageListCallbackName[] = "sl(";
189const char TranslateManager::kTargetLanguagesKey[] = "tl";
190
191// static
192base::LazyInstance<std::set<std::string> >
193    TranslateManager::supported_languages_ = LAZY_INSTANCE_INITIALIZER;
194
195TranslateManager::~TranslateManager() {
196  weak_method_factory_.InvalidateWeakPtrs();
197}
198
199// static
200TranslateManager* TranslateManager::GetInstance() {
201  return Singleton<TranslateManager>::get();
202}
203
204// static
205bool TranslateManager::IsTranslatableURL(const GURL& url) {
206  // A URLs is translatable unless it is one of the following:
207  // - empty (can happen for popups created with window.open(""))
208  // - an internal URL (chrome:// and others)
209  // - the devtools (which is considered UI)
210  // - Chrome OS file manager extension
211  // - an FTP page (as FTP pages tend to have long lists of filenames that may
212  //   confuse the CLD)
213  return !url.is_empty() &&
214         !url.SchemeIs(chrome::kChromeUIScheme) &&
215         !url.SchemeIs(chrome::kChromeDevToolsScheme) &&
216#ifdef FILE_MANAGER_EXTENSION
217         !(url.SchemeIs(extensions::kExtensionScheme) &&
218           url.DomainIs(kFileBrowserDomain)) &&
219#endif
220         !url.SchemeIs(chrome::kFtpScheme);
221}
222
223// static
224void TranslateManager::SetSupportedLanguages(const std::string& language_list) {
225  // The format is:
226  // sl({"sl": {"XX": "LanguageName", ...}, "tl": {"XX": "LanguageName", ...}})
227  // Where "sl(" is set in kLanguageListCallbackName
228  // and "tl" is kTargetLanguagesKey
229  if (!StartsWithASCII(language_list, kLanguageListCallbackName, false) ||
230      !EndsWith(language_list, ")", false)) {
231    // We don't have a NOTREACHED here since this can happen in ui_tests, even
232    // though the the BrowserMain function won't call us with parameters.ui_task
233    // is NULL some tests don't set it, so we must bail here.
234    return;
235  }
236  static const size_t kLanguageListCallbackNameLength =
237      strlen(kLanguageListCallbackName);
238  std::string languages_json = language_list.substr(
239      kLanguageListCallbackNameLength,
240      language_list.size() - kLanguageListCallbackNameLength - 1);
241  scoped_ptr<Value> json_value(
242      base::JSONReader::Read(languages_json, base::JSON_ALLOW_TRAILING_COMMAS));
243  if (json_value == NULL || !json_value->IsType(Value::TYPE_DICTIONARY)) {
244    NOTREACHED();
245    return;
246  }
247  // The first level dictionary contains two sub-dict, one for source languages
248  // and the other for target languages, we want to use the target languages.
249  DictionaryValue* language_dict =
250      static_cast<DictionaryValue*>(json_value.get());
251  DictionaryValue* target_languages = NULL;
252  if (!language_dict->GetDictionary(kTargetLanguagesKey, &target_languages) ||
253      target_languages == NULL) {
254    NOTREACHED();
255    return;
256  }
257  // Now we can clear our current state...
258  std::set<std::string>* supported_languages = supported_languages_.Pointer();
259  supported_languages->clear();
260  // ... and replace it with the values we just fetched from the server.
261  for (DictionaryValue::Iterator iter(*target_languages); !iter.IsAtEnd();
262       iter.Advance()) {
263    supported_languages_.Pointer()->insert(iter.key());
264  }
265}
266
267// static
268void TranslateManager::InitSupportedLanguages() {
269  // If our list of supported languages have not been set yet, we default
270  // to our hard coded list of languages in kDefaultSupportedLanguages.
271  if (supported_languages_.Pointer()->empty()) {
272    for (size_t i = 0; i < arraysize(kDefaultSupportedLanguages); ++i)
273      supported_languages_.Pointer()->insert(kDefaultSupportedLanguages[i]);
274  }
275}
276
277// static
278void TranslateManager::GetSupportedLanguages(
279    std::vector<std::string>* languages) {
280  DCHECK(languages && languages->empty());
281  InitSupportedLanguages();
282  std::set<std::string>* supported_languages = supported_languages_.Pointer();
283  std::set<std::string>::const_iterator iter = supported_languages->begin();
284  for (; iter != supported_languages->end(); ++iter)
285    languages->push_back(*iter);
286}
287
288// static
289std::string TranslateManager::GetLanguageCode(
290    const std::string& chrome_locale) {
291  // Only remove the country code for country specific languages we don't
292  // support specifically yet.
293  if (IsSupportedLanguage(chrome_locale))
294    return chrome_locale;
295
296  size_t hypen_index = chrome_locale.find('-');
297  if (hypen_index == std::string::npos)
298    return chrome_locale;
299  return chrome_locale.substr(0, hypen_index);
300}
301
302// static
303bool TranslateManager::IsSupportedLanguage(const std::string& page_language) {
304  InitSupportedLanguages();
305  return supported_languages_.Pointer()->count(page_language) != 0;
306}
307
308void TranslateManager::Observe(int type,
309                               const content::NotificationSource& source,
310                               const content::NotificationDetails& details) {
311  switch (type) {
312    case content::NOTIFICATION_NAV_ENTRY_COMMITTED: {
313      NavigationController* controller =
314          content::Source<NavigationController>(source).ptr();
315      content::LoadCommittedDetails* load_details =
316          content::Details<content::LoadCommittedDetails>(details).ptr();
317      NavigationEntry* entry = controller->GetActiveEntry();
318      if (!entry) {
319        NOTREACHED();
320        return;
321      }
322
323      TranslateTabHelper* translate_tab_helper =
324          TranslateTabHelper::FromWebContents(controller->GetWebContents());
325      if (!translate_tab_helper)
326        return;
327
328      // If the navigation happened while offline don't show the translate
329      // bar since there will be nothing to translate.
330      if (load_details->http_status_code == 0 ||
331          load_details->http_status_code == net::HTTP_INTERNAL_SERVER_ERROR) {
332        return;
333      }
334
335      if (!load_details->is_main_frame &&
336          translate_tab_helper->language_state().translation_declined()) {
337        // Some sites (such as Google map) may trigger sub-frame navigations
338        // when the user interacts with the page.  We don't want to show a new
339        // infobar if the user already dismissed one in that case.
340        return;
341      }
342      if (entry->GetTransitionType() != content::PAGE_TRANSITION_RELOAD &&
343          load_details->type != content::NAVIGATION_TYPE_SAME_PAGE) {
344        return;
345      }
346      // When doing a page reload, TAB_LANGUAGE_DETERMINED is not sent,
347      // so the translation needs to be explicitly initiated, but only when the
348      // page is translatable.
349      if (!translate_tab_helper->language_state().page_translatable())
350        return;
351      // Note that we delay it as the TranslateManager gets this notification
352      // before the WebContents and the WebContents processing might remove the
353      // current infobars.  Since InitTranslation might add an infobar, it must
354      // be done after that.
355      MessageLoop::current()->PostTask(FROM_HERE,
356          base::Bind(
357              &TranslateManager::InitiateTranslationPosted,
358              weak_method_factory_.GetWeakPtr(),
359              controller->GetWebContents()->GetRenderProcessHost()->GetID(),
360              controller->GetWebContents()->GetRenderViewHost()->GetRoutingID(),
361              translate_tab_helper->language_state().original_language()));
362      break;
363    }
364    case chrome::NOTIFICATION_TAB_LANGUAGE_DETERMINED: {
365      WebContents* tab = content::Source<WebContents>(source).ptr();
366      // We may get this notifications multiple times.  Make sure to translate
367      // only once.
368      TranslateTabHelper* translate_tab_helper =
369          TranslateTabHelper::FromWebContents(tab);
370      if (!translate_tab_helper)
371        return;
372
373      LanguageState& language_state = translate_tab_helper->language_state();
374      if (language_state.page_translatable() &&
375          !language_state.translation_pending() &&
376          !language_state.translation_declined() &&
377          !language_state.IsPageTranslated()) {
378        std::string language = *(content::Details<std::string>(details).ptr());
379        InitiateTranslation(tab, language);
380      }
381      break;
382    }
383    case chrome::NOTIFICATION_PAGE_TRANSLATED: {
384      // Only add translate infobar if it doesn't exist; if it already exists,
385      // just update the state, the actual infobar would have received the same
386      //  notification and update the visual display accordingly.
387      WebContents* tab = content::Source<WebContents>(source).ptr();
388      PageTranslatedDetails* page_translated_details =
389          content::Details<PageTranslatedDetails>(details).ptr();
390      PageTranslated(tab, page_translated_details);
391      break;
392    }
393    case chrome::NOTIFICATION_PROFILE_DESTROYED: {
394      PrefService* pref_service =
395          content::Source<Profile>(source).ptr()->GetPrefs();
396      notification_registrar_.Remove(this,
397                                     chrome::NOTIFICATION_PROFILE_DESTROYED,
398                                     source);
399      size_t count = accept_languages_.erase(pref_service);
400      // We should know about this profile since we are listening for
401      // notifications on it.
402      DCHECK(count == 1u);
403      PrefChangeRegistrar* pref_change_registrar =
404          pref_change_registrars_[pref_service];
405      count = pref_change_registrars_.erase(pref_service);
406      DCHECK(count == 1u);
407      delete pref_change_registrar;
408      break;
409    }
410    default:
411      NOTREACHED();
412  }
413}
414
415void TranslateManager::OnURLFetchComplete(const net::URLFetcher* source) {
416  if (translate_script_request_pending_.get() != source &&
417      language_list_request_pending_.get() != source) {
418    // Looks like crash on Mac is possibly caused with callback entering here
419    // with unknown fetcher when network is refreshed.
420    scoped_ptr<const net::URLFetcher> delete_ptr(source);
421    return;
422  }
423
424  bool error =
425      (source->GetStatus().status() != net::URLRequestStatus::SUCCESS ||
426      source->GetResponseCode() != net::HTTP_OK);
427  if (translate_script_request_pending_.get() == source) {
428    scoped_ptr<const net::URLFetcher> delete_ptr(
429        translate_script_request_pending_.release());
430    if (!error) {
431      base::StringPiece str = ResourceBundle::GetSharedInstance().
432          GetRawDataResource(IDR_TRANSLATE_JS);
433      DCHECK(translate_script_.empty());
434      str.CopyToString(&translate_script_);
435      std::string argument = "('";
436      std::string api_key = google_apis::GetAPIKey();
437      argument += net::EscapeQueryParamValue(api_key, true);
438      argument += "');\n";
439      std::string data;
440      source->GetResponseAsString(&data);
441      translate_script_ += argument + data;
442      // We'll expire the cached script after some time, to make sure long
443      // running browsers still get fixes that might get pushed with newer
444      // scripts.
445      MessageLoop::current()->PostDelayedTask(FROM_HERE,
446          base::Bind(&TranslateManager::ClearTranslateScript,
447                     weak_method_factory_.GetWeakPtr()),
448          translate_script_expiration_delay_);
449    }
450    // Process any pending requests.
451    std::vector<PendingRequest>::const_iterator iter;
452    for (iter = pending_requests_.begin(); iter != pending_requests_.end();
453         ++iter) {
454      const PendingRequest& request = *iter;
455      WebContents* web_contents =
456          tab_util::GetWebContentsByID(request.render_process_id,
457                                       request.render_view_id);
458      if (!web_contents) {
459        // The tab went away while we were retrieving the script.
460        continue;
461      }
462      NavigationEntry* entry = web_contents->GetController().GetActiveEntry();
463      if (!entry || entry->GetPageID() != request.page_id) {
464        // We navigated away from the page the translation was triggered on.
465        continue;
466      }
467
468      if (error) {
469        Profile* profile =
470            Profile::FromBrowserContext(web_contents->GetBrowserContext());
471        TranslateInfoBarDelegate::Create(
472            InfoBarService::FromWebContents(web_contents),
473            true,
474            TranslateInfoBarDelegate::TRANSLATION_ERROR,
475            TranslateErrors::NETWORK,
476            profile->GetPrefs(),
477            ShortcutConfig(),
478            request.source_lang,
479            request.target_lang);
480      } else {
481        // Translate the page.
482        DoTranslatePage(web_contents, translate_script_,
483                        request.source_lang, request.target_lang);
484      }
485    }
486    pending_requests_.clear();
487  } else {  // if (translate_script_request_pending_.get() == source)
488    scoped_ptr<const net::URLFetcher> delete_ptr(
489        language_list_request_pending_.release());
490    if (!error) {
491      std::string data;
492      source->GetResponseAsString(&data);
493      SetSupportedLanguages(data);
494    } else {
495      VLOG(9) << "Failed to Fetch languages from: " << kLanguageListFetchURL;
496    }
497  }
498}
499
500TranslateManager::TranslateManager()
501    : weak_method_factory_(this),
502      translate_script_expiration_delay_(
503          base::TimeDelta::FromDays(kTranslateScriptExpirationDelayDays)) {
504  notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
505                              content::NotificationService::AllSources());
506  notification_registrar_.Add(this,
507                              chrome::NOTIFICATION_TAB_LANGUAGE_DETERMINED,
508                              content::NotificationService::AllSources());
509  notification_registrar_.Add(this, chrome::NOTIFICATION_PAGE_TRANSLATED,
510                              content::NotificationService::AllSources());
511}
512
513void TranslateManager::InitiateTranslation(WebContents* web_contents,
514                                           const std::string& page_lang) {
515  Profile* profile =
516      Profile::FromBrowserContext(web_contents->GetBrowserContext());
517  PrefService* prefs = profile->GetOriginalProfile()->GetPrefs();
518  if (!prefs->GetBoolean(prefs::kEnableTranslate))
519    return;
520
521  // Allow disabling of translate from the command line to assist with
522  // automated browser testing.
523  if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableTranslate))
524    return;
525
526  std::string target_lang = GetTargetLanguage(prefs);
527  std::string language_code = GetLanguageCode(page_lang);
528  // Nothing to do if either the language Chrome is in or the language of the
529  // page is not supported by the translation server.
530  if (target_lang.empty() || !IsSupportedLanguage(language_code)) {
531    return;
532  }
533
534  // We don't want to translate:
535  // - any Chrome specific page (New Tab Page, Download, History... pages).
536  // - similar languages (ex: en-US to en).
537  // - any user black-listed URLs or user selected language combination.
538  // - any language the user configured as accepted languages.
539  GURL page_url = web_contents->GetURL();
540  if (!IsTranslatableURL(page_url) ||
541      language_code == target_lang ||
542      !TranslatePrefs::CanTranslate(prefs, language_code, page_url) ||
543      IsAcceptLanguage(web_contents, language_code)) {
544    return;
545  }
546
547  // If the user has previously selected "always translate" for this language we
548  // automatically translate.  Note that in incognito mode we disable that
549  // feature; the user will get an infobar, so they can control whether the
550  // page's text is sent to the translate server.
551  std::string auto_target_lang;
552  if (!web_contents->GetBrowserContext()->IsOffTheRecord() &&
553      TranslatePrefs::ShouldAutoTranslate(prefs, language_code,
554          &auto_target_lang)) {
555    // We need to confirm that the saved target language is still supported.
556    // Also, GetLanguageCode will take care of removing country code if any.
557    auto_target_lang = GetLanguageCode(auto_target_lang);
558    if (IsSupportedLanguage(auto_target_lang)) {
559      TranslatePage(web_contents, language_code, auto_target_lang);
560      return;
561    }
562  }
563
564  TranslateTabHelper* translate_tab_helper =
565      TranslateTabHelper::FromWebContents(web_contents);
566  if (!translate_tab_helper)
567    return;
568
569  std::string auto_translate_to =
570      translate_tab_helper->language_state().AutoTranslateTo();
571  if (!auto_translate_to.empty()) {
572    // This page was navigated through a click from a translated page.
573    TranslatePage(web_contents, language_code, auto_translate_to);
574    return;
575  }
576
577  // Prompts the user if he/she wants the page translated.
578  TranslateInfoBarDelegate::Create(
579      InfoBarService::FromWebContents(web_contents), false,
580      TranslateInfoBarDelegate::BEFORE_TRANSLATE, TranslateErrors::NONE,
581      profile->GetPrefs(), ShortcutConfig(),
582      language_code, target_lang);
583}
584
585void TranslateManager::InitiateTranslationPosted(
586    int process_id, int render_id, const std::string& page_lang) {
587  // The tab might have been closed.
588  WebContents* web_contents =
589      tab_util::GetWebContentsByID(process_id, render_id);
590  if (!web_contents)
591    return;
592
593  TranslateTabHelper* translate_tab_helper =
594      TranslateTabHelper::FromWebContents(web_contents);
595  if (translate_tab_helper->language_state().translation_pending())
596    return;
597
598  InitiateTranslation(web_contents, GetLanguageCode(page_lang));
599}
600
601void TranslateManager::TranslatePage(WebContents* web_contents,
602                                     const std::string& source_lang,
603                                     const std::string& target_lang) {
604  NavigationEntry* entry = web_contents->GetController().GetActiveEntry();
605  if (!entry) {
606    NOTREACHED();
607    return;
608  }
609
610  Profile* profile =
611      Profile::FromBrowserContext(web_contents->GetBrowserContext());
612
613  TranslateInfoBarDelegate::Create(
614      InfoBarService::FromWebContents(web_contents), true,
615      TranslateInfoBarDelegate::TRANSLATING, TranslateErrors::NONE,
616      profile->GetPrefs(), ShortcutConfig(), source_lang, target_lang);
617
618  if (!translate_script_.empty()) {
619    DoTranslatePage(web_contents, translate_script_, source_lang, target_lang);
620    return;
621  }
622
623  // The script is not available yet.  Queue that request and query for the
624  // script.  Once it is downloaded we'll do the translate.
625  content::RenderViewHost* rvh = web_contents->GetRenderViewHost();
626  PendingRequest request;
627  request.render_process_id = rvh->GetProcess()->GetID();
628  request.render_view_id = rvh->GetRoutingID();
629  request.page_id = entry->GetPageID();
630  request.source_lang = source_lang;
631  request.target_lang = target_lang;
632  pending_requests_.push_back(request);
633  RequestTranslateScript();
634}
635
636void TranslateManager::RevertTranslation(WebContents* web_contents) {
637  NavigationEntry* entry = web_contents->GetController().GetActiveEntry();
638  if (!entry) {
639    NOTREACHED();
640    return;
641  }
642  web_contents->GetRenderViewHost()->Send(new ChromeViewMsg_RevertTranslation(
643      web_contents->GetRenderViewHost()->GetRoutingID(), entry->GetPageID()));
644
645  TranslateTabHelper* translate_tab_helper =
646      TranslateTabHelper::FromWebContents(web_contents);
647  translate_tab_helper->language_state().set_current_language(
648      translate_tab_helper->language_state().original_language());
649}
650
651void TranslateManager::ReportLanguageDetectionError(WebContents* web_contents) {
652  UMA_HISTOGRAM_COUNTS("Translate.ReportLanguageDetectionError", 1);
653  // We'll open the URL in a new tab so that the user can tell us more.
654  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
655  if (!browser) {
656    NOTREACHED();
657    return;
658  }
659
660  GURL report_error_url = GURL(kReportLanguageDetectionErrorURL);
661
662  GURL page_url = web_contents->GetController().GetActiveEntry()->GetURL();
663  report_error_url = net::AppendQueryParameter(
664      report_error_url,
665      kUrlQueryName,
666      page_url.spec());
667
668  TranslateTabHelper* translate_tab_helper =
669      TranslateTabHelper::FromWebContents(web_contents);
670  report_error_url = net::AppendQueryParameter(
671      report_error_url,
672      kSourceLanguageQueryName,
673      translate_tab_helper->language_state().original_language());
674
675  report_error_url = AddHostLocaleToUrl(report_error_url);
676  report_error_url = AddApiKeyToUrl(report_error_url);
677
678  chrome::AddSelectedTabWithURL(browser, report_error_url,
679                                content::PAGE_TRANSITION_AUTO_BOOKMARK);
680}
681
682void TranslateManager::DoTranslatePage(WebContents* web_contents,
683                                       const std::string& translate_script,
684                                       const std::string& source_lang,
685                                       const std::string& target_lang) {
686  NavigationEntry* entry = web_contents->GetController().GetActiveEntry();
687  if (!entry) {
688    NOTREACHED();
689    return;
690  }
691
692  TranslateTabHelper* translate_tab_helper =
693      TranslateTabHelper::FromWebContents(web_contents);
694  if (!translate_tab_helper)
695    return;
696
697  translate_tab_helper->language_state().set_translation_pending(true);
698  web_contents->GetRenderViewHost()->Send(new ChromeViewMsg_TranslatePage(
699      web_contents->GetRenderViewHost()->GetRoutingID(), entry->GetPageID(),
700      translate_script, source_lang, target_lang));
701}
702
703void TranslateManager::PageTranslated(WebContents* web_contents,
704                                      PageTranslatedDetails* details) {
705  if ((details->error_type == TranslateErrors::NONE) &&
706      !IsSupportedLanguage(details->source_language)) {
707    // TODO(jcivelli): http://crbug.com/9390 We should change the "after
708    //                 translate" infobar to support unknown as the original
709    //                 language.
710    UMA_HISTOGRAM_COUNTS("Translate.ServerReportedUnsupportedLanguage", 1);
711    details->error_type = TranslateErrors::UNSUPPORTED_LANGUAGE;
712  }
713  Profile* profile =
714      Profile::FromBrowserContext(web_contents->GetBrowserContext());
715  PrefService* prefs = profile->GetPrefs();
716  TranslateInfoBarDelegate::Create(
717      InfoBarService::FromWebContents(web_contents), true,
718      (details->error_type == TranslateErrors::NONE) ?
719          TranslateInfoBarDelegate::AFTER_TRANSLATE :
720          TranslateInfoBarDelegate::TRANSLATION_ERROR,
721      details->error_type, prefs, ShortcutConfig(), details->source_language,
722      details->target_language);
723}
724
725bool TranslateManager::IsAcceptLanguage(WebContents* web_contents,
726                                        const std::string& language) {
727  Profile* profile =
728      Profile::FromBrowserContext(web_contents->GetBrowserContext());
729  profile = profile->GetOriginalProfile();
730  PrefService* pref_service = profile->GetPrefs();
731  PrefServiceLanguagesMap::const_iterator iter =
732      accept_languages_.find(pref_service);
733  if (iter == accept_languages_.end()) {
734    InitAcceptLanguages(pref_service);
735    // Listen for this profile going away, in which case we would need to clear
736    // the accepted languages for the profile.
737    notification_registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
738                                content::Source<Profile>(profile));
739    // Also start listening for changes in the accept languages.
740    DCHECK(pref_change_registrars_.find(pref_service) ==
741           pref_change_registrars_.end());
742    PrefChangeRegistrar* pref_change_registrar = new PrefChangeRegistrar;
743    pref_change_registrar->Init(pref_service);
744    pref_change_registrar->Add(
745        prefs::kAcceptLanguages,
746        base::Bind(&TranslateManager::InitAcceptLanguages,
747                   base::Unretained(this),
748                   pref_service));
749    pref_change_registrars_[pref_service] = pref_change_registrar;
750
751    iter = accept_languages_.find(pref_service);
752  }
753
754  return iter->second.count(language) != 0;
755}
756
757void TranslateManager::InitAcceptLanguages(PrefService* prefs) {
758  // We have been asked for this profile, build the languages.
759  std::string accept_langs_str = prefs->GetString(prefs::kAcceptLanguages);
760  std::vector<std::string> accept_langs_list;
761  LanguageSet accept_langs_set;
762  base::SplitString(accept_langs_str, ',', &accept_langs_list);
763  std::vector<std::string>::const_iterator iter;
764  std::string ui_lang =
765      GetLanguageCode(g_browser_process->GetApplicationLocale());
766  bool is_ui_english = StartsWithASCII(ui_lang, "en-", false);
767  for (iter = accept_langs_list.begin();
768       iter != accept_langs_list.end(); ++iter) {
769    // Get rid of the locale extension if any (ex: en-US -> en), but for Chinese
770    // for which the CLD reports zh-CN and zh-TW.
771    std::string accept_lang(*iter);
772    size_t index = iter->find("-");
773    if (index != std::string::npos && *iter != "zh-CN" && *iter != "zh-TW")
774      accept_lang = iter->substr(0, index);
775    // Special-case English until we resolve bug 36182 properly.
776    // Add English only if the UI language is not English. This will annoy
777    // users of non-English Chrome who can comprehend English until English is
778    // black-listed.
779    // TODO(jungshik): Once we determine that it's safe to remove English from
780    // the default Accept-Language values for most locales, remove this
781    // special-casing.
782    if (accept_lang != "en" || is_ui_english)
783      accept_langs_set.insert(accept_lang);
784  }
785  accept_languages_[prefs] = accept_langs_set;
786}
787
788void TranslateManager::FetchLanguageListFromTranslateServer(
789    PrefService* prefs) {
790  if (language_list_request_pending_.get() != NULL)
791    return;
792
793  // We don't want to do this when translate is disabled.
794  DCHECK(prefs != NULL);
795  if (CommandLine::ForCurrentProcess()->HasSwitch(
796      switches::kDisableTranslate) ||
797      (prefs != NULL && !prefs->GetBoolean(prefs::kEnableTranslate))) {
798    return;
799  }
800
801  GURL language_list_fetch_url = GURL(kLanguageListFetchURL);
802  language_list_fetch_url = AddHostLocaleToUrl(language_list_fetch_url);
803  language_list_fetch_url = AddApiKeyToUrl(language_list_fetch_url);
804
805  const CommandLine& command_line = *CommandLine::ForCurrentProcess();
806  if (command_line.HasSwitch(switches::kEnableTranslateAlphaLanguages)) {
807    language_list_fetch_url = net::AppendQueryParameter(
808        language_list_fetch_url,
809        kAlphaLanguageQueryName,
810        kAlphaLanguageQueryValue);
811  }
812
813  VLOG(9) << "Fetch supporting language list from: "
814          << language_list_fetch_url.spec().c_str();
815
816  language_list_request_pending_.reset(net::URLFetcher::Create(
817      1, language_list_fetch_url, net::URLFetcher::GET, this));
818  language_list_request_pending_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
819                                               net::LOAD_DO_NOT_SAVE_COOKIES);
820  language_list_request_pending_->SetRequestContext(
821      g_browser_process->system_request_context());
822  language_list_request_pending_->SetMaxRetriesOn5xx(
823      kMaxRetryLanguageListFetch);
824  language_list_request_pending_->Start();
825}
826
827void TranslateManager::CleanupPendingUlrFetcher() {
828  language_list_request_pending_.reset();
829  translate_script_request_pending_.reset();
830}
831
832void TranslateManager::RequestTranslateScript() {
833  if (translate_script_request_pending_.get() != NULL)
834    return;
835
836  GURL translate_script_url;
837  // Check if command-line contains an alternative URL for translate service.
838  const CommandLine& command_line = *CommandLine::ForCurrentProcess();
839  if (command_line.HasSwitch(switches::kTranslateScriptURL)) {
840    translate_script_url = GURL(
841        command_line.GetSwitchValueASCII(switches::kTranslateScriptURL));
842    if (!translate_script_url.is_valid() ||
843        !translate_script_url.query().empty()) {
844      LOG(WARNING) << "The following translate URL specified at the "
845                   << "command-line is invalid: "
846                   << translate_script_url.spec();
847      translate_script_url = GURL();
848    }
849  }
850  // Use default URL when command-line argument is not specified, or specified
851  // URL is invalid.
852  if (translate_script_url.is_empty())
853    translate_script_url = GURL(kTranslateScriptURL);
854
855  translate_script_url = net::AppendQueryParameter(
856      translate_script_url,
857      kCallbackQueryName,
858      kCallbackQueryValue);
859  translate_script_url = AddHostLocaleToUrl(translate_script_url);
860  translate_script_url = AddApiKeyToUrl(translate_script_url);
861
862  translate_script_request_pending_.reset(net::URLFetcher::Create(
863      0, translate_script_url, net::URLFetcher::GET, this));
864  translate_script_request_pending_->SetLoadFlags(
865      net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES);
866  translate_script_request_pending_->SetRequestContext(
867      g_browser_process->system_request_context());
868  translate_script_request_pending_->SetExtraRequestHeaders(
869      kTranslateScriptHeader);
870  translate_script_request_pending_->Start();
871}
872
873// static
874std::string TranslateManager::GetTargetLanguage(PrefService* prefs) {
875  std::string ui_lang =
876    GetLanguageCode(g_browser_process->GetApplicationLocale());
877  if (IsSupportedLanguage(ui_lang))
878    return ui_lang;
879
880  // Getting the accepted languages list
881  std::string accept_langs_str = prefs->GetString(prefs::kAcceptLanguages);
882
883  std::vector<std::string> accept_langs_list;
884  base::SplitString(accept_langs_str, ',', &accept_langs_list);
885
886  // Will translate to the first supported language on the Accepted Language
887  // list or not at all if no such candidate exists
888  std::vector<std::string>::iterator iter;
889  for (iter = accept_langs_list.begin();
890       iter != accept_langs_list.end(); ++iter) {
891    std::string lang_code = GetLanguageCode(*iter);
892    if (IsSupportedLanguage(lang_code))
893      return lang_code;
894  }
895  return std::string();
896}
897
898// static
899ShortcutConfiguration TranslateManager::ShortcutConfig() {
900  ShortcutConfiguration config;
901
902  // The android implementation does not offer a drop down for space
903  // reason so we are more aggressive showing the shortcuts for never translate.
904  #if defined(OS_ANDROID)
905  config.never_translate_min_count = 1;
906  #else
907  config.never_translate_min_count = 3;
908  #endif  // defined(OS_ANDROID)
909
910  config.always_translate_min_count = 3;
911  return config;
912}
913