iOS App 程式開發

利用 Google Translation API 添加即時翻譯功能 讓你的 App 更加升級!

Google Translation API 能提供基於機器學習 (Machine Learning) 的翻譯功能,將任何字串轉換成任何支援的語言形式。 此文將會深入解釋如何使用翻譯 API,讓我們可以在 app 中提供即時翻譯功能。
利用 Google Translation API 添加即時翻譯功能 讓你的 App 更加升級!
利用 Google Translation API 添加即時翻譯功能 讓你的 App 更加升級!
In: iOS App 程式開發, Machine Learning, Swift 程式語言

在 Google 為開發者提供的服務和 API 中,其中一個就是雲端翻譯 API,它能提供基於機器學習 (Machine Learning, ML) 的翻譯功能,將任何字串轉換成任何支援的語言形式。目前所支援的語言清單已經為數眾多,在未來也只會更完整。Google 提供了預先訓練的翻譯模型,但我們也可以訓練客製化的模型。在本次的教學中,我們馬上就會看到,翻譯是可以如此的快速、精準及高品質過程。

在本次文章中,我們會學習如何使用翻譯 API,讓我們可以在 app 中支援及提供即時翻譯功能。為了讓文章更清晰,我們不會訓練任何模型,因為這超出了本次教學的範疇。文章將會集中在 Google 翻譯 API 所提供的三種服務:

  1. 文本翻譯:將某些文本從來源語言翻譯到目標語言。
  2. 語言偵測:這個功能非常有用,即使未知來源語言,亦可以自動偵測。
  3. 支援語言:支援翻譯功能的所有語言清單。

Google 提供了用於整合到專案中的用戶端函式庫 (client library)但 iOS 不在支援的清單中。不過這對我們不是問題,因為我們將會基於 Google 所提供的 REST APIs 來實作,所以我們會以 Web 請求的方式來完成上述三種服務。當然,這表示你需要對 RESTful 服務、Web 請求、請求及響應是甚麼、HTTP 方法、以及其他相關的觀念有基礎的認識。不用擔心,你不需要精通這些技術,都可以瞭解我們在文章中所做的事。為了執行 Web 請求,我們將會編寫一個小方法,讓它稍後執行所有的「魔法」,到時候所有的疑慮將會一掃而空。

但 Cloud Translation API 有一個缺點,就是即使對於有限數量的請求,也沒有免費使用 API 的方案,因此開發人員必須付費才能夠嘗試此功能。這無疑令想要嘗試此 API 的人有所卻步。不過服務費用不高,每一百萬個字元需要 20 美元,價格還算合理,你可以在這裡瞭解更多細節。

教學文章的下一部分,我們將會開發一個 iOS app,藉此清楚呈現如何將 Google Cloud Translation API 所提供的翻譯服務,整合到我們的 iOS 專案之中。首先,我們將從 Google Cloud Platform 開始進行一些初步的工作,然後逐步產生 API 金鑰來「解鎖」我們的翻譯服務。如果你還沒在 Google 帳戶中設置好付款資訊,在這過程之中 Google 將會要求你提供付款資訊。

請記住,Google 文件的內容相當豐富而有用,所以如果你需要,可以隨時查看官方文件瞭解更多資訊。現在,讓我們看看今天將要開發的範例 app。

關於範例 App

我們將會透過一個簡單而重要的 iOS app 來瞭解 Google Cloud Translation API。我們不會從零開始這個範例,你可以先下載這個初始專案。這個專案除了與使用翻譯 API 相關的邏輯之外,其他必要部分都已經實作好了。

讓我簡單介紹一下這個專案:Translate 專案是一個基於導航的 app,由三個視圖控制器所組成:

  • EditorViewController:它包含了一個可以輸入來源文本的文本視圖,以及導覽列中的兩個按鈕,第一個按鈕是用來觸發來源文本語言偵測,第二是啟動文本轉換
  • LanguagesViewController:它包含了一個表格視圖,列出了所以從 Google 獲得的支援語言清單。
  • TranslationViewController:它包含了一個文本視圖,用來呈現來源文本的翻譯結果。

整個 app 的流程其實相當簡單:

  1. 使用者輸入一些文本。
  2. 使用者點擊翻譯按鈕。
  3. 獲取翻譯的支援語言清單,並在 LanguagesViewController 的表格視圖中顯示。
  4. 使用者選取目標語言來翻譯來源文本,並導覽到 TranslationViewController 畫面之中,翻譯完成後就會在這裡顯示翻譯的文本。

除此之外,使用者還可以使用 EditorViewController 之中的偵測語言按鈕,來自動偵測來源文本的語言。

下列的動畫展示了整個 app 的功能:

Google Translation Demo App

在初始專案之中,你可以找到一個叫做 GTAlertCollection 的客製函式庫(你也可以在 GitHub 上面找到它)。它的目的是希望單單調用適當的方法並傳遞一些參數,就可以呈現各種警報控制器;這為我們在之後的部分中節省大量的實作時間。

請下載初始專案並自行導覽:打開 Main.storyboard,然後查看場景層級與流程,看看不同的類別,並瞭解一下已經實作好的內容。當你準備好了以後,我們就可以開始進行下一部分內容,這一定很有趣!

在 Google Cloud Platform 控制台中創建專案

在能夠使用 Google 任何翻譯 API 之前,我們必須先在 Google Cloud Platform 控制台創建新專案,最終產生一個 API 金鑰,讓我們的 app 可以訪問翻譯 API 並向它發出請求,以及讓 Google 追蹤 API 的使用情況。在本部分中,我將會介紹創建新專案、啟用 Translation API、以及產生 API 金鑰所需要的步驟。

注意:我不會詳細介紹設置付款帳戶的步驟。如果你已經有一個付款帳戶,付款帳戶表示你已經完成了設置;否則,你將會在啟用 Translation API 步驟中被要求創建一個。在這種情況下,請按照螢幕上的說明進行操作,提供信用卡資料,並完成整個過程。

開始前請先連結你的 Google 帳戶,然後訪問這個頁面來管理你的 Google Cloud Platform 專案。如果你之前已經創建過專案,就會在這裡找到它們。

manage resources

點擊上方 Create Project 的按鈕,你會被導引到新的頁面中。在該頁面中,你要提供我們為 app 創建的專案名稱。如你所見,我把它命名為 Translation Project,你也可以任意為專案命名。當你都準備好之後,點擊 Create

Create Project

等待幾秒鐘之後,創建好的專案將會顯示在列表之中(如果沒有看到專案,請在專案準備好的訊息出現後,重新整理頁面)。

接著,點擊導覽菜單(藍色列上面的三明治菜單),並選擇 APIs & Services > Dashboard 的選項。即使我們已經創建了一個全新的專案,但是目前還沒有 API 連接到它,這就是我們接下來所需要做的事。

API Service Menu

在新的頁面中,請確認新專案的名稱(Translation Project 或是你為專案取的名稱)有顯示在藍色列之中;如果沒有,點擊向下箭頭並在出現的視窗中選擇它。當你看到專案已經被選擇後,點擊 ENABLE APIS AND SERVICES 按鈕。

Enable Google Translation API Services

上述的操作會把你帶到 API Library 的頁面之中。

API Library

在搜尋欄中輸入 translation,你就會看到頁面中只剩下 Cloud Translation API 一個結果,點擊它。

Cloud Translation API

以下的頁面提供了有關 API 的詳細資料及價格。如前文所述,在撰寫本篇文章時,使用翻譯 API 的費用為每一百萬字元 20 美元。閱讀頁面上的有用資訊,然後點擊藍色的 Create 按鈕。

Enable Google Translation API

如果你還沒啟用付款帳戶,那麼現在你就可以設置一個了。如果沒有付款帳戶,Google 將不會讓你繼續啟用 Cloud Translation API。

在啟用 API 之後,你將會登錄到包含詳細資料及概述的新頁面。在頁面上方,你會發現有一則訊息說這個 API 需要憑證,讓我們現在就來創建它吧!打開導航菜單(三明治菜單),並選擇 APIs & Services > Credentials 選項。在頁面中間的位置,你會看到一個名為 API Credentials 的方框和一個 Create credentials 的藍色按鈕。點擊這個按鈕,你應該會看到類似這張圖像的選項列表。

Create Credential Options

點擊第一個名為 API key 的選項,將會跳出一個小視窗,當中包含你創建的新金鑰,這正是我們所需要的東西!但是在完成之前,點擊小視窗底部的 Restrict Key 按鈕。

Create API Key

你將會瞭解金鑰的詳細資訊,你可以在此使用兩種不同類型的限制,來限制金鑰使用情況:

  • App 限制
  • API 限制

在 app 限制中,你可以允許特定類型的 app 使用特定金鑰發出請求。例如,你可以選擇 iOS app 選項,因為我們將僅通過 iOS app 使用該金鑰。

在 API 限制中,你將金鑰使用限制為專案中特定的已啟用 API。例如,如果我們在專案中啟用了更多 API,我們可以單擊 Select API 下拉列表,並選擇 Cloud Translation API 選項,以允許特定金鑰僅能夠與該 API 一起使用。

如果你進行了任何更改,請記得單擊 Save 按鈕。

API Restriction

完成金鑰的創建之後,你應該會在憑證頁面中看到以下內容:

API Key

先不要關閉這個頁面,因為我們馬上就會需要複製 API 金鑰的資訊。無論如何,目前在 Google Cloud Platform 上面的工作已經完成了。

開始撰寫程式:The TranslationAPI 列舉 (enum)

是時候開始撰寫程式碼了!讓我們從創建有用的列舉 (enum) 開始,我們會在當中定義 Cloud Translation API 的使用情況:

  • 偵測語言
  • 獲得支援語言
  • 翻譯

在初始專案之中打開 TranslationAPI.swift 檔案,並加入下列程式碼:

enum TranslationAPI {
    case detectLanguage
    case translate
    case supportedLanguages
}

我們將使用 Translation API 列舉,來指定我們待會要向其發出請求的 API。但僅僅列出 API 是不夠的,我們還可以添加一些函式,並透過以下步驟中讓開發過程更輕鬆。而這些函式會是甚麼呢?

第一個可以是回傳每個 API 請求 URL 的函式。我們有三個不同的 API,因此會有三個不同的URL:

enum TranslationAPI {
    // ...

    func getURL() -> String {
        var urlString = ""

        switch self {
        case .detectLanguage:
            urlString = "https://translation.googleapis.com/language/translate/v2/detect"

        case .translate:
            urlString = "https://translation.googleapis.com/language/translate/v2"

        case .supportedLanguages:
            urlString = "https://translation.googleapis.com/language/translate/v2/languages"
        }

        return urlString
    }
}

另一個函式是關於每個 API 所需的 HTTP 請求方法。如果要透過 Cloud Translation API 來獲取支援的語言,我們就要發出 GET 請求,並為另外兩個 API 發出 POST 請求。

enum TranslationAPI {
    // ...

    func getHTTPMethod() -> String {
        if self == .supportedLanguages {
            return "GET"
        } else {
            return "POST"
        }
    }
}

現在加入這一小段程式碼,待會你就會發現它的用處。

翻譯管理員 (The Translation Manager)

在這一部分,我們將會開始實作一個名為 TranslationManager 的類別,不過我們會在本教程的最後一部分才完成它。在這個類別之中,我們將會實作向 Google Cloud Translation API 發出 Web 請求所需要的所有程式碼,並在回傳給發出請求的視圖控制器之前,先獲取並解析結果。所以,讓我們做一些準備工作,讓我們很快就可以輕鬆地向翻譯 API 發出請求吧!

打開 TranslationManager.swift 檔案,並加入下列程式碼:

class TranslationManager: NSObject {

    static let shared = TranslationManager()

    private let apiKey = "YOUR-API-KEY"

    override init() {
        super.init()
    }
}

shared 屬性讓我們將 TranslationManager 作為一個單例 (singleton) 類別來使用;簡單來說,我們將會在整個專案中使用共享實例 (shared instance),而不是每當我們需要使用它時,才初始化一個全新的 TranslationManager 實例。

接著,我們將先前在 Google Cloud Platform 創建的 API 金鑰,指定到 apiKey 屬性之中。利用實際在 Google 獲得的 API 金鑰,來取代 “YOUR-API-KEY” ,我們很快就會用到它。

我們將會在類別之中繼續加入新屬性。到目前為止,上面的部分就是我們所需要的準備工作,那麼現在就讓我們來實作最重要的方法,也就是執行實際 Web 請求並獲得結果的那個函式。我們開始來定義它:

private func makeRequest(usingTranslationAPI api: TranslationAPI, urlParams: [String: String], completion: @escaping (_ results: [String: Any]?) -> Void) {

}

第一個參數是 TranslationAPI ,它將會幫助我們決定要使用的請求 URL 和 HTTP 方法。你將開始看到 TranslationAPI 列舉的值,因為它將減少不必要的程式碼。第二個參數是一個字典,它會包含我們將要進行的每個請求的 URL 參數值。

備註:API 金鑰始終是 URL 參數值之一。

完成處理器 (completion handler) 會將獲取的結果傳遞給呼叫此結果的方法,如果無法獲取結果,就會回傳一個 nil 值。

現在,讓我們實作這個方法吧!首先,我們必須將參數值(urlParams 字典)附加 (append) 到請求 URL 上。為此,我們將會初始化一個 URLComponents 物件。

if var components = URLComponents(string: api.getURL()) {

}

我們使用從 api 參數值的 getURL() 函數所獲得的請求 URL 字串,來完成 URLComponents 物件的初始化。請注意,這個初始化的過程可能會回傳 nil,所以我們需要解壓它。另外也請注意,我在這裡使用的是 if var,而非 if let,因為 components 物件將在下一步被改變,所以它不能是常數。

URLComponents 包含了一個預設為 nil 的 queryItems 陣列 (array)。這個陣列裡面需要 URLQueryItem 項目。我們將會初始化它,並將所有 URL 參數作為 URLQueryItem 物件逐個添加到裡面。

if var components = URLComponents(string: api.getURL()) {
    components.queryItems = [URLQueryItem]()

    for (key, value) in urlParams {
        components.queryItems?.append(URLQueryItem(name: key, value: value))
    }
}

下一步,使用 components 物件中已有的 URL 創建一個 URLRequest 物件,並向它傳遞適當的 HTTP 方法:

if var components = URLComponents(string: api.getURL()) {
    // ...

    if let url = components.url {
        var request = URLRequest(url: url)
        request.httpMethod = api.getHTTPMethod()
    }
}

我們藉由呼叫 getHTTPMethod() 函式,就可以立即輕易地獲得 HTTP 方法。

現在,我們將會創建一個 URLSession 物件,及一個 data task 來發出 URL 請求。

if var components = URLComponents(string: api.getURL()) {
    // ...

    if let url = components.url {
        // ...
    
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: request) { (results, response, error) in
    
        }
    
        task.resume()
    }

}

上述的 data task 將會在結束時回傳三個數值:

  • result:這是伺服器回傳的數據,如果沒有獲得任何東西,它就會是 nil。
  • response:這是一個 URLResponse 物件。
  • error:這會描述任何可能發生的錯誤。

我們將檢查 error 參數的數值,來啟動 data task 的完成處理器。如果 error 不是 nil,就表示有某些錯誤發生了,在這種情況下,我們就只能在函式中的完成處理器回傳 nil。

此外,我們將 response 轉換成一個 HTTPURLResponse 物件之後,就檢查裡面的 HTTP 狀態值,如果它是 200 或 201(分別代表成功執行 GET 及 POST 方法),就將獲取的數據轉換成字典,並透過完成處理器傳遞給呼叫者。請記得伺服器回傳的是 JSON 數據格式,所以我們將會使用 JSONSerialization 類別來轉換字典。

下列程式碼包含了上述所有的討論:

let task = session.dataTask(with: request) { (results, response, error) in
    if let error = error {
        print(error)
        completion(nil)
    } else {
        if let response = response as? HTTPURLResponse, let results = results {
            if response.statusCode == 200 || response.statusCode == 201 {
                do {
                    if let resultsDict = try JSONSerialization.jsonObject(with: results, options: JSONSerialization.ReadingOptions.mutableLeaves) as? [String: Any] {
                        completion(resultsDict)
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }
        } else {
            completion(nil)
        }
    }
}

可以實際執行 Web 請求的方法現在已經準備好了!我們將會在下一部分開始使用它。在我們結束這部分之前,可以看看以下的完整程式碼,讓你更清楚地理解整體流程:

private func makeRequest(usingTranslationAPI api: TranslationAPI, urlParams: [String: String], completion: @escaping (_ results: [String: Any]?) -> Void) {
    if var components = URLComponents(string: api.getURL()) {
        components.queryItems = [URLQueryItem]()

        for (key, value) in urlParams {
            components.queryItems?.append(URLQueryItem(name: key, value: value))
        }

        if let url = components.url {
            var request = URLRequest(url: url)
            request.httpMethod = api.getHTTPMethod()

            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: request) { (results, response, error) in
                if let error = error {
                    print(error)
                    completion(nil)
                } else {
                    if let response = response as? HTTPURLResponse, let results = results {
                        if response.statusCode == 200 || response.statusCode == 201 {
                            do {
                                if let resultsDict = try JSONSerialization.jsonObject(with: results, options: JSONSerialization.ReadingOptions.mutableLeaves) as? [String: Any] {
                                    print(resultsDict)
                                    completion(resultsDict)
                                }
                            } catch {
                                print(error.localizedDescription)
                            }
                        }
                    } else {
                        completion(nil)
                    }
                }
            }

            task.resume()
        }

    }
}

偵測語言來源 (Detecting Source Language)

我們將要使用的第一個 Google Cloud Translation API,就是語言偵測 API。我們只需要 POST 一些文本,就會獲得包含檢測語言結果的回應。我發現一個方便的功能是,我們可以透過單一請求發送多個文本來進行偵測。

備註:我們不會在這裡發送多個文本來做語言偵測,不過你可以記住這個特點,需要時你就可以在自己的專案中使用。你隨時可以更改我們接下來所要實作的方法,讓它支援多重文本輸入。

我建議你在這裡閱讀更多關於語言偵測的官方文件作參考。

讓我們回到正題。在 TranslationManager.swift 檔案的 TranslationManager 類別之中,宣告下列新屬性:

var sourceLanguageCode: String?

被偵測到的語言會被儲存在這個屬性之中,待會在翻譯的階段時你就會瞭解我們這樣做的原因。

現在,讓我們開始實作執行語言偵測的方法。這個方法會接收兩個參數:要被用來作語言偵測的文本來源,以及語言偵測請求成功時所需要的完成處理器。

func detectLanguage(forText text: String, completion: @escaping (_ language: String?) -> Void) {

}

根據 API 的開發文件,我們必須在發出請求的 URL 之中包含兩個參數:

  1. 來源文本
  2. API 金鑰

有了兩個數值之後,我們會用它們來創建一個新的字典:

func detectLanguage(forText text: String, completion: @escaping (_ language: String?) -> Void) {

    let urlParams = ["key": apiKey, "q": text]

}

現在,我們第一次使用剛剛所實作的 makeRequest(usingTranslationAPI:urlParams:completion:) 方法,我們會傳入剛剛定義的 urlParams 字典來呼叫它,而我們在這完成處理器中做的第一件事,就是確認數據有被回傳。

func detectLanguage(forText text: String, completion: @escaping (_ language: String?) -> Void) {
    let urlParams = ["key": apiKey, "q": text]

    makeRequest(usingTranslationAPI: .detectLanguage, urlParams: urlParams) { (results) in
        guard let results = results else { completion(nil); return }

    }
}

可以注意到 detectLanguage 的值是作為第一個參數被傳入的,因為它需要 TranslationAPI 的值。當然,如果 results 是 nil,nil 也會被回傳給完成處理器。

讓我們稍微看一下所預期的結果,假設我們為字串 Hello there!!! 進行語言偵測。

在正常情況下,回傳的數據應該會類似下面這樣:

["data": {
    detections =     (
                (
                        {
                confidence = 1;
                isReliable = 0;
                language = en;
            }
        )
    );
}]

data 是一個字典,而 detections 是一個內部陣列同樣為字典的二維陣列。language 金鑰以 ISO 639-1 代碼規範(像是 “en”, “el”, “fr”)包含偵測語言,這不是人可以閱讀的文本。根據官方文件說明,confidence 以及 isReliable 已經被棄用,所以不要使用他們來做任何關於語言偵測的相關決定。

備註:如果你想獲得可閱讀的翻譯文本,我可以提供一條建議:獲取支援的語言列表(請參考下一部分),然後根據語言代碼搜尋語言名稱。

記著上述的結果,讓我們繼續進行數據解析。如你將在下面的實作所見,一旦我們確保 datadetections 金鑰存在結果之中,我們就會作出所有檢測,並保留所找到的語言代碼(如果你有更改實作為傳送多個要檢測的文本,則會找到多於一個代碼。然後,確保有找到至少一個語言代碼後,我們就將它分配給 sourceLanguageCode 屬性,並將其傳遞給完成處理器。在任何其他情況下,我們只將 nil 傳遞給完成處理器。

if let data = results["data"] as? [String: Any], let detections = data["detections"] as? [[[String: Any]]] {
    var detectedLanguages = [String]()

    for detection in detections {
        for currentDetection in detection {
            if let language = currentDetection["language"] as? String {
                detectedLanguages.append(language)
            }
        }
    }

    if detectedLanguages.count > 0 {
        self.sourceLanguageCode = detectedLanguages[0]
        completion(detectedLanguages[0])
    } else {
        completion(nil)
    }

} else {
    completion(nil)
}

以下是 detectLanguage(forText:completion:) 方法的完整程式碼:

func detectLanguage(forText text: String, completion: @escaping (_ language: String?) -> Void) {
    let urlParams = ["key": apiKey, "q": text]

    makeRequest(usingTranslationAPI: .detectLanguage, urlParams: urlParams) { (results) in
        guard let results = results else { completion(nil); return }

        if let data = results["data"] as? [String: Any], let detections = data["detections"] as? [[[String: Any]]] {
            var detectedLanguages = [String]()

            for detection in detections {
                for currentDetection in detection {
                    if let language = currentDetection["language"] as? String {
                        detectedLanguages.append(language)
                    }
                }
            }

            if detectedLanguages.count > 0 {
                self.sourceLanguageCode = detectedLanguages[0]
                completion(detectedLanguages[0])
            } else {
                completion(nil)
            }

        } else {
            completion(nil)
        }
    }
}

現在讓我們改變目前的工作檔案,打開 EditorViewController.swift 檔,在 EditorViewController 的導覽列上面,有個名為 Detect Language按鈕,這個按鈕的目的是為任何輸入在文本視圖中的文本來執行語言偵測。這個按鈕已經連結到 detectLanguage(_:) IBAction 方法,但是裡頭還沒有實作任何功能。

如同我在描述範例程式的部分一樣,我們將會使用一個叫做 GTAlertCollection 的函式庫來顯示警報控制器 (alert controller)。因為警報控制器可以幫助我們輕鬆呈現多種不同樣式的警報,這樣我們就可以節省實作的時間。現在,在 detectLanguage(_:) 方法之中,我們將在無按鈕 (buttonless) 警報中顯示一個 “Please wait ⋯” 的訊息、以及活動指示器 (activity indicator)。

接著,我們將會呼叫上文實作的 detectLanguage(forText:completion:) 方法。獲得語言偵測的回傳結果後,我們就會將它顯示到一個新的警報上。如果有錯誤產生而沒有語言回傳,我們也會顯示相應的訊息。

@IBAction func detectLanguage(_ sender: Any) {
    if textView.text != "" {
        // Present a "Please wait..." buttonless alert with an activity indicator.
        alertCollection.presentActivityAlert(withTitle: "Detect Language", message: "Please wait while text language is being detected...", activityIndicatorColor: UIColor.blue, showLargeIndicator: false) { (presented) in
            if presented {


            }
        }

    }
}

請注意,如果沒有任何文本被輸入到文本視圖之中,我們就不會採取任何動作。

GTAlertCollection 類別中的 presentActivityAlert 方法顯示一個帶有活動指示器的無按鈕警報。我們可以視情況提供 activityIndicatorColor 以及 showLargeIndicator 參數值(待會我們會看到這個方法的簡易版本),不過現在我還是會包含這兩個參數值,用以更改活動的顏色及尺寸。而完成處理器中的 presented 參數值指示警報控制器何時呈現在視圖控制器上面。

備註:alertCollection 屬性已經在 viewDidLoad() 方法之中完成了初始化,而在初始化時,視圖控制器物件 (self) 已作為參數被提供。

下一步,我們就呼叫上文實作的 detectLanguage(forText:completion:) 方法,在完成處理器之中,我們將會解除無按鈕警報,並顯示一個新警報,當中包含所偵測到的語言代碼,或是沒有偵測語言回傳的錯誤訊息。

TranslationManager.shared.detectLanguage(forText: self.textView.text) { (language) in
    // Dismiss the buttonless alert.
    self.alertCollection.dismissAlert(completion: nil)

    if let language = language {
        // Present an alert with the detected language.
        self.alertCollection.presentSingleButtonAlert(withTitle: "Detect Language", message: "The following language was detected:\n\n\(language)", buttonTitle: "OK", actionHandler: {

        })

    } else {
        // Present an alert saying that an error occurred.
        self.alertCollection.presentSingleButtonAlert(withTitle: "Detect Language", message: "Oops! It seems that something went wrong and language cannot be detected.", buttonTitle: "OK", actionHandler: {

        })
    }
}

上述的程式碼應該沒有特別困難的部分,而 detectLanguage(_:) IBAction 方法也已經完成,以下是完整的程式碼:

@IBAction func detectLanguage(_ sender: Any) {
    if textView.text != "" {
        // Present a "Please wait..." buttonless alert with an activity indicator.
        alertCollection.presentActivityAlert(withTitle: "Detect Language", message: "Please wait while text language is being detected...", activityIndicatorColor: UIColor.blue, showLargeIndicator: false) { (presented) in
            if presented {
                TranslationManager.shared.detectLanguage(forText: self.textView.text) { (language) in

                    // Dismiss the buttonless alert.
                    self.alertCollection.dismissAlert(completion: nil)

                    if let language = language {
                        // Present an alert with the detected language.
                        self.alertCollection.presentSingleButtonAlert(withTitle: "Detect Language", message: "The following language was detected:\n\n\(language)", buttonTitle: "OK", actionHandler: {

                        })

                    } else {
                        // Present an alert saying that an error occurred.
                        self.alertCollection.presentSingleButtonAlert(withTitle: "Detect Language", message: "Oops! It seems that something went wrong and language cannot be detected.", buttonTitle: "OK", actionHandler: {

                        })
                    }
                }
            }
        }

    }
}

到現在,我們終於可以為 app 做第一次測試了!執行程式並輸入一些文本到文本視圖後,點擊 Detect Language 的按鈕。Google Cloud Translation API 將會偵測輸入的文本語言,然後馬上就可以看到語言代碼了。

Detect Language Demo

獲得支援語言 (Getting Supported Languages)

在前一部分,我們在 TranslationManager 類別之中創建了 detectLanguage(forText:completion:) 方法,用來偵測輸入在文本視圖中文本的語言。在這個及下個部分中,我們也會遵循這個模式,而我們將會創建新方法來獲取支援語言的清單,並執行實際的翻譯動作。

如我所說,某些文本輸入到文本視圖、並點擊了偵測語言按鈕後,翻譯的支援語言清單就會顯示。這份清單並不會寫死在我們的 app 中,反而是透過 Google Cloud Translation API 來獲取。這份清單只會在 app 啟動時獲取一次,所以如果你結束 app(不只是放入背景執行),這份清單會在下次執行時再獲取一次。

備註:為已獲取的支援語言進行儲存這個部分,就交由讀者自己實作。

再次打開 TranslationManager.swift 檔案,我們從定義下列結構開始著手:

struct TranslationLanguage {
    var code: String?
    var name: String?
}

這個結構用程式碼的方式呈現支援語言,我們可以簡單地假設每個回傳的支援語言都會包含一個語言代碼(例如 “en”)、以及一個人可閱讀的文本。然而,這並不能保證語言名稱(name 屬性)總是存在。為什麼?你很快就會瞭解。

現在讓我們回到 TranslationManager 類別之中,並宣告下列陣列為類別屬性:

var supportedLanguages = [TranslationLanguage]()

我們將把獲取的支援語言數據儲存到這陣列裡面。

現在,讓我們開始實作新的方法,來啟動獲得支援語言的請求,並解析結果:

func fetchSupportedLanguages(completion: @escaping (_ success: Bool) -> Void) {

}

這個新方法的完成處理器只會回傳一個旗標 (flag),它能標示獲取支援語言清單的動作是否成功,而實際的數據將會被保留在 supportedLanguages 陣列之中。

現在讓我們討論一下 URL 參數值。支援語言 API 最多可以接收三個 URL 參數:

  • API 密鑰(這是必要的參數)
  • 結果的目標語言
  • 支援語言的翻譯模型

目標語言明確地指出最後回傳人可閱讀文本的語言種類,舉例來說,如果請求 (作為一個 URL 參數值)之中目標語言指定為 “en”,回傳看起來將會是這樣:

["data": {
    languages =     (
        {
            language = af;
            name = Afrikaans;
        },
        {
            language = sq;
            name = Albanian;
        },
        {
            language = am;
            name = Amharic;
        },
        {
            language = ar;
            name = Arabic;
        },
        ...
        ...
        ...
    );
}]

如果是目標語言被省略的情況,結果看起來會是這樣:

["data": {
    languages =     (
        {
            language = af;
        },
        {
            language = sq;
        },
        {
            language = am;
        },
        {
            language = ar;
        },
        ...
        ...
        ...
    );
}]

當中的差異顯而易見:語言名稱並不存在於結果之中!在範例 app 中,我們會明確指出目標語言,但是在你自己的專案之中,如果你不需要伺服器回傳的這筆資料,你就可以省略它。

關於翻譯模型,目前有兩個值可以使用:

  1. base:這個值可以用來獲取基於短語機器翻譯 (Phrase-Based Machine Translation, PBMT) 的支援語言。
  2. nmt:這個值可以用來獲取基於神經機器翻譯 (Neural Machine Translation, NMT) 的支援語言。

這裡有兩個重點:首先,如果從請求 URL 中省略這個參數,所有支援語言都將會被回傳;第二,NMT 的支援語言只能翻譯成英文、或從英文翻譯。

這部分你可以自己決定要指定哪種模型。在範例 app 中,我們將會省略該 URL 查詢參數,並回傳所有支援語言。

在完成上述所有內容之後,讓我們繼續實作這個方法。我們將創建一個包含 URL 參數值的字典:

  • API 密鑰
  • 目標語言。在這裡,我們將使用設備的區域設置。如果 languageCode 為 nil,我們就將 “en”(英語)設為預設目標語言。
func fetchSupportedLanguages(completion: @escaping (_ success: Bool) -> Void) {
    var urlParams = [String: String]()
    urlParams["key"] = apiKey
    urlParams["target"] = Locale.current.languageCode ?? "en"
}

接下來,讓我們發出請求:

makeRequest(usingTranslationAPI: .supportedLanguages, urlParams: urlParams) { (results) in
    guard let results = results else { completion(false); return }

}

是時候來解析我們的結果了!根據先前的範例,回傳的數據中包含了一個名為 “data” 的字典,該字典中又包含一個 “languages” 陣列,而陣列中的每個項目都是包含語言數據的字典。

考慮到這個特性,我們可以繼續解析結果:

makeRequest(usingTranslationAPI: .supportedLanguages, urlParams: urlParams) { (results) in
    guard let results = results else { completion(false); return }

    if let data = results["data"] as? [String: Any], let languages = data["languages"] as? [[String: Any]] {

        for lang in languages {
            var languageCode: String?
            var languageName: String?

            if let code = lang["language"] as? String {
                languageCode = code
            }
            if let name = lang["name"] as? String {
                languageName = name
            }

            self.supportedLanguages.append(TranslationLanguage(code: languageCode, name: languageName))
        }

        completion(true)

    } else {
        completion(false)
    }

}

有了各種語言解析好的 codename 數據,我們可以創建一個 TranslationLanguage 物件,並添加到 supportedLanguages 集合之中。

實作上述方法後,下一步就是在顯示 LanguagesViewController 時,顯示出來時,是否應該獲取支援語言。在點擊 Translate 按鈕時,這個視圖控制器會被推送到導覽控制器 (navigation controller) 當中。

打開 LanguagesViewController.swift 檔案,並且在 LanguagesViewController 類別之中實作下列方法:

func checkForLanguagesExistence() {
    // Check if supported languages have been fetched by looking at the
    // number of items in the supported languages collection of the
    // TranslationManager shared instance.
    // If it's zero, no languages have been fetched, so ask user
    // if they want to fetch them now.
    if TranslationManager.shared.supportedLanguages.count == 0 {
        alertCollection.presentAlert(withTitle: "Supported Languages", message: "It seems that supported languages for translation have not been fetched yet. Would you like to get them now?", buttonTitles: ["Yes, fetch supported languages", "Not now"], cancelButtonIndex: 1, destructiveButtonIndices: nil) { (actionIndex) in

            // Check if user wants to fetch supported languages.
            if actionIndex == 0 {
                self.fetchSupportedLanguages()
            }
        }
    }
}

首先,我們來確認 TranslationManager 共享實例之中的 supportedLanguages 陣列裡面所存在的語言數量。如果沒有任何語言存在,我們就顯示一個警報控制器,來詢問使用者是否要立即獲取支援語言。如果是的話,我們就呼叫將在下一步實作的 fetchSupportedLanguages() 方法。在我們開始之前,先在 viewDidAppear(animated:) 裡面呼叫上面的方法:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    checkForLanguagesExistence()
}

讓我們開始來實作 fetchSupportedLanguages() 方法吧。由於我們要使用 TranslationManager 類別之中的 fetchSupportedLanguages(completion:) 方法來發送出 Web 請求,而且使用者將需要等待,所以我們會再次顯示無按鈕警報來標示正在獲取資料。一旦資料已經從伺服器回傳回來,我們將會重新載入表格視圖,並顯示所獲取的語言。

因為實作相當簡單,以下就是完整的程式碼:

func fetchSupportedLanguages() {
    // Show a "Please wait..." alert.
    alertCollection.presentActivityAlert(withTitle: "Supported Languages", message: "Please wait while translation supported languages are being fetched...") { (presented) in

        if presented {
            TranslationManager.shared.fetchSupportedLanguages(completion: { (success) in
                // Dismiss the alert.
                self.alertCollection.dismissAlert(completion: nil)

                // Check if supported languages were fetched successfully or not.
                if success {
                    // Display languages in the tableview.
                    DispatchQueue.main.async { [unowned self] in
                        self.tableView.reloadData()
                    }
                } else {
                    // Show an alert saying that something went wrong.
                    self.alertCollection.presentSingleButtonAlert(withTitle: "Supported Languages", message: "Oops! It seems that something went wrong and supported languages cannot be fetched.", buttonTitle: "OK", actionHandler: {

                    })
                }

            })
        }

    }
}

在我們看到支援語言列出在表格視圖之前,我們必須對起始專案的現有程式碼作一些小更改,特別是表格視圖中的兩個數據來源方法。首先,到 tableView(_:numberOfRowsInSection:) 方法,並依照下列程式碼來做更新:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return TranslationManager.shared.supportedLanguages.count
}

上面的程式碼將會在表格視圖中,回傳與支援語言數目一樣多的行數。

tableView(_:cellForRowAt:) 數據來源方法中,我們將會把每個語言的名稱及代碼填充到單元格的標籤之中。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "idLanguageCell", for: indexPath) as? LanguageCell else { return UITableViewCell() }

    let language = TranslationManager.shared.supportedLanguages[indexPath.row]

    cell.languageLabel.text = language.name ?? ""
    cell.codeLabel.text = language.code ?? ""

    return cell
}

完成上面兩個小調整之後,TranslationManager 共享實例中的 supportedLanguages 陣列已經成為了表格視圖的數據來源。

再一次執行 app,並在文本視圖中輸入文本後點擊 Translate 按鈕。伺服器會詢問你是否希望獲得支援語言,如果你同意,你就會看到結果呈現出來。

Supported Languages Demo

翻譯文本 (Translating Text)

我們的 app 現在已經可以獲取翻譯的支援語言,所以下一步就是在 LanguagesViewController 選取語言後進行翻譯。實際上,翻譯不會在那裡啟動,而是在 TranslationViewController 啟動,而翻譯文本將會顯示在文本視圖當中。正因為我們整個 app 是建立在導航的模式之上,所以透過在 LanguagesViewController 以及 TranslationViewController 之間做切換,很容易就可以將原始文本翻譯成各種語言。

備註:關於我們這部分使用到的翻譯 API,你可以在這裡閱讀相應的官方文件。

就如前兩部分一樣,我們將會在 TranslationManager 類別中實作一個新方法,這個方法將會觸發請求至翻譯 API,並解析回傳的資料。不過在開始之前,我們需要加入兩個新的屬性,來保留翻譯所需的資料。

打開 TranslationManager.swift 檔案,並宣告下列兩個屬性:

var textToTranslate: String?

var targetLanguageCode: String?

textToTranslate 會保留要翻譯的原始文本,而 targetLanguageCode 則會保留 LanguagesViewController 之中所選語言的代碼。

那就直接來實作 TranslationManager 類別中的最後一個方法吧:

func translate(completion: @escaping (_ translations: String?) -> Void) {
    guard let textToTranslate = textToTranslate, let targetLanguage = targetLanguageCode else { completion(nil); return }

}

透過這個方法的完成處理器,我們將會回傳翻譯後的文本,如果因為某些狀況而產生沒有回傳或解析翻譯,就會回傳 nil。此外,我們必須確認 textToTranslate 以及 targetLanguageCode 是有值而非 nil,因為我們需要它們來翻譯原始文本。

下一步是準備隨著請求一起發出的 URL 參數。讓我們先看一下需要在這裡提供的內容:

  • API 金鑰
  • 要翻譯的文本。參數名稱名為 q,它可以被多次提供,以在單次請求中執行多次翻譯。我們不會在這裡使用這個功能,我們只會傳遞單個文本。
  • 要翻譯的目標語言。
  • 回傳文本的格式。這可以是簡單的文本,也可以是 HTML 格式的格式化結果。
  • 我們可以選擇是否提供來源語言代碼;如果我們不提供,伺服器將在翻譯之前檢測來源語言。
  • 另外,我們也可以選擇是否提供翻譯模型(”base” 或 “nmt”,詳見上一部分)。如果省略該參數值,伺服器將進行 NMT。如果不是從英語翻譯或是翻譯到英語,則會自動使用 PBMT 翻譯。在這個範例 app 中,我們將會省略此參數,因為預設的行為聽起來適合 app 的需求。

讓我們創建一個包含 URL 參數的字典:

func translate(completion: @escaping (_ translations: String?) -> Void) {
    guard let textToTranslate = textToTranslate, let targetLanguage = targetLanguageCode else { completion(nil); return }

    var urlParams = [String: String]()
    urlParams["key"] = apiKey
    urlParams["q"] = textToTranslate
    urlParams["target"] = targetLanguage
    urlParams["format"] = "text"

    if let sourceLanguage = sourceLanguageCode {
        urlParams["source"] = sourceLanguage
    }

}

當成功檢測到文本語言時,sourceLanguageCode 屬性將會在語言偵測的階段獲得數值。實際上來說,如果在翻譯之前就進行語言檢測,那麼我們就可以將 source URL 參數跟著請求一起發送。而在相反的情況下,我們就不會傳遞它,反而在翻譯時動態地進行語言檢測。

下一步,發送翻譯請求:

func translate(completion: @escaping (_ translations: String?) -> Void) {
    // ...

    makeRequest(usingTranslationAPI: .translate, urlParams: urlParams) { (results) in
        guard let results = results else { completion(nil); return }

    }
}

確定 results 裡面有值,而不是 makeRequest(usingTranslationAPI:urlParams:completion:) 方法所回傳的 nil 之後,我們就可以解析數據來提取翻譯內容。

備註:請記住,q URL 參數可以在單次請求中作多次傳遞來進行即時翻譯。如果這樣做,那麼伺服器也將回傳多個翻譯結果。在這裡,我們不會作示範,我們只會傳遞一段文本進行翻譯,並獲得一個翻譯作為回傳值。

接下來你就可以看到,將英文 “Good morning!” 翻譯到法文時,伺服器回傳的一個簡單 JSON 結果:

["data": {
    translations =     (
                {
            detectedSourceLanguage = en;
            translatedText = "Bonjour!";
        }
    );
}]

有了 JSON 為回傳值後,我們可以進行解析:

if let data = results["data"] as? [String: Any], let translations = data["translations"] as? [[String: Any]] {
    var allTranslations = [String]()
    for translation in translations {
        if let translatedText = translation["translatedText"] as? String {
            allTranslations.append(translatedText)
        }
    }

    if allTranslations.count > 0 {
        completion(allTranslations[0])
    } else {
        completion(nil)
    }


} else {
    completion(nil)
}

在上面的程式碼之中,我們把所有回傳的翻譯結果集中到 allTranslations 陣列之中(雖然這種做法有助於需要多個翻譯回傳的情況,但我們只需要一筆翻譯也可使用)。然後,我們將第一個找到的翻譯回傳到完成處理器。

現在 translate(completion:) 方法已經準備好了,但是我們還沒辦法使用它,因為我們還缺了一些東西。

首先,打開 EditorViewController.swift 檔案,並到 translate(_:) IBAction 方法。如下面所述來更新它,輸入的文本將會被指派給 TranslationManager 共享實例的 textToTranslate 屬性裡:

@IBAction func translate(_ sender: Any) {
    if textView.text != "" {

        TranslationManager.shared.textToTranslate = textView.text

        performSegue(withIdentifier: "LanguagesViewControllerSegue", sender: self)
    }
}

加入這一行程式碼:

TranslationManager.shared.textToTranslate = textView.text

我們就可以確認有需要翻譯的來源文本。

接著,打開 LanguagesViewController.swift 檔案,並到 tableView(_:didSelectRowAt:) 表格視圖的委派方法之中,它目前應該還是空的。讓我們加入下列程式碼:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.cellForRow(at: indexPath)?.setSelected(false, animated: true)
    TranslationManager.shared.targetLanguageCode = TranslationManager.shared.supportedLanguages[indexPath.row].code
    performSegue(withIdentifier: "TranslationViewControllerSegue", sender: self)
}

在加入的這三行程式碼之中,發生了三件不同的事情:

  1. 我們從表格視圖上,點擊的單元格中刪除了選擇。
  2. 我們將被點擊語言的語言代碼保存到 TranslationManager 共享實例的 targetLanguageCode 屬性中。
  3. 我們執行 TranslationViewControllerSegue segue,來把 TranslationViewController 推送到導航堆疊並呈現。

讓我們回顧一下,到目前為止,在這一部分中我們為 TranslationManager 類別加入了一些新屬性,我們也實作了一個用來翻譯的新方法,並對 EditorViewController 以及 LanguagesViewController 類別做了一些必要的更新,因此我們收集到所有翻譯來源文本必要的數據。我們還需要到 TranslationViewController 啟動翻譯程序,這會在視圖控制器的視圖出現時發生。

打開 TranslationViewController.swift 檔案,並加入下列新方法的實作。在這裡,我們會顯示一個帶有等待訊息的無按鈕警報控制器,並呼叫 TranslationManager 類別的 translate(completion:) 方法,來翻譯原始文本。成功的話,我們會將翻譯結果呈現在 TranslationViewController 的文本視圖當中,否則我們只會顯示一條警告訊息,說明出現了某些問題。

就像下面這樣:

func initiateTranslation() {
    // Present a "Please wait..." alert.
    alertCollection.presentActivityAlert(withTitle: "Translation", message: "Your text is being translated...") { (presented) in
        if presented {

            TranslationManager.shared.translate(completion: { (translation) in

                // Dismiss the buttonless alert.
                self.alertCollection.dismissAlert(completion: nil)

                if let translation = translation {

                    DispatchQueue.main.async { [unowned self] in
                        self.textView.text = translation
                    }

                } else {
                    self.alertCollection.presentSingleButtonAlert(withTitle: "Translation", message: "Oops! It seems that something went wrong and translation cannot be done.", buttonTitle: "OK", actionHandler: {

                    })
                }

            })

        }
    }
}

不要忘了呼叫上面的方法。在 viewDidAppear(_:) 中執行此操作:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    initiateTranslation()
}

就是這樣!我們的 app 已經完成了,馬上來試試看吧!

Translation Demo

總結

能夠利用科技演進的優點,並將現有的服務(像是即時翻譯)整合到 app 當中,是現今是越來越重要的技巧。考量到這一點,當你決定要在專案之中使用 Google Cloud Translation APIs 時,本篇教學提供的範例 app 就是一個很好的指南。實際上,範例 app 的某些小部分都可以照原始情況來使用,只要做些微的調整就能夠涵蓋到所有沒有考慮的情況,像是之前提到翻譯多個文本的情況,但其實我們已經涵蓋了大部分的重要內容。

我能夠理解不是人人願意為了將翻譯功能整合到 app 之中而付費,但是整體看來,比起翻譯技術背後的知識,支付翻譯 API 的一點成本其實相當划算。最後,記等多閱讀前面提到的 Google API 官方說明文件。希望你喜歡我們這次的教學文章,下次再會!

如果有需要,你可以從 GitHub 下載完整的專案來參考。

譯者簡介:HengJay,iOS 初學者,閒暇之餘習慣透過線上 MOOC 資源學習新的技術,喜歡 Swift 平易近人的語法也喜歡狗狗,目前參與生醫領域相關應用的 App 開發,希望分享文章的同時也能持續精進自己的基礎。

LinkedIn: https://www.linkedin.com/in/hengjiewang/
Facebook: https://www.facebook.com/hengjie.wang 原文Using Google Cloud Translation API to Power Your App with Instant Translation
作者
Gabriel Theodoropoulos
資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。