How to change the language on Android at runtime and don’t go mad

轉(zhuǎn)載: How to change the language on Android at runtime and don’t go mad

There is a library called Lingver that implements the presented approach with just a few lined of code. Check it out!

Introduction

The topic is old as the hills, but still is being actively discussed among developers due to frequent API and behavior changes. The goal of this post is to gather all tips and address all pitfalls while implementing this functionality.

Disclaimer

Changing the language on Android at runtime was never officially encouraged or documented. The resource framework automatically selects the resources that best match the device. Such behavior is enough for common applications, so just make sure you have strict reasons to change it before proceeding further.

There are a ton of articles and answers on Stack Overflow but they usually lack enough of explanation. As a result, when this functionality gets broken, developers can’t easily fix it due to the messy API and lots of deprecated things. We don’t want to fall into the same trap, right? That’s why I want to go step by step to a final solution.

Getting started

Make sure that you are already familiar with the following concepts:

Resources,Configuration, and Locale.

Technically, to get localized data one should use Resources with the desired Locale set in their Configuration. Basically, there are three kinds of resources you should be worried about:

  • resources from Activity.getResources
  • resources from Application.getResources
  • the top level resources

The top level resources are created for a specific package during an application initialization. For instance, Activity titles declared in your manifest are loaded exactly from these resources. Often, all of these resources are the same instance, but it is not always the case.

Let’s see how we can change the locale across different API levels.

Up through API level 16

Changing the language on this stage is pretty straightforward. Consider the following code snippet:

public class LocaleManager {

    public static void setLocale(Context c) {
        setNewLocale(c, getLanguage(c));
    }

    public static void setNewLocale(Context c, String language) {
        persistLanguage(c, language);
        updateResources(c, language);
    }

    public static String getLanguage(Context c) { ... }

    private static void persistLanguage(Context c, String language) { ... }

    private static void updateResources(Context context, String language) {
        Locale locale = new Locale(language);
        Locale.setDefault(locale);

        Resources res = context.getResources();
        Configuration config = new Configuration(res.getConfiguration());
        config.locale = locale;
        res.updateConfiguration(config, res.getDisplayMetrics());
    }
}

So we have a class LocaleManager that wraps a logic of changing an application locale. Let’s focus on updateResources method. What we do here is update the resources via updateConfiguration with a config that includes the desired locale.

When to update

So far so good, but when to call it exactly you may ask. This part is a little bit tricky:

  • The first place is your “Settings” screen or whatever place you use to change the language in your application. **Note **that after the locale is changed youstill have to reload already fetched strings manually. We will talk how to do it correctly at the end of this section.
  • The other places are onCreate and onConfigurationChanged of your Application. Android **resets **the locale for the top level resources back to the device default on every application restart and configuration change. So make sure you perform a new update there.

Besides, you should persist information about a selected locale in some disk storage to get it back when you need it. SharedPreferences is a good choice.

Settings screen

Going back to the case with your “Settings” screen. Let’s imagine that you spent some time playing around your app and then changed the locale in your settings screen. The current activity and the other activities in the back stack used the previous locale to show content. You have to somehow refresh them. Well, the simplest way is to clear the existing task and start a new one. This is exactly when the first pitfall comes in.

1_RwUZFzrcFmfwvI3p_o9lhA.gif

Pitfall 1. Activity titles are not translated or mixed with different languages!

After the language change, activity titles are not translated properly sometimes even after restarting of an activity.

It took me some time to find out what’s going on. During a launch of an activity, its title (declared in a manifest file) is being loaded from the top level resources and cached. That’s the reason of getting the same title for the next time and ignoring a new locale you set.

How to reproduce

Imagine that your device language is Englishand your application consists of three activities: A, B, and C. You start the activity A and then open B. Titles for both activities are being cached. In the activity B you change the language to Ukrainianand start the activity C. HA! At this point, titles for A and B are cached in English while it is in Ukrainian for C.

Note that this behavior is relevant for all API levels.

How to clear the cache

The simplest way is to restart your application process (check ProcessPhoenix) right after you update the locale. However, it might be not acceptable for some applications as it is quite a heavy task and is far away from a seamless user experience.

Note that a configuration change clears the cache as well. Another dirty hack is to use Java Reflection API. By the way, let me know if you have any better way.

As an alternative, you can set titles manually in onCreate using local activity resources and do not depend on cached entities. You might want to use a workaround in your BaseActivity. See Appendix A.

API level 17

At this point, Android introduces support for bidirectional layouts along with a minor change in the resources API.

Since then, instead of modifying the localevariable directly you should use the setLocale method which additionally sets a layout direction internally. This is how updateResources method looks like now.

 private static void updateResources(Context context, String language) {
        ...
        if (Build.VERSION.SDK_INT >= 17) {
            config.setLocale(locale);
        } else {
            config.locale = locale;
        }
        res.updateConfiguration(config, res.getDisplayMetrics());
    }

API level 25

At this point, updateConfiguration for Resources gets deprecated in favor of createConfigurationContext(which was added in API 17).

So what do we change now? Basically, instead of updating the existing resources you need to create a new Context with properly configured Resources and put it as a base one for Application and Activity via attachBaseContext. As a result, all invocations of getResources will be delegated to the new resources instead of the top level instance.

public class LocaleManager {
    ...
    private static Context updateResources(Context context, String language) {
        Locale locale = new Locale(language);
        Locale.setDefault(locale);

        Resources res = context.getResources();
        Configuration config = new Configuration(res.getConfiguration());
        if (Build.VERSION.SDK_INT >= 17) {
            config.setLocale(locale);
            context = context.createConfigurationContext(config);
        } else {
            config.locale = locale;
            res.updateConfiguration(config, res.getDisplayMetrics());
        }
        return context;
    }
}
public class App extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(LocaleHelper.setLocale(base));
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        LocaleManager.setLocale(this);
    }
    ...
}

public abstract class BaseActivity extends AppCompatActivity {
    
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(LocaleHelper.setLocale(base));
    }
    ...
}

Well, to sum up, we use:

  • updateConfiguration for API < 17
  • createConfigurationContext for API ≥17

I was confused when the lovely activity titles were not translated again for API ≥17. What’s wrong this time?

1_jEVe2XIKXEa1d3dCENG1lA.jpeg

Pitfall 2. Activity titles are not translated using createConfigurationContext!

Let’s examine what we do step by step to find an issue:

  • We create a special Contextwhich owns a new localized Resources instance.
  • We put this Contextas a base one for an application and an activity via attachBaseContext.

Ah! Do you remember the top level resources we talked about previously? It seems that there is no way to update them with the help of createConfigurationContext. Consequently, the application uses the default locale to get titles.

Let’s see what options do we have to fix this behavior:

  • use updateConfigurationfor all API levelsto update the top level resources ignoring the deprecation
  • use updateConfigurationfor API <17 andcreateConfigurationContext for API ≥17 to respect the deprecation. As a side effect, you have to set activity titles in onCreate manually using local Resources (seeAppendix A)

Note that you have to invoke attachBaseContext in the other components like Service to update the resources for them as well. Another pitfall of using createConfigurationContext is that you can’t actually update the resources for Application after you change the language at runtime since attachBaseContext is never called again. Therefore, you have to restart the application to update the resources.

Okay, let’s check API level 26 section to make a final decision.

Note that applyOverrideConfiguration may be used asan alternative to attachBaseContext. It does the pretty similar thing but exists only for Activity.

API level 26

Up to API level 25 your application and activities share the same resources (aka the top level resources) by default. It means that a call of updateConfigurationfrom anyContext will update the resources. However, starting from API 26, resources for an application and an activity are separate entities, so you need to update them separately respectively (for instance, in onCreate of your Application and BaseActivity).

Conclusions

Let’s sum up and see what options we finally have:

  1. Use updateConfiguration for all API levels in onCreate of your Application andBaseActivity to update the resources ignoring the deprecation. Remember to deal with the cache issue in this case.
  2. Use updateConfigurationfor API <17 andcreateConfigurationContext for API ≥17 to respect the deprecation. Additionally, you have to set Activity titles manually using local resources (checkAppendix A).

What do you choose? To be a good citizen or prefer a simple solution despite the deprecation?

UPD:
If you want to play with the sample app, use a device below API 28. Starting from Android Pie, any usage of non-SDK interfaces is restricted, that’s why accessing the title cache for educational reasons is not possible anymore. https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces

Don’t worry, it doesn’t break anything regarding the approach itself.

UPD #2:

There is an issue while using appcompat 1.1.0 which will be fixed in the upcoming releases of the appcompat library. Please, refer to the following issue.

UPD #3

There is a library called Lingver that implements the presented approach with just a few lined of code. Check it out!

Please check out a library or a sampleproject on Github.


Appendix A

This is a possible workaround to set activity titles using local Resources instance*. *It intends to break the dependency on the cache and the top level resources.

public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        resetTitle();
    }

    private void resetTitle() {
        try {
            int label = getPackageManager().getActivityInfo(getComponentName(), GET_META_DATA).labelRes;
            if (label != 0) {
                setTitle(label);
            }
        } catch (NameNotFoundException e) { ... }
    }
    ...
}

Additional information

Locale.setDefault / Locale.getDefault

Gets the current value of the default locale for this instance of the Java Virtual Machine. It is used by many locale-sensitive methods if no locale is explicitly specified.

The default locale is used for locale-sensitive operations like formatting/parsing numbers or dates. Usually, it is important to keep it the same as a locale you use for showing content in your application.

LocaleList API

Starting in Android 7.0 (API level 24), Android provides enhanced support for multilingual users, allowing them to select multiple locales in settings.

  • LocaleList API is introduced along with setLocales/getLocales in Configuration.
  • accessing locale variable gets deprecated in favor of getLocales().get(0).

This new API allows developers to create more sophisticated app behavior. For instance, browser apps can avoid offering to translate pages in a language the user already knows.

However, if your goal is to lock the only one specific language, you can ignore this update.

Note that setLocale starts invoking setLocales with a list of just one locale under the hood since API 24.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末澎怒,一起剝皮案震驚了整個濱河市芜茵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驱证,老刑警劉巖蕾各,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彤灶,死亡現(xiàn)場離奇詭異询张,居然都是意外死亡劈榨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門晾咪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來收擦,“玉大人,你說我怎么就攤上這事谍倦∪福” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵昼蛀,是天一觀的道長宴猾。 經(jīng)常有香客問我,道長叼旋,這世上最難降的妖魔是什么仇哆? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮夫植,結(jié)果婚禮上讹剔,老公的妹妹穿的比我還像新娘。我一直安慰自己详民,他們只是感情好延欠,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沈跨,像睡著了一般由捎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上饿凛,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天狞玛,我揣著相機(jī)與錄音软驰,去河邊找鬼。 笑死心肪,一個胖子當(dāng)著我的面吹牛锭亏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蒙畴,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼贰镣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了膳凝?” 一聲冷哼從身側(cè)響起碑隆,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蹬音,沒想到半個月后上煤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡著淆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年劫狠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片永部。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡独泞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出苔埋,到底是詐尸還是另有隱情懦砂,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布组橄,位于F島的核電站荞膘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏玉工。R本人自食惡果不足惜羽资,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望遵班。 院中可真熱鬧屠升,春花似錦、人聲如沸狭郑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愿阐。三九已至微服,卻和暖如春趾疚,著一層夾襖步出監(jiān)牢的瞬間缨历,已是汗流浹背以蕴。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辛孵,地道東北人丛肮。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像魄缚,于是被迫代替她去往敵國和親宝与。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345