기본 콘텐츠로 건너뛰기

안드로이드 앱 만들기 : ML translation 에러 메시지를 번역 해 보자


원본출처: 티스토리 바로가기

Translation

번역을 시도하는 방법에는 여러 가지가 있습니다.  그중에는 비용이 들어가는 방법도 있고, 일부 무료인 방법도 있습니다.

  • 카카오 번역  API : 월 10,000자 까지는 무료 이후 1000자 단위로 18원 발
  • 구글 cloud 번역 API : 월 최대 500,000자 까지는 무료 이후부터는 비용 추가 (기본 옵션 선택 시)
  • 네이버 papago text 번역 : 1,000,000 단위 과금  20,000원 (과금 단위는 글자를 항상 올림)

등의 방법을 찾을 수 있습니다. 

 

오늘 하고 싶은 이야기는 간단한 문구를 그냥 번역해 보는 방법입니다. 

 

문서의 길이가 길고, 중요한 문서라고 한다면 비용이 들여서라도 번역은 정확하게 하는 것이 맞을 것 같습니다.  단지,  앱에서 사용하는 API들이 꼬부랑말(대부분 영어)로 되어 있는 것들이라서 API가 제공하는 오류 메시지를 그대로 보여 주는 것은 사용자 편의를 고려하지 않은 것이라 생각하게 되어 간편 번역을 해 보기로 했습니다. 

 

구글에서 제공하는 ML Kit에 보면 여러 가지가 있지만 오늘은 그중에서 Translation의 사용하는 방법에 대한 예시를 적어 두고자 합니다. 

 

https://developers.google.com/ml-kit/language/translation/android

 

Android에서 ML Kit를 사용하여 텍스트 번역  |  Google Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English Android에서 ML Kit를 사용하여 텍스트 번역 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

developers.google.com

 

IOS 개발은 아직 해 보지 않기 때문에... 안드로이드로 개발하는 부분만 말해 볼까 합니다.  이 글은 위에 있는 개발 가이드의 내용을 참조하여 작성하였습니다.

Gradle

앱 수준의 gradle 파일에 한 줄 넣어 주세요.

// ML Kit translate implementation 'com.google.mlkit:translate:17.0.1'

 

Viewmodel

다음은 가이드에 나와 있는 내용들을 다 정리하기가 너무 힘들어서 가이드의 예시용 sourcecode에서 viewModel 코드 파일을 그냥 복사했습니다.

 

/*  * Copyright 2019 Google Inc. All Rights Reserved.  *  * Licensed under the Apache License, Version 2.0 (the "License");  * you may not use this file except in compliance with the License.  * You may obtain a copy of the License at  *  * http://www.apache.org/licenses/LICENSE-2.0  *  *  Unless required by applicable law or agreed to in writing, software  *  distributed under the License is distributed on an "AS IS" BASIS,  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  *  See the License for the specific language governing permissions and  *  limitations under the License.  */  import android.app.Application import android.util.LruCache import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.billcoreatech.remotepayment0119.R import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.mlkit.common.model.DownloadConditions import com.google.mlkit.common.model.RemoteModelManager import com.google.mlkit.nl.translate.TranslateLanguage import com.google.mlkit.nl.translate.TranslateRemoteModel import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.Translator import com.google.mlkit.nl.translate.TranslatorOptions  import java.util.Locale  /**  * Model class for tracking available models and performing live translations  */ class TranslateViewModel(application: Application) : AndroidViewModel(application) {    companion object {     // This specifies the number of translators instance we want to keep in our LRU cache.     // Each instance of the translator is built with different options based on the source     // language and the target language, and since we want to be able to manage the number of     // translator instances to keep around, an LRU cache is an easy way to achieve this.     private const val NUM_TRANSLATORS = 3   }    private val modelManager: RemoteModelManager = RemoteModelManager.getInstance()   private val pendingDownloads: HashMap<String, Task<Void>> = hashMapOf()   private val translators =     object : LruCache<TranslatorOptions, Translator>(NUM_TRANSLATORS) {       override fun create(options: TranslatorOptions): Translator {         return Translation.getClient(options)       }       override fun entryRemoved(         evicted: Boolean,         key: TranslatorOptions,         oldValue: Translator,         newValue: Translator?,       ) {         oldValue.close()       }     }   val sourceLang = MutableLiveData<Language>()   val targetLang = MutableLiveData<Language>()   val sourceText = MutableLiveData<String>()   val translatedText = MediatorLiveData<ResultOrError>()   val availableModels = MutableLiveData<List<String>>()    // Gets a list of all available translation languages.   val availableLanguages: List<Language> = TranslateLanguage.getAllLanguages().map { Language(it) }    init {     // Create a translation result or error object.     val processTranslation =       OnCompleteListener<String> { task ->         if (task.isSuccessful) {           translatedText.value = ResultOrError(task.result, null)         } else {           translatedText.value = ResultOrError(null, task.exception)         }         // Update the list of downloaded models as more may have been         // automatically downloaded due to requested translation.         fetchDownloadedModels()       }     // Start translation if any of the following change: input text, source lang, target lang.     translatedText.addSource(sourceText) { translate().addOnCompleteListener(processTranslation) }     val languageObserver =       Observer<Language> { translate().addOnCompleteListener(processTranslation) }     translatedText.addSource(sourceLang, languageObserver)     translatedText.addSource(targetLang, languageObserver)      // Update the list of downloaded models.     fetchDownloadedModels()   }    private fun getModel(languageCode: String): TranslateRemoteModel {     return TranslateRemoteModel.Builder(languageCode).build()   }    // Updates the list of downloaded models available for local translation.   private fun fetchDownloadedModels() {     modelManager.getDownloadedModels(TranslateRemoteModel::class.java).addOnSuccessListener {       remoteModels ->       availableModels.value = remoteModels.sortedBy { it.language }.map { it.language }     }   }    // Starts downloading a remote model for local translation.   internal fun downloadLanguage(language: Language) {     val model = getModel(TranslateLanguage.fromLanguageTag(language.code)!!)     var downloadTask: Task<Void>?     if (pendingDownloads.containsKey(language.code)) {       downloadTask = pendingDownloads[language.code]       // found existing task. exiting       if (downloadTask != null && !downloadTask.isCanceled) {         return       }     }     downloadTask =       modelManager.download(model, DownloadConditions.Builder().build()).addOnCompleteListener {         pendingDownloads.remove(language.code)         fetchDownloadedModels()       }     pendingDownloads[language.code] = downloadTask   }    // Returns if a new model download task should be started.   fun requiresModelDownload(     lang: Language,     downloadedModels: List<String?>?,   ): Boolean {     return if (downloadedModels == null) {       true     } else !downloadedModels.contains(lang.code) && !pendingDownloads.containsKey(lang.code)   }    // Deletes a locally stored translation model.   internal fun deleteLanguage(language: Language) {     val model = getModel(TranslateLanguage.fromLanguageTag(language.code)!!)     modelManager.deleteDownloadedModel(model).addOnCompleteListener { fetchDownloadedModels() }     pendingDownloads.remove(language.code)   }    fun translate(): Task<String> {     val text = sourceText.value     val source = sourceLang.value     val target = targetLang.value     if (source == null || target == null || text == null || text.isEmpty()) {       return Tasks.forResult("")     }     val sourceLangCode = TranslateLanguage.fromLanguageTag(source.code)!!     val targetLangCode = TranslateLanguage.fromLanguageTag(target.code)!!     val options =       TranslatorOptions.Builder()         .setSourceLanguage(sourceLangCode)         .setTargetLanguage(targetLangCode)         .build()     return translators[options].downloadModelIfNeeded().continueWithTask { task ->       if (task.isSuccessful) {         translators[options].translate(text)       } else {         Tasks.forException<String>(           task.exception             ?: Exception(getApplication<Application>().getString(R.string.unknown_error))         )       }     }   }    /** Holds the result of the translation or any error. */   inner class ResultOrError(var result: String?, var error: Exception?)    /**    * Holds the language code (i.e. "en") and the corresponding localized full language name (i.e.    * "English")    */   class Language(val code: String) : Comparable<Language> {      private val displayName: String       get() = Locale(code).displayName      override fun equals(other: Any?): Boolean {       if (other === this) {         return true       }        if (other !is Language) {         return false       }        val otherLang = other as Language?       return otherLang!!.code == code     }      override fun toString(): String {       return "$code - $displayName"     }      override fun compareTo(other: Language): Int {       return this.displayName.compareTo(other.displayName)     }      override fun hashCode(): Int {       return code.hashCode()     }   }    override fun onCleared() {     super.onCleared()     // Each new instance of a translator needs to be closed appropriately. Here we utilize the     // ViewModel's onCleared() to clear our LruCache and close each Translator instance when     // this ViewModel is no longer used and destroyed.     translators.evictAll()   } }

 

 

MainActivity

예시에 앱에서는 콤보 박스를 이용해서 source 언어와 번역 후 사용할 target 언어를 선택하도록 하였으나, 만들고 있는 앱에서는 영어(en)를 한국어(ko)로 번역하는 것만 사용할 생각 이기 때문에 다음과 같은 부분들을 MainActivity에 추가해 주었습니다. 

 

import android.app.Activity ..... import java.security.MessageDigest  class MainActivity : ComponentActivity() {      private val translateView : TranslateViewModel by viewModels()          .....      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)          // 간단한 문서 번역 ML         translateView.sourceLang.value = TranslateViewModel.Language("en")         translateView.targetLang.value = TranslateViewModel.Language("ko")         if (!sp.getBoolean("isDownloadKR", false)) {             translateView.downloadLanguage(TranslateViewModel.Language("ko"))         }         // 한국어 모델은 download 을 한 번 받아야 해서          translateView.availableModels.observe(             this@MainActivity         ) {result ->             for (lang in result) {                 Log.e("", "lang=${lang}")                 if (lang == "kr") { ... // 다운로드 받은 걸 표시 해 두었다가 앱을 실행 할 때 마다 받는 건 방지 하도록 구현                 }             }         }          ...          setContent {              val scrollableState = rememberScrollState()              RemotePayment0119Theme {                 // A surface container using the 'background' color from the theme                 Column(                     modifier = Modifier                         .fillMaxSize()                         .padding(30.dp)                         .verticalScroll(scrollableState),                     horizontalAlignment = Alignment.CenterHorizontally,                     verticalArrangement = Arrangement.Center                 ) {                     DestinationsNavHost(navGraph = NavGraphs.root) {                         composable(JoinUserScreenDestination) {                             JoinUserScreen(                                 navigator = destinationsNavigator,                                 doSignup = { emailId, password ->                                     doSignupUser(emailId, password)                                 }                             )                         }                         composable(LoginOptionsDestination) {                             LoginOptions(                                 doLogin = { email, password ->                                     doEmailLogin(email, password)                                 },                                 doForGotPassword = { email ->                                     doForgotPassword(email)                                 },                                 doGoogleLogin = {                                     doGoogleLogin()                                 },                                 doFacebookLogin = {                                     doFacebookLogin()                                 },                                 doRegisterUser = {                                     destinationsNavigator.navigate(JoinUserScreenDestination)                                 },                                 doTranslateDownload = {                                 // 화면에서 버튼 클릭을 통해 일부로 받을 수 있도록 구현 할 수 도 있음.                                     translateView.downloadLanguage(TranslateViewModel.Language("ko"))                                     doToastMake(R.string.msgBeginDownload)                                 },                                 doEmailLoginWithLink = { emailid ->                                     doEmailLoginWithLink(emailid)                                 }                             )                         }                     }                 }             }         }     }      private fun doEmailLoginWithLink(emailId: MutableState<String>) {      }      private fun doSignupUser(emailId: MutableState<String>, password: MutableState<String>) {      }      private fun doFacebookLogin() {              }      private fun doGoogleLogin() {              }      private fun doForgotPassword(email: MutableState<String>) {      }      private fun doEmailLogin(email: MutableState<String>, password: MutableState<String>) {          if (email.value == "") {             doToastMake(R.string.EnterEmail)             return         }         if (password.value == "") {             doToastMake(R.string.EnterPassword)             return         }          auth.signInWithEmailAndPassword(email.value, password.value)             .addOnSuccessListener {                 doToastMake(R.string.msgSigninCompleted)             }             .addOnFailureListener {                 doToastMakeAppend(R.string.msgUserIdOrPasswordError, it.localizedMessage)             }     }      private fun doToastMake(msgResource: Int) {         Toast.makeText(this@MainActivity, getString(msgResource), Toast.LENGTH_SHORT).show()     }      private fun doToastMakeAppend(msgResource: Int, localizedMessage: String?) {          // 에러 메시지가 영어로 오기 때문에 그것을 번역해 본다.         // 번역이 된 이후에 toast 가 나오도록 구현         translateView.sourceText.value = "$localizedMessage"         translateView.translatedText.observe(             this@MainActivity         ) { result ->             Log.e("", "result = ${result.error} ${result.result}")             resultString = result.result.toString()              Toast.makeText(this@MainActivity, "${getString(msgResource)}:${resultString}", Toast.LENGTH_SHORT).show()         }      }  }  @Preview(showBackground = true) @Composable fun DefaultPreview() {     RemotePayment0119Theme {         DestinationsNavHost(navGraph = NavGraphs.root)     } }

코드를 살펴보면 onCreate에서 먼저 사용할 언어를 정했습니다. 영어와 한국어로... 여기서 실수를 했던 부분은 한국어를 kr이라고 했던 건데,  무관심하게 kr 이라고 할 수 있으나, 예제 앱에서도 볼 수 있지만, ko라고 해 주어야 됩니다. (다들 아시는 것이라 생각하고 있습니다.)

 

그리고는 doToastMakeAppend 함수에서 인수로 받은 오류 메시지를 번역해 보여 주는 방식으로 코드를 작성했습니다. 아직 잘 알지 못하는 부분은 번역을 위해 전달한 문장이 2줄인 경우 응답이 2번으로 나누어져 오는 것 같다는 것입니다.  그래서 Toast 가 2번 발생됩니다.  그걸 해소하는 건 이 글을 보시는 분들의 몫으로 남겨 둡니다. 

 

아무튼 이렇게 하면 어렵지 않게 영어 오류 메시지를 한글로 번역해서 보여 줄 수 있습니다. 저처럼 firebase의 기능을 활용하시는 분들께 한 가지 팁이 될지 바랍니다.

 

 

활용예시

이렇게 구현된 앱 샘플입니다.  firebase에 email을 통해 로그인하기 위해서 사용자 가입을 시도해 봅니다.  그때 입력된 password의 길이가 6자리 미만의 경우 오류 처리가 됩니다. 

private fun doSignupUser(emailId: MutableState<String>, password: MutableState<String>) {      if (emailId.value == "") {         doToastMake(R.string.EnterEmail)         return     }     if (password.value == "") {         doToastMake(R.string.EnterPassword)         return     }     auth.createUserWithEmailAndPassword(emailId.value, password.value)         .addOnSuccessListener {             doToastMake(R.string.msgCreateUserCompleted)         }         .addOnFailureListener {             doToastMakeAppend(R.string.msgCreateUserFailure, it.localizedMessage)         } }

이때 나오는 오류 메시지를 doToastMakeAppend 함수 호출을 통해 번역을 시도해 보는 것입니다. 

 

간편 번역 샘플

 

이상으로 ML Translation에 대해 알아보았습니다.

 

읽어 주셔서 감사합니다.

 

 

귤탐 당도선별 감귤 로열과, 3kg(S~M), 1박스 삼립 호빵 발효미종 단팥, 92g, 14개입 [엉클컴퍼니] 우리밀 찐빵/흑미찐빵/단호박찐빵/고구마찐빵 국산팥, 우리밀 고구마찐빵(20개입) 1300g 국산팥 우리밀 MORIT 여성용 방한장갑 터치스크린 다용도 고급겨울장갑 에이치머스 스마트폰 터치 방한 장갑
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

댓글

이 블로그의 인기 게시물

개인정보처리방침 안내

 billcoreaTech('https://billcoreatech.blogspot.com/'이하 'https://billcoreatech.blogspot')은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다. ○ 이 개인정보처리방침은 2021년 8월 26부터 적용됩니다. 제1조(개인정보의 처리 목적) billcoreaTech('https://billcoreatech.blogspot.com/'이하 'https://billcoreatech.blogspot')은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다. 1. 서비스 제공 맞춤서비스 제공을 목적으로 개인정보를 처리합니다. 제2조(개인정보의 처리 및 보유 기간) ① billcoreaTech은(는) 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다. ② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다. 1.<서비스 제공> <서비스 제공>와 관련한 개인정보는 수집.이용에 관한 동의일로부터<사용자의 설정시간>까지 위 이용목적을 위하여 보유.이용됩니다. 보유근거 : 앱의 기본기능 활용에 필요한 위치정보 제3조(개인정보의 제3자 제공) ① billcoreaTech은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다. ② billcoreaTech...

안드로이드 앱 만들기 : onBackPressed 가 deprecated 되었다니 ?

원본출처: 티스토리 바로가기 onBackPressed 가 deprecated 되었다? 이제 우리는 구글이 제안하는 안드로이드 13에 타기팅하는 앱을 제출 해야만 하는 시기에 도달하고 있습니다.  구글이 새로운 안드로이드 버전을 배포하기 시작하면서 오래된 안드로이드에 대한 게시를 제한 합니다.    그래서 이번에 API 33 인 안드로이드 13에 타겟팅 하는 앱을 작성해 보았습니다. 그러다 만난 몇 가지 사용 제한이 되는 것들에 대한 정리를 해 두고자 합니다.    onBackPressed는 사용자가 뒤로 가기 버튼을 클릭하는 경우 제어를 하기 위해서 사용했던 함수 입니다. MainActivity 에서 최종적으로 뒤로 가기를 클릭 하는 경우 앱을 종료시키는 기능도 사용이 되는 함수였는 데...   안드로이드 13에서는 더 이상 사용할 수 없는 (?)  - 사용은 가능 하나 소스 코드에 중간 줄이 생긴 모양을 보면서 코드를 지속적으로 봐야 합니다.    onBackPressed 어떻게 해소를 하면 될까요?   CallBack을 하나 만들어 봅니다. private val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { doCloseApps() } } 다른 건 없고 이런 모양으로 callback 함수를 하나 만들어 둡니다.  그러고 onCreate 에서 이 callback 이 호출 되도록 한 줄 넣어 주는 것으로 그 코딩은 마무리 됩니다.    @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(sav...