summaryrefslogtreecommitdiffstats
path: root/components/translate
diff options
context:
space:
mode:
authordroger <droger@chromium.org>2014-12-16 13:58:13 -0800
committerCommit bot <commit-bot@chromium.org>2014-12-16 21:58:38 +0000
commitbfba8a470717488726b909c8208f15c6a4512673 (patch)
tree56df55800551a343f58e3f46207971d58c1ae645 /components/translate
parent634a76e456748851c4d51a89a6b466b3f2d972cc (diff)
downloadchromium_src-bfba8a470717488726b909c8208f15c6a4512673.zip
chromium_src-bfba8a470717488726b909c8208f15c6a4512673.tar.gz
chromium_src-bfba8a470717488726b909c8208f15c6a4512673.tar.bz2
Upstream components/translate/ios
Review URL: https://codereview.chromium.org/809693003 Cr-Commit-Position: refs/heads/master@{#308676}
Diffstat (limited to 'components/translate')
-rw-r--r--components/translate/ios/DEPS4
-rw-r--r--components/translate/ios/browser/ios_translate_driver.h128
-rw-r--r--components/translate/ios/browser/ios_translate_driver.mm271
-rw-r--r--components/translate/ios/browser/js_language_detection_manager.h39
-rw-r--r--components/translate/ios/browser/js_language_detection_manager.mm53
-rw-r--r--components/translate/ios/browser/js_translate_manager.h46
-rw-r--r--components/translate/ios/browser/js_translate_manager.mm84
-rw-r--r--components/translate/ios/browser/js_translate_manager_unittest.mm92
-rw-r--r--components/translate/ios/browser/language_detection_controller.h94
-rw-r--r--components/translate/ios/browser/language_detection_controller.mm146
-rw-r--r--components/translate/ios/browser/language_detection_controller_unittest.mm78
-rw-r--r--components/translate/ios/browser/resources/language_detection.js165
-rw-r--r--components/translate/ios/browser/resources/translate_ios.js78
-rw-r--r--components/translate/ios/browser/translate_controller.h106
-rw-r--r--components/translate/ios/browser/translate_controller.mm140
-rw-r--r--components/translate/ios/browser/translate_controller_unittest.mm142
16 files changed, 1666 insertions, 0 deletions
diff --git a/components/translate/ios/DEPS b/components/translate/ios/DEPS
new file mode 100644
index 0000000..4dd6307
--- /dev/null
+++ b/components/translate/ios/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+ "+ios/web/public",
+ "+third_party/ocmock",
+]
diff --git a/components/translate/ios/browser/ios_translate_driver.h b/components/translate/ios/browser/ios_translate_driver.h
new file mode 100644
index 0000000..ee47d44
--- /dev/null
+++ b/components/translate/ios/browser/ios_translate_driver.h
@@ -0,0 +1,128 @@
+// Copyright 2014 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.
+
+#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_IOS_TRANSLATE_DRIVER_H_
+#define COMPONENTS_TRANSLATE_IOS_BROWSER_IOS_TRANSLATE_DRIVER_H_
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/memory/weak_ptr.h"
+#include "components/translate/core/browser/translate_driver.h"
+#include "components/translate/ios/browser/language_detection_controller.h"
+#include "components/translate/ios/browser/translate_controller.h"
+#include "ios/web/public/web_state/web_state_observer.h"
+
+@class CRWJSInjectionReceiver;
+
+namespace web {
+class NavigationManager;
+class WebState;
+}
+
+namespace translate {
+
+class TranslateManager;
+
+// Content implementation of TranslateDriver.
+class IOSTranslateDriver : public TranslateDriver,
+ public TranslateController::Observer,
+ public web::WebStateObserver {
+ public:
+ IOSTranslateDriver(web::WebState* web_state,
+ web::NavigationManager* navigation_manager,
+ TranslateManager* translate_manager);
+ ~IOSTranslateDriver() override;
+
+ LanguageDetectionController* language_detection_controller() {
+ return language_detection_controller_.get();
+ }
+
+ TranslateController* translate_controller() {
+ return translate_controller_.get();
+ }
+
+ // web::WebStateObserver methods.
+ void NavigationItemCommitted(
+ const web::LoadCommittedDetails& load_details) override;
+
+ // TranslateDriver methods.
+ void OnIsPageTranslatedChanged() override;
+ void OnTranslateEnabledChanged() override;
+ bool IsLinkNavigation() override;
+ void TranslatePage(int page_seq_no,
+ const std::string& translate_script,
+ const std::string& source_lang,
+ const std::string& target_lang) override;
+ void RevertTranslation(int page_seq_no) override;
+ bool IsOffTheRecord() override;
+ const std::string& GetContentsMimeType() override;
+ const GURL& GetLastCommittedURL() override;
+ const GURL& GetActiveURL() override;
+ const GURL& GetVisibleURL() override;
+ bool HasCurrentPage() override;
+ void OpenUrlInNewTab(const GURL& url) override;
+
+ private:
+ // Called when the translation was successfull.
+ void TranslationDidSucceed(const std::string& source_lang,
+ const std::string& target_lang,
+ int page_seq_no,
+ const std::string& original_page_language,
+ double translation_time);
+ // Checks if the current running page translation is finished or errored and
+ // notifies the browser accordingly. If the translation has not terminated,
+ // posts a task to check again later.
+ // Similar to TranslateHelper::CheckTranslateStatus on desktop.
+ void CheckTranslateStatus(const std::string& source_language,
+ const std::string& target_language,
+ int page_seq_no);
+
+ // Returns true if the user has not navigated away and the the page is not
+ // being destroyed.
+ bool IsPageValid(int page_seq_no) const;
+
+ // Callback for LanguageDetectionController.
+ void OnLanguageDetermined(
+ const LanguageDetectionController::DetectionDetails& details);
+
+ // TranslateController::Observer methods.
+ void OnTranslateScriptReady(bool success,
+ double load_time,
+ double ready_time) override;
+ void OnTranslateComplete(bool success,
+ const std::string& original_language,
+ double translation_time) override;
+
+ // The navigation manager of the tab we are associated with.
+ web::NavigationManager* navigation_manager_;
+
+ base::WeakPtr<TranslateManager> translate_manager_;
+ scoped_ptr<TranslateController> translate_controller_;
+ scoped_ptr<LanguageDetectionController> language_detection_controller_;
+ scoped_ptr<LanguageDetectionController::CallbackList::Subscription>
+ language_detection_callback_subscription_;
+
+ // An ever-increasing sequence number of the current page, used to match up
+ // translation requests with responses.
+ // This matches the similar field in TranslateHelper in the renderer on other
+ // platforms.
+ int page_seq_no_;
+
+ // When a translation is in progress, its page sequence number is stored in
+ // |pending_page_seq_no_|.
+ int pending_page_seq_no_;
+
+ // Parameters of the current translation.
+ std::string source_language_;
+ std::string target_language_;
+
+ base::WeakPtrFactory<IOSTranslateDriver> weak_method_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(IOSTranslateDriver);
+};
+
+} // namespace translate
+
+#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_IOS_TRANSLATE_DRIVER_H_
diff --git a/components/translate/ios/browser/ios_translate_driver.mm b/components/translate/ios/browser/ios_translate_driver.mm
new file mode 100644
index 0000000..d6d2117
--- /dev/null
+++ b/components/translate/ios/browser/ios_translate_driver.mm
@@ -0,0 +1,271 @@
+// Copyright 2014 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 "components/translate/ios/browser/ios_translate_driver.h"
+
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/message_loop/message_loop.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/time/time.h"
+#include "components/translate/core/browser/translate_client.h"
+#include "components/translate/core/browser/translate_manager.h"
+#include "components/translate/core/common/translate_constants.h"
+#include "components/translate/core/common/translate_errors.h"
+#include "components/translate/core/common/translate_metrics.h"
+#import "components/translate/ios/browser/js_language_detection_manager.h"
+#import "components/translate/ios/browser/js_translate_manager.h"
+#import "components/translate/ios/browser/language_detection_controller.h"
+#import "components/translate/ios/browser/translate_controller.h"
+#include "ios/web/public/browser_state.h"
+#include "ios/web/public/load_committed_details.h"
+#include "ios/web/public/navigation_item.h"
+#include "ios/web/public/navigation_manager.h"
+#include "ios/web/public/referrer.h"
+#include "ios/web/public/web_state/js/crw_js_injection_receiver.h"
+#include "ios/web/public/web_state/web_state.h"
+#include "ui/base/page_transition_types.h"
+#include "ui/base/window_open_disposition.h"
+#include "url/gurl.h"
+
+namespace translate {
+
+namespace {
+// The delay we wait in milliseconds before checking whether the translation has
+// finished.
+// Note: This should be kept in sync with the constant of the same name in
+// translate_ios.js.
+const int kTranslateStatusCheckDelayMs = 400;
+// Language name passed to the Translate element for it to detect the language.
+const char kAutoDetectionLanguage[] = "auto";
+
+} // namespace
+
+IOSTranslateDriver::IOSTranslateDriver(
+ web::WebState* web_state,
+ web::NavigationManager* navigation_manager,
+ TranslateManager* translate_manager)
+ : web::WebStateObserver(web_state),
+ navigation_manager_(navigation_manager),
+ translate_manager_(translate_manager->GetWeakPtr()),
+ page_seq_no_(0),
+ pending_page_seq_no_(0),
+ weak_method_factory_(this) {
+ DCHECK(navigation_manager_);
+ DCHECK(translate_manager_);
+ DCHECK(web::WebStateObserver::web_state());
+
+ CRWJSInjectionReceiver* receiver = web_state->GetJSInjectionReceiver();
+ DCHECK(receiver);
+
+ // Create the language detection controller.
+ JsLanguageDetectionManager* language_detection_manager =
+ static_cast<JsLanguageDetectionManager*>(
+ [receiver instanceOfClass:[JsLanguageDetectionManager class]]);
+ language_detection_controller_.reset(new LanguageDetectionController(
+ web_state, language_detection_manager,
+ translate_manager_->translate_client()->GetPrefs()));
+ language_detection_callback_subscription_ =
+ language_detection_controller_->RegisterLanguageDetectionCallback(
+ base::Bind(&IOSTranslateDriver::OnLanguageDetermined,
+ base::Unretained(this)));
+ // Create the translate controller.
+ JsTranslateManager* js_translate_manager = static_cast<JsTranslateManager*>(
+ [receiver instanceOfClass:[JsTranslateManager class]]);
+ translate_controller_.reset(
+ new TranslateController(web_state, js_translate_manager));
+ translate_controller_->set_observer(this);
+}
+
+IOSTranslateDriver::~IOSTranslateDriver() {
+}
+
+void IOSTranslateDriver::OnLanguageDetermined(
+ const LanguageDetectionController::DetectionDetails& details) {
+ if (!translate_manager_)
+ return;
+ translate_manager_->GetLanguageState().LanguageDetermined(
+ details.adopted_language, true);
+
+ if (web_state())
+ translate_manager_->InitiateTranslation(details.adopted_language);
+}
+
+// web::WebStateObserver methods
+
+void IOSTranslateDriver::NavigationItemCommitted(
+ const web::LoadCommittedDetails& load_details) {
+ // Interrupt pending translations and reset various data when a navigation
+ // happens. Desktop does it by tracking changes in the page ID, and
+ // through WebContentObserver, but these concepts do not exist on iOS.
+ if (!load_details.is_in_page) {
+ ++page_seq_no_;
+ translate_manager_->set_current_seq_no(page_seq_no_);
+ }
+
+ // TODO(droger): support navigation types, like content/ does.
+ const bool reload = ui::PageTransitionCoreTypeIs(
+ load_details.item->GetTransitionType(), ui::PAGE_TRANSITION_RELOAD);
+ translate_manager_->GetLanguageState().DidNavigate(load_details.is_in_page,
+ true, reload);
+}
+
+// TranslateDriver methods
+
+bool IOSTranslateDriver::IsLinkNavigation() {
+ return navigation_manager_->GetVisibleItem() &&
+ ui::PageTransitionCoreTypeIs(
+ navigation_manager_->GetVisibleItem()->GetTransitionType(),
+ ui::PAGE_TRANSITION_LINK);
+}
+
+void IOSTranslateDriver::OnTranslateEnabledChanged() {
+}
+
+void IOSTranslateDriver::OnIsPageTranslatedChanged() {
+}
+
+void IOSTranslateDriver::TranslatePage(int page_seq_no,
+ const std::string& translate_script,
+ const std::string& source_lang,
+ const std::string& target_lang) {
+ if (page_seq_no != page_seq_no_)
+ return; // The user navigated away.
+ source_language_ = source_lang;
+ target_language_ = target_lang;
+ pending_page_seq_no_ = page_seq_no;
+ translate_controller_->InjectTranslateScript(translate_script);
+}
+
+void IOSTranslateDriver::RevertTranslation(int page_seq_no) {
+ if (page_seq_no != page_seq_no_)
+ return; // The user navigated away.
+ translate_controller_->RevertTranslation();
+}
+
+bool IOSTranslateDriver::IsOffTheRecord() {
+ return navigation_manager_->GetBrowserState()->IsOffTheRecord();
+}
+
+const std::string& IOSTranslateDriver::GetContentsMimeType() {
+ return web_state()->GetContentsMimeType();
+}
+
+const GURL& IOSTranslateDriver::GetLastCommittedURL() {
+ return web_state()->GetLastCommittedURL();
+}
+
+const GURL& IOSTranslateDriver::GetActiveURL() {
+ web::NavigationItem* item = navigation_manager_->GetVisibleItem();
+ if (!item)
+ return GURL::EmptyGURL();
+ return item->GetURL();
+}
+
+const GURL& IOSTranslateDriver::GetVisibleURL() {
+ return web_state()->GetVisibleURL();
+}
+
+bool IOSTranslateDriver::HasCurrentPage() {
+ return (navigation_manager_->GetVisibleItem() != nullptr);
+}
+
+void IOSTranslateDriver::OpenUrlInNewTab(const GURL& url) {
+ web::WebState::OpenURLParams params(url, web::Referrer(), NEW_FOREGROUND_TAB,
+ ui::PAGE_TRANSITION_LINK, false);
+ web_state()->OpenURL(params);
+}
+
+void IOSTranslateDriver::TranslationDidSucceed(
+ const std::string& source_lang,
+ const std::string& target_lang,
+ int page_seq_no,
+ const std::string& original_page_language,
+ double translation_time) {
+ if (!IsPageValid(page_seq_no))
+ return;
+ std::string actual_source_lang;
+ translate::TranslateErrors::Type translate_errors = TranslateErrors::NONE;
+ // Translation was successfull; if it was auto, retrieve the source
+ // language the Translate Element detected.
+ if (source_lang == kAutoDetectionLanguage) {
+ actual_source_lang = original_page_language;
+ if (actual_source_lang.empty()) {
+ translate_errors = TranslateErrors::UNKNOWN_LANGUAGE;
+ } else if (actual_source_lang == target_lang) {
+ translate_errors = TranslateErrors::IDENTICAL_LANGUAGES;
+ }
+ } else {
+ actual_source_lang = source_lang;
+ }
+ if (translate_errors == TranslateErrors::NONE)
+ translate::ReportTimeToTranslate(translation_time);
+ // Notify the manage of completion.
+ translate_manager_->PageTranslated(actual_source_lang, target_lang,
+ translate_errors);
+}
+
+void IOSTranslateDriver::CheckTranslateStatus(
+ const std::string& source_language,
+ const std::string& target_language,
+ int page_seq_no) {
+ if (!IsPageValid(page_seq_no))
+ return;
+ translate_controller_->CheckTranslateStatus();
+}
+
+bool IOSTranslateDriver::IsPageValid(int page_seq_no) const {
+ bool user_navigated_away = page_seq_no != page_seq_no_;
+ return !user_navigated_away && web_state();
+}
+
+// TranslateController::Observer implementation.
+
+void IOSTranslateDriver::OnTranslateScriptReady(bool success,
+ double load_time,
+ double ready_time) {
+ if (!IsPageValid(pending_page_seq_no_))
+ return;
+
+ if (!success) {
+ translate_manager_->PageTranslated(source_language_, target_language_,
+ TranslateErrors::INITIALIZATION_ERROR);
+ return;
+ }
+
+ translate::ReportTimeToLoad(load_time);
+ translate::ReportTimeToBeReady(ready_time);
+ const char kAutoDetectionLanguage[] = "auto";
+ std::string source = (source_language_ != translate::kUnknownLanguageCode)
+ ? source_language_
+ : kAutoDetectionLanguage;
+ translate_controller_->StartTranslation(source_language_, target_language_);
+ // Check the status of the translation -- after a delay.
+ base::MessageLoop::current()->PostDelayedTask(
+ FROM_HERE, base::Bind(&IOSTranslateDriver::CheckTranslateStatus,
+ weak_method_factory_.GetWeakPtr(), source_language_,
+ target_language_, pending_page_seq_no_),
+ base::TimeDelta::FromMilliseconds(kTranslateStatusCheckDelayMs));
+}
+
+void IOSTranslateDriver::OnTranslateComplete(
+ bool success,
+ const std::string& original_language,
+ double translation_time) {
+ if (!IsPageValid(pending_page_seq_no_))
+ return;
+
+ if (!success) {
+ // TODO(toyoshim): Check |errorCode| of translate.js and notify it here.
+ translate_manager_->PageTranslated(source_language_, target_language_,
+ TranslateErrors::TRANSLATION_ERROR);
+ }
+
+ TranslationDidSucceed(source_language_, target_language_,
+ pending_page_seq_no_, original_language,
+ translation_time);
+}
+
+} // namespace translate
diff --git a/components/translate/ios/browser/js_language_detection_manager.h b/components/translate/ios/browser/js_language_detection_manager.h
new file mode 100644
index 0000000..b7ca492
--- /dev/null
+++ b/components/translate/ios/browser/js_language_detection_manager.h
@@ -0,0 +1,39 @@
+// Copyright 2013 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.
+
+#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_JS_LANGUAGE_DETECTION_MANAGER_H_
+#define COMPONENTS_TRANSLATE_IOS_BROWSER_JS_LANGUAGE_DETECTION_MANAGER_H_
+
+#import <Foundation/Foundation.h>
+
+#include "base/callback_forward.h"
+#include "base/strings/string16.h"
+#import "ios/web/public/web_state/js/crw_js_injection_manager.h"
+
+namespace language_detection {
+
+// Maximum length of the extracted text returned by |-extractTextContent|.
+// Matches desktop implementation.
+extern const size_t kMaxIndexChars;
+
+// Type for the callback called when the buffered text is retrieved.
+using BufferedTextCallback = base::Callback<void(const base::string16&)>;
+
+} // namespace language_detection
+
+// JsLanguageDetectionManager manages the scripts related to language detection.
+@interface JsLanguageDetectionManager : CRWJSInjectionManager
+
+// Retrieves the cached text content of the page from the JS side. Calls
+// |callback| with the page's text contents. The cache is purged on the JS side
+// after this call. |callback| must be non null.
+- (void)retrieveBufferedTextContent:
+ (const language_detection::BufferedTextCallback&)callback;
+
+// Starts detecting the language of the page.
+- (void)startLanguageDetection;
+
+@end
+
+#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_JS_LANGUAGE_DETECTION_MANAGER_H_
diff --git a/components/translate/ios/browser/js_language_detection_manager.mm b/components/translate/ios/browser/js_language_detection_manager.mm
new file mode 100644
index 0000000..d352899
--- /dev/null
+++ b/components/translate/ios/browser/js_language_detection_manager.mm
@@ -0,0 +1,53 @@
+// Copyright 2013 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.
+
+#import "components/translate/ios/browser/js_language_detection_manager.h"
+
+#include "base/callback.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/strings/string_util.h"
+#include "base/strings/sys_string_conversions.h"
+#import "ios/web/public/web_state/js/crw_js_base_manager.h"
+#import "ios/web/public/web_state/js/crw_js_message_manager.h"
+
+namespace language_detection {
+// Note: This should stay in sync with the constant in language_detection.js.
+const size_t kMaxIndexChars = 65535;
+} // namespace language_detection
+
+@implementation JsLanguageDetectionManager
+
+#pragma mark - Protected methods
+
+- (NSString*)scriptPath {
+ return @"language_detection";
+}
+
+- (NSString*)presenceBeacon {
+ return @"__gCrWeb.languageDetection";
+}
+
+- (NSArray*)directDependencies {
+ return @[ [CRWJSBaseManager class], [CRWJSMessageManager class], ];
+}
+
+#pragma mark - Public methods
+
+- (void)startLanguageDetection {
+ [self evaluate:@"__gCrWeb.languageDetection.detectLanguage()"
+ stringResultHandler:nil];
+}
+
+- (void)retrieveBufferedTextContent:
+ (const language_detection::BufferedTextCallback&)callback {
+ DCHECK(!callback.is_null());
+ // Copy the completion handler so that the block does not capture a reference.
+ __block language_detection::BufferedTextCallback blockCallback = callback;
+ [self evaluate:@"__gCrWeb.languageDetection.retrieveBufferedTextContent()"
+ stringResultHandler:^(NSString* result, NSError*) {
+ blockCallback.Run(base::SysNSStringToUTF16(result));
+ }];
+}
+
+@end
diff --git a/components/translate/ios/browser/js_translate_manager.h b/components/translate/ios/browser/js_translate_manager.h
new file mode 100644
index 0000000..e5cac85
--- /dev/null
+++ b/components/translate/ios/browser/js_translate_manager.h
@@ -0,0 +1,46 @@
+// Copyright 2013 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.
+
+#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_JS_TRANSLATE_MANAGER_H_
+#define COMPONENTS_TRANSLATE_IOS_BROWSER_JS_TRANSLATE_MANAGER_H_
+
+#import "ios/web/public/web_state/js/crw_js_injection_manager.h"
+
+#include <string>
+
+#include "base/time/time.h"
+
+@class NSString;
+
+// Manager for the injection of the Translate JavaScript.
+// Replicates functionality from TranslateHelper in
+// chrome/renderer/translate/translate_helper.cc.
+// JsTranslateManager injects the script in the page and calls it, but is not
+// responsible for loading it or caching it.
+@interface JsTranslateManager : CRWJSInjectionManager
+
+// The translation script. Must be set before |-inject| is called, and is reset
+// after the injection.
+@property(nonatomic, copy) NSString* script;
+
+// Injects JS to constantly check if the translate script is ready and informs
+// the Obj-C side when it is.
+- (void)injectWaitUntilTranslateReadyScript;
+
+// After a translation has been initiated, injects JS to check if the
+// translation has finished/failed and informs the Obj-C when it is.
+- (void)injectTranslateStatusScript;
+
+// Starts translation of the page from |source| language to |target| language.
+// Equivalent to TranslateHelper::StartTranslation().
+- (void)startTranslationFrom:(const std::string&)source
+ to:(const std::string&)target;
+
+// Reverts the translation. Assumes that no navigation happened since the page
+// has been translated.
+- (void)revertTranslation;
+
+@end
+
+#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_JS_TRANSLATE_MANAGER_H_
diff --git a/components/translate/ios/browser/js_translate_manager.mm b/components/translate/ios/browser/js_translate_manager.mm
new file mode 100644
index 0000000..0431a93
--- /dev/null
+++ b/components/translate/ios/browser/js_translate_manager.mm
@@ -0,0 +1,84 @@
+// Copyright 2013 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.
+
+#import "components/translate/ios/browser/js_translate_manager.h"
+
+#import <Foundation/Foundation.h>
+
+#include "base/logging.h"
+#include "base/mac/bundle_locations.h"
+#import "base/mac/scoped_nsobject.h"
+#include "base/memory/scoped_ptr.h"
+
+@implementation JsTranslateManager {
+ base::scoped_nsobject<NSString> _translationScript;
+}
+
+- (NSString*)script {
+ return _translationScript.get();
+}
+
+- (void)setScript:(NSString*)script {
+ // The translation script uses performance.now() for metrics, which is not
+ // supported except on iOS 8.0. To make the translation script work on these
+ // iOS versions, add some JavaScript to |script| that defines an
+ // implementation of performance.now().
+ NSString* const kPerformancePlaceholder =
+ @"var performance = window['performance'] || {};"
+ @"performance.now = performance['now'] ||"
+ @"(function () { return Date.now(); });\n";
+ script = [kPerformancePlaceholder stringByAppendingString:script];
+ // TODO(shreyasv): This leads to some duplicate code from
+ // CRWJSInjectionManager. Consider refactoring this to its own js injection
+ // manager.
+ NSString* path =
+ [base::mac::FrameworkBundle() pathForResource:@"translate_ios"
+ ofType:@"js"];
+ DCHECK(path);
+ NSError* error = nil;
+ NSString* content = [NSString stringWithContentsOfFile:path
+ encoding:NSUTF8StringEncoding
+ error:&error];
+ DCHECK(!error && [content length]);
+ script = [script stringByAppendingString:content];
+ _translationScript.reset([script copy]);
+}
+
+- (void)injectWaitUntilTranslateReadyScript {
+ [self.receiver evaluateJavaScript:@"__gCrWeb.translate.checkTranslateReady()"
+ stringResultHandler:nil];
+}
+
+- (void)injectTranslateStatusScript {
+ [self.receiver evaluateJavaScript:@"__gCrWeb.translate.checkTranslateStatus()"
+ stringResultHandler:nil];
+}
+
+- (void)startTranslationFrom:(const std::string&)source
+ to:(const std::string&)target {
+ NSString* js =
+ [NSString stringWithFormat:@"cr.googleTranslate.translate('%s','%s')",
+ source.c_str(), target.c_str()];
+ [self.receiver evaluateJavaScript:js stringResultHandler:nil];
+}
+
+- (void)revertTranslation {
+ DCHECK([self hasBeenInjected]);
+ [self.receiver evaluateJavaScript:@"cr.googleTranslate.revert()"
+ stringResultHandler:nil];
+}
+
+#pragma mark -
+#pragma mark CRWJSInjectionManager methods
+
+- (NSString*)injectionContent {
+ DCHECK(_translationScript);
+ return _translationScript.autorelease();
+}
+
+- (NSString*)presenceBeacon {
+ return @"cr.googleTranslate";
+}
+
+@end
diff --git a/components/translate/ios/browser/js_translate_manager_unittest.mm b/components/translate/ios/browser/js_translate_manager_unittest.mm
new file mode 100644
index 0000000..e011520
--- /dev/null
+++ b/components/translate/ios/browser/js_translate_manager_unittest.mm
@@ -0,0 +1,92 @@
+// Copyright 2013 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.
+
+#import "components/translate/ios/browser/js_translate_manager.h"
+
+#import "base/mac/scoped_nsobject.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/time/time.h"
+#include "grit/components_resources.h"
+#import "ios/web/public/test/crw_test_js_injection_receiver.h"
+#import "ios/web/public/test/js_test_util.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+#include "ui/base/resource/resource_bundle.h"
+
+using base::Time;
+using base::TimeDelta;
+
+@interface JsTranslateManager (Testing)
+- (double)performanceNow;
+@end
+
+@implementation JsTranslateManager (Testing)
+// Returns the time in milliseconds.
+- (double)performanceNow {
+ NSString* result =
+ web::EvaluateJavaScriptAsString(self.receiver, @"performance.now()");
+ return [result doubleValue];
+}
+@end
+
+class JsTranslateManagerTest : public PlatformTest {
+ protected:
+ JsTranslateManagerTest() {
+ receiver_.reset([[CRWTestJSInjectionReceiver alloc] init]);
+ manager_.reset([[JsTranslateManager alloc] initWithReceiver:receiver_]);
+ base::StringPiece script =
+ ResourceBundle::GetSharedInstance().GetRawDataResource(
+ IDR_TRANSLATE_JS);
+ [manager_ setScript:base::SysUTF8ToNSString(script.as_string() +
+ "('DummyKey');")];
+ }
+
+ bool IsDefined(NSString* name) {
+ NSString* script =
+ [NSString stringWithFormat:@"typeof %@ != 'undefined'", name];
+ return [web::EvaluateJavaScriptAsString(receiver_, script) isEqual:@"true"];
+ }
+
+ base::scoped_nsobject<CRWTestJSInjectionReceiver> receiver_;
+ base::scoped_nsobject<JsTranslateManager> manager_;
+};
+
+TEST_F(JsTranslateManagerTest, PerformancePlaceholder) {
+ [manager_ inject];
+ EXPECT_TRUE(IsDefined(@"performance"));
+ EXPECT_TRUE(IsDefined(@"performance.now"));
+
+ // Check that performance.now returns correct values.
+ NSTimeInterval intervalInSeconds = 0.3;
+ double startTime = [manager_ performanceNow];
+ [NSThread sleepForTimeInterval:intervalInSeconds];
+ double endTime = [manager_ performanceNow];
+ double timeElapsed = endTime - startTime;
+ // The tolerance is high to avoid flake.
+ EXPECT_NEAR(timeElapsed, intervalInSeconds * 1000, 100);
+}
+
+TEST_F(JsTranslateManagerTest, Inject) {
+ [manager_ inject];
+ EXPECT_TRUE([manager_ hasBeenInjected]);
+ EXPECT_EQ(nil, [manager_ script]);
+ // TODO(shreyasv): Switch to the util function in web/ once that CL lands.
+ __block BOOL block_was_called = NO;
+ [manager_ evaluate:@"cr.googleTranslate.libReady"
+ stringResultHandler:^(NSString* result, NSError*) {
+ block_was_called = YES;
+ EXPECT_NSEQ(@"false", result);
+ }];
+ // TODO(shreyasv): Move to |WaitUntilCondition| once that is moved to ios/.
+ const NSTimeInterval kTimeout = 5.0;
+ Time startTime = Time::Now();
+ while (!block_was_called &&
+ (Time::Now() - startTime < TimeDelta::FromSeconds(kTimeout))) {
+ NSDate* beforeDate = [NSDate dateWithTimeIntervalSinceNow:.01];
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+ beforeDate:beforeDate];
+ }
+
+ EXPECT_TRUE(block_was_called);
+}
diff --git a/components/translate/ios/browser/language_detection_controller.h b/components/translate/ios/browser/language_detection_controller.h
new file mode 100644
index 0000000..28f3efa
--- /dev/null
+++ b/components/translate/ios/browser/language_detection_controller.h
@@ -0,0 +1,94 @@
+// Copyright 2014 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.
+
+#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_LANGUAGE_DETECTION_CONTROLLER_H_
+#define COMPONENTS_TRANSLATE_IOS_BROWSER_LANGUAGE_DETECTION_CONTROLLER_H_
+
+#include <string>
+
+#include "base/callback_list.h"
+#include "base/gtest_prod_util.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "base/prefs/pref_member.h"
+#include "base/strings/string16.h"
+#include "ios/web/public/web_state/web_state_observer.h"
+
+class GURL;
+@class JsLanguageDetectionManager;
+class PrefService;
+
+namespace base {
+class DictionaryValue;
+}
+
+namespace web {
+class WebState;
+}
+
+namespace translate {
+
+class LanguageDetectionController : public web::WebStateObserver {
+ public:
+ // Language detection details, passed to language detection callbacks.
+ struct DetectionDetails {
+ // The language detected by the content (Content-Language).
+ std::string content_language;
+
+ // The language written in the lang attribute of the html element.
+ std::string html_root_language;
+
+ // The adopted language.
+ std::string adopted_language;
+ };
+
+ LanguageDetectionController(web::WebState* web_state,
+ JsLanguageDetectionManager* manager,
+ PrefService* prefs);
+ ~LanguageDetectionController() override;
+
+ // Callback types for language detection events.
+ typedef base::Callback<void(const DetectionDetails&)> Callback;
+ typedef base::CallbackList<void(const DetectionDetails&)> CallbackList;
+
+ // Registers a callback for language detection events.
+ scoped_ptr<CallbackList::Subscription> RegisterLanguageDetectionCallback(
+ const Callback& callback);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(LanguageDetectionControllerTest, OnTextCaptured);
+
+ // Starts the page language detection and initiates the translation process.
+ void StartLanguageDetection();
+
+ // Handles the "languageDetection.textCaptured" javascript command.
+ // |interacting| is true if the user is currently interacting with the page.
+ bool OnTextCaptured(const base::DictionaryValue& value,
+ const GURL& url,
+ bool interacting);
+
+ // Completion handler used to retrieve the text buffered by the
+ // JsLanguageDetectionManager.
+ void OnTextRetrieved(const std::string& http_content_language,
+ const std::string& html_lang,
+ const base::string16& text);
+
+ // web::WebStateObserver implementation:
+ void PageLoaded() override;
+ void URLHashChanged() override;
+ void HistoryStateChanged() override;
+ void WebStateDestroyed() override;
+
+ CallbackList language_detection_callbacks_;
+ base::scoped_nsobject<JsLanguageDetectionManager> js_manager_;
+ BooleanPrefMember translate_enabled_;
+ base::WeakPtrFactory<LanguageDetectionController> weak_method_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(LanguageDetectionController);
+};
+
+} // namespace translate
+
+#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_LANGUAGE_DETECTION_CONTROLLER_H_
diff --git a/components/translate/ios/browser/language_detection_controller.mm b/components/translate/ios/browser/language_detection_controller.mm
new file mode 100644
index 0000000..b8ea2be
--- /dev/null
+++ b/components/translate/ios/browser/language_detection_controller.mm
@@ -0,0 +1,146 @@
+// Copyright 2014 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 "components/translate/ios/browser/language_detection_controller.h"
+
+#include <string>
+
+#include "base/bind.h"
+#include "base/logging.h"
+#include "base/metrics/histogram.h"
+#include "base/prefs/pref_member.h"
+#include "base/time/time.h"
+#include "components/translate/core/common/translate_pref_names.h"
+#include "components/translate/core/language_detection/language_detection_util.h"
+#import "components/translate/ios/browser/js_language_detection_manager.h"
+#include "ios/web/public/string_util.h"
+#include "ios/web/public/url_scheme_util.h"
+#include "ios/web/public/web_state/web_state.h"
+
+namespace translate {
+
+namespace {
+// Name for the UMA metric used to track text extraction time.
+const char kTranslateCaptureText[] = "Translate.CaptureText";
+// Prefix for the language detection javascript commands. Must be kept in sync
+// with language_detection.js.
+const char kCommandPrefix[] = "languageDetection";
+}
+
+LanguageDetectionController::LanguageDetectionController(
+ web::WebState* web_state,
+ JsLanguageDetectionManager* manager,
+ PrefService* prefs)
+ : web::WebStateObserver(web_state),
+ js_manager_([manager retain]),
+ weak_method_factory_(this) {
+ DCHECK(web::WebStateObserver::web_state());
+ DCHECK(js_manager_);
+ translate_enabled_.Init(prefs::kEnableTranslate, prefs);
+ web_state->AddScriptCommandCallback(
+ base::Bind(&LanguageDetectionController::OnTextCaptured,
+ base::Unretained(this)),
+ kCommandPrefix);
+}
+
+LanguageDetectionController::~LanguageDetectionController() {
+}
+
+scoped_ptr<LanguageDetectionController::CallbackList::Subscription>
+LanguageDetectionController::RegisterLanguageDetectionCallback(
+ const Callback& callback) {
+ return language_detection_callbacks_.Add(callback);
+}
+
+void LanguageDetectionController::StartLanguageDetection() {
+ if (!translate_enabled_.GetValue())
+ return; // Translate disabled in preferences.
+ DCHECK(web_state());
+ const GURL& url = web_state()->GetVisibleURL();
+ if (!web::UrlHasWebScheme(url) || !web_state()->ContentIsHTML())
+ return;
+ [js_manager_ inject];
+ [js_manager_ startLanguageDetection];
+}
+
+bool LanguageDetectionController::OnTextCaptured(
+ const base::DictionaryValue& command,
+ const GURL& url,
+ bool interacting) {
+ std::string textCapturedCommand;
+ if (!command.GetString("command", &textCapturedCommand) ||
+ textCapturedCommand != "languageDetection.textCaptured" ||
+ !command.HasKey("translationAllowed")) {
+ NOTREACHED();
+ return false;
+ }
+ bool translation_allowed = false;
+ command.GetBoolean("translationAllowed", &translation_allowed);
+ if (!translation_allowed) {
+ // Translation not allowed by the page. Done processing.
+ return true;
+ }
+ if (!command.HasKey("captureTextTime") || !command.HasKey("htmlLang") ||
+ !command.HasKey("httpContentLanguage")) {
+ NOTREACHED();
+ return false;
+ }
+
+ int capture_text_time = 0;
+ command.GetInteger("captureTextTime", &capture_text_time);
+ UMA_HISTOGRAM_TIMES(kTranslateCaptureText,
+ base::TimeDelta::FromMillisecondsD(capture_text_time));
+ std::string html_lang;
+ command.GetString("htmlLang", &html_lang);
+ std::string http_content_language;
+ command.GetString("httpContentLanguage", &http_content_language);
+ // If there is no language defined in httpEquiv, use the HTTP header.
+ if (http_content_language.empty())
+ http_content_language = web_state()->GetContentLanguageHeader();
+
+ [js_manager_ retrieveBufferedTextContent:
+ base::Bind(&LanguageDetectionController::OnTextRetrieved,
+ weak_method_factory_.GetWeakPtr(),
+ http_content_language, html_lang)];
+ return true;
+}
+
+void LanguageDetectionController::OnTextRetrieved(
+ const std::string& http_content_language,
+ const std::string& html_lang,
+ const base::string16& text_content) {
+ std::string language = translate::DeterminePageLanguage(
+ http_content_language, html_lang,
+ web::GetStringByClippingLastWord(text_content,
+ language_detection::kMaxIndexChars),
+ nullptr /* cld_language */, nullptr /* is_cld_reliable */);
+ if (language.empty())
+ return; // No language detected.
+
+ DetectionDetails details;
+ details.content_language = http_content_language;
+ details.html_root_language = html_lang;
+ details.adopted_language = language;
+ language_detection_callbacks_.Notify(details);
+}
+
+// web::WebStateObserver implementation:
+
+void LanguageDetectionController::PageLoaded() {
+ StartLanguageDetection();
+}
+
+void LanguageDetectionController::URLHashChanged() {
+ StartLanguageDetection();
+}
+
+void LanguageDetectionController::HistoryStateChanged() {
+ StartLanguageDetection();
+}
+
+void LanguageDetectionController::WebStateDestroyed() {
+ web_state()->RemoveScriptCommandCallback(kCommandPrefix);
+}
+
+} // namespace translate
diff --git a/components/translate/ios/browser/language_detection_controller_unittest.mm b/components/translate/ios/browser/language_detection_controller_unittest.mm
new file mode 100644
index 0000000..e63163e
--- /dev/null
+++ b/components/translate/ios/browser/language_detection_controller_unittest.mm
@@ -0,0 +1,78 @@
+// Copyright 2014 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.
+
+#import "components/translate/ios/browser/language_detection_controller.h"
+
+#include "base/mac/bind_objc_block.h"
+#include "base/prefs/pref_registry_simple.h"
+#include "base/prefs/testing_pref_service.h"
+#include "base/strings/utf_string_conversions.h"
+#include "components/translate/core/common/translate_pref_names.h"
+#import "components/translate/ios/browser/js_language_detection_manager.h"
+#include "ios/web/public/test/test_web_state.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface MockJsLanguageDetectionManager : JsLanguageDetectionManager
+@end
+
+@implementation MockJsLanguageDetectionManager
+- (void)retrieveBufferedTextContent:
+ (const language_detection::BufferedTextCallback&)callback {
+ callback.Run(base::UTF8ToUTF16("Some content"));
+}
+@end
+
+namespace translate {
+
+namespace {
+
+class LanguageDetectionControllerTest : public PlatformTest {
+ protected:
+ LanguageDetectionControllerTest() {
+ prefs_.registry()->RegisterBooleanPref(prefs::kEnableTranslate, true);
+
+ base::scoped_nsobject<MockJsLanguageDetectionManager> js_manager(
+ [[MockJsLanguageDetectionManager alloc] init]);
+ controller_.reset(new LanguageDetectionController(
+ &web_state_, js_manager.get(), &prefs_));
+ }
+
+ LanguageDetectionController* controller() { return controller_.get(); }
+
+ private:
+ TestingPrefServiceSimple prefs_;
+ web::TestWebState web_state_;
+ scoped_ptr<LanguageDetectionController> controller_;
+};
+
+} // namespace
+
+// Tests that OnTextCaptured() correctly handles messages from the JS side and
+// informs the driver.
+TEST_F(LanguageDetectionControllerTest, OnTextCaptured) {
+ const std::string kRootLanguage("en");
+ const std::string kContentLanguage("fr");
+
+ __block bool block_was_called = false;
+ auto subscription =
+ controller()->RegisterLanguageDetectionCallback(base::BindBlock(
+ ^(const LanguageDetectionController::DetectionDetails& details) {
+ block_was_called = true;
+ EXPECT_EQ(kRootLanguage, details.html_root_language);
+ EXPECT_EQ(kContentLanguage, details.content_language);
+ }));
+
+ base::DictionaryValue command;
+ command.SetString("command", "languageDetection.textCaptured");
+ command.SetBoolean("translationAllowed", true);
+ command.SetInteger("captureTextTime", 10);
+ command.SetString("htmlLang", kRootLanguage);
+ command.SetString("httpContentLanguage", kContentLanguage);
+ controller()->OnTextCaptured(command, GURL("http://google.com"), false);
+
+ EXPECT_TRUE(block_was_called);
+}
+
+} // namespace translate
diff --git a/components/translate/ios/browser/resources/language_detection.js b/components/translate/ios/browser/resources/language_detection.js
new file mode 100644
index 0000000..7d54f5f
--- /dev/null
+++ b/components/translate/ios/browser/resources/language_detection.js
@@ -0,0 +1,165 @@
+// Copyright 2013 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.
+
+__gCrWeb['languageDetection'] = {};
+
+
+new function() {
+/**
+ * The cache of the text content that was extracted from the page
+ */
+__gCrWeb.languageDetection.bufferedTextContent = null;
+
+/**
+ * The number of active requests that have populated the cache. This is
+ * incremented every time a call to |__gCrWeb.languageDetection.detectLanguage|
+ * populates the buffer. This is decremented every time there is a call to
+ * retrieve the buffer. The buffer is purged when this goes down to 0.
+ */
+__gCrWeb.languageDetection.activeRequests = 0;
+
+/**
+ * Returns true if translation of the page is allowed.
+ * Translation is not allowed when a "notranslate" meta tag is defined.
+ * @return {Boolean} true if translation of the page is allowed.
+ */
+__gCrWeb.languageDetection['translationAllowed'] = function() {
+ var metaTags = document.getElementsByTagName('meta');
+ for (var i = 0; i < metaTags.length; ++i) {
+ if (metaTags[i].name === 'google') {
+ if (metaTags[i].content === 'notranslate' ||
+ metaTags[i].getAttribute('value') === 'notranslate') {
+ return false;
+ }
+ }
+ }
+ return true;
+};
+
+/**
+ * Gets the content of a meta tag by httpEquiv.
+ * The function is case insensitive.
+ * @param {String} httpEquiv Value of the "httpEquiv" attribute, has to be
+ * lower case.
+ * @return {String} Value of the "content" attribute of the meta tag.
+ */
+__gCrWeb.languageDetection['getMetaContentByHttpEquiv'] = function(httpEquiv) {
+ var metaTags = document.getElementsByTagName('meta');
+ for (var i = 0; i < metaTags.length; ++i) {
+ if (metaTags[i].httpEquiv.toLowerCase() === httpEquiv) {
+ return metaTags[i].content;
+ }
+ }
+ return '';
+};
+
+// Used by the |getTextContent| function below.
+__gCrWeb.languageDetection['nonTextNodeNames'] = {
+ 'SCRIPT': 1,
+ 'NOSCRIPT': 1,
+ 'STYLE': 1,
+ 'EMBED': 1,
+ 'OBJECT': 1
+};
+
+/**
+ * Walks a DOM tree to extract the text content.
+ * Does not walk into a node when its name is in |nonTextNodeNames|.
+ * @param {HTMLElement} node The DOM tree
+ * @param {Integer} maxLen Output will be truncated to |maxLen|
+ * @return {String} The text content
+ */
+__gCrWeb.languageDetection['getTextContent'] = function(node, maxLen) {
+ if (!node || maxLen <= 0) {
+ return '';
+ }
+
+ var txt = '';
+ // Formatting and filtering.
+ if (node.nodeType === document.ELEMENT_NODE) {
+ // Reject non-text nodes such as scripts.
+ if (__gCrWeb.languageDetection.nonTextNodeNames[node.nodeName]) {
+ return '';
+ }
+ if (node.nodeName === 'BR') {
+ return '\n';
+ }
+ var style = window.getComputedStyle(node);
+ // Only proceed if the element is visible.
+ if (style.display === 'none' || style.visibility === 'hidden') {
+ return '';
+ }
+ // No need to add a line break before |body| as it is the first element.
+ if (node.nodeName !== 'BODY' && style.display !== 'inline') {
+ txt = '\n';
+ }
+ }
+
+ if (node.hasChildNodes()) {
+ for (var childIdx = 0;
+ childIdx < node.childNodes.length && txt.length < maxLen;
+ childIdx++) {
+ txt += __gCrWeb.languageDetection.getTextContent(
+ node.childNodes[childIdx], maxLen - txt.length);
+ }
+ } else if (node.nodeType === document.TEXT_NODE && node.textContent) {
+ txt += node.textContent.substring(0, maxLen - txt.length);
+ }
+
+ return txt;
+};
+
+/**
+ * Detects if a page has content that needs translation and informs the native
+ * side. The text content of a page is cached in
+ * |__gCrWeb.languageDetection.bufferedTextContent| and retrived at a later time
+ * retrived at a later time directly from the Obj-C side. This is to avoid
+ * using |invokeOnHost|.
+ */
+__gCrWeb.languageDetection['detectLanguage'] = function() {
+ if (!__gCrWeb.languageDetection.translationAllowed()) {
+ __gCrWeb.message.invokeOnHost({
+ 'command': 'languageDetection.textCaptured',
+ 'translationAllowed': false});
+ } else {
+ // Constant for the maximum length of the extracted text returned by
+ // |-detectLanguage| to the native side.
+ // Matches desktop implementation.
+ // Note: This should stay in sync with the constant in
+ // js_language_detection_manager.mm .
+ var kMaxIndexChars = 65535;
+ var captureBeginTime = new Date();
+ __gCrWeb.languageDetection.activeRequests += 1;
+ __gCrWeb.languageDetection.bufferedTextContent =
+ __gCrWeb.languageDetection.getTextContent(document.body,
+ kMaxIndexChars);
+ var captureTextTime =
+ (new Date()).getMilliseconds() - captureBeginTime.getMilliseconds();
+ var httpContentLanguage =
+ __gCrWeb.languageDetection.getMetaContentByHttpEquiv(
+ 'content-language');
+ __gCrWeb.message.invokeOnHost({
+ 'command': 'languageDetection.textCaptured',
+ 'translationAllowed': true,
+ 'captureTextTime': captureTextTime,
+ 'htmlLang': document.documentElement.lang,
+ 'httpContentLanguage': httpContentLanguage});
+ }
+}
+
+/**
+ * Retrives the cached text content of a page. Returns it and then purges the
+ * cache.
+ */
+__gCrWeb.languageDetection['retrieveBufferedTextContent'] = function() {
+ var textContent = __gCrWeb.languageDetection.bufferedTextContent;
+ __gCrWeb.languageDetection.activeRequests -= 1;
+ if (__gCrWeb.languageDetection.activeRequests == 0) {
+ __gCrWeb.languageDetection.bufferedTextContent = null;
+ }
+ return textContent;
+}
+
+} // End of anonymous object
+
diff --git a/components/translate/ios/browser/resources/translate_ios.js b/components/translate/ios/browser/resources/translate_ios.js
new file mode 100644
index 0000000..356ad28
--- /dev/null
+++ b/components/translate/ios/browser/resources/translate_ios.js
@@ -0,0 +1,78 @@
+// Copyright 2014 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.
+
+__gCrWeb['translate'] = {};
+
+new function() {
+/**
+ * The delay a wait performed (in milliseconds) before checking whether the
+ * translation has finished.
+ * @type {number}
+ */
+__gCrWeb.translate.TRANSLATE_STATUS_CHECK_DELAY = 400;
+
+/**
+ * The delay in milliseconds that we'll wait to check if a page has finished
+ * loading before attempting a translation.
+ * @type {number}
+ */
+__gCrWeb.translate.TRANSLATE_LOAD_CHECK_DELAY = 150;
+
+/**
+ * The maximum number of times a check is performed to see if the translate
+ * library injected in the page is ready.
+ * @type {number}
+ */
+__gCrWeb.translate.MAX_TRANSLATE_INIT_CHECK_ATTEMPTS = 5;
+
+// The number of times polling for the ready status of the translate script has
+// been performed.
+var translationAttemptCount = 0;
+
+/**
+ * Polls every TRANSLATE_LOAD_CHECK_DELAY milliseconds to check if the translate
+ * script is ready and informs the host when it is.
+ */
+__gCrWeb.translate['checkTranslateReady'] = function() {
+ translationAttemptCount += 1;
+ if (cr.googleTranslate.libReady) {
+ translationAttemptCount = 0;
+ __gCrWeb.message.invokeOnHost({
+ 'command': 'translate.ready',
+ 'timeout': false,
+ 'loadTime': cr.googleTranslate.loadTime,
+ 'readyTime': cr.googleTranslate.readyTime});
+ } else if (translationAttemptCount >=
+ __gCrWeb.translate.MAX_TRANSLATE_INIT_CHECK_ATTEMPTS) {
+ __gCrWeb.message.invokeOnHost({
+ 'command': 'translate.ready',
+ 'timeout': true});
+ } else {
+ // The translation is still pending, check again later.
+ window.setTimeout(__gCrWeb.translate.checkTranslateReady,
+ __gCrWeb.translate.TRANSLATE_LOAD_CHECK_DELAY);
+ }
+}
+
+/**
+ * Polls every TRANSLATE_STATUS_CHECK_DELAY milliseconds to check if translate
+ * is ready and informs the host when it is.
+ */
+__gCrWeb.translate['checkTranslateStatus'] = function() {
+ if (cr.googleTranslate.error) {
+ __gCrWeb.message.invokeOnHost({'command': 'translate.status',
+ 'success': false});
+ } else if (cr.googleTranslate.finished) {
+ __gCrWeb.message.invokeOnHost({
+ 'command': 'translate.status',
+ 'success': true,
+ 'originalPageLanguage': cr.googleTranslate.sourceLang,
+ 'translationTime': cr.googleTranslate.translationTime});
+ } else {
+ // The translation is still pending, check again later.
+ window.setTimeout(__gCrWeb.translate.checkTranslateStatus,
+ __gCrWeb.translate.TRANSLATE_STATUS_CHECK_DELAY);
+ }
+}
+} // anonymous function
diff --git a/components/translate/ios/browser/translate_controller.h b/components/translate/ios/browser/translate_controller.h
new file mode 100644
index 0000000..6cf848e
--- /dev/null
+++ b/components/translate/ios/browser/translate_controller.h
@@ -0,0 +1,106 @@
+// Copyright 2014 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.
+
+#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_
+#define COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_
+
+#include <string>
+
+#include "base/gtest_prod_util.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "ios/web/public/web_state/web_state_observer.h"
+
+@class JsTranslateManager;
+class GURL;
+
+namespace base {
+class DictionaryValue;
+}
+
+namespace web {
+class WebState;
+}
+
+namespace translate {
+
+// TranslateController controls the translation of the page, by injecting the
+// translate scripts and monitoring the status.
+class TranslateController : public web::WebStateObserver {
+ public:
+ // Observer class to monitor the progress of the translation.
+ class Observer {
+ public:
+ // Called when the translate script is ready.
+ // In case of timeout, |success| is false.
+ virtual void OnTranslateScriptReady(bool success,
+ double load_time,
+ double ready_time) = 0;
+
+ // Called when the translation is complete.
+ virtual void OnTranslateComplete(bool success,
+ const std::string& original_language,
+ double translation_time) = 0;
+ };
+
+ TranslateController(web::WebState* web_state, JsTranslateManager* manager);
+ ~TranslateController() override;
+
+ // Sets the observer.
+ void set_observer(Observer* observer) { observer_ = observer; }
+
+ // Injects the translate script.
+ void InjectTranslateScript(const std::string& translate_script);
+
+ // Reverts the translation.
+ void RevertTranslation();
+
+ // Starts the translation. Must be called when the translation script is
+ // ready.
+ void StartTranslation(const std::string& source_language,
+ const std::string& target_language);
+
+ // Checks the translation status and calls the observer when it is done.
+ // This method must be called after StartTranslation().
+ void CheckTranslateStatus();
+
+ // Changes the JsTranslateManager used by this TranslateController.
+ // Only used for testing.
+ void SetJsTranslateManagerForTesting(JsTranslateManager* manager) {
+ js_manager_.reset([manager retain]);
+ }
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
+ OnJavascriptCommandReceived);
+ FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
+ OnTranslateScriptReadyTimeoutCalled);
+ FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
+ OnTranslateScriptReadyCalled);
+ FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationSuccess);
+ FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationFailure);
+
+ // Called when a JavaScript command is received.
+ bool OnJavascriptCommandReceived(const base::DictionaryValue& command,
+ const GURL& url,
+ bool interacting);
+ // Methods to handle specific JavaScript commands.
+ // Return false if the command is invalid.
+ bool OnTranslateReady(const base::DictionaryValue& command);
+ bool OnTranslateComplete(const base::DictionaryValue& command);
+
+ // web::WebStateObserver implementation:
+ void WebStateDestroyed() override;
+
+ Observer* observer_;
+ base::scoped_nsobject<JsTranslateManager> js_manager_;
+ base::WeakPtrFactory<TranslateController> weak_method_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(TranslateController);
+};
+
+} // namespace translate
+
+#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_
diff --git a/components/translate/ios/browser/translate_controller.mm b/components/translate/ios/browser/translate_controller.mm
new file mode 100644
index 0000000..6035ba5
--- /dev/null
+++ b/components/translate/ios/browser/translate_controller.mm
@@ -0,0 +1,140 @@
+// Copyright 2014 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.
+
+#import "components/translate/ios/browser/translate_controller.h"
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/logging.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/values.h"
+#import "components/translate/ios/browser/js_translate_manager.h"
+#include "ios/web/public/web_state/web_state.h"
+
+namespace translate {
+
+namespace {
+// Prefix for the translate javascript commands. Must be kept in sync with
+// translate_ios.js.
+const char kCommandPrefix[] = "translate";
+}
+
+TranslateController::TranslateController(web::WebState* web_state,
+ JsTranslateManager* manager)
+ : web::WebStateObserver(web_state),
+ observer_(nullptr),
+ js_manager_([manager retain]),
+ weak_method_factory_(this) {
+ DCHECK(js_manager_);
+ DCHECK(web::WebStateObserver::web_state());
+ web_state->AddScriptCommandCallback(
+ base::Bind(&TranslateController::OnJavascriptCommandReceived,
+ base::Unretained(this)),
+ kCommandPrefix);
+}
+
+TranslateController::~TranslateController() {
+}
+
+void TranslateController::InjectTranslateScript(
+ const std::string& translate_script) {
+ [js_manager_ setScript:base::SysUTF8ToNSString(translate_script)];
+ [js_manager_ inject];
+ [js_manager_ injectWaitUntilTranslateReadyScript];
+}
+
+void TranslateController::RevertTranslation() {
+ [js_manager_ revertTranslation];
+}
+
+void TranslateController::StartTranslation(const std::string& source_language,
+ const std::string& target_language) {
+ [js_manager_ startTranslationFrom:source_language to:target_language];
+}
+
+void TranslateController::CheckTranslateStatus() {
+ [js_manager_ injectTranslateStatusScript];
+}
+
+bool TranslateController::OnJavascriptCommandReceived(
+ const base::DictionaryValue& command,
+ const GURL& url,
+ bool interacting) {
+ const base::Value* value = nullptr;
+ command.Get("command", &value);
+ if (!value) {
+ return false;
+ }
+
+ std::string out_string;
+ value->GetAsString(&out_string);
+ if (out_string == "translate.ready")
+ return OnTranslateReady(command);
+ else if (out_string == "translate.status")
+ return OnTranslateComplete(command);
+
+ NOTREACHED();
+ return false;
+}
+
+bool TranslateController::OnTranslateReady(
+ const base::DictionaryValue& command) {
+ if (!command.HasKey("timeout")) {
+ NOTREACHED();
+ return false;
+ }
+
+ bool timeout = false;
+ double load_time = 0.;
+ double ready_time = 0.;
+
+ command.GetBoolean("timeout", &timeout);
+ if (!timeout) {
+ if (!command.HasKey("loadTime") || !command.HasKey("readyTime")) {
+ NOTREACHED();
+ return false;
+ }
+ command.GetDouble("loadTime", &load_time);
+ command.GetDouble("readyTime", &ready_time);
+ }
+ if (observer_)
+ observer_->OnTranslateScriptReady(!timeout, load_time, ready_time);
+ return true;
+}
+
+bool TranslateController::OnTranslateComplete(
+ const base::DictionaryValue& command) {
+ if (!command.HasKey("success")) {
+ NOTREACHED();
+ return false;
+ }
+
+ bool success = false;
+ std::string original_language;
+ double translation_time = 0.;
+
+ command.GetBoolean("success", &success);
+ if (success) {
+ if (!command.HasKey("originalPageLanguage") ||
+ !command.HasKey("translationTime")) {
+ NOTREACHED();
+ return false;
+ }
+ command.GetString("originalPageLanguage", &original_language);
+ command.GetDouble("translationTime", &translation_time);
+ }
+
+ if (observer_)
+ observer_->OnTranslateComplete(success, original_language,
+ translation_time);
+ return true;
+}
+
+// web::WebStateObserver implementation.
+
+void TranslateController::WebStateDestroyed() {
+ web_state()->RemoveScriptCommandCallback(kCommandPrefix);
+}
+
+} // namespace translate
diff --git a/components/translate/ios/browser/translate_controller_unittest.mm b/components/translate/ios/browser/translate_controller_unittest.mm
new file mode 100644
index 0000000..0e572b9
--- /dev/null
+++ b/components/translate/ios/browser/translate_controller_unittest.mm
@@ -0,0 +1,142 @@
+// Copyright 2014 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.
+
+#import "components/translate/ios/browser/translate_controller.h"
+
+#include "base/values.h"
+#import "components/translate/ios/browser/js_translate_manager.h"
+#include "ios/web/public/test/test_web_state.h"
+#include "testing/platform_test.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+#include "url/gurl.h"
+
+namespace translate {
+
+class TranslateControllerTest : public PlatformTest,
+ public TranslateController::Observer {
+ protected:
+ TranslateControllerTest()
+ : test_web_state_(new web::TestWebState),
+ success_(false),
+ ready_time_(0),
+ load_time_(0),
+ translation_time_(0),
+ on_script_ready_called_(false),
+ on_translate_complete_called_(false) {
+ mock_js_translate_manager_.reset(
+ [[OCMockObject niceMockForClass:[JsTranslateManager class]] retain]);
+ translate_controller_.reset(new TranslateController(
+ test_web_state_.get(), mock_js_translate_manager_));
+ translate_controller_->set_observer(this);
+ }
+
+ // TranslateController::Observer methods.
+ void OnTranslateScriptReady(bool success,
+ double load_time,
+ double ready_time) override {
+ on_script_ready_called_ = true;
+ success_ = success;
+ load_time_ = load_time;
+ ready_time_ = ready_time;
+ }
+
+ void OnTranslateComplete(bool success,
+ const std::string& original_language,
+ double translation_time) override {
+ on_translate_complete_called_ = true;
+ success_ = success;
+ original_language_ = original_language;
+ translation_time_ = translation_time;
+ }
+
+ scoped_ptr<web::TestWebState> test_web_state_;
+ base::scoped_nsobject<id> mock_js_translate_manager_;
+ scoped_ptr<TranslateController> translate_controller_;
+ bool success_;
+ double ready_time_;
+ double load_time_;
+ std::string original_language_;
+ double translation_time_;
+ bool on_script_ready_called_;
+ bool on_translate_complete_called_;
+};
+
+// Tests that OnJavascriptCommandReceived() returns false to malformed commands.
+TEST_F(TranslateControllerTest, OnJavascriptCommandReceived) {
+ base::DictionaryValue malformed_command;
+ EXPECT_FALSE(translate_controller_->OnJavascriptCommandReceived(
+ malformed_command, GURL("http://google.com"), false));
+}
+
+// Tests that OnTranslateScriptReady() is called when a timeout message is
+// recieved from the JS side.
+TEST_F(TranslateControllerTest, OnTranslateScriptReadyTimeoutCalled) {
+ base::DictionaryValue command;
+ command.SetString("command", "translate.ready");
+ command.SetBoolean("timeout", true);
+ command.SetDouble("loadTime", .0);
+ command.SetDouble("readyTime", .0);
+ EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived(
+ command, GURL("http://google.com"), false));
+ EXPECT_TRUE(on_script_ready_called_);
+ EXPECT_FALSE(on_translate_complete_called_);
+ EXPECT_FALSE(success_);
+}
+
+// Tests that OnTranslateScriptReady() is called with the right parameters when
+// a |translate.ready| message is recieved from the JS side.
+TEST_F(TranslateControllerTest, OnTranslateScriptReadyCalled) {
+ // Arbitrary values.
+ double some_load_time = 23.1;
+ double some_ready_time = 12.2;
+
+ base::DictionaryValue command;
+ command.SetString("command", "translate.ready");
+ command.SetBoolean("timeout", false);
+ command.SetDouble("loadTime", some_load_time);
+ command.SetDouble("readyTime", some_ready_time);
+ EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived(
+ command, GURL("http://google.com"), false));
+ EXPECT_TRUE(on_script_ready_called_);
+ EXPECT_FALSE(on_translate_complete_called_);
+ EXPECT_TRUE(success_);
+ EXPECT_EQ(some_load_time, load_time_);
+ EXPECT_EQ(some_ready_time, ready_time_);
+}
+
+// Tests that OnTranslateComplete() is called with the right parameters when a
+// |translate.status| message is recieved from the JS side.
+TEST_F(TranslateControllerTest, TranslationSuccess) {
+ // Arbitrary values.
+ std::string some_original_language("en");
+ double some_translation_time = 12.9;
+
+ base::DictionaryValue command;
+ command.SetString("command", "translate.status");
+ command.SetBoolean("success", true);
+ command.SetString("originalPageLanguage", some_original_language);
+ command.SetDouble("translationTime", some_translation_time);
+ EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived(
+ command, GURL("http://google.com"), false));
+ EXPECT_FALSE(on_script_ready_called_);
+ EXPECT_TRUE(on_translate_complete_called_);
+ EXPECT_TRUE(success_);
+ EXPECT_EQ(some_original_language, original_language_);
+ EXPECT_EQ(some_translation_time, translation_time_);
+}
+
+// Tests that OnTranslateComplete() is called with the right parameters when a
+// |translate.status| message is recieved from the JS side.
+TEST_F(TranslateControllerTest, TranslationFailure) {
+ base::DictionaryValue command;
+ command.SetString("command", "translate.status");
+ command.SetBoolean("success", false);
+ EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived(
+ command, GURL("http://google.com"), false));
+ EXPECT_FALSE(on_script_ready_called_);
+ EXPECT_TRUE(on_translate_complete_called_);
+ EXPECT_FALSE(success_);
+}
+
+} // namespace translate