Google MLKIT Natural Language Translation App | Android Development

ibrahimcanerdogan
5 min readFeb 26, 2024

--

With ML Kit’s on-device translation API, you can dynamically translate text between more than 50 languages.

  • Broad language support Translate between more than 50 different languages. See the complete list.
  • Proven translation models Powered by the same models used by the Google Translate app’s offline mode.
  • Dynamic model management Keep on-device storage requirements low by dynamically downloading and managing language packs.
  • Runs on the device Translations are performed quickly, and don’t require you to send users’ text to a remote server.

🟢 ANDROID WITH MACHINE LEARNING! (COURSE)

🟢 KOTLIN INTERVIEW BOOTCAMP! (COURSE)

Adapter

Started implementing a TranslationLanguageAdapter class for a RecyclerView in Android using Kotlin. The code you've provided defines the adapter and a nested LanguageViewHolder. The LanguageViewHolder is responsible for binding the data to the corresponding views using data binding.

class TranslationLanguageAdapter(
private val languages: List<SupportedLanguages>,
private val onItemClick: (SupportedLanguages) -> Unit
) : RecyclerView.Adapter<TranslationLanguageAdapter.LanguageViewHolder>() {

private var selectedItemPosition: Int = RecyclerView.NO_POSITION

class LanguageViewHolder(
private val binding: ItemTranslationLanguageBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(supportedLanguages: SupportedLanguages) {
binding.language = supportedLanguages
}
}
}
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LanguageViewHolder {
val binding = ItemTranslationLanguageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return LanguageViewHolder(binding)
}

override fun onBindViewHolder(holder: LanguageViewHolder, position: Int) {
val language = languages[position]
holder.bind(language)

// Set the background based on the selected item
if (position == selectedItemPosition) {
holder.itemView.setBackgroundResource(R.drawable.background_clicked_button)
} else {
holder.itemView.setBackgroundResource(R.drawable.background_unclicked_button)
}

holder.itemView.setOnClickListener {
// Update the selected item position
val previousSelectedItemPosition = selectedItemPosition
selectedItemPosition = holder.adapterPosition

// Notify the adapter about the item change to update backgrounds
notifyItemChanged(previousSelectedItemPosition)
notifyItemChanged(selectedItemPosition)
onItemClick.invoke(language)
}
}

override fun getItemCount(): Int {
return languages.size
}

Enum Class

The provided Kotlin code defines an enum class named SupportedLanguages that encapsulates information about various languages. Each language is represented by an enum entry, and each entry has three properties:

  1. langEnglish: Represents the English name of the language.
  2. langNative: Represents the native name of the language.
  3. lanCode: Represents the language code associated with the language.
enum class SupportedLanguages(val langEnglish: String, val langNative: String, val lanCode: String) {
English("English", "English", "en"),
German("German", "Deutsch", "de"),
Turkish("Turkish", "Türkçe", "tr"),
Spanish("Spanish", "español", "es"),
French("French", "français", "fr"),
Italian("Italian", "italiano", "it"),
Irish("Irish", "Gaeilge", "ga"),
Japanese("Japanese", "日本語", "ja"),
Korean("Korean", "한국어", "ko"),
Dutch("Dutch", "Nederlands", "nl"),
Chinese("Chinese", "中文", "zh"),
Norwegian("Norwegian", "norsk", "no"),
Polish("Polish", "polski", "pl"),
Afrikaans("Afrikaans", "Afrikaans", "af"),
Arabic("Arabic", "العربية", "ar"),
Belarusian("Belarusian", "беларуская", "be"),
Bulgarian("Bulgarian", "български", "bg"),
Bengali("Bengali", "বাংলা", "bn"),
Catalan("Catalan", "català", "ca"),
Czech("Czech", "čeština", "cs"),
Welsh("Welsh", "Cymraeg", "cy"),
Danish("Danish", "dansk", "da"),
Greek("Greek", "ελληνικά", "el"),
Estonian("Estonian", "eesti", "et"),
Persian("Persian", "فارسى", "fa"),
Finnish("Finnish", "suomi", "fi"),
Galician("Galician", "galego", "gl"),
Gujarati("Gujarati", "ગુજરાતી", "gu"),
Hebrew("Hebrew", "עברית", "he"),
Hindi("Hindi", "हिंदी", "hi"),
Croatian("Croatian", "hrvatski", "hr"),
Hungarian("Hungarian", "magyar", "hu"),
Indonesian("Indonesian", "Bahasa Indonesia", "id"),
Icelandic("Icelandic", "íslenska", "is"),
Georgian("Georgian", "ქართული", "ka"),
Kannada("Kannada", "ಕನ್ನಡ", "kn"),
Lithuanian("Lithuanian", "lietuvių", "lt"),
Latvian("Latvian", "latviešu", "lv"),
Macedonian("Macedonian", "македонски јазик", "mk"),
Marathi("Marathi", "मराठी", "mr"),
Malay("Malay", "Bahasa Malaysia", "ms"),
Maltese("Maltese", "Malti", "mt"),
Portuguese("Portuguese", "Português", "pt"),
Romanian("Romanian", "română", "ro"),
Russian("Russian", "русский", "ru"),
Slovak("Slovak", "slovenčina", "sk"),
Slovenian("Slovenian", "slovenski", "sl"),
Albanian("Albanian", "shqipe", "sq"),
Swedish("Swedish", "svenska", "sv"),
Swahili("Kiswahili", "Kiswahili", "sw"),
Tamil("Tamil", "தமிழ்", "ta"),
Telugu("Telugu", "తెలుగు", "te"),
Thai("Thai", "ไทย", "th"),
Ukrainian("Ukrainian", "українська", "uk"),
Urdu("Urdu", "اُردو", "ur"),
Vietnamese("Vietnamese", "Tiếng Việt", "vi"),
}

Translation Resource

The provided Kotlin code defines a sealed class named TranslationResource<T>. This class is commonly used for representing the different states that can occur during the process of fetching or processing data, such as success, loading, or encountering an error.

sealed class TranslationResource<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T) : TranslationResource<T>(data)
class Loading<T>(data: T? = null) : TranslationResource<T>(data)
class Error<T>(message: String, data: T? = null) : TranslationResource<T>(data, message)
}

ViewModel

The TranslationViewModel class you provided is a part of the Android Architecture Components and is designed to handle translation-related logic in an Android application. It uses the ViewModel class to manage and persist UI-related data across configuration changes.

This ViewModel is designed to be used with an associated UI component (like an Activity or Fragment) that observes the translateData LiveData to update the user interface based on the translation state. The use of the ViewModel class ensures that the translation-related data is retained during configuration changes, providing a robust and lifecycle-aware solution.

class TranslationViewModel : ViewModel() {

private val translate = MutableLiveData<TranslationResource<String?>>()
val translateData: LiveData<TranslationResource<String?>>
get() = translate

fun processTranslation(translateText: String, languageSource: String, languageTarget: String) {
translate.postValue(TranslationResource.Loading())

val options = TranslatorOptions.Builder()
.setSourceLanguage(languageSource)
.setTargetLanguage(languageTarget)
.build()

val translator = Translation.getClient(options)

val conditions = DownloadConditions.Builder()
.requireWifi()
.build()

translator.downloadModelIfNeeded(conditions)
.addOnSuccessListener {
translator.translate(translateText)
.addOnSuccessListener { translatedText ->
translate.postValue(TranslationResource.Success(translatedText))
}
.addOnFailureListener { exception ->
translate.postValue(TranslationResource.Error("Cannot be translate!"))
}
}
.addOnFailureListener { exception ->
translate.postValue(TranslationResource.Error("Model couldn’t be downloaded! Internet connection required."))
}
}
}

Fragment

The TranslationFragment class represents a fragment in an Android application responsible for displaying a user interface related to translation. It uses the Android Architecture Components, including ViewModel, LiveData, and data binding, to handle translation-related logic and UI updates. Here's a breakdown of the code:

class TranslationFragment : Fragment() {

private var _binding: FragmentTranslationBinding? = null
private val binding get() = _binding!!

private val viewModel: TranslationViewModel by viewModels()

private var languageCodeSource: String? = null
private var languageCodeTarget: String? = null

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTranslationBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

observeViewModel()
handleUIElements()
}

private fun handleUIElements() {
with(binding) {
// RecyclerView
recyclerViewSource.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
val sourceAdapter = TranslationLanguageAdapter(SupportedLanguages.entries) { selectedLanguage ->
languageCodeSource = selectedLanguage.lanCode
Toast.makeText(requireContext(), "Selected: ${selectedLanguage.langNative}", Toast.LENGTH_SHORT).show()
}
recyclerViewSource.adapter = sourceAdapter

recyclerViewTarget.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
val targetAdapter = TranslationLanguageAdapter(SupportedLanguages.entries) { selectedLanguage ->
languageCodeTarget = selectedLanguage.lanCode
Toast.makeText(requireContext(), "Selected: ${selectedLanguage.langNative}", Toast.LENGTH_SHORT).show()
}
recyclerViewTarget.adapter = targetAdapter

// Scroll Result Text
textViewTranslation.movementMethod = ScrollingMovementMethod()

buttonCopyTranslate.setOnClickListener { copyTextToClipboard() }

val textChangeListener = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
if (languageCodeSource != null && languageCodeTarget != null) {
Handler(Looper.getMainLooper()).postDelayed({
viewModel.processTranslation(s.toString(), languageCodeSource!!, languageCodeTarget!!)
buttonCopyTranslate.visibility = if (s.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
}, 500)
} else {
Toast.makeText(requireContext(), "Select Source & Target Language!", Toast.LENGTH_SHORT).show()
}
}
}

textInputTranslation.addTextChangedListener(textChangeListener)
}
}

private fun observeViewModel() {
viewModel.translateData.observe(viewLifecycleOwner, ::setTranslatedText)
}

private fun setTranslatedText(translationResource: TranslationResource<String?>?) {
when(translationResource) {
is TranslationResource.Success -> {
binding.circularProgress.visibility = View.INVISIBLE
translationResource.data?.let {
binding.textViewTranslation.text = it
}
}
is TranslationResource.Error -> {
binding.circularProgress.visibility = View.INVISIBLE
translationResource.data?.let {
binding.textViewTranslation.text = it
}
}
is TranslationResource.Loading -> {
binding.circularProgress.visibility = View.VISIBLE
}
else -> {
binding.circularProgress.visibility = View.INVISIBLE
binding.textViewTranslation.text = "null"
}
}
}

private fun copyTextToClipboard() {
val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Text", binding.textViewTranslation.text.toString())
clipboardManager.setPrimaryClip(clip)

Toast.makeText(requireContext(), "Translation copied to clipboard!", Toast.LENGTH_SHORT).show()
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

}

Design

The XML layout file you provided represents the UI layout for the TranslationFragment. It uses a LinearLayout as the root layout, and here's a breakdown of its structure:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:layout_marginBottom="?android:actionBarSize"
android:orientation="vertical"
tools:context=".identification.LanguageIdentificationFragment">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewSource"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:listitem="@layout/item_translation_language" />

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textLayoutTranslation"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:hint="Enter Text Translate!"
android:padding="10dp">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textInputTranslation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="@font/poppins_regular"
android:maxLength="2000"
android:textSize="14sp" />

</com.google.android.material.textfield.TextInputLayout>


<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewTarget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:listitem="@layout/item_translation_language" />

<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="10dp"
android:layout_weight="1">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewTranslation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_border_textview"
android:fontFamily="@font/poppins_regular"
android:hint="The text to be translated will appear here!"
android:padding="15dp"
android:scrollbars="vertical"
android:textSize="14sp" />

<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/circularProgress"
style="?attr/circularProgressIndicatorStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="invisible" />

<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/buttonCopyTranslate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@android:color/transparent"
android:padding="10dp"
android:src="@drawable/icon_copy"
android:tint="@color/material_dynamic_tertiary60"
android:visibility="invisible" />

</FrameLayout>

</LinearLayout>

İbrahim Can Erdoğan

LINKEDIN

YOUTUBE

UDEMY

GITHUB

--

--

ibrahimcanerdogan
ibrahimcanerdogan

Written by ibrahimcanerdogan

Hi, My name is Ibrahim, I am developing ebebek android app within Ebebek. I publish various articles in the field of programming and self-improvement.

No responses yet