How to handle the localisation or multi language support in android with examples?
Hello everyone,
Today in this article, we are going to learn about localisation to support the multiple languages in our android apps. Localisation is the basic need for the app as we want to engage the app for the different regions to make them understand their language. Generally, we handle the localisation in two different approaches.
- Update the resource configuration and recreate the activity, Here OS itself will figure out and load the correct locale for their resource. But we found that sometimes if the application is heavy to load the resource then it may see a black window for some moment while switching to another language, which is not good for the customer.
- Update the resource configuration and restart the main activity, and dynamically every string is loaded by calling a common function. In this case, the user can not see any black window to change the localisation. Let’s understand this approach in detail with an example.
Let’s take an example of two languages English
and Hindi
to support our app. So first of all we need to create the two folders in the res/values.
- res/values-en
- res/values-hi
Now we need to add our sting.xml file in each folder respectively.
<resources> <string name="title">Hello, I am the life saver.</string> <string name="select_language">Select Language</string> </resources>
<resources> <string name="title">नमस्ते, मैं जीवन रक्षक हूँ।</string> <string name="select_language">भाषा चुने</string> </resources>
Now let’s say we provide ab option to the user to select the language based on the there choice and comfort.
object LanguageList { val languageList: ArrayList<LanguageDTO> get() { val list: MutableList<LanguageDTO> = ArrayList<LanguageDTO>() list.add(LanguageDTO("en", "English")) list.add(LanguageDTO("hi", "हिंदी")) return list.toList() as ArrayList<LanguageDTO> } } @Parcelize data class LanguageDTO(val languageCode: String, val languageTitle: String): Parcelable
Let’s create an LocaleManager
file to update the resource configuration when user select any language and save into memory to support persistence of the this language.
object LocaleManager { fun setNewLocale(c: Context?, language: String) { updateResources(c, language) } private fun updateResources(context: Context?, language: String) { val locale = Locale(language) Locale.setDefault(locale) if (context != null) { val res = context.resources val config = Configuration(res.configuration) config.locale = locale res.updateConfiguration(config, res.displayMetrics) } } }
Now let’s create a SharedPref file to save the selected language code to persist.
object SharedPref { private const val PREFS_NAME = "Language_prefs" private var sharedPreferences: SharedPreferences? = null private fun getInstance(context: Context): SharedPreferences? { if (sharedPreferences == null) sharedPreferences = context.getSharedPreferences( PREFS_NAME, Context.MODE_PRIVATE ) return sharedPreferences } fun getStringParams(aContext: Context, paramName: String?, defaultValue: String?): String? { return getInstance(aContext)?.getString(paramName, defaultValue) } @SuppressLint("ApplySharedPref") fun setStringParams(aContext: Context, paramName: String?, paramValue: String?) { val editor = getInstance(aContext)?.edit() editor?.putString(paramName, paramValue) editor?.commit() } }
object SharedPrefsConstants { const val USER_SELECTED_LANGUAGE_CODE = "selectedLanguageCode" }
Now whenever we load the sting id from the resource, first we need to get the language code from the saved preference update the locale to get the correct string id from the resources. Let’s create the common function to call for those string which required for localise.
object LocalizationUtil { fun getLocalisedString(context: Context, strId: Int): String { val lan = SharedPref.getStringParams( context, SharedPrefsConstants.USER_SELECTED_LANGUAGE_CODE, null ) if (lan != null) LocaleManager.setNewLocale(context, lan) return context.resources.getString(strId) } }
So for here we are done for update the language and save the selected language into the SharedPref. Now we need to create the view to show and select option provide to user to change the language. So let create an fragment xml file.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:padding="16dp"> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/tvTitle" android:layout_marginTop="200dp" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" android:text="@string/title"/> <Button android:id="@+id/selectLanguage" android:layout_width="match_parent" android:layout_marginTop="20dp" android:layout_height="wrap_content" android:text="@string/select_language" app:layout_constraintTop_toBottomOf="@+id/tvTitle"/> </androidx.constraintlayout.widget.ConstraintLayout>
tvTitle.text = LocalizationUtil.getLocalisedString(requireContext(), R.string.title) selectLanguage.text = LocalizationUtil.getLocalisedString(requireContext(), R.string.select_language)
we can load the restring file by calling this function and when user change the language from the option then we need to start the activity.
it?.let { clickLanguage -> SharedPref.setStringParams( requireContext(), SharedPrefsConstants.USER_SELECTED_LANGUAGE_CODE, clickLanguage.languageCode ) requireActivity().finish() requireActivity().startActivity(requireActivity().intent) }
This is all the for localisation change and update the configuration. Let me add the full code here.
class FirstFragment : Fragment(R.layout.first_fragment) { private var binding: FirstFragmentBinding? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FirstFragmentBinding.bind(view) initView() } private fun initView() { binding?.apply { tvTitle.text = LocalizationUtil.getLocalisedString(requireContext(), R.string.title) selectLanguage.text = LocalizationUtil.getLocalisedString(requireContext(), R.string.select_language) selectLanguage.setOnClickListener { val languageCode: String? = SharedPref.getStringParams( requireContext(), SharedPrefsConstants.USER_SELECTED_LANGUAGE_CODE, null ) showBottomSheet(LanguageList.languageList, languageCode) } } } override fun onDestroyView() { super.onDestroyView() binding = null } private fun showBottomSheet(list: ArrayList<LanguageDTO>, selectedItemId: String?) { val bottomSheetFragment = LanguageChangeBottomSheet { it?.let { clickLanguage -> SharedPref.setStringParams( requireContext(), SharedPrefsConstants.USER_SELECTED_LANGUAGE_CODE, clickLanguage.languageCode ) requireActivity().finish() requireActivity().startActivity(requireActivity().intent) } } val args = Bundle() args.putParcelableArrayList(LanguageChangeBottomSheet.LANGUAGE_LIST, list) args.putString(LanguageChangeBottomSheet.SELECTED_LANGUAGE_COED, selectedItemId) bottomSheetFragment.arguments = args bottomSheetFragment.show( requireActivity().supportFragmentManager, bottomSheetFragment.tag ) } }
class LanguageChangeBottomSheet( private val listener: (LanguageDTO?) -> Unit ) : BottomSheetDialogFragment() { private var binding: LanguageChangeSheetBinding? = null private var genericList: ArrayList<LanguageDTO> = arrayListOf<LanguageDTO>() private var adapter: LanguageAdapter? = null private var selectedLanguageCode: String? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { dialog?.setOnShowListener { dialog -> val d: BottomSheetDialog = dialog as BottomSheetDialog val bottomSheetInternal: View = d.findViewById(com.google.android.material.R.id.design_bottom_sheet)!! val bottomBehavior = BottomSheetBehavior.from(bottomSheetInternal) bottomBehavior.state = BottomSheetBehavior.STATE_EXPANDED bottomBehavior.isDraggable = false } return inflater.inflate(R.layout.language_change_sheet, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) genericList = arguments?.getParcelableArrayList<LanguageDTO>(LANGUAGE_LIST) as ArrayList<LanguageDTO> selectedLanguageCode = arguments?.getString(SELECTED_LANGUAGE_COED) ?: "en" binding = LanguageChangeSheetBinding.bind(view) initView() } private fun initView() { binding?.apply { // setup recyclerview recyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) val itemDecoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL) ContextCompat.getDrawable(requireContext(), R.drawable.layer_divider) ?.let { itemDecoration.setDrawable(it) } recyclerView.addItemDecoration(itemDecoration) setRecyclerViewFixedHeight() attachAdapter(genericList) } } private fun LanguageChangeSheetBinding.setRecyclerViewFixedHeight() { val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) val height = displayMetrics.heightPixels * 70 / 100 recyclerView.layoutParams.height = height } private fun attachAdapter(list: ArrayList<LanguageDTO>) { binding?.apply { adapter = LanguageAdapter(list, itemOnClick, selectedLanguageCode) recyclerView.adapter = adapter } } @SuppressLint("NotifyDataSetChanged") val itemOnClick: (LanguageDTO) -> Unit = { languageDTO -> lifecycleScope.launch { delay(100) adapter?.notifyDataSetChanged() delay(100) listener.invoke(languageDTO) dismiss() } } companion object { const val LANGUAGE_LIST = "language_list" const val SELECTED_LANGUAGE_COED = "selected_language_code" } }
class LanguageAdapter( private var addressList: ArrayList<LanguageDTO>, private val itemClickListener: (LanguageDTO) -> Unit, private var selectedItemId: String? ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var languageFilterList = ArrayList<LanguageDTO>() class LanguageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) init { languageFilterList = addressList } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val listView = LayoutInflater.from(parent.context).inflate(R.layout.language_item, parent, false) return LanguageHolder(listView) } override fun getItemCount(): Int { return languageFilterList.size } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val language = languageFilterList[position] holder.itemView.tv_title.text = language.languageTitle if (language.languageCode == selectedItemId) { holder.itemView.radioButton.setImageDrawable( ContextCompat.getDrawable( holder.itemView.context, R.drawable.ic_language_selected ) ) } else { holder.itemView.radioButton.setImageDrawable( ContextCompat.getDrawable( holder.itemView.context, R.drawable.ic_language_unselected ) ) } holder.itemView.setOnClickListener { holder.itemView.radioButton.setImageDrawable( ContextCompat.getDrawable( holder.itemView.context, R.drawable.ic_language_selected ) ) selectedItemId = language.languageCode itemClickListener(language) } } }
I am a very enthusiastic Android developer to build solid Android apps. I have a keen interest in developing for Android and have published apps to the Google Play Store. I always open to learning new technologies. For any help drop us a line anytime at contact@mobologicplus.com