Fixes option-right in textfields when VoiceOver is set to read to the right of the...
[chromium-blink-merge.git] / chrome / renderer / translate_helper.cc
blob4c63436c860be9bf3f1573cd3b88a992f2fca2e7
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.
5 #include "chrome/renderer/translate_helper.h"
7 #include "base/bind.h"
8 #include "base/compiler_specific.h"
9 #include "base/logging.h"
10 #include "base/message_loop.h"
11 #include "base/metrics/histogram.h"
12 #include "base/utf_string_conversions.h"
13 #include "chrome/common/chrome_constants.h"
14 #include "chrome/common/render_messages.h"
15 #include "content/public/renderer/render_view.h"
16 #include "third_party/WebKit/Source/WebKit/chromium/public/WebDocument.h"
17 #include "third_party/WebKit/Source/WebKit/chromium/public/WebElement.h"
18 #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h"
19 #include "third_party/WebKit/Source/WebKit/chromium/public/WebScriptSource.h"
20 #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h"
21 #include "third_party/cld/encodings/compact_lang_det/win/cld_unicodetext.h"
22 #include "v8/include/v8.h"
23 #include "webkit/glue/dom_operations.h"
25 using WebKit::WebDocument;
26 using WebKit::WebElement;
27 using WebKit::WebFrame;
28 using WebKit::WebScriptSource;
29 using WebKit::WebString;
30 using WebKit::WebView;
32 // The delay in milliseconds that we'll wait before checking to see if the
33 // translate library injected in the page is ready.
34 static const int kTranslateInitCheckDelayMs = 150;
36 // The maximum number of times we'll check to see if the translate library
37 // injected in the page is ready.
38 static const int kMaxTranslateInitCheckAttempts = 5;
40 // The delay we wait in milliseconds before checking whether the translation has
41 // finished.
42 static const int kTranslateStatusCheckDelayMs = 400;
44 // Language name passed to the Translate element for it to detect the language.
45 static const char* const kAutoDetectionLanguage = "auto";
47 ////////////////////////////////////////////////////////////////////////////////
48 // TranslateHelper, public:
50 TranslateHelper::TranslateHelper(content::RenderView* render_view)
51 : content::RenderViewObserver(render_view),
52 translation_pending_(false),
53 page_id_(-1),
54 ALLOW_THIS_IN_INITIALIZER_LIST(weak_method_factory_(this)) {
57 TranslateHelper::~TranslateHelper() {
58 CancelPendingTranslation();
61 void TranslateHelper::PageCaptured(const string16& contents) {
62 WebDocument document = render_view()->GetWebView()->mainFrame()->document();
63 // If the page explicitly specifies a language, use it, otherwise we'll
64 // determine it based on the text content using the CLD.
65 std::string language = GetPageLanguageFromMetaTag(&document);
66 if (language.empty()) {
67 base::TimeTicks begin_time = base::TimeTicks::Now();
68 language = DetermineTextLanguage(contents);
69 UMA_HISTOGRAM_MEDIUM_TIMES("Renderer4.LanguageDetection",
70 base::TimeTicks::Now() - begin_time);
71 } else {
72 VLOG(1) << "PageLanguageFromMetaTag: " << language;
75 Send(new ChromeViewHostMsg_TranslateLanguageDetermined(
76 routing_id(), language, IsPageTranslatable(&document)));
79 void TranslateHelper::CancelPendingTranslation() {
80 weak_method_factory_.InvalidateWeakPtrs();
81 translation_pending_ = false;
82 page_id_ = -1;
83 source_lang_.clear();
84 target_lang_.clear();
87 // static
88 bool TranslateHelper::IsPageTranslatable(WebDocument* document) {
89 std::vector<WebElement> meta_elements;
90 webkit_glue::GetMetaElementsWithAttribute(document,
91 ASCIIToUTF16("name"),
92 ASCIIToUTF16("google"),
93 &meta_elements);
94 std::vector<WebElement>::const_iterator iter;
95 for (iter = meta_elements.begin(); iter != meta_elements.end(); ++iter) {
96 WebString attribute = iter->getAttribute("value");
97 if (attribute.isNull()) // We support both 'value' and 'content'.
98 attribute = iter->getAttribute("content");
99 if (attribute.isNull())
100 continue;
101 if (LowerCaseEqualsASCII(attribute, "notranslate"))
102 return false;
104 return true;
107 // static
108 std::string TranslateHelper::GetPageLanguageFromMetaTag(WebDocument* document) {
109 // The META language tag looks like:
110 // <meta http-equiv="content-language" content="en">
111 // It can contain more than one language:
112 // <meta http-equiv="content-language" content="en, fr">
113 std::vector<WebElement> meta_elements;
114 webkit_glue::GetMetaElementsWithAttribute(document,
115 ASCIIToUTF16("http-equiv"),
116 ASCIIToUTF16("content-language"),
117 &meta_elements);
118 if (meta_elements.empty())
119 return std::string();
121 // We don't expect more than one such tag. If there are several, just use the
122 // first one.
123 WebString attribute = meta_elements[0].getAttribute("content");
124 if (attribute.isEmpty())
125 return std::string();
127 // The value is supposed to be ASCII.
128 if (!IsStringASCII(attribute))
129 return std::string();
131 std::string language = StringToLowerASCII(UTF16ToASCII(attribute));
132 size_t coma_index = language.find(',');
133 if (coma_index != std::string::npos) {
134 // There are more than 1 language specified, just keep the first one.
135 language = language.substr(0, coma_index);
137 TrimWhitespaceASCII(language, TRIM_ALL, &language);
138 return language;
141 // static
142 std::string TranslateHelper::DetermineTextLanguage(const string16& text) {
143 std::string language = chrome::kUnknownLanguageCode;
144 int num_languages = 0;
145 int text_bytes = 0;
146 bool is_reliable = false;
147 Language cld_language =
148 DetectLanguageOfUnicodeText(NULL, text.c_str(), true, &is_reliable,
149 &num_languages, NULL, &text_bytes);
150 // We don't trust the result if the CLD reports that the detection is not
151 // reliable, or if the actual text used to detect the language was less than
152 // 100 bytes (short texts can often lead to wrong results).
153 if (is_reliable && text_bytes >= 100 && cld_language != NUM_LANGUAGES &&
154 cld_language != UNKNOWN_LANGUAGE && cld_language != TG_UNKNOWN_LANGUAGE) {
155 // We should not use LanguageCode_ISO_639_1 because it does not cover all
156 // the languages CLD can detect. As a result, it'll return the invalid
157 // language code for tradtional Chinese among others.
158 // |LanguageCodeWithDialect| will go through ISO 639-1, ISO-639-2 and
159 // 'other' tables to do the 'right' thing. In addition, it'll return zh-CN
160 // for Simplified Chinese.
161 language = LanguageCodeWithDialects(cld_language);
163 VLOG(1) << "Detected lang_id: " << language << ", from Text:\n" << text
164 << "\n*************************************\n";
165 return language;
168 ////////////////////////////////////////////////////////////////////////////////
169 // TranslateHelper, protected:
171 bool TranslateHelper::IsTranslateLibAvailable() {
172 bool lib_available = false;
173 if (!ExecuteScriptAndGetBoolResult(
174 "typeof cr != 'undefined' && typeof cr.googleTranslate != 'undefined' && "
175 "typeof cr.googleTranslate.translate == 'function'", &lib_available)) {
176 NOTREACHED();
177 return false;
179 return lib_available;
182 bool TranslateHelper::IsTranslateLibReady() {
183 bool lib_ready = false;
184 if (!ExecuteScriptAndGetBoolResult("cr.googleTranslate.libReady",
185 &lib_ready)) {
186 NOTREACHED();
187 return false;
189 return lib_ready;
192 bool TranslateHelper::HasTranslationFinished() {
193 bool translation_finished = false;
194 if (!ExecuteScriptAndGetBoolResult("cr.googleTranslate.finished",
195 &translation_finished)) {
196 NOTREACHED() << "crGoogleTranslateGetFinished returned unexpected value.";
197 return true;
200 return translation_finished;
203 bool TranslateHelper::HasTranslationFailed() {
204 bool translation_failed = false;
205 if (!ExecuteScriptAndGetBoolResult("cr.googleTranslate.error",
206 &translation_failed)) {
207 NOTREACHED() << "crGoogleTranslateGetError returned unexpected value.";
208 return true;
211 return translation_failed;
214 bool TranslateHelper::StartTranslation() {
215 bool translate_success = false;
216 if (!ExecuteScriptAndGetBoolResult("cr.googleTranslate.translate('" +
217 source_lang_ + "','" + target_lang_ + "')",
218 &translate_success)) {
219 NOTREACHED();
220 return false;
222 return translate_success;
225 std::string TranslateHelper::GetOriginalPageLanguage() {
226 std::string lang;
227 ExecuteScriptAndGetStringResult("cr.googleTranslate.sourceLang", &lang);
228 return lang;
231 bool TranslateHelper::DontDelayTasks() {
232 return false;
235 ////////////////////////////////////////////////////////////////////////////////
236 // TranslateHelper, private:
239 bool TranslateHelper::OnMessageReceived(const IPC::Message& message) {
240 bool handled = true;
241 IPC_BEGIN_MESSAGE_MAP(TranslateHelper, message)
242 IPC_MESSAGE_HANDLER(ChromeViewMsg_TranslatePage, OnTranslatePage)
243 IPC_MESSAGE_HANDLER(ChromeViewMsg_RevertTranslation, OnRevertTranslation)
244 IPC_MESSAGE_UNHANDLED(handled = false)
245 IPC_END_MESSAGE_MAP()
246 return handled;
249 void TranslateHelper::OnTranslatePage(int page_id,
250 const std::string& translate_script,
251 const std::string& source_lang,
252 const std::string& target_lang) {
253 if (render_view()->GetPageId() != page_id)
254 return; // We navigated away, nothing to do.
256 if (translation_pending_ && page_id == page_id_ &&
257 target_lang_ == target_lang) {
258 // A similar translation is already under way, nothing to do.
259 return;
262 // Any pending translation is now irrelevant.
263 CancelPendingTranslation();
265 // Set our states.
266 translation_pending_ = true;
267 page_id_ = page_id;
268 // If the source language is undetermined, we'll let the translate element
269 // detect it.
270 source_lang_ = (source_lang != chrome::kUnknownLanguageCode) ?
271 source_lang : kAutoDetectionLanguage;
272 target_lang_ = target_lang;
274 if (!IsTranslateLibAvailable()) {
275 // Evaluate the script to add the translation related method to the global
276 // context of the page.
277 ExecuteScript(translate_script);
278 DCHECK(IsTranslateLibAvailable());
281 TranslatePageImpl(0);
284 void TranslateHelper::OnRevertTranslation(int page_id) {
285 if (render_view()->GetPageId() != page_id)
286 return; // We navigated away, nothing to do.
288 if (!IsTranslateLibAvailable()) {
289 NOTREACHED();
290 return;
293 WebFrame* main_frame = GetMainFrame();
294 if (!main_frame)
295 return;
297 CancelPendingTranslation();
299 main_frame->executeScript(
300 WebScriptSource(ASCIIToUTF16("cr.googleTranslate.revert()")));
303 void TranslateHelper::CheckTranslateStatus() {
304 // If this is not the same page, the translation has been canceled. If the
305 // view is gone, the page is closing.
306 if (page_id_ != render_view()->GetPageId() || !render_view()->GetWebView())
307 return;
309 // First check if there was an error.
310 if (HasTranslationFailed()) {
311 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR);
312 return; // There was an error.
315 if (HasTranslationFinished()) {
316 std::string actual_source_lang;
317 // Translation was successfull, if it was auto, retrieve the source
318 // language the Translate Element detected.
319 if (source_lang_ == kAutoDetectionLanguage) {
320 actual_source_lang = GetOriginalPageLanguage();
321 if (actual_source_lang.empty()) {
322 NotifyBrowserTranslationFailed(TranslateErrors::UNKNOWN_LANGUAGE);
323 return;
324 } else if (actual_source_lang == target_lang_) {
325 NotifyBrowserTranslationFailed(TranslateErrors::IDENTICAL_LANGUAGES);
326 return;
328 } else {
329 actual_source_lang = source_lang_;
332 if (!translation_pending_) {
333 NOTREACHED();
334 return;
337 translation_pending_ = false;
339 // Notify the browser we are done.
340 render_view()->Send(new ChromeViewHostMsg_PageTranslated(
341 render_view()->GetRoutingID(), render_view()->GetPageId(),
342 actual_source_lang, target_lang_, TranslateErrors::NONE));
343 return;
346 // The translation is still pending, check again later.
347 MessageLoop::current()->PostDelayedTask(
348 FROM_HERE,
349 base::Bind(&TranslateHelper::CheckTranslateStatus,
350 weak_method_factory_.GetWeakPtr()),
351 base::TimeDelta::FromMilliseconds(
352 DontDelayTasks() ? 0 : kTranslateStatusCheckDelayMs));
355 bool TranslateHelper::ExecuteScript(const std::string& script) {
356 WebFrame* main_frame = GetMainFrame();
357 if (!main_frame)
358 return false;
359 main_frame->executeScript(WebScriptSource(ASCIIToUTF16(script)));
360 return true;
363 bool TranslateHelper::ExecuteScriptAndGetBoolResult(const std::string& script,
364 bool* value) {
365 DCHECK(value);
366 WebFrame* main_frame = GetMainFrame();
367 if (!main_frame)
368 return false;
370 v8::Handle<v8::Value> v = main_frame->executeScriptAndReturnValue(
371 WebScriptSource(ASCIIToUTF16(script)));
372 if (v.IsEmpty() || !v->IsBoolean())
373 return false;
375 *value = v->BooleanValue();
376 return true;
379 bool TranslateHelper::ExecuteScriptAndGetStringResult(const std::string& script,
380 std::string* value) {
381 DCHECK(value);
382 WebFrame* main_frame = GetMainFrame();
383 if (!main_frame)
384 return false;
386 v8::Handle<v8::Value> v = main_frame->executeScriptAndReturnValue(
387 WebScriptSource(ASCIIToUTF16(script)));
388 if (v.IsEmpty() || !v->IsString())
389 return false;
391 v8::Local<v8::String> v8_str = v->ToString();
392 int length = v8_str->Utf8Length() + 1;
393 scoped_array<char> str(new char[length]);
394 v8_str->WriteUtf8(str.get(), length);
395 *value = str.get();
396 return true;
399 void TranslateHelper::TranslatePageImpl(int count) {
400 DCHECK_LT(count, kMaxTranslateInitCheckAttempts);
401 if (page_id_ != render_view()->GetPageId() || !render_view()->GetWebView())
402 return;
404 if (!IsTranslateLibReady()) {
405 // The library is not ready, try again later, unless we have tried several
406 // times unsucessfully already.
407 if (++count >= kMaxTranslateInitCheckAttempts) {
408 NotifyBrowserTranslationFailed(TranslateErrors::INITIALIZATION_ERROR);
409 return;
411 MessageLoop::current()->PostDelayedTask(
412 FROM_HERE,
413 base::Bind(&TranslateHelper::TranslatePageImpl,
414 weak_method_factory_.GetWeakPtr(), count),
415 base::TimeDelta::FromMilliseconds(
416 DontDelayTasks() ? 0 : count * kTranslateInitCheckDelayMs));
417 return;
420 if (!StartTranslation()) {
421 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR);
422 return;
424 // Check the status of the translation.
425 MessageLoop::current()->PostDelayedTask(
426 FROM_HERE,
427 base::Bind(&TranslateHelper::CheckTranslateStatus,
428 weak_method_factory_.GetWeakPtr()),
429 base::TimeDelta::FromMilliseconds(
430 DontDelayTasks() ? 0 : kTranslateStatusCheckDelayMs));
433 void TranslateHelper::NotifyBrowserTranslationFailed(
434 TranslateErrors::Type error) {
435 translation_pending_ = false;
436 // Notify the browser there was an error.
437 render_view()->Send(new ChromeViewHostMsg_PageTranslated(
438 render_view()->GetRoutingID(), page_id_, source_lang_,
439 target_lang_, error));
442 WebFrame* TranslateHelper::GetMainFrame() {
443 WebView* web_view = render_view()->GetWebView();
444 if (!web_view) {
445 // When the WebView is going away, the render view should have called
446 // CancelPendingTranslation() which should have stopped any pending work, so
447 // that case should not happen.
448 NOTREACHED();
449 return NULL;
451 return web_view->mainFrame();