{"id":8747,"date":"2022-12-06T08:01:00","date_gmt":"2022-12-06T08:01:00","guid":{"rendered":"https:\/\/phraseapp.com\/blog\/?p=6277"},"modified":"2023-05-09T12:45:19","modified_gmt":"2023-05-09T10:45:19","slug":"best-practices-for-android-localization-revisited-and-expanded","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/","title":{"rendered":"The Ultimate Guide to Android Localization"},"content":{"rendered":"\n<div id=\"acf\/text-block_912d824631dabb1be46baa6ab4d74a4a\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p class=\"p5\"><span class=\"s1\">In 2021, the <a href=\"https:\/\/www.statista.com\/statistics\/218984\/number-of-global-mobile-users-since-2010\/\">number of mobile users worldwide<\/a> surpassed 7B, and it will likely rise to roughly 7.5B in 2025. <\/span><span class=\"s1\">People across the globe\u00a0spend more time than ever before using mobile apps\u2014with China and India overtaking the US in terms of revenue generated by mobile apps. Non-English usage is even higher for Android-based applications. In fact, the highest app download rates in <\/span><span class=\"s1\">Google Play are in non-English speaking countries.<\/span><\/p>\n<p class=\"p5\"><span class=\"s1\">For most Android app development companies, this figures make it obvious that running an English-only app can hardly meet the expectations of all users across markets. Therefore, making an app adaptable to multiple cultures and languages can play a key role in staying ahead of the competition. This tutorial will walk you through the process of Android internationalization step by step to help you make the most of your app for global users.<\/span><\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_69_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Overview<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-does-android-handle-localization\" title=\"How does Android handle localization?\">How does Android handle localization?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-do-i-add-languages-to-an-android-application\" title=\"How do I add languages to an Android application?\">How do I add languages to an Android application?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-do-i-add-a-new-string-and-its-translations-in-the-translations-editor\" title=\"How do I add a new string and its translations in the Translations Editor?\">How do I add a new string and its translations in the Translations Editor?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-can-i-support-a-users-app-specific-locale-preference-in-android\" title=\"How can I support a user\u2019s app-specific locale preference in Android?\">How can I support a user\u2019s app-specific locale preference in Android?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-do-i-add-a-language-picker-to-an-android-app\" title=\"How do I add a language picker to an Android app?\">How do I add a language picker to an Android app?<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-to-localize-numbers-in-android\" title=\"How to localize numbers in Android?\">How to localize numbers in Android?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-do-i-localize-currency-in-android\" title=\"How do I localize currency in Android?\">How do I localize currency in Android?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-does-localizing-date-and-time-formats-work-in-android\" title=\"How does localizing date and time formats work in Android?\">How does localizing date and time formats work in Android?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-to-handle-plurals-in-translated-strings-in-android\" title=\"How to handle plurals in translated strings in Android?\">How to handle plurals in translated strings in Android?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-do-i-work-with-dynamic-values-in-translated-strings-in-android\" title=\"How do I work with dynamic values in translated strings in Android?\">How do I work with dynamic values in translated strings in Android?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-to-localize-accessibility-for-android-apps\" title=\"How to localize accessibility for Android apps?\">How to localize accessibility for Android apps?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#other-best-practices-for-android-internationalization\" title=\"Other best practices for Android internationalization\">Other best practices for Android internationalization<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#how-do-i-test-my-android-app-with-pseudolocales\" title=\"How do I test my Android app with pseudolocales?\">How do I test my Android app with pseudolocales?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#spotting-localization-issues\" title=\"Spotting localization issues\">Spotting localization issues<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/best-practices-for-android-localization-revisited-and-expanded\/#wrapping-up-our-android-localization-guide\" title=\"Wrapping up our Android localization guide\">Wrapping up our Android localization guide<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"how-does-android-handle-localization\"><\/span>How does Android handle localization?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When it comes to multilingual Android apps, the mobile operating system relies on a combination of language and locale. Language is what gets translated, and locale dictates how regional settings related to the language are displayed: currency, time, dates, etc. Android users can go to <em>Settings \u279e Languages<\/em> and select a combination of language\/locale, which would make all the installed apps adhere to them.<\/p>\n<p><span role=\"image\" aria-label=\"\ud83d\uddd2\">\ud83d\uddd2<\/span> <em><span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\">Note \u00bb <\/span><\/em>A user can have multiple languages\/locales selected and list them in order of priority. If the app doesn\u2019t support the default language\/locale, the next language\/locale in the list will be used instead. If the app doesn\u2019t support any of the languages\/locales in the user\u2019s list, it will display content from project\u2019s default resources.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-add-languages-to-an-android-application\"><\/span>How do I add languages to an Android application?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>To add multiple languages to your Android app, go to <em>Module Name \u279e res \u279e values,<\/em> right-click the <code>strings.xml<\/code> file, and select <em>Open Translations Editor<\/em>. The editor will give you the overview of all the string keys and values used in the application.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You will notice that there is already a <code>strings.xml<\/code> file and a key (<code>app_name<\/code>) added in the translations editor. This is the default <code>strings.xml<\/code> file which will be used if your app doesn\u2019t support user\u2019s preferred Android OS language. We will use English (en) as the default language for this tutorial.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17939 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/translation-editor-setup.png\" alt=\"Translation editor default setup with default file strings.xml and first key created | Phrase \" width=\"389\" height=\"85\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/translation-editor-setup.png 389w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/translation-editor-setup-300x66.png 300w\" sizes=\"(max-width: 389px) 100vw, 389px\" \/><\/p>\n<p>To add a new locale, click on the &#8220;<span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\">Add Locale&#8221; <\/span>button, i.e. the globe icon. You can either choose a language or a combination of a language and locale (for API versions higher than 24), depending on your use case.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17942 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-dropdown-menu.png\" alt=\"Dropdown menu from which to select desired locales for the project | Phrase\" width=\"566\" height=\"348\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-dropdown-menu.png 566w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-dropdown-menu-300x184.png 300w\" sizes=\"(max-width: 566px) 100vw, 566px\" \/><\/p>\n<p>After adding locales, the translations editor will list the newly added locales (Japanese and Spanish in this example).<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17943 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/translation-editor-with-added-locales.png\" alt=\"Translation editor's project overview table with added columns for Spanish and Japanese | Phrase\" width=\"566\" height=\"75\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/translation-editor-with-added-locales.png 566w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/translation-editor-with-added-locales-300x40.png 300w\" sizes=\"(max-width: 566px) 100vw, 566px\" \/><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-add-a-new-string-and-its-translations-in-the-translations-editor\"><\/span>How do I add a new string and its translations in the Translations Editor?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We will now add a new string key \u201cgreeting\u201d and its values in all the locales we&#8217;ve chosen. Click the &#8220;+&#8221; icon, and an <em>Add Key<\/em> dialog box will appear. In the <em>Key<\/em> field, enter \u201cgreeting,\u201d add &#8220;Hello&#8221; to the <em>Default Value<\/em>\u00a0field, and click OK. We will use this key to reference the string when we remove the hardcoded string from the app code.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17944 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/add-key-dialog.png\" alt=\"Dialog window for adding a new key with fields for key name, default value and resource folder | Phrase\" width=\"447\" height=\"179\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/add-key-dialog.png 447w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/add-key-dialog-300x120.png 300w\" sizes=\"(max-width: 447px) 100vw, 447px\" \/><\/p>\n<p>You will now see the newly added key in the translations editor.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-17945 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/te-with-added-key.png\" alt=\"Project overview table with added row for new key | Phrase\" width=\"515\" height=\"69\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/te-with-added-key.png 515w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/te-with-added-key-300x40.png 300w\" sizes=\"(max-width: 515px) 100vw, 515px\" \/><\/p>\n<p>Now we need to add the translations for the key \u201cgreeting\u201d in Japanese and Spanish (our supported languages).<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> The translations editor will mark the key in red if you miss to add the translation for any of the locales. You can mark a key untranslatable\u00a0from the checkbox for any strings that you do not want to translate and only use default values instead.<\/p>\n<p>After adding the keys as well as their values, the translation editor will look like this.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17946 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/te-with-keys-and-values.png\" alt=\"Project overview table with added Japanese and Spanish translations for greetings key | Phrase\" width=\"527\" height=\"98\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/te-with-keys-and-values.png 527w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/te-with-keys-and-values-300x56.png 300w\" sizes=\"(max-width: 527px) 100vw, 527px\" \/><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get the code for the Android demo app used in this tutorial on <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/add_multiple_languages\">GitHub<\/a>.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You can also <a href=\"https:\/\/phrase.com\/blog\/posts\/internationalizing-jetpack-compose-android-apps\/\">add locales and their translated strings manually<\/a> without using the Translations Editor using Strings.xml files.<\/p>\n<p>We have added string keys and values for multiple locales, now we need to reference those keys.<\/p>\n<p>Currently, our Android application is using hardcoded strings in a Text composable located in\u00a0 <code>MainActivity.kt<\/code>. The text value is set to \u201cHello\u201d.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"7b457d08-ebe7-4f58-86c0-978ef505c84a\" data-enlighter-title=\"MainActivity.kt\">Column() {\r\n    Text(text = \"Hello\", fontSize = 60.sp)\r\n   }<\/pre>\n<p>When we run the app on an Android Phone, whose default language is set to English, Japanese or Spanish, we can see that the app always displays the same piece of text.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17947 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-475x1024.png\" alt=\"Demo phone screen displaying the word Hello | Phrase\" width=\"475\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-951x2048.png 951w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello.png 1170w\" sizes=\"(max-width: 475px) 100vw, 475px\" \/><\/p>\n<p>We need to replace the hardcoded text with the string keys we had defined for our Android app to be able to display content in the user\u2019s preferred language. To do that, go to <code>MainActivity.kt<\/code> and replace the hardcoded string with the following:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"0109003a-de2c-4acc-97d9-96a4470a3f81\" data-enlighter-title=\"MainActivity.kt\">Text(\r\n \/\/ getString is a function that will return the\r\n \/\/ translated String using the \"greeting\" key\r\n  text = getString(R.string.greeting), fontSize = 60.sp\r\n)<\/pre>\n<p>If we run the <a class=\"notion-link-token notion-enable-hover\" href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/remove_hard_coded_Strings\" target=\"_blank\" rel=\"noopener noreferrer\" data-token-index=\"1\" data-reactroot=\"\"><span class=\"link-annotation-unknown-block-id-2071612201\">application<\/span><\/a> again, we will see that it is able to display translated content, based on the user\u2019s preferred language (from left to right in English, Japanese, and Spanish).<\/p>\n<table style=\"width: 100%; border-style: hidden;\">\n<tbody>\n<tr>\n<td style=\"width: 33.3333%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\" wp-image-17947 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello.png\" alt=\"Demo phone screen displaying the word Hello | Phrase\" width=\"249\" height=\"536\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-951x2048.png 951w\" sizes=\"(max-width: 249px) 100vw, 249px\" \/><\/p>\n<p style=\"text-align: center;\">English<\/p>\n<\/td>\n<td style=\"width: 33.3332%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17949 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja.png\" alt=\"Demo phone screen with language selection option in Japanese | Phrase\" width=\"1170\" height=\"2520\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-ja-951x2048.png 951w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p style=\"text-align: center;\">Japanese<\/p>\n<\/td>\n<td style=\"width: 33.3334%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17950 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es.png\" alt=\"Demo phone screen with greeting in Spanish | Phrase\" width=\"1170\" height=\"2520\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/phone-screen-hello-es-951x2048.png 951w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p style=\"text-align: center;\">Spanish<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2><span class=\"ez-toc-section\" id=\"how-can-i-support-a-users-app-specific-locale-preference-in-android\"><\/span>How can I support a user\u2019s app-specific locale preference in Android?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In the past, Android used to apply the user\u2019s preferred language to all apps installed on a smartphone, but as of Android 13, users can set their preferred language for each app. To take advantage of these new APIs, we need to list all supported languages for our application: Create a file called <code>res\/xml\/locales_config.xml<\/code>\u00a0and specify your app\u2019s languages as follows:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-group=\"3ff24063-ab7c-4072-9db1-dd1fb2b60ad7\" data-enlighter-title=\"res\/xml\/locales_config.xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;locale-config xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"&gt;\r\n    \/\/add all the supported lanaguges here\r\n    &lt;locale android:name=\"ja\"\/&gt;\r\n    &lt;locale android:name=\"es\"\/&gt;\r\n    &lt;locale android:name=\"en\"\/&gt;\r\n&lt;\/locale-config&gt;<\/pre>\n<p>In the manifest, add a line pointing to this new file:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-group=\"c31a1b54-bb67-4274-a5d6-acd2f791a657\" data-enlighter-title=\"res\/xml\/locales_config.xml\">&lt;manifest\r\n    &lt;application\r\n    ...\r\n    android:localeConfig=\"@xml\/locales_config\"&gt;\r\n    &lt;\/application&gt;\r\n&lt;\/manifest&gt;<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You might face build issues while linking this file on Android Gradle plugin (AGP) version 7.3.0-alpha07 through 7.3.0-beta02. Use AGP 7.3.0-beta04 or later, AGP 7.4.0-alpha05 or later, or a prior AGP release.<\/p>\n<p>Users can now chose the language of our Android application in 2 ways:<\/p>\n<ul>\n<li>System Settings: Go to <em>Settings \u279e System \u279e Languages &amp; Input \u279e App Languages \u279e (select app).<\/em> Here you will see the all languages our Android application supports. By default, it will pick up the system\u2019s default language.<\/li>\n<\/ul>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17951 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/app-languages-setting.png\" alt=\"Setting language preferences in the system settings | Phrase\" width=\"426\" height=\"927\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/app-languages-setting.png 426w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/app-languages-setting-138x300.png 138w\" sizes=\"(max-width: 426px) 100vw, 426px\" \/><\/p>\n<ul>\n<li>In the App Language Picker: Newer APIs make it possible for a user to select their preferred language from within the application. We\u2019ll cover that next.<\/li>\n<\/ul>\n<h3><span class=\"ez-toc-section\" id=\"how-do-i-add-a-language-picker-to-an-android-app\"><\/span>How do I add a language picker to an Android app?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let&#8217;s implement now an in-app language picker for our Android application. In the app, we have a dropdown menu where users can select a locale from those supported by our app.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17952 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/in-app-language-picker-473x1024.gif\" alt=\"Demo screen showing in-app language picker while displaying default English greeting | Phrase\" width=\"473\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/in-app-language-picker-473x1024.gif 473w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/in-app-language-picker-138x300.gif 138w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/in-app-language-picker-768x1664.gif 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/in-app-language-picker-709x1536.gif 709w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/in-app-language-picker-945x2048.gif 945w\" sizes=\"(max-width: 473px) 100vw, 473px\" \/><\/p>\n<p>In the \u00a0<code><span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\">MainActivity.kt<\/span><\/code> file, add the following code to the<span class=\"notion-enable-hover\" data-token-index=\"4\" data-reactroot=\"\">\u00a0<code>OnCreate()<\/code><\/span> method:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"1e0e6fa8-8d5c-47db-82a7-eb8767a7406e\" data-enlighter-title=\"MainActivity.kt\">import android.app.LocaleManager\r\nimport android.content.Context\r\n\r\nclass MainActivity: ComponentActivity() {\r\n    override fun onCreate(savedInstanceState: Bundle ? ) {\r\n        super.onCreate(savedInstanceState)\r\n\r\n        val localeManager = getSystemService(Context.LOCALE_SERVICE) as LocaleManager\r\n        val currentLocale = localeManager.applicationLocales.toLanguageTags()\r\n        \/\/ ...\r\n    }\r\n}<\/pre>\n<p><code>LocaleManager<\/code> helps us get the current locale or set another one. The changes to the locale made using <code>LocaleManager<\/code> persist even after closing and restarting the app. We use <code>LocaleManager<\/code> to get the <code>applicationLocales<\/code>, which is the current locale of our app. We then convert it to a language tag (\u201cen\u201d, \u201cja,\u201d etc.), which is a string.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> <code>localeManager.applicationLocales<\/code> will return empty if there is no language set for our app. In this case, our app will use the system&#8217;s default language.<\/p>\n<p>Now we need to display a dropdown menu for the user to be able to select a locale. In the composable inside <code>MainActivity.kt<\/code>, add the following code to the <code>setContent<\/code> method:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"b0e2d935-4196-4a9f-91e4-e9f503103f45\" data-enlighter-title=\"MainActivity.kt\">class MainActivity : ComponentActivity() {\r\n  @RequiresApi(Build.VERSION_CODES.TIRAMISU)\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n\r\n    val localeManager = getSystemService(Context.LOCALE_SERVICE) as LocaleManager\r\n    val currentLocale = localeManager.applicationLocales.toLanguageTags()\r\n\r\n    setContent {\r\n      PhraseTheme {\r\n        Surface() {\r\n\r\n                    \/\/ This list contains the supported locales displayed\r\n          \/\/ in dropdown menu.\r\n          val supportedLocales = listOf(\"en\", \"es\", \"ja\")\r\n          var expanded by remember { mutableStateOf(false) }\r\n\r\n                    \/\/ This represents the current locale of our app.\r\n          var selectedLocale by remember {\r\n            mutableStateOf(currentLocale.ifEmpty { \"Not Set\" })\r\n          }\r\n\r\n          Column() {\r\n\r\n                        \/\/ Now add the DropDown Menu Composable.\r\n            ExposedDropdownMenuBox(\r\n              expanded = expanded,\r\n              onExpandedChange = {\r\n                expanded = !expanded\r\n              }\r\n            ) {\r\n              TextField(\r\n                readOnly = true,\r\n                value = selectedLocale,\r\n                onValueChange = { },\r\n                trailingIcon = {\r\n                  ExposedDropdownMenuDefaults.TrailingIcon(\r\n                    expanded = expanded\r\n                  )\r\n                },\r\n                colors = ExposedDropdownMenuDefaults.textFieldColors()\r\n              )\r\n              ExposedDropdownMenu(\r\n                expanded = expanded,\r\n                onDismissRequest = {\r\n                  expanded = false\r\n                }\r\n              ) {\r\n                supportedLocales.forEach { selectionOption -&gt;\r\n\r\n                  \/\/ onClick will be triggered when a locale is selected \r\n                                    \/\/ from the dropdown menu\r\n                  DropdownMenuItem(onClick = {\r\n                    selectedLocale = selectionOption\r\n\r\n                                        \/\/ Set the selected locale using localeManager.\r\n                    localeManager.applicationLocales =\r\n                      LocaleList(Locale.forLanguageTag(selectedLocale))\r\n                    expanded = false\r\n                  }\r\n                  ) {\r\n                    Text(text = selectionOption)\r\n                  }\r\n                }\r\n              }\r\n            }\r\n                    \r\n          \t\/\/ This text Composable is refreshed whenever the app\r\n            \/\/ locale is changed.\r\n            Text(\r\n              text = getString(R.string.greeting),\r\n              fontSize = 60.sp,\r\n              modifier = Modifier.padding(top = 30.dp)\r\n            )\r\n\r\n                        \/\/ We also need a way to reset the selected locale ie. \r\n                        \/\/ to make our application use default System language instead. \r\n                        \/\/ For this, we add a Button Composable -\r\n            Button(onClick = {\r\n              localeManager.applicationLocales = LocaleList.getEmptyLocaleList()\r\n            }, modifier = Modifier.padding(top = 200.dp)) {\r\n              Text(text = \"Reset\")\r\n            }\r\n          }\r\n        }\r\n      }\r\n    }\r\n  }\r\n}<\/pre>\n<p>We are done, feel free to check out the project on <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/set_languages\">GitHub<\/a>.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> To support newer APIs on <a href=\"https:\/\/developer.android.com\/about\/versions\/13\/features\/app-languages#android12-impl\">Android 12 or lower<\/a>, use the <a href=\"https:\/\/developer.android.com\/guide\/topics\/resources\/app-languages#androidx-impl\">AndroidX support library<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-to-localize-numbers-in-android\"><\/span>How to localize numbers in Android?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Numbers are formatted differently depending on the locale. For example, &#8220;1000000&#8221; will be display as<\/p>\n<ul>\n<li>1.000.000 in Brazil (pt-BR)<\/li>\n<li>1,000,000 in the US (en-US)<\/li>\n<li>10,00,000 in India (en-IN)<\/li>\n<\/ul>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> <a href=\"https:\/\/phrase.com\/blog\/posts\/number-localization\/\">A Concise Guide to Number Localization<\/a> covers numeral systems, separators, percentages and more.<\/p>\n<p>At the moment, we have an <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/internationalize_number_begin\">application<\/a> that only shows an Integer in a Text composable by directly converting it into a string in the <code>MainActivity.kt<\/code> file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"b3530f01-7c6b-4e31-aad3-8d5ba89da2d8\" data-enlighter-title=\"MainActivity.kt\">class MainActivity : ComponentActivity() {\r\n  @RequiresApi(Build.VERSION_CODES.TIRAMISU)\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n        \/\/ ...\r\n    val number = 1000000\r\n\r\n    setContent {\r\n                        \/\/ ...\r\n            Text(\r\n              text = number.toString(),\r\n              fontSize = 60.sp,\r\n            )\r\n        }\r\n      }\r\n    }<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17953 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-switcher-numbers-473x1024.gif\" alt=\"Demo app with locale switcher displaying unformatted number | Phrase\" width=\"473\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-switcher-numbers-473x1024.gif 473w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-switcher-numbers-138x300.gif 138w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-switcher-numbers-768x1664.gif 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-switcher-numbers-709x1536.gif 709w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/locale-switcher-numbers-945x2048.gif 945w\" sizes=\"(max-width: 473px) 100vw, 473px\" \/><\/p>\n<p>As we can see above, without internationalization in place, the number looks the same in all locales, and it doesn&#8217;t have any formatting. To change that, we will format the number based on the locale using the <code>NumberFormat<\/code> class. Add the following code to the <code>MainActivity.kt<\/code> file:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"e55728e0-43ec-4792-b879-7f39a4a5d04d\" data-enlighter-title=\"MainActivity.kt\">import java.text.NumberFormat\r\n\r\nclass MainActivity : ComponentActivity() {\r\n  @RequiresApi(Build.VERSION_CODES.TIRAMISU)\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n        \/\/ ...\r\n    val number = 1000000\r\n        val formattedNumber = NumberFormat.getInstance().format(number)\r\n\r\n    setContent {\r\n                        \/\/ ...\r\n            Text(\r\n              text = formattedNumber,\r\n              fontSize = 60.sp,\r\n            )\r\n        }\r\n      }\r\n    }<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17954 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/numbers-formatted-473x1024.gif\" alt=\"Demo app showing differently formatted numbers depending on locale selected | Phrase\" width=\"473\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/numbers-formatted-473x1024.gif 473w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/numbers-formatted-138x300.gif 138w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/numbers-formatted-768x1664.gif 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/numbers-formatted-709x1536.gif 709w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/numbers-formatted-945x2048.gif 945w\" sizes=\"(max-width: 473px) 100vw, 473px\" \/><\/p>\n<p>The number is now formatted according to the locale. Check out the final code on <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/internationalize_number_end\">GitHub<\/a>.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> When using TalkBack, Google&#8217;s screen reader included on Android devices, numbers are always read based on the user\u2019s locale. Find out more about it in our <a href=\"https:\/\/phrase.com\/blog\/posts\/android-accessibility\/\">Android Accessibility Tutorial for Multilingual Apps<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-currency-in-android\"><\/span>How do I localize currency in Android?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Whenever we want to display an amount of money, we use a currency symbol ($, \u20ac, etc.) or an <a href=\"https:\/\/www.iban.com\/currency-codes\">ISO 4217 currency code<\/a> (USD, EUR, etc.) next to the formatted amount. When it comes to Android, the <code>NumberFormat<\/code> class provides a method to format a sum depending on the user\u2019s locale. We will keep using the previous example to format an amount of 1000000 units in the respective currency. Add the following code to the <code>MainActivity.kt<\/code> file:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"2e118bf0-1e4a-4220-b019-bf8e0b3629f4\" data-enlighter-title=\"MainActivity.kt\">import java.text.NumberFormat\r\nimport java.util.*\r\n\r\nclass MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n    \/\/ ...\r\n    \/\/Add this\r\n    val amount = 1000000\r\n    val formattedCurrency = NumberFormat.getCurrencyInstance().format(amount)\r\n\r\n    setContent {\r\n       \/\/ ...\r\n       Text(\r\n           text = formattedCurrency,\r\n           fontSize = 60.sp,\r\n           )\r\n  \r\n          }\r\n        }\r\n    }<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17955 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/formatted-with-currency-473x1024.gif\" alt=\"Demo app showing numbers correctly formatted as monetary amounts inlcuding currency symbol | Phrase\" width=\"473\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/formatted-with-currency-473x1024.gif 473w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/formatted-with-currency-138x300.gif 138w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/formatted-with-currency-768x1664.gif 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/formatted-with-currency-709x1536.gif 709w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/formatted-with-currency-945x2048.gif 945w\" sizes=\"(max-width: 473px) 100vw, 473px\" \/><\/p>\n<p>The amount is now formatted based on the locale.<\/p>\n<p>By default, <code>.getCurrencyInstance()<\/code> will take in the current locale of your app. You can also pass any other locale tags in the function, for example, <code>getCurrencyInstance(Locale(LANGUAGE, COUNTRY))<\/code> to format an amount in any locale.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> To get a currency code in the ISO 4217 format, you can use the <code>NumberFormat.getCurrencyInstance().currency.currencyCode<\/code> method.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-does-localizing-date-and-time-formats-work-in-android\"><\/span>How does localizing date and time formats work in Android?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>To give an overview of localizing date and time formats in Android, we will convert the <a href=\"https:\/\/phrase.com\/blog\/posts\/localized-date-time-android\/\">standard time<\/a> format into a human-readable format, taking the user\u2019s time zone and locale into consideration. We will use the <a href=\"https:\/\/www.iso.org\/iso-8601-date-and-time-format.html\">ISO 8601<\/a> format, which is a string used to represent the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Coordinated_Universal_Time\">Coordinated Universal Time (UTC)<\/a>. For example, the UTC format of January 31, 2022, 17:55:50, is represented as 2022-01-31T17:55:50Z in the ISO 8601 format. The Z at the end represents a zero UTC offset.<\/p>\n<p>We will use <code>java.time<\/code> APIs to internationalize and localize the time format.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> To use <code>java-time<\/code> APIs for Android versions lower than 26, you will need to add a\u00a0backwards compatibility.<\/p>\n<p>First, we will convert the UTC timestamp into the user\u2019s time zone and display it (without localization). Go to the <code>MainActivity.kt<\/code>\u00a0and add the following code:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"96003f4e-d3bd-42e7-b655-fb101205476a\" data-enlighter-title=\"MainActivity.kt\">import java.time.Instant\r\nimport java.time.ZoneId\r\nimport java.time.ZonedDateTime\r\n\r\nclass MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n\r\n  val UtcTimeStamp = \"2022-01-31T16:27:23Z\"\r\n  val timestampInstant = Instant.parse(UtcTimeStamp)\r\n  val zonedDateTime = ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())\r\n    \/\/ ...\r\n        }\r\n}<\/pre>\n<p><code>Instant.parse(UtcTimeStamp)<\/code> takes in the UTC string as a parameter and returns a Java time <a href=\"https:\/\/docs.oracle.com\/javase\/8\/docs\/api\/java\/time\/Instant.html\">Instant<\/a> object. The Java time <code>Instant<\/code> class represents time passed in seconds since the origin (epoch) <code>1970-01-01T00:00:00Z<\/code> .<\/p>\n<p>With the help of the <code>ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())<\/code> method, we convert <code>timestampInstant<\/code> from UTC time into the user\u2019s time zone.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Both <code>ZonedDateTime<\/code> and <code>Instant<\/code> represent a moment in time. The only difference is that <code>ZonedDateTime<\/code> has extra helper methods related to time zones and time offsets while Instant has no timezone or offset specified.<\/p>\n<p>Inside the Text composable in the <code>MainActivity.kt<\/code> file, we convert <code>zonedDateTime<\/code> into a string.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"30d119a8-5e41-4efd-a2d1-11e914fe0f79\" data-enlighter-title=\"MainActivity.kt\">import java.time.Instant\r\nimport java.time.ZoneId\r\nimport java.time.ZonedDateTime\r\n\r\nclass MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n\r\n    val UtcTimeStamp = \"2022-01-31T16:27:23Z\"\r\n    val timestampInstant = Instant.parse(UtcTimeStamp)\r\n    val zonedDateTime = ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())\r\n\r\n    setContent {\r\n                      \/\/ ...\r\n            Text(\r\n              text = zonedDateTime.toString(),\r\n              fontSize = 20.sp,\r\n            )\r\n                }\r\n      }\r\n}<\/pre>\n<p>Let\u2019s run the application with users in different time zones. Here is what users in India (on the left) and the UK (on the right) get displayed on their screens:<\/p>\n<table style=\"border-collapse: collapse; width: 100%;\">\n<tbody>\n<tr>\n<td style=\"width: 50%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-17956 size-full aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india.png\" alt=\"Demo app for users in India with Indian time stamp in UTC format | Phrase\" width=\"1170\" height=\"2520\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-india-951x2048.png 951w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p style=\"text-align: center;\">India<\/p>\n<\/td>\n<td style=\"width: 50%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-17957 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk.png\" alt=\"Demo app for UK users with time stamp of UK time in UTC format | Phrase\" width=\"1170\" height=\"2520\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timezone-demo-uk-951x2048.png 951w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p style=\"text-align: center;\">United Kingdom<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>As you can see, the UTC timestamp is converted to the user\u2019s default time zone (in the ISO 8601 format). We can now localize this timestamp taking the user\u2019s locale settings into account with the help of the <code><span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\">DateTimeFormatter<\/span><\/code> class. In<span class=\"notion-enable-hover\" data-token-index=\"3\" data-reactroot=\"\"> the <code>MainActivity.kt<\/code><\/span> file, make the following changes:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"a43aaed4-72af-40cd-99d8-d9fc41151f68\" data-enlighter-title=\"MainActivity.kt\">import java.time.Instant\r\nimport java.time.ZoneId\r\nimport java.time.ZonedDateTime\r\nimport java.time.format.DateTimeFormatter\r\nimport java.time.format.FormatStyle\r\n\r\nclass MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n\r\n    val UtcTimeStamp = \"2022-01-31T16:27:23Z\"\r\n    val timestampInstant = Instant.parse(UtcTimeStamp)\r\n    val zonedDateTime = ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())\r\n    val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)\r\n    val localizedDate = zonedDateTime.format(dateFormatter)\r\n\r\n    setContent {\r\n                        \/\/ ...\r\n            Text(\r\n              text = localizedDate.toString(),\r\n              fontSize = 20.sp,\r\n              modifier = Modifier.padding(top = 30.dp)\r\n            )\r\n           }\r\n      }\r\n}<\/pre>\n<p>Let\u2019s run the app again. In the <code>hi-IN<\/code> locale, the date is displayed in the following way:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17958 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india-475x1024.png\" alt=\"Demo app with timestamp formatted after local Indian conventions | Phrase\" width=\"475\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india-951x2048.png 951w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-india.png 1170w\" sizes=\"(max-width: 475px) 100vw, 475px\" \/><\/p>\n<p>Users whose locale is set to <code>en-UK<\/code>, will see the same date as follows:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17959 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk-475x1024.png\" alt=\"Demo app with UK time format localized for a UK user | Phrase\" width=\"475\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk-951x2048.png 951w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timestamp-uk.png 1170w\" sizes=\"(max-width: 475px) 100vw, 475px\" \/><\/p>\n<p>You can get the final version of the app on <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/i18n_date_time\">GitHub<\/a>. In the example above, we have added absolute date and time, but you can add <a href=\"https:\/\/phrase.com\/blog\/posts\/localized-date-time-android\/\">relative date and time formats<\/a> (today, yesterday, etc.) as well.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-to-handle-plurals-in-translated-strings-in-android\"><\/span>How to handle plurals in translated strings in Android?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Different languages have different grammatical rules for expressing quantity. In English, for example, you write \u201c1 minute\u201d, but for any other quantity, including 0, you write \u201c<em>n<\/em> minutes\u201d. Other languages like Arabic make finer distinctions.<\/p>\n<p>At the moment, our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/plurals\">application<\/a> lets users type in a number of minutes for a timer.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17960 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-unformatted-473x1024.gif\" alt=\"Timer demo app with field for typing in the number of minutes | Phrase\" width=\"473\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-unformatted-473x1024.gif 473w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-unformatted-138x300.gif 138w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-unformatted-768x1664.gif 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-unformatted-709x1536.gif 709w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-unformatted-945x2048.gif 945w\" sizes=\"(max-width: 473px) 100vw, 473px\" \/><\/p>\n<p>As you can see, the \u201cminute\u201d label does\u2019t change even when you type in \u201c0\u201d in the text field. The same can be observed from the code in<span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>MainActivity.kt<\/code><\/span>:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"aaad061b-ba0b-42ab-92fb-67e93dbeb9d5\" data-enlighter-title=\"MainActivity.kt\">class MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n    setContent {\r\n\r\n                    \/\/ Current value of count entered by user in the TextField\r\n          var count by remember { mutableStateOf(\"\") }\r\n\r\n          Row(){\r\n            \/\/ ...\r\n            TextField(\r\n              modifier = Modifier.width(80.dp),\r\n              value = count,\r\n                        \r\n               \/\/ We set the value of the count to the value entered by user.\r\n              onValueChange = { count = it },\r\n              keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),\r\n            )\r\n            Text(\r\n              text = getString(R.string.label_minutes), fontSize = 20.sp, modifier = Modifier\r\n                .padding(start = 10.dp)\r\n            )\r\n          }\r\n        }\r\n      }\r\n    }<\/pre>\n<p>And the associated key in <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>strings.xml<\/code><\/span>\u00a0file in <span class=\"notion-enable-hover\" data-token-index=\"3\" data-reactroot=\"\"><em>res<\/em> \u279e <em>values.<\/em><\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-group=\"7e14f029-4554-4f5c-8cfe-1c5d24460b39\" data-enlighter-title=\"strings.xml\">&lt;string name=\"label_minutes\"&gt;minute&lt;\/string&gt;<\/pre>\n<p>To make sure that the minute labels respond to the quantity, we need to define all possible labels within the <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>&lt;plurals&gt;<\/code><\/span> tag in the<span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"> <code>strings.xml<\/code><\/span> file for the default language of our app, i.e. English.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"bcf4408f-34c7-4d4c-a528-421f0616c26c\" data-enlighter-title=\"strings.xml\">\/\/ Remove this\r\n&lt;!--    &lt;string name=\"label_minutes\" translatable=\"false\"&gt;minute&lt;\/string&gt;--&gt;\r\n\r\n\/\/ Add this\r\n  &lt;plurals name=\"label_minutes\"&gt;\r\n     &lt;item quantity=\"one\"&gt;minute&lt;\/item&gt;\r\n     &lt;item quantity=\"other\"&gt;minutes&lt;\/item&gt;\r\n &lt;\/plurals&gt;<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Besides <code>one<\/code> and <code>other<\/code>, Android also supports more attributes, including <code>zero<\/code>, <code>two<\/code>, <code>few<\/code> ,<code>many<\/code><em>.<\/em> This allows for complex plurals, like those in Arabic, for example.<\/p>\n<p>Now, in the <code>Mainactivity.kt<\/code> file, do the following:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"ae35d147-6956-43ff-a23d-26cfb7b9e911\" data-enlighter-title=\"Mainactivity.kt\">import androidx.compose.ui.platform.LocalContext\r\n\r\nclass MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n    setContent {\r\n                    \/\/ Current value of count entered by user in the TextField\r\n          var count by remember { mutableStateOf(\"\") }\r\n                    val resources = LocalContext.current.resources\r\n\r\n          Row(){\r\n            \/\/ ...\r\n            TextField(\r\n              modifier = Modifier.width(80.dp),\r\n              value = count,\r\n                            \/\/ If it (value entered by user) is empty, we set it to 0\r\n              onValueChange = { count = it.ifEmpty { \"0\" } },\r\n              keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),\r\n            )\r\n            Text(\r\n                            \/\/ Pass the count\/quantity to the getQuantityString() method\r\n                text =  resources.getQuantityString(R.plurals.label_minutes, count.toInt()),\r\n            )\r\n          }\r\n        }\r\n      }\r\n    }<\/pre>\n<p>On running the <a class=\"notion-link-token notion-enable-hover\" href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/plurals_end\" target=\"_blank\" rel=\"noopener noreferrer\" data-token-index=\"1\" data-reactroot=\"\"><span class=\"link-annotation-unknown-block-id--1925335629\">final application<\/span><\/a>, we can see that the correct string is loaded, respecting the entered quantity.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17961 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-formatted-473x1024.gif\" alt=\"Timer demo app with dynamic adjustment of the word minute, depending on the quantity of minutes specified by the user | Phrase\" width=\"473\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-formatted-473x1024.gif 473w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-formatted-138x300.gif 138w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-formatted-768x1664.gif 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-formatted-709x1536.gif 709w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/timer-formatted-945x2048.gif 945w\" sizes=\"(max-width: 473px) 100vw, 473px\" \/><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-dynamic-values-in-translated-strings-in-android\"><\/span>How do I work with dynamic values in translated strings in Android?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Strings may sometime contain text that should not be translated into other languages. This could be a brand name, URL address, Unicode emoji character, an address, etc. You can use the <code>&lt;xliff:g&gt;<\/code> placeholder tag in the <code>strings.xml<\/code> file to mark text that should not be translated.<\/p>\n<p>For example, our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/interpolation\">application<\/a> displays the following text:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17962 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-475x1024.png\" alt=\"Demo app saying &quot;Visit our website xy.com to learn more&quot; | Phrase\" width=\"475\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-951x2048.png 951w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text.png 1170w\" sizes=\"(max-width: 475px) 100vw, 475px\" \/><\/p>\n<p><span class=\"notion-enable-hover\" data-token-index=\"3\" data-reactroot=\"\">Inside the <code>Mainactivity.kt<\/code> file, we have a Text composable<em>.<\/em><\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"507fa9dd-8f3d-483e-a67a-80dcf7596be7\" data-enlighter-title=\"Mainactivity.kt\">class MainActivity : ComponentActivity() {\r\n  override fun onCreate(savedInstanceState: Bundle?) {\r\n    super.onCreate(savedInstanceState)\r\n\r\n    setContent {\r\n\r\n                 \/\/ ...\r\n         Text(\r\n           text =  getString(R.string.landing_url),\r\n           fontSize = 20.sp,\r\n           modifier = Modifier.padding(start = 10.dp)\r\n            )\r\n         }\r\n       }\r\n     }<\/pre>\n<p>And the associated key in <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>strings.xml<\/code><\/span>\u00a0file in <span class=\"notion-enable-hover\" data-token-index=\"3\" data-reactroot=\"\"><em>res<\/em> \u279e <em>values.<\/em><\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-group=\"bbac3dc0-2a07-4a55-ac3f-179addc9800b\" data-enlighter-title=\"strings.xml\">&lt;string name=\"landing_url\"&gt;Visit our website https:\/\/xyz.com to know more.&lt;\/string&gt;<\/pre>\n<p>Here, you can notice that the website URL, <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>https:\/\/xyz.com<\/code><\/span>, must remain same in all languages. However, the relative position of the URL might differ depending on the language. To mark the text that should not be translated, we use the <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>&lt;xliff:g&gt;<\/code><\/span>\u00a0placeholder tag. Go to <span class=\"notion-enable-hover\" data-token-index=\"5\" data-reactroot=\"\"><em>Module Name \u279e res \u279e values \u279e strings <\/em><\/span>and make the following changes to the\u00a0<span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>xml<\/code><\/span> files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\">\/\/For default strings.xml file \r\n&lt;string  name=\"landing_url\"&gt; Visit us at &lt;xliff:g  id=\"application_homepage\"&gt;https:\/\/xyz.com\/&lt;\/xliff:g&gt; to know more.&lt;\/string&gt;\r\n\r\n\/\/For the German strings.xml file, similarly add this string\r\n&lt;string  name=\"landing_url\"&gt;Erfahren Sie mehr unter &lt;xliff:g  id=\"application_homepage\"&gt;https:\/\/xyz.com\/&lt;\/xliff:g&gt;&lt;\/string&gt;<\/pre>\n<p>Always add an <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>id<\/code> <\/span>attribute to the placeholder tag which explains what the placeholder is for. This is for your translators to understand the context of the untranslatable string. Let\u2019s run the <a class=\"notion-link-token notion-enable-hover\" href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/interpolation_end\" target=\"_blank\" rel=\"noopener noreferrer\" data-token-index=\"3\" data-reactroot=\"\"><span class=\"link-annotation-unknown-block-id-1622709608\">final version<\/span><\/a> of the app in the English and German locales to see the difference.<\/p>\n<table style=\"border-collapse: collapse; width: 100%;\">\n<tbody>\n<tr>\n<td style=\"width: 50%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-17962\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text.png\" alt=\"Demo app saying &quot;Visit our website xy.com to learn more&quot; | Phrase\" width=\"1170\" height=\"2520\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-951x2048.png 951w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p style=\"text-align: center;\">English<\/p>\n<\/td>\n<td style=\"width: 50%; border-style: hidden;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-17963\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de.png\" alt=\"Demo app saying &quot;Visit our website xy.com to learn more&quot; in German | Phrase\" width=\"1170\" height=\"2520\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-text-de-951x2048.png 951w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p style=\"text-align: center;\">German<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Note the relative position of the URL is different in both languages.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You can also use <a href=\"https:\/\/phrase.com\/blog\/posts\/internationalizing-jetpack-compose-android-apps\/\">dynamic strings<\/a> with <code>&lt;xliff:g&gt;<\/code> tags which will be displayed in run time.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-to-localize-accessibility-for-android-apps\"><\/span>How to localize accessibility for Android apps?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As briefly noted above, Android is using Talkback, Google&#8217;s reader included on all Android devices, to cater to people with disability. Talkback helps users navigate with gestures and describes the content displayed on the screen. It can also take advantage of all the strategies for i18n and l10n we have already discussed without any extra work. However, there are some APIs that are exclusively meant for accessibility.<\/p>\n<p>When you define an Image or Icon composable, Talkback needs a textual description of the visual element. This can be achieved by adding a <code>contentDesription<\/code> parameter. For example, for an <code>IconButton<\/code> composable, <code>contentDescription<\/code> can be set by passing a string to the composable.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\">IconButton(onClick = onClick) {\r\n    Icon(imageVector = Icons.Filled.Share, contentDescription = \"share\")\r\n}<\/pre>\n<p>When the user now hovers over the share icon, Talkback says \u201cshare\u201d to the user. However, because we used a hard coded string, Talkback will not respect the user\u2019s locale settings. To fix this, do the following:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\">IconButton(onClick = onClick) {\r\n    Icon(imageVector = Icons.Filled.Share,\r\n    \/\/define \"label_share\" key in all the supported string.xml files\r\n    contentDescription = stringResource(R.string.label_share)\r\n  }<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You can also set <code>contentDescription<\/code> to <code>null<\/code> for decorative images.<\/p>\n<p>Now, Talkback will narrate the label in the user\u2019s language. Check out our <a href=\"https:\/\/phrase.com\/blog\/posts\/android-accessibility\/\">Android Accessibility Tutorial for Multilingual Apps<\/a> to learn more.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"other-best-practices-for-android-internationalization\"><\/span>Other best practices for Android internationalization<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let&#8217;s review briefly some other Android i18n best practices so you don&#8217;t miss anything:<\/p>\n<ul>\n<li>Provide a sufficient description for string resources declared in the <code>strings.xml<\/code> file for the translator to understand the context in which a string is used. Instead of just declaring a string key in <code>strings.xml<\/code> file, provide comments explaining the context.\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\" data-enlighter-group=\"6e506263-791e-4df9-a886-975b488e47e5\" data-enlighter-title=\"strings.xml\">&lt;!-- The action for signing up after filling a form. This text is displayed on a button. --&gt;\r\n&lt;string  name=\"sign_up_button\"&gt;Sign Up&lt;\/string&gt;<\/pre>\n<\/li>\n<li>Design a flexible layout. It can be challenging to guess the exact width or height that a piece of text in any language will require on screens. For example, the English word \u201cskip\u201d takes only 4 characters but its German equivalent, \u201c\u00fcberspringen,\u201d takes up 12 characters. Avoiding making widths and heights fixed\u2014unless really important. Instead, make Composables wrap content or make them scrollable, depending on your use case. Check out a few examples of how to make <a href=\"https:\/\/phrase.com\/blog\/posts\/internationalizing-jetpack-compose-android-apps\/\">localization-friendly layouts using Jetpack Compose<\/a>.<\/li>\n<li>Just as we defined different <code>string.xml<\/code> files for all locales, you can also add different images for all locales. Android will switch to default images in case it cannot find images for the user\u2019s locale.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-test-my-android-app-with-pseudolocales\"><\/span>How do I test my Android app with pseudolocales?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Pseudolocalization is a method to simulate internationalization of text that might lead to UI, layout, and other translation-related issues while maintaining readability. Pseudolocalization helps you test localization without actually needing to translate your Android application. To use Android pseudolocales, you need to be running an API level 18 or higher.<\/p>\n<p>Let us run an <a href=\"https:\/\/github.com\/PhraseApp-Blog\/android-i18n-l10n\/tree\/pseudo\">application<\/a> whose default language is set to English, installed on a phone whose locale is set to <code>en-UK<\/code>.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17966 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-475x1024.png\" alt=\"Screen of demo app saying &quot;Visit us at xyz.com to learn more&quot; as well as a Next and Back button | Phrase\" width=\"475\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-951x2048.png 951w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons.png 1170w\" sizes=\"(max-width: 475px) 100vw, 475px\" \/><\/p>\n<p>To enable pseudolocale testing on an Android device, go to the <span class=\"notion-enable-hover\" data-token-index=\"1\" data-reactroot=\"\"><code>build.gradle<\/code><\/span> file of your project and add the following code:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"kotlin\" data-enlighter-group=\"62eccefa-aab4-4d3c-bcc6-3b9696b7bbc1\" data-enlighter-title=\"build.gradle\">buildTypes {\r\n  debug {\r\n    pseudoLocalesEnabled true\r\n  }\r\n}<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Only enable pseudoLocalesEnabled for debug builds and not for a release build.<\/p>\n<p>Sync and run the project. Now, turn on the <a href=\"https:\/\/developer.android.com\/studio\/debug\/dev-options\">developer options<\/a> from the phone settings and restart your device or emulator. Android provides the following 2 pseudolocales to test your application.<\/p>\n<ul>\n<li>English(XA): Adds Latin accents to base English UI text and expands on original text by adding non-accented text to showcase any UI bugs that might have originated because of expanded text.<\/li>\n<li>AR(XB): Sets the direction of content from left-to-right(LTR) to right-to-left(RTL), reversing the order of characters; this is to mimic behaviour for various RTL languages like Hebrew, Urdu, etc.<\/li>\n<\/ul>\n<h3><span class=\"ez-toc-section\" id=\"spotting-localization-issues\"><\/span>Spotting localization issues<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Navigate the phone settings and select English(XA) as the default system language. On running the app, when you select English(XA) as the default locale, you will notice the following:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-17967 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized-475x1024.png\" alt=\"Demo app with accents added to all of the English text except the Back button | Phrase\" width=\"475\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized-475x1024.png 475w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized-139x300.png 139w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized-768x1654.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized-713x1536.png 713w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized-951x2048.png 951w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/demo-app-buttons-pseudolocalized.png 1170w\" sizes=\"(max-width: 475px) 100vw, 475px\" \/><\/p>\n<p>As we can see:<\/p>\n<ul>\n<li>The Text composable and the <em>Next<\/em> button now contain added accents and expanded texts, which does not break out the UI. \u2705<\/li>\n<li>The <em>Back<\/em> button is same as before instead of using accents and expanded text. This means we have hard coded the string. \u274c<\/li>\n<\/ul>\n<p>Similar to that, you can select to run the app again after selecting AR(XB) as the default locale to check if there are any UI issues when viewing RTL locales.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out our guide to <a href=\"https:\/\/phrase.com\/blog\/posts\/android-accessibility\/\">supporting RTL layouts in Android apps<\/a>.<\/p>\n<p>In conclusion, pseudolocalization has helped us fix UI issues without needing to translate our application.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"wrapping-up-our-android-localization-guide\"><\/span>Wrapping up our Android localization guide<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>And that about does it for this one. We hope you&#8217;ve enjoyed this foray into some best practices for making Android apps ready for global markets. As soon as your translators are ready to take over the process, have a look at <a href=\"https:\/\/phrase.com\/platform\/strings\/\">Phrase Strings<\/a>.<\/p>\n<p>With its CLI and Bitbucket, GitHub, and GitLab sync, your i18n can be on autopilot. The fully-featured Phrase web console, with machine learning and smart suggestions, is a joy for translators to use. Once translations are ready, they can sync back to your project automatically. You set it and forget it, leaving you to focus on the code you love.<\/p>\n<p>Check out all\u00a0<a href=\"https:\/\/phrase.com\/roles\/developers\/\">Phrase features for developers<\/a> and see for yourself how they can streamline your <a href=\"https:\/\/phrase.com\/solutions\/app-localization\/\">app localization<\/a> workflows.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Learn best practices for Android internationalization and localization to make your app accessible to users worldwide in the language they speak.<\/p>\n","protected":false},"author":47,"featured_media":37351,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":true,"_modified_date":"","_searchwp_excluded":"","footnotes":""},"categories":[40],"class_list":["post-8747","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-software-localization"],"acf":[],"_links":{"self":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/8747"}],"collection":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/users\/47"}],"replies":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/comments?post=8747"}],"version-history":[{"count":13,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/8747\/revisions"}],"predecessor-version":[{"id":94258,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/8747\/revisions\/94258"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/37351"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=8747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=8747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}