Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] - Add Page Translation #25391

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import("//brave/ios/browser/api/query_filter/headers.gni")
import("//brave/ios/browser/api/session_restore/headers.gni")
import("//brave/ios/browser/api/skus/headers.gni")
import("//brave/ios/browser/api/storekit_receipt/headers.gni")
import("//brave/ios/browser/api/translate/headers.gni")
import("//brave/ios/browser/api/unicode/headers.gni")
import("//brave/ios/browser/api/url/headers.gni")
import("//brave/ios/browser/api/url_sanitizer/headers.gni")
Expand Down Expand Up @@ -128,6 +129,7 @@ brave_core_public_headers += credential_provider_public_headers
brave_core_public_headers += developer_options_code_public_headers
brave_core_public_headers += browser_api_storekit_receipt_public_headers
brave_core_public_headers += webcompat_reporter_public_headers
brave_core_public_headers += browser_api_translate_public_headers
brave_core_public_headers += browser_api_unicode_public_headers

action("brave_core_umbrella_header") {
Expand Down
3 changes: 3 additions & 0 deletions ios/brave-ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,9 @@ var braveTarget: PackageDescription.Target = .target(
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/Paged/YoutubeQualityScript.js"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/Sandboxed/BraveLeoScript.js"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/Sandboxed/DarkReaderScript.js"),
.copy(
"Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/Sandboxed/BraveTranslateScript.js"
),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/Sandboxed/DeAmpScript.js"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/Sandboxed/FaviconScript.js"),
.copy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import Foundation
import SwiftUI
import Translation
import os.log

class BraveTranslateSession {
struct RequestMessage: Codable {
let method: String
let url: URL
let headers: [String: String]
let body: String
}

static func isPhraseTranslationRequest(
_ request: RequestMessage
) -> Bool {
return request.url.path.starts(with: "/translate_a/")
}

class func isTranslationSupported(
from source: Locale.Language,
to target: Locale.Language
) async -> Bool {
if #available(iOS 18.0, *) {
#if !targetEnvironment(simulator)
let availability = LanguageAvailability()
let status = await availability.status(from: source, to: target)
switch status {
case .installed, .supported:
return true
case .unsupported:
return false
@unknown default:
return false
}
#endif
}

guard let sourceLanguage = source.languageCode?.identifier,
let targetLanguage = target.languageCode?.identifier
else {
return false
}
return sourceLanguage != targetLanguage
}

func translate(
_ request: RequestMessage
) async throws -> (data: Data, response: URLResponse) {
var urlRequest = URLRequest(url: request.url)
urlRequest.httpMethod = request.method
urlRequest.httpBody = request.body.data(using: .utf8)
request.headers.forEach { (key, value) in
urlRequest.setValue(value, forHTTPHeaderField: key)
}

let session = URLSession(configuration: .ephemeral)
defer { session.finishTasksAndInvalidate() }
return try await session.data(for: urlRequest)
}
}

struct BraveTranslateContainerView: View {
var onTranslationSessionUpdated: ((BraveTranslateSession?) async -> Void)?

@ObservedObject
var languageInfo: BraveTranslateLanguageInfo

var body: some View {
Color.clear
.osAvailabilityModifiers({ view in
if #available(iOS 18.0, *) {
#if !targetEnvironment(simulator)
view
.translationTask(
.init(source: languageInfo.pageLanguage, target: languageInfo.currentLanguage),
action: { session in
do {
try await session.prepareTranslation()
await onTranslationSessionUpdated?(BraveTranslateSessionApple(session: session))
} catch {
Logger.module.error("Translate Session Unavailable: \(error)")
await onTranslationSessionUpdated?(nil)
}
}
)
#else
view.task {
await onTranslationSessionUpdated?(BraveTranslateSession())
}
#endif
} else {
view.task {
await onTranslationSessionUpdated?(BraveTranslateSession())
}
}
})
}
}

#if !targetEnvironment(simulator)
@available(iOS 18.0, *)
private class BraveTranslateSessionApple: BraveTranslateSession {
private weak var session: TranslationSession?

init(session: TranslationSession) {
self.session = session
}

override func translate(
_ request: RequestMessage
) async throws -> (data: Data, response: URLResponse) {
// Do not attempt to translate requests to html and css from Brave-Translate script
guard Self.isPhraseTranslationRequest(request) else {
return try await super.translate(request)
}

guard let session = session else {
throw BraveTranslateError.otherError
}

let components = URLComponents(string: "https://translate.brave.com?\(request.body)")
let phrases = components!.queryItems!.map({ "\($0.value ?? "")" })
let results = try await session.translations(
from: phrases.map({ .init(sourceText: $0) })
).map({ $0.targetText })

let data = try JSONSerialization.data(withJSONObject: results, options: [])
let response =
HTTPURLResponse(
url: request.url,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "text/html"]
)
?? URLResponse(
url: request.url,
mimeType: "text/html",
expectedContentLength: data.count,
textEncodingName: "UTF-8"
)
return (data, response)
}
}
#endif
Loading
Loading