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"
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
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),
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
);
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;
88 bool TranslateHelper::IsPageTranslatable(WebDocument
* document
) {
89 std::vector
<WebElement
> meta_elements
;
90 webkit_glue::GetMetaElementsWithAttribute(document
,
92 ASCIIToUTF16("google"),
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())
101 if (LowerCaseEqualsASCII(attribute
, "notranslate"))
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"),
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
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
);
142 std::string
TranslateHelper::DetermineTextLanguage(const string16
& text
) {
143 std::string language
= chrome::kUnknownLanguageCode
;
144 int num_languages
= 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";
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
)) {
179 return lib_available
;
182 bool TranslateHelper::IsTranslateLibReady() {
183 bool lib_ready
= false;
184 if (!ExecuteScriptAndGetBoolResult("cr.googleTranslate.libReady",
192 bool TranslateHelper::HasTranslationFinished() {
193 bool translation_finished
= false;
194 if (!ExecuteScriptAndGetBoolResult("cr.googleTranslate.finished",
195 &translation_finished
)) {
196 NOTREACHED() << "crGoogleTranslateGetFinished returned unexpected value.";
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.";
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
)) {
222 return translate_success
;
225 std::string
TranslateHelper::GetOriginalPageLanguage() {
227 ExecuteScriptAndGetStringResult("cr.googleTranslate.sourceLang", &lang
);
231 bool TranslateHelper::DontDelayTasks() {
235 ////////////////////////////////////////////////////////////////////////////////
236 // TranslateHelper, private:
239 bool TranslateHelper::OnMessageReceived(const IPC::Message
& message
) {
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()
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.
262 // Any pending translation is now irrelevant.
263 CancelPendingTranslation();
266 translation_pending_
= true;
268 // If the source language is undetermined, we'll let the translate element
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()) {
293 WebFrame
* main_frame
= GetMainFrame();
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())
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
);
324 } else if (actual_source_lang
== target_lang_
) {
325 NotifyBrowserTranslationFailed(TranslateErrors::IDENTICAL_LANGUAGES
);
329 actual_source_lang
= source_lang_
;
332 if (!translation_pending_
) {
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
));
346 // The translation is still pending, check again later.
347 MessageLoop::current()->PostDelayedTask(
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();
359 main_frame
->executeScript(WebScriptSource(ASCIIToUTF16(script
)));
363 bool TranslateHelper::ExecuteScriptAndGetBoolResult(const std::string
& script
,
366 WebFrame
* main_frame
= GetMainFrame();
370 v8::Handle
<v8::Value
> v
= main_frame
->executeScriptAndReturnValue(
371 WebScriptSource(ASCIIToUTF16(script
)));
372 if (v
.IsEmpty() || !v
->IsBoolean())
375 *value
= v
->BooleanValue();
379 bool TranslateHelper::ExecuteScriptAndGetStringResult(const std::string
& script
,
380 std::string
* value
) {
382 WebFrame
* main_frame
= GetMainFrame();
386 v8::Handle
<v8::Value
> v
= main_frame
->executeScriptAndReturnValue(
387 WebScriptSource(ASCIIToUTF16(script
)));
388 if (v
.IsEmpty() || !v
->IsString())
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
);
399 void TranslateHelper::TranslatePageImpl(int count
) {
400 DCHECK_LT(count
, kMaxTranslateInitCheckAttempts
);
401 if (page_id_
!= render_view()->GetPageId() || !render_view()->GetWebView())
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
);
411 MessageLoop::current()->PostDelayedTask(
413 base::Bind(&TranslateHelper::TranslatePageImpl
,
414 weak_method_factory_
.GetWeakPtr(), count
),
415 base::TimeDelta::FromMilliseconds(
416 DontDelayTasks() ? 0 : count
* kTranslateInitCheckDelayMs
));
420 if (!StartTranslation()) {
421 NotifyBrowserTranslationFailed(TranslateErrors::TRANSLATION_ERROR
);
424 // Check the status of the translation.
425 MessageLoop::current()->PostDelayedTask(
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();
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.
451 return web_view
->mainFrame();