MessageTranslationViewController.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. //
  2. // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import UIKit
  6. @objcMembers class MessageTranslationViewController: UIViewController {
  7. @IBOutlet weak var fromLabel: UILabel!
  8. @IBOutlet weak var fromButton: UIButton!
  9. @IBOutlet weak var toButton: UIButton!
  10. @IBOutlet weak var toLabel: UILabel!
  11. @IBOutlet weak var textViewsContainerView: UIView!
  12. @IBOutlet weak var originalTextView: UITextView!
  13. @IBOutlet weak var originalTextViewHeight: NSLayoutConstraint!
  14. @IBOutlet weak var translateTextView: UITextView!
  15. @IBOutlet weak var buttonsContainerView: UIView!
  16. @IBOutlet weak var buttonsContainerViewHeight: NSLayoutConstraint!
  17. var originalMessage: String?
  18. var availableTranslations: [NCTranslation]?
  19. var userLanguageCode: String?
  20. var activeAccount: TalkAccount?
  21. let textHorizontalPadding = 12.0
  22. let textVerticalPadding = 10.0
  23. let textContainerPadding = 16.0
  24. var translatedText: String = ""
  25. var detectedFromLanguageCode: String = ""
  26. var selectedFromLanguageCode: String = ""
  27. var selectedToLanguageCode: String = ""
  28. var translationErrorMessage: String = ""
  29. var modifyingProfileView = UIActivityIndicatorView()
  30. var didTriggerInitialTranslation: Bool = false
  31. required init?(coder aDecoder: NSCoder) {
  32. super.init(coder: aDecoder)
  33. }
  34. init(message: String, availableTranslations: [NCTranslation]) {
  35. super.init(nibName: "MessageTranslationViewController", bundle: .main)
  36. self.originalMessage = message
  37. self.availableTranslations = availableTranslations
  38. self.userLanguageCode = Locale.current.languageCode
  39. self.activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  40. }
  41. override func viewDidLoad() {
  42. super.viewDidLoad()
  43. self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
  44. self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
  45. self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
  46. self.navigationController?.navigationBar.isTranslucent = false
  47. self.navigationItem.title = NSLocalizedString("Translation", comment: "")
  48. let appearance = UINavigationBarAppearance()
  49. appearance.configureWithOpaqueBackground()
  50. appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
  51. appearance.backgroundColor = NCAppBranding.themeColor()
  52. self.navigationItem.standardAppearance = appearance
  53. self.navigationItem.compactAppearance = appearance
  54. self.navigationItem.scrollEdgeAppearance = appearance
  55. self.fromLabel.text = (NSLocalizedString("From", comment: "'From' which language user wants to translate text") + ":")
  56. self.toLabel.text = (NSLocalizedString("To", comment: "'To' which language user wants to translate text") + ":")
  57. self.originalTextView.text = originalMessage
  58. self.originalTextView.textContainerInset = UIEdgeInsets(top: textVerticalPadding, left: textHorizontalPadding,
  59. bottom: textVerticalPadding, right: textHorizontalPadding)
  60. self.translateTextView.textContainerInset = UIEdgeInsets(top: textVerticalPadding, left: textHorizontalPadding,
  61. bottom: textVerticalPadding, right: textHorizontalPadding)
  62. self.originalTextView.textContainer.lineFragmentPadding = 0
  63. self.translateTextView.textContainer.lineFragmentPadding = 0
  64. self.originalTextView.layer.cornerRadius = 8
  65. self.translateTextView.layer.cornerRadius = 8
  66. self.modifyingProfileView = UIActivityIndicatorView()
  67. self.modifyingProfileView.color = NCAppBranding.themeTextColor()
  68. self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancelButtonPressed))
  69. self.navigationItem.leftBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
  70. }
  71. override func viewWillAppear(_ animated: Bool) {
  72. if !didTriggerInitialTranslation {
  73. self.setTranslatingUI()
  74. self.didTriggerInitialTranslation = true
  75. self.configureFromButton(title: NSLocalizedString("Detecting language", comment: ""), enabled: false)
  76. self.configureToButton(title: initialToLanguage(), enabled: false, fromLanguageCode: "")
  77. self.adjustOriginalTextViewSizeToViewSize(size: self.view.bounds.size)
  78. if NCDatabaseManager.sharedInstance().hasTranslationProviders(forAccountId: activeAccount?.accountId ?? "") {
  79. NCAPIController.sharedInstance().getAvailableTranslations(for: activeAccount) { languages, languageDetection, error, _ in
  80. if let translations = languages as? [NCTranslation], error == nil {
  81. self.availableTranslations = translations
  82. if languageDetection {
  83. self.translateOriginalText(from: "", to: self.userLanguageCode ?? "")
  84. } else {
  85. self.configureFromButton(title: nil, enabled: true)
  86. self.configureToButton(title: nil, enabled: false, fromLanguageCode: "")
  87. self.removeTranslatingUI()
  88. }
  89. } else {
  90. self.showTranslationError(message: NSLocalizedString("Could not get available languages", comment: ""))
  91. self.removeTranslatingUI()
  92. }
  93. }
  94. } else {
  95. self.translateOriginalText(from: "", to: userLanguageCode ?? "")
  96. }
  97. }
  98. }
  99. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  100. super.viewWillTransition(to: size, with: coordinator)
  101. coordinator.animate(alongsideTransition: nil) { _ in
  102. self.adjustOriginalTextViewSizeToViewSize(size: self.view.bounds.size)
  103. }
  104. }
  105. func cancelButtonPressed() {
  106. self.dismiss(animated: true, completion: nil)
  107. }
  108. func adjustOriginalTextViewSizeToViewSize(size: CGSize) {
  109. let font = originalTextView.font ?? UIFont.systemFont(ofSize: 16)
  110. let height = (originalMessage?.height(withConstrainedWidth: size.width - textContainerPadding * 2 - textHorizontalPadding * 2, font: font) ?? 0) + textVerticalPadding * 2
  111. let maxHeight = (size.height - textViewsContainerView.frame.origin.y) / 2.0 - (textVerticalPadding * 2)
  112. self.originalTextViewHeight.constant = min(height, maxHeight)
  113. }
  114. func initialToLanguage() -> String {
  115. let availableToLanguage = Locale.current.localizedString(forLanguageCode: userLanguageCode ?? "") ?? ""
  116. guard let availableTranslations = availableTranslations else { return availableToLanguage }
  117. for availableTranslation in availableTranslations where availableTranslation.to == userLanguageCode {
  118. return availableTranslation.toLabel
  119. }
  120. return availableToLanguage
  121. }
  122. // MARK: - Available Languages
  123. func availableFromLanguagesLabels() -> [String] {
  124. var availableFromLanguages: [String] = []
  125. guard let availableTranslations = availableTranslations else { return availableFromLanguages }
  126. for availableTranslation in availableTranslations where !availableFromLanguages.contains(availableTranslation.fromLabel) {
  127. availableFromLanguages.append(availableTranslation.fromLabel)
  128. }
  129. return availableFromLanguages
  130. }
  131. func fromLanguageLabel(languageCode: String) -> String {
  132. let fromLanguageLabel: String = ""
  133. guard let availableTranslations = availableTranslations else { return fromLanguageLabel }
  134. for availableTranslation in availableTranslations where availableTranslation.from == languageCode {
  135. return availableTranslation.fromLabel
  136. }
  137. return fromLanguageLabel
  138. }
  139. func fromLanguageCode(languageLabel: String) -> String {
  140. let fromLanguageCode: String = ""
  141. guard let availableTranslations = availableTranslations else { return fromLanguageCode }
  142. for availableTranslation in availableTranslations where availableTranslation.fromLabel == languageLabel {
  143. return availableTranslation.from
  144. }
  145. return fromLanguageCode
  146. }
  147. func availableToLanguagesLabels(fromLanguageCode: String) -> [String] {
  148. var availableToLanguages: [String] = []
  149. guard let availableTranslations = availableTranslations else { return availableToLanguages }
  150. for availableTranslation in availableTranslations where
  151. availableTranslation.from == fromLanguageCode && !availableToLanguages.contains(availableTranslation.toLabel) {
  152. availableToLanguages.append(availableTranslation.toLabel)
  153. }
  154. return availableToLanguages
  155. }
  156. func toLanguageLabel(languageCode: String) -> String {
  157. let toLanguageLabel: String = ""
  158. guard let availableTranslations = availableTranslations else { return toLanguageLabel }
  159. for availableTranslation in availableTranslations where availableTranslation.to == languageCode {
  160. return availableTranslation.toLabel
  161. }
  162. return toLanguageLabel
  163. }
  164. func toLanguageCode(languageLabel: String) -> String {
  165. let toLanguageCode: String = ""
  166. guard let availableTranslations = availableTranslations else { return toLanguageCode }
  167. for availableTranslation in availableTranslations where availableTranslation.toLabel == languageLabel {
  168. return availableTranslation.to
  169. }
  170. return toLanguageCode
  171. }
  172. // MARK: - Translate
  173. func translateOriginalText(from: String, to: String) {
  174. self.setTranslatingUI()
  175. NCAPIController.sharedInstance().translateMessage(originalMessage, from: from, to: to, for: activeAccount) { responseDict, error, _ in
  176. self.removeTranslatingUI()
  177. if let responseDict = responseDict as? [String: String] {
  178. if let translatedText = responseDict["text"] {
  179. self.translatedText = translatedText
  180. self.translateTextView.text = translatedText
  181. }
  182. if let translatedFrom = responseDict["from"] {
  183. self.detectedFromLanguageCode = translatedFrom
  184. self.selectedToLanguageCode = translatedFrom
  185. let detectedText = from.isEmpty ? " (" + NSLocalizedString("detected", comment: "") + ")" : ""
  186. let title = self.fromLanguageLabel(languageCode: translatedFrom) + detectedText
  187. self.configureFromButton(title: title, enabled: !from.isEmpty)
  188. self.configureToButton(title: self.toButton.titleLabel?.text, enabled: true, fromLanguageCode: translatedFrom)
  189. }
  190. if let errorMessage = responseDict["message"] {
  191. self.translationErrorMessage = errorMessage
  192. }
  193. }
  194. if error != nil {
  195. if self.detectedFromLanguageCode.isEmpty {
  196. self.configureFromButton(title: nil, enabled: true)
  197. self.configureToButton(title: nil, enabled: false, fromLanguageCode: "")
  198. }
  199. var errorMessage = NSLocalizedString("An error occurred trying to translate message", comment: "")
  200. if !self.translationErrorMessage.isEmpty {
  201. errorMessage = self.translationErrorMessage
  202. }
  203. self.showTranslationError(message: errorMessage)
  204. }
  205. }
  206. }
  207. // MARK: - User Interface
  208. func setTranslatingUI() {
  209. self.translateTextView.text = ""
  210. self.modifyingProfileView.startAnimating()
  211. self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: modifyingProfileView)
  212. }
  213. func removeTranslatingUI() {
  214. self.modifyingProfileView.stopAnimating()
  215. self.navigationItem.rightBarButtonItem = nil
  216. }
  217. func configureFromButton(title: String?, enabled: Bool) {
  218. let title = title ?? NSLocalizedString("Select language", comment: "")
  219. self.fromButton.setTitle(title, for: .normal)
  220. var actions: [UIAction] = []
  221. for languageLabel in availableFromLanguagesLabels() {
  222. actions.append(UIAction(title: languageLabel, image: nil, handler: { _ in
  223. self.fromLanguageLabelSelected(languageLabel: languageLabel)
  224. }))
  225. }
  226. self.fromButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
  227. self.fromButton.showsMenuAsPrimaryAction = true
  228. self.fromButton.isEnabled = enabled
  229. }
  230. func fromLanguageLabelSelected(languageLabel: String) {
  231. let fromLanguageCode = fromLanguageCode(languageLabel: languageLabel)
  232. self.selectedFromLanguageCode = fromLanguageCode
  233. self.fromButton.setTitle(languageLabel, for: .normal)
  234. self.configureToButton(title: nil, enabled: true, fromLanguageCode: fromLanguageCode)
  235. }
  236. func configureToButton(title: String?, enabled: Bool, fromLanguageCode: String) {
  237. let title = title ?? NSLocalizedString("Select language", comment: "")
  238. self.toButton.setTitle(title, for: .normal)
  239. var actions: [UIAction] = []
  240. for languageLabel in availableToLanguagesLabels(fromLanguageCode: fromLanguageCode) {
  241. actions.append(UIAction(title: languageLabel, image: nil, handler: { _ in
  242. self.toLanguageLabelSelected(languageLabel: languageLabel)
  243. }))
  244. }
  245. self.toButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
  246. self.toButton.showsMenuAsPrimaryAction = true
  247. self.toButton.isEnabled = enabled
  248. }
  249. func toLanguageLabelSelected(languageLabel: String) {
  250. let toLanguageCode = toLanguageCode(languageLabel: languageLabel)
  251. self.selectedToLanguageCode = toLanguageCode
  252. self.toButton.setTitle(languageLabel, for: .normal)
  253. self.translateOriginalText(from: selectedFromLanguageCode, to: selectedToLanguageCode)
  254. }
  255. func showTranslationError(message: String) {
  256. let errorDialog = UIAlertController(title: NSLocalizedString("Translation failed", comment: ""), message: message, preferredStyle: .alert)
  257. let okAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default)
  258. errorDialog.addAction(okAction)
  259. self.present(errorDialog, animated: true, completion: nil)
  260. }
  261. }